From c48d9085155de964caf95537d1e3f7251281b056 Mon Sep 17 00:00:00 2001
From: yhua <yhua@vmware.com>
Date: Fri, 24 Feb 2017 18:30:57 +0800
Subject: [PATCH] add new ldap auth and import user feature

---
 .travis.yml                        |   5 +-
 docs/swagger.yaml                  | 109 +++++++-
 src/common/models/ldap.go          |  34 ++-
 src/common/utils/ldap/ldap.go      | 382 ++++++++++++++++++++++++++++
 src/common/utils/ldap/ldap_test.go | 258 +++++++++++++++++++
 src/ui/api/ldap.go                 | 395 +++++++++++++++++------------
 src/ui/api/ldap_test.go            |  95 -------
 src/ui/auth/ldap/ldap.go           | 168 ++----------
 src/ui/auth/ldap/ldap_test.go      | 106 +++++++-
 src/ui/router.go                   |   2 +
 tests/docker-compose.test.yml      |  31 ---
 tests/ldap_test.ldif               |  14 +
 tests/ldapprepare.sh               |  15 ++
 13 files changed, 1168 insertions(+), 446 deletions(-)
 create mode 100644 src/common/utils/ldap/ldap.go
 create mode 100644 src/common/utils/ldap/ldap_test.go
 delete mode 100644 src/ui/api/ldap_test.go
 create mode 100644 tests/ldap_test.ldif
 create mode 100755 tests/ldapprepare.sh

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