Merge pull request #3683 from stonezdj/local_ldap_enhance

Ldap enhancement
This commit is contained in:
stone 2017-11-27 14:36:20 +08:00 committed by GitHub
commit 30e536b18b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 254 additions and 26 deletions

View File

@ -11,6 +11,7 @@ LDAP_FILTER=$ldap_filter
LDAP_UID=$ldap_uid
LDAP_SCOPE=$ldap_scope
LDAP_TIMEOUT=$ldap_timeout
LDAP_VERIFY_CERT=true
DATABASE_TYPE=mysql
MYSQL_HOST=$db_host
MYSQL_PORT=$db_port

View File

@ -81,6 +81,10 @@ var (
env: "LDAP_TIMEOUT",
parse: parseStringToInt,
},
common.LDAPVerifyCert: &parser{
env: "LDAP_VERIFY_CERT",
parse: parseStringToBool,
},
common.EmailHost: "EMAIL_HOST",
common.EmailPort: &parser{
env: "EMAIL_PORT",

View File

@ -90,11 +90,16 @@ func TestLoadFromEnv(t *testing.T) {
t.Fatalf("failed to set env: %v", err)
}
if err := os.Setenv("LDAP_VERIFY_CERT", "false"); err != nil {
t.Fatalf("failed to set env: %v", err)
}
cfgs = map[string]interface{}{}
err = LoadFromEnv(cfgs, false)
assert.Nil(t, err)
assert.Equal(t, extEndpoint, cfgs[common.ExtEndpoint])
assert.Equal(t, ldapURL, cfgs[common.LDAPURL])
assert.Equal(t, false, cfgs[common.LDAPVerifyCert])
os.Clearenv()
if err := os.Setenv("LDAP_URL", ldapURL); err != nil {
@ -104,6 +109,10 @@ func TestLoadFromEnv(t *testing.T) {
t.Fatalf("failed to set env: %v", err)
}
if err := os.Setenv("LDAP_VERIFY_CERT", "true"); err != nil {
t.Fatalf("failed to set env: %v", err)
}
cfgs = map[string]interface{}{
common.LDAPURL: "ldap_url",
}
@ -111,4 +120,5 @@ func TestLoadFromEnv(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, extEndpoint, cfgs[common.ExtEndpoint])
assert.Equal(t, "ldap_url", cfgs[common.LDAPURL])
assert.Equal(t, true, cfgs[common.LDAPVerifyCert])
}

View File

@ -48,6 +48,7 @@ const (
LDAPFilter = "ldap_filter"
LDAPScope = "ldap_scope"
LDAPTimeout = "ldap_timeout"
LDAPVerifyCert = "ldap_verify_cert"
TokenServiceURL = "token_service_url"
RegistryURL = "registry_url"
EmailHost = "email_host"

View File

@ -33,6 +33,7 @@ type LDAP struct {
UID string `json:"uid"`
Scope int `json:"scope"`
Timeout int `json:"timeout"` // in second
VerifyCert bool `json:"verify_cert"`
}
// Database ...

View File

@ -24,6 +24,7 @@ type LdapConf struct {
LdapUID string `json:"ldap_uid"`
LdapScope int `json:"ldap_scope"`
LdapConnectionTimeout int `json:"ldap_connection_timeout"`
LdapVerifyCert bool `json:"ldap_verify_cert"`
}
// LdapUser ...

View File

@ -60,6 +60,7 @@ func GetSystemLdapConf() (models.LdapConf, error) {
ldapConfs.LdapUID = ldap.UID
ldapConfs.LdapScope = ldap.Scope
ldapConfs.LdapConnectionTimeout = ldap.Timeout
ldapConfs.LdapVerifyCert = ldap.VerifyCert
// ldapConfs = config.LDAP().URL
// ldapConfs.LdapSearchDn = config.LDAP().SearchDn
@ -205,7 +206,8 @@ func SearchUser(ldapConfs models.LdapConf) ([]models.LdapUser, error) {
for _, ldapEntry := range result.Entries {
var u models.LdapUser
for _, attr := range ldapEntry.Attributes {
val := attr.Values[0]
//OpenLDAP sometimes contains leading space in username
val := strings.TrimSpace(attr.Values[0])
log.Debugf("Current ldap entry attr name: %s\n", attr.Name)
switch strings.ToLower(attr.Name) {
case strings.ToLower(ldapConfs.LdapUID):
@ -337,6 +339,7 @@ func dialLDAP(ldapConfs models.LdapConf) (*goldap.Conn, error) {
var ldap *goldap.Conn
splitLdapURL := strings.Split(ldapConfs.LdapURL, "://")
protocol, hostport := splitLdapURL[0], splitLdapURL[1]
host := strings.Split(hostport, ":")[0]
// Sets a Dial Timeout for LDAP
connectionTimeout := ldapConfs.LdapConnectionTimeout
@ -346,7 +349,8 @@ func dialLDAP(ldapConfs models.LdapConf) (*goldap.Conn, error) {
case "ldap":
ldap, err = goldap.Dial("tcp", hostport)
case "ldaps":
ldap, err = goldap.DialTLS("tcp", hostport, &tls.Config{InsecureSkipVerify: true})
log.Debug("Start to dial ldaps")
ldap, err = goldap.DialTLS("tcp", hostport, &tls.Config{ServerName: host, InsecureSkipVerify: !ldapConfs.LdapVerifyCert})
}
return ldap, err
@ -401,3 +405,38 @@ func searchLDAP(ldapConfs models.LdapConf, ldap *goldap.Conn) (*goldap.SearchRes
return result, nil
}
// SearchAndImportUser - Search user in LDAP, if this user exist, import it to database
func SearchAndImportUser(username string) (int64, error) {
var err error
var userID int64
ldapConfs, err := GetSystemLdapConf()
if err != nil {
log.Errorf("Can not get ldap configuration, error %v", err)
return 0, err
}
ldapConfs, err = ValidateLdapConf(ldapConfs)
if err != nil {
log.Errorf("Invalid ldap request, error: %v", err)
return 0, err
}
ldapConfs.LdapFilter = MakeFilter(username, ldapConfs.LdapFilter, ldapConfs.LdapUID)
log.Debugf("Search with LDAP with filter %s", ldapConfs.LdapFilter)
ldapUsers, err := SearchUser(ldapConfs)
if err != nil {
log.Errorf("Can not search ldap, error %v, filter: %s", err, ldapConfs.LdapFilter)
return 0, err
}
if len(ldapUsers) > 0 {
log.Debugf("Importing user %s to local database", ldapUsers[0].Username)
if userID, err = ImportUser(ldapUsers[0]); err != nil {
log.Errorf("Can not import ldap user to local db, error %v", err)
return 0, err
}
}
return userID, err
}

View File

@ -17,6 +17,7 @@ package ldap
import (
//"fmt"
//"strings"
"os"
"testing"
@ -65,6 +66,45 @@ var adminServerLdapTestConfig = map[string]interface{}{
common.AdminInitialPassword: "password",
}
var adminServerDefaultConfigWithVerifyCert = map[string]interface{}{
common.ExtEndpoint: "https://host01.com",
common.AUTHMode: common.LDAPAuth,
common.DatabaseType: "mysql",
common.MySQLHost: "127.0.0.1",
common.MySQLPort: 3306,
common.MySQLUsername: "root",
common.MySQLPassword: "root123",
common.MySQLDatabase: "registry",
common.SQLiteFile: "/tmp/registry.db",
common.SelfRegistration: true,
common.LDAPURL: "ldap://127.0.0.1:389",
common.LDAPSearchDN: "cn=admin,dc=example,dc=com",
common.LDAPSearchPwd: "admin",
common.LDAPBaseDN: "dc=example,dc=com",
common.LDAPUID: "uid",
common.LDAPFilter: "",
common.LDAPScope: 3,
common.LDAPTimeout: 30,
common.LDAPVerifyCert: true,
common.TokenServiceURL: "http://token_service",
common.RegistryURL: "http://registry",
common.EmailHost: "127.0.0.1",
common.EmailPort: 25,
common.EmailUsername: "user01",
common.EmailPassword: "password",
common.EmailFrom: "from",
common.EmailSSL: true,
common.EmailIdentity: "",
common.ProjectCreationRestriction: common.ProCrtRestrAdmOnly,
common.MaxJobWorkers: 3,
common.TokenExpiration: 30,
common.CfgExpiration: 5,
common.AdminInitialPassword: "password",
common.AdmiralEndpoint: "http://www.vmware.com",
common.WithNotary: false,
common.WithClair: false,
}
func TestMain(t *testing.T) {
server, err := test.NewAdminserver(adminServerLdapTestConfig)
if err != nil {
@ -125,6 +165,7 @@ func TestGetSystemLdapConf(t *testing.T) {
}
func TestValidateLdapConf(t *testing.T) {
testLdapConfig, err := GetSystemLdapConf()
if err != nil {
t.Fatalf("failed to get system ldap config %v", err)
@ -138,6 +179,7 @@ func TestValidateLdapConf(t *testing.T) {
}
func TestMakeFilter(t *testing.T) {
testLdapConfig, err := GetSystemLdapConf()
if err != nil {
@ -254,3 +296,26 @@ func TestSearchUser(t *testing.T) {
t.Errorf("unexpected ldap user search result: %s = %s", "ldapUsers[0].Username", ldapUsers[0].Username)
}
}
func TestSearchAndImportUser(t *testing.T) {
userID, err := SearchAndImportUser("test")
if err != nil {
t.Fatalf("Failed on error : %v ", err)
}
if userID <= 0 {
t.Fatalf("userID= %v", userID)
}
}
func TestSearchAndImportUserNotExist(t *testing.T) {
userID, _ := SearchAndImportUser("notexist")
if userID > 0 {
t.Fatal("Can not import a non exist ldap user!")
t.Fail()
}
}

View File

@ -40,6 +40,7 @@ var (
common.LDAPFilter,
common.LDAPScope,
common.LDAPTimeout,
common.LDAPVerifyCert,
common.EmailHost,
common.EmailPort,
common.EmailUsername,
@ -80,6 +81,7 @@ var (
common.EmailSSL,
common.EmailInsecure,
common.SelfRegistration,
common.LDAPVerifyCert,
}
passwordKeys = []string{

View File

@ -17,10 +17,13 @@ package api
import (
"fmt"
"net/http"
"strings"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
)
// ProjectMemberAPI handles request to /api/projects/{}/members/{}
@ -158,12 +161,38 @@ func (pma *ProjectMemberAPI) Post() {
var req memberReq
pma.DecodeJSONReq(&req)
username := req.Username
username := strings.TrimSpace(req.Username)
userID := checkUserExists(username)
if userID <= 0 {
log.Warningf("User does not exist, user name: %s", username)
pma.RenderError(http.StatusNotFound, "User does not exist")
return
//check current authorization mode
authMode, err := config.AuthMode()
if err != nil {
log.Errorf("Failed the retrieve auth_mode, error: %s", err)
pma.RenderError(http.StatusInternalServerError, "Failed to retrieve auth_mode")
return
}
if authMode != "ldap_auth" {
log.Errorf("User does not exist, user name: %s", username)
pma.RenderError(http.StatusNotFound, "User does not exist")
return
}
//search and import user
newUserID, err := ldapUtils.SearchAndImportUser(username)
if err != nil {
log.Errorf("Search and import user failed, error: %v ", err)
pma.RenderError(http.StatusInternalServerError, "Failed to search and import user")
return
}
if newUserID <= 0 {
log.Error("Failed to create user")
pma.RenderError(http.StatusNotFound, "Failed to create user")
return
}
userID = int(newUserID)
}
rolelist, err := dao.GetUserProjectRoles(userID, projectID)
if err != nil {

View File

@ -173,7 +173,6 @@ func LDAP() (*models.LDAP, error) {
if err != nil {
return nil, err
}
ldap := &models.LDAP{}
ldap.URL = cfg[common.LDAPURL].(string)
ldap.SearchDN = cfg[common.LDAPSearchDN].(string)
@ -183,6 +182,11 @@ func LDAP() (*models.LDAP, error) {
ldap.Filter = cfg[common.LDAPFilter].(string)
ldap.Scope = int(cfg[common.LDAPScope].(float64))
ldap.Timeout = int(cfg[common.LDAPTimeout].(float64))
if cfg[common.LDAPVerifyCert] != nil {
ldap.VerifyCert = cfg[common.LDAPVerifyCert].(bool)
} else {
ldap.VerifyCert = true
}
return ldap, nil
}

View File

@ -64,6 +64,7 @@ export class Configuration {
ldap_timeout: NumberValueItem;
ldap_uid: StringValueItem;
ldap_url: StringValueItem;
ldap_verify_cert: BoolValueItem;
email_host: StringValueItem;
email_identity: StringValueItem;
email_from: StringValueItem;
@ -89,6 +90,7 @@ export class Configuration {
this.ldap_timeout = new NumberValueItem(5, true);
this.ldap_uid = new StringValueItem("", true);
this.ldap_url = new StringValueItem("", true);
this.ldap_verify_cert = new BoolValueItem(true, true);
this.email_host = new StringValueItem("", true);
this.email_identity = new StringValueItem("", true);
this.email_from = new StringValueItem("", true);

View File

@ -111,6 +111,15 @@
<span class="tooltip-content">{{'CONFIG.TOOLTIP.LDAP_SCOPE' | translate}}</span>
</a>
</div>
<div class="form-group">
<label for="ldapVerifyCert">{{'CONFIG.LDAP.VERIFY_CERT' | translate}}</label>
<clr-checkbox name="ldapVerifyCert" id="ldapVerifyCert" [clrChecked]="currentConfig.ldap_verify_cert.value" [clrDisabled]="disabled(currentConfig.ldap_scope)" (clrCheckedChange)="setVerifyCertValue($event)">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_CERT' | translate}}</span>
</a>
</clr-checkbox>
</div>
</section>
<section class="form-block">
<div class="form-group">

View File

@ -50,6 +50,10 @@ export class ConfigurationAuthComponent {
}
}
setVerifyCertValue($event: any) {
this.currentConfig.ldap_verify_cert.value = $event;
}
disabled(prop: any): boolean {
return !(prop && prop.editable);
}

View File

@ -425,7 +425,8 @@
"TOKEN_EXPIRATION": "The expiration time (in minutes) of a token created by the token service. Default is 30 minutes.",
"PRO_CREATION_RESTRICTION": "The flag to define what users have permission to create projects. By default, everyone can create a project. Set to 'Admin Only' so that only an administrator can create a project.",
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday."
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday.",
"VERIFY_CERT": "Verify Cert from LDAP Server"
},
"LDAP": {
"URL": "LDAP URL",
@ -434,7 +435,8 @@
"BASE_DN": "LDAP Base DN",
"FILTER": "LDAP Filter",
"UID": "LDAP UID",
"SCOPE": "LDAP Scope"
"SCOPE": "LDAP Scope",
"VERIFY_CERT": "LDAP Verify Cert"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",

View File

@ -426,7 +426,8 @@
"TOKEN_EXPIRATION": "El tiempo de expiración (en minutos) del token creado por el servicio de tokens. Por defecto son 30 minutos.",
"PRO_CREATION_RESTRICTION": "Marca para definir qué usuarios tienen permisos para crear proyectos. Por defecto, todos pueden crear proyectos. Seleccione 'Solo Administradores' para que solamente los administradores puedan crear proyectos.",
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday."
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday.",
"VERIFY_CERT": "Verify Cert from LDAP Server"
},
"LDAP": {
"URL": "LDAP URL",
@ -435,7 +436,8 @@
"BASE_DN": "LDAP Base DN",
"FILTER": "LDAP Filtro",
"UID": "LDAP UID",
"SCOPE": "LDAP Ámbito"
"SCOPE": "LDAP Ámbito",
"VERIFY_CERT": "LDAP Verify Cert"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",

View File

@ -425,7 +425,8 @@
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间分钟默认为30分钟。",
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书.",
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。"
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。".
"VERIFY_CERT": "检查来自LDAP服务端的证书"
},
"LDAP": {
"URL": "LDAP URL",
@ -434,7 +435,8 @@
"BASE_DN": "LDAP基础DN",
"FILTER": "LDAP过滤器",
"UID": "LDAP用户UID的属性",
"SCOPE": "LDAP搜索范围"
"SCOPE": "LDAP搜索范围",
"VERIFY_CERT": "LDAP 检查证书"
},
"SCANNING": {
"TRIGGER_SCAN_ALL_SUCCESS": "启动扫描所有镜像任务成功!",

View File

@ -5,11 +5,12 @@ docker rm -f $NAME 2>/dev/null
docker run --env LDAP_ORGANISATION="Harbor." \
--env LDAP_DOMAIN="example.com" \
--env LDAP_ADMIN_PASSWORD="admin" \
--env LDAP_TLS_VERIFY_CLIENT="never" \
-p 389:389 \
-p 636:636 \
--detach --name $NAME osixia/openldap:1.1.7
sleep 3
sleep 5
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

View File

@ -24,17 +24,18 @@ Init LDAP
${rc} ${output}= Run And Return Rc And Output ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'
Log ${output}
Sleep 2
Input Text xpath=//*[@id="ldapUrl"] ldap://${output}
Input Text xpath=//*[@id="ldapUrl"] ldaps://${output}
Sleep 1
Input Text xpath=//*[@id="ldapSearchDN"] cn=admin,dc=example,dc=org
Input Text xpath=//*[@id="ldapSearchDN"] cn=admin,dc=example,dc=com
Sleep 1
Input Text xpath=//*[@id="ldapSearchPwd"] admin
Sleep 1
Input Text xpath=//*[@id="ldapBaseDN"] dc=example,dc=org
Input Text xpath=//*[@id="ldapBaseDN"] dc=example,dc=com
Sleep 1
Input Text xpath=//*[@id="ldapUid"] cn
Sleep 1
Capture Page Screenshot
Disable Ldap Verify Cert Checkbox
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
Sleep 2
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]
@ -45,6 +46,44 @@ Switch To Configure
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/section/ul/li[3]/a
Sleep 2
Test Ldap Connection
${rc} ${output}= Run And Return Rc And Output ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'
Log ${output}
Sleep 2
Input Text xpath=//*[@id="ldapUrl"] ldaps://${output}
Sleep 1
Input Text xpath=//*[@id="ldapSearchDN"] cn=admin,dc=example,dc=com
Sleep 1
Input Text xpath=//*[@id="ldapSearchPwd"] admin
Sleep 1
Input Text xpath=//*[@id="ldapBaseDN"] dc=example,dc=com
Sleep 1
Input Text xpath=//*[@id="ldapUid"] cn
Sleep 1
# default is checked, click test connection to verify fail as no cert.
Click Element xpath=${test_ldap_xpath}
Sleep 1
Wait Until Page Contains Failed to verify LDAP server with error
Sleep 5
Disable Ldap Verify Cert Checkbox
# ldap checkbox unchecked, click test connection to verify success.
Sleep 1
Click Element xpath=${test_ldap_xpath}
Capture Page Screenshot
Wait Until Page Contains Connection to LDAP server is verified timeout=15
Disable Ldap Verify Cert Checkbox
Mouse Down xpath=//*[@id="clr-checkbox-ldapVerifyCert"]
Mouse Up xpath=//*[@id="clr-checkbox-ldapVerifyCert"]
Sleep 2
Capture Page Screenshot
Ldap Verify Cert Checkbox Should Be Disabled
Ldap Verify Cert Checkbox Should Be Disabled
Checkbox Should Not Be Selected xpath=//*[@id="clr-checkbox-ldapVerifyCert"]
Set Pro Create Admin Only
#set limit to admin only
Sleep 2

View File

@ -17,4 +17,5 @@ Documentation This resource provides any keywords related to the Harbor private
*** Variables ***
${project_create_xpath} //project//div[@class="option-left"]/button
${self_reg_xpath} //input[@id="clr-checkbox-selfReg"]
${self_reg_xpath} //input[@id="clr-checkbox-selfReg"]
${test_ldap_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]

View File

@ -71,11 +71,12 @@ Switch To LDAP
Config Harbor cfg auth=ldap_auth http_proxy=https
Prepare
Up Harbor
${rc}= Run And Return Rc docker pull vmware/harbor-ldap-test:1.1.1
${rc}= Run And Return Rc docker pull osixia/openldap:1.1.7
Log ${rc}
Should Be Equal As Integers ${rc} 0
${rc}= Run And Return Rc docker run --name ldap-container -p 389:389 --detach vmware/harbor-ldap-test:1.1.1
${rc} ${output}= Run And Return Rc And Output cd tests && ./ldapprepare.sh
Log ${rc}
Log ${output}
Should Be Equal As Integers ${rc} 0
${rc} ${output}= Run And Return Rc And Output docker ps
Log ${output}

View File

@ -328,30 +328,38 @@ Test Case - Admin Push Signed Image
Test Case - Admin Push Un-Signed Image
${rc} ${output}= Run And Return Rc And Output docker push ${ip}/library/hello-world:latest
Log To Console ${output}
Test Case - Ldap Sign in and out
Test Case - Ldap Verify Cert
Switch To LDAP
Init Chrome Driver
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
Switch To Configure
Test Ldap Connection
Close Browser
Test Case - Ldap Sign in and out
Init Chrome Driver
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
Switch To Configure
Init LDAP
Logout Harbor
Sign In Harbor ${HARBOR_URL} user001 user001
Sign In Harbor ${HARBOR_URL} test 123456
Close Browser
Test Case - Ldap User Create Project
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
Sign In Harbor ${HARBOR_URL} user001 user001
Sign In Harbor ${HARBOR_URL} test 123456
Create An New Project project${d}
Close Browser
Test Case - Ldap User Push An Image
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
Sign In Harbor ${HARBOR_URL} user001 user001
Sign In Harbor ${HARBOR_URL} test 123456
Create An New Project project${d}
Push Image ${ip} user001 user001 project${d} hello-world:latest
Push Image ${ip} test 123456 project${d} hello-world:latest
Go Into Project project${d}
Wait Until Page Contains project${d}/hello-world
Close Browser