diff --git a/make/common/templates/adminserver/env b/make/common/templates/adminserver/env index 0b7c555bc..cfcd1b591 100644 --- a/make/common/templates/adminserver/env +++ b/make/common/templates/adminserver/env @@ -11,6 +11,7 @@ LDAP_FILTER=$ldap_filter LDAP_UID=$ldap_uid LDAP_SCOPE=$ldap_scope LDAP_TIMEOUT=$ldap_timeout +LDAP_VERIFY_CERT=true DATABASE_TYPE=mysql MYSQL_HOST=$db_host MYSQL_PORT=$db_port diff --git a/src/adminserver/systemcfg/systemcfg.go b/src/adminserver/systemcfg/systemcfg.go index 9cbb5d4c8..b60aedd43 100644 --- a/src/adminserver/systemcfg/systemcfg.go +++ b/src/adminserver/systemcfg/systemcfg.go @@ -81,6 +81,10 @@ var ( env: "LDAP_TIMEOUT", parse: parseStringToInt, }, + common.LDAPVerifyCert: &parser{ + env: "LDAP_VERIFY_CERT", + parse: parseStringToBool, + }, common.EmailHost: "EMAIL_HOST", common.EmailPort: &parser{ env: "EMAIL_PORT", diff --git a/src/adminserver/systemcfg/systemcfg_test.go b/src/adminserver/systemcfg/systemcfg_test.go index 6a55ceece..cbc71a29a 100644 --- a/src/adminserver/systemcfg/systemcfg_test.go +++ b/src/adminserver/systemcfg/systemcfg_test.go @@ -90,11 +90,16 @@ func TestLoadFromEnv(t *testing.T) { t.Fatalf("failed to set env: %v", err) } + if err := os.Setenv("LDAP_VERIFY_CERT", "false"); err != nil { + t.Fatalf("failed to set env: %v", err) + } + cfgs = map[string]interface{}{} err = LoadFromEnv(cfgs, false) assert.Nil(t, err) assert.Equal(t, extEndpoint, cfgs[common.ExtEndpoint]) assert.Equal(t, ldapURL, cfgs[common.LDAPURL]) + assert.Equal(t, false, cfgs[common.LDAPVerifyCert]) os.Clearenv() if err := os.Setenv("LDAP_URL", ldapURL); err != nil { @@ -104,6 +109,10 @@ func TestLoadFromEnv(t *testing.T) { t.Fatalf("failed to set env: %v", err) } + if err := os.Setenv("LDAP_VERIFY_CERT", "true"); err != nil { + t.Fatalf("failed to set env: %v", err) + } + cfgs = map[string]interface{}{ common.LDAPURL: "ldap_url", } @@ -111,4 +120,5 @@ func TestLoadFromEnv(t *testing.T) { assert.Nil(t, err) assert.Equal(t, extEndpoint, cfgs[common.ExtEndpoint]) assert.Equal(t, "ldap_url", cfgs[common.LDAPURL]) + assert.Equal(t, true, cfgs[common.LDAPVerifyCert]) } diff --git a/src/common/const.go b/src/common/const.go index ae39ee3ea..c3f01cc81 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -48,6 +48,7 @@ const ( LDAPFilter = "ldap_filter" LDAPScope = "ldap_scope" LDAPTimeout = "ldap_timeout" + LDAPVerifyCert = "ldap_verify_cert" TokenServiceURL = "token_service_url" RegistryURL = "registry_url" EmailHost = "email_host" diff --git a/src/common/models/config.go b/src/common/models/config.go index ba5c91863..d5c7c4e21 100644 --- a/src/common/models/config.go +++ b/src/common/models/config.go @@ -33,6 +33,7 @@ type LDAP struct { UID string `json:"uid"` Scope int `json:"scope"` Timeout int `json:"timeout"` // in second + VerifyCert bool `json:"verify_cert"` } // Database ... diff --git a/src/common/models/ldap.go b/src/common/models/ldap.go index e26cdfe3f..48e03cc77 100644 --- a/src/common/models/ldap.go +++ b/src/common/models/ldap.go @@ -24,6 +24,7 @@ type LdapConf struct { LdapUID string `json:"ldap_uid"` LdapScope int `json:"ldap_scope"` LdapConnectionTimeout int `json:"ldap_connection_timeout"` + LdapVerifyCert bool `json:"ldap_verify_cert"` } // LdapUser ... diff --git a/src/common/utils/ldap/ldap.go b/src/common/utils/ldap/ldap.go index dc1a7cd97..524008793 100644 --- a/src/common/utils/ldap/ldap.go +++ b/src/common/utils/ldap/ldap.go @@ -60,6 +60,7 @@ func GetSystemLdapConf() (models.LdapConf, error) { ldapConfs.LdapUID = ldap.UID ldapConfs.LdapScope = ldap.Scope ldapConfs.LdapConnectionTimeout = ldap.Timeout + ldapConfs.LdapVerifyCert = ldap.VerifyCert // ldapConfs = config.LDAP().URL // ldapConfs.LdapSearchDn = config.LDAP().SearchDn @@ -205,7 +206,8 @@ func SearchUser(ldapConfs models.LdapConf) ([]models.LdapUser, error) { for _, ldapEntry := range result.Entries { var u models.LdapUser for _, attr := range ldapEntry.Attributes { - val := attr.Values[0] + //OpenLDAP sometimes contains leading space in username + val := strings.TrimSpace(attr.Values[0]) log.Debugf("Current ldap entry attr name: %s\n", attr.Name) switch strings.ToLower(attr.Name) { case strings.ToLower(ldapConfs.LdapUID): @@ -337,6 +339,7 @@ func dialLDAP(ldapConfs models.LdapConf) (*goldap.Conn, error) { var ldap *goldap.Conn splitLdapURL := strings.Split(ldapConfs.LdapURL, "://") protocol, hostport := splitLdapURL[0], splitLdapURL[1] + host := strings.Split(hostport, ":")[0] // Sets a Dial Timeout for LDAP connectionTimeout := ldapConfs.LdapConnectionTimeout @@ -346,7 +349,8 @@ func dialLDAP(ldapConfs models.LdapConf) (*goldap.Conn, error) { case "ldap": ldap, err = goldap.Dial("tcp", hostport) case "ldaps": - ldap, err = goldap.DialTLS("tcp", hostport, &tls.Config{InsecureSkipVerify: true}) + log.Debug("Start to dial ldaps") + ldap, err = goldap.DialTLS("tcp", hostport, &tls.Config{ServerName: host, InsecureSkipVerify: !ldapConfs.LdapVerifyCert}) } return ldap, err @@ -401,3 +405,38 @@ func searchLDAP(ldapConfs models.LdapConf, ldap *goldap.Conn) (*goldap.SearchRes return result, nil } + +// SearchAndImportUser - Search user in LDAP, if this user exist, import it to database +func SearchAndImportUser(username string) (int64, error) { + var err error + var userID int64 + + ldapConfs, err := GetSystemLdapConf() + if err != nil { + log.Errorf("Can not get ldap configuration, error %v", err) + return 0, err + } + ldapConfs, err = ValidateLdapConf(ldapConfs) + if err != nil { + log.Errorf("Invalid ldap request, error: %v", err) + return 0, err + } + + ldapConfs.LdapFilter = MakeFilter(username, ldapConfs.LdapFilter, ldapConfs.LdapUID) + log.Debugf("Search with LDAP with filter %s", ldapConfs.LdapFilter) + + ldapUsers, err := SearchUser(ldapConfs) + if err != nil { + log.Errorf("Can not search ldap, error %v, filter: %s", err, ldapConfs.LdapFilter) + return 0, err + } + + if len(ldapUsers) > 0 { + log.Debugf("Importing user %s to local database", ldapUsers[0].Username) + if userID, err = ImportUser(ldapUsers[0]); err != nil { + log.Errorf("Can not import ldap user to local db, error %v", err) + return 0, err + } + } + return userID, err +} diff --git a/src/common/utils/ldap/ldap_test.go b/src/common/utils/ldap/ldap_test.go index 9dae96da7..bfff44330 100644 --- a/src/common/utils/ldap/ldap_test.go +++ b/src/common/utils/ldap/ldap_test.go @@ -17,6 +17,7 @@ package ldap import ( //"fmt" //"strings" + "os" "testing" @@ -65,6 +66,45 @@ var adminServerLdapTestConfig = map[string]interface{}{ common.AdminInitialPassword: "password", } +var adminServerDefaultConfigWithVerifyCert = map[string]interface{}{ + common.ExtEndpoint: "https://host01.com", + common.AUTHMode: common.LDAPAuth, + common.DatabaseType: "mysql", + common.MySQLHost: "127.0.0.1", + common.MySQLPort: 3306, + common.MySQLUsername: "root", + common.MySQLPassword: "root123", + common.MySQLDatabase: "registry", + common.SQLiteFile: "/tmp/registry.db", + 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.CfgExpiration: 5, + common.AdminInitialPassword: "password", + common.AdmiralEndpoint: "http://www.vmware.com", + common.WithNotary: false, + common.WithClair: false, +} + func TestMain(t *testing.T) { server, err := test.NewAdminserver(adminServerLdapTestConfig) if err != nil { @@ -125,6 +165,7 @@ func TestGetSystemLdapConf(t *testing.T) { } func TestValidateLdapConf(t *testing.T) { + testLdapConfig, err := GetSystemLdapConf() if err != nil { t.Fatalf("failed to get system ldap config %v", err) @@ -138,6 +179,7 @@ func TestValidateLdapConf(t *testing.T) { } func TestMakeFilter(t *testing.T) { + testLdapConfig, err := GetSystemLdapConf() if err != nil { @@ -254,3 +296,26 @@ func TestSearchUser(t *testing.T) { t.Errorf("unexpected ldap user search result: %s = %s", "ldapUsers[0].Username", ldapUsers[0].Username) } } + +func TestSearchAndImportUser(t *testing.T) { + + userID, err := SearchAndImportUser("test") + + if err != nil { + t.Fatalf("Failed on error : %v ", err) + } + + if userID <= 0 { + t.Fatalf("userID= %v", userID) + } +} + +func TestSearchAndImportUserNotExist(t *testing.T) { + + userID, _ := SearchAndImportUser("notexist") + + if userID > 0 { + t.Fatal("Can not import a non exist ldap user!") + t.Fail() + } +} diff --git a/src/ui/api/config.go b/src/ui/api/config.go index 768394ba4..79beb1d52 100644 --- a/src/ui/api/config.go +++ b/src/ui/api/config.go @@ -40,6 +40,7 @@ var ( common.LDAPFilter, common.LDAPScope, common.LDAPTimeout, + common.LDAPVerifyCert, common.EmailHost, common.EmailPort, common.EmailUsername, @@ -80,6 +81,7 @@ var ( common.EmailSSL, common.EmailInsecure, common.SelfRegistration, + common.LDAPVerifyCert, } passwordKeys = []string{ diff --git a/src/ui/api/member.go b/src/ui/api/member.go index e1bc02041..0ac5e35af 100644 --- a/src/ui/api/member.go +++ b/src/ui/api/member.go @@ -17,10 +17,13 @@ package api import ( "fmt" "net/http" + "strings" "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/config" ) // ProjectMemberAPI handles request to /api/projects/{}/members/{} @@ -158,12 +161,38 @@ func (pma *ProjectMemberAPI) Post() { var req memberReq pma.DecodeJSONReq(&req) - username := req.Username + username := strings.TrimSpace(req.Username) userID := checkUserExists(username) if userID <= 0 { - log.Warningf("User does not exist, user name: %s", username) - pma.RenderError(http.StatusNotFound, "User does not exist") - return + //check current authorization mode + authMode, err := config.AuthMode() + if err != nil { + log.Errorf("Failed the retrieve auth_mode, error: %s", err) + pma.RenderError(http.StatusInternalServerError, "Failed to retrieve auth_mode") + return + } + + if authMode != "ldap_auth" { + log.Errorf("User does not exist, user name: %s", username) + pma.RenderError(http.StatusNotFound, "User does not exist") + return + } + + //search and import user + newUserID, err := ldapUtils.SearchAndImportUser(username) + if err != nil { + log.Errorf("Search and import user failed, error: %v ", err) + pma.RenderError(http.StatusInternalServerError, "Failed to search and import user") + return + } + + if newUserID <= 0 { + log.Error("Failed to create user") + pma.RenderError(http.StatusNotFound, "Failed to create user") + return + } + + userID = int(newUserID) } rolelist, err := dao.GetUserProjectRoles(userID, projectID) if err != nil { diff --git a/src/ui/config/config.go b/src/ui/config/config.go index 8d3d9c8bf..1609e82d3 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -173,7 +173,6 @@ func LDAP() (*models.LDAP, error) { if err != nil { return nil, err } - ldap := &models.LDAP{} ldap.URL = cfg[common.LDAPURL].(string) ldap.SearchDN = cfg[common.LDAPSearchDN].(string) @@ -183,6 +182,11 @@ func LDAP() (*models.LDAP, error) { ldap.Filter = cfg[common.LDAPFilter].(string) ldap.Scope = int(cfg[common.LDAPScope].(float64)) ldap.Timeout = int(cfg[common.LDAPTimeout].(float64)) + if cfg[common.LDAPVerifyCert] != nil { + ldap.VerifyCert = cfg[common.LDAPVerifyCert].(bool) + } else { + ldap.VerifyCert = true + } return ldap, nil } diff --git a/src/ui_ng/lib/src/config/config.ts b/src/ui_ng/lib/src/config/config.ts index fe024927c..decc1fd95 100644 --- a/src/ui_ng/lib/src/config/config.ts +++ b/src/ui_ng/lib/src/config/config.ts @@ -64,6 +64,7 @@ export class Configuration { ldap_timeout: NumberValueItem; ldap_uid: StringValueItem; ldap_url: StringValueItem; + ldap_verify_cert: BoolValueItem; email_host: StringValueItem; email_identity: StringValueItem; email_from: StringValueItem; @@ -89,6 +90,7 @@ export class Configuration { this.ldap_timeout = new NumberValueItem(5, true); this.ldap_uid = new StringValueItem("", true); this.ldap_url = new StringValueItem("", true); + this.ldap_verify_cert = new BoolValueItem(true, true); this.email_host = new StringValueItem("", true); this.email_identity = new StringValueItem("", true); this.email_from = new StringValueItem("", true); diff --git a/src/ui_ng/src/app/config/auth/config-auth.component.html b/src/ui_ng/src/app/config/auth/config-auth.component.html index d982f0894..23bbf383b 100644 --- a/src/ui_ng/src/app/config/auth/config-auth.component.html +++ b/src/ui_ng/src/app/config/auth/config-auth.component.html @@ -111,6 +111,15 @@ {{'CONFIG.TOOLTIP.LDAP_SCOPE' | translate}} +