The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/services/ldap/ldap.go

560 lines
13 KiB

package ldap
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/davecgh/go-spew/spew"
LDAP "gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
models "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
// IConnection is interface for LDAP connection manipulation
type IConnection interface {
Bind(username, password string) error
UnauthenticatedBind(username string) error
Search(*LDAP.SearchRequest) (*LDAP.SearchResult, error)
StartTLS(*tls.Config) error
Close()
}
// IAuth is interface for LDAP authorization
type IAuth interface {
Login(query *models.LoginUserQuery) error
SyncUser(query *models.LoginUserQuery) error
GetGrafanaUserFor(
ctx *models.ReqContext,
user *UserInfo,
) (*models.User, error)
Users() ([]*UserInfo, error)
}
// Auth is basic struct of LDAP authorization
type Auth struct {
server *ServerConfig
conn IConnection
requireSecondBind bool
log log.Logger
}
var (
// ErrInvalidCredentials is returned if username and password do not match
ErrInvalidCredentials = errors.New("Invalid Username or Password")
)
var dial = func(network, addr string) (IConnection, error) {
return LDAP.Dial(network, addr)
}
// New creates the new LDAP auth
func New(server *ServerConfig) IAuth {
return &Auth{
server: server,
log: log.New("ldap"),
}
}
// Dial dials in the LDAP
func (auth *Auth) Dial() error {
if hookDial != nil {
return hookDial(auth)
}
var err error
var certPool *x509.CertPool
if auth.server.RootCACert != "" {
certPool = x509.NewCertPool()
for _, caCertFile := range strings.Split(auth.server.RootCACert, " ") {
pem, err := ioutil.ReadFile(caCertFile)
if err != nil {
return err
}
if !certPool.AppendCertsFromPEM(pem) {
return errors.New("Failed to append CA certificate " + caCertFile)
}
}
}
var clientCert tls.Certificate
if auth.server.ClientCert != "" && auth.server.ClientKey != "" {
clientCert, err = tls.LoadX509KeyPair(auth.server.ClientCert, auth.server.ClientKey)
if err != nil {
return err
}
}
for _, host := range strings.Split(auth.server.Host, " ") {
address := fmt.Sprintf("%s:%d", host, auth.server.Port)
if auth.server.UseSSL {
tlsCfg := &tls.Config{
InsecureSkipVerify: auth.server.SkipVerifySSL,
ServerName: host,
RootCAs: certPool,
}
if len(clientCert.Certificate) > 0 {
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
}
if auth.server.StartTLS {
auth.conn, err = dial("tcp", address)
if err == nil {
if err = auth.conn.StartTLS(tlsCfg); err == nil {
return nil
}
}
} else {
auth.conn, err = LDAP.DialTLS("tcp", address, tlsCfg)
}
} else {
auth.conn, err = dial("tcp", address)
}
if err == nil {
return nil
}
}
return err
}
// Login logs in the user
func (auth *Auth) Login(query *models.LoginUserQuery) error {
// connect to ldap server
if err := auth.Dial(); err != nil {
return err
}
defer auth.conn.Close()
// perform initial authentication
if err := auth.initialBind(query.Username, query.Password); err != nil {
return err
}
// find user entry & attributes
user, err := auth.searchForUser(query.Username)
if err != nil {
return err
}
auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
// check if a second user bind is needed
if auth.requireSecondBind {
err = auth.secondBind(user, query.Password)
if err != nil {
return err
}
}
grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
// SyncUser syncs user with Grafana
func (auth *Auth) SyncUser(query *models.LoginUserQuery) error {
// connect to ldap server
err := auth.Dial()
if err != nil {
return err
}
defer auth.conn.Close()
err = auth.serverBind()
if err != nil {
return err
}
// find user entry & attributes
user, err := auth.searchForUser(query.Username)
if err != nil {
auth.log.Error("Failed searching for user in ldap", "error", err)
return err
}
auth.log.Debug("Ldap User found", "info", spew.Sdump(user))
grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user)
if err != nil {
return err
}
query.User = grafanaUser
return nil
}
func (auth *Auth) GetGrafanaUserFor(
ctx *models.ReqContext,
user *UserInfo,
) (*models.User, error) {
extUser := &models.ExternalUserInfo{
AuthModule: "ldap",
AuthId: user.DN,
Name: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
Login: user.Username,
Email: user.Email,
Groups: user.MemberOf,
OrgRoles: map[int64]models.RoleType{},
}
for _, group := range auth.server.Groups {
// only use the first match for each org
if extUser.OrgRoles[group.OrgId] != "" {
continue
}
if user.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
}
}
}
// validate that the user has access
// if there are no ldap group mappings access is true
// otherwise a single group must match
if len(auth.server.Groups) > 0 && len(extUser.OrgRoles) < 1 {
auth.log.Info(
"Ldap Auth: user does not belong in any of the specified ldap groups",
"username", user.Username,
"groups", user.MemberOf,
)
return nil, ErrInvalidCredentials
}
// add/update user in grafana
upsertUserCmd := &models.UpsertUserCommand{
ReqContext: ctx,
ExternalUser: extUser,
SignupAllowed: setting.LdapAllowSignup,
}
err := bus.Dispatch(upsertUserCmd)
if err != nil {
return nil, err
}
return upsertUserCmd.Result, nil
}
func (auth *Auth) serverBind() error {
bindFn := func() error {
return auth.conn.Bind(auth.server.BindDN, auth.server.BindPassword)
}
if auth.server.BindPassword == "" {
bindFn = func() error {
return auth.conn.UnauthenticatedBind(auth.server.BindDN)
}
}
// bind_dn and bind_password to bind
if err := bindFn(); err != nil {
auth.log.Info("LDAP initial bind failed, %v", err)
if ldapErr, ok := err.(*LDAP.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
}
return err
}
return nil
}
func (auth *Auth) secondBind(user *UserInfo, userPassword string) error {
if err := auth.conn.Bind(user.DN, userPassword); err != nil {
auth.log.Info("Second bind failed", "error", err)
if ldapErr, ok := err.(*LDAP.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
}
return err
}
return nil
}
func (auth *Auth) initialBind(username, userPassword string) error {
if auth.server.BindPassword != "" || auth.server.BindDN == "" {
userPassword = auth.server.BindPassword
auth.requireSecondBind = true
}
bindPath := auth.server.BindDN
if strings.Contains(bindPath, "%s") {
bindPath = fmt.Sprintf(auth.server.BindDN, username)
}
bindFn := func() error {
return auth.conn.Bind(bindPath, userPassword)
}
if userPassword == "" {
bindFn = func() error {
return auth.conn.UnauthenticatedBind(bindPath)
}
}
if err := bindFn(); err != nil {
auth.log.Info("Initial bind failed", "error", err)
if ldapErr, ok := err.(*LDAP.Error); ok {
if ldapErr.ResultCode == 49 {
return ErrInvalidCredentials
}
}
return err
}
return nil
}
func (auth *Auth) searchForUser(username string) (*UserInfo, error) {
var searchResult *LDAP.SearchResult
var err error
for _, searchBase := range auth.server.SearchBaseDNs {
attributes := make([]string, 0)
inputs := auth.server.Attr
attributes = appendIfNotEmpty(attributes,
inputs.Username,
inputs.Surname,
inputs.Email,
inputs.Name,
inputs.MemberOf)
searchReq := LDAP.SearchRequest{
BaseDN: searchBase,
Scope: LDAP.ScopeWholeSubtree,
DerefAliases: LDAP.NeverDerefAliases,
Attributes: attributes,
Filter: strings.Replace(
auth.server.SearchFilter,
"%s", LDAP.EscapeFilter(username),
-1,
),
}
auth.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
searchResult, err = auth.conn.Search(&searchReq)
if err != nil {
return nil, err
}
if len(searchResult.Entries) > 0 {
break
}
}
if len(searchResult.Entries) == 0 {
return nil, ErrInvalidCredentials
}
if len(searchResult.Entries) > 1 {
return nil, errors.New("Ldap search matched more than one entry, please review your filter setting")
}
var memberOf []string
if auth.server.GroupSearchFilter == "" {
memberOf = getLdapAttrArray(auth.server.Attr.MemberOf, searchResult)
} else {
// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
var groupSearchResult *LDAP.SearchResult
for _, groupSearchBase := range auth.server.GroupSearchBaseDNs {
var filter_replace string
if auth.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(auth.server.Attr.Username, searchResult)
} else {
filter_replace = getLdapAttr(auth.server.GroupSearchFilterUserAttribute, searchResult)
}
filter := strings.Replace(
auth.server.GroupSearchFilter, "%s",
LDAP.EscapeFilter(filter_replace),
-1,
)
auth.log.Info("Searching for user's groups", "filter", filter)
// support old way of reading settings
groupIdAttribute := auth.server.Attr.MemberOf
// but prefer dn attribute if default settings are used
if groupIdAttribute == "" || groupIdAttribute == "memberOf" {
groupIdAttribute = "dn"
}
groupSearchReq := LDAP.SearchRequest{
BaseDN: groupSearchBase,
Scope: LDAP.ScopeWholeSubtree,
DerefAliases: LDAP.NeverDerefAliases,
Attributes: []string{groupIdAttribute},
Filter: filter,
}
groupSearchResult, err = auth.conn.Search(&groupSearchReq)
if err != nil {
return nil, err
}
if len(groupSearchResult.Entries) > 0 {
for i := range groupSearchResult.Entries {
memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i))
}
break
}
}
}
return &UserInfo{
DN: searchResult.Entries[0].DN,
LastName: getLdapAttr(auth.server.Attr.Surname, searchResult),
FirstName: getLdapAttr(auth.server.Attr.Name, searchResult),
Username: getLdapAttr(auth.server.Attr.Username, searchResult),
Email: getLdapAttr(auth.server.Attr.Email, searchResult),
MemberOf: memberOf,
}, nil
}
func (ldap *Auth) Users() ([]*UserInfo, error) {
var result *LDAP.SearchResult
var err error
server := ldap.server
if err := ldap.Dial(); err != nil {
return nil, err
}
defer ldap.conn.Close()
for _, base := range server.SearchBaseDNs {
attributes := make([]string, 0)
inputs := server.Attr
attributes = appendIfNotEmpty(
attributes,
inputs.Username,
inputs.Surname,
inputs.Email,
inputs.Name,
inputs.MemberOf,
)
req := LDAP.SearchRequest{
BaseDN: base,
Scope: LDAP.ScopeWholeSubtree,
DerefAliases: LDAP.NeverDerefAliases,
Attributes: attributes,
// Doing a star here to get all the users in one go
Filter: strings.Replace(server.SearchFilter, "%s", "*", -1),
}
result, err = ldap.conn.Search(&req)
if err != nil {
return nil, err
}
if len(result.Entries) > 0 {
break
}
}
return ldap.serializeUsers(result), nil
}
func (ldap *Auth) serializeUsers(users *LDAP.SearchResult) []*UserInfo {
var serialized []*UserInfo
for index := range users.Entries {
serialize := &UserInfo{
DN: getLdapAttrN(
"dn",
users,
index,
),
LastName: getLdapAttrN(
ldap.server.Attr.Surname,
users,
index,
),
FirstName: getLdapAttrN(
ldap.server.Attr.Name,
users,
index,
),
Username: getLdapAttrN(
ldap.server.Attr.Username,
users,
index,
),
Email: getLdapAttrN(
ldap.server.Attr.Email,
users,
index,
),
MemberOf: getLdapAttrArrayN(
ldap.server.Attr.MemberOf,
users,
index,
),
}
serialized = append(serialized, serialize)
}
return serialized
}
func appendIfNotEmpty(slice []string, values ...string) []string {
for _, v := range values {
if v != "" {
slice = append(slice, v)
}
}
return slice
}
func getLdapAttr(name string, result *LDAP.SearchResult) string {
return getLdapAttrN(name, result, 0)
}
func getLdapAttrN(name string, result *LDAP.SearchResult, n int) string {
if strings.ToLower(name) == "dn" {
return result.Entries[n].DN
}
for _, attr := range result.Entries[n].Attributes {
if attr.Name == name {
if len(attr.Values) > 0 {
return attr.Values[0]
}
}
}
return ""
}
func getLdapAttrArray(name string, result *LDAP.SearchResult) []string {
return getLdapAttrArrayN(name, result, 0)
}
func getLdapAttrArrayN(name string, result *LDAP.SearchResult, n int) []string {
for _, attr := range result.Entries[n].Attributes {
if attr.Name == name {
return attr.Values
}
}
return []string{}
}