diff --git a/make/common/templates/adminserver/env b/make/common/templates/adminserver/env index dcb8f642a..9a5fabfac 100644 --- a/make/common/templates/adminserver/env +++ b/make/common/templates/adminserver/env @@ -12,6 +12,10 @@ LDAP_UID=$ldap_uid LDAP_SCOPE=$ldap_scope LDAP_TIMEOUT=$ldap_timeout LDAP_VERIFY_CERT=$ldap_verify_cert +LDAP_GROUP_BASEDN=$ldap_group_basedn +LDAP_GROUP_FILTER=$ldap_group_filter +LDAP_GROUP_GID=$ldap_group_gid +LDAP_GROUP_SCOPE=$ldap_group_scope DATABASE_TYPE=mysql MYSQL_HOST=$db_host MYSQL_PORT=$db_port diff --git a/make/harbor.cfg b/make/harbor.cfg index f784947f9..3a1ab6b67 100644 --- a/make/harbor.cfg +++ b/make/harbor.cfg @@ -91,6 +91,18 @@ ldap_timeout = 5 #Verify certificate from LDAP server ldap_verify_cert = true +#The base dn from which to lookup a group in LDAP/AD +ldap_group_basedn = ou=group,dc=mydomain,dc=com + +#filter to search LDAP/AD group +ldap_group_filter = objectclass=group + +#The attribute used to name a LDAP/AD group, it could be cn, name +ldap_group_gid = cn + +#The scope to search for ldap groups. 0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE +ldap_group_scope = 2 + #Turn on or off the self-registration feature self_registration = on diff --git a/make/prepare b/make/prepare index e5df111ef..73addcc16 100755 --- a/make/prepare +++ b/make/prepare @@ -224,6 +224,10 @@ ldap_uid = rcp.get("configuration", "ldap_uid") ldap_scope = rcp.get("configuration", "ldap_scope") ldap_timeout = rcp.get("configuration", "ldap_timeout") ldap_verify_cert = rcp.get("configuration", "ldap_verify_cert") +ldap_group_basedn = rcp.get("configuration", "ldap_group_basedn") +ldap_group_filter = rcp.get("configuration", "ldap_group_filter") +ldap_group_gid = rcp.get("configuration", "ldap_group_gid") +ldap_group_scope = rcp.get("configuration", "ldap_group_scope") db_password = rcp.get("configuration", "db_password") db_host = rcp.get("configuration", "db_host") db_user = rcp.get("configuration", "db_user") @@ -325,6 +329,10 @@ render(os.path.join(templates_dir, "adminserver", "env"), ldap_scope=ldap_scope, ldap_verify_cert=ldap_verify_cert, ldap_timeout=ldap_timeout, + ldap_group_basedn=ldap_group_basedn, + ldap_group_filter=ldap_group_filter, + ldap_group_gid=ldap_group_gid, + ldap_group_scope=ldap_group_scope, db_password=db_password, db_host=db_host, db_user=db_user, diff --git a/src/adminserver/systemcfg/systemcfg.go b/src/adminserver/systemcfg/systemcfg.go index 664d669d9..d48bd66cb 100644 --- a/src/adminserver/systemcfg/systemcfg.go +++ b/src/adminserver/systemcfg/systemcfg.go @@ -89,6 +89,13 @@ var ( env: "LDAP_VERIFY_CERT", parse: parseStringToBool, }, + common.LDAPGroupBaseDN: "LDAP_GROUP_BASEDN", + common.LDAPGroupSearchFilter: "LDAP_GROUP_FILTER", + common.LDAPGroupAttributeName: "LDAP_GROUP_GID", + common.LDAPGroupSearchScope: &parser{ + env: "LDAP_GROUP_SCOPE", + parse: parseStringToInt, + }, common.EmailHost: "EMAIL_HOST", common.EmailPort: &parser{ env: "EMAIL_PORT", @@ -152,7 +159,7 @@ var ( repeatLoadEnvs = map[string]interface{}{ common.ExtEndpoint: "EXT_ENDPOINT", common.MySQLPassword: "MYSQL_PWD", - common.MySQLHost: "MYSQL_HOST", + common.MySQLHost: "MYSQL_HOST", common.MySQLUsername: "MYSQL_USR", common.MySQLDatabase: "MYSQL_DATABASE", common.MySQLPort: &parser{ @@ -179,8 +186,8 @@ var ( common.ClairDBPassword: "CLAIR_DB_PASSWORD", common.ClairDBHost: "CLAIR_DB_HOST", common.ClairDBUsername: "CLAIR_DB_USERNAME", - common.ClairDBPort: &parser{ - env: "CLAIR_DB_PORT", + common.ClairDBPort: &parser{ + env: "CLAIR_DB_PORT", parse: parseStringToInt, }, common.UAAEndpoint: "UAA_ENDPOINT", @@ -395,4 +402,5 @@ func validLdapScope(cfg map[string]interface{}, isMigrate bool) { ldapScope = 0 } cfg[ldapScopeKey] = ldapScope + } diff --git a/src/common/const.go b/src/common/const.go index 44444bd06..1004714c1 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -59,6 +59,10 @@ const ( LDAPScope = "ldap_scope" LDAPTimeout = "ldap_timeout" LDAPVerifyCert = "ldap_verify_cert" + LDAPGroupBaseDN = "ldap_group_base_dn" + LDAPGroupSearchFilter = "ldap_group_search_filter" + LDAPGroupAttributeName = "ldap_group_attribute_name" + LDAPGroupSearchScope = "ldap_group_search_scope" TokenServiceURL = "token_service_url" RegistryURL = "registry_url" EmailHost = "email_host" diff --git a/src/common/models/ldap.go b/src/common/models/ldap.go index 48e03cc77..28e09d026 100644 --- a/src/common/models/ldap.go +++ b/src/common/models/ldap.go @@ -27,12 +27,21 @@ type LdapConf struct { LdapVerifyCert bool `json:"ldap_verify_cert"` } +// LdapGroupConf holds information about ldap group +type LdapGroupConf struct { + LdapGroupBaseDN string `json:"ldap_group_base_dn,omitempty"` + LdapGroupFilter string `json:"ldap_group_filter,omitempty"` + LdapGroupNameAttribute string `json:"ldap_group_name_attribute,omitempty"` + LdapGroupSearchScope int `json:"ldap_group_search_scope"` +} + // LdapUser ... type LdapUser struct { - Username string `json:"ldap_username"` - Email string `json:"ldap_email"` - Realname string `json:"ldap_realname"` - DN string `json:"-"` + Username string `json:"ldap_username"` + Email string `json:"ldap_email"` + Realname string `json:"ldap_realname"` + DN string `json:"-"` + GroupDNList []string `json:"ldap_groupdn"` } //LdapImportUser ... @@ -45,3 +54,9 @@ type LdapFailedImportUser struct { UID string `json:"uid"` Error string `json:"err_msg"` } + +// LdapGroup ... +type LdapGroup struct { + GroupName string `json:"group_name,omitempty"` + GroupDN string `json:"group_dn,omitempty"` +} diff --git a/src/common/utils/ldap/ldap.go b/src/common/utils/ldap/ldap.go index 77cf1b462..505b3f277 100644 --- a/src/common/utils/ldap/ldap.go +++ b/src/common/utils/ldap/ldap.go @@ -185,6 +185,7 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { for _, ldapEntry := range result.Entries { var u models.LdapUser + groupDNList := []string{} for _, attr := range ldapEntry.Attributes { //OpenLdap sometimes contain leading space in useranme val := strings.TrimSpace(attr.Values[0]) @@ -200,7 +201,10 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { u.Email = val case "email": u.Email = val + case "memberof": + groupDNList = append(groupDNList, val) } + u.GroupDNList = groupDNList } u.DN = ldapEntry.DN ldapUsers = append(ldapUsers, u) @@ -248,20 +252,28 @@ func (session *Session) Open() error { // SearchLdap to search ldap with the provide filter func (session *Session) SearchLdap(filter string) (*goldap.SearchResult, error) { - - if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil { - return nil, fmt.Errorf("Can not bind search dn, error: %v", err) - } - - attributes := []string{"uid", "cn", "mail", "email"} + attributes := []string{"uid", "cn", "mail", "email", "memberof"} lowerUID := strings.ToLower(session.ldapConfig.LdapUID) if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" { attributes = append(attributes, session.ldapConfig.LdapUID) } + return session.SearchLdapAttribute(session.ldapConfig.LdapBaseDn, filter, attributes) +} + +// SearchLdapAttribute - to search ldap with the provide filter, with specified attributes +func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes []string) (*goldap.SearchResult, error) { + + if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil { + return nil, fmt.Errorf("Can not bind search dn, error: %v", err) + } + filter = strings.TrimSpace(filter) + if !(strings.HasPrefix(filter, "(") || strings.HasSuffix(filter, ")")) { + filter = "(" + filter + ")" + } log.Debugf("Search ldap with filter:%v", filter) searchRequest := goldap.NewSearchRequest( - session.ldapConfig.LdapBaseDn, + baseDN, session.ldapConfig.LdapScope, goldap.NeverDerefAliases, 0, //Unlimited results @@ -318,3 +330,69 @@ func (session *Session) Close() { session.ldapConn.Close() } } + +//SearchGroupByName ... +func (session *Session) SearchGroupByName(groupName string) ([]models.LdapGroup, error) { + ldapGroupConfig, err := config.LDAPGroupConf() + log.Debugf("Ldap group config: %+v", ldapGroupConfig) + if err != nil { + return nil, err + } + return session.searchGroup(ldapGroupConfig.LdapGroupBaseDN, ldapGroupConfig.LdapGroupFilter, groupName, ldapGroupConfig.LdapGroupNameAttribute) +} + +//SearchGroupByDN ... +func (session *Session) SearchGroupByDN(groupDN string) ([]models.LdapGroup, error) { + ldapGroupConfig, err := config.LDAPGroupConf() + log.Debugf("Ldap group config: %+v", ldapGroupConfig) + if err != nil { + return nil, err + } + return session.searchGroup(groupDN, ldapGroupConfig.LdapGroupFilter, "", ldapGroupConfig.LdapGroupNameAttribute) +} + +func (session *Session) searchGroup(baseDN, filter, groupName, groupNameAttribute string) ([]models.LdapGroup, error) { + ldapGroups := make([]models.LdapGroup, 0) + log.Debugf("Groupname: %v, basedn: %v", groupName, baseDN) + ldapFilter := createGroupSearchFilter(filter, groupName, groupNameAttribute) + attributes := []string{groupNameAttribute} + result, err := session.SearchLdapAttribute(baseDN, ldapFilter, attributes) + if err != nil { + return nil, err + } + for _, ldapEntry := range result.Entries { + var group models.LdapGroup + group.GroupDN = ldapEntry.DN + for _, attr := range ldapEntry.Attributes { + //OpenLdap sometimes contain leading space in useranme + val := strings.TrimSpace(attr.Values[0]) + log.Debugf("Current ldap entry attr name: %s\n", attr.Name) + switch strings.ToLower(attr.Name) { + case strings.ToLower(groupNameAttribute): + group.GroupName = val + } + } + ldapGroups = append(ldapGroups, group) + } + return ldapGroups, nil +} + +func createGroupSearchFilter(oldFilter, groupName, groupNameAttribute string) string { + filter := "" + groupName = goldap.EscapeFilter(groupName) + groupNameAttribute = goldap.EscapeFilter(groupNameAttribute) + if len(oldFilter) == 0 { + if len(groupName) == 0 { + filter = groupNameAttribute + "=*" + } else { + filter = groupNameAttribute + "=*" + groupName + "*" + } + } else { + if len(groupName) == 0 { + filter = oldFilter + } else { + filter = "(&(" + oldFilter + ")(" + groupNameAttribute + "=*" + groupName + "*))" + } + } + return filter +} diff --git a/src/common/utils/ldap/ldap_test.go b/src/common/utils/ldap/ldap_test.go index 7f242d548..03e15955d 100644 --- a/src/common/utils/ldap/ldap_test.go +++ b/src/common/utils/ldap/ldap_test.go @@ -15,15 +15,16 @@ package ldap import ( "os" + "reflect" "testing" - "github.com/vmware/harbor/src/common/models" - "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/test" uiConfig "github.com/vmware/harbor/src/ui/config" + goldap "gopkg.in/ldap.v2" ) var adminServerLdapTestConfig = map[string]interface{}{ @@ -217,6 +218,14 @@ func TestSearchUser(t *testing.T) { t.Fatalf("failed to search user test!") } + result2, err := session.SearchUser("mike") + if err != nil || len(result2) == 0 { + t.Fatalf("failed to search user mike!") + } + if len(result2[0].GroupDNList) < 1 && result2[0].GroupDNList[0] != "cn=harbor_users,ou=groups,dc=example,dc=com" { + t.Fatalf("failed to search user mike's memberof") + } + } func TestFormatURL(t *testing.T) { @@ -254,3 +263,80 @@ func TestFormatURL(t *testing.T) { } } + +func Test_createGroupSearchFilter(t *testing.T) { + type args struct { + oldFilter string + groupName string + groupNameAttribute string + } + tests := []struct { + name string + args args + want string + }{ + {"Normal Filter", args{oldFilter: "objectclass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, "(&(objectclass=groupOfNames)(cn=*harbor_users*))"}, + {"Empty Old", args{groupName: "harbor_users", groupNameAttribute: "cn"}, "cn=*harbor_users*"}, + {"Empty Both", args{groupNameAttribute: "cn"}, "cn=*"}, + {"Empty name", args{oldFilter: "objectclass=groupOfNames", groupNameAttribute: "cn"}, "objectclass=groupOfNames"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createGroupSearchFilter(tt.args.oldFilter, tt.args.groupName, tt.args.groupNameAttribute); got != tt.want { + t.Errorf("createGroupSearchFilter() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSession_SearchGroup(t *testing.T) { + type fields struct { + ldapConfig models.LdapConf + ldapConn *goldap.Conn + } + type args struct { + baseDN string + filter string + groupName string + groupNameAttribute string + } + + ldapConfig := models.LdapConf{ + LdapURL: adminServerLdapTestConfig[common.LDAPURL].(string) + ":389", + LdapSearchDn: adminServerLdapTestConfig[common.LDAPSearchDN].(string), + LdapScope: 2, + LdapSearchPassword: adminServerLdapTestConfig[common.LDAPSearchPwd].(string), + LdapBaseDn: adminServerLdapTestConfig[common.LDAPBaseDN].(string), + } + + tests := []struct { + name string + fields fields + args args + want []models.LdapGroup + wantErr bool + }{ + {"normal search", + fields{ldapConfig: ldapConfig}, + args{baseDN: "dc=example,dc=com", filter: "objectClass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, + []models.LdapGroup{models.LdapGroup{GroupName: "harbor_users", GroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &Session{ + ldapConfig: tt.fields.ldapConfig, + ldapConn: tt.fields.ldapConn, + } + session.Open() + defer session.Close() + got, err := session.searchGroup(tt.args.baseDN, tt.args.filter, tt.args.groupName, tt.args.groupNameAttribute) + if (err != nil) != tt.wantErr { + t.Errorf("Session.SearchGroup() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Session.SearchGroup() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/common/utils/test/adminserver.go b/src/common/utils/test/adminserver.go index 8c4397275..7a21276df 100644 --- a/src/common/utils/test/adminserver.go +++ b/src/common/utils/test/adminserver.go @@ -42,6 +42,10 @@ var adminServerDefaultConfig = map[string]interface{}{ common.LDAPFilter: "", common.LDAPScope: 3, common.LDAPTimeout: 30, + common.LDAPGroupBaseDN: "dc=example,dc=com", + common.LDAPGroupSearchFilter: "objectClass=groupOfNames", + common.LDAPGroupSearchScope: 2, + common.LDAPGroupAttributeName: "cn", common.TokenServiceURL: "http://token_service", common.RegistryURL: "http://registry", common.EmailHost: "127.0.0.1", diff --git a/src/ui/api/config.go b/src/ui/api/config.go index acd5cd09b..5fcbf6462 100644 --- a/src/ui/api/config.go +++ b/src/ui/api/config.go @@ -41,6 +41,10 @@ var ( common.LDAPScope, common.LDAPTimeout, common.LDAPVerifyCert, + common.LDAPGroupAttributeName, + common.LDAPGroupBaseDN, + common.LDAPGroupSearchFilter, + common.LDAPGroupSearchScope, common.EmailHost, common.EmailPort, common.EmailUsername, @@ -66,6 +70,9 @@ var ( common.LDAPBaseDN, common.LDAPUID, common.LDAPFilter, + common.LDAPGroupAttributeName, + common.LDAPGroupBaseDN, + common.LDAPGroupSearchFilter, common.EmailHost, common.EmailUsername, common.EmailPassword, @@ -80,6 +87,7 @@ var ( common.EmailPort, common.LDAPScope, common.LDAPTimeout, + common.LDAPGroupSearchScope, common.TokenExpiration, } diff --git a/src/ui/config/config.go b/src/ui/config/config.go index c3d36e331..2967e9222 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "os" + "strconv" "strings" "github.com/vmware/harbor/src/adminserver/client" @@ -205,6 +206,35 @@ func LDAPConf() (*models.LdapConf, error) { return ldapConf, nil } +// LDAPGroupConf returns the setting of ldap group search +func LDAPGroupConf() (*models.LdapGroupConf, error) { + + cfg, err := mg.Get() + if err != nil { + return nil, err + } + + ldapGroupConf := &models.LdapGroupConf{LdapGroupSearchScope: 2} + if _, ok := cfg[common.LDAPGroupBaseDN]; ok { + ldapGroupConf.LdapGroupBaseDN = cfg[common.LDAPGroupBaseDN].(string) + } + if _, ok := cfg[common.LDAPGroupSearchFilter]; ok { + ldapGroupConf.LdapGroupFilter = cfg[common.LDAPGroupSearchFilter].(string) + } + if _, ok := cfg[common.LDAPGroupAttributeName]; ok { + ldapGroupConf.LdapGroupNameAttribute = cfg[common.LDAPGroupAttributeName].(string) + } + if _, ok := cfg[common.LDAPGroupSearchScope]; ok { + if scopeStr, ok := cfg[common.LDAPGroupSearchScope].(string); ok { + ldapGroupConf.LdapGroupSearchScope, err = strconv.Atoi(scopeStr) + } + if scopeFloat, ok := cfg[common.LDAPGroupSearchScope].(float64); ok { + ldapGroupConf.LdapGroupSearchScope = int(scopeFloat) + } + } + return ldapGroupConf, nil +} + // TokenExpiration returns the token expiration time (in minute) func TokenExpiration() (int, error) { cfg, err := mg.Get() diff --git a/src/ui/config/config_test.go b/src/ui/config/config_test.go index aa1508adb..0b0698b65 100644 --- a/src/ui/config/config_test.go +++ b/src/ui/config/config_test.go @@ -75,6 +75,10 @@ func TestConfig(t *testing.T) { t.Fatalf("failed to get ldap settings: %v", err) } + if _, err := LDAPGroupConf(); err != nil { + t.Fatalf("failed to get ldap group settings: %v", err) + } + if _, err := TokenExpiration(); err != nil { t.Fatalf("failed to get token expiration: %v", err) }