Merge remote-tracking branch 'upstream/master' into repEnhance

This commit is contained in:
pfh 2018-01-05 14:09:03 +08:00
commit 13308ce9d8
59 changed files with 623 additions and 294 deletions

View File

@ -306,6 +306,8 @@ modify_composefile_clair:
@cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRTPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME) @cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRTPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
@$(SEDCMD) -i 's/__postgresql_version__/$(CLAIRDBVERSION)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME) @$(SEDCMD) -i 's/__postgresql_version__/$(CLAIRDBVERSION)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
@$(SEDCMD) -i 's/__clair_version__/$(CLAIRVERSION)-$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME) @$(SEDCMD) -i 's/__clair_version__/$(CLAIRVERSION)-$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
@cp $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSECLAIRTPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSECLAIRFILENAME)
@$(SEDCMD) -i 's/__clair_version__/$(CLAIRVERSION)-$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSECLAIRFILENAME)
modify_sourcefiles: modify_sourcefiles:
@echo "change mode of source files." @echo "change mode of source files."

View File

@ -4,32 +4,31 @@ log:
fields: fields:
service: registry service: registry
storage: storage:
cache: cache:
layerinfo: inmemory layerinfo: inmemory
filesystem: filesystem:
rootdirectory: /storage rootdirectory: /storage
maintenance: maintenance:
uploadpurging: uploadpurging:
enabled: false enabled: false
delete: delete:
enabled: true enabled: true
http: http:
addr: :5000 addr: :5000
secret: placeholder secret: placeholder
debug: debug:
addr: localhost:5001 addr: localhost:5001
auth: auth:
token: token:
issuer: harbor-token-issuer issuer: harbor-token-issuer
realm: $ui_url/service/token realm: $ui_url/service/token
rootcertbundle: /etc/registry/root.crt rootcertbundle: /etc/registry/root.crt
service: harbor-registry service: harbor-registry
notifications: notifications:
endpoints: endpoints:
- name: harbor - name: harbor
disabled: false disabled: false
url: http://ui:8080/service/notifications url: http://ui:8080/service/notifications
timeout: 3000ms timeout: 3000ms
threshold: 5 threshold: 5
backoff: 1s backoff: 1s

View File

@ -4,14 +4,14 @@ log:
fields: fields:
service: registry service: registry
storage: storage:
cache: cache:
layerinfo: redis layerinfo: redis
Place_holder_for_Storage_configureation Place_holder_for_Storage_configureation
maintenance: maintenance:
uploadpurging: uploadpurging:
enabled: false enabled: false
delete: delete:
enabled: true enabled: true
redis: redis:
addr: $redis_url addr: $redis_url
db: 0 db: 0
@ -24,10 +24,10 @@ redis:
idletimeout: 300s idletimeout: 300s
http: http:
addr: :5000 addr: :5000
secret: placeholder secret: placeholder
debug: debug:
addr: localhost:5001 addr: localhost:5001
auth: auth:
token: token:
issuer: harbor-token-issuer issuer: harbor-token-issuer
@ -37,9 +37,9 @@ auth:
notifications: notifications:
endpoints: endpoints:
- name: harbor - name: harbor
disabled: false disabled: false
url: http://ui:8080/service/notifications url: http://ui:8080/service/notifications
timeout: 3000ms timeout: 3000ms
threshold: 5 threshold: 5
backoff: 1s backoff: 1s

View File

@ -76,7 +76,7 @@ services:
volumes: volumes:
- ./common/config/ui/app.conf:/etc/ui/app.conf:z - ./common/config/ui/app.conf:/etc/ui/app.conf:z
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z - ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z
- ./common/config/ui/certificates/:/etc/ui/certifates/ - ./common/config/ui/certificates/:/etc/ui/certificates/
- /data/secretkey:/etc/ui/key:z - /data/secretkey:/etc/ui/key:z
- /data/ca_download/:/etc/ui/ca/:z - /data/ca_download/:/etc/ui/ca/:z
- /data/psc/:/etc/ui/token/:z - /data/psc/:/etc/ui/token/:z

View File

@ -0,0 +1,32 @@
version: '2'
services:
ui:
networks:
harbor-clair:
aliases:
- harbor-ui
jobservice:
networks:
- harbor-clair
registry:
networks:
- harbor-clair
clair:
networks:
- harbor-clair
container_name: clair
image: vmware/clair-photon:__clair_version__
restart: always
cpu_quota: 150000
depends_on:
- log
volumes:
- ./common/config/clair:/config
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "clair"
networks:
harbor-clair:
external: false

View File

@ -142,4 +142,5 @@ uaa_endpoint = uaa.mydomain.org
uaa_clientid = id uaa_clientid = id
uaa_clientsecret = secret uaa_clientsecret = secret
uaa_verify_cert = true uaa_verify_cert = true
uaa_ca_cert = /path/to/ca.pem
############# #############

View File

@ -165,7 +165,7 @@ if [ $with_notary ] && [ ! $harbor_ha ]
then then
prepare_para="${prepare_para} --with-notary" prepare_para="${prepare_para} --with-notary"
fi fi
if [ $with_clair ] && [ ! $harbor_ha ] if [ $with_clair ]
then then
prepare_para="${prepare_para} --with-clair" prepare_para="${prepare_para} --with-clair"
fi fi
@ -182,7 +182,7 @@ if [ $with_notary ] && [ ! $harbor_ha ]
then then
docker_compose_list="${docker_compose_list} -f docker-compose.notary.yml" docker_compose_list="${docker_compose_list} -f docker-compose.notary.yml"
fi fi
if [ $with_clair ] && [ ! $harbor_ha ] if [ $with_clair ]
then then
docker_compose_list="${docker_compose_list} -f docker-compose.clair.yml" docker_compose_list="${docker_compose_list} -f docker-compose.clair.yml"
fi fi
@ -199,6 +199,8 @@ if [ $harbor_ha ]
then then
mv docker-compose.yml docker-compose.yml.bak mv docker-compose.yml docker-compose.yml.bak
cp ha/docker-compose.yml docker-compose.yml cp ha/docker-compose.yml docker-compose.yml
mv docker-compose.clair.yml docker-compose.clair.yml.bak
cp ha/docker-compose.clair.yml docker-compose.clair.yml
fi fi
docker-compose $docker_compose_list up -d docker-compose $docker_compose_list up -d

View File

@ -30,8 +30,13 @@ def validate(conf, args):
redis_url = rcp.get("configuration", "redis_url") redis_url = rcp.get("configuration", "redis_url")
if redis_url is None or len(redis_url) < 1: if redis_url is None or len(redis_url) < 1:
raise Exception("Error: In HA mode redis is required redis_url need to point to an redis cluster") raise Exception("Error: In HA mode redis is required redis_url need to point to an redis cluster")
if args.notary_mode or args.clair_mode: if args.notary_mode:
raise Exception("Error: HA mode doesn't support clair and notary currently") raise Exception("Error: HA mode doesn't support Notary currently")
if args.clair_mode:
clair_db_host = rcp.get("configuration", "clair_db_host")
if "postgres" == clair_db_host:
raise Exception("Error: In HA mode, clair_db_host in harbor.cfg needs to point to an external Postgres DB address.")
cert_path = rcp.get("configuration", "ssl_cert") cert_path = rcp.get("configuration", "ssl_cert")
cert_key_path = rcp.get("configuration", "ssl_cert_key") cert_key_path = rcp.get("configuration", "ssl_cert_key")
shared_cert_key = os.path.join(base_dir, "ha", os.path.basename(cert_key_path)) shared_cert_key = os.path.join(base_dir, "ha", os.path.basename(cert_key_path))
@ -245,6 +250,7 @@ uaa_endpoint = rcp.get("configuration", "uaa_endpoint")
uaa_clientid = rcp.get("configuration", "uaa_clientid") uaa_clientid = rcp.get("configuration", "uaa_clientid")
uaa_clientsecret = rcp.get("configuration", "uaa_clientsecret") uaa_clientsecret = rcp.get("configuration", "uaa_clientsecret")
uaa_verify_cert = rcp.get("configuration", "uaa_verify_cert") uaa_verify_cert = rcp.get("configuration", "uaa_verify_cert")
uaa_ca_cert = rcp.get("configuration", "uaa_ca_cert")
secret_key = get_secret_key(secretkey_path) secret_key = get_secret_key(secretkey_path)
log_rotate_count = rcp.get("configuration", "log_rotate_count") log_rotate_count = rcp.get("configuration", "log_rotate_count")
@ -275,6 +281,7 @@ log_config_dir = prep_conf_dir (config_dir, "log")
adminserver_conf_env = os.path.join(config_dir, "adminserver", "env") adminserver_conf_env = os.path.join(config_dir, "adminserver", "env")
ui_conf_env = os.path.join(config_dir, "ui", "env") ui_conf_env = os.path.join(config_dir, "ui", "env")
ui_conf = os.path.join(config_dir, "ui", "app.conf") ui_conf = os.path.join(config_dir, "ui", "app.conf")
ui_cert_dir = os.path.join(config_dir, "ui", "certificates")
jobservice_conf = os.path.join(config_dir, "jobservice", "app.conf") jobservice_conf = os.path.join(config_dir, "jobservice", "app.conf")
registry_conf = os.path.join(config_dir, "registry", "config.yml") registry_conf = os.path.join(config_dir, "registry", "config.yml")
db_conf_env = os.path.join(config_dir, "db", "env") db_conf_env = os.path.join(config_dir, "db", "env")
@ -382,6 +389,16 @@ shutil.copyfile(os.path.join(templates_dir, "jobservice", "app.conf"), jobservic
print("Generated configuration file: %s" % ui_conf) print("Generated configuration file: %s" % ui_conf)
shutil.copyfile(os.path.join(templates_dir, "ui", "app.conf"), ui_conf) shutil.copyfile(os.path.join(templates_dir, "ui", "app.conf"), ui_conf)
if auth_mode == "uaa_auth":
if os.path.isfile(uaa_ca_cert):
if not os.path.isdir(ui_cert_dir):
os.makedirs(ui_cert_dir, mode=0o600)
ui_uaa_ca = os.path.join(ui_cert_dir, "uaa_ca.pem")
print("Copying UAA CA cert to %s" % ui_uaa_ca)
shutil.copyfile(uaa_ca_cert, ui_uaa_ca)
else:
print("Can not find UAA CA cert: %s, skip" % uaa_ca_cert)
def validate_crt_subj(dirty_subj): def validate_crt_subj(dirty_subj):
subj_list = [item for item in dirty_subj.strip().split("/") \ subj_list = [item for item in dirty_subj.strip().split("/") \

View File

@ -46,7 +46,7 @@ func TestAuthModeCanBeModified(t *testing.T) {
t.Fatalf("failed to register user: %v", err) t.Fatalf("failed to register user: %v", err)
} }
defer func(id int64) { defer func(id int64) {
if err := deleteUser(id); err != nil { if err := CleanUser(id); err != nil {
t.Fatalf("failed to delete user %d: %v", id, err) t.Fatalf("failed to delete user %d: %v", id, err)
} }
}(id) }(id)

View File

@ -258,9 +258,12 @@ func DeleteUser(userID int) error {
} }
// ChangeUserProfile ... // ChangeUserProfile ...
func ChangeUserProfile(user models.User) error { func ChangeUserProfile(user models.User, cols ...string) error {
o := GetOrmer() o := GetOrmer()
if _, err := o.Update(&user, "Email", "Realname", "Comment"); err != nil { if len(cols) == 0 {
cols = []string{"Email", "Realname", "Comment"}
}
if _, err := o.Update(&user, cols...); err != nil {
log.Errorf("update user failed, error: %v", err) log.Errorf("update user failed, error: %v", err)
return err return err
} }
@ -290,3 +293,12 @@ func OnBoardUser(u *models.User) error {
} }
return nil return nil
} }
//CleanUser - Clean this user information from DB
func CleanUser(id int64) error {
if _, err := GetOrmer().QueryTable(&models.User{}).
Filter("UserID", id).Delete(); err != nil {
return err
}
return nil
}

View File

@ -39,7 +39,7 @@ func TestDeleteUser(t *testing.T) {
t.Fatalf("failed to register user: %v", err) t.Fatalf("failed to register user: %v", err)
} }
defer func(id int64) { defer func(id int64) {
if err := deleteUser(id); err != nil { if err := CleanUser(id); err != nil {
t.Fatalf("failed to delete user %d: %v", id, err) t.Fatalf("failed to delete user %d: %v", id, err)
} }
}(id) }(id)
@ -88,13 +88,5 @@ func TestOnBoardUser(t *testing.T) {
err = OnBoardUser(u) err = OnBoardUser(u)
assert.Nil(err) assert.Nil(err)
assert.True(u.UserID == id) assert.True(u.UserID == id)
deleteUser(int64(id)) CleanUser(int64(id))
}
func deleteUser(id int64) error {
if _, err := GetOrmer().QueryTable(&models.User{}).
Filter("UserID", id).Delete(); err != nil {
return err
}
return nil
} }

View File

@ -1,21 +0,0 @@
// Copyright (c) 2017 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 models
import (
"testing"
)
func TestMain(m *testing.M) {
}

View File

@ -89,14 +89,15 @@ func (r *RepTarget) Valid(v *validation.Validation) {
v.SetError("name", "max length is 64") v.SetError("name", "max length is 64")
} }
if len(r.URL) == 0 { url, err := utils.ParseEndpoint(r.URL)
v.SetError("endpoint", "can not be empty") if err != nil {
} v.SetError("endpoint", err.Error())
} else {
r.URL = utils.FormatEndpoint(r.URL) // Prevent SSRF security issue #3755
r.URL = url.Scheme + "://" + url.Host + url.Path
if len(r.URL) > 64 { if len(r.URL) > 64 {
v.SetError("endpoint", "max length is 64") v.SetError("endpoint", "max length is 64")
}
} }
// password is encoded using base64, the length of this field // password is encoded using base64, the length of this field

View File

@ -0,0 +1,131 @@
// Copyright (c) 2017 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 models
import (
"testing"
"github.com/astaxie/beego/validation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidOfTarget(t *testing.T) {
cases := []struct {
target RepTarget
err bool
expected RepTarget
}{
// name is null
{
RepTarget{
Name: "",
},
true,
RepTarget{}},
// url is null
{
RepTarget{
Name: "endpoint01",
URL: "",
},
true,
RepTarget{},
},
// invalid url
{
RepTarget{
Name: "endpoint01",
URL: "ftp://example.com",
},
true,
RepTarget{},
},
// invalid url
{
RepTarget{
Name: "endpoint01",
URL: "ftp://example.com",
},
true,
RepTarget{},
},
// valid url
{
RepTarget{
Name: "endpoint01",
URL: "example.com",
},
false,
RepTarget{
Name: "endpoint01",
URL: "http://example.com",
},
},
// valid url
{
RepTarget{
Name: "endpoint01",
URL: "http://example.com",
},
false,
RepTarget{
Name: "endpoint01",
URL: "http://example.com",
},
},
// valid url
{
RepTarget{
Name: "endpoint01",
URL: "https://example.com",
},
false,
RepTarget{
Name: "endpoint01",
URL: "https://example.com",
},
},
// valid url
{
RepTarget{
Name: "endpoint01",
URL: "http://example.com/redirect?key=value",
},
false,
RepTarget{
Name: "endpoint01",
URL: "http://example.com/redirect",
}},
}
for _, c := range cases {
v := &validation.Validation{}
c.target.Valid(v)
if c.err {
require.True(t, v.HasErrors())
continue
}
require.False(t, v.HasErrors())
assert.Equal(t, c.expected, c.target)
}
}

View File

@ -129,7 +129,7 @@ func (r *Registry) Catalog() ([]string, error) {
// Ping ... // Ping ...
func (r *Registry) Ping() error { func (r *Registry) Ping() error {
req, err := http.NewRequest("GET", buildPingURL(r.Endpoint.String()), nil) req, err := http.NewRequest(http.MethodHead, buildPingURL(r.Endpoint.String()), nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -28,7 +28,7 @@ import (
func TestPing(t *testing.T) { func TestPing(t *testing.T) {
server := test.NewServer( server := test.NewServer(
&test.RequestHandlerMapping{ &test.RequestHandlerMapping{
Method: "GET", Method: http.MethodHead,
Pattern: "/v2/", Pattern: "/v2/",
Handler: test.Handler(nil), Handler: test.Handler(nil),
}) })

View File

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"strings" "strings"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
@ -179,16 +180,20 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
InsecureSkipVerify: cfg.SkipTLSVerify, InsecureSkipVerify: cfg.SkipTLSVerify,
} }
if !cfg.SkipTLSVerify && len(cfg.CARootPath) > 0 { if !cfg.SkipTLSVerify && len(cfg.CARootPath) > 0 {
content, err := ioutil.ReadFile(cfg.CARootPath) if _, err := os.Stat(cfg.CARootPath); !os.IsNotExist(err) {
if err != nil { content, err := ioutil.ReadFile(cfg.CARootPath)
return nil, err if err != nil {
} return nil, err
pool := x509.NewCertPool() }
//Do not throw error if the certificate is malformed, so we can put a place holder. pool := x509.NewCertPool()
if ok := pool.AppendCertsFromPEM(content); !ok { //Do not throw error if the certificate is malformed, so we can put a place holder.
log.Warningf("Failed to append certificate to cert pool, cert path: %s", cfg.CARootPath) if ok := pool.AppendCertsFromPEM(content); !ok {
log.Warningf("Failed to append certificate to cert pool, cert path: %s", cfg.CARootPath)
} else {
tc.RootCAs = pool
}
} else { } else {
tc.RootCAs = pool log.Warningf("The root certificate file %s is not found, skip configuring root cert in UAA client.", cfg.CARootPath)
} }
} }
hc := &http.Client{ hc := &http.Client{

View File

@ -98,7 +98,7 @@ func TestNewClientWithCACert(t *testing.T) {
CARootPath: "/notexist", CARootPath: "/notexist",
} }
_, err := NewDefaultClient(cfg) _, err := NewDefaultClient(cfg)
assert.NotNil(err) assert.Nil(err)
//Skip if it's malformed. //Skip if it's malformed.
cfg.CARootPath = path.Join(currPath(), "test", "non-ca.pem") cfg.CARootPath = path.Join(currPath(), "test", "non-ca.pem")
_, err = NewDefaultClient(cfg) _, err = NewDefaultClient(cfg)

View File

@ -29,27 +29,24 @@ import (
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
) )
// FormatEndpoint formats endpoint // ParseEndpoint parses endpoint to a URL
func FormatEndpoint(endpoint string) string { func ParseEndpoint(endpoint string) (*url.URL, error) {
endpoint = strings.TrimSpace(endpoint) endpoint = strings.Trim(endpoint, " ")
endpoint = strings.TrimRight(endpoint, "/") endpoint = strings.TrimRight(endpoint, "/")
if !strings.HasPrefix(endpoint, "http://") && if len(endpoint) == 0 {
!strings.HasPrefix(endpoint, "https://") { return nil, fmt.Errorf("empty URL")
}
i := strings.Index(endpoint, "://")
if i >= 0 {
scheme := endpoint[:i]
if scheme != "http" && scheme != "https" {
return nil, fmt.Errorf("invalid scheme: %s", scheme)
}
} else {
endpoint = "http://" + endpoint endpoint = "http://" + endpoint
} }
return endpoint return url.ParseRequestURI(endpoint)
}
// ParseEndpoint parses endpoint to a URL
func ParseEndpoint(endpoint string) (*url.URL, error) {
endpoint = FormatEndpoint(endpoint)
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
return u, nil
} }
// ParseRepository splits a repository into two parts: project and rest // ParseRepository splits a repository into two parts: project and rest

View File

@ -23,37 +23,30 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestParseEndpoint(t *testing.T) { func TestParseEndpoint(t *testing.T) {
endpoint := "example.com" cases := []struct {
u, err := ParseEndpoint(endpoint) input string
if err != nil { err bool
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err) expected string
}{
{" example.com/ ", false, "http://example.com"},
{"ftp://example.com", true, ""},
{"http://example.com", false, "http://example.com"},
{"https://example.com", false, "https://example.com"},
{"http://example!@#!?//#", true, ""},
} }
if u.String() != "http://example.com" { for _, c := range cases {
t.Errorf("unexpected endpoint: %s != %s", endpoint, "http://example.com") u, err := ParseEndpoint(c.input)
} if c.err {
require.NotNil(t, err)
endpoint = "https://example.com" continue
u, err = ParseEndpoint(endpoint) }
if err != nil { require.Nil(t, err)
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err) assert.Equal(t, c.expected, u.String())
}
if u.String() != "https://example.com" {
t.Errorf("unexpected endpoint: %s != %s", endpoint, "https://example.com")
}
endpoint = " example.com/ "
u, err = ParseEndpoint(endpoint)
if err != nil {
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err)
}
if u.String() != "http://example.com" {
t.Errorf("unexpected endpoint: %s != %s", endpoint, "http://example.com")
} }
} }

View File

@ -106,7 +106,9 @@ func (e *EmailAPI) Ping() {
addr := net.JoinHostPort(host, strconv.Itoa(port)) addr := net.JoinHostPort(host, strconv.Itoa(port))
if err := email.Ping(addr, identity, username, if err := email.Ping(addr, identity, username,
password, pingEmailTimeout, ssl, insecure); err != nil { password, pingEmailTimeout, ssl, insecure); err != nil {
log.Debugf("ping %s failed: %v", addr, err) log.Errorf("failed to ping email server: %v", err)
e.CustomAbort(http.StatusBadRequest, err.Error()) // do not return any detail information of the error, or may cause SSRF security issue #3755
e.RenderError(http.StatusBadRequest, "failed to ping email server")
return
} }
} }

View File

@ -16,7 +16,6 @@ package api
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -36,7 +35,7 @@ func TestPingEmail(t *testing.T) {
assert.Equal(401, code, "the status code of ping email server with non-admin user should be 401") assert.Equal(401, code, "the status code of ping email server with non-admin user should be 401")
//case 2: bad request //case 2: empty email host
settings := `{ settings := `{
"email_host": "" "email_host": ""
}` }`
@ -47,7 +46,7 @@ func TestPingEmail(t *testing.T) {
return return
} }
assert.Equal(400, code, "the status code of ping email server should be 400") assert.Equal(400, code)
//case 3: secure connection with admin role //case 3: secure connection with admin role
settings = `{ settings = `{
@ -58,18 +57,13 @@ func TestPingEmail(t *testing.T) {
"email_ssl": true "email_ssl": true
}` }`
code, body, err := apiTest.PingEmail(*admin, []byte(settings)) code, _, err = apiTest.PingEmail(*admin, []byte(settings))
if err != nil { if err != nil {
t.Errorf("failed to test ping email server: %v", err) t.Errorf("failed to test ping email server: %v", err)
return return
} }
assert.Equal(400, code, "the status code of ping email server should be 400") assert.Equal(400, code)
if !strings.Contains(body, "535") {
t.Errorf("unexpected error: %s does not contains 535", body)
return
}
//case 4: ping email server whose settings are read from config //case 4: ping email server whose settings are read from config
code, _, err = apiTest.PingEmail(*admin, nil) code, _, err = apiTest.PingEmail(*admin, nil)
@ -78,5 +72,5 @@ func TestPingEmail(t *testing.T) {
return return
} }
assert.Equal(400, code, "the status code of ping email server should be 400") assert.Equal(400, code)
} }

View File

@ -29,7 +29,7 @@ type LdapAPI struct {
} }
const ( const (
pingErrorMessage = "LDAP connection test failed!" pingErrorMessage = "LDAP connection test failed"
loadSystemErrorMessage = "Can't load system configuration!" loadSystemErrorMessage = "Can't load system configuration!"
canNotOpenLdapSession = "Can't open LDAP session!" canNotOpenLdapSession = "Can't open LDAP session!"
searchLdapFailMessage = "LDAP search failed!" searchLdapFailMessage = "LDAP search failed!"
@ -72,6 +72,7 @@ func (l *LdapAPI) Ping() {
if err != nil { if err != nil {
log.Errorf("ldap connect fail, error: %v", err) log.Errorf("ldap connect fail, error: %v", err)
// do not return any detail information of the error, or may cause SSRF security issue #3755
l.RenderError(http.StatusBadRequest, pingErrorMessage) l.RenderError(http.StatusBadRequest, pingErrorMessage)
return return
} }

View File

@ -16,15 +16,12 @@ package api
import ( import (
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils"
registry_error "github.com/vmware/harbor/src/common/utils/error"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry" "github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth" "github.com/vmware/harbor/src/common/utils/registry/auth"
@ -60,27 +57,15 @@ func (t *TargetAPI) Prepare() {
func (t *TargetAPI) ping(endpoint, username, password string, insecure bool) { func (t *TargetAPI) ping(endpoint, username, password string, insecure bool) {
registry, err := newRegistryClient(endpoint, insecure, username, password) registry, err := newRegistryClient(endpoint, insecure, username, password)
if err != nil { if err == nil {
// timeout, dns resolve error, connection refused, etc. err = registry.Ping()
if urlErr, ok := err.(*url.Error); ok {
if netErr, ok := urlErr.Err.(net.Error); ok {
t.CustomAbort(http.StatusBadRequest, netErr.Error())
}
t.CustomAbort(http.StatusBadRequest, urlErr.Error())
}
log.Errorf("failed to create registry client: %#v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
if err = registry.Ping(); err != nil { if err != nil {
if regErr, ok := err.(*registry_error.HTTPError); ok { log.Errorf("failed to ping target: %v", err)
t.CustomAbort(regErr.StatusCode, regErr.Detail) // do not return any detail information of the error, or may cause SSRF security issue #3755
} t.RenderError(http.StatusBadRequest, "failed to ping target")
return
log.Errorf("failed to ping registry %s: %v", registry.Endpoint.String(), err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
} }
@ -117,7 +102,14 @@ func (t *TargetAPI) Ping() {
} }
if req.Endpoint != nil { if req.Endpoint != nil {
target.URL = *req.Endpoint url, err := utils.ParseEndpoint(*req.Endpoint)
if err != nil {
t.HandleBadRequest(err.Error())
return
}
// Prevent SSRF security issue #3755
target.URL = url.Scheme + "://" + url.Host + url.Path
} }
if req.Username != nil { if req.Username != nil {
target.Username = *req.Username target.Username = *req.Username
@ -129,11 +121,6 @@ func (t *TargetAPI) Ping() {
target.Insecure = *req.Insecure target.Insecure = *req.Insecure
} }
if len(target.URL) == 0 {
t.HandleBadRequest("empty endpoint")
return
}
t.ping(target.URL, target.Username, target.Password, target.Insecure) t.ping(target.URL, target.Username, target.Password, target.Insecure)
} }

View File

@ -39,6 +39,34 @@ type AuthenticateHelper interface {
OnBoardUser(u *models.User) error OnBoardUser(u *models.User) error
// Get user information from account repository // Get user information from account repository
SearchUser(username string) (*models.User, error) SearchUser(username string) (*models.User, error)
// Update user information after authenticate, such as Onboard or sync info etc
PostAuthenticate(u *models.User) error
}
// DefaultAuthenticateHelper - default AuthenticateHelper implementation
type DefaultAuthenticateHelper struct {
}
// Authenticate ...
func (d *DefaultAuthenticateHelper) Authenticate(m models.AuthModel) (*models.User, error) {
return nil, 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, fill in the user model based
// on the data record of the user
func (d *DefaultAuthenticateHelper) OnBoardUser(u *models.User) error {
return nil
}
//SearchUser - Get user information from account repository
func (d *DefaultAuthenticateHelper) SearchUser(username string) (*models.User, error) {
return nil, nil
}
//PostAuthenticate - Update user information after authenticate, such as Onboard or sync info etc
func (d *DefaultAuthenticateHelper) PostAuthenticate(u *models.User) error {
return nil
} }
var registry = make(map[string]AuthenticateHelper) var registry = make(map[string]AuthenticateHelper)
@ -79,6 +107,9 @@ func Login(m models.AuthModel) (*models.User, error) {
lock.Lock(m.Principal) lock.Lock(m.Principal)
time.Sleep(frozenTime) time.Sleep(frozenTime)
} }
authenticator.PostAuthenticate(user)
return user, err return user, err
} }
@ -112,3 +143,12 @@ func SearchUser(username string) (*models.User, error) {
} }
return helper.SearchUser(username) return helper.SearchUser(username)
} }
// PostAuthenticate -
func PostAuthenticate(u *models.User) error {
helper, err := getHelper()
if err != nil {
return err
}
return helper.PostAuthenticate(u)
}

View File

@ -21,7 +21,9 @@ import (
) )
// Auth implements Authenticator interface to authenticate user against DB. // Auth implements Authenticator interface to authenticate user against DB.
type Auth struct{} type Auth struct {
auth.DefaultAuthenticateHelper
}
// Authenticate calls dao to authenticate user. // Authenticate calls dao to authenticate user.
func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) { func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
@ -32,12 +34,6 @@ func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
return u, nil 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 // SearchUser - Check if user exist in local db
func (d *Auth) SearchUser(username string) (*models.User, error) { func (d *Auth) SearchUser(username string) (*models.User, error) {
var queryCondition = models.User{ var queryCondition = models.User{

View File

@ -16,6 +16,7 @@ package ldap
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
@ -26,7 +27,9 @@ import (
) )
// Auth implements AuthenticateHelper interface to authenticate against LDAP // Auth implements AuthenticateHelper interface to authenticate against LDAP
type Auth struct{} type Auth struct {
auth.DefaultAuthenticateHelper
}
// Authenticate checks user's credential against LDAP based on basedn template and LDAP URL, // 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 // if the check is successful a dummy record will be inserted into DB, such that this user can
@ -68,7 +71,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
u := models.User{} u := models.User{}
u.Username = ldapUsers[0].Username u.Username = ldapUsers[0].Username
u.Email = ldapUsers[0].Email u.Email = strings.TrimSpace(ldapUsers[0].Email)
u.Realname = ldapUsers[0].Realname u.Realname = ldapUsers[0].Realname
dn := ldapUsers[0].DN dn := ldapUsers[0].DN
@ -78,34 +81,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
log.Warningf("Failed to bind user, username: %s, dn: %s, error: %v", u.Username, dn, err) log.Warningf("Failed to bind user, username: %s, dn: %s, error: %v", u.Username, dn, err)
return nil, nil return nil, nil
} }
exist, err := dao.UserExists(u, "username")
if err != nil {
return nil, err
}
if exist {
currentUser, err := dao.GetUser(u)
if err != nil {
return nil, err
}
u.UserID = currentUser.UserID
u.HasAdminRole = currentUser.HasAdminRole
} else {
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 = user.UserID
}
return &u, nil return &u, nil
} }
// OnBoardUser will check if a user exists in user table, if not insert the user and // OnBoardUser will check if a user exists in user table, if not insert the user and
@ -153,6 +129,52 @@ func (l *Auth) SearchUser(username string) (*models.User, error) {
return &user, nil return &user, nil
} }
//PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser
func (l *Auth) PostAuthenticate(u *models.User) error {
exist, err := dao.UserExists(*u, "username")
if err != nil {
return err
}
if exist {
queryCondition := models.User{
Username: u.Username,
}
dbUser, err := dao.GetUser(queryCondition)
if err != nil {
return err
}
if dbUser == nil {
fmt.Printf("User not found in DB %+v", u)
return nil
}
u.UserID = dbUser.UserID
u.HasAdminRole = dbUser.HasAdminRole
if dbUser.Email != u.Email {
Re := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
if !Re.MatchString(u.Email) {
log.Debugf("Not a valid email address: %v, skip to sync", u.Email)
} else {
dao.ChangeUserProfile(*u, "Email")
}
u.Email = dbUser.Email
}
return nil
}
err = auth.OnBoardUser(u)
if err != nil {
return err
}
if u.UserID <= 0 {
return fmt.Errorf("Can not OnBoardUser %v", u)
}
return nil
}
func init() { func init() {
auth.Register("ldap_auth", &Auth{}) auth.Register("ldap_auth", &Auth{})
} }

View File

@ -19,6 +19,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
@ -204,3 +205,59 @@ func TestAuthenticateHelperSearchUser(t *testing.T) {
t.Error("Failed to search user test") t.Error("Failed to search user test")
} }
} }
func TestPostAuthentication(t *testing.T) {
assert := assert.New(t)
user1 := &models.User{
Username: "test003",
Email: "test003@vmware.com",
Realname: "test003",
}
queryCondition := models.User{
Username: "test003",
Realname: "test003",
}
err := auth.OnBoardUser(user1)
assert.Nil(err)
user2 := &models.User{
Username: "test003",
Email: "234invalidmail@@@@@",
}
auth.PostAuthenticate(user2)
dbUser, err := dao.GetUser(queryCondition)
if err != nil {
t.Fatalf("Failed to get user, error %v", err)
}
assert.EqualValues("test003@vmware.com", dbUser.Email)
user3 := &models.User{
Username: "test003",
}
auth.PostAuthenticate(user3)
dbUser, err = dao.GetUser(queryCondition)
if err != nil {
t.Fatalf("Failed to get user, error %v", err)
}
assert.EqualValues("test003@vmware.com", dbUser.Email)
user4 := &models.User{
Username: "test003",
Email: "test003@example.com",
}
auth.PostAuthenticate(user4)
dbUser, err = dao.GetUser(queryCondition)
if err != nil {
t.Fatalf("Failed to get user, error %v", err)
}
assert.EqualValues("test003@example.com", dbUser.Email)
dao.CleanUser(int64(dbUser.UserID))
}

View File

@ -16,6 +16,7 @@ package uaa
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"sync" "sync"
@ -38,6 +39,7 @@ func CreateClient() (uaa.Client, error) {
ClientSecret: UAASettings.ClientSecret, ClientSecret: UAASettings.ClientSecret,
Endpoint: UAASettings.Endpoint, Endpoint: UAASettings.Endpoint,
SkipTLSVerify: !UAASettings.VerifyCert, SkipTLSVerify: !UAASettings.VerifyCert,
CARootPath: os.Getenv("UAA_CA_ROOT"),
} }
return uaa.NewDefaultClient(cfg) return uaa.NewDefaultClient(cfg)
} }
@ -46,6 +48,7 @@ func CreateClient() (uaa.Client, error) {
type Auth struct { type Auth struct {
sync.Mutex sync.Mutex
client uaa.Client client uaa.Client
auth.DefaultAuthenticateHelper
} }
//Authenticate ... //Authenticate ...

View File

@ -1,6 +1,6 @@
{ {
"name": "harbor-ui", "name": "harbor-ui",
"version": "0.6.2", "version": "0.6.6",
"description": "Harbor shared UI components based on Clarity and Angular4", "description": "Harbor shared UI components based on Clarity and Angular4",
"scripts": { "scripts": {
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json", "start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",

View File

@ -1,6 +1,6 @@
{ {
"name": "harbor-ui", "name": "harbor-ui",
"version": "0.6.2", "version": "0.6.6",
"description": "Harbor shared UI components based on Clarity and Angular4", "description": "Harbor shared UI components based on Clarity and Angular4",
"author": "VMware", "author": "VMware",
"module": "index.js", "module": "index.js",

View File

@ -214,7 +214,7 @@ export class HarborLibraryModule {
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }, config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService }, config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService }, config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
//Do initializing // Do initializing
TranslateServiceInitializer, TranslateServiceInitializer,
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,

View File

@ -26,8 +26,25 @@ export const REPOSITORY_STYLE = `.option-right {
font-size: 32px; font-size: 32px;
} }
pre { .no-info-div {
white-space: pre-wrap; background: white;
border: 1px;
border-style: solid;
border-color: #CCCCCC;
padding: 12px 12px 12px 12px;
}
.info-div {
background: white;
border: 1px;
border-style: solid;
border-color: #CCCCCC;
padding: 0px 12px 24px 12px;
}
.info-pre {
border: 0px;
max-height: fit-content;
} }
#info-edit-button { #info-edit-button {

View File

@ -24,12 +24,18 @@ export const REPOSITORY_TEMPLATE = `
<section id="info" role="tabpanel" aria-labelledby="repo-info" [hidden]='!isCurrentTabContent("info")'> <section id="info" role="tabpanel" aria-labelledby="repo-info" [hidden]='!isCurrentTabContent("info")'>
<form #repoInfoForm="ngForm"> <form #repoInfoForm="ngForm">
<div id="info-edit-button"> <div id="info-edit-button">
<button class="btn btn-sm" [disabled]="editing" (click)="editInfo()" >{{'BUTTON.EDIT' | translate}}</button> <button class="btn btn-sm" [disabled]="editing || !hasProjectAdminRole " (click)="editInfo()" >{{'BUTTON.EDIT' | translate}}</button>
</div> </div>
<div> <div *ngIf="!editing">
<h3 *ngIf="!editing && !hasInfo()" >{{'REPOSITORY.NO_INFO' | translate }}</h3> <div *ngIf="!hasInfo()" class="no-info-div">
<pre *ngIf="!editing && hasInfo()" ><code>{{ imageInfo }}</code></pre> <p>{{'REPOSITORY.NO_INFO' | translate }}<p>
<textarea *ngIf="editing" name="info-edit-textarea" [(ngModel)]="imageInfo"></textarea> </div>
<div *ngIf="hasInfo()" class="info-div">
<pre class="info-pre">{{ imageInfo }}</pre>
</div>
</div>
<div *ngIf="editing">
<textarea rows="5" name="info-edit-textarea" [(ngModel)]="imageInfo"></textarea>
</div> </div>
<div class="btn-sm" *ngIf="editing"> <div class="btn-sm" *ngIf="editing">
<button class="btn btn-primary" [disabled]="!hasChanges()" (click)="saveInfo()" >{{'BUTTON.SAVE' | translate}}</button> <button class="btn btn-primary" [disabled]="!hasChanges()" (click)="saveInfo()" >{{'BUTTON.SAVE' | translate}}</button>

View File

@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { HttpModule, Http } from '@angular/http'; import { HttpModule, Http } from '@angular/http';
import { ClarityModule } from 'clarity-angular'; import { ClarityModule } from 'clarity-angular';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from "@ngx-translate/core"; import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from '@ngx-translate/core';
import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler'; import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslatorJsonLoader } from '../i18n/local-json.loader'; import { TranslatorJsonLoader } from '../i18n/local-json.loader';

View File

@ -1,6 +1,6 @@
{ {
"name": "harbor", "name": "harbor",
"version": "1.2.0", "version": "1.3.0",
"description": "Harbor UI with Clarity", "description": "Harbor UI with Clarity",
"angular-cli": {}, "angular-cli": {},
"scripts": { "scripts": {
@ -31,7 +31,7 @@
"clarity-icons": "^0.10.17", "clarity-icons": "^0.10.17",
"clarity-ui": "^0.10.17", "clarity-ui": "^0.10.17",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"harbor-ui": "^0.6.0-test-5", "harbor-ui": "0.6.6",
"intl": "^1.2.5", "intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2", "mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0", "ngx-cookie": "^1.0.0",

View File

@ -21,7 +21,7 @@ export default {
plugins: [ plugins: [
nodeResolve({jsnext: true, module: true, browser: true}), nodeResolve({jsnext: true, module: true, browser: true}),
commonjs({ commonjs({
include: ['node_modules/**'] include: ['node_modules/**'],
}), }),
uglify() uglify()
] ]

View File

@ -48,3 +48,4 @@
position: relative; position: relative;
top: -9px; top: -9px;
} }
.bg{position: absolute;top: 60px; right: 0px;width: 100%; height: 100%; background-size: cover;}

View File

@ -1,4 +1,5 @@
<div class="login-wrapper login-wrapper-override" [ngStyle]="{'background-image': 'url(' + customLoginBgImg + ')'}"> <div class="login-wrapper login-wrapper-override">
<div class="bg" *ngIf="customLoginBgImg" [ngStyle]="{'background-image': 'url(static/images/' + customLoginBgImg + ')'}"></div>
<form #signInForm="ngForm" class="login"> <form #signInForm="ngForm" class="login">
<label class="title"> {{customAppTitle? customAppTitle:(appTitle | translate)}}<span class="trademark tm-font">&#8482;</span> <label class="title"> {{customAppTitle? customAppTitle:(appTitle | translate)}}<span class="trademark tm-font">&#8482;</span>
</label> </label>

View File

@ -57,7 +57,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
//custom skin //custom skin
let customSkinObj = this.skinableConfig.getProjects(); let customSkinObj = this.skinableConfig.getProject();
if (customSkinObj && customSkinObj.projectName) { if (customSkinObj && customSkinObj.projectName) {
this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.projectName}).subscribe(res => { this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.projectName}).subscribe(res => {
//Placeholder text //Placeholder text

View File

@ -8,22 +8,47 @@
<search-result></search-result> <search-result></search-result>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
<nav class="sidenav" style="padding: 12px 36px;" *ngIf="isUserExisting"> <clr-vertical-nav [clrVerticalNavCollapsible]="true" *ngIf="isUserExisting">
<section class="sidenav-content" style="padding-top: 20px;"> <a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/projects">
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link nav-link-override">{{'SIDE_NAV.PROJECTS' | translate}}</a> <clr-icon shape="organization" clrVerticalNavIcon></clr-icon>
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link nav-link-override" style="margin-top: 4px;">{{'SIDE_NAV.LOGS' | translate}}</a> {{'SIDE_NAV.PROJECTS' | translate}}
<section class="nav-group collapsible" *ngIf="isSystemAdmin" style="margin-top: 4px;"> </a>
<input id="tabsystem" type="checkbox"> <a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/logs">
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label> <clr-icon shape="list" clrVerticalNavIcon></clr-icon>
<ul class="nav-list"> {{'SIDE_NAV.LOGS' | translate}}
<li><a class="nav-link nav-link-override" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}</a></li> </a>
<li><a class="nav-link nav-link-override" routerLink="/harbor/replications" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</a></li> <clr-vertical-nav-group *ngIf="isSystemAdmin" routerLinkActive="active">
<li><a class="nav-link nav-link-override" routerLink="/harbor/registry" routerLinkActive="active">{{'APP_TITLE.REG' | translate}}</a></li> <clr-icon shape="administrator" clrVerticalNavIcon></clr-icon>
<li><a class="nav-link nav-link-override" routerLink="/harbor/configs" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}</a></li> {{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}
</ul> <a routerLink="#" hidden aria-hidden="true"></a>
</section> <clr-vertical-nav-group-children *clrIfExpanded="true">
</section> <a clrVerticalNavLink
</nav> routerLink="/harbor/users"
routerLinkActive="active">
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
</a>
<a clrVerticalNavLink
routerLink="/harbor/registries"
routerLinkActive="active">
<clr-icon shape="block" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}
</a>
<a clrVerticalNavLink
routerLink="/harbor/replications"
routerLinkActive="active">
<clr-icon shape="cloud-traffic" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}
</a>
<a clrVerticalNavLink
routerLink="/harbor/configs"
routerLinkActive="active">
<clr-icon shape="cog" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}
</a>
</clr-vertical-nav-group-children>
</clr-vertical-nav-group>
</clr-vertical-nav>
</div> </div>
</clr-main-container> </clr-main-container>
<account-settings-modal></account-settings-modal> <account-settings-modal></account-settings-modal>

View File

@ -2,7 +2,7 @@
<div class="branding"> <div class="branding">
<a href="javascript:void(0)" class="nav-link" (click)="homeAction()"> <a href="javascript:void(0)" class="nav-link" (click)="homeAction()">
<clr-icon shape="vm-bug" *ngIf="!customStyle?.headerLogo"></clr-icon> <clr-icon shape="vm-bug" *ngIf="!customStyle?.headerLogo"></clr-icon>
<img [attr.src]="customStyle?.headerLogo" *ngIf="customStyle?.headerLogo" style="width: 36px;height: 36px; object-fit: fill;"> <img [attr.src]="'static/images/'+customStyle?.headerLogo" *ngIf="customStyle?.headerLogo" style="width: 36px;height: 36px; object-fit: fill;">
<span class="title">{{customProjectName?.projectName? customProjectName?.projectName:(appTitle | translate)}}</span> <span class="title">{{customProjectName?.projectName? customProjectName?.projectName:(appTitle | translate)}}</span>
</a> </a>
</div> </div>

View File

@ -80,11 +80,15 @@ const harborRoutes: Routes = [
component: UserComponent, component: UserComponent,
canActivate: [SystemAdminGuard] canActivate: [SystemAdminGuard]
}, },
{
path: 'registries',
component: DestinationPageComponent,
canActivate: [SystemAdminGuard]
},
{ {
path: 'replications', path: 'replications',
component: TotalReplicationPageComponent, component: TotalReplicationPageComponent,
canActivate: [SystemAdminGuard], canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard],
}, },
{ {
path: 'replications/:id/rule', path: 'replications/:id/rule',

View File

@ -1,4 +1,4 @@
<h2 class="custom-h2">{{'REPLICATION.ENDPOINTS' | translate}}</h2> <h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}</h2>
<div style="margin-top: 24px;"> <div style="margin-top: 24px;">
<hbr-endpoint></hbr-endpoint> <hbr-endpoint></hbr-endpoint>
</div> </div>

View File

@ -11,6 +11,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// This Module is used as Container For Endpoint and Replication Rules
// Will deprecated on Harbor 1.4.0
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({

View File

@ -1,4 +1,4 @@
<h2 class="custom-h2">{{'REPLICATION.REPLICATION_RULE' | translate}}</h2> <h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</h2>
<div style="margin-top: 24px;"> <div style="margin-top: 24px;">
<hbr-replication [readonly]="false" [withReplicationJob]='true' (openCreateRule)="openCreatePage()" (openEdit)="openEditPage($event)" (redirect)="customRedirect($event)"></hbr-replication> <hbr-replication [readonly]="false" [withReplicationJob]='true' (openCreateRule)="openCreatePage()" (openEdit)="openEditPage($event)" (redirect)="customRedirect($event)"></hbr-replication>
</div> </div>

View File

@ -35,7 +35,7 @@ export class AboutDialogComponent implements OnInit{
ngOnInit(): void { ngOnInit(): void {
// custom skin // custom skin
let customSkinObj = this.skinableConfig.getProjects(); let customSkinObj = this.skinableConfig.getProject();
if (customSkinObj) { if (customSkinObj) {
let selectedLang = this.translate.currentLang; let selectedLang = this.translate.currentLang;
this.customName = customSkinObj; this.customName = customSkinObj;

View File

@ -12,7 +12,7 @@ export class SkinableConfig {
constructor(private http: Http) {} constructor(private http: Http) {}
public getCustomFile(): Promise<any> { public getCustomFile(): Promise<any> {
return this.http.get('../setting.json') return this.http.get('../static/setting.json')
.toPromise() .toPromise()
.then(response => { this.customSkinData = response.json(); return this.customSkinData; }) .then(response => { this.customSkinData = response.json(); return this.customSkinData; })
.catch(error => { .catch(error => {
@ -24,9 +24,9 @@ export class SkinableConfig {
return this.customSkinData; return this.customSkinData;
} }
public getProjects() { public getProject() {
if (this.customSkinData) { if (this.customSkinData) {
return this.customSkinData.projects; return this.customSkinData.project;
}else { }else {
return null; return null;
} }

View File

@ -102,7 +102,8 @@
"SYSTEM_MGMT": { "SYSTEM_MGMT": {
"NAME": "Administration", "NAME": "Administration",
"USER": "Users", "USER": "Users",
"REPLICATION": "Replication", "REGISTRY": "Registries",
"REPLICATION": "Replications",
"CONFIG": "Configuration" "CONFIG": "Configuration"
}, },
"LOGS": "Logs" "LOGS": "Logs"

View File

@ -98,7 +98,8 @@
"SYSTEM_MGMT": { "SYSTEM_MGMT": {
"NAME": "Administración", "NAME": "Administración",
"USER": "Usuarios", "USER": "Usuarios",
"REPLICATION": "Replicación", "REGISTRY": "Registries",
"REPLICATION": "Replicacións",
"CONFIG": "Configuración" "CONFIG": "Configuración"
}, },
"LOGS": "Logs" "LOGS": "Logs"

View File

@ -98,6 +98,7 @@
"SYSTEM_MGMT": { "SYSTEM_MGMT": {
"NAME": "系统管理", "NAME": "系统管理",
"USER": "用户管理", "USER": "用户管理",
"REGISTRY": "仓库管理",
"REPLICATION": "复制管理", "REPLICATION": "复制管理",
"CONFIG": "配置管理" "CONFIG": "配置管理"
}, },

View File

@ -1,11 +1,11 @@
{ {
"headerBgColor": "#004a70", "headerBgColor": "",
"headerLogo": "", "headerLogo": "",
"loginBgImg": "", "loginBgImg": "",
"appTitle": "VMware Harbor", "appTitle": "",
"projects": { "project": {
"companyName": "vmware", "companyName": "",
"projectName": "Harbor", "projectName": "",
"introduction": { "introduction": {
"zh-cn": "", "zh-cn": "",
"es-es": "", "es-es": "",

View File

@ -16,5 +16,5 @@
Documentation This resource provides any keywords related to the Harbor private registry appliance Documentation This resource provides any keywords related to the Harbor private registry appliance
*** Variables *** *** Variables ***
${administration_user_tag_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/section/ul/li[1]/a ${administration_user_tag_xpath} //clr-vertical-nav-group-children/a[contains(.,'Users')]
${administration_tag_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/section/label ${administration_tag_xpath} //clr-vertical-nav-group[contains(.,'Admin')]

View File

@ -38,14 +38,14 @@ Init LDAP
Sleep 1 Sleep 1
Capture Page Screenshot Capture Page Screenshot
Disable Ldap Verify Cert Checkbox 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] Click Element xpath=${config_save_button_xpath}
Sleep 2 Sleep 2
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3] Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]
Sleep 1 Sleep 1
Capture Page Screenshot Capture Page Screenshot
Switch To Configure Switch To Configure
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/section/ul/li[4]/a Click Element xpath=${configuration_xpath}
Sleep 2 Sleep 2
Test Ldap Connection Test Ldap Connection
@ -89,32 +89,32 @@ Ldap Verify Cert Checkbox Should Be Disabled
Set Pro Create Admin Only Set Pro Create Admin Only
#set limit to admin only #set limit to admin only
Sleep 2 Sleep 2
Click Element xpath=//clr-main-container//nav//ul/li[4] Click Element xpath=${configuration_xpath}
Sleep 1 Sleep 1
Click Element xpath=//select[@id="proCreation"] Click Element xpath=//select[@id="proCreation"]
Click Element xpath=//select[@id="proCreation"]//option[@value="adminonly"] Click Element xpath=//select[@id="proCreation"]//option[@value="adminonly"]
Sleep 1 Sleep 1
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1] Click Element xpath=${config_save_button_xpath}
Capture Page Screenshot AdminCreateOnly.png Capture Page Screenshot AdminCreateOnly.png
Set Pro Create Every One Set Pro Create Every One
#set limit to Every One #set limit to Every One
Click Element xpath=//clr-main-container//nav//ul/li[4] Click Element xpath=${configuration_xpath}
Sleep 1 Sleep 1
Click Element xpath=//select[@id="proCreation"] Click Element xpath=//select[@id="proCreation"]
Click Element xpath=//select[@id="proCreation"]//option[@value="everyone"] Click Element xpath=//select[@id="proCreation"]//option[@value="everyone"]
Sleep 1 Sleep 1
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1] Click Element xpath=${config_save_button_xpath}
Sleep 2 Sleep 2
Capture Page Screenshot EveryoneCreate.png Capture Page Screenshot EveryoneCreate.png
Disable Self Reg Disable Self Reg
Click Element xpath=//clr-main-container//nav//ul/li[4] Click Element xpath=${configuration_xpath}
Mouse Down xpath=${self_reg_xpath} Mouse Down xpath=${self_reg_xpath}
Mouse Up xpath=${self_reg_xpath} Mouse Up xpath=${self_reg_xpath}
Sleep 1 Sleep 1
Self Reg Should Be Disabled Self Reg Should Be Disabled
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1] Click Element xpath=${config_save_button_xpath}
Capture Page Screenshot DisableSelfReg.png Capture Page Screenshot DisableSelfReg.png
Sleep 1 Sleep 1
@ -123,7 +123,7 @@ Enable Self Reg
Mouse Up xpath=${self_reg_xpath} Mouse Up xpath=${self_reg_xpath}
Sleep 1 Sleep 1
Self Reg Should Be Enabled Self Reg Should Be Enabled
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1] Click Element xpath=${config_save_button_xpath}
Capture Page Screenshot EnableSelfReg.png Capture Page Screenshot EnableSelfReg.png
Sleep 1 Sleep 1
@ -142,13 +142,13 @@ Project Creation Should Not Display
## System settings ## System settings
Switch To System Settings Switch To System Settings
Sleep 1 Sleep 1
Click Element xpath=//clr-main-container//nav//ul/li[4] Click Element xpath=${configuration_xpath}
Click Element xpath=//*[@id="config-system"] Click Element xpath=//*[@id="config-system"]
Modify Token Expiration Modify Token Expiration
[Arguments] ${minutes} [Arguments] ${minutes}
Input Text xpath=//*[@id="tokenExpiration"] ${minutes} Input Text xpath=//*[@id="tokenExpiration"] ${minutes}
Click Button xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1] Click Button xpath=${config_save_button_xpath}
Sleep 1 Sleep 1
Token Must Be Match Token Must Be Match
@ -159,7 +159,7 @@ Token Must Be Match
Check Verify Remote Cert Check Verify Remote Cert
Mouse Down xpath=//*[@id="clr-checkbox-verifyRemoteCert"] Mouse Down xpath=//*[@id="clr-checkbox-verifyRemoteCert"]
Mouse Up xpath=//*[@id="clr-checkbox-verifyRemoteCert"] Mouse Up xpath=//*[@id="clr-checkbox-verifyRemoteCert"]
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1] Click Element xpath=${config_save_button_xpath}
Capture Page Screenshot RemoteCert.png Capture Page Screenshot RemoteCert.png
Sleep 1 Sleep 1
@ -191,7 +191,7 @@ Config Email
Mouse Down xpath=//*[@id="clr-checkbox-emailInsecure"] Mouse Down xpath=//*[@id="clr-checkbox-emailInsecure"]
Mouse Up xpath=//*[@id="clr-checkbox-emailInsecure"] Mouse Up xpath=//*[@id="clr-checkbox-emailInsecure"]
Sleep 1 Sleep 1
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1] Click Element xpath=${config_save_button_xpath}
Sleep 6 Sleep 6
Verify Email Verify Email
@ -206,11 +206,11 @@ Set Scan All To None
click element //vulnerability-config//select click element //vulnerability-config//select
click element //vulnerability-config//select/option[@value='none'] click element //vulnerability-config//select/option[@value='none']
sleep 1 sleep 1
click element //config//div/button[contains(.,'SAVE')] click element ${config_save_button_xpath}
Set Scan All To Daily Set Scan All To Daily
click element //vulnerability-config//select click element //vulnerability-config//select
click element //vulnerability-config//select/option[@value='daily'] click element //vulnerability-config//select/option[@value='daily']
sleep 1 sleep 1
click element //config//div/button[contains(.,'SAVE')] click element ${config_save_button_xpath}
Click Scan Now Click Scan Now
click element //vulnerability-config//button[contains(.,'SCAN')] click element //vulnerability-config//button[contains(.,'SCAN')]

View File

@ -19,3 +19,5 @@ Documentation This resource provides any keywords related to the Harbor private
${project_create_xpath} //clr-dg-action-bar//button[contains(.,'New')] ${project_create_xpath} //clr-dg-action-bar//button[contains(.,'New')]
${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] ${test_ldap_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]
${config_save_button_xpath} //config//div/button[contains(.,'SAVE')]
${configuration_xpath} //clr-vertical-nav-group-children/a[contains(.,'Configuration')]

View File

@ -56,7 +56,7 @@ Switch To Log
Sleep 1 Sleep 1
Switch To Replication Switch To Replication
Click Element xpath=${replication_xpath} Click Element xpath=${project_replication_xpath}
Sleep 1 Sleep 1
Back To projects Back To projects

View File

@ -20,6 +20,6 @@ ${create_project_button_css} .btn
${project_name_xpath} //*[@id="create_project_name"] ${project_name_xpath} //*[@id="create_project_name"]
${project_public_xpath} //input[@name='public']/..//label ${project_public_xpath} //input[@name='public']/..//label
${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-container.main-container div.content-container div.content-area.content-area-override project div.row div.col-lg-12.col-md-12.col-sm-12.col-xs-12 div.row.flex-items-xs-between div.option-left create-project clr-modal div.modal div.modal-dialog div.modal-content div.modal-footer button.btn.btn-primary ${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-container.main-container div.content-container div.content-area.content-area-override project div.row div.col-lg-12.col-md-12.col-sm-12.col-xs-12 div.row.flex-items-xs-between div.option-left create-project clr-modal div.modal div.modal-dialog div.modal-content div.modal-footer button.btn.btn-primary
${log_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[2] ${log_xpath} //clr-main-container/div/clr-vertical-nav/div/a[contains(.,'Logs')]
${projects_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[1] ${projects_xpath} //clr-main-container/div/clr-vertical-nav/div/a[contains(.,'Projects')]
${replication_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/nav/ul/li[4]/a ${project_replication_xpath} //project-detail//a[contains(.,'Replication')]

View File

@ -26,3 +26,4 @@ ${destination_url_xpath} //*[@id='destination_url']
${destination_username_xpath} //*[@id='destination_username'] ${destination_username_xpath} //*[@id='destination_username']
${destination_password_xpath} //*[@id='destination_password'] ${destination_password_xpath} //*[@id='destination_password']
${replicaton_save_xpath} //button[contains(.,'OK')] ${replicaton_save_xpath} //button[contains(.,'OK')]
${replication_xpath} //clr-vertical-nav-group-children/a[contains(.,'Replication')]

View File

@ -31,7 +31,7 @@ Change Password
Sleep 1 Sleep 1
Click Element xpath=//password-setting/clr-modal//button[2] Click Element xpath=//password-setting/clr-modal//button[2]
Sleep 2 Sleep 2
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[2] Click Element xpath=${log_xpath}
Sleep 1 Sleep 1
Update User Comment Update User Comment