mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-02 04:51:22 +01:00
Merge pull request #1467 from yhua123/dev
add new ldap auth and import user feature
This commit is contained in:
commit
c4606d0383
@ -84,6 +84,9 @@ script:
|
||||
- export REGISTRY_URL=$IP:5000
|
||||
- echo $REGISTRY_URL
|
||||
- ./tests/pushimage.sh
|
||||
- cd tests
|
||||
- sudo ./ldapprepare.sh
|
||||
- cd ..
|
||||
- ./tests/coverage4gotest.sh
|
||||
- goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
|
||||
|
@ -1356,8 +1356,8 @@ paths:
|
||||
parameters:
|
||||
- name: ldapconf
|
||||
in: body
|
||||
description: ldap configuration.
|
||||
required: true
|
||||
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:
|
||||
@ -1365,12 +1365,76 @@ paths:
|
||||
responses:
|
||||
200:
|
||||
description: Ping ldap service successfully.
|
||||
401:
|
||||
description: Only admin has this authority.
|
||||
403:
|
||||
400:
|
||||
description: Inviald ldap configuration parameters.
|
||||
401:
|
||||
description: User need to login first.
|
||||
403:
|
||||
description: Only admin has this authority.
|
||||
500:
|
||||
description: Unexpected internal errors.
|
||||
/ldap/users/search:
|
||||
post:
|
||||
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
|
||||
- name: ldap_conf
|
||||
in: body
|
||||
description: ldap search configuration. ldapconf field can input ldap service configuration. If this item are blank, will load default configuration will load current configuration from the system.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/definitions/LdapConf'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
200:
|
||||
description: Search ldap users successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/LdapUsers'
|
||||
400:
|
||||
description: Inviald ldap configuration parameters.
|
||||
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.
|
||||
500:
|
||||
description: Failed import some users.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/LdapFailedImportUsers'
|
||||
/configurations:
|
||||
get:
|
||||
summary: Get system configurations.
|
||||
@ -1881,7 +1945,7 @@ definitions:
|
||||
description: The serach filter of ldap service.
|
||||
ldap_uid:
|
||||
type: string
|
||||
description: The serach uid of ldap service.
|
||||
description: The serach uid from ldap service attributes.
|
||||
ldap_scope:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -1890,3 +1954,32 @@ definitions:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The connect timeout of ldap service(second).
|
||||
LdapUsers:
|
||||
type: object
|
||||
properties:
|
||||
ldap_username:
|
||||
type: string
|
||||
description: search ldap user name based on ldapconf.
|
||||
ldap_realname:
|
||||
type: string
|
||||
description: system will try to guess the user realname form "uid" or "cn" attribute.
|
||||
ldap_email:
|
||||
type: string
|
||||
description: system will try to guess the user email address form "mail" or "email" attribute.
|
||||
LdapImportUsers:
|
||||
type: object
|
||||
properties:
|
||||
ldap_uid_list:
|
||||
type: array
|
||||
description: selected uid list
|
||||
items:
|
||||
type: string
|
||||
LdapFailedImportUsers:
|
||||
type: object
|
||||
properties:
|
||||
ldap_uid:
|
||||
type: string
|
||||
description: the uid can't add to system.
|
||||
error:
|
||||
type: string
|
||||
description: fail reason.
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
package models
|
||||
|
||||
// LdapConf holds information about repository that accessed most
|
||||
// LdapConf holds information about ldap configuration
|
||||
type LdapConf struct {
|
||||
LdapURL string `json:"ldap_url"`
|
||||
LdapSearchDn string `json:"ldap_search_dn"`
|
||||
@ -26,3 +26,21 @@ type LdapConf struct {
|
||||
LdapScope int `json:"ldap_scope"`
|
||||
LdapConnectionTimeout int `json:"ldap_connection_timeout"`
|
||||
}
|
||||
|
||||
// LdapUser ...
|
||||
type LdapUser struct {
|
||||
Username string `json:"ldap_username"`
|
||||
Email string `json:"ldap_email"`
|
||||
Realname string `json:"ldap_realname"`
|
||||
}
|
||||
|
||||
//LdapImportUser ...
|
||||
type LdapImportUser struct {
|
||||
LdapUIDList []string `json:"ldap_uid_list"`
|
||||
}
|
||||
|
||||
// LdapFailedImportUser ...
|
||||
type LdapFailedImportUser struct {
|
||||
UID string `json:"uid"`
|
||||
Error string `json:"err_msg"`
|
||||
}
|
||||
|
382
src/common/utils/ldap/ldap.go
Normal file
382
src/common/utils/ldap/ldap.go
Normal file
@ -0,0 +1,382 @@
|
||||
/*
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
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 (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
|
||||
goldap "gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
var attributes = []string{"uid", "cn", "mail", "email"}
|
||||
|
||||
// GetSystemLdapConf ...
|
||||
func GetSystemLdapConf() (models.LdapConf, error) {
|
||||
var err error
|
||||
var ldapConfs models.LdapConf
|
||||
var authMode string
|
||||
|
||||
authMode, err = config.AuthMode()
|
||||
if err != nil {
|
||||
log.Errorf("can't load auth mode from system, error: %v", err)
|
||||
return ldapConfs, err
|
||||
}
|
||||
|
||||
if authMode != "ldap_auth" {
|
||||
return ldapConfs, fmt.Errorf("system auth_mode isn't ldap_auth, please check configuration")
|
||||
}
|
||||
|
||||
ldap, err := config.LDAP()
|
||||
|
||||
ldapConfs.LdapURL = ldap.URL
|
||||
ldapConfs.LdapSearchDn = ldap.SearchDN
|
||||
ldapConfs.LdapSearchPassword = ldap.SearchPassword
|
||||
ldapConfs.LdapBaseDn = ldap.BaseDN
|
||||
ldapConfs.LdapFilter = ldap.Filter
|
||||
ldapConfs.LdapUID = ldap.UID
|
||||
ldapConfs.LdapScope = ldap.Scope
|
||||
ldapConfs.LdapConnectionTimeout = ldap.Timeout
|
||||
|
||||
// ldapConfs = config.LDAP().URL
|
||||
// ldapConfs.LdapSearchDn = config.LDAP().SearchDn
|
||||
// ldapConfs.LdapSearchPassword = config.LDAP().SearchPwd
|
||||
// ldapConfs.LdapBaseDn = config.LDAP().BaseDn
|
||||
// ldapConfs.LdapFilter = config.LDAP().Filter
|
||||
// ldapConfs.LdapUID = config.LDAP().UID
|
||||
// ldapConfs.LdapScope, err = strconv.Atoi(config.LDAP().Scope)
|
||||
// if err != nil {
|
||||
// log.Errorf("invalid LdapScope format from system, error: %v", err)
|
||||
// return ldapConfs, err
|
||||
// }
|
||||
|
||||
// ldapConfs.LdapConnectionTimeout, err = strconv.Atoi(config.LDAP().ConnectTimeout)
|
||||
// if err != nil {
|
||||
// log.Errorf("invalid LdapConnectionTimeout format from system, error: %v", err)
|
||||
// return ldapConfs, err
|
||||
// }
|
||||
|
||||
return ldapConfs, nil
|
||||
|
||||
}
|
||||
|
||||
// ValidateLdapConf ...
|
||||
func ValidateLdapConf(ldapConfs models.LdapConf) (models.LdapConf, error) {
|
||||
var err error
|
||||
|
||||
if ldapConfs.LdapURL == "" {
|
||||
return ldapConfs, fmt.Errorf("can not get any available LDAP_URL")
|
||||
}
|
||||
|
||||
ldapConfs.LdapURL, err = formatLdapURL(ldapConfs.LdapURL)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("invalid LdapURL format, error: %v", err)
|
||||
return ldapConfs, err
|
||||
}
|
||||
|
||||
// Compatible with legacy codes
|
||||
// in previous harbor.cfg:
|
||||
// the scope to search for users, 1-LDAP_SCOPE_BASE, 2-LDAP_SCOPE_ONELEVEL, 3-LDAP_SCOPE_SUBTREE
|
||||
switch ldapConfs.LdapScope {
|
||||
case 1:
|
||||
ldapConfs.LdapScope = goldap.ScopeBaseObject
|
||||
case 2:
|
||||
ldapConfs.LdapScope = goldap.ScopeSingleLevel
|
||||
case 3:
|
||||
ldapConfs.LdapScope = goldap.ScopeWholeSubtree
|
||||
default:
|
||||
return ldapConfs, fmt.Errorf("invalid ldap search scope")
|
||||
}
|
||||
|
||||
// value := reflect.ValueOf(ldapConfs)
|
||||
// lType := reflect.TypeOf(ldapConfs)
|
||||
// for i := 0; i < value.NumField(); i++ {
|
||||
// fmt.Printf("Field %d: %v %v\n", i, value.Field(i), lType.Field(i).Name)
|
||||
// }
|
||||
|
||||
return ldapConfs, nil
|
||||
|
||||
}
|
||||
|
||||
// MakeFilter ...
|
||||
func MakeFilter(username string, ldapFilter string, ldapUID string) string {
|
||||
|
||||
var filterTag string
|
||||
|
||||
if username == "" {
|
||||
filterTag = "*"
|
||||
} else {
|
||||
filterTag = username
|
||||
}
|
||||
|
||||
if ldapFilter == "" {
|
||||
ldapFilter = "(" + ldapUID + "=" + filterTag + ")"
|
||||
} else {
|
||||
if !strings.Contains(ldapFilter, ldapUID+"=") {
|
||||
ldapFilter = "(&" + ldapFilter + "(" + ldapUID + "=" + filterTag + "))"
|
||||
} else {
|
||||
ldapFilter = strings.Replace(ldapFilter, ldapUID+"=*", ldapUID+"="+filterTag, -1)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("one or more ldapFilter: ", ldapFilter)
|
||||
|
||||
return ldapFilter
|
||||
}
|
||||
|
||||
// ConnectTest ...
|
||||
func ConnectTest(ldapConfs models.LdapConf) error {
|
||||
|
||||
var ldapConn *goldap.Conn
|
||||
var err error
|
||||
|
||||
ldapConn, err = dialLDAP(ldapConfs, ldapConn)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ldapConn.Close()
|
||||
|
||||
if ldapConfs.LdapSearchDn != "" {
|
||||
err = bindLDAPSearchDN(ldapConfs, ldapConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// SearchUser ...
|
||||
func SearchUser(ldapConfs models.LdapConf) ([]models.LdapUser, error) {
|
||||
var ldapUsers []models.LdapUser
|
||||
var ldapConn *goldap.Conn
|
||||
var err error
|
||||
|
||||
ldapConn, err = dialLDAP(ldapConfs, ldapConn)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ldapConn.Close()
|
||||
|
||||
if ldapConfs.LdapSearchDn != "" {
|
||||
err = bindLDAPSearchDN(ldapConfs, ldapConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ldapConfs.LdapBaseDn == "" {
|
||||
return nil, fmt.Errorf("can not get any available LDAP_BASE_DN")
|
||||
}
|
||||
|
||||
result, err := searchLDAP(ldapConfs, ldapConn)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ldapEntry := range result.Entries {
|
||||
var u models.LdapUser
|
||||
for _, attr := range ldapEntry.Attributes {
|
||||
val := attr.Values[0]
|
||||
switch attr.Name {
|
||||
case ldapConfs.LdapUID:
|
||||
u.Username = val
|
||||
case "uid":
|
||||
u.Realname = val
|
||||
case "cn":
|
||||
u.Realname = val
|
||||
case "mail":
|
||||
u.Email = val
|
||||
case "email":
|
||||
u.Email = val
|
||||
}
|
||||
}
|
||||
ldapUsers = append(ldapUsers, u)
|
||||
}
|
||||
|
||||
return ldapUsers, nil
|
||||
}
|
||||
|
||||
func formatLdapURL(ldapURL string) (string, error) {
|
||||
|
||||
var protocol, hostport string
|
||||
var err error
|
||||
|
||||
_, err = url.Parse(ldapURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse Ldap Host ERR: %s", err)
|
||||
}
|
||||
|
||||
if strings.Contains(ldapURL, "://") {
|
||||
splitLdapURL := strings.Split(ldapURL, "://")
|
||||
protocol, hostport = splitLdapURL[0], splitLdapURL[1]
|
||||
if !((protocol == "ldap") || (protocol == "ldaps")) {
|
||||
return "", fmt.Errorf("unknown ldap protocl")
|
||||
}
|
||||
} else {
|
||||
hostport = ldapURL
|
||||
protocol = "ldap"
|
||||
}
|
||||
|
||||
if strings.Contains(hostport, ":") {
|
||||
splitHostPort := strings.Split(hostport, ":")
|
||||
port, error := strconv.Atoi(splitHostPort[1])
|
||||
if error != nil {
|
||||
return "", fmt.Errorf("illegal url port")
|
||||
}
|
||||
if port == 636 {
|
||||
protocol = "ldaps"
|
||||
}
|
||||
|
||||
} else {
|
||||
switch protocol {
|
||||
case "ldap":
|
||||
hostport = hostport + ":389"
|
||||
case "ldaps":
|
||||
hostport = hostport + ":636"
|
||||
}
|
||||
}
|
||||
|
||||
fLdapURL := protocol + "://" + hostport
|
||||
|
||||
return fLdapURL, nil
|
||||
|
||||
}
|
||||
|
||||
// ImportUser ...
|
||||
func ImportUser(user models.LdapUser) (int64, error) {
|
||||
var u models.User
|
||||
u.Username = user.Username
|
||||
u.Email = user.Email
|
||||
u.Realname = user.Realname
|
||||
|
||||
log.Debug("username:", u.Username, ",email:", u.Email)
|
||||
exist, err := dao.UserExists(u, "username")
|
||||
if err != nil {
|
||||
log.Errorf("system checking user %s failed, error: %v", user.Username, err)
|
||||
return 0, fmt.Errorf("internal_error")
|
||||
}
|
||||
|
||||
if exist {
|
||||
return 0, fmt.Errorf("duplicate_username")
|
||||
}
|
||||
|
||||
exist, err = dao.UserExists(u, "email")
|
||||
if err != nil {
|
||||
log.Errorf("system checking %s mailbox failed, error: %v", user.Username, err)
|
||||
return 0, fmt.Errorf("internal_error")
|
||||
}
|
||||
|
||||
if exist {
|
||||
return 0, fmt.Errorf("duplicate_mailbox")
|
||||
}
|
||||
|
||||
u.Password = "12345678AbC"
|
||||
u.Comment = "registered from LDAP."
|
||||
if u.Email == "" {
|
||||
u.Email = u.Username + "@placeholder.com"
|
||||
}
|
||||
|
||||
UserID, err := dao.Register(u)
|
||||
if err != nil {
|
||||
log.Errorf("system register user %s failed, error: %v", user.Username, err)
|
||||
return 0, fmt.Errorf("registe_user_error")
|
||||
}
|
||||
|
||||
return UserID, nil
|
||||
}
|
||||
|
||||
func dialLDAP(ldapConfs models.LdapConf, ldap *goldap.Conn) (*goldap.Conn, error) {
|
||||
var err error
|
||||
|
||||
//log.Debug("ldapConfs.LdapURL:", ldapConfs.LdapURL)
|
||||
|
||||
splitLdapURL := strings.Split(ldapConfs.LdapURL, "://")
|
||||
protocol, hostport := splitLdapURL[0], splitLdapURL[1]
|
||||
|
||||
// Sets a Dial Timeout for LDAP
|
||||
connectionTimeout := ldapConfs.LdapConnectionTimeout
|
||||
goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second
|
||||
|
||||
switch protocol {
|
||||
case "ldap":
|
||||
ldap, err = goldap.Dial("tcp", hostport)
|
||||
case "ldaps":
|
||||
ldap, err = goldap.DialTLS("tcp", hostport, &tls.Config{InsecureSkipVerify: true})
|
||||
}
|
||||
|
||||
return ldap, err
|
||||
}
|
||||
|
||||
func bindLDAPSearchDN(ldapConfs models.LdapConf, ldap *goldap.Conn) error {
|
||||
|
||||
var err error
|
||||
|
||||
ldapSearchDn := ldapConfs.LdapSearchDn
|
||||
ldapSearchPassword := ldapConfs.LdapSearchPassword
|
||||
|
||||
err = ldap.Bind(ldapSearchDn, ldapSearchPassword)
|
||||
if err != nil {
|
||||
log.Debug("Bind search dn error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchLDAP(ldapConfs models.LdapConf, ldap *goldap.Conn) (*goldap.SearchResult, error) {
|
||||
|
||||
var err error
|
||||
ldapBaseDn := ldapConfs.LdapBaseDn
|
||||
ldapScope := ldapConfs.LdapScope
|
||||
ldapFilter := ldapConfs.LdapFilter
|
||||
|
||||
searchRequest := goldap.NewSearchRequest(
|
||||
ldapBaseDn,
|
||||
ldapScope,
|
||||
goldap.NeverDerefAliases,
|
||||
0, // Unlimited results.
|
||||
0, // Search Timeout.
|
||||
false, // Types Only
|
||||
ldapFilter,
|
||||
attributes,
|
||||
nil,
|
||||
)
|
||||
|
||||
result, err := ldap.Search(searchRequest)
|
||||
|
||||
if err != nil {
|
||||
log.Debug("LDAP search error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
258
src/common/utils/ldap/ldap_test.go
Normal file
258
src/common/utils/ldap/ldap_test.go
Normal file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
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 (
|
||||
//"fmt"
|
||||
//"strings"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/vmware/harbor/src/common/config"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/test"
|
||||
uiConfig "github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
var adminServerLdapTestConfig = map[string]interface{}{
|
||||
config.ExtEndpoint: "host01.com",
|
||||
config.AUTHMode: "ldap_auth",
|
||||
config.DatabaseType: "mysql",
|
||||
config.MySQLHost: "127.0.0.1",
|
||||
config.MySQLPort: 3306,
|
||||
config.MySQLUsername: "root",
|
||||
config.MySQLPassword: "root123",
|
||||
config.MySQLDatabase: "registry",
|
||||
config.SQLiteFile: "/tmp/registry.db",
|
||||
//config.SelfRegistration: true,
|
||||
config.LDAPURL: "ldap://127.0.0.1",
|
||||
config.LDAPSearchDN: "cn=admin,dc=example,dc=com",
|
||||
config.LDAPSearchPwd: "admin",
|
||||
config.LDAPBaseDN: "dc=example,dc=com",
|
||||
config.LDAPUID: "uid",
|
||||
config.LDAPFilter: "",
|
||||
config.LDAPScope: 3,
|
||||
config.LDAPTimeout: 30,
|
||||
// config.TokenServiceURL: "",
|
||||
// config.RegistryURL: "",
|
||||
// config.EmailHost: "",
|
||||
// config.EmailPort: 25,
|
||||
// config.EmailUsername: "",
|
||||
// config.EmailPassword: "password",
|
||||
// config.EmailFrom: "from",
|
||||
// config.EmailSSL: true,
|
||||
// config.EmailIdentity: "",
|
||||
// config.ProjectCreationRestriction: config.ProCrtRestrAdmOnly,
|
||||
// config.VerifyRemoteCert: false,
|
||||
// config.MaxJobWorkers: 3,
|
||||
// config.TokenExpiration: 30,
|
||||
config.CfgExpiration: 5,
|
||||
// config.JobLogDir: "/var/log/jobs",
|
||||
// config.UseCompressedJS: true,
|
||||
config.AdminInitialPassword: "password",
|
||||
}
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
server, err := test.NewAdminserver(adminServerLdapTestConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a mock admin server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
if err := os.Setenv("ADMIN_SERVER_URL", server.URL); err != nil {
|
||||
t.Fatalf("failed to set env %s: %v", "ADMIN_SERVER_URL", err)
|
||||
}
|
||||
|
||||
secretKeyPath := "/tmp/secretkey"
|
||||
_, err = test.GenerateKey(secretKeyPath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate secret key: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(secretKeyPath)
|
||||
|
||||
if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil {
|
||||
t.Fatalf("failed to set env %s: %v", "KEY_PATH", err)
|
||||
}
|
||||
|
||||
if err := uiConfig.Init(); err != nil {
|
||||
t.Fatalf("failed to initialize configurations: %v", err)
|
||||
}
|
||||
|
||||
// if err := uiConfig.Load(); err != nil {
|
||||
// t.Fatalf("failed to load configurations: %v", err)
|
||||
// }
|
||||
|
||||
// mode, err := uiConfig.AuthMode()
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to get auth mode: %v", err)
|
||||
// }
|
||||
|
||||
database, err := uiConfig.Database()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get database configuration: %v", err)
|
||||
}
|
||||
|
||||
if err := dao.InitDatabase(database); err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSystemLdapConf(t *testing.T) {
|
||||
|
||||
testLdapConfig, err := GetSystemLdapConf()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get system ldap config %v", err)
|
||||
}
|
||||
|
||||
if testLdapConfig.LdapURL != "ldap://127.0.0.1" {
|
||||
t.Errorf("unexpected LdapURL: %s != %s", testLdapConfig.LdapURL, "ldap://test.ldap.com")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLdapConf(t *testing.T) {
|
||||
testLdapConfig, err := GetSystemLdapConf()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get system ldap config %v", err)
|
||||
}
|
||||
|
||||
testLdapConfig, err = ValidateLdapConf(testLdapConfig)
|
||||
|
||||
if testLdapConfig.LdapScope != 2 {
|
||||
t.Errorf("unexpected LdapScope: %d != %d", testLdapConfig.LdapScope, 2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeFilter(t *testing.T) {
|
||||
testLdapConfig, err := GetSystemLdapConf()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get system ldap config %v", err)
|
||||
}
|
||||
|
||||
testLdapConfig.LdapFilter = "(ou=people)"
|
||||
tempUsername := ""
|
||||
|
||||
tempFilter := MakeFilter(tempUsername, testLdapConfig.LdapFilter, testLdapConfig.LdapUID)
|
||||
if tempFilter != "(&(ou=people)(uid=*))" {
|
||||
t.Errorf("unexpected tempFilter: %s != %s", tempFilter, "(&(ou=people)(uid=*))")
|
||||
}
|
||||
|
||||
tempUsername = "user0001"
|
||||
tempFilter = MakeFilter(tempUsername, testLdapConfig.LdapFilter, testLdapConfig.LdapUID)
|
||||
if tempFilter != "(&(ou=people)(uid=user0001))" {
|
||||
t.Errorf("unexpected tempFilter: %s != %s", tempFilter, "(&(ou=people)(uid=user0001)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatLdapURL(t *testing.T) {
|
||||
testLdapConfig, err := GetSystemLdapConf()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get system ldap config %v", err)
|
||||
}
|
||||
|
||||
testLdapConfig.LdapURL = "test.ldap.com"
|
||||
tempLdapURL, err := formatLdapURL(testLdapConfig.LdapURL)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to format Ldap URL %v", err)
|
||||
}
|
||||
|
||||
if tempLdapURL != "ldap://test.ldap.com:389" {
|
||||
t.Errorf("unexpected tempLdapURL: %s != %s", tempLdapURL, "ldap://test.ldap.com:389")
|
||||
}
|
||||
|
||||
testLdapConfig.LdapURL = "ldaps://test.ldap.com"
|
||||
tempLdapURL, err = formatLdapURL(testLdapConfig.LdapURL)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to format Ldap URL %v", err)
|
||||
}
|
||||
|
||||
if tempLdapURL != "ldaps://test.ldap.com:636" {
|
||||
t.Errorf("unexpected tempLdapURL: %s != %s", tempLdapURL, "ldap://test.ldap.com:636")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportUser(t *testing.T) {
|
||||
var u models.LdapUser
|
||||
var user models.User
|
||||
u.Username = "ldapUser0001"
|
||||
u.Realname = "ldapUser"
|
||||
_, err := ImportUser(u)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add Ldap user: %v", err)
|
||||
}
|
||||
|
||||
user.Username = "ldapUser0001"
|
||||
user.Email = "ldapUser0001@placeholder.com"
|
||||
|
||||
exist, err := dao.UserExists(user, "username")
|
||||
if !exist {
|
||||
t.Errorf("failed to add Ldap username: %v", err)
|
||||
}
|
||||
|
||||
exist, err = dao.UserExists(user, "email")
|
||||
if !exist {
|
||||
t.Errorf("failed to add Ldap user email: %v", err)
|
||||
}
|
||||
|
||||
_, err = ImportUser(u)
|
||||
if err.Error() != "duplicate_username" {
|
||||
t.Fatalf("failed to checking duplicate user: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestConnectTest(t *testing.T) {
|
||||
|
||||
testLdapConfig, err := GetSystemLdapConf()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get system ldap config %v", err)
|
||||
}
|
||||
|
||||
testLdapConfig.LdapURL = "ldap://localhost:389"
|
||||
|
||||
err = ConnectTest(testLdapConfig)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected ldap connect fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchUser(t *testing.T) {
|
||||
|
||||
testLdapConfig, err := GetSystemLdapConf()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get system ldap config %v", err)
|
||||
}
|
||||
testLdapConfig.LdapURL = "ldap://localhost:389"
|
||||
testLdapConfig.LdapFilter = MakeFilter("", testLdapConfig.LdapFilter, testLdapConfig.LdapUID)
|
||||
|
||||
ldapUsers, err := SearchUser(testLdapConfig)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected ldap search fail: %v", err)
|
||||
}
|
||||
|
||||
if ldapUsers[0].Username != "test" {
|
||||
t.Errorf("unexpected ldap user search result: %s = %s", "ldapUsers[0].Username", ldapUsers[0].Username)
|
||||
}
|
||||
}
|
@ -18,26 +18,21 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
|
||||
goldap "gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
// LdapAPI handles requesst to /api/ldap/ping /api/ldap/search
|
||||
// LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import
|
||||
type LdapAPI struct {
|
||||
api.BaseAPI
|
||||
}
|
||||
|
||||
var ldapConfs models.LdapConf
|
||||
const metaChars = "&|!=~*<>()"
|
||||
|
||||
// Prepare ...
|
||||
func (l *LdapAPI) Prepare() {
|
||||
@ -52,20 +47,34 @@ func (l *LdapAPI) Prepare() {
|
||||
if !isSysAdmin {
|
||||
l.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ping ...
|
||||
func (l *LdapAPI) Ping() {
|
||||
l.DecodeJSONReqAndValidate(&ldapConfs)
|
||||
var err error
|
||||
var ldapConfs models.LdapConf
|
||||
|
||||
err := validateLdapReq(ldapConfs)
|
||||
l.Ctx.Input.CopyBody(1 << 32)
|
||||
if string(l.Ctx.Input.RequestBody) == "" {
|
||||
ldapConfs, err = ldapUtils.GetSystemLdapConf()
|
||||
if err != nil {
|
||||
log.Errorf("Can't load system configuration, error: %v", err)
|
||||
l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't load system configuration: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
l.DecodeJSONReqAndValidate(&ldapConfs)
|
||||
}
|
||||
|
||||
ldapConfs, err = ldapUtils.ValidateLdapConf(ldapConfs)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid ldap request, error: %v", err)
|
||||
l.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid ldap request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = connectTest(ldapConfs)
|
||||
err = ldapUtils.ConnectTest(ldapConfs)
|
||||
if err != nil {
|
||||
log.Errorf("Ldap connect fail, error: %v", err)
|
||||
l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap connect fail: %v", err))
|
||||
@ -73,87 +82,155 @@ func (l *LdapAPI) Ping() {
|
||||
}
|
||||
}
|
||||
|
||||
func validateLdapReq(ldapConfs models.LdapConf) error {
|
||||
ldapURL := ldapConfs.LdapURL
|
||||
if ldapURL == "" {
|
||||
return fmt.Errorf("can not get any available LDAP_URL")
|
||||
}
|
||||
log.Debug("ldapURL:", ldapURL)
|
||||
|
||||
ldapConnectionTimeout := ldapConfs.LdapConnectionTimeout
|
||||
log.Debug("ldapConnectionTimeout:", ldapConnectionTimeout)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func connectTest(ldapConfs models.LdapConf) error {
|
||||
|
||||
var ldap *goldap.Conn
|
||||
var protocol, hostport string
|
||||
var host, port string
|
||||
// Search ...
|
||||
func (l *LdapAPI) Search() {
|
||||
var err error
|
||||
var ldapUsers []models.LdapUser
|
||||
var ldapConfs models.LdapConf
|
||||
|
||||
ldapURL := ldapConfs.LdapURL
|
||||
|
||||
// This routine keeps compability with the old format used on harbor.cfg
|
||||
|
||||
if strings.Contains(ldapURL, "://") {
|
||||
splitLdapURL := strings.Split(ldapURL, "://")
|
||||
protocol, hostport = splitLdapURL[0], splitLdapURL[1]
|
||||
if !((protocol == "ldap") || (protocol == "ldaps")) {
|
||||
return fmt.Errorf("unknown ldap protocl")
|
||||
l.Ctx.Input.CopyBody(1 << 32)
|
||||
if string(l.Ctx.Input.RequestBody) == "" {
|
||||
ldapConfs, err = ldapUtils.GetSystemLdapConf()
|
||||
if err != nil {
|
||||
log.Errorf("Can't load system configuration, error: %v", err)
|
||||
l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't load system configuration: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
hostport = ldapURL
|
||||
protocol = "ldap"
|
||||
l.DecodeJSONReqAndValidate(&ldapConfs)
|
||||
}
|
||||
|
||||
// This tries to detect the used port, if not defined
|
||||
if strings.Contains(hostport, ":") {
|
||||
splitHostPort := strings.Split(hostport, ":")
|
||||
host, port = splitHostPort[0], splitHostPort[1]
|
||||
_, error := strconv.Atoi(splitHostPort[1])
|
||||
if error != nil {
|
||||
return fmt.Errorf("illegal url format")
|
||||
}
|
||||
} else {
|
||||
host = hostport
|
||||
switch protocol {
|
||||
case "ldap":
|
||||
port = "389"
|
||||
case "ldaps":
|
||||
port = "636"
|
||||
}
|
||||
}
|
||||
|
||||
// Sets a Dial Timeout for LDAP
|
||||
connectionTimeout := ldapConfs.LdapConnectionTimeout
|
||||
goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second
|
||||
|
||||
switch protocol {
|
||||
case "ldap":
|
||||
ldap, err = goldap.Dial("tcp", fmt.Sprintf("%s:%s", host, port))
|
||||
case "ldaps":
|
||||
ldap, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%s", host, port), &tls.Config{InsecureSkipVerify: true})
|
||||
}
|
||||
ldapConfs, err = ldapUtils.ValidateLdapConf(ldapConfs)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
log.Errorf("Invalid ldap request, error: %v", err)
|
||||
l.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid ldap request: %v", err))
|
||||
return
|
||||
}
|
||||
defer ldap.Close()
|
||||
|
||||
ldapSearchDn := ldapConfs.LdapSearchDn
|
||||
if ldapSearchDn != "" {
|
||||
log.Debug("Search DN: ", ldapSearchDn)
|
||||
ldapSearchPassword := ldapConfs.LdapSearchPassword
|
||||
err = ldap.Bind(ldapSearchDn, ldapSearchPassword)
|
||||
searchName := l.GetString("username")
|
||||
|
||||
if searchName != "" {
|
||||
for _, c := range metaChars {
|
||||
if strings.ContainsRune(searchName, c) {
|
||||
log.Errorf("the search username contains meta char: %q", c)
|
||||
l.RenderError(http.StatusBadRequest, fmt.Sprintf("the search username contains meta char: %q", c))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ldapConfs.LdapFilter = ldapUtils.MakeFilter(searchName, ldapConfs.LdapFilter, ldapConfs.LdapUID)
|
||||
|
||||
ldapUsers, err = ldapUtils.SearchUser(ldapConfs)
|
||||
|
||||
if err != nil {
|
||||
log.Debug("Bind search dn error", err)
|
||||
return err
|
||||
}
|
||||
log.Errorf("Ldap search fail, error: %v", err)
|
||||
l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap search fail: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
l.Data["json"] = ldapUsers
|
||||
l.ServeJSON()
|
||||
|
||||
}
|
||||
|
||||
// ImportUser ...
|
||||
func (l *LdapAPI) ImportUser() {
|
||||
var ldapImportUsers models.LdapImportUser
|
||||
var ldapFailedImportUsers []models.LdapFailedImportUser
|
||||
var ldapConfs models.LdapConf
|
||||
|
||||
ldapConfs, err := ldapUtils.GetSystemLdapConf()
|
||||
if err != nil {
|
||||
log.Errorf("Can't load system configuration, error: %v", err)
|
||||
l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't load system configuration: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
l.DecodeJSONReqAndValidate(&ldapImportUsers)
|
||||
|
||||
ldapConfs, err = ldapUtils.ValidateLdapConf(ldapConfs)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid ldap request, error: %v", err)
|
||||
l.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid ldap request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ldapFailedImportUsers, err = importUsers(ldapConfs, ldapImportUsers.LdapUIDList)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Ldap import user fail, error: %v", err)
|
||||
l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap import user fail: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(ldapFailedImportUsers) > 0 {
|
||||
log.Errorf("Import ldap user have internal error")
|
||||
l.RenderError(http.StatusInternalServerError, fmt.Sprintf("import ldap user have internal error"))
|
||||
l.Data["json"] = ldapFailedImportUsers
|
||||
l.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models.LdapFailedImportUser, error) {
|
||||
var failedImportUser []models.LdapFailedImportUser
|
||||
var u models.LdapFailedImportUser
|
||||
|
||||
tempFilter := ldapConfs.LdapFilter
|
||||
|
||||
for _, tempUID := range ldapImportUsers {
|
||||
u.UID = tempUID
|
||||
u.Error = ""
|
||||
|
||||
if u.UID == "" {
|
||||
u.Error = "empty_uid"
|
||||
failedImportUser = append(failedImportUser, u)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range metaChars {
|
||||
if strings.ContainsRune(u.UID, c) {
|
||||
u.Error = "invaild_username"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if u.Error != "" {
|
||||
failedImportUser = append(failedImportUser, u)
|
||||
continue
|
||||
}
|
||||
|
||||
ldapConfs.LdapFilter = ldapUtils.MakeFilter(u.UID, tempFilter, ldapConfs.LdapUID)
|
||||
|
||||
ldapUsers, err := ldapUtils.SearchUser(ldapConfs)
|
||||
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 {
|
||||
u.UID = tempUID
|
||||
u.Error = "unknown_user"
|
||||
failedImportUser = append(failedImportUser, u)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = ldapUtils.ImportUser(ldapUsers[0])
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
@ -1,95 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vmware/harbor/tests/apitests/apilib"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var ldapConf apilib.LdapConf
|
||||
|
||||
func TestLdapPost(t *testing.T) {
|
||||
fmt.Println("Testing ldap post")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
//case 1: ping ldap server without admin role
|
||||
CommonAddUser()
|
||||
code, err := apiTest.LdapPost(*testUser, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(403, code, "Ping ldap server status should be 403")
|
||||
}
|
||||
//case 2: ping ldap server with admin role, but empty ldapConf
|
||||
code, err = apiTest.LdapPost(*admin, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "Ping ldap server status should be 400")
|
||||
}
|
||||
|
||||
//case 3: ping ldap server with admin role, but bad format of ldapConf
|
||||
ldapConf.LdapURL = "http://127.0.0.1"
|
||||
code, err = apiTest.LdapPost(*admin, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "Ping ldap server status should be 400")
|
||||
}
|
||||
//case 4: ping ldap server with admin role, but bad format of ldapConf
|
||||
ldapConf.LdapURL = "127.0.0.1:sss"
|
||||
code, err = apiTest.LdapPost(*admin, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "Ping ldap server status should be 400")
|
||||
}
|
||||
//case 5: ping ldap server with admin role, ldap protocol, without port
|
||||
ldapConf.LdapURL = "127.0.0.1"
|
||||
code, err = apiTest.LdapPost(*admin, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Ping ldap server status should be 200")
|
||||
}
|
||||
//not success, will try later
|
||||
/*
|
||||
//case 6: ping ldap server with admin role, ldaps protocol without port
|
||||
ldapConf.LdapURL = "ldaps://127.0.0.1"
|
||||
code, err = apiTest.LdapPost(*admin, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Ping ldap server status should be 200")
|
||||
}*/
|
||||
//case 7: ping ldap server with admin role, ldap protocol, port, ldapSearchDn, but wrong password
|
||||
ldapConf.LdapURL = "ldap://127.0.0.1:389"
|
||||
ldapConf.LdapSearchDn = "cn=admin,dc=example,dc=org"
|
||||
code, err = apiTest.LdapPost(*admin, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(400, code, "Ping ldap server status should be 400")
|
||||
}
|
||||
//case 8: ping ldap server with admin role, ldap protocol, port, ldapSearchDn, right password
|
||||
ldapConf.LdapURL = "ldap://127.0.0.1:389"
|
||||
ldapConf.LdapSearchDn = "cn=admin,dc=example,dc=org"
|
||||
ldapConf.LdapSearchPassword = "admin"
|
||||
code, err = apiTest.LdapPost(*admin, ldapConf)
|
||||
if err != nil {
|
||||
t.Error("Error occured while ping ldap server")
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Ping ldap server status should be 200")
|
||||
}
|
||||
CommonDelUser()
|
||||
}
|
@ -16,19 +16,14 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/auth"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
|
||||
goldap "gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
// Auth implements Authenticator interface to authenticate against LDAP
|
||||
@ -36,55 +31,6 @@ type Auth struct{}
|
||||
|
||||
const metaChars = "&|!=~*<>()"
|
||||
|
||||
// Connect checks the LDAP configuration directives, and connects to the LDAP URL
|
||||
// Returns an LDAP connection
|
||||
func Connect(settings *models.LDAP) (*goldap.Conn, error) {
|
||||
ldapURL := settings.URL
|
||||
if ldapURL == "" {
|
||||
return nil, errors.New("can not get any available LDAP_URL")
|
||||
}
|
||||
log.Debug("ldapURL:", ldapURL)
|
||||
|
||||
// This routine keeps compability with the old format used on harbor.cfg
|
||||
splitLdapURL := strings.Split(ldapURL, "://")
|
||||
protocol, hostport := splitLdapURL[0], splitLdapURL[1]
|
||||
|
||||
var host, port string
|
||||
|
||||
// This tries to detect the used port, if not defined
|
||||
if strings.Contains(hostport, ":") {
|
||||
splitHostPort := strings.Split(hostport, ":")
|
||||
host, port = splitHostPort[0], splitHostPort[1]
|
||||
} else {
|
||||
host = hostport
|
||||
switch protocol {
|
||||
case "ldap":
|
||||
port = "389"
|
||||
case "ldaps":
|
||||
port = "636"
|
||||
}
|
||||
}
|
||||
|
||||
// Sets a Dial Timeout for LDAP
|
||||
goldap.DefaultTimeout = time.Duration(settings.Timeout) * time.Second
|
||||
|
||||
var ldap *goldap.Conn
|
||||
var err error
|
||||
switch protocol {
|
||||
case "ldap":
|
||||
ldap, err = goldap.Dial("tcp", fmt.Sprintf("%s:%s", host, port))
|
||||
case "ldaps":
|
||||
ldap, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%s", host, port), &tls.Config{InsecureSkipVerify: true})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ldap, nil
|
||||
|
||||
}
|
||||
|
||||
// Authenticate checks user's credential against LDAP based on basedn template and LDAP URL,
|
||||
// if the check is successful a dummy record will be inserted into DB, such that this user can
|
||||
// be associated to other entities in the system.
|
||||
@ -97,105 +43,39 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
}
|
||||
}
|
||||
|
||||
settings, err := config.LDAP()
|
||||
ldapConfs, err := ldapUtils.GetSystemLdapConf()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("can't load system configuration: %v", err)
|
||||
}
|
||||
|
||||
ldap, err := Connect(settings)
|
||||
ldapConfs, err = ldapUtils.ValidateLdapConf(ldapConfs)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("invalid ldap request: %v", err)
|
||||
}
|
||||
|
||||
ldapBaseDn := settings.BaseDN
|
||||
if ldapBaseDn == "" {
|
||||
return nil, errors.New("can not get any available LDAP_BASE_DN")
|
||||
}
|
||||
log.Debug("baseDn:", ldapBaseDn)
|
||||
ldapConfs.LdapFilter = ldapUtils.MakeFilter(p, ldapConfs.LdapFilter, ldapConfs.LdapUID)
|
||||
|
||||
ldapUsers, err := ldapUtils.SearchUser(ldapConfs)
|
||||
|
||||
ldapSearchDn := settings.SearchDN
|
||||
if ldapSearchDn != "" {
|
||||
log.Debug("Search DN: ", ldapSearchDn)
|
||||
ldapSearchPwd := settings.SearchPassword
|
||||
err = ldap.Bind(ldapSearchDn, ldapSearchPwd)
|
||||
if err != nil {
|
||||
log.Debug("Bind search dn error", err)
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("ldap search fail: %v", err)
|
||||
}
|
||||
|
||||
attrName := settings.UID
|
||||
filter := settings.Filter
|
||||
if filter != "" {
|
||||
filter = "(&" + filter + "(" + attrName + "=" + m.Principal + "))"
|
||||
} else {
|
||||
filter = "(" + attrName + "=" + m.Principal + ")"
|
||||
}
|
||||
log.Debug("one or more filter", filter)
|
||||
|
||||
ldapScope := settings.Scope
|
||||
var scope int
|
||||
if ldapScope == 1 {
|
||||
scope = goldap.ScopeBaseObject
|
||||
} else if ldapScope == 2 {
|
||||
scope = goldap.ScopeSingleLevel
|
||||
} else {
|
||||
scope = goldap.ScopeWholeSubtree
|
||||
}
|
||||
attributes := []string{"uid", "cn", "mail", "email"}
|
||||
|
||||
searchRequest := goldap.NewSearchRequest(
|
||||
ldapBaseDn,
|
||||
scope,
|
||||
goldap.NeverDerefAliases,
|
||||
0, // Unlimited results. TODO: Limit this (as we expect only one result)?
|
||||
0, // Search Timeout. TODO: Limit this (check what is the unit of timeout) and make configurable
|
||||
false, // Types Only
|
||||
filter,
|
||||
attributes,
|
||||
nil,
|
||||
)
|
||||
|
||||
result, err := ldap.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Entries) == 0 {
|
||||
if len(ldapUsers) == 0 {
|
||||
log.Warningf("Not found an entry.")
|
||||
return nil, nil
|
||||
} else if len(result.Entries) != 1 {
|
||||
} else if len(ldapUsers) != 1 {
|
||||
log.Warningf("Found more than one entry.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
entry := result.Entries[0]
|
||||
bindDN := entry.DN
|
||||
log.Debug("found entry:", bindDN)
|
||||
err = ldap.Bind(bindDN, m.Password)
|
||||
if err != nil {
|
||||
log.Debug("Bind user error", err)
|
||||
return nil, err
|
||||
}
|
||||
defer ldap.Close()
|
||||
|
||||
u := models.User{}
|
||||
u.Username = ldapUsers[0].Username
|
||||
u.Email = ldapUsers[0].Email
|
||||
u.Realname = ldapUsers[0].Realname
|
||||
|
||||
for _, attr := range entry.Attributes {
|
||||
val := attr.Values[0]
|
||||
switch attr.Name {
|
||||
case "uid":
|
||||
u.Realname = val
|
||||
case "cn":
|
||||
u.Realname = val
|
||||
case "mail":
|
||||
u.Email = val
|
||||
case "email":
|
||||
u.Email = val
|
||||
}
|
||||
}
|
||||
u.Username = m.Principal
|
||||
log.Debug("username:", u.Username, ",email:", u.Email)
|
||||
exist, err := dao.UserExists(u, "username")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -208,19 +88,21 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
}
|
||||
u.UserID = currentUser.UserID
|
||||
} else {
|
||||
u.Realname = m.Principal
|
||||
u.Password = "12345678AbC"
|
||||
u.Comment = "registered from LDAP."
|
||||
if u.Email == "" {
|
||||
u.Email = u.Username + "@placeholder.com"
|
||||
}
|
||||
userID, err := dao.Register(u)
|
||||
// u.Password = "12345678AbC"
|
||||
// u.Comment = "registered from LDAP."
|
||||
// if u.Email == "" {
|
||||
// u.Email = u.Username + "@placeholder.com"
|
||||
// }
|
||||
userID, err := ldapUtils.ImportUser(ldapUsers[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Errorf("Can't import user %s, error: %v", ldapUsers[0].Username, err)
|
||||
return nil, fmt.Errorf("can't import user %s, error: %v", ldapUsers[0].Username, err)
|
||||
}
|
||||
u.UserID = int(userID)
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -1,9 +1,113 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
//"fmt"
|
||||
//"strings"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/vmware/harbor/src/common/config"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/test"
|
||||
uiConfig "github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
var adminServerLdapTestConfig = map[string]interface{}{
|
||||
config.ExtEndpoint: "host01.com",
|
||||
config.AUTHMode: "ldap_auth",
|
||||
config.DatabaseType: "mysql",
|
||||
config.MySQLHost: "127.0.0.1",
|
||||
config.MySQLPort: 3306,
|
||||
config.MySQLUsername: "root",
|
||||
config.MySQLPassword: "root123",
|
||||
config.MySQLDatabase: "registry",
|
||||
config.SQLiteFile: "/tmp/registry.db",
|
||||
//config.SelfRegistration: true,
|
||||
config.LDAPURL: "ldap://127.0.0.1",
|
||||
config.LDAPSearchDN: "cn=admin,dc=example,dc=com",
|
||||
config.LDAPSearchPwd: "admin",
|
||||
config.LDAPBaseDN: "dc=example,dc=com",
|
||||
config.LDAPUID: "uid",
|
||||
config.LDAPFilter: "",
|
||||
config.LDAPScope: 3,
|
||||
config.LDAPTimeout: 30,
|
||||
// config.TokenServiceURL: "",
|
||||
// config.RegistryURL: "",
|
||||
// config.EmailHost: "",
|
||||
// config.EmailPort: 25,
|
||||
// config.EmailUsername: "",
|
||||
// config.EmailPassword: "password",
|
||||
// config.EmailFrom: "from",
|
||||
// config.EmailSSL: true,
|
||||
// config.EmailIdentity: "",
|
||||
// config.ProjectCreationRestriction: config.ProCrtRestrAdmOnly,
|
||||
// config.VerifyRemoteCert: false,
|
||||
// config.MaxJobWorkers: 3,
|
||||
// config.TokenExpiration: 30,
|
||||
config.CfgExpiration: 5,
|
||||
// config.JobLogDir: "/var/log/jobs",
|
||||
// config.UseCompressedJS: true,
|
||||
config.AdminInitialPassword: "password",
|
||||
}
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
server, err := test.NewAdminserver(adminServerLdapTestConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a mock admin server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
if err := os.Setenv("ADMIN_SERVER_URL", server.URL); err != nil {
|
||||
t.Fatalf("failed to set env %s: %v", "ADMIN_SERVER_URL", err)
|
||||
}
|
||||
|
||||
secretKeyPath := "/tmp/secretkey"
|
||||
_, err = test.GenerateKey(secretKeyPath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to generate secret key: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(secretKeyPath)
|
||||
|
||||
if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil {
|
||||
t.Fatalf("failed to set env %s: %v", "KEY_PATH", err)
|
||||
}
|
||||
|
||||
if err := uiConfig.Init(); err != nil {
|
||||
t.Fatalf("failed to initialize configurations: %v", err)
|
||||
}
|
||||
|
||||
// if err := uiConfig.Load(); err != nil {
|
||||
// t.Fatalf("failed to load configurations: %v", err)
|
||||
// }
|
||||
|
||||
// mode, err := uiConfig.AuthMode()
|
||||
// if err != nil {
|
||||
// t.Fatalf("failed to get auth mode: %v", err)
|
||||
// }
|
||||
|
||||
database, err := uiConfig.Database()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get database configuration: %v", err)
|
||||
}
|
||||
|
||||
if err := dao.InitDatabase(database); err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
var person models.AuthModel
|
||||
var auth *Auth
|
||||
person.Principal = "test"
|
||||
person.Password = "123456"
|
||||
user, err := auth.Authenticate(person)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected ldap authenticate fail: %v", err)
|
||||
}
|
||||
if user.Username != "test" {
|
||||
t.Errorf("unexpected ldap user authenticate fail: %s = %s", "user.Username", user.Username)
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,8 @@ func initRouters() {
|
||||
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
|
||||
beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert")
|
||||
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
|
||||
beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "post:Search")
|
||||
beego.Router("/api/ldap/users/import", &api.LdapAPI{}, "post:ImportUser")
|
||||
|
||||
//external service that hosted on harbor process:
|
||||
beego.Router("/service/notifications", &service.NotificationHandler{})
|
||||
|
@ -33,34 +33,3 @@ services:
|
||||
- /data/secretkey:/etc/adminserver/key
|
||||
ports:
|
||||
- 8888:80
|
||||
ldap:
|
||||
image: osixia/openldap:1.1.7
|
||||
restart: always
|
||||
environment:
|
||||
LDAP_LOG_LEVEL: "256"
|
||||
LDAP_ORGANISATION: "Example Inc."
|
||||
LDAP_DOMAIN: "example.org"
|
||||
LDAP_BASE_DN: ""
|
||||
LDAP_ADMIN_PASSWORD: "admin"
|
||||
LDAP_CONFIG_PASSWORD: "config"
|
||||
LDAP_READONLY_USER: "false"
|
||||
LDAP_BACKEND: "hdb"
|
||||
LDAP_TLS: "true"
|
||||
LDAP_TLS_CRT_FILENAME: "ldap.crt"
|
||||
LDAP_TLS_KEY_FILENAME: "ldap.key"
|
||||
LDAP_TLS_CA_CRT_FILENAME: "ca.crt"
|
||||
LDAP_TLS_ENFORCE: "false"
|
||||
LDAP_TLS_CIPHER_SUITE: "SECURE256:-VERS-SSL3.0"
|
||||
LDAP_TLS_PROTOCOL_MIN: "3.1"
|
||||
LDAP_TLS_VERIFY_CLIENT: "demand"
|
||||
LDAP_REPLICATION: "false"
|
||||
LDAP_REMOVE_CONFIG_AFTER_SETUP: "true"
|
||||
LDAP_SSL_HELPER_PREFIX: "ldap"
|
||||
volumes:
|
||||
- /var/lib/ldap
|
||||
- /etc/ldap/slapd.d
|
||||
- /container/service/slapd/assets/certs/
|
||||
hostname: "example.org"
|
||||
ports:
|
||||
- 389:389
|
||||
- 636:636
|
||||
|
14
tests/ldap_test.ldif
Normal file
14
tests/ldap_test.ldif
Normal file
@ -0,0 +1,14 @@
|
||||
dn: uid=test,dc=example,dc=com
|
||||
uid: test
|
||||
cn: test
|
||||
sn: 3
|
||||
objectClass: top
|
||||
objectClass: posixAccount
|
||||
objectClass: inetOrgPerson
|
||||
loginShell: /bin/bash
|
||||
homeDirectory: /home/test
|
||||
uidNumber: 1001
|
||||
gidNumber: 1001
|
||||
userPassword: 123456
|
||||
mail: test@example.com
|
||||
gecos: test
|
15
tests/ldapprepare.sh
Executable file
15
tests/ldapprepare.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
NAME=ldap_server
|
||||
docker rm -f $NAME 2>/dev/null
|
||||
|
||||
docker run --env LDAP_ORGANISATION="Harbor." \
|
||||
--env LDAP_DOMAIN="example.com" \
|
||||
--env LDAP_ADMIN_PASSWORD="admin" \
|
||||
-p 389:389 \
|
||||
-p 636:636 \
|
||||
--detach --name $NAME osixia/openldap:1.1.7
|
||||
|
||||
sleep 3
|
||||
docker cp ldap_test.ldif ldap_server:/
|
||||
docker exec ldap_server ldapadd -x -D "cn=admin,dc=example,dc=com" -w admin -f /ldap_test.ldif -ZZ
|
||||
|
Loading…
Reference in New Issue
Block a user