From 2494a7aaab5f24d322fe743e9637d7492ed8b831 Mon Sep 17 00:00:00 2001 From: stonezdj Date: Thu, 5 Jul 2018 12:21:00 +0800 Subject: [PATCH] Add REST API to search LDAP by Group DN Used to verify and search DN when adding group member to project --- docs/swagger.yaml | 14 +++- src/ui/api/harborapi_test.go | 25 ++++++- src/ui/api/ldap.go | 69 +++++++++++------- src/ui/api/ldap_test.go | 136 +++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 src/ui/api/ldap_test.go diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ba1d9c0b8..6bd684f8e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2325,14 +2325,18 @@ paths: summary: Search available ldap groups. description: > This endpoint searches the available ldap groups based on related - configuration parameters. Support searched by input ladp configuration, - load configuration from the system and specific filter. + 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: @@ -2342,6 +2346,10 @@ paths: 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: @@ -2372,6 +2380,7 @@ paths: description: Only admin has this authority. '500': description: Unexpected internal errors. + /ldap/users/import: post: summary: Import selected available ldap users. @@ -3650,6 +3659,7 @@ definitions: ldap_group_dn: type: string description: The DN of the LDAP group if group type is 1 (LDAP group). + Resource: type: object properties: diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index 7ac4e4f1b..691181d8c 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -30,6 +30,7 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils" + ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" "github.com/vmware/harbor/src/ui/config" "github.com/vmware/harbor/src/ui/filter" "github.com/vmware/harbor/tests/apitests/apilib" @@ -77,6 +78,25 @@ type usrInfo struct { } func init() { + ldapConfig := models.LdapConf{ + LdapURL: "ldap://127.0.0.1:389", + LdapSearchDn: "cn=admin,dc=example,dc=com", + LdapSearchPassword: "admin", + LdapBaseDn: "dc=example,dc=com", + LdapUID: "cn", + LdapScope: 2, + LdapConnectionTimeout: 5, + } + ldapGroupConfig := models.LdapGroupConf{ + LdapGroupBaseDN: "ou=groups,dc=example,dc=com", + LdapGroupFilter: "objectclass=groupOfNames", + LdapGroupSearchScope: 2, + LdapGroupNameAttribute: "cn", + } + ldapTestConfig, err := ldapUtils.CreateWithAllConfig(ldapConfig, ldapGroupConfig) + if err != nil { + log.Fatalf("failed to initialize configurations: %v", err) + } if err := config.Init(); err != nil { log.Fatalf("failed to initialize configurations: %v", err) } @@ -134,7 +154,10 @@ func init() { beego.Router("/api/systeminfo", &SystemInfoAPI{}, "get:GetGeneralInfo") beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo") beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert") - beego.Router("/api/ldap/ping", &LdapAPI{}, "post:Ping") + beego.Router("/api/ldap/ping", &LdapAPI{ldapConfig: ldapTestConfig, useTestConfig: true}, "post:Ping") + beego.Router("/api/ldap/users/search", &LdapAPI{ldapConfig: ldapTestConfig, useTestConfig: true}, "get:Search") + beego.Router("/api/ldap/groups/search", &LdapAPI{ldapConfig: ldapTestConfig, useTestConfig: true}, "get:SearchGroup") + beego.Router("/api/ldap/users/import", &LdapAPI{ldapConfig: ldapTestConfig, useTestConfig: true}, "post:ImportUser") beego.Router("/api/configurations", &ConfigAPI{}) beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset") beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") diff --git a/src/ui/api/ldap.go b/src/ui/api/ldap.go index 7f4412d4f..cd35feca1 100644 --- a/src/ui/api/ldap.go +++ b/src/ui/api/ldap.go @@ -21,11 +21,15 @@ import ( ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/ui/auth" + + goldap "gopkg.in/ldap.v2" ) // LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import type LdapAPI struct { BaseController + ldapConfig *ldapUtils.Session + useTestConfig bool // Only used for unit test } const ( @@ -47,6 +51,14 @@ func (l *LdapAPI) Prepare() { l.HandleForbidden(l.SecurityCtx.GetUsername()) return } + if l.useTestConfig { + ldapCfg, err := ldapUtils.LoadSystemLdapConfig() + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("Can't load system configuration, error: %v", err)) + return + } + l.ldapConfig = ldapCfg + } } // Ping ... @@ -55,16 +67,11 @@ func (l *LdapAPI) Ping() { LdapConnectionTimeout: 5, } var err error - var ldapSession *ldapUtils.Session l.Ctx.Input.CopyBody(1 << 32) if string(l.Ctx.Input.RequestBody) == "" { - ldapSession, err = ldapUtils.LoadSystemLdapConfig() - if err != nil { - l.HandleInternalServerError(fmt.Sprintf("Can't load system configuration, error: %v", err)) - return - } + ldapSession := *l.ldapConfig err = ldapSession.ConnectionTest() } else { l.DecodeJSONReqAndValidate(&ldapConfs) @@ -81,7 +88,7 @@ func (l *LdapAPI) Ping() { func (l *LdapAPI) Search() { var err error var ldapUsers []models.LdapUser - ldapSession, err := ldapUtils.LoadSystemLdapConfig() + ldapSession := *l.ldapConfig if err = ldapSession.Open(); err != nil { l.HandleInternalServerError(fmt.Sprintf("Can't Open LDAP session, error: %v", err)) return @@ -106,11 +113,10 @@ func (l *LdapAPI) Search() { func (l *LdapAPI) ImportUser() { var ldapImportUsers models.LdapImportUser var ldapFailedImportUsers []models.LdapFailedImportUser - var ldapConfs models.LdapConf l.DecodeJSONReqAndValidate(&ldapImportUsers) - ldapFailedImportUsers, err := importUsers(ldapConfs, ldapImportUsers.LdapUIDList) + ldapFailedImportUsers, err := importUsers(ldapImportUsers.LdapUIDList, l.ldapConfig) if err != nil { l.HandleInternalServerError(fmt.Sprintf("LDAP import user fail, error: %v", err)) @@ -127,17 +133,12 @@ func (l *LdapAPI) ImportUser() { } -func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models.LdapFailedImportUser, error) { +func importUsers(ldapImportUsers []string, ldapConfig *ldapUtils.Session) ([]models.LdapFailedImportUser, error) { var failedImportUser []models.LdapFailedImportUser var u models.LdapFailedImportUser - ldapSession, err := ldapUtils.LoadSystemLdapConfig() - if err != nil { - log.Errorf("Can't load system configuration, error: %v", err) - return nil, err - } - - if err = ldapSession.Open(); err != nil { + ldapSession := *ldapConfig + if err := ldapSession.Open(); err != nil { log.Errorf("Can't connect to LDAP, error: %v", err) } defer ldapSession.Close() @@ -194,17 +195,35 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. // SearchGroup ... Search LDAP by groupname func (l *LdapAPI) SearchGroup() { + var ldapGroups []models.LdapGroup + var err error searchName := l.GetString("groupname") - ldapSession, err := ldapUtils.LoadSystemLdapConfig() - if err != nil { - l.HandleInternalServerError(fmt.Sprintf("Can't get LDAP system config, error: %v", err)) - return - } + groupDN := l.GetString("groupdn") + ldapSession := *l.ldapConfig ldapSession.Open() defer ldapSession.Close() - ldapGroups, err := ldapSession.SearchGroupByName(searchName) - if err != nil { - l.HandleInternalServerError(fmt.Sprintf("Can't search LDAP group by name, error: %v", err)) + + //Search LDAP group by groupName or group DN + if len(searchName) > 0 { + ldapGroups, err = ldapSession.SearchGroupByName(searchName) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("Can't search LDAP group by name, error: %v", err)) + return + } + } else if len(groupDN) > 0 { + if _, err := goldap.ParseDN(groupDN); err != nil { + l.HandleBadRequest(fmt.Sprintf("Invalid DN: %v", err)) + return + } + ldapGroups, err = ldapSession.SearchGroupByDN(groupDN) + if err != nil { + //OpenLDAP usually return an error if DN is not found + l.HandleNotFound(fmt.Sprintf("Search LDAP group fail, error: %v", err)) + return + } + } + if len(ldapGroups) == 0 { + l.HandleNotFound("No ldap group found") return } l.Data["json"] = ldapGroups diff --git a/src/ui/api/ldap_test.go b/src/ui/api/ldap_test.go new file mode 100644 index 000000000..b9dd6b6cd --- /dev/null +++ b/src/ui/api/ldap_test.go @@ -0,0 +1,136 @@ +package api + +import ( + "net/http" + "testing" + + "github.com/vmware/harbor/src/common/models" +) + +func TestLDAPPing(t *testing.T) { + cases := []*codeCheckingCase{ + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/ldap/ping", + }, + code: http.StatusUnauthorized, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/ldap/ping", + credential: admin, + }, + code: http.StatusOK, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/ldap/ping", + bodyJSON: &models.LdapConf{ + LdapURL: "ldap://127.0.0.1:389", + LdapSearchDn: "cn=admin,dc=example,dc=com", + LdapSearchPassword: "admin", + LdapBaseDn: "dc=example,dc=com", + LdapUID: "cn", + LdapScope: 2, + LdapConnectionTimeout: 5, + }, + credential: admin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestLDAPUserSearch(t *testing.T) { + cases := []*codeCheckingCase{ + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/ldap/users/search?username=mike", + }, + code: http.StatusUnauthorized, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/ldap/users/search?username=mike", + credential: admin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestLDAPGroupSearch(t *testing.T) { + cases := []*codeCheckingCase{ + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/ldap/groups/search?groupname=harbor_users", + }, + code: http.StatusUnauthorized, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/ldap/groups/search?groupname=harbor_users", + credential: admin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestLDAPGroupSearchWithDN(t *testing.T) { + cases := []*codeCheckingCase{ + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/ldap/groups/search?groupdn=cn=harbor_users,ou=groups,dc=example,dc=com", + }, + code: http.StatusUnauthorized, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/ldap/groups/search?groupname=cn=harbor_users,ou=groups,dc=example,dc=com", + credential: admin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestLDAPImportUser(t *testing.T) { + cases := []*codeCheckingCase{ + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/ldap/users/import", + bodyJSON: &models.LdapImportUser{ + LdapUIDList: []string{"mike", "mike02"}, + }, + }, + code: http.StatusUnauthorized, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/ldap/users/import", + bodyJSON: &models.LdapImportUser{ + LdapUIDList: []string{"mike", "mike02"}, + }, + credential: admin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +}