Move ldap API to new program model

Fix some issue with the LDAP connection test

Signed-off-by: stonezdj <stonezdj@gmail.com>
This commit is contained in:
stonezdj 2021-02-28 09:07:20 +08:00
parent b181d4df16
commit 5a35b7a9c4
27 changed files with 1618 additions and 1380 deletions

View File

@ -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

View File

@ -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"

View File

@ -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"`
}

View File

@ -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")

View File

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

View File

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

View File

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

View File

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

View File

@ -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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 + ")"
}

View File

@ -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 + ")"
}

515
src/pkg/ldap/ldap_test.go Normal file
View File

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

146
src/pkg/ldap/manager.go Normal file
View File

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

View File

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

View File

@ -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"`
}

View File

@ -45,6 +45,7 @@ func New() http.Handler {
ReplicationAPI: newReplicationAPI(),
SysteminfoAPI: newSystemInfoAPI(),
PingAPI: newPingAPI(),
LdapAPI: newLdapAPI(),
GCAPI: newGCAPI(),
QuotaAPI: newQuotaAPI(),
RetentionAPI: newRetentionAPI(),

View File

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

View File

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

View File

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

View File

@ -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