mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
Merge pull request #14351 from stonezdj/21feb26_ldap_refact
Move ldap API to new program model
This commit is contained in:
commit
035caad146
@ -1310,120 +1310,6 @@ paths:
|
|||||||
description: No registry found.
|
description: No registry found.
|
||||||
'500':
|
'500':
|
||||||
description: Unexpected internal errors.
|
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:
|
/usergroups:
|
||||||
get:
|
get:
|
||||||
summary: Get all user groups information
|
summary: Get all user groups information
|
||||||
|
@ -19,6 +19,129 @@ security:
|
|||||||
- basic: []
|
- basic: []
|
||||||
- {}
|
- {}
|
||||||
paths:
|
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:
|
/projects:
|
||||||
get:
|
get:
|
||||||
summary: List projects
|
summary: List projects
|
||||||
@ -5207,3 +5330,72 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
extras:
|
extras:
|
||||||
type: string
|
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"
|
||||||
|
@ -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"`
|
|
||||||
}
|
|
@ -41,6 +41,7 @@ const (
|
|||||||
ResourceHelmChartVersionLabel = Resource("helm-chart-version-label")
|
ResourceHelmChartVersionLabel = Resource("helm-chart-version-label")
|
||||||
ResourceLabel = Resource("label")
|
ResourceLabel = Resource("label")
|
||||||
ResourceLog = Resource("log")
|
ResourceLog = Resource("log")
|
||||||
|
ResourceLdapUser = Resource("ldap-user")
|
||||||
ResourceMember = Resource("member")
|
ResourceMember = Resource("member")
|
||||||
ResourceMetadata = Resource("metadata")
|
ResourceMetadata = Resource("metadata")
|
||||||
ResourceQuota = Resource("quota")
|
ResourceQuota = Resource("quota")
|
||||||
|
@ -63,5 +63,7 @@ var (
|
|||||||
|
|
||||||
{Resource: rbac.ResourceOIDCEndpoint, Action: rbac.ActionUpdate},
|
{Resource: rbac.ResourceOIDCEndpoint, Action: rbac.ActionUpdate},
|
||||||
{Resource: rbac.ResourceOIDCEndpoint, Action: rbac.ActionRead},
|
{Resource: rbac.ResourceOIDCEndpoint, Action: rbac.ActionRead},
|
||||||
|
{Resource: rbac.ResourceLdapUser, Action: rbac.ActionCreate},
|
||||||
|
{Resource: rbac.ResourceLdapUser, Action: rbac.ActionList},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
129
src/controller/ldap/controller.go
Normal file
129
src/controller/ldap/controller.go
Normal 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)
|
||||||
|
}
|
146
src/controller/ldap/controller_test.go
Normal file
146
src/controller/ldap/controller_test.go
Normal 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{})
|
||||||
|
}
|
@ -112,10 +112,6 @@ func init() {
|
|||||||
beego.Router("/api/registries", &RegistryAPI{}, "get:List;post:Post")
|
beego.Router("/api/registries", &RegistryAPI{}, "get:List;post:Post")
|
||||||
beego.Router("/api/registries/ping", &RegistryAPI{}, "post:Ping")
|
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/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/configurations", &ConfigAPI{})
|
||||||
beego.Router("/api/configs", &ConfigAPI{}, "get:GetInternalConfig")
|
beego.Router("/api/configs", &ConfigAPI{}, "get:GetInternalConfig")
|
||||||
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
||||||
|
@ -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()
|
|
||||||
}
|
|
@ -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())
|
|
||||||
}
|
|
@ -26,10 +26,10 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/common/dao/group"
|
"github.com/goharbor/harbor/src/common/dao/group"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"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/auth"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/ldap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserGroupAPI ...
|
// UserGroupAPI ...
|
||||||
|
@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils/test"
|
"github.com/goharbor/harbor/src/common/utils/test"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"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/auth"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
coreConfig "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")
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -15,7 +15,9 @@
|
|||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/ldap/model"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -26,7 +28,9 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"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/auth"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"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.")
|
log.Debugf("LDAP authentication failed for empty user id.")
|
||||||
return nil, auth.NewErrAuth("Empty user id")
|
return nil, auth.NewErrAuth("Empty user id")
|
||||||
}
|
}
|
||||||
|
ldapSession, err := ldapCtl.Ctl.Session(context.Background())
|
||||||
ldapSession, err := ldapUtils.LoadSystemLdapConfig()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can not load system ldap config: %v", err)
|
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
|
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.
|
// Retrieve ldap related info in login to avoid too many traffic with LDAP server.
|
||||||
// Get group admin dn
|
// Get group admin dn
|
||||||
groupCfg, err := config.LDAPGroupConf()
|
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)
|
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
|
// 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
|
// Attach user group
|
||||||
for _, groupDN := range ldapUsers[0].GroupDNList {
|
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)
|
log.Warningf("Can not get the ldap group name with DN %v", dn)
|
||||||
continue
|
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)
|
u.GroupIDs, err = group.PopulateGroup(userGroups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -159,24 +161,27 @@ func (l *Auth) OnBoardUser(u *models.User) error {
|
|||||||
// SearchUser -- Search user in ldap
|
// SearchUser -- Search user in ldap
|
||||||
func (l *Auth) SearchUser(username string) (*models.User, error) {
|
func (l *Auth) SearchUser(username string) (*models.User, error) {
|
||||||
var user models.User
|
var user models.User
|
||||||
ldapSession, err := ldapUtils.LoadSystemLdapConfig()
|
s, err := ldapCtl.Ctl.Session(context.Background())
|
||||||
if err = ldapSession.Open(); err != nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = s.Open(); err != nil {
|
||||||
return nil, fmt.Errorf("Failed to load system ldap config, %v", err)
|
return nil, fmt.Errorf("Failed to load system ldap config, %v", err)
|
||||||
}
|
}
|
||||||
defer ldapSession.Close()
|
defer s.Close()
|
||||||
ldapUsers, err := ldapSession.SearchUser(username)
|
lUsers, err := s.SearchUser(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to search user in ldap")
|
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")
|
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.Username = strings.TrimSpace(lUsers[0].Username)
|
||||||
user.Realname = strings.TrimSpace(ldapUsers[0].Realname)
|
user.Realname = strings.TrimSpace(lUsers[0].Realname)
|
||||||
user.Email = strings.TrimSpace(ldapUsers[0].Email)
|
user.Email = strings.TrimSpace(lUsers[0].Email)
|
||||||
|
|
||||||
log.Debugf("Found ldap user %v", user)
|
log.Debugf("Found ldap user %v", user)
|
||||||
} else {
|
} else {
|
||||||
@ -191,18 +196,18 @@ func (l *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) {
|
|||||||
if _, err := goldap.ParseDN(groupKey); err != nil {
|
if _, err := goldap.ParseDN(groupKey); err != nil {
|
||||||
return nil, auth.ErrInvalidLDAPGroupDN
|
return nil, auth.ErrInvalidLDAPGroupDN
|
||||||
}
|
}
|
||||||
ldapSession, err := ldapUtils.LoadSystemLdapConfig()
|
s, err := ldapCtl.Ctl.Session(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can not load system ldap config: %v", err)
|
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)
|
log.Warningf("ldap connection fail: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer ldapSession.Close()
|
defer s.Close()
|
||||||
userGroupList, err := ldapSession.SearchGroupByDN(groupKey)
|
userGroupList, err := s.SearchGroupByDN(groupKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("ldap search group fail: %v", err)
|
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)
|
return nil, fmt.Errorf("Failed to searh ldap group with groupDN:%v", groupKey)
|
||||||
}
|
}
|
||||||
userGroup := models.UserGroup{
|
userGroup := models.UserGroup{
|
||||||
GroupName: userGroupList[0].GroupName,
|
GroupName: userGroupList[0].Name,
|
||||||
LdapGroupDN: userGroupList[0].GroupDN,
|
LdapGroupDN: userGroupList[0].Dn,
|
||||||
}
|
}
|
||||||
return &userGroup, nil
|
return &userGroup, nil
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/secret"
|
"github.com/goharbor/harbor/src/common/secret"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/ldap/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -132,37 +133,37 @@ func TokenPrivateKeyPath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LDAPConf returns the setting of ldap server
|
// LDAPConf returns the setting of ldap server
|
||||||
func LDAPConf() (*models.LdapConf, error) {
|
func LDAPConf() (*model.LdapConf, error) {
|
||||||
err := cfgMgr.Load()
|
err := cfgMgr.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &models.LdapConf{
|
return &model.LdapConf{
|
||||||
LdapURL: cfgMgr.Get(common.LDAPURL).GetString(),
|
URL: cfgMgr.Get(common.LDAPURL).GetString(),
|
||||||
LdapSearchDn: cfgMgr.Get(common.LDAPSearchDN).GetString(),
|
SearchDn: cfgMgr.Get(common.LDAPSearchDN).GetString(),
|
||||||
LdapSearchPassword: cfgMgr.Get(common.LDAPSearchPwd).GetString(),
|
SearchPassword: cfgMgr.Get(common.LDAPSearchPwd).GetString(),
|
||||||
LdapBaseDn: cfgMgr.Get(common.LDAPBaseDN).GetString(),
|
BaseDn: cfgMgr.Get(common.LDAPBaseDN).GetString(),
|
||||||
LdapUID: cfgMgr.Get(common.LDAPUID).GetString(),
|
UID: cfgMgr.Get(common.LDAPUID).GetString(),
|
||||||
LdapFilter: cfgMgr.Get(common.LDAPFilter).GetString(),
|
Filter: cfgMgr.Get(common.LDAPFilter).GetString(),
|
||||||
LdapScope: cfgMgr.Get(common.LDAPScope).GetInt(),
|
Scope: cfgMgr.Get(common.LDAPScope).GetInt(),
|
||||||
LdapConnectionTimeout: cfgMgr.Get(common.LDAPTimeout).GetInt(),
|
ConnectionTimeout: cfgMgr.Get(common.LDAPTimeout).GetInt(),
|
||||||
LdapVerifyCert: cfgMgr.Get(common.LDAPVerifyCert).GetBool(),
|
VerifyCert: cfgMgr.Get(common.LDAPVerifyCert).GetBool(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAPGroupConf returns the setting of ldap group search
|
// LDAPGroupConf returns the setting of ldap group search
|
||||||
func LDAPGroupConf() (*models.LdapGroupConf, error) {
|
func LDAPGroupConf() (*model.GroupConf, error) {
|
||||||
err := cfgMgr.Load()
|
err := cfgMgr.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &models.LdapGroupConf{
|
return &model.GroupConf{
|
||||||
LdapGroupBaseDN: cfgMgr.Get(common.LDAPGroupBaseDN).GetString(),
|
BaseDN: cfgMgr.Get(common.LDAPGroupBaseDN).GetString(),
|
||||||
LdapGroupFilter: cfgMgr.Get(common.LDAPGroupSearchFilter).GetString(),
|
Filter: cfgMgr.Get(common.LDAPGroupSearchFilter).GetString(),
|
||||||
LdapGroupNameAttribute: cfgMgr.Get(common.LDAPGroupAttributeName).GetString(),
|
NameAttribute: cfgMgr.Get(common.LDAPGroupAttributeName).GetString(),
|
||||||
LdapGroupSearchScope: cfgMgr.Get(common.LDAPGroupSearchScope).GetInt(),
|
SearchScope: cfgMgr.Get(common.LDAPGroupSearchScope).GetInt(),
|
||||||
LdapGroupAdminDN: cfgMgr.Get(common.LDAPGroupAdminDn).GetString(),
|
AdminDN: cfgMgr.Get(common.LDAPGroupAdminDn).GetString(),
|
||||||
LdapGroupMembershipAttribute: cfgMgr.Get(common.LDAPGroupMembershipAttribute).GetString(),
|
MembershipAttribute: cfgMgr.Get(common.LDAPGroupMembershipAttribute).GetString(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,3 +74,15 @@ func NewFilterBuilder(filter string) (*FilterBuilder, error) {
|
|||||||
}
|
}
|
||||||
return &FilterBuilder{packet: p}, nil
|
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 + ")"
|
||||||
|
}
|
@ -18,73 +18,53 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/ldap/model"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
goldap "github.com/go-ldap/ldap/v3"
|
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"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotFound ...
|
// ErrNotFound ...
|
||||||
var ErrNotFound = errors.New("entity not found")
|
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 ...
|
// ErrDNSyntax ...
|
||||||
var ErrDNSyntax = errors.New("invalid DN syntax")
|
var ErrDNSyntax = errors.New("invalid DN syntax")
|
||||||
|
|
||||||
// ErrInvalidFilter ...
|
// ErrInvalidFilter ...
|
||||||
var ErrInvalidFilter = errors.New("invalid filter syntax")
|
var ErrInvalidFilter = errors.New("invalid filter syntax")
|
||||||
|
|
||||||
|
// ErrEmptyBaseDN ...
|
||||||
|
var ErrEmptyBaseDN = errors.New("empty base dn")
|
||||||
|
|
||||||
// Session - define a LDAP session
|
// Session - define a LDAP session
|
||||||
type Session struct {
|
type Session struct {
|
||||||
ldapConfig models.LdapConf
|
basicCfg model.LdapConf
|
||||||
ldapGroupConfig models.LdapGroupConf
|
groupCfg model.GroupConf
|
||||||
ldapConn *goldap.Conn
|
ldapConn *goldap.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadSystemLdapConfig - load LDAP configure
|
// NewSession create session with configs
|
||||||
func LoadSystemLdapConfig() (*Session, error) {
|
func NewSession(basicCfg model.LdapConf, groupCfg model.GroupConf) *Session {
|
||||||
|
return &Session{
|
||||||
ldapConf, err := config.LDAPConf()
|
basicCfg: basicCfg,
|
||||||
|
groupCfg: groupCfg,
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
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
|
// TestConfig - test ldap session connection, out of the scope of normal session create/close
|
||||||
func (session *Session) ConnectionTest() error {
|
func TestConfig(ldapConfig model.LdapConf) (bool, error) {
|
||||||
session, err := LoadSystemLdapConfig()
|
ts := NewSession(ldapConfig, model.GroupConf{})
|
||||||
if err != nil {
|
if err := ts.Open(); err != nil {
|
||||||
return fmt.Errorf("Failed to load system ldap config")
|
if goldap.IsErrorWithCode(err, goldap.ErrorNetwork) {
|
||||||
}
|
return false, ErrLDAPServerTimeout
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
return false, ErrLDAPPingFail
|
||||||
ldapConfig.LdapSearchPassword = session.ldapConfig.LdapSearchPassword
|
|
||||||
}
|
}
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
testSession, err := CreateWithAllConfig(ldapConfig, ldapGroupConfig)
|
if ts.basicCfg.SearchDn == "" {
|
||||||
|
return false, ErrEmptyBaseDN
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
err = testSession.Open()
|
if err := ts.Bind(ts.basicCfg.SearchDn, ts.basicCfg.SearchPassword); err != nil {
|
||||||
|
if goldap.IsErrorWithCode(err, goldap.LDAPResultInvalidCredentials) {
|
||||||
if err != nil {
|
return false, ErrInvalidCredential
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer testSession.Close()
|
|
||||||
|
|
||||||
if testSession.ldapConfig.LdapSearchDn != "" {
|
|
||||||
err = testSession.Bind(testSession.ldapConfig.LdapSearchDn, testSession.ldapConfig.LdapSearchPassword)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchUser - search LDAP user by name
|
// SearchUser - search LDAP user by name
|
||||||
func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
|
func (s *Session) SearchUser(username string) ([]model.User, error) {
|
||||||
var ldapUsers []models.LdapUser
|
var ldapUsers []model.User
|
||||||
ldapFilter, err := createUserSearchFilter(session.ldapConfig.LdapFilter, session.ldapConfig.LdapUID, username)
|
ldapFilter, err := createUserSearchFilter(s.basicCfg.Filter, s.basicCfg.UID, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := session.SearchLdap(ldapFilter)
|
result, err := s.SearchLdap(ldapFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ldapEntry := range result.Entries {
|
for _, ldapEntry := range result.Entries {
|
||||||
var u models.LdapUser
|
var u model.User
|
||||||
groupDNList := []string{}
|
groupDNList := make([]string, 0)
|
||||||
groupAttr := strings.ToLower(session.ldapGroupConfig.LdapGroupMembershipAttribute)
|
groupAttr := strings.ToLower(s.groupCfg.MembershipAttribute)
|
||||||
for _, attr := range ldapEntry.Attributes {
|
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])
|
val := strings.TrimSpace(attr.Values[0])
|
||||||
log.Debugf("Current ldap entry attr name: %s\n", attr.Name)
|
log.Debugf("Current ldap entry attr name: %s\n", attr.Name)
|
||||||
switch strings.ToLower(attr.Name) {
|
switch strings.ToLower(attr.Name) {
|
||||||
case strings.ToLower(session.ldapConfig.LdapUID):
|
case strings.ToLower(s.basicCfg.UID):
|
||||||
u.Username = val
|
u.Username = val
|
||||||
case "uid":
|
case "uid":
|
||||||
u.Realname = val
|
u.Realname = val
|
||||||
@ -225,10 +174,8 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
|
|||||||
}
|
}
|
||||||
u.GroupDNList = groupDNList
|
u.GroupDNList = groupDNList
|
||||||
}
|
}
|
||||||
|
|
||||||
u.DN = ldapEntry.DN
|
u.DN = ldapEntry.DN
|
||||||
ldapUsers = append(ldapUsers, u)
|
ldapUsers = append(ldapUsers, u)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ldapUsers, nil
|
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
|
// Bind with specified DN and password, used in authentication
|
||||||
func (session *Session) Bind(dn string, password string) error {
|
func (s *Session) Bind(dn string, password string) error {
|
||||||
return session.ldapConn.Bind(dn, password)
|
return s.ldapConn.Bind(dn, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open - open Session, should invoke Close for each Open call
|
// Open - open Session, should invoke Close for each Open call
|
||||||
func (session *Session) Open() error {
|
func (s *Session) Open() error {
|
||||||
|
ldapURL, err := formatURL(s.basicCfg.URL)
|
||||||
splitLdapURL := strings.Split(session.ldapConfig.LdapURL, "://")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
splitLdapURL := strings.Split(ldapURL, "://")
|
||||||
protocol, hostport := splitLdapURL[0], splitLdapURL[1]
|
protocol, hostport := splitLdapURL[0], splitLdapURL[1]
|
||||||
host := strings.Split(hostport, ":")[0]
|
host := strings.Split(hostport, ":")[0]
|
||||||
|
|
||||||
connectionTimeout := session.ldapConfig.LdapConnectionTimeout
|
connectionTimeout := s.basicCfg.ConnectionTimeout
|
||||||
goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second
|
goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second
|
||||||
|
|
||||||
switch protocol {
|
switch protocol {
|
||||||
@ -256,14 +206,14 @@ func (session *Session) Open() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
session.ldapConn = ldap
|
s.ldapConn = ldap
|
||||||
case "ldaps":
|
case "ldaps":
|
||||||
log.Debug("Start to dial 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
session.ldapConn = ldap
|
s.ldapConn = ldap
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -271,27 +221,27 @@ func (session *Session) Open() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SearchLdap to search ldap with the provide filter
|
// 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"}
|
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" {
|
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
|
// Add the Group membership attribute
|
||||||
groupAttr := strings.TrimSpace(session.ldapGroupConfig.LdapGroupMembershipAttribute)
|
groupAttr := strings.TrimSpace(s.groupCfg.MembershipAttribute)
|
||||||
log.Debugf("Membership attribute: %s\n", groupAttr)
|
log.Debugf("Membership attribute: %s\n", groupAttr)
|
||||||
attributes = append(attributes, 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
|
// 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 {
|
if err := s.Bind(s.basicCfg.SearchDn, s.basicCfg.SearchPassword); err != nil {
|
||||||
return nil, fmt.Errorf("Can not bind search dn, error: %v", err)
|
return nil, fmt.Errorf("can not bind search dn, error: %v", err)
|
||||||
}
|
}
|
||||||
filter = normalizeFilter(filter)
|
filter = normalizeFilter(filter)
|
||||||
if len(filter) == 0 {
|
if len(filter) == 0 {
|
||||||
@ -304,7 +254,7 @@ func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes []
|
|||||||
log.Debugf("Search ldap with filter:%v", filter)
|
log.Debugf("Search ldap with filter:%v", filter)
|
||||||
searchRequest := goldap.NewSearchRequest(
|
searchRequest := goldap.NewSearchRequest(
|
||||||
baseDN,
|
baseDN,
|
||||||
session.ldapConfig.LdapScope,
|
s.basicCfg.Scope,
|
||||||
goldap.NeverDerefAliases,
|
goldap.NeverDerefAliases,
|
||||||
0, // Unlimited results
|
0, // Unlimited results
|
||||||
0, // Search Timeout
|
0, // Search Timeout
|
||||||
@ -314,7 +264,7 @@ func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes []
|
|||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
result, err := session.ldapConn.Search(searchRequest)
|
result, err := s.ldapConn.Search(searchRequest)
|
||||||
if result != nil {
|
if result != nil {
|
||||||
log.Debugf("Found entries:%v\n", len(result.Entries))
|
log.Debugf("Found entries:%v\n", len(result.Entries))
|
||||||
} else {
|
} else {
|
||||||
@ -351,26 +301,26 @@ func createUserSearchFilter(origFilter, ldapUID, username string) (string, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close - close current session
|
// Close - close current session
|
||||||
func (session *Session) Close() {
|
func (s *Session) Close() {
|
||||||
if session.ldapConn != nil {
|
if s.ldapConn != nil {
|
||||||
session.ldapConn.Close()
|
s.ldapConn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchGroupByName ...
|
// SearchGroupByName ...
|
||||||
func (session *Session) SearchGroupByName(groupName string) ([]models.LdapGroup, error) {
|
func (s *Session) SearchGroupByName(groupName string) ([]model.Group, error) {
|
||||||
return session.searchGroup(session.ldapGroupConfig.LdapGroupBaseDN,
|
return s.searchGroup(s.groupCfg.BaseDN,
|
||||||
session.ldapGroupConfig.LdapGroupFilter,
|
s.groupCfg.Filter,
|
||||||
groupName,
|
groupName,
|
||||||
session.ldapGroupConfig.LdapGroupNameAttribute)
|
s.groupCfg.NameAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchGroupByDN ...
|
// 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 {
|
if _, err := goldap.ParseDN(groupDN); err != nil {
|
||||||
return nil, ErrDNSyntax
|
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 {
|
if serverError, ok := err.(*goldap.Error); ok {
|
||||||
log.Debugf("resultCode:%v", serverError.ResultCode)
|
log.Debugf("resultCode:%v", serverError.ResultCode)
|
||||||
}
|
}
|
||||||
@ -380,20 +330,20 @@ func (session *Session) SearchGroupByDN(groupDN string) ([]models.LdapGroup, err
|
|||||||
return groupList, err
|
return groupList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) groupBaseDN() string {
|
func (s *Session) groupBaseDN() string {
|
||||||
if len(session.ldapGroupConfig.LdapGroupBaseDN) == 0 {
|
if len(s.groupCfg.BaseDN) == 0 {
|
||||||
return session.ldapConfig.LdapBaseDn
|
return s.basicCfg.BaseDn
|
||||||
}
|
}
|
||||||
return session.ldapGroupConfig.LdapGroupBaseDN
|
return s.groupCfg.BaseDN
|
||||||
}
|
}
|
||||||
|
|
||||||
// searchGroup -- Given a group DN and filter, search group
|
// searchGroup -- Given a group DN and filter, search group
|
||||||
func (session *Session) searchGroup(groupDN, filter, gName, groupNameAttribute string) ([]models.LdapGroup, error) {
|
func (s *Session) searchGroup(groupDN, filter, gName, groupNameAttribute string) ([]model.Group, error) {
|
||||||
ldapGroups := make([]models.LdapGroup, 0)
|
ldapGroups := make([]model.Group, 0)
|
||||||
log.Debugf("Groupname: %v, groupDN: %v", gName, groupDN)
|
log.Debugf("Groupname: %v, groupDN: %v", gName, groupDN)
|
||||||
|
|
||||||
// Check current group DN is under the LDAP group base DN
|
// 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 {
|
if err != nil {
|
||||||
return ldapGroups, err
|
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
|
// 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
|
// 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
|
// 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 {
|
if err != nil {
|
||||||
return ldapGroups, err
|
return ldapGroups, err
|
||||||
}
|
}
|
||||||
@ -422,9 +372,9 @@ func (session *Session) searchGroup(groupDN, filter, gName, groupNameAttribute s
|
|||||||
if len(result.Entries[0].Attributes) > 0 {
|
if len(result.Entries[0].Attributes) > 0 {
|
||||||
groupName = result.Entries[0].Attributes[0].Values[0]
|
groupName = result.Entries[0].Attributes[0].Values[0]
|
||||||
}
|
}
|
||||||
group := models.LdapGroup{
|
group := model.Group{
|
||||||
GroupDN: result.Entries[0].DN,
|
Dn: result.Entries[0].DN,
|
||||||
GroupName: groupName,
|
Name: groupName,
|
||||||
}
|
}
|
||||||
ldapGroups = append(ldapGroups, group)
|
ldapGroups = append(ldapGroups, group)
|
||||||
|
|
||||||
@ -468,15 +418,3 @@ func createGroupSearchFilter(baseFilter, groupName, groupNameAttr string) (strin
|
|||||||
fb := base.And(gFilter)
|
fb := base.And(gFilter)
|
||||||
return fb.String()
|
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
515
src/pkg/ldap/ldap_test.go
Normal 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
146
src/pkg/ldap/manager.go
Normal 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
|
||||||
|
}
|
69
src/pkg/ldap/manager_test.go
Normal file
69
src/pkg/ldap/manager_test.go
Normal 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{})
|
||||||
|
}
|
64
src/pkg/ldap/model/ldap.go
Normal file
64
src/pkg/ldap/model/ldap.go
Normal 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"`
|
||||||
|
}
|
@ -45,6 +45,7 @@ func New() http.Handler {
|
|||||||
ReplicationAPI: newReplicationAPI(),
|
ReplicationAPI: newReplicationAPI(),
|
||||||
SysteminfoAPI: newSystemInfoAPI(),
|
SysteminfoAPI: newSystemInfoAPI(),
|
||||||
PingAPI: newPingAPI(),
|
PingAPI: newPingAPI(),
|
||||||
|
LdapAPI: newLdapAPI(),
|
||||||
GCAPI: newGCAPI(),
|
GCAPI: newGCAPI(),
|
||||||
QuotaAPI: newQuotaAPI(),
|
QuotaAPI: newQuotaAPI(),
|
||||||
RetentionAPI: newRetentionAPI(),
|
RetentionAPI: newRetentionAPI(),
|
||||||
|
96
src/server/v2.0/handler/ldap.go
Normal file
96
src/server/v2.0/handler/ldap.go
Normal 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)
|
||||||
|
}
|
@ -32,10 +32,6 @@ func registerLegacyRoutes() {
|
|||||||
beego.Router("/api/"+version+"/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
|
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+"/users/:id/cli_secret", &api.UserAPI{}, "put:SetCLISecret")
|
||||||
beego.Router("/api/"+version+"/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{})
|
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+"/email/ping", &api.EmailAPI{}, "post:Ping")
|
||||||
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
|
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
|
||||||
beego.Router("/api/"+version+"/search", &api.SearchAPI{})
|
beego.Router("/api/"+version+"/search", &api.SearchAPI{})
|
||||||
|
107
src/testing/pkg/ldap/manager.go
Normal file
107
src/testing/pkg/ldap/manager.go
Normal 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
|
||||||
|
}
|
@ -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/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/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/immutable/dao --name DAO --output ./immutable/dao --outpkg dao
|
||||||
|
//go:generate mockery --case snake --dir ../../pkg/ldap --name Manager --output ./ldap --outpkg ldap
|
||||||
|
Loading…
Reference in New Issue
Block a user