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 --- model/ldap/ldap.go | 413 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 model/ldap/ldap.go (limited to 'model/ldap/ldap.go') 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 +} -- cgit v1.2.3-1-g7c22