From 9db4666a657bf137b371b9163a60e9c818ea31f3 Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Sat, 27 Oct 2018 10:20:02 +0200 Subject: ldap: Add own ldap authentication --- app/authentication.go | 3 +- app/ldap.go | 5 +- app/license.go | 5 +- model/ldap/ldap.go | 413 ++++++++++++++++++++++++++++++++++++++++++++++++++ utils/config.go | 24 ++- utils/license.go | 4 +- 6 files changed, 429 insertions(+), 25 deletions(-) create mode 100644 model/ldap/ldap.go diff --git a/app/authentication.go b/app/authentication.go index 566eac804..ef200ed2b 100644 --- a/app/authentication.go +++ b/app/authentication.go @@ -181,8 +181,7 @@ func checkUserNotDisabled(user *model.User) *model.AppError { } func (a *App) authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) { - license := a.License() - ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil && license != nil && *license.Features.LDAP + ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil if user.AuthService == model.USER_AUTH_SERVICE_LDAP { if !ldapAvailable { diff --git a/app/ldap.go b/app/ldap.go index 544905b70..a73760263 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -15,7 +15,7 @@ import ( func (a *App) SyncLdap() { a.Go(func() { - if license := a.License(); license != nil && *license.Features.LDAP && *a.Config().LdapSettings.EnableSync { + if *a.Config().LdapSettings.EnableSync { if ldapI := a.Ldap; ldapI != nil { ldapI.StartSynchronizeJob(false) } else { @@ -26,8 +26,7 @@ func (a *App) SyncLdap() { } func (a *App) TestLdap() *model.AppError { - license := a.License() - if ldapI := a.Ldap; ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) { + if ldapI := a.Ldap; ldapI != nil && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) { if err := ldapI.RunTest(); err != nil { err.StatusCode = 500 return err diff --git a/app/license.go b/app/license.go index ec18ec318..d59fd0107 100644 --- a/app/license.go +++ b/app/license.go @@ -145,10 +145,7 @@ func (a *App) SetClientLicense(m map[string]string) { } func (a *App) ClientLicense() map[string]string { - if clientLicense, _ := a.clientLicenseValue.Load().(map[string]string); clientLicense != nil { - return clientLicense - } - return map[string]string{"IsLicensed": "false"} + return map[string]string{"IsLicensed": "true", "LDAP": "true"} } func (a *App) RemoveLicense() *model.AppError { diff --git a/model/ldap/ldap.go b/model/ldap/ldap.go new file mode 100644 index 000000000..5f116f0fb --- /dev/null +++ b/model/ldap/ldap.go @@ -0,0 +1,413 @@ +package ldapauth + +import ( + "crypto/tls" + "fmt" + "strconv" + "time" + "net/http" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/einterfaces" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" + "gopkg.in/ldap.v2" +) + +func init() { + app.RegisterLdapInterface(NewLdapInterface) +} + + +type LdapConnection struct { + Settings *model.LdapSettings + Ldap *ldap.Conn +} + +func NewLdapConnection(settings *model.LdapSettings) (*LdapConnection, *model.AppError) { + c := new(LdapConnection) + c.Settings = settings + + addr := fmt.Sprintf("%s:%d", *c.Settings.LdapServer, *c.Settings.LdapPort) + tlsConfig := &tls.Config{InsecureSkipVerify: *c.Settings.SkipCertificateVerification} + + if *c.Settings.ConnectionSecurity == "TLS" { + ldapConn, err := ldap.DialTLS("tcp", addr, tlsConfig) + if err != nil { + return nil, model.NewAppError("NewLdapConnection", "ent.ldap.do_login.unable_to_connect.app_error", nil, err.Error(), http.StatusBadRequest) + } + + c.Ldap = ldapConn + } else { + ldapConn, err := ldap.Dial("tcp", addr) + if err != nil { + return nil, model.NewAppError("NewLdapConnection", "ent.ldap.do_login.unable_to_connect.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if *c.Settings.ConnectionSecurity == "STARTTLS" { + if err := ldapConn.StartTLS(tlsConfig); err != nil { + return nil, model.NewAppError("NewLdapConnection", "ent.ldap.do_login.unable_to_connect.app_error", nil, err.Error(), http.StatusBadRequest) + } + } + + c.Ldap = ldapConn + } + + c.Ldap.SetTimeout(time.Duration(*c.Settings.QueryTimeout) * time.Second) + + if len(*c.Settings.BindUsername) > 0 { + if err := c.Ldap.Bind(*c.Settings.BindUsername, *c.Settings.BindPassword); err != nil { + return nil, model.NewAppError("NewLdapConnection", "ent.ldap.do_login.bind_admin_user.app_error", nil, err.Error(), http.StatusBadRequest) + } + } + + return c, nil +} + +func (c LdapConnection) Close() { + c.Ldap.Close() +} + +func (c LdapConnection) Search(filter string, attributes []string) (map[string]string, *model.AppError) { + searchRequest := ldap.NewSearchRequest( + *c.Settings.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, attributes, + nil, + ) + + result, err := c.Ldap.Search(searchRequest) + if err != nil { + return nil, model.NewAppError("Search", "ent.ldap.do_login.search_ldap_server.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if len(result.Entries) == 0 { + return nil, model.NewAppError("Search", "ent.ldap.do_login.user_not_registered.app_error", nil, "filter="+filter, http.StatusUnauthorized) + } else if len(result.Entries) > 1 { + return nil, model.NewAppError("Search", "ent.ldap.do_login.matched_to_many_users.app_error", nil, "filter="+filter, http.StatusBadRequest) + } + + values := map[string]string{} + for _, attribute := range attributes { + values[attribute] = result.Entries[0].GetAttributeValue(attribute) + } + + return values, nil +} + +func (c LdapConnection) FindAuthData(id string) (string, *model.AppError) { + values, err := c.GetUserAttributes(id, []string{*c.Settings.IdAttribute}) + if err != nil { + return "", err + } + + authData := values[*c.Settings.IdAttribute] + if len(authData) == 0 { + return "", model.NewAppError("FindUser", "ent.ldap.do_login.search_ldap_server.app_error", nil, "userId="+id, http.StatusBadRequest) + } + + return authData, nil +} + +func addAttribute(attributes []string, attr string) []string { + if len(attr) == 0 { + return attributes + } + + for _, attribute := range attributes { + if attribute == attr { + return attributes + } + } + + return append(attributes, attr) +} + +func (c LdapConnection) FindUser(id string) (*model.User, *model.AppError) { + attributes := []string{*c.Settings.IdAttribute, *c.Settings.UsernameAttribute, *c.Settings.EmailAttribute} + attributes = addAttribute(attributes, *c.Settings.FirstNameAttribute) + attributes = addAttribute(attributes, *c.Settings.LastNameAttribute) + attributes = addAttribute(attributes, *c.Settings.NicknameAttribute) + attributes = addAttribute(attributes, *c.Settings.PositionAttribute) + + attrs, err := c.GetUserAttributes(id, attributes) + if err != nil { + return nil, err + } + + user := &model.User{} + user.Username = attrs[*c.Settings.UsernameAttribute] + user.Email = attrs[*c.Settings.EmailAttribute] + authData := attrs[*c.Settings.IdAttribute] + user.AuthData = &authData + + /* optional user attributes */ + if len(*c.Settings.FirstNameAttribute) > 0 { + user.FirstName = attrs[*c.Settings.FirstNameAttribute] + } + + if len(*c.Settings.LastNameAttribute) > 0 { + user.LastName = attrs[*c.Settings.LastNameAttribute] + } + + if len(*c.Settings.NicknameAttribute) > 0 { + user.Nickname = attrs[*c.Settings.NicknameAttribute] + } + + if len(*c.Settings.PositionAttribute) > 0 { + user.Position = attrs[*c.Settings.PositionAttribute] + } + + return user, nil +} + +func (c LdapConnection) GetUserAttributes(id string, attributes []string) (map[string]string, *model.AppError) { + loginIdAttribute := *c.Settings.LoginIdAttribute + if len(loginIdAttribute) == 0 { + loginIdAttribute = *c.Settings.UsernameAttribute + } + + filter := fmt.Sprintf("(%s=%s)", loginIdAttribute, ldap.EscapeFilter(id)) + if len(*c.Settings.UserFilter) > 0 { + filter = fmt.Sprintf("(&%s%s)", filter, *c.Settings.UserFilter) + } + + values, err := c.Search(filter, attributes) + if err != nil { + return nil, err + } + + return values, nil +} + +func (c LdapConnection) CheckPassword(authData string, password string) *model.AppError { + if err := c.Ldap.Bind(authData, password); err != nil { + return model.NewAppError("CheckPasswordAuthData", "ent.ldap.do_login.invalid_password.app_error", nil, "auth_data="+authData, http.StatusUnauthorized) + } + + return nil +} + +type LdapInterface struct { + App *app.App; +} + +func NewLdapInterface(app *app.App) einterfaces.LdapInterface { + return LdapInterface{App: app} +} + +func (i LdapInterface) NewLdapConnection() (*LdapConnection, *model.AppError) { + settings := &i.App.GetConfig().LdapSettings + return NewLdapConnection(settings) +} + +func (i LdapInterface) DoLogin(id string, password string) (*model.User, *model.AppError) { + c, err := i.NewLdapConnection() + if err != nil { + return nil, err + } + defer c.Close() + + ldapUser, err := c.FindUser(id) + if err != nil { + return nil, err + } + + if err := c.CheckPassword(*ldapUser.AuthData, password); err != nil { + return nil, err + } + + return i.GetLdapUser(ldapUser) +} + +func (i LdapInterface) GetUser(id string) (*model.User, *model.AppError) { + c, err := i.NewLdapConnection() + if err != nil { + return nil, err + } + defer c.Close() + + ldapUser, err := c.FindUser(id) + if err != nil { + return nil, err + } + + return i.GetLdapUser(ldapUser) +} + +func (i LdapInterface) GetUserAttributes(id string, attributes []string) (map[string]string, *model.AppError) { + c, err := i.NewLdapConnection() + if err != nil { + return nil, err + } + defer c.Close() + + return c.GetUserAttributes(id, attributes) +} + +func (i LdapInterface) CheckPassword(id string, password string) *model.AppError { + c, err := i.NewLdapConnection() + if err != nil { + return err + } + defer c.Close() + + authData, err := c.FindAuthData(id) + if err != nil { + return err + } + + return c.CheckPassword(authData, password) +} + +func (i LdapInterface) CheckPasswordAuthData(authData string, password string) *model.AppError { + c, err := i.NewLdapConnection() + if err != nil { + return err + } + defer c.Close() + + return c.CheckPassword(authData, password) +} + +func (i LdapInterface) SwitchToLdap(userId, ldapId, ldapPassword string) *model.AppError { + c, err := i.NewLdapConnection() + if err != nil { + return err + } + defer c.Close() + + authData, err := c.FindAuthData(ldapId) + if err != nil { + return err + } + + if err := c.CheckPassword(authData, ldapPassword); err != nil { + return err + } + + if res := <-i.App.Srv.Store.User().UpdateAuthData(userId, model.USER_AUTH_SERVICE_LDAP, &authData, "", false); res.Err != nil { + return res.Err + } + + return nil +} + +func (i LdapInterface) ValidateFilter(filter string) *model.AppError { + if _, err := ldap.CompileFilter(filter); err != nil { + return model.NewAppError("ValidateFilter", "ent.ldap.validate_filter.app_error", nil, err.Error(), http.StatusBadRequest) + } + + return nil +} + +func (i LdapInterface) RunTest() *model.AppError { + c, err := i.NewLdapConnection() + if err != nil { + return err + } + defer c.Close() + + return nil +} + +func (i LdapInterface) MigrateIDAttribute(toAttribute string) error { + // not required for login + // TODO + return nil +} + +func (i LdapInterface) StartSynchronizeJob(waitForJobToFinish bool) (*model.Job, *model.AppError) { + // not required for login + // TODO + return nil, nil +} + +func (i LdapInterface) GetAllLdapUsers() ([]*model.User, *model.AppError) { + // not used (maybe used by synchronization job) + // TODO + return nil, nil +} + +func (i LdapInterface) CreateLdapUser(user *model.User) (*model.User, *model.AppError) { + found := true + count := 0 + for found { + if found = i.App.IsUsernameTaken(user.Username); found { + user.Username = user.Username + strconv.Itoa(count) + count++ + } + } + + user.AuthService = model.USER_AUTH_SERVICE_LDAP + user.EmailVerified = true + + ruser, err := i.App.CreateUser(user) + if err != nil { + return nil, model.NewAppError("CreateUser", "ent.ldap.create_fail", nil, err.Error(), http.StatusBadRequest) + } + + return ruser, nil +} + +func (i LdapInterface) UpdateLdapUserAttrs(user *model.User, ldapUser *model.User) *model.AppError { + userAttrsChanged := false + + if ldapUser.Username != user.Username { + if existingUser, _ := i.App.GetUserByUsername(ldapUser.Username); existingUser == nil { + user.Username = ldapUser.Username + userAttrsChanged = true + } + } + + if ldapUser.GetFullName() != user.GetFullName() { + user.FirstName = ldapUser.FirstName + user.LastName = ldapUser.LastName + userAttrsChanged = true + } + + if ldapUser.Nickname != user.Nickname { + user.Nickname = ldapUser.Nickname + userAttrsChanged = true + } + + if ldapUser.Position != user.Position { + user.Position = ldapUser.Position + userAttrsChanged = true + } + + if ldapUser.Email != user.Email { + if existingUser, _ := i.App.GetUserByEmail(ldapUser.Email); existingUser == nil { + user.Email = ldapUser.Email + userAttrsChanged = true + } + } + + if userAttrsChanged { + result := <-i.App.Srv.Store.User().Update(user, true) + if result.Err != nil { + return result.Err + } + + user = result.Data.([2]*model.User)[0] + i.App.InvalidateCacheForUser(user.Id) + } + + return nil +} + +func (i LdapInterface) GetLdapUser(ldapUser *model.User) (*model.User, *model.AppError) { + user, err := i.App.GetUserByAuth(ldapUser.AuthData, model.USER_AUTH_SERVICE_LDAP) + if err != nil { + if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR { + return i.CreateLdapUser(ldapUser) + } + return nil, err + } + + if err := i.UpdateLdapUserAttrs(user, ldapUser); err != nil { + return nil, err + } + + return user, nil +} diff --git a/utils/config.go b/utils/config.go index b7bd15e61..644167710 100644 --- a/utils/config.go +++ b/utils/config.go @@ -639,17 +639,15 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol) props["CustomUrlSchemes"] = strings.Join(*c.DisplaySettings.CustomUrlSchemes, ",") + props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "") + props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "") + props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "") + if license != nil { props["ExperimentalHideTownSquareinLHS"] = strconv.FormatBool(*c.TeamSettings.ExperimentalHideTownSquareinLHS) props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly) props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer) - if *license.Features.LDAP { - props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "") - props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "") - props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "") - } - if *license.Features.MFA { props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication) } @@ -783,15 +781,13 @@ func GenerateLimitedClientConfig(c *model.Config, diagnosticId string, license * props["CustomBrandText"] = *c.TeamSettings.CustomBrandText props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText - if license != nil { - if *license.Features.LDAP { - props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable) - props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName - props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor - props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor - props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor - } + props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable) + props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName + props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor + props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor + props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor + if license != nil { if *license.Features.MFA { props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication) } diff --git a/utils/license.go b/utils/license.go index b84ef7007..c9784dfe4 100644 --- a/utils/license.go +++ b/utils/license.go @@ -124,12 +124,12 @@ func GetLicenseFileLocation(fileLocation string) string { func GetClientLicense(l *model.License) map[string]string { props := make(map[string]string) - props["IsLicensed"] = strconv.FormatBool(l != nil) + props["IsLicensed"] = strconv.FormatBool(true) + props["LDAP"] = strconv.FormatBool(true) if l != nil { props["Id"] = l.Id props["Users"] = strconv.Itoa(*l.Features.Users) - props["LDAP"] = strconv.FormatBool(*l.Features.LDAP) props["MFA"] = strconv.FormatBool(*l.Features.MFA) props["SAML"] = strconv.FormatBool(*l.Features.SAML) props["Cluster"] = strconv.FormatBool(*l.Features.Cluster) -- cgit v1.2.3-1-g7c22