mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-08 19:50:05 +01:00
Merge pull request #4412 from stonezdj/add_ldap_group_param
Add LDAP Group Search Configure Param
This commit is contained in:
commit
139a0c59ba
@ -12,6 +12,10 @@ LDAP_UID=$ldap_uid
|
||||
LDAP_SCOPE=$ldap_scope
|
||||
LDAP_TIMEOUT=$ldap_timeout
|
||||
LDAP_VERIFY_CERT=$ldap_verify_cert
|
||||
LDAP_GROUP_BASEDN=$ldap_group_basedn
|
||||
LDAP_GROUP_FILTER=$ldap_group_filter
|
||||
LDAP_GROUP_GID=$ldap_group_gid
|
||||
LDAP_GROUP_SCOPE=$ldap_group_scope
|
||||
DATABASE_TYPE=mysql
|
||||
MYSQL_HOST=$db_host
|
||||
MYSQL_PORT=$db_port
|
||||
|
@ -91,6 +91,18 @@ ldap_timeout = 5
|
||||
#Verify certificate from LDAP server
|
||||
ldap_verify_cert = true
|
||||
|
||||
#The base dn from which to lookup a group in LDAP/AD
|
||||
ldap_group_basedn = ou=group,dc=mydomain,dc=com
|
||||
|
||||
#filter to search LDAP/AD group
|
||||
ldap_group_filter = objectclass=group
|
||||
|
||||
#The attribute used to name a LDAP/AD group, it could be cn, name
|
||||
ldap_group_gid = cn
|
||||
|
||||
#The scope to search for ldap groups. 0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE
|
||||
ldap_group_scope = 2
|
||||
|
||||
#Turn on or off the self-registration feature
|
||||
self_registration = on
|
||||
|
||||
|
@ -224,6 +224,10 @@ ldap_uid = rcp.get("configuration", "ldap_uid")
|
||||
ldap_scope = rcp.get("configuration", "ldap_scope")
|
||||
ldap_timeout = rcp.get("configuration", "ldap_timeout")
|
||||
ldap_verify_cert = rcp.get("configuration", "ldap_verify_cert")
|
||||
ldap_group_basedn = rcp.get("configuration", "ldap_group_basedn")
|
||||
ldap_group_filter = rcp.get("configuration", "ldap_group_filter")
|
||||
ldap_group_gid = rcp.get("configuration", "ldap_group_gid")
|
||||
ldap_group_scope = rcp.get("configuration", "ldap_group_scope")
|
||||
db_password = rcp.get("configuration", "db_password")
|
||||
db_host = rcp.get("configuration", "db_host")
|
||||
db_user = rcp.get("configuration", "db_user")
|
||||
@ -325,6 +329,10 @@ render(os.path.join(templates_dir, "adminserver", "env"),
|
||||
ldap_scope=ldap_scope,
|
||||
ldap_verify_cert=ldap_verify_cert,
|
||||
ldap_timeout=ldap_timeout,
|
||||
ldap_group_basedn=ldap_group_basedn,
|
||||
ldap_group_filter=ldap_group_filter,
|
||||
ldap_group_gid=ldap_group_gid,
|
||||
ldap_group_scope=ldap_group_scope,
|
||||
db_password=db_password,
|
||||
db_host=db_host,
|
||||
db_user=db_user,
|
||||
|
@ -89,6 +89,13 @@ var (
|
||||
env: "LDAP_VERIFY_CERT",
|
||||
parse: parseStringToBool,
|
||||
},
|
||||
common.LDAPGroupBaseDN: "LDAP_GROUP_BASEDN",
|
||||
common.LDAPGroupSearchFilter: "LDAP_GROUP_FILTER",
|
||||
common.LDAPGroupAttributeName: "LDAP_GROUP_GID",
|
||||
common.LDAPGroupSearchScope: &parser{
|
||||
env: "LDAP_GROUP_SCOPE",
|
||||
parse: parseStringToInt,
|
||||
},
|
||||
common.EmailHost: "EMAIL_HOST",
|
||||
common.EmailPort: &parser{
|
||||
env: "EMAIL_PORT",
|
||||
@ -152,7 +159,7 @@ var (
|
||||
repeatLoadEnvs = map[string]interface{}{
|
||||
common.ExtEndpoint: "EXT_ENDPOINT",
|
||||
common.MySQLPassword: "MYSQL_PWD",
|
||||
common.MySQLHost: "MYSQL_HOST",
|
||||
common.MySQLHost: "MYSQL_HOST",
|
||||
common.MySQLUsername: "MYSQL_USR",
|
||||
common.MySQLDatabase: "MYSQL_DATABASE",
|
||||
common.MySQLPort: &parser{
|
||||
@ -179,8 +186,8 @@ var (
|
||||
common.ClairDBPassword: "CLAIR_DB_PASSWORD",
|
||||
common.ClairDBHost: "CLAIR_DB_HOST",
|
||||
common.ClairDBUsername: "CLAIR_DB_USERNAME",
|
||||
common.ClairDBPort: &parser{
|
||||
env: "CLAIR_DB_PORT",
|
||||
common.ClairDBPort: &parser{
|
||||
env: "CLAIR_DB_PORT",
|
||||
parse: parseStringToInt,
|
||||
},
|
||||
common.UAAEndpoint: "UAA_ENDPOINT",
|
||||
@ -395,4 +402,5 @@ func validLdapScope(cfg map[string]interface{}, isMigrate bool) {
|
||||
ldapScope = 0
|
||||
}
|
||||
cfg[ldapScopeKey] = ldapScope
|
||||
|
||||
}
|
||||
|
@ -59,6 +59,10 @@ const (
|
||||
LDAPScope = "ldap_scope"
|
||||
LDAPTimeout = "ldap_timeout"
|
||||
LDAPVerifyCert = "ldap_verify_cert"
|
||||
LDAPGroupBaseDN = "ldap_group_base_dn"
|
||||
LDAPGroupSearchFilter = "ldap_group_search_filter"
|
||||
LDAPGroupAttributeName = "ldap_group_attribute_name"
|
||||
LDAPGroupSearchScope = "ldap_group_search_scope"
|
||||
TokenServiceURL = "token_service_url"
|
||||
RegistryURL = "registry_url"
|
||||
EmailHost = "email_host"
|
||||
|
@ -27,12 +27,21 @@ type LdapConf struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
// LdapUser ...
|
||||
type LdapUser struct {
|
||||
Username string `json:"ldap_username"`
|
||||
Email string `json:"ldap_email"`
|
||||
Realname string `json:"ldap_realname"`
|
||||
DN string `json:"-"`
|
||||
Username string `json:"ldap_username"`
|
||||
Email string `json:"ldap_email"`
|
||||
Realname string `json:"ldap_realname"`
|
||||
DN string `json:"-"`
|
||||
GroupDNList []string `json:"ldap_groupdn"`
|
||||
}
|
||||
|
||||
//LdapImportUser ...
|
||||
@ -45,3 +54,9 @@ 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:"group_dn,omitempty"`
|
||||
}
|
||||
|
@ -185,6 +185,7 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
|
||||
|
||||
for _, ldapEntry := range result.Entries {
|
||||
var u models.LdapUser
|
||||
groupDNList := []string{}
|
||||
for _, attr := range ldapEntry.Attributes {
|
||||
//OpenLdap sometimes contain leading space in useranme
|
||||
val := strings.TrimSpace(attr.Values[0])
|
||||
@ -200,7 +201,10 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
|
||||
u.Email = val
|
||||
case "email":
|
||||
u.Email = val
|
||||
case "memberof":
|
||||
groupDNList = append(groupDNList, val)
|
||||
}
|
||||
u.GroupDNList = groupDNList
|
||||
}
|
||||
u.DN = ldapEntry.DN
|
||||
ldapUsers = append(ldapUsers, u)
|
||||
@ -248,20 +252,28 @@ func (session *Session) Open() error {
|
||||
|
||||
// SearchLdap to search ldap with the provide filter
|
||||
func (session *Session) SearchLdap(filter string) (*goldap.SearchResult, error) {
|
||||
|
||||
if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil {
|
||||
return nil, fmt.Errorf("Can not bind search dn, error: %v", err)
|
||||
}
|
||||
|
||||
attributes := []string{"uid", "cn", "mail", "email"}
|
||||
attributes := []string{"uid", "cn", "mail", "email", "memberof"}
|
||||
lowerUID := strings.ToLower(session.ldapConfig.LdapUID)
|
||||
|
||||
if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" {
|
||||
attributes = append(attributes, session.ldapConfig.LdapUID)
|
||||
}
|
||||
return session.SearchLdapAttribute(session.ldapConfig.LdapBaseDn, filter, attributes)
|
||||
}
|
||||
|
||||
// SearchLdapAttribute - to search ldap with the provide filter, with specified attributes
|
||||
func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes []string) (*goldap.SearchResult, error) {
|
||||
|
||||
if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil {
|
||||
return nil, fmt.Errorf("Can not bind search dn, error: %v", err)
|
||||
}
|
||||
filter = strings.TrimSpace(filter)
|
||||
if !(strings.HasPrefix(filter, "(") || strings.HasSuffix(filter, ")")) {
|
||||
filter = "(" + filter + ")"
|
||||
}
|
||||
log.Debugf("Search ldap with filter:%v", filter)
|
||||
searchRequest := goldap.NewSearchRequest(
|
||||
session.ldapConfig.LdapBaseDn,
|
||||
baseDN,
|
||||
session.ldapConfig.LdapScope,
|
||||
goldap.NeverDerefAliases,
|
||||
0, //Unlimited results
|
||||
@ -318,3 +330,69 @@ func (session *Session) Close() {
|
||||
session.ldapConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
//SearchGroupByName ...
|
||||
func (session *Session) SearchGroupByName(groupName string) ([]models.LdapGroup, error) {
|
||||
ldapGroupConfig, err := config.LDAPGroupConf()
|
||||
log.Debugf("Ldap group config: %+v", ldapGroupConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session.searchGroup(ldapGroupConfig.LdapGroupBaseDN, ldapGroupConfig.LdapGroupFilter, groupName, ldapGroupConfig.LdapGroupNameAttribute)
|
||||
}
|
||||
|
||||
//SearchGroupByDN ...
|
||||
func (session *Session) SearchGroupByDN(groupDN string) ([]models.LdapGroup, error) {
|
||||
ldapGroupConfig, err := config.LDAPGroupConf()
|
||||
log.Debugf("Ldap group config: %+v", ldapGroupConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session.searchGroup(groupDN, ldapGroupConfig.LdapGroupFilter, "", ldapGroupConfig.LdapGroupNameAttribute)
|
||||
}
|
||||
|
||||
func (session *Session) searchGroup(baseDN, filter, groupName, groupNameAttribute string) ([]models.LdapGroup, error) {
|
||||
ldapGroups := make([]models.LdapGroup, 0)
|
||||
log.Debugf("Groupname: %v, basedn: %v", groupName, baseDN)
|
||||
ldapFilter := createGroupSearchFilter(filter, groupName, groupNameAttribute)
|
||||
attributes := []string{groupNameAttribute}
|
||||
result, err := session.SearchLdapAttribute(baseDN, ldapFilter, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ldapEntry := range result.Entries {
|
||||
var group models.LdapGroup
|
||||
group.GroupDN = ldapEntry.DN
|
||||
for _, attr := range ldapEntry.Attributes {
|
||||
//OpenLdap sometimes contain leading space in useranme
|
||||
val := strings.TrimSpace(attr.Values[0])
|
||||
log.Debugf("Current ldap entry attr name: %s\n", attr.Name)
|
||||
switch strings.ToLower(attr.Name) {
|
||||
case strings.ToLower(groupNameAttribute):
|
||||
group.GroupName = val
|
||||
}
|
||||
}
|
||||
ldapGroups = append(ldapGroups, group)
|
||||
}
|
||||
return ldapGroups, nil
|
||||
}
|
||||
|
||||
func createGroupSearchFilter(oldFilter, groupName, groupNameAttribute string) string {
|
||||
filter := ""
|
||||
groupName = goldap.EscapeFilter(groupName)
|
||||
groupNameAttribute = goldap.EscapeFilter(groupNameAttribute)
|
||||
if len(oldFilter) == 0 {
|
||||
if len(groupName) == 0 {
|
||||
filter = groupNameAttribute + "=*"
|
||||
} else {
|
||||
filter = groupNameAttribute + "=*" + groupName + "*"
|
||||
}
|
||||
} else {
|
||||
if len(groupName) == 0 {
|
||||
filter = oldFilter
|
||||
} else {
|
||||
filter = "(&(" + oldFilter + ")(" + groupNameAttribute + "=*" + groupName + "*))"
|
||||
}
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
@ -15,15 +15,16 @@ package ldap
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"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"
|
||||
goldap "gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
var adminServerLdapTestConfig = map[string]interface{}{
|
||||
@ -217,6 +218,14 @@ func TestSearchUser(t *testing.T) {
|
||||
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) {
|
||||
@ -254,3 +263,80 @@ func TestFormatURL(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_createGroupSearchFilter(t *testing.T) {
|
||||
type args struct {
|
||||
oldFilter string
|
||||
groupName string
|
||||
groupNameAttribute string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"Normal Filter", args{oldFilter: "objectclass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, "(&(objectclass=groupOfNames)(cn=*harbor_users*))"},
|
||||
{"Empty Old", args{groupName: "harbor_users", groupNameAttribute: "cn"}, "cn=*harbor_users*"},
|
||||
{"Empty Both", args{groupNameAttribute: "cn"}, "cn=*"},
|
||||
{"Empty name", args{oldFilter: "objectclass=groupOfNames", groupNameAttribute: "cn"}, "objectclass=groupOfNames"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := createGroupSearchFilter(tt.args.oldFilter, tt.args.groupName, tt.args.groupNameAttribute); got != tt.want {
|
||||
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 {
|
||||
baseDN string
|
||||
filter string
|
||||
groupName string
|
||||
groupNameAttribute string
|
||||
}
|
||||
|
||||
ldapConfig := models.LdapConf{
|
||||
LdapURL: adminServerLdapTestConfig[common.LDAPURL].(string) + ":389",
|
||||
LdapSearchDn: adminServerLdapTestConfig[common.LDAPSearchDN].(string),
|
||||
LdapScope: 2,
|
||||
LdapSearchPassword: adminServerLdapTestConfig[common.LDAPSearchPwd].(string),
|
||||
LdapBaseDn: adminServerLdapTestConfig[common.LDAPBaseDN].(string),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []models.LdapGroup
|
||||
wantErr bool
|
||||
}{
|
||||
{"normal search",
|
||||
fields{ldapConfig: ldapConfig},
|
||||
args{baseDN: "dc=example,dc=com", filter: "objectClass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"},
|
||||
[]models.LdapGroup{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.baseDN, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,10 @@ var adminServerDefaultConfig = map[string]interface{}{
|
||||
common.LDAPFilter: "",
|
||||
common.LDAPScope: 3,
|
||||
common.LDAPTimeout: 30,
|
||||
common.LDAPGroupBaseDN: "dc=example,dc=com",
|
||||
common.LDAPGroupSearchFilter: "objectClass=groupOfNames",
|
||||
common.LDAPGroupSearchScope: 2,
|
||||
common.LDAPGroupAttributeName: "cn",
|
||||
common.TokenServiceURL: "http://token_service",
|
||||
common.RegistryURL: "http://registry",
|
||||
common.EmailHost: "127.0.0.1",
|
||||
|
@ -41,6 +41,10 @@ var (
|
||||
common.LDAPScope,
|
||||
common.LDAPTimeout,
|
||||
common.LDAPVerifyCert,
|
||||
common.LDAPGroupAttributeName,
|
||||
common.LDAPGroupBaseDN,
|
||||
common.LDAPGroupSearchFilter,
|
||||
common.LDAPGroupSearchScope,
|
||||
common.EmailHost,
|
||||
common.EmailPort,
|
||||
common.EmailUsername,
|
||||
@ -66,6 +70,9 @@ var (
|
||||
common.LDAPBaseDN,
|
||||
common.LDAPUID,
|
||||
common.LDAPFilter,
|
||||
common.LDAPGroupAttributeName,
|
||||
common.LDAPGroupBaseDN,
|
||||
common.LDAPGroupSearchFilter,
|
||||
common.EmailHost,
|
||||
common.EmailUsername,
|
||||
common.EmailPassword,
|
||||
@ -80,6 +87,7 @@ var (
|
||||
common.EmailPort,
|
||||
common.LDAPScope,
|
||||
common.LDAPTimeout,
|
||||
common.LDAPGroupSearchScope,
|
||||
common.TokenExpiration,
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/adminserver/client"
|
||||
@ -205,6 +206,35 @@ func LDAPConf() (*models.LdapConf, error) {
|
||||
return ldapConf, nil
|
||||
}
|
||||
|
||||
// LDAPGroupConf returns the setting of ldap group search
|
||||
func LDAPGroupConf() (*models.LdapGroupConf, error) {
|
||||
|
||||
cfg, err := mg.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ldapGroupConf := &models.LdapGroupConf{LdapGroupSearchScope: 2}
|
||||
if _, ok := cfg[common.LDAPGroupBaseDN]; ok {
|
||||
ldapGroupConf.LdapGroupBaseDN = cfg[common.LDAPGroupBaseDN].(string)
|
||||
}
|
||||
if _, ok := cfg[common.LDAPGroupSearchFilter]; ok {
|
||||
ldapGroupConf.LdapGroupFilter = cfg[common.LDAPGroupSearchFilter].(string)
|
||||
}
|
||||
if _, ok := cfg[common.LDAPGroupAttributeName]; ok {
|
||||
ldapGroupConf.LdapGroupNameAttribute = cfg[common.LDAPGroupAttributeName].(string)
|
||||
}
|
||||
if _, ok := cfg[common.LDAPGroupSearchScope]; ok {
|
||||
if scopeStr, ok := cfg[common.LDAPGroupSearchScope].(string); ok {
|
||||
ldapGroupConf.LdapGroupSearchScope, err = strconv.Atoi(scopeStr)
|
||||
}
|
||||
if scopeFloat, ok := cfg[common.LDAPGroupSearchScope].(float64); ok {
|
||||
ldapGroupConf.LdapGroupSearchScope = int(scopeFloat)
|
||||
}
|
||||
}
|
||||
return ldapGroupConf, nil
|
||||
}
|
||||
|
||||
// TokenExpiration returns the token expiration time (in minute)
|
||||
func TokenExpiration() (int, error) {
|
||||
cfg, err := mg.Get()
|
||||
|
@ -75,6 +75,10 @@ func TestConfig(t *testing.T) {
|
||||
t.Fatalf("failed to get ldap settings: %v", err)
|
||||
}
|
||||
|
||||
if _, err := LDAPGroupConf(); err != nil {
|
||||
t.Fatalf("failed to get ldap group settings: %v", err)
|
||||
}
|
||||
|
||||
if _, err := TokenExpiration(); err != nil {
|
||||
t.Fatalf("failed to get token expiration: %v", err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user