Add REST API to search LDAP by Group DN

Used to verify and search DN when adding group member to project
This commit is contained in:
stonezdj 2018-07-05 12:21:00 +08:00
parent e1000d5984
commit 2494a7aaab
4 changed files with 216 additions and 28 deletions

View File

@ -2325,14 +2325,18 @@ paths:
summary: Search available ldap groups. summary: Search available ldap groups.
description: > description: >
This endpoint searches the available ldap groups based on related This endpoint searches the available ldap groups based on related
configuration parameters. Support searched by input ladp configuration, configuration parameters. support to search by groupname or groupdn.
load configuration from the system and specific filter.
parameters: parameters:
- name: groupname - name: groupname
in: query in: query
type: string type: string
required: false required: false
description: Ldap group name description: Ldap group name
- name: groupdn
in: query
type: string
required: false
description: The LDAP group DN
tags: tags:
- Products - Products
responses: responses:
@ -2342,6 +2346,10 @@ paths:
type: array type: array
items: items:
$ref: '#/definitions/UserGroup' $ref: '#/definitions/UserGroup'
"400":
description: The Ldap group DN is invalid.
'404':
description: No ldap group found.
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
/ldap/users/search: /ldap/users/search:
@ -2372,6 +2380,7 @@ paths:
description: Only admin has this authority. description: Only admin has this authority.
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
/ldap/users/import: /ldap/users/import:
post: post:
summary: Import selected available ldap users. summary: Import selected available ldap users.
@ -3650,6 +3659,7 @@ definitions:
ldap_group_dn: ldap_group_dn:
type: string type: string
description: The DN of the LDAP group if group type is 1 (LDAP group). description: The DN of the LDAP group if group type is 1 (LDAP group).
Resource: Resource:
type: object type: object
properties: properties:

View File

@ -30,6 +30,7 @@ import (
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils" "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/config"
"github.com/vmware/harbor/src/ui/filter" "github.com/vmware/harbor/src/ui/filter"
"github.com/vmware/harbor/tests/apitests/apilib" "github.com/vmware/harbor/tests/apitests/apilib"
@ -77,6 +78,25 @@ type usrInfo struct {
} }
func init() { 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 { if err := config.Init(); err != nil {
log.Fatalf("failed to initialize configurations: %v", err) 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", &SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo") beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo")
beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert") 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", &ConfigAPI{})
beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset") beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset")
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")

View File

@ -21,11 +21,15 @@ import (
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/auth" "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 // LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import
type LdapAPI struct { type LdapAPI struct {
BaseController BaseController
ldapConfig *ldapUtils.Session
useTestConfig bool // Only used for unit test
} }
const ( const (
@ -47,6 +51,14 @@ func (l *LdapAPI) Prepare() {
l.HandleForbidden(l.SecurityCtx.GetUsername()) l.HandleForbidden(l.SecurityCtx.GetUsername())
return 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 ... // Ping ...
@ -55,16 +67,11 @@ func (l *LdapAPI) Ping() {
LdapConnectionTimeout: 5, LdapConnectionTimeout: 5,
} }
var err error var err error
var ldapSession *ldapUtils.Session
l.Ctx.Input.CopyBody(1 << 32) l.Ctx.Input.CopyBody(1 << 32)
if string(l.Ctx.Input.RequestBody) == "" { if string(l.Ctx.Input.RequestBody) == "" {
ldapSession, err = ldapUtils.LoadSystemLdapConfig() ldapSession := *l.ldapConfig
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("Can't load system configuration, error: %v", err))
return
}
err = ldapSession.ConnectionTest() err = ldapSession.ConnectionTest()
} else { } else {
l.DecodeJSONReqAndValidate(&ldapConfs) l.DecodeJSONReqAndValidate(&ldapConfs)
@ -81,7 +88,7 @@ func (l *LdapAPI) Ping() {
func (l *LdapAPI) Search() { func (l *LdapAPI) Search() {
var err error var err error
var ldapUsers []models.LdapUser var ldapUsers []models.LdapUser
ldapSession, err := ldapUtils.LoadSystemLdapConfig() ldapSession := *l.ldapConfig
if err = ldapSession.Open(); err != nil { if err = ldapSession.Open(); err != nil {
l.HandleInternalServerError(fmt.Sprintf("Can't Open LDAP session, error: %v", err)) l.HandleInternalServerError(fmt.Sprintf("Can't Open LDAP session, error: %v", err))
return return
@ -106,11 +113,10 @@ func (l *LdapAPI) Search() {
func (l *LdapAPI) ImportUser() { func (l *LdapAPI) ImportUser() {
var ldapImportUsers models.LdapImportUser var ldapImportUsers models.LdapImportUser
var ldapFailedImportUsers []models.LdapFailedImportUser var ldapFailedImportUsers []models.LdapFailedImportUser
var ldapConfs models.LdapConf
l.DecodeJSONReqAndValidate(&ldapImportUsers) l.DecodeJSONReqAndValidate(&ldapImportUsers)
ldapFailedImportUsers, err := importUsers(ldapConfs, ldapImportUsers.LdapUIDList) ldapFailedImportUsers, err := importUsers(ldapImportUsers.LdapUIDList, l.ldapConfig)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("LDAP import user fail, error: %v", err)) 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 failedImportUser []models.LdapFailedImportUser
var u models.LdapFailedImportUser var u models.LdapFailedImportUser
ldapSession, err := ldapUtils.LoadSystemLdapConfig() ldapSession := *ldapConfig
if err != nil { if err := ldapSession.Open(); err != nil {
log.Errorf("Can't load system configuration, error: %v", err)
return nil, err
}
if err = ldapSession.Open(); err != nil {
log.Errorf("Can't connect to LDAP, error: %v", err) log.Errorf("Can't connect to LDAP, error: %v", err)
} }
defer ldapSession.Close() defer ldapSession.Close()
@ -194,19 +195,37 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models.
// SearchGroup ... Search LDAP by groupname // SearchGroup ... Search LDAP by groupname
func (l *LdapAPI) SearchGroup() { func (l *LdapAPI) SearchGroup() {
var ldapGroups []models.LdapGroup
var err error
searchName := l.GetString("groupname") searchName := l.GetString("groupname")
ldapSession, err := ldapUtils.LoadSystemLdapConfig() groupDN := l.GetString("groupdn")
if err != nil { ldapSession := *l.ldapConfig
l.HandleInternalServerError(fmt.Sprintf("Can't get LDAP system config, error: %v", err))
return
}
ldapSession.Open() ldapSession.Open()
defer ldapSession.Close() defer ldapSession.Close()
ldapGroups, err := ldapSession.SearchGroupByName(searchName)
//Search LDAP group by groupName or group DN
if len(searchName) > 0 {
ldapGroups, err = ldapSession.SearchGroupByName(searchName)
if err != nil { if err != nil {
l.HandleInternalServerError(fmt.Sprintf("Can't search LDAP group by name, error: %v", err)) l.HandleInternalServerError(fmt.Sprintf("Can't search LDAP group by name, error: %v", err))
return 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 l.Data["json"] = ldapGroups
l.ServeJSON() l.ServeJSON()
} }

136
src/ui/api/ldap_test.go Normal file
View File

@ -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...)
}