diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index 5ab64ec7d53c5..c0e707af99ff8 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -53,17 +53,41 @@ var ( Name: "user-search-base", Usage: "The LDAP base at which user accounts will be searched for.", }, + cli.StringFlag{ + Name: "group-search-base", + Usage: "The LDAP base at which groups will be searched for.", + }, + cli.StringFlag{ + Name: "group-search-filter", + Usage: "An LDAP filter declaring how to find the groups a user is member of.", + }, + cli.StringFlag{ + Name: "user-attribute-in-group", + Usage: "The LDAP attribute of a user referenced by groups.", + }, + cli.StringFlag{ + Name: "member-group-filter", + Usage: "An LDAP filter specifying if a group should be allowed to login. If both user filter and member group filter are set, the user must satisfy both conditions to log in.", + }, + cli.StringFlag{ + Name: "admin-group-filter", + Usage: "An LDAP filter specifying an admin user group. If both admin filter and admin group filter are set, the user must satisfy both conditions to get admin privileges.", + }, + cli.StringFlag{ + Name: "restricted-group-filter", + Usage: "An LDAP filter specifying a restricted user group. If both restricted filter and restricted group filter are set, the user must satisfy both conditions to be a restricted user.", + }, cli.StringFlag{ Name: "user-filter", - Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.", + Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate. If both user filter and member group filter are set, the user must satisfy both conditions to log in.", }, cli.StringFlag{ Name: "admin-filter", - Usage: "An LDAP filter specifying if a user should be given administrator privileges.", + Usage: "An LDAP filter specifying if a user should be given administrator privileges. If both admin filter and admin group filter are set, the user must satisfy both conditions to get admin privileges.", }, cli.StringFlag{ Name: "restricted-filter", - Usage: "An LDAP filter specifying if a user should be given restricted status.", + Usage: "An LDAP filter specifying if a user should be given restricted status. Use an asterisk ('*') to set all users that do not match Admin Filter as restricted. If both restricted filter and restricted group filter are set, the user must satisfy both conditions to be a restricted user.", }, cli.BoolFlag{ Name: "allow-deactivate-all", @@ -212,6 +236,24 @@ func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error { if c.IsSet("user-search-base") { config.Source.UserBase = c.String("user-search-base") } + if c.IsSet("group-search-base") { + config.Source.GroupSearchBase = c.String("group-search-base") + } + if c.IsSet("group-search-filter") { + config.Source.GroupSearchFilter = c.String("group-search-filter") + } + if c.IsSet("user-attribute-in-group") { + config.Source.UserAttributeInGroup = c.String("user-attribute-in-group") + } + if c.IsSet("member-group-filter") { + config.Source.MemberGroupFilter = c.String("member-group-filter") + } + if c.IsSet("admin-group-filter") { + config.Source.AdminGroupFilter = c.String("admin-group-filter") + } + if c.IsSet("restricted-group-filter") { + config.Source.RestrictedGroupFilter = c.String("restricted-group-filter") + } if c.IsSet("username-attribute") { config.Source.AttributeUsername = c.String("username-attribute") } diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go index 87f4f789ab0ab..67336febc0f29 100644 --- a/cmd/admin_auth_ldap_test.go +++ b/cmd/admin_auth_ldap_test.go @@ -50,6 +50,12 @@ func TestAddLdapBindDn(t *testing.T) { "--attributes-in-bind", "--synchronize-users", "--page-size", "99", + "--group-search-base", "ou=Groups,dc=full-domain-bind,dc=org", + "--group-search-filter", "(&(objectClass=groupOfNames)(member=%s))", + "--user-attribute-in-group", "uid", + "--member-group-filter", "(cn=user-group)", + "--admin-group-filter", "(cn=admin-group)", + "--restricted-group-filter", "(cn=restricted-group)", }, loginSource: &models.LoginSource{ Type: models.LoginLDAP, @@ -66,6 +72,12 @@ func TestAddLdapBindDn(t *testing.T) { BindDN: "cn=readonly,dc=full-domain-bind,dc=org", BindPassword: "secret-bind-full", UserBase: "ou=Users,dc=full-domain-bind,dc=org", + GroupSearchBase: "ou=Groups,dc=full-domain-bind,dc=org", + GroupSearchFilter: "(&(objectClass=groupOfNames)(member=%s))", + UserAttributeInGroup: "uid", + MemberGroupFilter: "(cn=user-group)", + AdminGroupFilter: "(cn=admin-group)", + RestrictedGroupFilter: "(cn=restricted-group)", AttributeUsername: "uid-bind full", AttributeName: "givenName-bind full", AttributeSurname: "sn-bind full", @@ -274,6 +286,12 @@ func TestAddLdapSimpleAuth(t *testing.T) { "--email-attribute", "mail-simple full", "--public-ssh-key-attribute", "publickey-simple full", "--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org", + "--group-search-base", "ou=Groups,dc=full-domain-bind,dc=org", + "--group-search-filter", "(&(objectClass=groupOfNames)(member=%s))", + "--user-attribute-in-group", "uid", + "--member-group-filter", "(cn=user-group)", + "--admin-group-filter", "(cn=admin-group)", + "--restricted-group-filter", "(cn=restricted-group)", }, loginSource: &models.LoginSource{ Type: models.LoginDLDAP, @@ -288,6 +306,12 @@ func TestAddLdapSimpleAuth(t *testing.T) { SkipVerify: true, UserDN: "cn=%s,ou=Users,dc=full-domain-simple,dc=org", UserBase: "ou=Users,dc=full-domain-simple,dc=org", + GroupSearchBase: "ou=Groups,dc=full-domain-bind,dc=org", + GroupSearchFilter: "(&(objectClass=groupOfNames)(member=%s))", + UserAttributeInGroup: "uid", + MemberGroupFilter: "(cn=user-group)", + AdminGroupFilter: "(cn=admin-group)", + RestrictedGroupFilter: "(cn=restricted-group)", AttributeUsername: "uid-simple full", AttributeName: "givenName-simple full", AttributeSurname: "sn-simple full", @@ -513,6 +537,12 @@ func TestUpdateLdapBindDn(t *testing.T) { "--bind-password", "secret-bind-full", "--synchronize-users", "--page-size", "99", + "--group-search-base", "ou=Groups,dc=full-domain-bind,dc=org", + "--group-search-filter", "(&(objectClass=groupOfNames)(member=%s))", + "--user-attribute-in-group", "uid", + "--member-group-filter", "(cn=user-group)", + "--admin-group-filter", "(cn=admin-group)", + "--restricted-group-filter", "(cn=restricted-group)", }, id: 23, existingLoginSource: &models.LoginSource{ @@ -539,6 +569,12 @@ func TestUpdateLdapBindDn(t *testing.T) { BindDN: "cn=readonly,dc=full-domain-bind,dc=org", BindPassword: "secret-bind-full", UserBase: "ou=Users,dc=full-domain-bind,dc=org", + GroupSearchBase: "ou=Groups,dc=full-domain-bind,dc=org", + GroupSearchFilter: "(&(objectClass=groupOfNames)(member=%s))", + UserAttributeInGroup: "uid", + MemberGroupFilter: "(cn=user-group)", + AdminGroupFilter: "(cn=admin-group)", + RestrictedGroupFilter: "(cn=restricted-group)", AttributeUsername: "uid-bind full", AttributeName: "givenName-bind full", AttributeSurname: "sn-bind full", @@ -907,6 +943,102 @@ func TestUpdateLdapBindDn(t *testing.T) { }, errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2", }, + // case 24 + { + args: []string{ + "ldap-test", + "--id", "1", + "--group-search-base", "ou=Groups,dc=full-domain-bind,dc=org", + }, + loginSource: &models.LoginSource{ + Type: models.LoginLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + GroupSearchBase: "ou=Groups,dc=full-domain-bind,dc=org", + }, + }, + }, + }, + // case 25 + { + args: []string{ + "ldap-test", + "--id", "1", + "--group-search-filter", "(&(objectClass=groupOfNames)(member=%s))", + }, + loginSource: &models.LoginSource{ + Type: models.LoginLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + GroupSearchFilter: "(&(objectClass=groupOfNames)(member=%s))", + }, + }, + }, + }, + // case 26 + { + args: []string{ + "ldap-test", + "--id", "1", + "--user-attribute-in-group", "uid", + }, + loginSource: &models.LoginSource{ + Type: models.LoginLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + UserAttributeInGroup: "uid", + }, + }, + }, + }, + // case 27 + { + args: []string{ + "ldap-test", + "--id", "1", + "--member-group-filter", "(cn=user-group)", + }, + loginSource: &models.LoginSource{ + Type: models.LoginLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + MemberGroupFilter: "(cn=user-group)", + }, + }, + }, + }, + // case 28 + { + args: []string{ + "ldap-test", + "--id", "1", + "--admin-group-filter", "(cn=admin-group)", + }, + loginSource: &models.LoginSource{ + Type: models.LoginLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + AdminGroupFilter: "(cn=admin-group)", + }, + }, + }, + }, + // case 29 + { + args: []string{ + "ldap-test", + "--id", "1", + "--restricted-group-filter", "(cn=restricted-group)", + }, + loginSource: &models.LoginSource{ + Type: models.LoginLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + RestrictedGroupFilter: "(cn=restricted-group)", + }, + }, + }, + }, } for n, c := range cases { @@ -991,6 +1123,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { "--email-attribute", "mail-simple full", "--public-ssh-key-attribute", "publickey-simple full", "--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org", + "--group-search-base", "ou=Groups,dc=full-domain-bind,dc=org", + "--group-search-filter", "(&(objectClass=groupOfNames)(member=%s))", + "--user-attribute-in-group", "uid", + "--member-group-filter", "(cn=user-group)", + "--admin-group-filter", "(cn=admin-group)", + "--restricted-group-filter", "(cn=restricted-group)", }, id: 7, loginSource: &models.LoginSource{ @@ -1006,6 +1144,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { SkipVerify: true, UserDN: "cn=%s,ou=Users,dc=full-domain-simple,dc=org", UserBase: "ou=Users,dc=full-domain-simple,dc=org", + GroupSearchBase: "ou=Groups,dc=full-domain-bind,dc=org", + GroupSearchFilter: "(&(objectClass=groupOfNames)(member=%s))", + UserAttributeInGroup: "uid", + MemberGroupFilter: "(cn=user-group)", + AdminGroupFilter: "(cn=admin-group)", + RestrictedGroupFilter: "(cn=restricted-group)", AttributeUsername: "uid-simple full", AttributeName: "givenName-simple full", AttributeSurname: "sn-simple full", @@ -1308,6 +1452,102 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { }, errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM", }, + // case 20 + { + args: []string{ + "ldap-test", + "--id", "1", + "--group-search-base", "ou=Groups,dc=full-domain-bind,dc=org", + }, + loginSource: &models.LoginSource{ + Type: models.LoginDLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + GroupSearchBase: "ou=Groups,dc=full-domain-bind,dc=org", + }, + }, + }, + }, + // case 21 + { + args: []string{ + "ldap-test", + "--id", "1", + "--group-search-filter", "(&(objectClass=groupOfNames)(member=%s))", + }, + loginSource: &models.LoginSource{ + Type: models.LoginDLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + GroupSearchFilter: "(&(objectClass=groupOfNames)(member=%s))", + }, + }, + }, + }, + // case 22 + { + args: []string{ + "ldap-test", + "--id", "1", + "--user-attribute-in-group", "uid", + }, + loginSource: &models.LoginSource{ + Type: models.LoginDLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + UserAttributeInGroup: "uid", + }, + }, + }, + }, + // case 23 + { + args: []string{ + "ldap-test", + "--id", "1", + "--member-group-filter", "(cn=user-group)", + }, + loginSource: &models.LoginSource{ + Type: models.LoginDLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + MemberGroupFilter: "(cn=user-group)", + }, + }, + }, + }, + // case 24 + { + args: []string{ + "ldap-test", + "--id", "1", + "--admin-group-filter", "(cn=admin-group)", + }, + loginSource: &models.LoginSource{ + Type: models.LoginDLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + AdminGroupFilter: "(cn=admin-group)", + }, + }, + }, + }, + // case 25 + { + args: []string{ + "ldap-test", + "--id", "1", + "--restricted-group-filter", "(cn=restricted-group)", + }, + loginSource: &models.LoginSource{ + Type: models.LoginDLDAP, + Cfg: &models.LDAPConfig{ + Source: &ldap.Source{ + RestrictedGroupFilter: "(cn=restricted-group)", + }, + }, + }, + }, } for n, c := range cases { diff --git a/integrations/auth_ldap_test.go b/integrations/auth_ldap_test.go index 520a611eab136..9161c49b71da2 100644 --- a/integrations/auth_ldap_test.go +++ b/integrations/auth_ldap_test.go @@ -40,7 +40,7 @@ var gitLDAPUsers = []ldapUser{ { UserName: "hermes", Password: "hermes", - FullName: "Conrad Hermes", + FullName: "Hermes Conrad", Email: "hermes@planetexpress.com", SSHKeys: []string{ "SHA256:qLY06smKfHoW/92yXySpnxFR10QFrLdRjf/GNPvwcW8", @@ -123,16 +123,61 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string) { session.MakeRequest(t, req, http.StatusFound) } -func TestLDAPUserSignin(t *testing.T) { - if skipLDAPTests() { - t.Skip() - return - } - defer prepareTestEnv(t)() - addAuthSourceLDAP(t, "") +func addAuthSourceLDAPWithGroupsUsingDN(t *testing.T) { + session := loginUser(t, "user1") + csrf := GetCSRF(t, session, "/admin/auths/new") + req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{ + "_csrf": csrf, + "type": "2", + "name": "ldap", + "host": getLDAPServerHost(), + "port": "389", + "bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com", + "bind_password": "password", + "user_base": "ou=people,dc=planetexpress,dc=com", + "filter": "(&(objectClass=inetOrgPerson)(uid=%s))", + "group_search_base": "ou=people,dc=planetexpress,dc=com", + "group_search_filter": "(&(objectClass=groupOfNames)(member=%s))", + "user_attribute_in_group": "", + "member_group_filter": "(cn=git)", + "admin_group_filter": "(cn=admin_staff)", + "attribute_username": "uid", + "attribute_name": "givenName", + "attribute_surname": "sn", + "attribute_mail": "mail", + "is_active": "on", + }) + session.MakeRequest(t, req, http.StatusFound) +} - u := gitLDAPUsers[0] +func addAuthSourceLDAPWithGroupsUsingCN(t *testing.T) { + session := loginUser(t, "user1") + csrf := GetCSRF(t, session, "/admin/auths/new") + req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{ + "_csrf": csrf, + "type": "2", + "name": "ldap", + "host": getLDAPServerHost(), + "port": "389", + "bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com", + "bind_password": "password", + "user_base": "ou=people,dc=planetexpress,dc=com", + "filter": "(&(objectClass=inetOrgPerson)(uid=%s))", + "group_search_base": "ou=people,dc=planetexpress,dc=com", + "group_search_filter": "(&(objectClass=groupOfNames)(member=cn=%s,ou=people,dc=planetexpress,dc=com))", + "user_attribute_in_group": "cn", + "member_group_filter": "(cn=git)", + "admin_group_filter": "(cn=admin_staff)", + "attribute_username": "uid", + "attribute_name": "givenName", + "attribute_surname": "sn", + "attribute_mail": "mail", + "is_active": "on", + }) + session.MakeRequest(t, req, http.StatusFound) +} +func testSingleLDAPSignin(t *testing.T, u *ldapUser) { session := loginUserWithPassword(t, u.UserName, u.Password) req := NewRequest(t, "GET", "/user/settings") resp := session.MakeRequest(t, req, http.StatusOK) @@ -142,6 +187,52 @@ func TestLDAPUserSignin(t *testing.T) { assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name")) assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name")) assert.Equal(t, u.Email, htmlDoc.GetInputValueByName("email")) + + reqAdmin := NewRequest(t, "GET", "/admin") + if u.IsAdmin { + adminResp := session.MakeRequest(t, reqAdmin, http.StatusOK) + assert.Equal(t, http.StatusOK, adminResp.Code) + } else { + adminResp := session.MakeRequest(t, reqAdmin, http.StatusForbidden) + assert.Equal(t, http.StatusForbidden, adminResp.Code) + } +} + +func TestLDAPUserSignin(t *testing.T) { + if skipLDAPTests() { + t.Skip() + return + } + defer prepareTestEnv(t)() + addAuthSourceLDAP(t, "") + + testSingleLDAPSignin(t, &gitLDAPUsers[0]) +} + +func TestLDAPUserSigninWithGroupsUsingDN(t *testing.T) { + if skipLDAPTests() { + t.Skip() + return + } + defer prepareTestEnv(t)() + addAuthSourceLDAPWithGroupsUsingDN(t) + + for _, u := range gitLDAPUsers { + testSingleLDAPSignin(t, &u) + } +} + +func TestLDAPUserSigninWithGroupsUsingCN(t *testing.T) { + if skipLDAPTests() { + t.Skip() + return + } + defer prepareTestEnv(t)() + addAuthSourceLDAPWithGroupsUsingCN(t) + + for _, u := range gitLDAPUsers { + testSingleLDAPSignin(t, &u) + } } func TestLDAPUserSync(t *testing.T) { @@ -208,6 +299,32 @@ func TestLDAPUserSigninFailed(t *testing.T) { testLoginFailed(t, u.UserName, u.Password, i18n.Tr("en", "form.username_password_incorrect")) } +func TestLDAPUserSigninFailedWithGroupsUsingDN(t *testing.T) { + if skipLDAPTests() { + t.Skip() + return + } + defer prepareTestEnv(t)() + addAuthSourceLDAPWithGroupsUsingDN(t) + + for _, u := range otherLDAPUsers { + testLoginFailed(t, u.UserName, u.Password, i18n.Tr("en", "form.username_password_incorrect")) + } +} + +func TestLDAPUserSigninFailedWithGroupsUsingCN(t *testing.T) { + if skipLDAPTests() { + t.Skip() + return + } + defer prepareTestEnv(t)() + addAuthSourceLDAPWithGroupsUsingCN(t) + + for _, u := range otherLDAPUsers { + testLoginFailed(t, u.UserName, u.Password, i18n.Tr("en", "form.username_password_incorrect")) + } +} + func TestLDAPUserSSHKeySync(t *testing.T) { if skipLDAPTests() { t.Skip() diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go index 7fc62607e5215..afe7f32dd1c5d 100644 --- a/modules/auth/auth_form.go +++ b/modules/auth/auth_form.go @@ -20,6 +20,12 @@ type AuthenticationForm struct { BindPassword string UserBase string UserDN string + GroupSearchBase string + GroupSearchFilter string + UserAttributeInGroup string + MemberGroupFilter string + AdminGroupFilter string + RestrictedGroupFilter string AttributeUsername string AttributeName string AttributeSurname string diff --git a/modules/auth/ldap/ldap.go b/modules/auth/ldap/ldap.go index 66676f2829d56..4694d10c8263c 100644 --- a/modules/auth/ldap/ldap.go +++ b/modules/auth/ldap/ldap.go @@ -37,6 +37,12 @@ type Source struct { BindPassword string // Bind DN password UserBase string // Base search path for users UserDN string // Template for the DN of the user for simple auth + GroupSearchBase string // Base search path for groups + GroupSearchFilter string // Query group filter to validate entry + UserAttributeInGroup string // User attribute inserted into group filter + MemberGroupFilter string // Query group filter to check if user is allowed to log in + AdminGroupFilter string // Query group filter to check if user is admin + RestrictedGroupFilter string // Query group filter to check if user is restricted AttributeUsername string // Username attribute AttributeName string // First name attribute AttributeSurname string // Surname attribute @@ -73,6 +79,17 @@ func (ls *Source) sanitizedUserQuery(username string) (string, bool) { return fmt.Sprintf(ls.Filter, username), true } +func (ls *Source) sanitizedGroupQuery(username string) (string, bool) { + // See http://tools.ietf.org/search/rfc4515 + badCharacters := "\x00()*\\" + if strings.ContainsAny(username, badCharacters) { + log.Debug("'%s' contains invalid query characters. Aborting.", username) + return "", false + } + + return fmt.Sprintf(ls.GroupSearchFilter, username), true +} + func (ls *Source) sanitizedUserDN(username string) (string, bool) { // See http://tools.ietf.org/search/rfc4514: "special characters" badCharacters := "\x00()*\\,='\"#+;<>" @@ -201,6 +218,114 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool { return false } +// CheckGroupFilter : +func (ls *Source) CheckGroupFilter(l *ldap.Conn, groupSR *ldap.SearchResult, filter string) bool { + for _, groupEntry := range groupSR.Entries { + search := ldap.NewSearchRequest(groupEntry.DN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, filter, []string{}, nil) + sr, err := l.Search(search) + if (err == nil) && (len(sr.Entries) > 0) { + return true + } + } + log.Trace("LDAP group search with filter %v found no matching entries", filter) + return false +} + +// ProcessUserEntry : +func (ls *Source) ProcessUserEntry(entry *ldap.Entry, l *ldap.Conn) *SearchResult { + username := entry.GetAttributeValue(ls.AttributeUsername) + firstname := entry.GetAttributeValue(ls.AttributeName) + surname := entry.GetAttributeValue(ls.AttributeSurname) + mail := entry.GetAttributeValue(ls.AttributeMail) + + var sshPublicKey []string + if len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 { + sshPublicKey = entry.GetAttributeValues(ls.AttributeSSHPublicKey) + } + + adminFilterSet := len(strings.TrimSpace(ls.AdminFilter)) > 0 + adminGroupFilterSet := len(strings.TrimSpace(ls.AdminGroupFilter)) > 0 + restrictedFilterSet := len(strings.TrimSpace(ls.RestrictedFilter)) > 0 + restrictedGroupFilterSet := len(strings.TrimSpace(ls.RestrictedGroupFilter)) > 0 + + var isInAdminGroup = false + var isInRestrictedGroup = false + if len(strings.TrimSpace(ls.GroupSearchBase)) > 0 && len(strings.TrimSpace(ls.GroupSearchFilter)) > 0 { + var groupUID string + if len(strings.TrimSpace(ls.UserAttributeInGroup)) > 0 { + groupUID = entry.GetAttributeValue(ls.UserAttributeInGroup) + } else { + groupUID = entry.DN + } + log.Trace("User attribute used in LDAP group: %v", groupUID) + + groupFilter, ok := ls.sanitizedGroupQuery(groupUID) + if !ok { + return nil + } + + groupSearch := ldap.NewSearchRequest( + ls.GroupSearchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter, []string{}, nil) + + sr, err := l.Search(groupSearch) + if err != nil { + log.Error("LDAP group search failed unexpectedly! (%v)", err) + return nil + } + + if len(strings.TrimSpace(ls.MemberGroupFilter)) > 0 { + if !ls.CheckGroupFilter(l, sr, ls.MemberGroupFilter) { + log.Error("No group matched the required member group filter!") + return nil + } + } + + if adminGroupFilterSet { + isInAdminGroup = ls.CheckGroupFilter(l, sr, ls.AdminGroupFilter) + if isInAdminGroup { + log.Info("LDAP user %s is in admin group!", username) + } + } + + if restrictedGroupFilterSet { + isInRestrictedGroup = ls.CheckGroupFilter(l, sr, ls.RestrictedGroupFilter) + if isInRestrictedGroup { + log.Info("LDAP user %s is in restricted group!", username) + } + } + } + + var isAdmin = false + if adminFilterSet && adminGroupFilterSet { + isAdmin = isInAdminGroup && checkAdmin(l, ls, entry.DN) + } else if adminFilterSet { + isAdmin = checkAdmin(l, ls, entry.DN) + } else if adminGroupFilterSet { + isAdmin = isInAdminGroup + } + + var isRestricted = false + if !isAdmin { + if restrictedFilterSet && restrictedGroupFilterSet { + isRestricted = isInRestrictedGroup && checkRestricted(l, ls, entry.DN) + } else if restrictedFilterSet { + isRestricted = checkRestricted(l, ls, entry.DN) + } else if restrictedGroupFilterSet { + isRestricted = isInRestrictedGroup + } + } + + return &SearchResult{ + Username: username, + Name: firstname, + Surname: surname, + Mail: mail, + SSHPublicKey: sshPublicKey, + IsAdmin: isAdmin, + IsRestricted: isRestricted, + } +} + // SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult { // See https://tools.ietf.org/search/rfc4513#section-5.1.2 @@ -276,14 +401,15 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul return nil } - var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 - attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail} - if isAttributeSSHPublicKeySet { + if len(strings.TrimSpace(ls.UserAttributeInGroup)) > 0 { + attribs = append(attribs, ls.UserAttributeInGroup) + } + if len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 { attribs = append(attribs, ls.AttributeSSHPublicKey) } - log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, userFilter, userDN) + log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.UserAttributeInGroup, userFilter, userDN) search := ldap.NewSearchRequest( userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, attribs, nil) @@ -302,20 +428,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul return nil } - var sshPublicKey []string - - username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername) - firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName) - surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname) - mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail) - if isAttributeSSHPublicKeySet { - sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey) - } - isAdmin := checkAdmin(l, ls, userDN) - var isRestricted bool - if !isAdmin { - isRestricted = checkRestricted(l, ls, userDN) - } + result := ls.ProcessUserEntry(sr.Entries[0], l) if !directBind && ls.AttributesInBind { // binds user (checking password) after looking-up attributes in BindDN context @@ -325,15 +438,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul } } - return &SearchResult{ - Username: username, - Name: firstname, - Surname: surname, - Mail: mail, - SSHPublicKey: sshPublicKey, - IsAdmin: isAdmin, - IsRestricted: isRestricted, - } + return result } // UsePagedSearch returns if need to use paged search @@ -387,23 +492,14 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) { return nil, err } - result := make([]*SearchResult, len(sr.Entries)) + results := make([]*SearchResult, 0, len(sr.Entries)) - for i, v := range sr.Entries { - result[i] = &SearchResult{ - Username: v.GetAttributeValue(ls.AttributeUsername), - Name: v.GetAttributeValue(ls.AttributeName), - Surname: v.GetAttributeValue(ls.AttributeSurname), - Mail: v.GetAttributeValue(ls.AttributeMail), - IsAdmin: checkAdmin(l, ls, v.DN), - } - if !result[i].IsAdmin { - result[i].IsRestricted = checkRestricted(l, ls, v.DN) - } - if isAttributeSSHPublicKeySet { - result[i].SSHPublicKey = v.GetAttributeValues(ls.AttributeSSHPublicKey) + for _, v := range sr.Entries { + result := ls.ProcessUserEntry(v, l) + if result != nil { + results = append(results, result) } } - return result, nil + return results, nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c31d77e1a58ec..0c13b50006569 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2080,9 +2080,19 @@ auths.allow_deactivate_all = Allow an empty search result to deactivate all user auths.use_paged_search = Use Paged Search auths.search_page_size = Page Size auths.filter = User Filter +auths.filter_helper = If both user filter and member group filter are set, the user must satisfy both conditions to log in. auths.admin_filter = Admin Filter +auths.admin_filter_helper = If both admin filter and admin group filter are set, the user must satisfy both conditions to get admin privileges. auths.restricted_filter = Restricted Filter -auths.restricted_filter_helper = Leave empty to not set any users as restricted. Use an asterisk ('*') to set all users that do not match Admin Filter as restricted. +auths.restricted_filter_helper = Leave empty to not set any users as restricted. Use an asterisk ('*') to set all users that do not match Admin Filter as restricted. If both restricted filter and restricted group filter are set, the user must satisfy both conditions to be a restricted user. +auths.group_search_base = Group Search Base +auths.group_search_filter = Group Search Filter +auths.user_attribute_in_group = User Attribute in Group +auths.user_attribute_in_group_helper = The value of this user attribute is inserted into the group search filter. To use the DN, leave this field empty. +auths.member_group_filter = Member Group Filter +auths.admin_group_filter = Admin Group Filter +auths.restricted_group_filter = Restricted Group Filter +auths.restricted_group_filter_helper = If both restricted filter and restricted group filter are set, the user must satisfy both conditions to be a restricted user. auths.ms_ad_sa = MS AD Search Attributes auths.smtp_auth = SMTP Authentication Type auths.smtphost = SMTP Host diff --git a/routers/admin/auths.go b/routers/admin/auths.go index a4fd5290b74ae..7984b73065273 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auths.go @@ -121,6 +121,12 @@ func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig { UserDN: form.UserDN, BindPassword: form.BindPassword, UserBase: form.UserBase, + GroupSearchBase: form.GroupSearchBase, + GroupSearchFilter: form.GroupSearchFilter, + UserAttributeInGroup: form.UserAttributeInGroup, + MemberGroupFilter: form.MemberGroupFilter, + AdminGroupFilter: form.AdminGroupFilter, + RestrictedGroupFilter: form.RestrictedGroupFilter, AttributeUsername: form.AttributeUsername, AttributeName: form.AttributeName, AttributeSurname: form.AttributeSurname, diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 15ab2b227bd79..c4bc7b462c6c9 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -69,16 +69,46 @@
+

{{.i18n.Tr "admin.auths.filter_helper"}}

+

{{.i18n.Tr "admin.auths.admin_filter_helper"}}

{{.i18n.Tr "admin.auths.restricted_filter_helper"}}

+
+ + +
+
+ + +
+
+ + +

{{.i18n.Tr "admin.auths.user_attribute_in_group_helper"}}

+
+
+ + +

{{.i18n.Tr "admin.auths.filter_helper"}}

+
+
+ + +

{{.i18n.Tr "admin.auths.admin_filter_helper"}}

+
+
+ + +

{{.i18n.Tr "admin.auths.restricted_group_filter_helper"}}

+
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl index f5806c829c54d..b33e6e7206686 100644 --- a/templates/admin/auth/source/ldap.tmpl +++ b/templates/admin/auth/source/ldap.tmpl @@ -41,16 +41,46 @@
+

{{.i18n.Tr "admin.auths.filter_helper"}}

+

{{.i18n.Tr "admin.auths.admin_filter_helper"}}

{{.i18n.Tr "admin.auths.restricted_filter_helper"}}

+
+ + +
+
+ + +
+
+ + +

{{.i18n.Tr "admin.auths.user_attribute_in_group_helper"}}

+
+
+ + +

{{.i18n.Tr "admin.auths.filter_helper"}}

+
+
+ + +

{{.i18n.Tr "admin.auths.admin_filter_helper"}}

+
+
+ + +

{{.i18n.Tr "admin.auths.restricted_group_filter_helper"}}

+