From ec67974104222fb3bec6d7b773b90bce93315e73 Mon Sep 17 00:00:00 2001 From: stonezdj Date: Wed, 22 Nov 2017 12:19:13 +0800 Subject: [PATCH] Refactor ldap Changes include: 1. Use Session to manage the lifecycle of ldap connections 2. Abstract common AuthenticateHelper interface for db_auth, ldap_auth, uaa_auth mode --- src/common/utils/ldap/ldap.go | 504 +++++++++++++---------------- src/common/utils/ldap/ldap_test.go | 283 +++++++--------- src/ui/api/ldap.go | 111 +++---- src/ui/api/member.go | 35 +- src/ui/auth/auth_test.go | 24 ++ src/ui/auth/authenticator.go | 54 +++- src/ui/auth/db/db.go | 17 +- src/ui/auth/db/db_test.go | 132 +++++++- src/ui/auth/ldap/ldap.go | 76 ++++- src/ui/auth/ldap/ldap_test.go | 78 ++++- src/ui/auth/uaa/uaa.go | 20 +- 11 files changed, 754 insertions(+), 580 deletions(-) diff --git a/src/common/utils/ldap/ldap.go b/src/common/utils/ldap/ldap.go index 524008793..dfdf33c02 100644 --- a/src/common/utils/ldap/ldap.go +++ b/src/common/utils/ldap/ldap.go @@ -15,15 +15,13 @@ package ldap import ( + "crypto/tls" "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" @@ -31,77 +29,68 @@ import ( goldap "gopkg.in/ldap.v2" ) -// GetSystemLdapConf ... -func GetSystemLdapConf() (models.LdapConf, error) { - var err error - var ldapConfs models.LdapConf - var authMode string +//Session - define a LDAP session +type Session struct { + ldapConfig models.LdapConf + ldapConn *goldap.Conn +} - authMode, err = config.AuthMode() +//LoadSystemLdapConfig - load LDAP configure from adminserver +func LoadSystemLdapConfig() (*Session, error) { + var session Session + + authMode, err := config.AuthMode() if err != nil { log.Errorf("can't load auth mode from system, error: %v", err) - return ldapConfs, err + return nil, err } if authMode != "ldap_auth" { - return ldapConfs, fmt.Errorf("system auth_mode isn't ldap_auth, please check configuration") + return nil, fmt.Errorf("system auth_mode isn't ldap_auth, please check configuration") } ldap, err := config.LDAP() + if err != nil { - return ldapConfs, err + return nil, err + } + if ldap.URL == "" { + return nil, fmt.Errorf("can not get any available LDAP_URL") } - 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.LdapVerifyCert = ldap.VerifyCert + ldapURL, err := formatURL(ldap.URL) + if err != nil { + return nil, err + } - // 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 - // } + session.ldapConfig.LdapURL = ldapURL + session.ldapConfig.LdapSearchDn = ldap.SearchDN + session.ldapConfig.LdapSearchPassword = ldap.SearchPassword + session.ldapConfig.LdapBaseDn = ldap.BaseDN + session.ldapConfig.LdapFilter = ldap.Filter + session.ldapConfig.LdapUID = ldap.UID + session.ldapConfig.LdapConnectionTimeout = ldap.Timeout + session.ldapConfig.LdapVerifyCert = ldap.VerifyCert + log.Debugf("Load system configuration: %v", ldap) - // 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 + switch ldap.Scope { + case 1: + session.ldapConfig.LdapScope = goldap.ScopeBaseObject + case 2: + session.ldapConfig.LdapScope = goldap.ScopeSingleLevel + case 3: + session.ldapConfig.LdapScope = goldap.ScopeWholeSubtree + default: + log.Errorf("invalid ldap search scope %v", ldap.Scope) + return nil, fmt.Errorf("invalid ldap search scope") + } + return &session, nil } -// ValidateLdapConf ... -func ValidateLdapConf(ldapConfs models.LdapConf) (models.LdapConf, error) { - var err error +// CreateWithUIConfig - create a Session with config from UI +func CreateWithUIConfig(ldapConfs models.LdapConf) (*Session, 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 @@ -110,131 +99,44 @@ func ValidateLdapConf(ldapConfs models.LdapConf) (models.LdapConf, error) { case 3: ldapConfs.LdapScope = goldap.ScopeWholeSubtree default: - return ldapConfs, fmt.Errorf("invalid ldap search scope") + return nil, 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 - + return createWithInternalConfig(ldapConfs) } -// MakeFilter ... -func MakeFilter(username string, ldapFilter string, ldapUID string) string { +// createWithInternalConfig - create a Session with internal config +func createWithInternalConfig(ldapConfs models.LdapConf) (*Session, error) { - var filterTag string + var session Session - if username == "" { - filterTag = "*" - } else { - filterTag = username + if ldapConfs.LdapURL == "" { + return nil, fmt.Errorf("can not get any available LDAP_URL") } - 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) - - 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) - - 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) - + ldapURL, err := formatURL(ldapConfs.LdapURL) if err != nil { return nil, err } - for _, ldapEntry := range result.Entries { - var u models.LdapUser - for _, attr := range ldapEntry.Attributes { - //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): - u.Username = val - case "uid": - u.Realname = val - case "cn": - u.Realname = val - case "mail": - u.Email = val - case "email": - u.Email = val - } - } - u.DN = ldapEntry.DN - ldapUsers = append(ldapUsers, u) - } + session.ldapConfig.LdapURL = ldapURL + session.ldapConfig.LdapSearchDn = ldapConfs.LdapSearchDn + session.ldapConfig.LdapSearchPassword = ldapConfs.LdapSearchPassword + session.ldapConfig.LdapBaseDn = ldapConfs.LdapBaseDn + session.ldapConfig.LdapFilter = ldapConfs.LdapFilter + session.ldapConfig.LdapUID = ldapConfs.LdapUID + session.ldapConfig.LdapConnectionTimeout = ldapConfs.LdapConnectionTimeout + session.ldapConfig.LdapVerifyCert = ldapConfs.LdapVerifyCert + session.ldapConfig.LdapScope = ldapConfs.LdapScope + return &session, nil - return ldapUsers, nil } -func formatLdapURL(ldapURL string) (string, error) { +func formatURL(ldapURL string) (string, error) { var protocol, hostport string - var err error - _, err = url.Parse(ldapURL) + _, err := url.Parse(ldapURL) if err != nil { return "", fmt.Errorf("parse Ldap Host ERR: %s", err) } @@ -243,7 +145,7 @@ func formatLdapURL(ldapURL string) (string, error) { splitLdapURL := strings.Split(ldapURL, "://") protocol, hostport = splitLdapURL[0], splitLdapURL[1] if !((protocol == "ldap") || (protocol == "ldaps")) { - return "", fmt.Errorf("unknown ldap protocl") + return "", fmt.Errorf("unknown ldap protocol") } } else { hostport = ldapURL @@ -275,128 +177,159 @@ func formatLdapURL(ldapURL string) (string, error) { } -// 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") +//ConnectionTest - test ldap session connection with system default setting +func (session *Session) ConnectionTest() error { + session, err := LoadSystemLdapConfig() if err != nil { - log.Errorf("system checking user %s failed, error: %v", user.Username, err) - return 0, fmt.Errorf("internal_error") + return fmt.Errorf("Failed to load system ldap config") } - 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 = "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 + return ConnectionTestWithConfig(session.ldapConfig) } -// Bind establish a connection to ldap based on ldapConfs and bind the user with given parameters. -func Bind(ldapConfs models.LdapConf, dn string, password string) error { - conn, err := dialLDAP(ldapConfs) +//ConnectionTestWithConfig - test ldap session connection, out of the scope of normal session create/close +func ConnectionTestWithConfig(ldapConfig models.LdapConf) error { + + //If no password present, use the system default password + if ldapConfig.LdapSearchPassword == "" { + + session, err := LoadSystemLdapConfig() + + if err != nil { + return fmt.Errorf("Failed to load system ldap config") + } + + ldapConfig.LdapSearchPassword = session.ldapConfig.LdapSearchPassword + } + + testSession, err := createWithInternalConfig(ldapConfig) + if err != nil { return err } - defer conn.Close() - if ldapConfs.LdapSearchDn != "" { - if err := bindLDAPSearchDN(ldapConfs, conn); err != nil { + err = testSession.Open() + + if err != nil { + return err + } + + defer testSession.Close() + + if testSession.ldapConfig.LdapSearchDn != "" { + err = testSession.Bind(testSession.ldapConfig.LdapSearchDn, testSession.ldapConfig.LdapSearchPassword) + if err != nil { return err } } - return conn.Bind(dn, password) -} - -func dialLDAP(ldapConfs models.LdapConf) (*goldap.Conn, error) { - - var err 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 - goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second - - switch protocol { - case "ldap": - ldap, err = goldap.Dial("tcp", hostport) - case "ldaps": - log.Debug("Start to dial ldaps") - ldap, err = goldap.DialTLS("tcp", hostport, &tls.Config{ServerName: host, InsecureSkipVerify: !ldapConfs.LdapVerifyCert}) - } - - 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) { +//SearchUser - search LDAP user by name +func (session *Session) SearchUser(username string) ([]models.LdapUser, error) { + var ldapUsers []models.LdapUser + ldapFilter := session.createUserFilter(username) + result, err := session.SearchLdap(ldapFilter) - var err error - ldapBaseDn := ldapConfs.LdapBaseDn - ldapScope := ldapConfs.LdapScope - ldapFilter := ldapConfs.LdapFilter + if err != nil { + return nil, err + } + + for _, ldapEntry := range result.Entries { + var u models.LdapUser + 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(session.ldapConfig.LdapUID): + u.Username = val + case "uid": + u.Realname = val + case "cn": + u.Realname = val + case "mail": + u.Email = val + case "email": + u.Email = val + } + } + u.DN = ldapEntry.DN + ldapUsers = append(ldapUsers, u) + + } + + return ldapUsers, nil + +} + +// Bind with specified DN and password, used in authentication +func (session *Session) Bind(dn string, password string) error { + return session.ldapConn.Bind(dn, password) +} + +//Open - open Session +func (session *Session) Open() error { + + splitLdapURL := strings.Split(session.ldapConfig.LdapURL, "://") + protocol, hostport := splitLdapURL[0], splitLdapURL[1] + host := strings.Split(hostport, ":")[0] + + connectionTimeout := session.ldapConfig.LdapConnectionTimeout + goldap.DefaultTimeout = time.Duration(connectionTimeout) * time.Second + + switch protocol { + case "ldap": + ldap, err := goldap.Dial("tcp", hostport) + if err != nil { + return err + } + session.ldapConn = ldap + case "ldaps": + log.Debug("Start to dial ldaps") + ldap, err := goldap.DialTLS("tcp", hostport, &tls.Config{ServerName: host, InsecureSkipVerify: !session.ldapConfig.LdapVerifyCert}) + if err != nil { + return err + } + session.ldapConn = ldap + } + + return nil + +} + +// 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"} - lowerUID := strings.ToLower(ldapConfs.LdapUID) + lowerUID := strings.ToLower(session.ldapConfig.LdapUID) + if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" { - attributes = append(attributes, ldapConfs.LdapUID) + attributes = append(attributes, session.ldapConfig.LdapUID) } + log.Debugf("Search ldap with filter:%v", filter) searchRequest := goldap.NewSearchRequest( - ldapBaseDn, - ldapScope, + session.ldapConfig.LdapBaseDn, + session.ldapConfig.LdapScope, goldap.NeverDerefAliases, - 0, // Unlimited results. - 0, // Search Timeout. - false, // Types Only - ldapFilter, + 0, //Unlimited results + 0, //Search Timeout + false, //Types only + filter, attributes, nil, ) - result, err := ldap.Search(searchRequest) + result, err := session.ldapConn.Search(searchRequest) + if result != nil { + log.Debugf("Found entries:%v\n", len(result.Entries)) + } else { + log.Debugf("No entries") + } if err != nil { log.Debug("LDAP search error", err) @@ -404,39 +337,36 @@ 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 +//CreateUserFilter - create filter to search user with specified username +func (session *Session) createUserFilter(username string) string { + var filterTag string - 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 + if username == "" { + filterTag = "*" + } else { + filterTag = username } - ldapConfs.LdapFilter = MakeFilter(username, ldapConfs.LdapFilter, ldapConfs.LdapUID) - log.Debugf("Search with LDAP with filter %s", ldapConfs.LdapFilter) + ldapFilter := session.ldapConfig.LdapFilter + ldapUID := session.ldapConfig.LdapUID - ldapUsers, err := SearchUser(ldapConfs) - if err != nil { - log.Errorf("Can not search ldap, error %v, filter: %s", err, ldapConfs.LdapFilter) - return 0, err + if ldapFilter == "" { + ldapFilter = "(" + ldapUID + "=" + filterTag + ")" + } else { + ldapFilter = "(&" + ldapFilter + "(" + ldapUID + "=" + filterTag + "))" } - 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 + log.Debug("ldap filter :", ldapFilter) + + return ldapFilter +} + +//Close - close current session +func (session *Session) Close() { + if session.ldapConn != nil { + session.ldapConn.Close() + } } diff --git a/src/common/utils/ldap/ldap_test.go b/src/common/utils/ldap/ldap_test.go index bfff44330..cf088c1c0 100644 --- a/src/common/utils/ldap/ldap_test.go +++ b/src/common/utils/ldap/ldap_test.go @@ -11,19 +11,16 @@ // 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/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" @@ -40,29 +37,15 @@ var adminServerLdapTestConfig = map[string]interface{}{ common.MySQLDatabase: "registry", common.SQLiteFile: "/tmp/registry.db", //config.SelfRegistration: true, - common.LDAPURL: "ldap://127.0.0.1", - common.LDAPSearchDN: "cn=admin,dc=example,dc=com", - common.LDAPSearchPwd: "admin", - common.LDAPBaseDN: "dc=example,dc=com", - common.LDAPUID: "uid", - common.LDAPFilter: "", - common.LDAPScope: 3, - common.LDAPTimeout: 30, - // 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, - common.CfgExpiration: 5, - // config.JobLogDir: "/var/log/jobs", + common.LDAPURL: "ldap://127.0.0.1", + common.LDAPSearchDN: "cn=admin,dc=example,dc=com", + common.LDAPSearchPwd: "admin", + common.LDAPBaseDN: "dc=example,dc=com", + common.LDAPUID: "uid", + common.LDAPFilter: "", + common.LDAPScope: 3, + common.LDAPTimeout: 30, + common.CfgExpiration: 5, common.AdminInitialPassword: "password", } @@ -132,15 +115,6 @@ func TestMain(t *testing.T) { 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) @@ -151,171 +125,134 @@ func TestMain(t *testing.T) { } } -func TestGetSystemLdapConf(t *testing.T) { - - testLdapConfig, err := GetSystemLdapConf() - +func TestLoadSystemLdapConfig(t *testing.T) { + session, err := LoadSystemLdapConfig() 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) + if session.ldapConfig.LdapURL != "ldap://127.0.0.1:389" { + t.Errorf("unexpected LdapURL: %s != %s", session.ldapConfig.LdapURL, "ldap://127.0.0.1:389") } - 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) + if session.ldapConfig.LdapScope != 2 { + t.Errorf("unexpected LdapScope: %d != %d", session.ldapConfig.LdapScope, 2) } } func TestConnectTest(t *testing.T) { - - testLdapConfig, err := GetSystemLdapConf() - + session, err := LoadSystemLdapConfig() if err != nil { - t.Fatalf("failed to get system ldap config %v", err) + t.Errorf("failed to load system ldap config") + } + err = session.ConnectionTest() + if err != nil { + t.Errorf("Unexpected ldap connect fail: %v", err) } - testLdapConfig.LdapURL = "ldap://localhost:389" +} - err = ConnectTest(testLdapConfig) - if err != nil { - t.Errorf("unexpected ldap connect fail: %v", err) +func TestCreateUIConfig(t *testing.T) { + var testConfigs = []struct { + config models.LdapConf + internalValue int + }{ + { + models.LdapConf{ + LdapScope: 3, + LdapURL: "ldaps://127.0.0.1", + }, 2}, + { + models.LdapConf{ + LdapScope: 2, + LdapURL: "ldaps://127.0.0.1", + }, 1}, + { + models.LdapConf{ + LdapScope: 1, + LdapURL: "ldaps://127.0.0.1", + }, 0}, } + + for _, val := range testConfigs { + session, err := CreateWithUIConfig(val.config) + if err != nil { + t.Fatalf("Can not create with ui config, err:%v", err) + } + if session.ldapConfig.LdapScope != val.internalValue { + t.Fatalf("Test failed expected %v, actual %v", val.internalValue, session.ldapConfig.LdapScope) + } + } + } func TestSearchUser(t *testing.T) { - testLdapConfig, err := GetSystemLdapConf() - + session, err := LoadSystemLdapConfig() if err != nil { - t.Fatalf("failed to get system ldap config %v", err) + t.Fatalf("Can not load system ldap config") } - testLdapConfig.LdapURL = "ldap://localhost:389" - testLdapConfig.LdapFilter = MakeFilter("", testLdapConfig.LdapFilter, testLdapConfig.LdapUID) - - ldapUsers, err := SearchUser(testLdapConfig) + err = session.Open() if err != nil { - t.Errorf("unexpected ldap search fail: %v", err) + t.Fatalf("failed to create ldap session %v", err) } - if ldapUsers[0].Username != "test" { - t.Errorf("unexpected ldap user search result: %s = %s", "ldapUsers[0].Username", ldapUsers[0].Username) + err = session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword) + if err != nil { + t.Fatalf("failed to bind search dn") + } + + defer session.Close() + + result, err := session.SearchUser("test") + if err != nil || len(result) == 0 { + t.Fatalf("failed to search user test!") + } + +} + +func InitTest(ldapTestConfig map[string]interface{}, t *testing.T) { + server, err := test.NewAdminserver(ldapTestConfig) + 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) + } + + if err := uiConfig.Init(); err != nil { + t.Fatalf("failed to initialize configurations: %v ", err) } } -func TestSearchAndImportUser(t *testing.T) { +func TestFormatURL(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!") + var invalidURL = "http://localhost:389" + _, err := formatURL(invalidURL) + if err == nil { + t.Fatalf("Should failed on invalid URL %v", invalidURL) t.Fail() } + + var urls = []struct { + rawURL string + goodURL string + }{ + {"ldaps://127.0.0.1", "ldaps://127.0.0.1:636"}, + {"ldap://9.123.102.33", "ldap://9.123.102.33:389"}, + {"ldaps://127.0.0.1:389", "ldaps://127.0.0.1:389"}, + {"ldap://127.0.0.1:636", "ldaps://127.0.0.1:636"}, + {"112.122.122.122", "ldap://112.122.122.122:389"}, + } + + for _, u := range urls { + goodURL, err := formatURL(u.rawURL) + if err != nil || goodURL != u.goodURL { + t.Fatalf("Faild on URL: raw=%v, expected:%v, actual:%v", u.rawURL, u.goodURL, goodURL) + } + } + } diff --git a/src/ui/api/ldap.go b/src/ui/api/ldap.go index 48ffa9e0d..e4de922fd 100644 --- a/src/ui/api/ldap.go +++ b/src/ui/api/ldap.go @@ -15,7 +15,6 @@ package api import ( - "encoding/json" "fmt" "net/http" "strings" @@ -23,6 +22,7 @@ import ( "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" ) // LdapAPI handles requesst to /api/ldap/ping /api/ldap/user/search /api/ldap/user/import @@ -47,47 +47,28 @@ func (l *LdapAPI) Prepare() { // Ping ... func (l *LdapAPI) Ping() { - var err error var ldapConfs models.LdapConf - 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) - v := map[string]interface{}{} - if err := json.Unmarshal(l.Ctx.Input.RequestBody, - &v); err != nil { - log.Errorf("failed to unmarshal LDAP server settings: %v", err) - l.RenderError(http.StatusInternalServerError, "") - return - } - if _, ok := v["ldap_search_password"]; !ok { - settings, 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 - } - ldapConfs.LdapSearchPassword = settings.LdapSearchPassword - } - } + var ldapSession *ldapUtils.Session - ldapConfs, err = ldapUtils.ValidateLdapConf(ldapConfs) + l.Ctx.Input.CopyBody(1 << 32) + + ldapSession, err := ldapUtils.LoadSystemLdapConfig() if err != nil { - log.Errorf("Invalid ldap request, error: %v", err) - l.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid ldap request: %v", err)) + log.Errorf("Can't load system configuration, error: %v", err) + l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't load system configuration: %v", err)) return } - err = ldapUtils.ConnectTest(ldapConfs) + if string(l.Ctx.Input.RequestBody) == "" { + err = ldapSession.ConnectionTest() + } else { + l.DecodeJSONReqAndValidate(&ldapConfs) + err = ldapUtils.ConnectionTestWithConfig(ldapConfs) + } + if err != nil { - log.Errorf("Ldap connect fail, error: %v", err) + log.Errorf("ldap connect fail, error: %v", err) l.RenderError(http.StatusBadRequest, fmt.Sprintf("ldap connect fail: %v", err)) return } @@ -98,26 +79,26 @@ func (l *LdapAPI) Search() { var err error var ldapUsers []models.LdapUser var ldapConfs models.LdapConf - + var ldapSession *ldapUtils.Session l.Ctx.Input.CopyBody(1 << 32) if string(l.Ctx.Input.RequestBody) == "" { - ldapConfs, err = ldapUtils.GetSystemLdapConf() + ldapSession, err = ldapUtils.LoadSystemLdapConfig() if err != nil { - log.Errorf("Can't load system configuration, error: %v", err) + 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) + ldapSession, err = ldapUtils.CreateWithUIConfig(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)) + if err = ldapSession.Open(); err != nil { + log.Errorf("can't Open ldap session, error: %v", err) + l.RenderError(http.StatusInternalServerError, fmt.Sprintf("can't open ldap session: %v", err)) return } + defer ldapSession.Close() searchName := l.GetString("username") @@ -131,9 +112,7 @@ func (l *LdapAPI) Search() { } } - ldapConfs.LdapFilter = ldapUtils.MakeFilter(searchName, ldapConfs.LdapFilter, ldapConfs.LdapUID) - - ldapUsers, err = ldapUtils.SearchUser(ldapConfs) + ldapUsers, err = ldapSession.SearchUser(searchName) if err != nil { log.Errorf("Ldap search fail, error: %v", err) @@ -152,23 +131,9 @@ func (l *LdapAPI) ImportUser() { 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) + ldapFailedImportUsers, err := importUsers(ldapConfs, ldapImportUsers.LdapUIDList) if err != nil { log.Errorf("Ldap import user fail, error: %v", err) @@ -190,7 +155,16 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. var failedImportUser []models.LdapFailedImportUser var u models.LdapFailedImportUser - tempFilter := ldapConfs.LdapFilter + ldapSession, err := ldapUtils.LoadSystemLdapConfig() + if err != nil { + log.Errorf("can't load system configuration, error: %v", err) + return nil, err + } + + if err = ldapSession.Open(); err != nil { + log.Errorf("Can't connect to ldap, error: %v", err) + } + defer ldapSession.Close() for _, tempUID := range ldapImportUsers { u.UID = tempUID @@ -214,9 +188,7 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. continue } - ldapConfs.LdapFilter = ldapUtils.MakeFilter(u.UID, tempFilter, ldapConfs.LdapUID) - - ldapUsers, err := ldapUtils.SearchUser(ldapConfs) + ldapUsers, err := ldapSession.SearchUser(u.UID) if err != nil { u.UID = tempUID u.Error = "failed_search_user" @@ -225,16 +197,21 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. continue } - if ldapUsers == nil { + if ldapUsers == nil || len(ldapUsers) <= 0 { u.UID = tempUID u.Error = "unknown_user" failedImportUser = append(failedImportUser, u) continue } - _, err = ldapUtils.ImportUser(ldapUsers[0]) + var user models.User - if err != nil { + user.Username = ldapUsers[0].Username + user.Realname = ldapUsers[0].Realname + user.Email = ldapUsers[0].Email + err = auth.OnBoardUser(&user) + + if err != nil || user.UserID <= 0 { u.UID = tempUID u.Error = err.Error() failedImportUser = append(failedImportUser, u) diff --git a/src/ui/api/member.go b/src/ui/api/member.go index 0ac5e35af..a7c3ed778 100644 --- a/src/ui/api/member.go +++ b/src/ui/api/member.go @@ -21,9 +21,8 @@ import ( "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" + "github.com/vmware/harbor/src/ui/auth" ) // ProjectMemberAPI handles request to /api/projects/{}/members/{} @@ -164,35 +163,35 @@ func (pma *ProjectMemberAPI) Post() { username := strings.TrimSpace(req.Username) userID := checkUserExists(username) if userID <= 0 { - //check current authorization mode - authMode, err := config.AuthMode() + + user, err := auth.SearchUser(username) + if err != nil { - log.Errorf("Failed the retrieve auth_mode, error: %s", err) - pma.RenderError(http.StatusInternalServerError, "Failed to retrieve auth_mode") + log.Errorf("Failed the search user, error: %v", err) + pma.RenderError(http.StatusInternalServerError, "Failed to search user") return } - if authMode != "ldap_auth" { - log.Errorf("User does not exist, user name: %s", username) - pma.RenderError(http.StatusNotFound, "User does not exist") + if user == nil { + log.Errorf("Current user doesn't exist: %v", username) + pma.RenderError(http.StatusNotFound, "Failed to search user: "+username) return } - //search and import user - newUserID, err := ldapUtils.SearchAndImportUser(username) + err = auth.OnBoardUser(user) + if err != nil { - log.Errorf("Search and import user failed, error: %v ", err) - pma.RenderError(http.StatusInternalServerError, "Failed to search and import user") + log.Errorf("Failed the onboard user, error: %s", err) + pma.RenderError(http.StatusInternalServerError, "Failed to onboard user") return } - - if newUserID <= 0 { - log.Error("Failed to create user") - pma.RenderError(http.StatusNotFound, "Failed to create user") + if user.UserID <= 0 { + log.Error("Failed the onboard user, UserId <=0") + pma.RenderError(http.StatusInternalServerError, "Failed to onboard user") return } + userID = user.UserID - userID = int(newUserID) } rolelist, err := dao.GetUserProjectRoles(userID, projectID) if err != nil { diff --git a/src/ui/auth/auth_test.go b/src/ui/auth/auth_test.go index 13b38efd2..c4faa0b9b 100644 --- a/src/ui/auth/auth_test.go +++ b/src/ui/auth/auth_test.go @@ -16,10 +16,34 @@ package auth import ( "testing" "time" + + "github.com/vmware/harbor/src/common" ) var l = NewUserLock(2 * time.Second) +var adminServerLdapTestConfig = map[string]interface{}{ + common.ExtEndpoint: "host01.com", + common.AUTHMode: "ldap_auth", + 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.LDAPURL: "ldap://127.0.0.1", + common.LDAPSearchDN: "cn=admin,dc=example,dc=com", + common.LDAPSearchPwd: "admin", + common.LDAPBaseDN: "dc=example,dc=com", + common.LDAPUID: "uid", + common.LDAPFilter: "", + common.LDAPScope: 3, + common.LDAPTimeout: 30, + common.CfgExpiration: 5, + common.AdminInitialPassword: "password", +} + func TestLock(t *testing.T) { t.Log("Locking john") l.Lock("john") diff --git a/src/ui/auth/authenticator.go b/src/ui/auth/authenticator.go index cd396052f..d4e19b476 100644 --- a/src/ui/auth/authenticator.go +++ b/src/ui/auth/authenticator.go @@ -37,22 +37,27 @@ const ( var lock = NewUserLock(frozenTime) -// Authenticator provides interface to authenticate user credentials. -type Authenticator interface { +// AuthenticateHelper provides interface to authenticate user credentials. +type AuthenticateHelper interface { // Authenticate ... Authenticate(m models.AuthModel) (*models.User, error) + // OnBoardUser will check if a user exists in user table, if not insert the user and + // put the id in the pointer of user model, if it does exist, return the user's profile. + OnBoardUser(u *models.User) error + // Get user information from account repository + SearchUser(username string) (*models.User, error) } -var registry = make(map[string]Authenticator) +var registry = make(map[string]AuthenticateHelper) // Register add different authenticators to registry map. -func Register(name string, authenticator Authenticator) { +func Register(name string, AuthenticateHelper AuthenticateHelper) { if _, dup := registry[name]; dup { log.Infof("authenticator: %s has been registered", name) return } - registry[name] = authenticator + registry[name] = AuthenticateHelper } // Login authenticates user credentials based on setting. @@ -83,3 +88,42 @@ func Login(m models.AuthModel) (*models.User, error) { } return user, err } + +// getAuthenticatorByMode -- +func getAuthenticateHelperByMode(authMode string) (AuthenticateHelper, error) { + if authMode == "" { + authMode = "db_auth" + } + AuthenticateHelper, ok := registry[authMode] + if !ok { + return nil, fmt.Errorf("Can not get current authenticator") + } + return AuthenticateHelper, nil +} + +// OnBoardUser will check if a user exists in user table, if not insert the user and +// put the id in the pointer of user model, if it does exist, return the user's profile. +func OnBoardUser(user *models.User) error { + authMode, err := config.AuthMode() + if err != nil { + return err + } + auth, err := getAuthenticateHelperByMode(authMode) + if err != nil { + return err + } + return auth.OnBoardUser(user) +} + +// SearchUser -- +func SearchUser(username string) (*models.User, error) { + authMode, err := config.AuthMode() + if err != nil { + return nil, err + } + auth, err := getAuthenticateHelperByMode(authMode) + if err != nil { + return nil, err + } + return auth.SearchUser(username) +} diff --git a/src/ui/auth/db/db.go b/src/ui/auth/db/db.go index d6593a238..c75f396bf 100644 --- a/src/ui/auth/db/db.go +++ b/src/ui/auth/db/db.go @@ -15,9 +15,9 @@ package db import ( - "github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/ui/auth" ) // Auth implements Authenticator interface to authenticate user against DB. @@ -32,6 +32,21 @@ func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) { return u, nil } +// OnBoardUser - Dummy implementation when auth_mod is db_auth +func (d *Auth) OnBoardUser(user *models.User) error { + //No need to create user in local database + return nil +} + +// SearchUser - Check if user exist in local db +func (d *Auth) SearchUser(username string) (*models.User, error) { + var queryCondition = models.User{ + Username: username, + } + + return dao.GetUser(queryCondition) +} + func init() { auth.Register("db_auth", &Auth{}) } diff --git a/src/ui/auth/db/db_test.go b/src/ui/auth/db/db_test.go index 1231be783..028138478 100644 --- a/src/ui/auth/db/db_test.go +++ b/src/ui/auth/db/db_test.go @@ -14,9 +14,139 @@ package db import ( + "log" + "os" "testing" + + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/utils/test" + + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/ui/auth" + uiConfig "github.com/vmware/harbor/src/ui/config" ) -func TestMain(t *testing.T) { +var adminServerTestConfig = map[string]interface{}{ + common.ExtEndpoint: "host01.com", + common.AUTHMode: "db_auth", + 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", + //config.SelfRegistration: true, + common.LDAPURL: "ldap://127.0.0.1", + common.LDAPSearchDN: "cn=admin,dc=example,dc=com", + common.LDAPSearchPwd: "admin", + common.LDAPBaseDN: "dc=example,dc=com", + common.LDAPUID: "uid", + common.LDAPFilter: "", + common.LDAPScope: 3, + common.LDAPTimeout: 30, + // 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, + common.CfgExpiration: 5, + // config.JobLogDir: "/var/log/jobs", + common.AdminInitialPassword: "password", } +func TestMain(t *testing.T) { + server, err := test.NewAdminserver(adminServerTestConfig) + if err != nil { + t.Fatalf("failed to create a mock admin server: %v", err) + } + defer server.Close() + + if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil { + t.Fatalf("failed to set env %s: %v", "ADMINSERVER_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) + } + + 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 TestSearchUser(t *testing.T) { + //insert user first + user := &models.User{ + Username: "existuser", + Email: "existuser@placeholder.com", + Realname: "Existing user", + } + + err := dao.OnBoardUser(user) + if err != nil { + t.Fatalf("Failed to OnBoardUser %v", user) + } + + var auth *Auth + newUser, err := auth.SearchUser("existuser") + if err != nil { + t.Fatalf("Failed to search user, error %v", err) + } + if newUser == nil { + t.Fatalf("Failed to search user %v", newUser) + } + +} + +func TestAuthenticateHelperOnboardUser(t *testing.T) { + user := models.User{ + Username: "test01", + Realname: "test01", + Email: "test01@example.com", + } + + err := auth.OnBoardUser(&user) + if err != nil { + t.Errorf("Failed to onboard user error: %v", err) + } + +} + +func TestAuthenticateHelperSearchUser(t *testing.T) { + + user, err := auth.SearchUser("admin") + if err != nil { + t.Error("Failed to search user, admin") + } + + if user == nil { + t.Error("Failed to search user admin") + } +} diff --git a/src/ui/auth/ldap/ldap.go b/src/ui/auth/ldap/ldap.go index 1fa2d0f5a..5c11e8561 100644 --- a/src/ui/auth/ldap/ldap.go +++ b/src/ui/auth/ldap/ldap.go @@ -25,7 +25,7 @@ import ( "github.com/vmware/harbor/src/ui/auth" ) -// Auth implements Authenticator interface to authenticate against LDAP +// Auth implements AuthenticateHelper interface to authenticate against LDAP type Auth struct{} const metaChars = "&|!=~*<>()" @@ -46,21 +46,19 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { } } - ldapConfs, err := ldapUtils.GetSystemLdapConf() + ldapSession, err := ldapUtils.LoadSystemLdapConfig() if err != nil { - return nil, fmt.Errorf("can't load system configuration: %v", err) + return nil, fmt.Errorf("can not load system ldap config: %v", err) } - ldapConfs, err = ldapUtils.ValidateLdapConf(ldapConfs) - - if err != nil { - return nil, fmt.Errorf("invalid ldap request: %v", err) + if err = ldapSession.Open(); err != nil { + log.Warningf("ldap connection fail: %v", err) + return nil, nil } + defer ldapSession.Close() - ldapConfs.LdapFilter = ldapUtils.MakeFilter(p, ldapConfs.LdapFilter, ldapConfs.LdapUID) - - ldapUsers, err := ldapUtils.SearchUser(ldapConfs) + ldapUsers, err := ldapSession.SearchUser(p) if err != nil { log.Warningf("ldap search fail: %v", err) @@ -83,7 +81,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { dn := ldapUsers[0].DN log.Debugf("username: %s, dn: %s", u.Username, dn) - if err := ldapUtils.Bind(ldapConfs, dn, m.Password); err != nil { + if err = ldapSession.Bind(dn, m.Password); err != nil { log.Warningf("Failed to bind user, username: %s, dn: %s, error: %v", u.Username, dn, err) return nil, nil } @@ -100,18 +98,68 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { u.UserID = currentUser.UserID u.HasAdminRole = currentUser.HasAdminRole } else { - userID, err := ldapUtils.ImportUser(ldapUsers[0]) - if err != nil { + var user models.User + user.Username = ldapUsers[0].Username + user.Email = ldapUsers[0].Email + user.Realname = ldapUsers[0].Realname + + err = auth.OnBoardUser(&user) + if err != nil || user.UserID <= 0 { 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) + u.UserID = user.UserID } return &u, nil } +// OnBoardUser will check if a user exists in user table, if not insert the user and +// put the id in the pointer of user model, if it does exist, return the user's profile. +func (l *Auth) OnBoardUser(u *models.User) error { + if u.Email == "" { + if strings.Contains(u.Username, "@") { + u.Email = u.Username + } else { + u.Email = u.Username + "@placeholder.com" + } + } + u.Password = "12345678AbC" //Password is not kept in local db + u.Comment = "from LDAP." //Source is from LDAP + + return dao.OnBoardUser(u) +} + +//SearchUser -- Search user in ldap +func (l *Auth) SearchUser(username string) (*models.User, error) { + var user models.User + ldapSession, err := ldapUtils.LoadSystemLdapConfig() + if err = ldapSession.Open(); err != nil { + return nil, fmt.Errorf("Failed to load system ldap config, %v", err) + } + + ldapUsers, err := ldapSession.SearchUser(username) + if err != nil { + return nil, fmt.Errorf("Failed to search user in ldap") + } + + if len(ldapUsers) > 1 { + log.Warningf("There are more than one user found, return the first user") + } + if len(ldapUsers) > 0 { + + user.Username = strings.TrimSpace(ldapUsers[0].Username) + user.Realname = strings.TrimSpace(ldapUsers[0].Realname) + + log.Debugf("Found ldap user %v", user) + } else { + return nil, fmt.Errorf("No user found, %v", username) + } + + return &user, nil +} + func init() { auth.Register("ldap_auth", &Auth{}) } diff --git a/src/ui/auth/ldap/ldap_test.go b/src/ui/auth/ldap/ldap_test.go index fdbda990a..46224ddd6 100644 --- a/src/ui/auth/ldap/ldap_test.go +++ b/src/ui/auth/ldap/ldap_test.go @@ -24,6 +24,7 @@ import ( "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/test" + "github.com/vmware/harbor/src/ui/auth" uiConfig "github.com/vmware/harbor/src/ui/config" ) @@ -91,15 +92,6 @@ func TestMain(t *testing.T) { 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) @@ -141,3 +133,71 @@ func TestAuthenticate(t *testing.T) { t.Errorf("Nil user for empty credentials") } } + +func TestSearchUser(t *testing.T) { + var username = "test" + var auth *Auth + user, err := auth.SearchUser(username) + if err != nil { + t.Errorf("Search user failed %v", err) + } + if user == nil { + t.Errorf("Search user failed %v", user) + } +} +func TestSearchUser_02(t *testing.T) { + var username = "nonexist" + var auth *Auth + user, _ := auth.SearchUser(username) + if user != nil { + t.Errorf("Should failed to search nonexist user") + } + +} + +func TestOnboardUser(t *testing.T) { + user := &models.User{ + Username: "sample", + Email: "sample@example.com", + Realname: "Sample", + } + + var auth *Auth + err := auth.OnBoardUser(user) + if err != nil { + t.Errorf("Failed to onboard user") + } + if user.UserID <= 0 { + t.Errorf("Failed to onboard user") + } +} + +func TestAuthenticateHelperOnboardUser(t *testing.T) { + user := models.User{ + Username: "test01", + Realname: "test01", + Email: "test01@example.com", + } + + err := auth.OnBoardUser(&user) + if err != nil { + t.Errorf("Failed to onboard user error: %v", err) + } + + if user.UserID <= 0 { + t.Errorf("Failed to onboard user, userid: %v", user.UserID) + } + +} + +func TestAuthenticateHelperSearchUser(t *testing.T) { + + user, err := auth.SearchUser("test") + if err != nil { + t.Error("Failed to search user, test") + } + + if user == nil { + t.Error("Failed to search user test") + } +} diff --git a/src/ui/auth/uaa/uaa.go b/src/ui/auth/uaa/uaa.go index 4ce8f2bae..d2c25edd0 100644 --- a/src/ui/auth/uaa/uaa.go +++ b/src/ui/auth/uaa/uaa.go @@ -20,7 +20,6 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/uaa" - "github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/ui/config" ) @@ -66,7 +65,7 @@ func doAuth(username, password string, client uaa.Client) (*models.User, error) return nil, err } -// Auth is the implementation of Authenticator to access uaa for authentication. +// Auth is the implementation of AuthenticateHelper to access uaa for authentication. type Auth struct{} //Authenticate ... @@ -78,6 +77,17 @@ func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) { return doAuth(m.Principal, m.Password, client) } -func init() { - auth.Register(auth.UAAAuth, &Auth{}) -} +// OnBoardUser will check if a user exists in user table, if not insert the user and +// put the id in the pointer of user model, if it does exist, return the user's profile. +// func (u *Auth) OnBoardUser(user *models.User) error { +// panic("not implemented") +// } + +// // SearchUser - search user on uaa server +// func (u *Auth) SearchUser(username string) (*models.User, error) { +// panic("not implemented") +// } + +// func init() { +// auth.Register(auth.UAAAuth, &Auth{}) +// }