Merge pull request #4412 from stonezdj/add_ldap_group_param

Add LDAP Group Search Configure Param
This commit is contained in:
stone 2018-03-15 17:02:28 +08:00 committed by GitHub
commit 139a0c59ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 277 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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