From 5a35b7a9c4921075708ce5944bd0414dc4280f76 Mon Sep 17 00:00:00 2001 From: stonezdj Date: Sun, 28 Feb 2021 09:07:20 +0800 Subject: [PATCH] Move ldap API to new program model Fix some issue with the LDAP connection test Signed-off-by: stonezdj --- api/v2.0/legacy_swagger.yaml | 114 ---- api/v2.0/swagger.yaml | 192 ++++++ src/common/models/ldap.go | 64 -- src/common/rbac/const.go | 1 + src/common/rbac/system/policies.go | 2 + src/common/utils/ldap/ldap_test.go | 636 ------------------ src/controller/ldap/controller.go | 129 ++++ src/controller/ldap/controller_test.go | 146 ++++ src/core/api/harborapi_test.go | 4 - src/core/api/ldap.go | 256 ------- src/core/api/ldap_test.go | 89 --- src/core/api/usergroup.go | 2 +- src/core/auth/db/db_test.go | 20 - src/core/auth/ldap/ldap.go | 49 +- src/core/config/config.go | 39 +- src/{common/utils => pkg}/ldap/filter.go | 12 + src/{common/utils => pkg}/ldap/filter_test.go | 0 src/{common/utils => pkg}/ldap/ldap.go | 240 +++---- src/pkg/ldap/ldap_test.go | 515 ++++++++++++++ src/pkg/ldap/manager.go | 146 ++++ src/pkg/ldap/manager_test.go | 69 ++ src/pkg/ldap/model/ldap.go | 64 ++ src/server/v2.0/handler/handler.go | 1 + src/server/v2.0/handler/ldap.go | 96 +++ src/server/v2.0/route/legacy.go | 4 - src/testing/pkg/ldap/manager.go | 107 +++ src/testing/pkg/pkg.go | 1 + 27 files changed, 1618 insertions(+), 1380 deletions(-) delete mode 100644 src/common/models/ldap.go delete mode 100644 src/common/utils/ldap/ldap_test.go create mode 100644 src/controller/ldap/controller.go create mode 100644 src/controller/ldap/controller_test.go delete mode 100644 src/core/api/ldap.go delete mode 100644 src/core/api/ldap_test.go rename src/{common/utils => pkg}/ldap/filter.go (85%) rename src/{common/utils => pkg}/ldap/filter_test.go (100%) rename src/{common/utils => pkg}/ldap/ldap.go (58%) create mode 100644 src/pkg/ldap/ldap_test.go create mode 100644 src/pkg/ldap/manager.go create mode 100644 src/pkg/ldap/manager_test.go create mode 100644 src/pkg/ldap/model/ldap.go create mode 100644 src/server/v2.0/handler/ldap.go create mode 100644 src/testing/pkg/ldap/manager.go diff --git a/api/v2.0/legacy_swagger.yaml b/api/v2.0/legacy_swagger.yaml index e9f1d4604..19edea0c1 100644 --- a/api/v2.0/legacy_swagger.yaml +++ b/api/v2.0/legacy_swagger.yaml @@ -1310,120 +1310,6 @@ paths: description: No registry found. '500': description: Unexpected internal errors. - /ldap/ping: - post: - summary: Ping available ldap service. - description: | - This endpoint ping the available ldap service for test related configuration parameters. - parameters: - - name: ldapconf - in: body - description: 'ldap configuration. support input ldap service configuration. If it''s a empty request, will load current configuration from the system.' - required: false - schema: - $ref: '#/definitions/LdapConf' - tags: - - Products - responses: - '200': - description: Ping ldap service successfully. - '400': - description: Inviald ldap configuration parameters. - '401': - description: User need to login first. - '403': - description: Only admin has this authority. - '415': - $ref: '#/responses/UnsupportedMediaType' - '500': - description: Unexpected internal errors. - /ldap/groups/search: - get: - summary: Search available ldap groups. - description: | - This endpoint searches the available ldap groups based on related configuration parameters. support to search by groupname or groupdn. - parameters: - - name: groupname - in: query - type: string - required: false - description: Ldap group name - - name: groupdn - in: query - type: string - required: false - description: The LDAP group DN - tags: - - Products - responses: - '200': - description: Search ldap group successfully. - schema: - type: array - items: - $ref: '#/definitions/UserGroup' - '400': - description: The Ldap group DN is invalid. - '404': - description: No ldap group found. - '500': - description: Unexpected internal errors. - /ldap/users/search: - get: - summary: Search available ldap users. - description: | - This endpoint searches the available ldap users based on related configuration parameters. Support searched by input ladp configuration, load configuration from the system and specific filter. - parameters: - - name: username - in: query - type: string - required: false - description: Registered user ID - tags: - - Products - responses: - '200': - description: Search ldap users successfully. - schema: - type: array - items: - $ref: '#/definitions/LdapUsers' - '401': - description: User need to login first. - '403': - description: Only admin has this authority. - '500': - description: Unexpected internal errors. - /ldap/users/import: - post: - summary: Import selected available ldap users. - description: | - This endpoint adds the selected available ldap users to harbor based on related configuration parameters from the system. System will try to guess the user email address and realname, add to harbor user information. - If have errors when import user, will return the list of importing failed uid and the failed reason. - parameters: - - name: uid_list - in: body - description: The uid listed for importing. This list will check users validity of ldap service based on configuration from the system. - required: true - schema: - $ref: '#/definitions/LdapImportUsers' - tags: - - Products - responses: - '200': - description: Add ldap users successfully. - '401': - description: User need to login first. - '403': - description: Only admin has this authority. - '404': - description: Failed import some users. - schema: - type: array - items: - $ref: '#/definitions/LdapFailedImportUsers' - '415': - $ref: '#/responses/UnsupportedMediaType' /usergroups: get: summary: Get all user groups information diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 9fc075d82..c71bbec52 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -19,6 +19,129 @@ security: - basic: [] - {} paths: + /ldap/ping: + post: + operationId: pingLdap + summary: Ping available ldap service. + description: | + This endpoint ping the available ldap service for test related configuration parameters. + parameters: + - name: ldapconf + in: body + description: 'ldap configuration. support input ldap service configuration. If it is a empty request, will load current configuration from the system.' + required: false + schema: + $ref: '#/definitions/LdapConf' + tags: + - Ldap + responses: + '200': + description: Ping ldap service successfully. + schema: + $ref: '#/definitions/LdapPingResult' + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' + /ldap/users/search: + get: + operationId: searchLdapUser + summary: Search available ldap users. + description: | + This endpoint searches the available ldap users based on related configuration parameters. Support searched by input ladp configuration, load configuration from the system and specific filter. + parameters: + - name: username + in: query + type: string + required: false + description: Registered user ID + tags: + - Ldap + responses: + '200': + description: Search ldap users successfully. + schema: + type: array + items: + $ref: '#/definitions/LdapUser' + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' + /ldap/users/import: + post: + operationId: importLdapUser + summary: Import selected available ldap users. + description: | + This endpoint adds the selected available ldap users to harbor based on related configuration parameters from the system. System will try to guess the user email address and realname, add to harbor user information. If have errors when import user, will return the list of importing failed uid and the failed reason. + parameters: + - name: uid_list + in: body + description: The uid listed for importing. This list will check users validity of ldap service based on configuration from the system. + required: true + schema: + $ref: '#/definitions/LdapImportUsers' + tags: + - Ldap + responses: + '200': + description: Add ldap users successfully. + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + description: Failed import some users. + schema: + type: array + items: + $ref: '#/definitions/LdapFailedImportUser' + '500': + $ref: '#/responses/500' + /ldap/groups/search: + get: + summary: Search available ldap groups. + operationId: searchLdapGroup + description: | + This endpoint searches the available ldap groups based on related configuration parameters. support to search by groupname or groupdn. + parameters: + - name: groupname + in: query + type: string + required: false + description: Ldap group name + - name: groupdn + in: query + type: string + required: false + description: The LDAP group DN + tags: + - Ldap + responses: + '200': + description: Search ldap group successfully. + schema: + type: array + items: + $ref: '#/definitions/UserGroup' + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' /projects: get: summary: List projects @@ -5207,3 +5330,72 @@ definitions: type: string extras: type: string + LdapConf: + type: object + description: The ldap configure properties + properties: + ldap_url: + type: string + description: The url of ldap service. + ldap_search_dn: + type: string + description: The search dn of ldap service. + ldap_search_password: + type: string + description: The search password of ldap service. + ldap_base_dn: + type: string + description: The base dn of ldap service. + ldap_filter: + type: string + description: The serach filter of ldap service. + ldap_uid: + type: string + description: The serach uid from ldap service attributes. + ldap_scope: + type: integer + format: int64 + description: The serach scope of ldap service. + ldap_connection_timeout: + type: integer + format: int64 + description: The connect timeout of ldap service(second). + ldap_verify_cert: + type: boolean + description: Verify Ldap server certificate. + LdapPingResult: + type: object + description: The ldap ping result + properties: + success: + type: boolean + description: Test success + message: + type: string + description: The ping operation output message. + LdapImportUsers: + type: object + properties: + ldap_uid_list: + type: array + description: selected uid list + items: + type: string + LdapFailedImportUser: + type: object + x-go-type: + type: FailedImportUser + import: + package: "github.com/goharbor/harbor/src/pkg/ldap/model" + LdapUser: + type: object + x-go-type: + type: User + import: + package: "github.com/goharbor/harbor/src/pkg/ldap/model" + UserGroup: + type: object + x-go-type: + type: Group + import: + package: "github.com/goharbor/harbor/src/pkg/ldap/model" diff --git a/src/common/models/ldap.go b/src/common/models/ldap.go deleted file mode 100644 index 34c690738..000000000 --- a/src/common/models/ldap.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package models - -// LdapConf holds information about ldap configuration -type LdapConf struct { - LdapURL string `json:"ldap_url"` - LdapSearchDn string `json:"ldap_search_dn"` - LdapSearchPassword string `json:"ldap_search_password"` - LdapBaseDn string `json:"ldap_base_dn"` - LdapFilter string `json:"ldap_filter"` - LdapUID string `json:"ldap_uid"` - LdapScope int `json:"ldap_scope"` - LdapConnectionTimeout int `json:"ldap_connection_timeout"` - 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"` - LdapGroupAdminDN string `json:"ldap_group_admin_dn,omitempty"` - LdapGroupMembershipAttribute string `json:"ldap_group_membership_attribute,omitempty"` -} - -// LdapUser ... -type LdapUser struct { - Username string `json:"ldap_username"` - Email string `json:"ldap_email"` - Realname string `json:"ldap_realname"` - DN string `json:"-"` - GroupDNList []string `json:"ldap_groupdn"` -} - -// LdapImportUser ... -type LdapImportUser struct { - LdapUIDList []string `json:"ldap_uid_list"` -} - -// LdapFailedImportUser ... -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:"ldap_group_dn,omitempty"` -} diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index b0d0c4aa8..4eec77ceb 100755 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -41,6 +41,7 @@ const ( ResourceHelmChartVersionLabel = Resource("helm-chart-version-label") ResourceLabel = Resource("label") ResourceLog = Resource("log") + ResourceLdapUser = Resource("ldap-user") ResourceMember = Resource("member") ResourceMetadata = Resource("metadata") ResourceQuota = Resource("quota") diff --git a/src/common/rbac/system/policies.go b/src/common/rbac/system/policies.go index b9cd42841..ac803acba 100644 --- a/src/common/rbac/system/policies.go +++ b/src/common/rbac/system/policies.go @@ -63,5 +63,7 @@ var ( {Resource: rbac.ResourceOIDCEndpoint, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceOIDCEndpoint, Action: rbac.ActionRead}, + {Resource: rbac.ResourceLdapUser, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceLdapUser, Action: rbac.ActionList}, } ) diff --git a/src/common/utils/ldap/ldap_test.go b/src/common/utils/ldap/ldap_test.go deleted file mode 100644 index 0272d250e..000000000 --- a/src/common/utils/ldap/ldap_test.go +++ /dev/null @@ -1,636 +0,0 @@ -package ldap - -import ( - "os" - "reflect" - "testing" - - goldap "github.com/go-ldap/ldap/v3" - "github.com/goharbor/harbor/src/common" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils/test" - uiConfig "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/lib/log" -) - -var ldapTestConfig = map[string]interface{}{ - common.ExtEndpoint: "host01.com", - common.AUTHMode: "ldap_auth", - common.DatabaseType: "postgresql", - common.PostGreSQLHOST: "127.0.0.1", - common.PostGreSQLPort: 5432, - common.PostGreSQLUsername: "postgres", - common.PostGreSQLPassword: "root123", - common.PostGreSQLDatabase: "registry", - // config.SelfRegistration: true, - common.LDAPURL: "ldap://127.0.0.1", - common.LDAPSearchDN: "cn=admin,dc=example,dc=com", - common.LDAPSearchPwd: "admin", - common.LDAPBaseDN: "dc=example,dc=com", - common.LDAPUID: "uid", - common.LDAPFilter: "", - common.LDAPScope: 3, - common.LDAPTimeout: 30, - common.AdminInitialPassword: "password", -} - -var defaultConfigWithVerifyCert = map[string]interface{}{ - common.ExtEndpoint: "https://host01.com", - common.AUTHMode: common.LDAPAuth, - common.DatabaseType: "postgresql", - common.PostGreSQLHOST: "127.0.0.1", - common.PostGreSQLPort: 5432, - common.PostGreSQLUsername: "postgres", - common.PostGreSQLPassword: "root123", - common.PostGreSQLDatabase: "registry", - common.SelfRegistration: true, - common.LDAPURL: "ldap://127.0.0.1:389", - common.LDAPSearchDN: "cn=admin,dc=example,dc=com", - common.LDAPSearchPwd: "admin", - common.LDAPBaseDN: "dc=example,dc=com", - common.LDAPUID: "uid", - common.LDAPFilter: "", - common.LDAPScope: 3, - common.LDAPTimeout: 30, - common.LDAPVerifyCert: true, - common.TokenServiceURL: "http://token_service", - common.RegistryURL: "http://registry", - common.EmailHost: "127.0.0.1", - common.EmailPort: 25, - common.EmailUsername: "user01", - common.EmailPassword: "password", - common.EmailFrom: "from", - common.EmailSSL: true, - common.EmailIdentity: "", - common.ProjectCreationRestriction: common.ProCrtRestrAdmOnly, - common.MaxJobWorkers: 3, - common.TokenExpiration: 30, - common.AdminInitialPassword: "password", - common.WithNotary: false, -} - -func TestMain(m *testing.M) { - test.InitDatabaseFromEnv() - secretKeyPath := "/tmp/secretkey" - _, err := test.GenerateKey(secretKeyPath) - if err != nil { - log.Errorf("failed to generate secret key: %v", err) - return - } - defer os.Remove(secretKeyPath) - - if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil { - log.Fatalf("failed to set env %s: %v", "KEY_PATH", err) - } - - uiConfig.Init() - - uiConfig.Upload(ldapTestConfig) - - os.Exit(m.Run()) - -} - -func TestLoadSystemLdapConfig(t *testing.T) { - session, err := LoadSystemLdapConfig() - if err != nil { - t.Fatalf("failed to get system ldap config %v", err) - } - - if session.ldapConfig.LdapURL != "ldap://127.0.0.1:389" { - t.Errorf("unexpected LdapURL: %s != %s", session.ldapConfig.LdapURL, "ldap://127.0.0.1:389") - } - -} - -func TestConnectTest(t *testing.T) { - session, err := LoadSystemLdapConfig() - if err != nil { - t.Errorf("failed to load system ldap config") - } - err = session.ConnectionTest() - if err != nil { - t.Errorf("Unexpected ldap connect fail: %v", err) - } - -} - -func TestCreateWithConfig(t *testing.T) { - var testConfigs = []struct { - config models.LdapConf - internalValue int - }{ - { - models.LdapConf{ - LdapScope: 3, - LdapURL: "ldaps://127.0.0.1", - }, 2}, - { - models.LdapConf{ - LdapScope: 2, - LdapURL: "ldaps://127.0.0.1", - }, 1}, - { - models.LdapConf{ - LdapScope: 1, - LdapURL: "ldaps://127.0.0.1", - }, 0}, - { - models.LdapConf{ - LdapScope: 1, - LdapURL: "ldaps://127.0.0.1:abc", - }, -1}, - } - - for _, val := range testConfigs { - _, err := CreateWithConfig(val.config) - if val.internalValue < 0 { - if err == nil { - t.Fatalf("Should have error with url :%v", val.config) - } - continue - } - if err != nil { - t.Fatalf("Can not create with ui config, err:%v", err) - } - } - -} - -func TestSearchUser(t *testing.T) { - - session, err := LoadSystemLdapConfig() - if err != nil { - t.Fatalf("Can not load system ldap config") - } - err = session.Open() - if err != nil { - t.Fatalf("failed to create ldap session %v", err) - } - - err = session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword) - if err != nil { - t.Fatalf("failed to bind search dn") - } - - defer session.Close() - - result, err := session.SearchUser("test") - if err != nil || len(result) == 0 { - 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) { - - var invalidURL = "http://localhost:389" - _, err := formatURL(invalidURL) - if err == nil { - t.Fatalf("Should failed on invalid URL %v", invalidURL) - t.Fail() - } - - var urls = []struct { - rawURL string - goodURL string - }{ - {"ldaps://127.0.0.1", "ldaps://127.0.0.1:636"}, - {"ldap://9.123.102.33", "ldap://9.123.102.33:389"}, - {"ldaps://127.0.0.1:389", "ldaps://127.0.0.1:389"}, - {"ldap://127.0.0.1:636", "ldaps://127.0.0.1:636"}, - {"112.122.122.122", "ldap://112.122.122.122:389"}, - {"ldap:\\wrong url", ""}, - } - - for _, u := range urls { - goodURL, err := formatURL(u.rawURL) - if u.goodURL == "" { - if err == nil { - t.Fatalf("Should failed on wrong url, %v", u.rawURL) - } - continue - } - if err != nil || goodURL != u.goodURL { - t.Fatalf("Faild on URL: raw=%v, expected:%v, actual:%v", u.rawURL, u.goodURL, goodURL) - } - } - -} - -func Test_createGroupSearchFilter(t *testing.T) { - type args struct { - oldFilter string - groupName string - groupNameAttribute string - } - tests := []struct { - name string - args args - want string - wantErr error - }{ - {"Normal Filter", args{oldFilter: "objectclass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, "(&(objectclass=groupOfNames)(cn=*harbor_users*))", nil}, - {"Empty Old", args{groupName: "harbor_users", groupNameAttribute: "cn"}, "(cn=*harbor_users*)", nil}, - {"Empty Both", args{groupNameAttribute: "cn"}, "(cn=*)", nil}, - {"Empty name", args{oldFilter: "objectclass=groupOfNames", groupNameAttribute: "cn"}, "(objectclass=groupOfNames)", nil}, - {"Empty name with complex filter", args{oldFilter: "(&(objectClass=groupOfNames)(cn=*sample*))", groupNameAttribute: "cn"}, "(&(objectClass=groupOfNames)(cn=*sample*))", nil}, - {"Empty name with bad filter", args{oldFilter: "(&(objectClass=groupOfNames),cn=*sample*)", groupNameAttribute: "cn"}, "", ErrInvalidFilter}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got, err := createGroupSearchFilter(tt.args.oldFilter, tt.args.groupName, tt.args.groupNameAttribute); got != tt.want && err != tt.wantErr { - 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 { - groupDN string - filter string - groupName string - groupNameAttribute string - } - - ldapConfig := models.LdapConf{ - LdapURL: ldapTestConfig[common.LDAPURL].(string) + ":389", - LdapSearchDn: ldapTestConfig[common.LDAPSearchDN].(string), - LdapScope: 2, - LdapSearchPassword: ldapTestConfig[common.LDAPSearchPwd].(string), - LdapBaseDn: ldapTestConfig[common.LDAPBaseDN].(string), - } - - tests := []struct { - name string - fields fields - args args - want []models.LdapGroup - wantErr bool - }{ - {"normal search", - fields{ldapConfig: ldapConfig}, - args{groupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", filter: "objectClass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, - []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.groupDN, 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) - } - }) - } -} - -func TestSession_SearchGroupByDN(t *testing.T) { - ldapConfig := models.LdapConf{ - LdapURL: ldapTestConfig[common.LDAPURL].(string) + ":389", - LdapSearchDn: ldapTestConfig[common.LDAPSearchDN].(string), - LdapScope: 2, - LdapSearchPassword: ldapTestConfig[common.LDAPSearchPwd].(string), - LdapBaseDn: ldapTestConfig[common.LDAPBaseDN].(string), - } - ldapGroupConfig := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=example,dc=com", - LdapGroupFilter: "objectclass=groupOfNames", - LdapGroupNameAttribute: "cn", - LdapGroupSearchScope: 2, - } - ldapGroupConfig2 := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=example,dc=com", - LdapGroupFilter: "objectclass=groupOfNames", - LdapGroupNameAttribute: "o", - LdapGroupSearchScope: 2, - } - groupConfigWithEmptyBaseDN := models.LdapGroupConf{ - LdapGroupBaseDN: "", - LdapGroupFilter: "(objectclass=groupOfNames)", - LdapGroupNameAttribute: "cn", - LdapGroupSearchScope: 2, - } - groupConfigWithFilter := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=example,dc=com", - LdapGroupFilter: "(cn=*admin*)", - LdapGroupNameAttribute: "cn", - LdapGroupSearchScope: 2, - } - groupConfigWithDifferentGroupDN := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=harbor,dc=example,dc=com", - LdapGroupFilter: "(objectclass=groupOfNames)", - LdapGroupNameAttribute: "cn", - LdapGroupSearchScope: 2, - } - - type fields struct { - ldapConfig models.LdapConf - ldapGroupConfig models.LdapGroupConf - ldapConn *goldap.Conn - } - type args struct { - groupDN string - } - tests := []struct { - name string - fields fields - args args - want []models.LdapGroup - wantErr bool - }{ - {"normal search", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig}, - args{groupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}, - []models.LdapGroup{{GroupName: "harbor_users", GroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false}, - {"search non-exist group", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig}, - args{groupDN: "cn=harbor_non_users,ou=groups,dc=example,dc=com"}, - nil, true}, - {"search invalid group dn", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig}, - args{groupDN: "random string"}, - nil, true}, - {"search with gid = cn", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig}, - args{groupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}, - []models.LdapGroup{{GroupName: "harbor_group", GroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, - {"search with gid = o", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig2}, - args{groupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}, - []models.LdapGroup{{GroupName: "hgroup", GroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, - {"search with empty group base dn", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithEmptyBaseDN}, - args{groupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}, - []models.LdapGroup{{GroupName: "harbor_group", GroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, - {"search with group filter success", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithFilter}, - args{groupDN: "cn=harbor_admin,ou=groups,dc=example,dc=com"}, - []models.LdapGroup{{GroupName: "harbor_admin", GroupDN: "cn=harbor_admin,ou=groups,dc=example,dc=com"}}, false}, - {"search with group filter fail", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithFilter}, - args{groupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}, - []models.LdapGroup{}, false}, - {"search with different group base dn success", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithDifferentGroupDN}, - args{groupDN: "cn=harbor_root,dc=harbor,dc=example,dc=com"}, - []models.LdapGroup{{GroupName: "harbor_root", GroupDN: "cn=harbor_root,dc=harbor,dc=example,dc=com"}}, false}, - {"search with different group base dn fail", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithDifferentGroupDN}, - args{groupDN: "cn=harbor_guest,ou=groups,dc=example,dc=com"}, - []models.LdapGroup{}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - session := &Session{ - ldapConfig: tt.fields.ldapConfig, - ldapGroupConfig: tt.fields.ldapGroupConfig, - ldapConn: tt.fields.ldapConn, - } - session.Open() - defer session.Close() - got, err := session.SearchGroupByDN(tt.args.groupDN) - if (err != nil) != tt.wantErr { - t.Errorf("Session.SearchGroupByDN() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Session.SearchGroupByDN() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestSession_SearchGroupByName(t *testing.T) { - ldapConfig := models.LdapConf{ - LdapURL: ldapTestConfig[common.LDAPURL].(string) + ":389", - LdapSearchDn: ldapTestConfig[common.LDAPSearchDN].(string), - LdapScope: 2, - LdapSearchPassword: ldapTestConfig[common.LDAPSearchPwd].(string), - LdapBaseDn: ldapTestConfig[common.LDAPBaseDN].(string), - } - ldapGroupConfig := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=example,dc=com", - LdapGroupFilter: "objectclass=groupOfNames", - LdapGroupNameAttribute: "cn", - LdapGroupSearchScope: 2, - } - ldapGroupConfig2 := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=example,dc=com", - LdapGroupFilter: "objectclass=groupOfNames", - LdapGroupNameAttribute: "o", - LdapGroupSearchScope: 2, - } - groupConfigWithFilter := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=example,dc=com", - LdapGroupFilter: "(cn=*admin*)", - LdapGroupNameAttribute: "cn", - LdapGroupSearchScope: 2, - } - groupConfigWithDifferentGroupDN := models.LdapGroupConf{ - LdapGroupBaseDN: "dc=harbor,dc=example,dc=com", - LdapGroupFilter: "(objectclass=groupOfNames)", - LdapGroupNameAttribute: "cn", - LdapGroupSearchScope: 2, - } - - type fields struct { - ldapConfig models.LdapConf - ldapGroupConfig models.LdapGroupConf - ldapConn *goldap.Conn - } - type args struct { - groupName string - } - tests := []struct { - name string - fields fields - args args - want []models.LdapGroup - wantErr bool - }{ - {"normal search", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig}, - args{groupName: "harbor_users"}, - []models.LdapGroup{{GroupName: "harbor_users", GroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false}, - {"search non-exist group", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig}, - args{groupName: "harbor_non_users"}, - []models.LdapGroup{}, false}, - {"search with gid = o", - fields{ldapConfig: ldapConfig, ldapGroupConfig: ldapGroupConfig2}, - args{groupName: "hgroup"}, - []models.LdapGroup{{GroupName: "hgroup", GroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, - {"search with group filter success", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithFilter}, - args{groupName: "harbor_admin"}, - []models.LdapGroup{{GroupName: "harbor_admin", GroupDN: "cn=harbor_admin,ou=groups,dc=example,dc=com"}}, false}, - {"search with group filter fail", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithFilter}, - args{groupName: "harbor_users"}, - []models.LdapGroup{}, false}, - {"search with different group base dn success", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithDifferentGroupDN}, - args{groupName: "harbor_root"}, - []models.LdapGroup{{GroupName: "harbor_root", GroupDN: "cn=harbor_root,dc=harbor,dc=example,dc=com"}}, false}, - {"search with different group base dn fail", - fields{ldapConfig: ldapConfig, ldapGroupConfig: groupConfigWithDifferentGroupDN}, - args{groupName: "harbor_guest"}, - []models.LdapGroup{}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - session := &Session{ - ldapConfig: tt.fields.ldapConfig, - ldapGroupConfig: tt.fields.ldapGroupConfig, - ldapConn: tt.fields.ldapConn, - } - session.Open() - defer session.Close() - got, err := session.SearchGroupByName(tt.args.groupName) - if (err != nil) != tt.wantErr { - t.Errorf("Session.SearchGroupByName() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Session.SearchGroupByName() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestCreateUserSearchFilter(t *testing.T) { - type args struct { - origFilter string - ldapUID string - username string - } - cases := []struct { - name string - in args - want string - wantErr error - }{ - {name: `Normal test`, in: args{"(objectclass=inetorgperson)", "cn", "sample"}, want: "(&(objectclass=inetorgperson)(cn=sample)", wantErr: nil}, - {name: `Bad original filter`, in: args{"(objectclass=inetorgperson)ldap*", "cn", "sample"}, want: "", wantErr: ErrInvalidFilter}, - {name: `Complex original filter`, in: args{"(&(objectclass=inetorgperson)(|(memberof=cn=harbor_users,ou=groups,dc=example,dc=com)(memberof=cn=harbor_admin,ou=groups,dc=example,dc=com)(memberof=cn=harbor_guest,ou=groups,dc=example,dc=com)))", "cn", "sample"}, want: "(&(&(objectclass=inetorgperson)(|(memberof=cn=harbor_users,ou=groups,dc=example,dc=com)(memberof=cn=harbor_admin,ou=groups,dc=example,dc=com)(memberof=cn=harbor_guest,ou=groups,dc=example,dc=com)))(cn=sample)", wantErr: nil}, - {name: `Empty original filter`, in: args{"", "cn", "sample"}, want: "(cn=sample)", wantErr: nil}, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - got, gotErr := createUserSearchFilter(tt.in.origFilter, tt.in.ldapUID, tt.in.origFilter) - if got != tt.want && gotErr != tt.wantErr { - t.Errorf(`(%v) = %v; want "%v"`, tt.in, got, tt.want) - } - - }) - } -} - -func TestNormalizeFilter(t *testing.T) { - type args struct { - filter string - } - tests := []struct { - name string - args args - want string - }{ - {"normal test", args{"(objectclass=user)"}, "(objectclass=user)"}, - {"with space", args{" (objectclass=user) "}, "(objectclass=user)"}, - {"nothing", args{"objectclass=user"}, "(objectclass=user)"}, - {"and condition", args{"&(objectclass=user)(cn=admin)"}, "(&(objectclass=user)(cn=admin))"}, - {"or condition", args{"|(objectclass=user)(cn=admin)"}, "(|(objectclass=user)(cn=admin))"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := normalizeFilter(tt.args.filter); got != tt.want { - t.Errorf("normalizeFilter() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestUnderBaseDN(t *testing.T) { - type args struct { - baseDN string - childDN string - } - cases := []struct { - name string - in args - wantError bool - want bool - }{ - { - name: `normal`, - in: args{"dc=example,dc=com", "cn=admin,dc=example,dc=com"}, - wantError: false, - want: true, - }, - { - name: `false`, - in: args{"dc=vmware,dc=com", "cn=admin,dc=example,dc=com"}, - wantError: false, - want: false, - }, - { - name: `same dn`, - in: args{"cn=admin,dc=example,dc=com", "cn=admin,dc=example,dc=com"}, - wantError: false, - want: true, - }, - { - name: `error format in base`, - in: args{"abc", "cn=admin,dc=example,dc=com"}, - wantError: true, - want: false, - }, - { - name: `error format in child`, - in: args{"dc=vmware,dc=com", "wrong format"}, - wantError: true, - want: false, - }, - { - name: `should be case-insensitive`, - in: args{"CN=Users,CN=harbor,DC=com", "cn=harbor_group_1,cn=users,cn=harbor,dc=com"}, - wantError: false, - want: true, - }, - } - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - got, err := UnderBaseDN(tt.in.baseDN, tt.in.childDN) - if (err != nil) != tt.wantError { - t.Errorf("UnderBaseDN error = %v, wantErr %v", err, tt.wantError) - return - } - if got != tt.want { - t.Errorf(`(%v) = %v; want "%v"`, tt.in, got, tt.want) - } - }) - } -} diff --git a/src/controller/ldap/controller.go b/src/controller/ldap/controller.go new file mode 100644 index 000000000..584b1dcbb --- /dev/null +++ b/src/controller/ldap/controller.go @@ -0,0 +1,129 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ldap + +import ( + "context" + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/ldap" + "github.com/goharbor/harbor/src/pkg/ldap/model" +) + +var ( + // Ctl Global instance of the LDAP controller + Ctl = NewController() +) + +// Controller define the operations related to LDAP +type Controller interface { + // Ping test the ldap config + Ping(ctx context.Context, cfg model.LdapConf) (bool, error) + // SearchUser search ldap user with name + SearchUser(ctx context.Context, username string) ([]model.User, error) + // ImportUser import ldap users to harbor + ImportUser(ctx context.Context, importUsers []string) ([]model.FailedImportUser, error) + // SearchGroup search ldap group by name or by dn + SearchGroup(ctx context.Context, groupName, groupDN string) ([]model.Group, error) + // Create ldap session with system config + Session(ctx context.Context) (*ldap.Session, error) +} + +type controller struct { + mgr ldap.Manager +} + +// NewController ... +func NewController() Controller { + return &controller{mgr: ldap.Mgr} +} + +func (c *controller) Session(ctx context.Context) (*ldap.Session, error) { + cfg, groupCfg, err := c.ldapConfigs() + if err != nil { + return nil, err + } + return ldap.NewSession(*cfg, *groupCfg), nil +} + +func (c *controller) Ping(ctx context.Context, cfg model.LdapConf) (bool, error) { + if len(cfg.SearchPassword) == 0 { + pwd, err := defaultPassword() + if err != nil { + return false, err + } + if len(pwd) == 0 { + return false, ldap.ErrEmptyPassword + } + cfg.SearchPassword = pwd + } + return c.mgr.Ping(ctx, cfg) +} + +func (c *controller) ldapConfigs() (*model.LdapConf, *model.GroupConf, error) { + cfg, err := config.LDAPConf() + if err != nil { + return nil, nil, err + } + groupCfg, err := config.LDAPGroupConf() + if err != nil { + log.Warningf("failed to get the ldap group config, error %v", err) + groupCfg = &model.GroupConf{} + } + return cfg, groupCfg, nil +} + +func (c *controller) SearchUser(ctx context.Context, username string) ([]model.User, error) { + cfg, groupCfg, err := c.ldapConfigs() + if err != nil { + return nil, err + } + return c.mgr.SearchUser(ctx, ldap.NewSession(*cfg, *groupCfg), username) +} + +func defaultPassword() (string, error) { + mod, err := config.AuthMode() + if err != nil { + return "", err + } + if mod == common.LDAPAuth { + conf, err := config.LDAPConf() + if err != nil { + return "", err + } + if len(conf.SearchPassword) == 0 { + return "", ldap.ErrEmptyPassword + } + return conf.SearchPassword, nil + } + return "", ldap.ErrEmptyPassword +} + +func (c *controller) ImportUser(ctx context.Context, ldapImportUsers []string) ([]model.FailedImportUser, error) { + cfg, groupCfg, err := c.ldapConfigs() + if err != nil { + return nil, err + } + return c.mgr.ImportUser(ctx, ldap.NewSession(*cfg, *groupCfg), ldapImportUsers) +} + +func (c *controller) SearchGroup(ctx context.Context, groupName, groupDN string) ([]model.Group, error) { + cfg, groupCfg, err := c.ldapConfigs() + if err != nil { + return nil, err + } + return c.mgr.SearchGroup(ctx, ldap.NewSession(*cfg, *groupCfg), groupName, groupDN) +} diff --git a/src/controller/ldap/controller_test.go b/src/controller/ldap/controller_test.go new file mode 100644 index 000000000..36cf6181e --- /dev/null +++ b/src/controller/ldap/controller_test.go @@ -0,0 +1,146 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ldap + +import ( + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/pkg/ldap/model" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/goharbor/harbor/src/testing/mock" + "github.com/goharbor/harbor/src/testing/pkg/ldap" + "testing" + + "github.com/stretchr/testify/suite" +) + +var defaultConfigWithVerifyCert = map[string]interface{}{ + common.ExtEndpoint: "https://host01.com", + common.AUTHMode: common.LDAPAuth, + common.DatabaseType: "postgresql", + common.PostGreSQLHOST: "127.0.0.1", + common.PostGreSQLPort: 5432, + common.PostGreSQLUsername: "postgres", + common.PostGreSQLPassword: "root123", + common.PostGreSQLDatabase: "registry", + common.SelfRegistration: true, + common.LDAPURL: "ldap://127.0.0.1:389", + common.LDAPSearchDN: "cn=admin,dc=example,dc=com", + common.LDAPSearchPwd: "admin", + common.LDAPBaseDN: "dc=example,dc=com", + common.LDAPUID: "uid", + common.LDAPFilter: "", + common.LDAPScope: 2, + common.LDAPTimeout: 30, + common.LDAPVerifyCert: false, + common.LDAPGroupBaseDN: "ou=groups,dc=example,dc=com", + common.LDAPGroupSearchScope: 2, + common.LDAPGroupSearchFilter: "objectclass=groupOfNames", + common.LDAPGroupAttributeName: "cn", + common.TokenServiceURL: "http://token_service", + common.RegistryURL: "http://registry", + common.EmailHost: "127.0.0.1", + common.EmailPort: 25, + common.EmailUsername: "user01", + common.EmailPassword: "password", + common.EmailFrom: "from", + common.EmailSSL: true, + common.EmailIdentity: "", + common.ProjectCreationRestriction: common.ProCrtRestrAdmOnly, + common.MaxJobWorkers: 3, + common.TokenExpiration: 30, + common.AdminInitialPassword: "password", + common.WithNotary: false, +} + +var ldapCfg = model.LdapConf{ + URL: "ldap://127.0.0.1", + SearchDn: "cn=admin,dc=example,dc=com", + SearchPassword: "admin", + BaseDn: "dc=example,dc=com", + UID: "cn", + Scope: 2, + ConnectionTimeout: 30, +} + +var ldapCfgNoPwd = model.LdapConf{ + URL: "ldap://127.0.0.1", + SearchDn: "cn=admin,dc=example,dc=com", + BaseDn: "dc=example,dc=com", + UID: "cn", + Scope: 2, + ConnectionTimeout: 30, +} + +var groupCfg = model.GroupConf{ + BaseDN: "dc=example,dc=com", + NameAttribute: "cn", + SearchScope: 2, + Filter: "objectclass=groupOfNames", + MembershipAttribute: "memberof", +} + +type controllerTestSuite struct { + htesting.Suite + controller Controller +} + +func (c *controllerTestSuite) SetupTest() { + c.controller = Ctl + config.Upload(defaultConfigWithVerifyCert) +} + +func (c *controllerTestSuite) TestPing() { + result, err := c.controller.Ping(c.Context(), ldapCfg) + c.Nil(err) + c.True(result) +} + +func (c *controllerTestSuite) TestPingNoPassword() { + result, err := c.controller.Ping(c.Context(), ldapCfgNoPwd) + c.Nil(err) + c.True(result) +} + +func (c *controllerTestSuite) TestSearchUser() { + users, err := c.controller.SearchUser(c.Context(), "mike02") + c.Nil(err) + c.True(len(users) > 0) +} + +func (c *controllerTestSuite) TestSearchGroup() { + groups, err := c.controller.SearchGroup(c.Context(), "", "cn=harbor_dev,ou=groups,dc=example,dc=com") + c.Nil(err) + c.True(len(groups) > 0) +} + +func (c *controllerTestSuite) TestImportUser() { + mgr := &ldap.Manager{} + mock.OnAnything(mgr, "ImportUser").Return(nil, nil) + c.controller = &controller{mgr: mgr} + failedUsers, err := c.controller.ImportUser(c.Context(), []string{"mike02"}) + c.Nil(err) + c.True(len(failedUsers) == 0) +} + +func (c *controllerTestSuite) TestSession() { + session, err := c.controller.Session(c.Context()) + c.Nil(err) + c.NotNil(session) +} + +func TestControllerTestSuite(t *testing.T) { + suite.Run(t, &controllerTestSuite{}) +} diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 6863fdb47..64bf14a70 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -112,10 +112,6 @@ func init() { beego.Router("/api/registries", &RegistryAPI{}, "get:List;post:Post") beego.Router("/api/registries/ping", &RegistryAPI{}, "post:Ping") beego.Router("/api/registries/:id([0-9]+)", &RegistryAPI{}, "get:Get;put:Put;delete:Delete") - beego.Router("/api/ldap/ping", &LdapAPI{}, "post:Ping") - beego.Router("/api/ldap/users/search", &LdapAPI{}, "get:Search") - beego.Router("/api/ldap/groups/search", &LdapAPI{}, "get:SearchGroup") - beego.Router("/api/ldap/users/import", &LdapAPI{}, "post:ImportUser") beego.Router("/api/configurations", &ConfigAPI{}) beego.Router("/api/configs", &ConfigAPI{}, "get:GetInternalConfig") beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") diff --git a/src/core/api/ldap.go b/src/core/api/ldap.go deleted file mode 100644 index c5134ff21..000000000 --- a/src/core/api/ldap.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2018 Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package api - -import ( - "fmt" - - "github.com/goharbor/harbor/src/common/models" - ldapUtils "github.com/goharbor/harbor/src/common/utils/ldap" - "github.com/goharbor/harbor/src/core/auth" - "github.com/goharbor/harbor/src/lib/log" - - "errors" - "strings" - - goldap "github.com/go-ldap/ldap/v3" - "github.com/goharbor/harbor/src/core/config" -) - -// LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import -type LdapAPI struct { - BaseController - ldapConfig *ldapUtils.Session -} - -const ( - pingErrorMessage = "LDAP connection test failed" - loadSystemErrorMessage = "Can't load system configuration!" - canNotOpenLdapSession = "Can't open LDAP session!" - searchLdapFailMessage = "LDAP search failed!" - importUserError = "Found internal error when importing LDAP user!" -) - -// Prepare ... -func (l *LdapAPI) Prepare() { - l.BaseController.Prepare() - if !l.SecurityCtx.IsAuthenticated() { - l.SendUnAuthorizedError(errors.New("Unauthorized")) - return - } - if !l.SecurityCtx.IsSysAdmin() { - l.SendForbiddenError(errors.New(l.SecurityCtx.GetUsername())) - return - } - - // check the auth_mode except ping - if strings.EqualFold(l.Ctx.Request.RequestURI, "/api/ldap/ping") { - return - } - authMode, err := config.AuthMode() - if err != nil { - l.SendInternalServerError(fmt.Errorf("Can't load system configuration, error: %v", err)) - return - } - if authMode != "ldap_auth" { - l.SendInternalServerError(errors.New("system auth_mode isn't ldap_auth, please check configuration")) - return - } - ldapCfg, err := ldapUtils.LoadSystemLdapConfig() - if err != nil { - l.SendInternalServerError(fmt.Errorf("Can't load system configuration, error: %v", err)) - return - } - l.ldapConfig = ldapCfg - -} - -// Ping ... -func (l *LdapAPI) Ping() { - var err error - var ldapConfs = models.LdapConf{ - LdapConnectionTimeout: 5, - } - - l.Ctx.Input.CopyBody(1 << 32) - - if string(l.Ctx.Input.RequestBody) == "" { - ldapSession := *l.ldapConfig - err = ldapSession.ConnectionTest() - } else { - var isValid bool - isValid, err = l.DecodeJSONReqAndValidate(&ldapConfs) - if !isValid { - l.SendBadRequestError(err) - return - } - err = ldapUtils.ConnectionTestWithConfig(ldapConfs) - } - - if err != nil { - l.SendInternalServerError(fmt.Errorf("LDAP connect fail, error: %v", err)) - return - } -} - -// Search ... -func (l *LdapAPI) Search() { - var err error - var ldapUsers []models.LdapUser - ldapSession := *l.ldapConfig - if err = ldapSession.Open(); err != nil { - l.SendInternalServerError(fmt.Errorf("can't Open LDAP session, error: %v", err)) - return - } - defer ldapSession.Close() - - searchName := l.GetString("username") - - ldapUsers, err = ldapSession.SearchUser(searchName) - - if err != nil { - l.SendInternalServerError(fmt.Errorf("LDAP search fail, error: %v", err)) - return - } - - l.Data["json"] = ldapUsers - l.ServeJSON() - -} - -// ImportUser ... -func (l *LdapAPI) ImportUser() { - var ldapImportUsers models.LdapImportUser - var ldapFailedImportUsers []models.LdapFailedImportUser - - isValid, err := l.DecodeJSONReqAndValidate(&ldapImportUsers) - if !isValid { - l.SendBadRequestError(err) - return - } - - ldapFailedImportUsers, err = importUsers(ldapImportUsers.LdapUIDList, l.ldapConfig) - - if err != nil { - l.SendInternalServerError(fmt.Errorf("LDAP import user fail, error: %v", err)) - return - } - - if len(ldapFailedImportUsers) > 0 { - // Some user require json format response. - l.SendNotFoundError(errors.New("ldap user is not found")) - l.Data["json"] = ldapFailedImportUsers - l.ServeJSON() - return - } - -} - -func importUsers(ldapImportUsers []string, ldapConfig *ldapUtils.Session) ([]models.LdapFailedImportUser, error) { - var failedImportUser []models.LdapFailedImportUser - var u models.LdapFailedImportUser - - ldapSession := *ldapConfig - if err := ldapSession.Open(); err != nil { - log.Errorf("Can't connect to LDAP, error: %v", err) - } - defer ldapSession.Close() - - for _, tempUID := range ldapImportUsers { - u.UID = tempUID - u.Error = "" - - if u.UID == "" { - u.Error = "empty_uid" - failedImportUser = append(failedImportUser, u) - continue - } - - if u.Error != "" { - failedImportUser = append(failedImportUser, u) - continue - } - - ldapUsers, err := ldapSession.SearchUser(u.UID) - if err != nil { - u.UID = tempUID - u.Error = "failed_search_user" - failedImportUser = append(failedImportUser, u) - log.Errorf("Invalid LDAP search request for %s, error: %v", tempUID, err) - continue - } - - if ldapUsers == nil || len(ldapUsers) <= 0 { - u.UID = tempUID - u.Error = "unknown_user" - failedImportUser = append(failedImportUser, u) - continue - } - - var user models.User - - user.Username = ldapUsers[0].Username - user.Realname = ldapUsers[0].Realname - user.Email = ldapUsers[0].Email - err = auth.OnBoardUser(&user) - - if err != nil || user.UserID <= 0 { - u.UID = tempUID - u.Error = err.Error() - failedImportUser = append(failedImportUser, u) - log.Errorf("Can't import user %s, error: %s", tempUID, u.Error) - } - - } - - return failedImportUser, nil -} - -// SearchGroup ... Search LDAP by groupname -func (l *LdapAPI) SearchGroup() { - var ldapGroups []models.LdapGroup - var err error - searchName := l.GetString("groupname") - groupDN := l.GetString("groupdn") - ldapSession := *l.ldapConfig - ldapSession.Open() - defer ldapSession.Close() - - // Search LDAP group by groupName or group DN - if len(searchName) > 0 { - ldapGroups, err = ldapSession.SearchGroupByName(searchName) - if err != nil { - l.SendInternalServerError(fmt.Errorf("can't search LDAP group by name, error: %v", err)) - return - } - } else if len(groupDN) > 0 { - if _, err := goldap.ParseDN(groupDN); err != nil { - l.SendBadRequestError(fmt.Errorf("invalid DN: %v", err)) - return - } - ldapGroups, err = ldapSession.SearchGroupByDN(groupDN) - if err != nil { - // OpenLDAP usually return an error if DN is not found - l.SendNotFoundError(fmt.Errorf("search LDAP group fail, error: %v", err)) - return - } - } - if len(ldapGroups) == 0 { - l.SendNotFoundError(errors.New("No ldap group found")) - return - } - l.Data["json"] = ldapGroups - l.ServeJSON() -} diff --git a/src/core/api/ldap_test.go b/src/core/api/ldap_test.go deleted file mode 100644 index f12b399ab..000000000 --- a/src/core/api/ldap_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2018 Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package api - -import ( - "fmt" - "testing" - - "github.com/goharbor/harbor/src/common" - "github.com/goharbor/harbor/src/common/utils/test" - "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/testing/apitests/apilib" - "github.com/stretchr/testify/assert" -) - -var ldapTestConfig = map[string]interface{}{ - common.ExtEndpoint: "host01.com", - common.AUTHMode: "ldap_auth", - common.DatabaseType: "postgresql", - common.PostGreSQLHOST: "127.0.0.1", - common.PostGreSQLPort: 5432, - common.PostGreSQLUsername: "postgres", - common.PostGreSQLPassword: "root123", - common.PostGreSQLDatabase: "registry", - common.LDAPURL: "ldap://127.0.0.1", - common.LDAPSearchDN: "cn=admin,dc=example,dc=com", - common.LDAPSearchPwd: "admin", - common.LDAPBaseDN: "dc=example,dc=com", - common.LDAPUID: "uid", - common.LDAPFilter: "", - common.LDAPScope: 2, - common.LDAPTimeout: 30, - common.AdminInitialPassword: "password", - common.LDAPGroupSearchFilter: "objectclass=groupOfNames", - common.LDAPGroupBaseDN: "dc=example,dc=com", - common.LDAPGroupAttributeName: "cn", - common.LDAPGroupSearchScope: 2, - common.LDAPGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com", -} - -func TestLdapGroupsSearch(t *testing.T) { - - fmt.Println("Testing Ldap Groups Search") - assert := assert.New(t) - config.InitWithSettings(ldapTestConfig) - apiTest := newHarborAPI() - - ldapGroup := apilib.LdapGroupsSearch{ - GroupName: "harbor_users", - GroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", - } - - // case 1: search group by name - code, groups, err := apiTest.LdapGroupsSearch(ldapGroup.GroupName, "", *admin) - if err != nil { - t.Error("Error occurred while search ldap groups", err.Error()) - t.Log(err) - } else { - assert.Equal(200, code, "Search ldap group status should be 200") - assert.Equal(1, len(groups), "Search ldap groups record should be 1") - assert.Equal(ldapGroup.GroupDN, groups[0].GroupDN, "Group DNs should be equal") - assert.Equal(ldapGroup.GroupName, groups[0].GroupName, "Group names should be equal") - } - - // case 2: search group by DN - code, groups, err = apiTest.LdapGroupsSearch("", ldapGroup.GroupDN, *admin) - if err != nil { - t.Error("Error occurred while search ldap groups", err.Error()) - t.Log(err) - } else { - assert.Equal(200, code, "Search ldap groups status should be 200") - assert.Equal(1, len(groups), "Search ldap groups record should be 1 ") - assert.Equal(ldapGroup.GroupDN, groups[0].GroupDN, "Group DNs should be equal") - assert.Equal(ldapGroup.GroupName, groups[0].GroupName, "Group names should be equal") - } - - config.InitWithSettings(test.GetDefaultConfigMap()) -} diff --git a/src/core/api/usergroup.go b/src/core/api/usergroup.go index a66065288..01b581486 100644 --- a/src/core/api/usergroup.go +++ b/src/core/api/usergroup.go @@ -26,10 +26,10 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao/group" "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils/ldap" "github.com/goharbor/harbor/src/core/auth" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/ldap" ) // UserGroupAPI ... diff --git a/src/core/auth/db/db_test.go b/src/core/auth/db/db_test.go index b76d8e08f..cdea76c4f 100644 --- a/src/core/auth/db/db_test.go +++ b/src/core/auth/db/db_test.go @@ -23,7 +23,6 @@ import ( "github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils/ldap" "github.com/goharbor/harbor/src/core/auth" "github.com/goharbor/harbor/src/core/config" coreConfig "github.com/goharbor/harbor/src/core/config" @@ -119,22 +118,3 @@ func TestAuthenticateHelperSearchUser(t *testing.T) { t.Error("Failed to search user admin") } } - -func TestLdapConnectionTest(t *testing.T) { - var ldapConfig = models.LdapConf{ - LdapURL: "ldap://127.0.0.1", - LdapSearchDn: "cn=admin,dc=example,dc=com", - LdapSearchPassword: "admin", - LdapBaseDn: "dc=example,dc=com", - LdapFilter: "", - LdapUID: "cn", - LdapScope: 3, - LdapConnectionTimeout: 10, - LdapVerifyCert: false, - } - // Test ldap connection under auth_mod is db_auth - err := ldap.ConnectionTestWithConfig(ldapConfig) - if err != nil { - t.Fatalf("Failed to test ldap server! error %v", err) - } -} diff --git a/src/core/auth/ldap/ldap.go b/src/core/auth/ldap/ldap.go index 8102cef11..864e4c377 100644 --- a/src/core/auth/ldap/ldap.go +++ b/src/core/auth/ldap/ldap.go @@ -15,7 +15,9 @@ package ldap import ( + "context" "fmt" + "github.com/goharbor/harbor/src/pkg/ldap/model" "regexp" "strings" @@ -26,7 +28,9 @@ import ( "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/models" - ldapUtils "github.com/goharbor/harbor/src/common/utils/ldap" + ldapCtl "github.com/goharbor/harbor/src/controller/ldap" + "github.com/goharbor/harbor/src/pkg/ldap" + "github.com/goharbor/harbor/src/core/auth" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/lib/log" @@ -47,9 +51,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { log.Debugf("LDAP authentication failed for empty user id.") return nil, auth.NewErrAuth("Empty user id") } - - ldapSession, err := ldapUtils.LoadSystemLdapConfig() - + ldapSession, err := ldapCtl.Ctl.Session(context.Background()) if err != nil { return nil, fmt.Errorf("can not load system ldap config: %v", err) } @@ -91,7 +93,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { return &u, nil } -func (l *Auth) attachLDAPGroup(ldapUsers []models.LdapUser, u *models.User, sess *ldapUtils.Session) { +func (l *Auth) attachLDAPGroup(ldapUsers []model.User, u *models.User, sess *ldap.Session) { // Retrieve ldap related info in login to avoid too many traffic with LDAP server. // Get group admin dn groupCfg, err := config.LDAPGroupConf() @@ -99,7 +101,7 @@ func (l *Auth) attachLDAPGroup(ldapUsers []models.LdapUser, u *models.User, sess log.Warningf("Failed to fetch ldap group configuration:%v", err) // most likely user doesn't configure user group info, it should not block user login } - groupAdminDN := utils.TrimLower(groupCfg.LdapGroupAdminDN) + groupAdminDN := utils.TrimLower(groupCfg.AdminDN) // Attach user group for _, groupDN := range ldapUsers[0].GroupDNList { @@ -121,7 +123,7 @@ func (l *Auth) attachLDAPGroup(ldapUsers []models.LdapUser, u *models.User, sess log.Warningf("Can not get the ldap group name with DN %v", dn) continue } - userGroups = append(userGroups, models.UserGroup{GroupName: lGroups[0].GroupName, LdapGroupDN: dn, GroupType: common.LDAPGroupType}) + userGroups = append(userGroups, models.UserGroup{GroupName: lGroups[0].Name, LdapGroupDN: dn, GroupType: common.LDAPGroupType}) } u.GroupIDs, err = group.PopulateGroup(userGroups) if err != nil { @@ -159,24 +161,27 @@ func (l *Auth) OnBoardUser(u *models.User) error { // SearchUser -- Search user in ldap func (l *Auth) SearchUser(username string) (*models.User, error) { var user models.User - ldapSession, err := ldapUtils.LoadSystemLdapConfig() - if err = ldapSession.Open(); err != nil { + s, err := ldapCtl.Ctl.Session(context.Background()) + if err != nil { + return nil, err + } + if err = s.Open(); err != nil { return nil, fmt.Errorf("Failed to load system ldap config, %v", err) } - defer ldapSession.Close() - ldapUsers, err := ldapSession.SearchUser(username) + defer s.Close() + lUsers, err := s.SearchUser(username) if err != nil { return nil, fmt.Errorf("Failed to search user in ldap") } - if len(ldapUsers) > 1 { + if len(lUsers) > 1 { log.Warningf("There are more than one user found, return the first user") } - if len(ldapUsers) > 0 { + if len(lUsers) > 0 { - user.Username = strings.TrimSpace(ldapUsers[0].Username) - user.Realname = strings.TrimSpace(ldapUsers[0].Realname) - user.Email = strings.TrimSpace(ldapUsers[0].Email) + user.Username = strings.TrimSpace(lUsers[0].Username) + user.Realname = strings.TrimSpace(lUsers[0].Realname) + user.Email = strings.TrimSpace(lUsers[0].Email) log.Debugf("Found ldap user %v", user) } else { @@ -191,18 +196,18 @@ func (l *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) { if _, err := goldap.ParseDN(groupKey); err != nil { return nil, auth.ErrInvalidLDAPGroupDN } - ldapSession, err := ldapUtils.LoadSystemLdapConfig() + s, err := ldapCtl.Ctl.Session(context.Background()) if err != nil { return nil, fmt.Errorf("can not load system ldap config: %v", err) } - if err = ldapSession.Open(); err != nil { + if err = s.Open(); err != nil { log.Warningf("ldap connection fail: %v", err) return nil, err } - defer ldapSession.Close() - userGroupList, err := ldapSession.SearchGroupByDN(groupKey) + defer s.Close() + userGroupList, err := s.SearchGroupByDN(groupKey) if err != nil { log.Warningf("ldap search group fail: %v", err) @@ -213,8 +218,8 @@ func (l *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) { return nil, fmt.Errorf("Failed to searh ldap group with groupDN:%v", groupKey) } userGroup := models.UserGroup{ - GroupName: userGroupList[0].GroupName, - LdapGroupDN: userGroupList[0].GroupDN, + GroupName: userGroupList[0].Name, + LdapGroupDN: userGroupList[0].Dn, } return &userGroup, nil } diff --git a/src/core/config/config.go b/src/core/config/config.go index 20b3b5817..990f6014f 100755 --- a/src/core/config/config.go +++ b/src/core/config/config.go @@ -28,6 +28,7 @@ import ( "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/secret" "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/ldap/model" ) const ( @@ -132,37 +133,37 @@ func TokenPrivateKeyPath() string { } // LDAPConf returns the setting of ldap server -func LDAPConf() (*models.LdapConf, error) { +func LDAPConf() (*model.LdapConf, error) { err := cfgMgr.Load() if err != nil { return nil, err } - return &models.LdapConf{ - LdapURL: cfgMgr.Get(common.LDAPURL).GetString(), - LdapSearchDn: cfgMgr.Get(common.LDAPSearchDN).GetString(), - LdapSearchPassword: cfgMgr.Get(common.LDAPSearchPwd).GetString(), - LdapBaseDn: cfgMgr.Get(common.LDAPBaseDN).GetString(), - LdapUID: cfgMgr.Get(common.LDAPUID).GetString(), - LdapFilter: cfgMgr.Get(common.LDAPFilter).GetString(), - LdapScope: cfgMgr.Get(common.LDAPScope).GetInt(), - LdapConnectionTimeout: cfgMgr.Get(common.LDAPTimeout).GetInt(), - LdapVerifyCert: cfgMgr.Get(common.LDAPVerifyCert).GetBool(), + return &model.LdapConf{ + URL: cfgMgr.Get(common.LDAPURL).GetString(), + SearchDn: cfgMgr.Get(common.LDAPSearchDN).GetString(), + SearchPassword: cfgMgr.Get(common.LDAPSearchPwd).GetString(), + BaseDn: cfgMgr.Get(common.LDAPBaseDN).GetString(), + UID: cfgMgr.Get(common.LDAPUID).GetString(), + Filter: cfgMgr.Get(common.LDAPFilter).GetString(), + Scope: cfgMgr.Get(common.LDAPScope).GetInt(), + ConnectionTimeout: cfgMgr.Get(common.LDAPTimeout).GetInt(), + VerifyCert: cfgMgr.Get(common.LDAPVerifyCert).GetBool(), }, nil } // LDAPGroupConf returns the setting of ldap group search -func LDAPGroupConf() (*models.LdapGroupConf, error) { +func LDAPGroupConf() (*model.GroupConf, error) { err := cfgMgr.Load() if err != nil { return nil, err } - return &models.LdapGroupConf{ - LdapGroupBaseDN: cfgMgr.Get(common.LDAPGroupBaseDN).GetString(), - LdapGroupFilter: cfgMgr.Get(common.LDAPGroupSearchFilter).GetString(), - LdapGroupNameAttribute: cfgMgr.Get(common.LDAPGroupAttributeName).GetString(), - LdapGroupSearchScope: cfgMgr.Get(common.LDAPGroupSearchScope).GetInt(), - LdapGroupAdminDN: cfgMgr.Get(common.LDAPGroupAdminDn).GetString(), - LdapGroupMembershipAttribute: cfgMgr.Get(common.LDAPGroupMembershipAttribute).GetString(), + return &model.GroupConf{ + BaseDN: cfgMgr.Get(common.LDAPGroupBaseDN).GetString(), + Filter: cfgMgr.Get(common.LDAPGroupSearchFilter).GetString(), + NameAttribute: cfgMgr.Get(common.LDAPGroupAttributeName).GetString(), + SearchScope: cfgMgr.Get(common.LDAPGroupSearchScope).GetInt(), + AdminDN: cfgMgr.Get(common.LDAPGroupAdminDn).GetString(), + MembershipAttribute: cfgMgr.Get(common.LDAPGroupMembershipAttribute).GetString(), }, nil } diff --git a/src/common/utils/ldap/filter.go b/src/pkg/ldap/filter.go similarity index 85% rename from src/common/utils/ldap/filter.go rename to src/pkg/ldap/filter.go index de9b6c462..098180021 100644 --- a/src/common/utils/ldap/filter.go +++ b/src/pkg/ldap/filter.go @@ -74,3 +74,15 @@ func NewFilterBuilder(filter string) (*FilterBuilder, error) { } return &FilterBuilder{packet: p}, nil } + +// normalizeFilter - add '(' and ')' in ldap filter if it doesn't exist +func normalizeFilter(filter string) string { + norFilter := strings.TrimSpace(filter) + if len(norFilter) == 0 { + return norFilter + } + if strings.HasPrefix(norFilter, "(") && strings.HasSuffix(norFilter, ")") { + return norFilter + } + return "(" + norFilter + ")" +} diff --git a/src/common/utils/ldap/filter_test.go b/src/pkg/ldap/filter_test.go similarity index 100% rename from src/common/utils/ldap/filter_test.go rename to src/pkg/ldap/filter_test.go diff --git a/src/common/utils/ldap/ldap.go b/src/pkg/ldap/ldap.go similarity index 58% rename from src/common/utils/ldap/ldap.go rename to src/pkg/ldap/ldap.go index 0067de7b4..7930b3f06 100644 --- a/src/common/utils/ldap/ldap.go +++ b/src/pkg/ldap/ldap.go @@ -18,73 +18,53 @@ import ( "crypto/tls" "errors" "fmt" + "github.com/goharbor/harbor/src/pkg/ldap/model" "net/url" "strconv" "strings" "time" goldap "github.com/go-ldap/ldap/v3" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/lib/log" ) // ErrNotFound ... var ErrNotFound = errors.New("entity not found") +// ErrEmptyPassword ... +var ErrEmptyPassword = errors.New("empty password") + +// ErrInvalidCredential ... +var ErrInvalidCredential = errors.New("invalid credential") + +// ErrLDAPServerTimeout ... +var ErrLDAPServerTimeout = errors.New("ldap server network timeout") + +// ErrLDAPPingFail ... +var ErrLDAPPingFail = errors.New("fail to ping LDAP server") + // ErrDNSyntax ... var ErrDNSyntax = errors.New("invalid DN syntax") // ErrInvalidFilter ... var ErrInvalidFilter = errors.New("invalid filter syntax") +// ErrEmptyBaseDN ... +var ErrEmptyBaseDN = errors.New("empty base dn") + // Session - define a LDAP session type Session struct { - ldapConfig models.LdapConf - ldapGroupConfig models.LdapGroupConf - ldapConn *goldap.Conn + basicCfg model.LdapConf + groupCfg model.GroupConf + ldapConn *goldap.Conn } -// LoadSystemLdapConfig - load LDAP configure -func LoadSystemLdapConfig() (*Session, error) { - - ldapConf, err := config.LDAPConf() - - if err != nil { - return nil, err +// NewSession create session with configs +func NewSession(basicCfg model.LdapConf, groupCfg model.GroupConf) *Session { + return &Session{ + basicCfg: basicCfg, + groupCfg: groupCfg, } - - ldapGroupConfig, err := config.LDAPGroupConf() - - if err != nil { - return nil, err - } - - return CreateWithAllConfig(*ldapConf, *ldapGroupConfig) -} - -// CreateWithConfig - -func CreateWithConfig(ldapConf models.LdapConf) (*Session, error) { - return CreateWithAllConfig(ldapConf, models.LdapGroupConf{}) -} - -// CreateWithAllConfig - create a Session with internal config -func CreateWithAllConfig(ldapConf models.LdapConf, ldapGroupConfig models.LdapGroupConf) (*Session, error) { - var session Session - - if ldapConf.LdapURL == "" { - return nil, fmt.Errorf("can not get any available LDAP_URL") - } - - ldapURL, err := formatURL(ldapConf.LdapURL) - if err != nil { - return nil, err - } - - ldapConf.LdapURL = ldapURL - session.ldapConfig = ldapConf - session.ldapGroupConfig = ldapGroupConfig - return &session, nil } func formatURL(ldapURL string) (string, error) { @@ -132,82 +112,51 @@ func formatURL(ldapURL string) (string, error) { } -// ConnectionTest - test ldap session connection with system default setting -func (session *Session) ConnectionTest() error { - session, err := LoadSystemLdapConfig() - if err != nil { - return fmt.Errorf("Failed to load system ldap config") - } - - return ConnectionTestWithAllConfig(session.ldapConfig, session.ldapGroupConfig) -} - -// ConnectionTestWithConfig - -func ConnectionTestWithConfig(ldapConfig models.LdapConf) error { - return ConnectionTestWithAllConfig(ldapConfig, models.LdapGroupConf{}) -} - -// ConnectionTestWithAllConfig - test ldap session connection, out of the scope of normal session create/close -func ConnectionTestWithAllConfig(ldapConfig models.LdapConf, ldapGroupConfig models.LdapGroupConf) error { - - // If no password present, use the system default password - if ldapConfig.LdapSearchPassword == "" { - - session, err := LoadSystemLdapConfig() - - if err != nil { - return fmt.Errorf("Failed to load system ldap config") +// TestConfig - test ldap session connection, out of the scope of normal session create/close +func TestConfig(ldapConfig model.LdapConf) (bool, error) { + ts := NewSession(ldapConfig, model.GroupConf{}) + if err := ts.Open(); err != nil { + if goldap.IsErrorWithCode(err, goldap.ErrorNetwork) { + return false, ErrLDAPServerTimeout } - - ldapConfig.LdapSearchPassword = session.ldapConfig.LdapSearchPassword + return false, ErrLDAPPingFail } + defer ts.Close() - testSession, err := CreateWithAllConfig(ldapConfig, ldapGroupConfig) - - if err != nil { - return err + if ts.basicCfg.SearchDn == "" { + return false, ErrEmptyBaseDN } - err = testSession.Open() - - if err != nil { - return err - } - - defer testSession.Close() - - if testSession.ldapConfig.LdapSearchDn != "" { - err = testSession.Bind(testSession.ldapConfig.LdapSearchDn, testSession.ldapConfig.LdapSearchPassword) - if err != nil { - return err + if err := ts.Bind(ts.basicCfg.SearchDn, ts.basicCfg.SearchPassword); err != nil { + if goldap.IsErrorWithCode(err, goldap.LDAPResultInvalidCredentials) { + return false, ErrInvalidCredential } } - - return nil + return true, nil } // SearchUser - search LDAP user by name -func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { - var ldapUsers []models.LdapUser - ldapFilter, err := createUserSearchFilter(session.ldapConfig.LdapFilter, session.ldapConfig.LdapUID, username) +func (s *Session) SearchUser(username string) ([]model.User, error) { + var ldapUsers []model.User + ldapFilter, err := createUserSearchFilter(s.basicCfg.Filter, s.basicCfg.UID, username) if err != nil { return nil, err } - result, err := session.SearchLdap(ldapFilter) + result, err := s.SearchLdap(ldapFilter) if err != nil { return nil, err } for _, ldapEntry := range result.Entries { - var u models.LdapUser - groupDNList := []string{} - groupAttr := strings.ToLower(session.ldapGroupConfig.LdapGroupMembershipAttribute) + var u model.User + groupDNList := make([]string, 0) + groupAttr := strings.ToLower(s.groupCfg.MembershipAttribute) for _, attr := range ldapEntry.Attributes { - // OpenLdap sometimes contain leading space in useranme + // OpenLdap sometimes contain leading space in username 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(session.ldapConfig.LdapUID): + case strings.ToLower(s.basicCfg.UID): u.Username = val case "uid": u.Realname = val @@ -225,10 +174,8 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { } u.GroupDNList = groupDNList } - u.DN = ldapEntry.DN ldapUsers = append(ldapUsers, u) - } return ldapUsers, nil @@ -236,18 +183,21 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { } // Bind with specified DN and password, used in authentication -func (session *Session) Bind(dn string, password string) error { - return session.ldapConn.Bind(dn, password) +func (s *Session) Bind(dn string, password string) error { + return s.ldapConn.Bind(dn, password) } // Open - open Session, should invoke Close for each Open call -func (session *Session) Open() error { - - splitLdapURL := strings.Split(session.ldapConfig.LdapURL, "://") +func (s *Session) Open() error { + ldapURL, err := formatURL(s.basicCfg.URL) + if err != nil { + return err + } + splitLdapURL := strings.Split(ldapURL, "://") protocol, hostport := splitLdapURL[0], splitLdapURL[1] host := strings.Split(hostport, ":")[0] - connectionTimeout := session.ldapConfig.LdapConnectionTimeout + connectionTimeout := s.basicCfg.ConnectionTimeout goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second switch protocol { @@ -256,14 +206,14 @@ func (session *Session) Open() error { if err != nil { return err } - session.ldapConn = ldap + s.ldapConn = ldap case "ldaps": log.Debug("Start to dial ldaps") - ldap, err := goldap.DialTLS("tcp", hostport, &tls.Config{ServerName: host, InsecureSkipVerify: !session.ldapConfig.LdapVerifyCert}) + ldap, err := goldap.DialTLS("tcp", hostport, &tls.Config{ServerName: host, InsecureSkipVerify: !s.basicCfg.VerifyCert}) if err != nil { return err } - session.ldapConn = ldap + s.ldapConn = ldap } return nil @@ -271,27 +221,27 @@ func (session *Session) Open() error { } // SearchLdap to search ldap with the provide filter -func (session *Session) SearchLdap(filter string) (*goldap.SearchResult, error) { +func (s *Session) SearchLdap(filter string) (*goldap.SearchResult, error) { attributes := []string{"uid", "cn", "mail", "email"} - lowerUID := strings.ToLower(session.ldapConfig.LdapUID) + lowerUID := strings.ToLower(s.basicCfg.UID) if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" { - attributes = append(attributes, session.ldapConfig.LdapUID) + attributes = append(attributes, s.basicCfg.UID) } // Add the Group membership attribute - groupAttr := strings.TrimSpace(session.ldapGroupConfig.LdapGroupMembershipAttribute) + groupAttr := strings.TrimSpace(s.groupCfg.MembershipAttribute) log.Debugf("Membership attribute: %s\n", groupAttr) attributes = append(attributes, groupAttr) - return session.SearchLdapAttribute(session.ldapConfig.LdapBaseDn, filter, attributes) + return s.SearchLdapAttribute(s.basicCfg.BaseDn, 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) { +func (s *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) + if err := s.Bind(s.basicCfg.SearchDn, s.basicCfg.SearchPassword); err != nil { + return nil, fmt.Errorf("can not bind search dn, error: %v", err) } filter = normalizeFilter(filter) if len(filter) == 0 { @@ -304,7 +254,7 @@ func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes [] log.Debugf("Search ldap with filter:%v", filter) searchRequest := goldap.NewSearchRequest( baseDN, - session.ldapConfig.LdapScope, + s.basicCfg.Scope, goldap.NeverDerefAliases, 0, // Unlimited results 0, // Search Timeout @@ -314,7 +264,7 @@ func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes [] nil, ) - result, err := session.ldapConn.Search(searchRequest) + result, err := s.ldapConn.Search(searchRequest) if result != nil { log.Debugf("Found entries:%v\n", len(result.Entries)) } else { @@ -351,26 +301,26 @@ func createUserSearchFilter(origFilter, ldapUID, username string) (string, error } // Close - close current session -func (session *Session) Close() { - if session.ldapConn != nil { - session.ldapConn.Close() +func (s *Session) Close() { + if s.ldapConn != nil { + s.ldapConn.Close() } } // SearchGroupByName ... -func (session *Session) SearchGroupByName(groupName string) ([]models.LdapGroup, error) { - return session.searchGroup(session.ldapGroupConfig.LdapGroupBaseDN, - session.ldapGroupConfig.LdapGroupFilter, +func (s *Session) SearchGroupByName(groupName string) ([]model.Group, error) { + return s.searchGroup(s.groupCfg.BaseDN, + s.groupCfg.Filter, groupName, - session.ldapGroupConfig.LdapGroupNameAttribute) + s.groupCfg.NameAttribute) } // SearchGroupByDN ... -func (session *Session) SearchGroupByDN(groupDN string) ([]models.LdapGroup, error) { +func (s *Session) SearchGroupByDN(groupDN string) ([]model.Group, error) { if _, err := goldap.ParseDN(groupDN); err != nil { return nil, ErrDNSyntax } - groupList, err := session.searchGroup(groupDN, session.ldapGroupConfig.LdapGroupFilter, "", session.ldapGroupConfig.LdapGroupNameAttribute) + groupList, err := s.searchGroup(groupDN, s.groupCfg.Filter, "", s.groupCfg.NameAttribute) if serverError, ok := err.(*goldap.Error); ok { log.Debugf("resultCode:%v", serverError.ResultCode) } @@ -380,20 +330,20 @@ func (session *Session) SearchGroupByDN(groupDN string) ([]models.LdapGroup, err return groupList, err } -func (session *Session) groupBaseDN() string { - if len(session.ldapGroupConfig.LdapGroupBaseDN) == 0 { - return session.ldapConfig.LdapBaseDn +func (s *Session) groupBaseDN() string { + if len(s.groupCfg.BaseDN) == 0 { + return s.basicCfg.BaseDn } - return session.ldapGroupConfig.LdapGroupBaseDN + return s.groupCfg.BaseDN } // searchGroup -- Given a group DN and filter, search group -func (session *Session) searchGroup(groupDN, filter, gName, groupNameAttribute string) ([]models.LdapGroup, error) { - ldapGroups := make([]models.LdapGroup, 0) +func (s *Session) searchGroup(groupDN, filter, gName, groupNameAttribute string) ([]model.Group, error) { + ldapGroups := make([]model.Group, 0) log.Debugf("Groupname: %v, groupDN: %v", gName, groupDN) // Check current group DN is under the LDAP group base DN - isChild, err := UnderBaseDN(session.groupBaseDN(), groupDN) + isChild, err := UnderBaseDN(s.groupBaseDN(), groupDN) if err != nil { return ldapGroups, err } @@ -411,7 +361,7 @@ func (session *Session) searchGroup(groupDN, filter, gName, groupNameAttribute s // There maybe many groups under the LDAP group base DN // If return all groups in LDAP group base DN, it might get "Size Limit Exceeded" error // Take the groupDN as the baseDN in the search request to avoid return too many records - result, err := session.SearchLdapAttribute(groupDN, ldapFilter, []string{groupNameAttribute}) + result, err := s.SearchLdapAttribute(groupDN, ldapFilter, []string{groupNameAttribute}) if err != nil { return ldapGroups, err } @@ -422,9 +372,9 @@ func (session *Session) searchGroup(groupDN, filter, gName, groupNameAttribute s if len(result.Entries[0].Attributes) > 0 { groupName = result.Entries[0].Attributes[0].Values[0] } - group := models.LdapGroup{ - GroupDN: result.Entries[0].DN, - GroupName: groupName, + group := model.Group{ + Dn: result.Entries[0].DN, + Name: groupName, } ldapGroups = append(ldapGroups, group) @@ -468,15 +418,3 @@ func createGroupSearchFilter(baseFilter, groupName, groupNameAttr string) (strin fb := base.And(gFilter) return fb.String() } - -// normalizeFilter - add '(' and ')' in ldap filter if it doesn't exist -func normalizeFilter(filter string) string { - norFilter := strings.TrimSpace(filter) - if len(norFilter) == 0 { - return norFilter - } - if strings.HasPrefix(norFilter, "(") && strings.HasSuffix(norFilter, ")") { - return norFilter - } - return "(" + norFilter + ")" -} diff --git a/src/pkg/ldap/ldap_test.go b/src/pkg/ldap/ldap_test.go new file mode 100644 index 000000000..6f1b4836f --- /dev/null +++ b/src/pkg/ldap/ldap_test.go @@ -0,0 +1,515 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ldap + +import ( + "context" + goldap "github.com/go-ldap/ldap/v3" + "github.com/goharbor/harbor/src/pkg/ldap/model" + "github.com/stretchr/testify/assert" + "reflect" + + "os" + "testing" +) + +var ldapCfg = model.LdapConf{ + URL: "ldap://127.0.0.1", + SearchDn: "cn=admin,dc=example,dc=com", + SearchPassword: "admin", + BaseDn: "dc=example,dc=com", + UID: "cn", + Scope: 2, + ConnectionTimeout: 30, +} + +var groupCfg = model.GroupConf{ + BaseDN: "dc=example,dc=com", + NameAttribute: "cn", + SearchScope: 2, + Filter: "objectclass=groupOfNames", + MembershipAttribute: "memberof", +} + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +func TestConnectTest(t *testing.T) { + suc, err := Mgr.Ping(context.Background(), ldapCfg) + if err != nil { + t.Errorf("Unexpected ldap connect fail: %v", err) + } + assert.True(t, suc, "ping test should be success!") +} + +func TestSearchUser(t *testing.T) { + session := NewSession(ldapCfg, groupCfg) + err := session.Open() + if err != nil { + t.Fatalf("failed to create ldap session %v", err) + } + + err = session.Bind(session.basicCfg.SearchDn, session.basicCfg.SearchPassword) + if err != nil { + t.Fatalf("failed to bind search dn") + } + + defer session.Close() + + result, err := session.SearchUser("test") + if err != nil || len(result) == 0 { + t.Fatalf("failed to search user test!") + } + + result2, err := session.SearchUser("admin_user") + if err != nil || len(result2) == 0 { + t.Fatalf("failed to search user admin_user!") + } + if len(result2[0].GroupDNList) < 1 && result2[0].GroupDNList[0] != "cn=harbor_admin,ou=groups,dc=example,dc=com" { + t.Fatalf("failed to search user mike's memberof") + } + +} + +func TestFormatURL(t *testing.T) { + + var invalidURL = "http://localhost:389" + _, err := formatURL(invalidURL) + if err == nil { + t.Fatalf("Should failed on invalid URL %v", invalidURL) + } + + var urls = []struct { + rawURL string + goodURL string + }{ + {"ldaps://127.0.0.1", "ldaps://127.0.0.1:636"}, + {"ldap://9.123.102.33", "ldap://9.123.102.33:389"}, + {"ldaps://127.0.0.1:389", "ldaps://127.0.0.1:389"}, + {"ldap://127.0.0.1:636", "ldaps://127.0.0.1:636"}, + {"112.122.122.122", "ldap://112.122.122.122:389"}, + {"ldap:\\wrong url", ""}, + } + + for _, u := range urls { + goodURL, err := formatURL(u.rawURL) + if u.goodURL == "" { + if err == nil { + t.Fatalf("Should failed on wrong url, %v", u.rawURL) + } + continue + } + if err != nil || goodURL != u.goodURL { + t.Fatalf("Faild on URL: raw=%v, expected:%v, actual:%v", u.rawURL, u.goodURL, goodURL) + } + } + +} + +func Test_createGroupSearchFilter(t *testing.T) { + type args struct { + oldFilter string + groupName string + groupNameAttribute string + } + tests := []struct { + name string + args args + want string + wantErr error + }{ + {"Normal Filter", args{oldFilter: "objectclass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, "(&(objectclass=groupOfNames)(cn=*harbor_users*))", nil}, + {"Empty Old", args{groupName: "harbor_users", groupNameAttribute: "cn"}, "(cn=*harbor_users*)", nil}, + {"Empty Both", args{groupNameAttribute: "cn"}, "(cn=*)", nil}, + {"Empty name", args{oldFilter: "objectclass=groupOfNames", groupNameAttribute: "cn"}, "(objectclass=groupOfNames)", nil}, + {"Empty name with complex filter", args{oldFilter: "(&(objectClass=groupOfNames)(cn=*sample*))", groupNameAttribute: "cn"}, "(&(objectClass=groupOfNames)(cn=*sample*))", nil}, + {"Empty name with bad filter", args{oldFilter: "(&(objectClass=groupOfNames),cn=*sample*)", groupNameAttribute: "cn"}, "", ErrInvalidFilter}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, err := createGroupSearchFilter(tt.args.oldFilter, tt.args.groupName, tt.args.groupNameAttribute); got != tt.want && err != tt.wantErr { + t.Errorf("createGroupSearchFilter() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSession_SearchGroup(t *testing.T) { + type fields struct { + ldapConfig model.LdapConf + ldapConn *goldap.Conn + } + type args struct { + groupDN string + filter string + groupName string + groupNameAttribute string + } + + ldapConfig := model.LdapConf{ + URL: "ldap://127.0.0.1:389", + SearchDn: "cn=admin,dc=example,dc=com", + Scope: 2, + SearchPassword: "admin", + BaseDn: "dc=example,dc=com", + } + + tests := []struct { + name string + fields fields + args args + want []model.Group + wantErr bool + }{ + {"normal search", + fields{ldapConfig: ldapConfig}, + args{groupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", filter: "objectClass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, + []model.Group{{Name: "harbor_users", Dn: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &Session{ + basicCfg: tt.fields.ldapConfig, + ldapConn: tt.fields.ldapConn, + } + session.Open() + defer session.Close() + got, err := session.searchGroup(tt.args.groupDN, 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) + } + }) + } +} + +func TestSession_SearchGroupByDN(t *testing.T) { + ldapGroupConfig := model.GroupConf{ + BaseDN: "dc=example,dc=com", + Filter: "objectclass=groupOfNames", + NameAttribute: "cn", + SearchScope: 2, + } + ldapGroupConfig2 := model.GroupConf{ + BaseDN: "dc=example,dc=com", + Filter: "objectclass=groupOfNames", + NameAttribute: "o", + SearchScope: 2, + } + groupConfigWithEmptyBaseDN := model.GroupConf{ + BaseDN: "", + Filter: "(objectclass=groupOfNames)", + NameAttribute: "cn", + SearchScope: 2, + } + groupConfigWithFilter := model.GroupConf{ + BaseDN: "dc=example,dc=com", + Filter: "(cn=*admin*)", + NameAttribute: "cn", + SearchScope: 2, + } + groupConfigWithDifferentGroupDN := model.GroupConf{ + BaseDN: "dc=harbor,dc=example,dc=com", + Filter: "(objectclass=groupOfNames)", + NameAttribute: "cn", + SearchScope: 2, + } + + type fields struct { + ldapConfig model.LdapConf + ldapGroupConfig model.GroupConf + ldapConn *goldap.Conn + } + type args struct { + groupDN string + } + tests := []struct { + name string + fields fields + args args + want []model.Group + wantErr bool + }{ + {"normal search", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig}, + args{groupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}, + []model.Group{{Name: "harbor_users", Dn: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false}, + {"search non-exist group", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig}, + args{groupDN: "cn=harbor_non_users,ou=groups,dc=example,dc=com"}, + nil, true}, + {"search invalid group dn", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig}, + args{groupDN: "random string"}, + nil, true}, + {"search with gid = cn", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig}, + args{groupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}, + []model.Group{{Name: "harbor_group", Dn: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, + {"search with gid = o", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig2}, + args{groupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}, + []model.Group{{Name: "hgroup", Dn: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, + {"search with empty group base dn", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithEmptyBaseDN}, + args{groupDN: "cn=harbor_group,ou=groups,dc=example,dc=com"}, + []model.Group{{Name: "harbor_group", Dn: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, + {"search with group filter success", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithFilter}, + args{groupDN: "cn=harbor_admin,ou=groups,dc=example,dc=com"}, + []model.Group{{Name: "harbor_admin", Dn: "cn=harbor_admin,ou=groups,dc=example,dc=com"}}, false}, + {"search with group filter fail", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithFilter}, + args{groupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}, + []model.Group{}, false}, + {"search with different group base dn success", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithDifferentGroupDN}, + args{groupDN: "cn=harbor_root,dc=harbor,dc=example,dc=com"}, + []model.Group{{Name: "harbor_root", Dn: "cn=harbor_root,dc=harbor,dc=example,dc=com"}}, false}, + {"search with different group base dn fail", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithDifferentGroupDN}, + args{groupDN: "cn=harbor_guest,ou=groups,dc=example,dc=com"}, + []model.Group{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &Session{ + basicCfg: tt.fields.ldapConfig, + groupCfg: tt.fields.ldapGroupConfig, + ldapConn: tt.fields.ldapConn, + } + session.Open() + defer session.Close() + got, err := session.SearchGroupByDN(tt.args.groupDN) + if (err != nil) != tt.wantErr { + t.Errorf("Session.SearchGroupByDN() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Session.SearchGroupByDN() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSession_SearchGroupByName(t *testing.T) { + ldapGroupConfig := model.GroupConf{ + BaseDN: "dc=example,dc=com", + Filter: "objectclass=groupOfNames", + NameAttribute: "cn", + SearchScope: 2, + } + ldapGroupConfig2 := model.GroupConf{ + BaseDN: "dc=example,dc=com", + Filter: "objectclass=groupOfNames", + NameAttribute: "o", + SearchScope: 2, + } + groupConfigWithFilter := model.GroupConf{ + BaseDN: "dc=example,dc=com", + Filter: "(cn=*admin*)", + NameAttribute: "cn", + SearchScope: 2, + } + groupConfigWithDifferentGroupDN := model.GroupConf{ + BaseDN: "dc=harbor,dc=example,dc=com", + Filter: "(objectclass=groupOfNames)", + NameAttribute: "cn", + SearchScope: 2, + } + + type fields struct { + ldapConfig model.LdapConf + ldapGroupConfig model.GroupConf + ldapConn *goldap.Conn + } + type args struct { + groupName string + } + tests := []struct { + name string + fields fields + args args + want []model.Group + wantErr bool + }{ + {"normal search", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig}, + args{groupName: "harbor_users"}, + []model.Group{{Name: "harbor_users", Dn: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false}, + {"search non-exist group", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig}, + args{groupName: "harbor_non_users"}, + []model.Group{}, false}, + {"search with gid = o", + fields{ldapConfig: ldapCfg, ldapGroupConfig: ldapGroupConfig2}, + args{groupName: "hgroup"}, + []model.Group{{Name: "hgroup", Dn: "cn=harbor_group,ou=groups,dc=example,dc=com"}}, false}, + {"search with group filter success", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithFilter}, + args{groupName: "harbor_admin"}, + []model.Group{{Name: "harbor_admin", Dn: "cn=harbor_admin,ou=groups,dc=example,dc=com"}}, false}, + {"search with group filter fail", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithFilter}, + args{groupName: "harbor_users"}, + []model.Group{}, false}, + {"search with different group base dn success", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithDifferentGroupDN}, + args{groupName: "harbor_root"}, + []model.Group{{Name: "harbor_root", Dn: "cn=harbor_root,dc=harbor,dc=example,dc=com"}}, false}, + {"search with different group base dn fail", + fields{ldapConfig: ldapCfg, ldapGroupConfig: groupConfigWithDifferentGroupDN}, + args{groupName: "harbor_guest"}, + []model.Group{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := &Session{ + basicCfg: tt.fields.ldapConfig, + groupCfg: tt.fields.ldapGroupConfig, + ldapConn: tt.fields.ldapConn, + } + session.Open() + defer session.Close() + got, err := session.SearchGroupByName(tt.args.groupName) + if (err != nil) != tt.wantErr { + t.Errorf("Session.SearchGroupByName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Session.SearchGroupByName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCreateUserSearchFilter(t *testing.T) { + type args struct { + origFilter string + ldapUID string + username string + } + cases := []struct { + name string + in args + want string + wantErr error + }{ + {name: `Normal test`, in: args{"(objectclass=inetorgperson)", "cn", "sample"}, want: "(&(objectclass=inetorgperson)(cn=sample)", wantErr: nil}, + {name: `Bad original filter`, in: args{"(objectclass=inetorgperson)ldap*", "cn", "sample"}, want: "", wantErr: ErrInvalidFilter}, + {name: `Complex original filter`, in: args{"(&(objectclass=inetorgperson)(|(memberof=cn=harbor_users,ou=groups,dc=example,dc=com)(memberof=cn=harbor_admin,ou=groups,dc=example,dc=com)(memberof=cn=harbor_guest,ou=groups,dc=example,dc=com)))", "cn", "sample"}, want: "(&(&(objectclass=inetorgperson)(|(memberof=cn=harbor_users,ou=groups,dc=example,dc=com)(memberof=cn=harbor_admin,ou=groups,dc=example,dc=com)(memberof=cn=harbor_guest,ou=groups,dc=example,dc=com)))(cn=sample)", wantErr: nil}, + {name: `Empty original filter`, in: args{"", "cn", "sample"}, want: "(cn=sample)", wantErr: nil}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := createUserSearchFilter(tt.in.origFilter, tt.in.ldapUID, tt.in.origFilter) + if got != tt.want && gotErr != tt.wantErr { + t.Errorf(`(%v) = %v; want "%v"`, tt.in, got, tt.want) + } + + }) + } +} + +func TestNormalizeFilter(t *testing.T) { + type args struct { + filter string + } + tests := []struct { + name string + args args + want string + }{ + {"normal test", args{"(objectclass=user)"}, "(objectclass=user)"}, + {"with space", args{" (objectclass=user) "}, "(objectclass=user)"}, + {"nothing", args{"objectclass=user"}, "(objectclass=user)"}, + {"and condition", args{"&(objectclass=user)(cn=admin)"}, "(&(objectclass=user)(cn=admin))"}, + {"or condition", args{"|(objectclass=user)(cn=admin)"}, "(|(objectclass=user)(cn=admin))"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := normalizeFilter(tt.args.filter); got != tt.want { + t.Errorf("normalizeFilter() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnderBaseDN(t *testing.T) { + type args struct { + baseDN string + childDN string + } + cases := []struct { + name string + in args + wantError bool + want bool + }{ + { + name: `normal`, + in: args{"dc=example,dc=com", "cn=admin,dc=example,dc=com"}, + wantError: false, + want: true, + }, + { + name: `false`, + in: args{"dc=vmware,dc=com", "cn=admin,dc=example,dc=com"}, + wantError: false, + want: false, + }, + { + name: `same dn`, + in: args{"cn=admin,dc=example,dc=com", "cn=admin,dc=example,dc=com"}, + wantError: false, + want: true, + }, + { + name: `error format in base`, + in: args{"abc", "cn=admin,dc=example,dc=com"}, + wantError: true, + want: false, + }, + { + name: `error format in child`, + in: args{"dc=vmware,dc=com", "wrong format"}, + wantError: true, + want: false, + }, + { + name: `should be case-insensitive`, + in: args{"CN=Users,CN=harbor,DC=com", "cn=harbor_group_1,cn=users,cn=harbor,dc=com"}, + wantError: false, + want: true, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got, err := UnderBaseDN(tt.in.baseDN, tt.in.childDN) + if (err != nil) != tt.wantError { + t.Errorf("UnderBaseDN error = %v, wantErr %v", err, tt.wantError) + return + } + if got != tt.want { + t.Errorf(`(%v) = %v; want "%v"`, tt.in, got, tt.want) + } + }) + } +} diff --git a/src/pkg/ldap/manager.go b/src/pkg/ldap/manager.go new file mode 100644 index 000000000..fc2fbf906 --- /dev/null +++ b/src/pkg/ldap/manager.go @@ -0,0 +1,146 @@ +package ldap + +import ( + "context" + "fmt" + goldap "github.com/go-ldap/ldap/v3" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/core/auth" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/ldap/model" +) + +var ( + // Mgr default quota manager + Mgr = New() +) + +// Manager is used for ldap management +type Manager interface { + // Ping ldap test + Ping(ctx context.Context, cfg model.LdapConf) (bool, error) + SearchUser(ctx context.Context, sess *Session, username string) ([]model.User, error) + ImportUser(ctx context.Context, sess *Session, ldapImportUsers []string) ([]model.FailedImportUser, error) + SearchGroup(ctx context.Context, sess *Session, groupName, groupDN string) ([]model.Group, error) +} + +// New returns a default implementation of Manager +func New() Manager { + return &manager{} +} + +type manager struct { +} + +func (m *manager) Ping(ctx context.Context, cfg model.LdapConf) (bool, error) { + return TestConfig(cfg) +} + +func (m *manager) SearchUser(ctx context.Context, sess *Session, username string) ([]model.User, error) { + users := make([]model.User, 0) + if err := sess.Open(); err != nil { + return users, err + } + defer sess.Close() + + ldapUsers, err := sess.SearchUser(username) + if err != nil { + return users, err + } + for _, u := range ldapUsers { + ldapUser := model.User{ + Username: u.Username, + Realname: u.Realname, + GroupDNList: u.GroupDNList, + Email: u.Email, + } + users = append(users, ldapUser) + } + return users, nil +} + +func (m *manager) ImportUser(ctx context.Context, sess *Session, ldapImportUsers []string) ([]model.FailedImportUser, error) { + failedImportUser := make([]model.FailedImportUser, 0) + if err := sess.Open(); err != nil { + return failedImportUser, err + } + defer sess.Close() + + for _, tempUID := range ldapImportUsers { + var u model.FailedImportUser + u.UID = tempUID + u.Error = "" + + if u.UID == "" { + u.Error = "empty_uid" + failedImportUser = append(failedImportUser, u) + continue + } + + if u.Error != "" { + failedImportUser = append(failedImportUser, u) + continue + } + + ldapUsers, err := sess.SearchUser(u.UID) + if err != nil { + u.UID = tempUID + u.Error = "failed_search_user" + failedImportUser = append(failedImportUser, u) + log.Errorf("Invalid LDAP search request for %s, error: %v", tempUID, err) + continue + } + + if ldapUsers == nil || len(ldapUsers) <= 0 { + u.UID = tempUID + u.Error = "unknown_user" + failedImportUser = append(failedImportUser, u) + continue + } + + var user models.User + + user.Username = ldapUsers[0].Username + user.Realname = ldapUsers[0].Realname + user.Email = ldapUsers[0].Email + err = auth.OnBoardUser(&user) + + if err != nil || user.UserID <= 0 { + u.UID = tempUID + u.Error = "failed to import user: " + u.UID + failedImportUser = append(failedImportUser, u) + log.Errorf("Can't import user %s, error: %s", tempUID, u.Error) + } + + } + + return failedImportUser, nil +} + +func (m *manager) SearchGroup(ctx context.Context, sess *Session, groupName, groupDN string) ([]model.Group, error) { + err := sess.Open() + if err != nil { + return nil, err + } + defer sess.Close() + + ldapGroups := make([]model.Group, 0) + + // Search LDAP group by groupName or group DN + if len(groupName) > 0 { + ldapGroups, err = sess.SearchGroupByName(groupName) + if err != nil { + return nil, err + } + } else if len(groupDN) > 0 { + if _, err := goldap.ParseDN(groupDN); err != nil { + return nil, fmt.Errorf("invalid DN: %v", err) + } + ldapGroups, err = sess.SearchGroupByDN(groupDN) + if err != nil { + return nil, err + } + } + + return ldapGroups, nil +} diff --git a/src/pkg/ldap/manager_test.go b/src/pkg/ldap/manager_test.go new file mode 100644 index 000000000..e1cf28bcb --- /dev/null +++ b/src/pkg/ldap/manager_test.go @@ -0,0 +1,69 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ldap + +import ( + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" + "testing" +) + +type ManagerTestSuite struct { + htesting.Suite +} + +func (suite *ManagerTestSuite) SetupSuite() { + suite.Suite.SetupSuite() + suite.ClearSQLs = []string{"delete from harbor_user where username = 'mike02'"} +} + +func (suite *ManagerTestSuite) TestPing() { + ctx := suite.Context() + suc, err := Mgr.Ping(ctx, ldapCfg) + suite.Nil(err) + suite.True(suc) +} + +func (suite *ManagerTestSuite) TestSearchUser() { + ctx := suite.Context() + sess := NewSession(ldapCfg, groupCfg) + users, err := Mgr.SearchUser(ctx, sess, "mike02") + suite.Nil(err) + suite.True(len(users) > 0) + suite.Equal("mike02", users[0].Username) +} + +func (suite *ManagerTestSuite) TestImportUser() { + ctx := suite.Context() + sess := NewSession(ldapCfg, groupCfg) + failedUsers, err := Mgr.ImportUser(ctx, sess, []string{"mike03"}) + suite.Nil(err) + suite.True(len(failedUsers) > 0) +} + +func (suite *ManagerTestSuite) TestSearchGroup() { + ctx := suite.Context() + ugs, err := Mgr.SearchGroup(ctx, NewSession(ldapCfg, groupCfg), "harbor_admin", "") + suite.Nil(err) + suite.True(len(ugs) > 0) + suite.Equal("cn=harbor_admin,ou=groups,dc=example,dc=com", ugs[0].Dn) + ugs2, err := Mgr.SearchGroup(ctx, NewSession(ldapCfg, groupCfg), "", "cn=harbor_admin,ou=groups,dc=example,dc=com") + suite.Nil(err) + suite.True(len(ugs2) > 0) + suite.Equal("harbor_admin", ugs[0].Name) +} +func TestManagerTestSuite(t *testing.T) { + suite.Run(t, &ManagerTestSuite{}) +} diff --git a/src/pkg/ldap/model/ldap.go b/src/pkg/ldap/model/ldap.go new file mode 100644 index 000000000..ee899add2 --- /dev/null +++ b/src/pkg/ldap/model/ldap.go @@ -0,0 +1,64 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +// LdapConf holds information about ldap configuration +type LdapConf struct { + URL string `json:"ldap_url"` + SearchDn string `json:"ldap_search_dn"` + SearchPassword string `json:"ldap_search_password"` + BaseDn string `json:"ldap_base_dn"` + Filter string `json:"ldap_filter"` + UID string `json:"ldap_uid"` + Scope int `json:"ldap_scope"` + ConnectionTimeout int `json:"ldap_connection_timeout"` + VerifyCert bool `json:"ldap_verify_cert"` +} + +// GroupConf holds information about ldap group +type GroupConf struct { + BaseDN string `json:"ldap_group_base_dn,omitempty"` + Filter string `json:"ldap_group_filter,omitempty"` + NameAttribute string `json:"ldap_group_name_attribute,omitempty"` + SearchScope int `json:"ldap_group_search_scope"` + AdminDN string `json:"ldap_group_admin_dn,omitempty"` + MembershipAttribute string `json:"ldap_group_membership_attribute,omitempty"` +} + +// User ... +type User struct { + Username string `json:"ldap_username"` + Email string `json:"ldap_email"` + Realname string `json:"ldap_realname"` + DN string `json:"-"` + GroupDNList []string `json:"ldap_groupdn"` +} + +// ImportUser ... +type ImportUser struct { + UIDList []string `json:"ldap_uid_list"` +} + +// FailedImportUser ... +type FailedImportUser struct { + UID string `json:"uid"` + Error string `json:"err_msg"` +} + +// Group ... +type Group struct { + Name string `json:"group_name,omitempty"` + Dn string `json:"ldap_group_dn,omitempty"` +} diff --git a/src/server/v2.0/handler/handler.go b/src/server/v2.0/handler/handler.go index 6d3b54630..1eb94f5ae 100644 --- a/src/server/v2.0/handler/handler.go +++ b/src/server/v2.0/handler/handler.go @@ -45,6 +45,7 @@ func New() http.Handler { ReplicationAPI: newReplicationAPI(), SysteminfoAPI: newSystemInfoAPI(), PingAPI: newPingAPI(), + LdapAPI: newLdapAPI(), GCAPI: newGCAPI(), QuotaAPI: newQuotaAPI(), RetentionAPI: newRetentionAPI(), diff --git a/src/server/v2.0/handler/ldap.go b/src/server/v2.0/handler/ldap.go new file mode 100644 index 000000000..44a6a02b4 --- /dev/null +++ b/src/server/v2.0/handler/ldap.go @@ -0,0 +1,96 @@ +package handler + +import ( + "context" + "fmt" + "github.com/go-openapi/runtime/middleware" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/controller/ldap" + "github.com/goharbor/harbor/src/lib/errors" + ldapModel "github.com/goharbor/harbor/src/pkg/ldap/model" + "github.com/goharbor/harbor/src/server/v2.0/models" + operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/ldap" +) + +type ldapAPI struct { + BaseAPI + ctl ldap.Controller +} + +func newLdapAPI() *ldapAPI { + return &ldapAPI{ctl: ldap.Ctl} +} + +func (l *ldapAPI) PingLdap(ctx context.Context, params operation.PingLdapParams) middleware.Responder { + if err := l.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceConfiguration); err != nil { + return l.SendError(ctx, err) + } + basicCfg := ldapModel.LdapConf{ + URL: params.Ldapconf.LdapURL, + BaseDn: params.Ldapconf.LdapBaseDn, + SearchDn: params.Ldapconf.LdapSearchDn, + Filter: params.Ldapconf.LdapFilter, + SearchPassword: params.Ldapconf.LdapSearchPassword, + UID: params.Ldapconf.LdapUID, + Scope: int(params.Ldapconf.LdapScope), + VerifyCert: params.Ldapconf.LdapVerifyCert, + } + payload := &models.LdapPingResult{} + suc, err := l.ctl.Ping(ctx, basicCfg) + payload.Success = suc + if err != nil { + payload.Message = fmt.Sprintf("error: %v", err) + } + return operation.NewPingLdapOK().WithPayload(payload) +} + +func (l *ldapAPI) SearchLdapUser(ctx context.Context, params operation.SearchLdapUserParams) middleware.Responder { + if err := l.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceLdapUser); err != nil { + return l.SendError(ctx, err) + } + var username string + if params.Username != nil { + username = *params.Username + } + ldapUsers, err := l.ctl.SearchUser(ctx, username) + if err != nil { + return l.SendError(ctx, err) + } + return operation.NewSearchLdapUserOK().WithPayload(ldapUsers) +} + +func (l *ldapAPI) ImportLdapUser(ctx context.Context, params operation.ImportLdapUserParams) middleware.Responder { + if err := l.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceLdapUser); err != nil { + return l.SendError(ctx, err) + } + failedList, err := l.ctl.ImportUser(ctx, params.UIDList.LdapUIDList) + if err != nil { + return l.SendError(ctx, err) + } + if len(failedList) == 0 { + return operation.NewImportLdapUserOK() + } + return operation.NewImportLdapUserNotFound().WithPayload(failedList) +} + +func (l *ldapAPI) SearchLdapGroup(ctx context.Context, params operation.SearchLdapGroupParams) middleware.Responder { + if err := l.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceLdapUser); err != nil { + return l.SendError(ctx, err) + } + var groupName, groupDN string + if params.Groupname != nil && len(*params.Groupname) > 0 { + groupName = *params.Groupname + + } + if params.Groupdn != nil { + groupDN = *params.Groupdn + } + ug, err := l.ctl.SearchGroup(ctx, groupName, groupDN) + if err != nil { + return l.SendError(ctx, err) + } + if len(ug) == 0 { + return l.SendError(ctx, errors.NotFoundError(fmt.Errorf("group name:%v, group DN:%v", groupName, groupDN))) + } + return operation.NewSearchLdapGroupOK().WithPayload(ug) +} diff --git a/src/server/v2.0/route/legacy.go b/src/server/v2.0/route/legacy.go index 40d61f7b3..e8894a8a2 100755 --- a/src/server/v2.0/route/legacy.go +++ b/src/server/v2.0/route/legacy.go @@ -32,10 +32,6 @@ func registerLegacyRoutes() { beego.Router("/api/"+version+"/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/"+version+"/users/:id/cli_secret", &api.UserAPI{}, "put:SetCLISecret") beego.Router("/api/"+version+"/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{}) - beego.Router("/api/"+version+"/ldap/ping", &api.LdapAPI{}, "post:Ping") - beego.Router("/api/"+version+"/ldap/users/search", &api.LdapAPI{}, "get:Search") - beego.Router("/api/"+version+"/ldap/groups/search", &api.LdapAPI{}, "get:SearchGroup") - beego.Router("/api/"+version+"/ldap/users/import", &api.LdapAPI{}, "post:ImportUser") beego.Router("/api/"+version+"/email/ping", &api.EmailAPI{}, "post:Ping") beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth") beego.Router("/api/"+version+"/search", &api.SearchAPI{}) diff --git a/src/testing/pkg/ldap/manager.go b/src/testing/pkg/ldap/manager.go new file mode 100644 index 000000000..991cf5ee7 --- /dev/null +++ b/src/testing/pkg/ldap/manager.go @@ -0,0 +1,107 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package ldap + +import ( + context "context" + + ldap "github.com/goharbor/harbor/src/pkg/ldap" + mock "github.com/stretchr/testify/mock" + + model "github.com/goharbor/harbor/src/pkg/ldap/model" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// ImportUser provides a mock function with given fields: ctx, sess, ldapImportUsers +func (_m *Manager) ImportUser(ctx context.Context, sess *ldap.Session, ldapImportUsers []string) ([]model.FailedImportUser, error) { + ret := _m.Called(ctx, sess, ldapImportUsers) + + var r0 []model.FailedImportUser + if rf, ok := ret.Get(0).(func(context.Context, *ldap.Session, []string) []model.FailedImportUser); ok { + r0 = rf(ctx, sess, ldapImportUsers) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.FailedImportUser) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *ldap.Session, []string) error); ok { + r1 = rf(ctx, sess, ldapImportUsers) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Ping provides a mock function with given fields: ctx, cfg +func (_m *Manager) Ping(ctx context.Context, cfg model.LdapConf) (bool, error) { + ret := _m.Called(ctx, cfg) + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, model.LdapConf) bool); ok { + r0 = rf(ctx, cfg) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, model.LdapConf) error); ok { + r1 = rf(ctx, cfg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SearchGroup provides a mock function with given fields: ctx, sess, groupName, groupDN +func (_m *Manager) SearchGroup(ctx context.Context, sess *ldap.Session, groupName string, groupDN string) ([]model.Group, error) { + ret := _m.Called(ctx, sess, groupName, groupDN) + + var r0 []model.Group + if rf, ok := ret.Get(0).(func(context.Context, *ldap.Session, string, string) []model.Group); ok { + r0 = rf(ctx, sess, groupName, groupDN) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.Group) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *ldap.Session, string, string) error); ok { + r1 = rf(ctx, sess, groupName, groupDN) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SearchUser provides a mock function with given fields: ctx, sess, username +func (_m *Manager) SearchUser(ctx context.Context, sess *ldap.Session, username string) ([]model.User, error) { + ret := _m.Called(ctx, sess, username) + + var r0 []model.User + if rf, ok := ret.Get(0).(func(context.Context, *ldap.Session, string) []model.User); ok { + r0 = rf(ctx, sess, username) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *ldap.Session, string) error); ok { + r1 = rf(ctx, sess, username) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/src/testing/pkg/pkg.go b/src/testing/pkg/pkg.go index ba1c05023..ebb996560 100644 --- a/src/testing/pkg/pkg.go +++ b/src/testing/pkg/pkg.go @@ -35,3 +35,4 @@ package pkg //go:generate mockery --case snake --dir ../../pkg/robot/dao --name DAO --output ./robot/dao --outpkg dao //go:generate mockery --case snake --dir ../../pkg/repository/dao --name DAO --output ./repository/dao --outpkg dao //go:generate mockery --case snake --dir ../../pkg/immutable/dao --name DAO --output ./immutable/dao --outpkg dao +//go:generate mockery --case snake --dir ../../pkg/ldap --name Manager --output ./ldap --outpkg ldap