diff --git a/Makefile b/Makefile index d42a8c97e..75ea0af33 100644 --- a/Makefile +++ b/Makefile @@ -306,6 +306,8 @@ modify_composefile_clair: @cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRTPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME) @$(SEDCMD) -i 's/__postgresql_version__/$(CLAIRDBVERSION)/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: @echo "change mode of source files." diff --git a/make/common/templates/registry/config.yml b/make/common/templates/registry/config.yml index 4fd13d26d..c49805a04 100644 --- a/make/common/templates/registry/config.yml +++ b/make/common/templates/registry/config.yml @@ -4,32 +4,31 @@ log: fields: service: registry storage: - cache: - layerinfo: inmemory - filesystem: - rootdirectory: /storage - maintenance: - uploadpurging: - enabled: false - delete: - enabled: true + cache: + layerinfo: inmemory + filesystem: + rootdirectory: /storage + maintenance: + uploadpurging: + enabled: false + delete: + enabled: true http: - addr: :5000 - secret: placeholder - debug: - addr: localhost:5001 + addr: :5000 + secret: placeholder + debug: + addr: localhost:5001 auth: token: issuer: harbor-token-issuer realm: $ui_url/service/token rootcertbundle: /etc/registry/root.crt service: harbor-registry - notifications: endpoints: - - name: harbor - disabled: false - url: http://ui:8080/service/notifications - timeout: 3000ms - threshold: 5 - backoff: 1s + - name: harbor + disabled: false + url: http://ui:8080/service/notifications + timeout: 3000ms + threshold: 5 + backoff: 1s diff --git a/make/common/templates/registry/config_ha.yml b/make/common/templates/registry/config_ha.yml index bedf6f482..f3b04fcb1 100644 --- a/make/common/templates/registry/config_ha.yml +++ b/make/common/templates/registry/config_ha.yml @@ -4,14 +4,14 @@ log: fields: service: registry storage: - cache: - layerinfo: redis - Place_holder_for_Storage_configureation - maintenance: - uploadpurging: - enabled: false - delete: - enabled: true + cache: + layerinfo: redis + Place_holder_for_Storage_configureation + maintenance: + uploadpurging: + enabled: false + delete: + enabled: true redis: addr: $redis_url db: 0 @@ -24,10 +24,10 @@ redis: idletimeout: 300s http: - addr: :5000 - secret: placeholder - debug: - addr: localhost:5001 + addr: :5000 + secret: placeholder + debug: + addr: localhost:5001 auth: token: issuer: harbor-token-issuer @@ -37,9 +37,9 @@ auth: notifications: endpoints: - - name: harbor - disabled: false - url: http://ui:8080/service/notifications - timeout: 3000ms - threshold: 5 - backoff: 1s + - name: harbor + disabled: false + url: http://ui:8080/service/notifications + timeout: 3000ms + threshold: 5 + backoff: 1s diff --git a/make/docker-compose.tpl b/make/docker-compose.tpl index 26a87a6e3..a6755236b 100644 --- a/make/docker-compose.tpl +++ b/make/docker-compose.tpl @@ -76,7 +76,7 @@ services: volumes: - ./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/certificates/:/etc/ui/certifates/ + - ./common/config/ui/certificates/:/etc/ui/certificates/ - /data/secretkey:/etc/ui/key:z - /data/ca_download/:/etc/ui/ca/:z - /data/psc/:/etc/ui/token/:z diff --git a/make/ha/docker-compose.clair.tpl b/make/ha/docker-compose.clair.tpl new file mode 100644 index 000000000..3a5590e0c --- /dev/null +++ b/make/ha/docker-compose.clair.tpl @@ -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 diff --git a/make/harbor.cfg b/make/harbor.cfg index 8df07e977..156a6acfa 100644 --- a/make/harbor.cfg +++ b/make/harbor.cfg @@ -142,4 +142,5 @@ uaa_endpoint = uaa.mydomain.org uaa_clientid = id uaa_clientsecret = secret uaa_verify_cert = true +uaa_ca_cert = /path/to/ca.pem ############# diff --git a/make/install.sh b/make/install.sh index f80805618..069c4b722 100755 --- a/make/install.sh +++ b/make/install.sh @@ -165,7 +165,7 @@ if [ $with_notary ] && [ ! $harbor_ha ] then prepare_para="${prepare_para} --with-notary" fi -if [ $with_clair ] && [ ! $harbor_ha ] +if [ $with_clair ] then prepare_para="${prepare_para} --with-clair" fi @@ -182,7 +182,7 @@ if [ $with_notary ] && [ ! $harbor_ha ] then docker_compose_list="${docker_compose_list} -f docker-compose.notary.yml" fi -if [ $with_clair ] && [ ! $harbor_ha ] +if [ $with_clair ] then docker_compose_list="${docker_compose_list} -f docker-compose.clair.yml" fi @@ -199,6 +199,8 @@ if [ $harbor_ha ] then mv docker-compose.yml docker-compose.yml.bak 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 docker-compose $docker_compose_list up -d diff --git a/make/prepare b/make/prepare index 226fcc707..ba5e7476c 100755 --- a/make/prepare +++ b/make/prepare @@ -30,8 +30,13 @@ def validate(conf, args): redis_url = rcp.get("configuration", "redis_url") 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") - if args.notary_mode or args.clair_mode: - raise Exception("Error: HA mode doesn't support clair and notary currently") + if args.notary_mode: + 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_key_path = rcp.get("configuration", "ssl_cert_key") 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_clientsecret = rcp.get("configuration", "uaa_clientsecret") 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) 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") ui_conf_env = os.path.join(config_dir, "ui", "env") 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") registry_conf = os.path.join(config_dir, "registry", "config.yml") 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) 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): subj_list = [item for item in dirty_subj.strip().split("/") \ diff --git a/src/common/dao/config_test.go b/src/common/dao/config_test.go index d48628ddb..447dff871 100644 --- a/src/common/dao/config_test.go +++ b/src/common/dao/config_test.go @@ -46,7 +46,7 @@ func TestAuthModeCanBeModified(t *testing.T) { t.Fatalf("failed to register user: %v", err) } 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) } }(id) @@ -68,4 +68,4 @@ func TestAuthModeCanBeModified(t *testing.T) { t.Errorf("unexpected result: %t != %t", flag, false) } } -} \ No newline at end of file +} diff --git a/src/common/dao/user.go b/src/common/dao/user.go index 17ed5e37f..2ce881ad5 100644 --- a/src/common/dao/user.go +++ b/src/common/dao/user.go @@ -258,9 +258,12 @@ func DeleteUser(userID int) error { } // ChangeUserProfile ... -func ChangeUserProfile(user models.User) error { +func ChangeUserProfile(user models.User, cols ...string) error { 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) return err } @@ -290,3 +293,12 @@ func OnBoardUser(u *models.User) error { } 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 +} diff --git a/src/common/dao/user_test.go b/src/common/dao/user_test.go index f8bf76e92..edcbce6a6 100644 --- a/src/common/dao/user_test.go +++ b/src/common/dao/user_test.go @@ -39,7 +39,7 @@ func TestDeleteUser(t *testing.T) { t.Fatalf("failed to register user: %v", err) } 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) } }(id) @@ -88,13 +88,5 @@ func TestOnBoardUser(t *testing.T) { err = OnBoardUser(u) assert.Nil(err) assert.True(u.UserID == id) - deleteUser(int64(id)) -} - -func deleteUser(id int64) error { - if _, err := GetOrmer().QueryTable(&models.User{}). - Filter("UserID", id).Delete(); err != nil { - return err - } - return nil + CleanUser(int64(id)) } diff --git a/src/common/models/models_test.go b/src/common/models/models_test.go deleted file mode 100644 index 822312399..000000000 --- a/src/common/models/models_test.go +++ /dev/null @@ -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) { -} diff --git a/src/common/models/replication_job.go b/src/common/models/replication_job.go index 8c5744800..8c14234d9 100644 --- a/src/common/models/replication_job.go +++ b/src/common/models/replication_job.go @@ -89,14 +89,15 @@ func (r *RepTarget) Valid(v *validation.Validation) { v.SetError("name", "max length is 64") } - if len(r.URL) == 0 { - v.SetError("endpoint", "can not be empty") - } - - r.URL = utils.FormatEndpoint(r.URL) - - if len(r.URL) > 64 { - v.SetError("endpoint", "max length is 64") + url, err := utils.ParseEndpoint(r.URL) + if err != nil { + v.SetError("endpoint", err.Error()) + } else { + // Prevent SSRF security issue #3755 + r.URL = url.Scheme + "://" + url.Host + url.Path + if len(r.URL) > 64 { + v.SetError("endpoint", "max length is 64") + } } // password is encoded using base64, the length of this field diff --git a/src/common/models/target_test.go b/src/common/models/target_test.go new file mode 100644 index 000000000..f8bf2ea92 --- /dev/null +++ b/src/common/models/target_test.go @@ -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) + } +} diff --git a/src/common/utils/registry/registry.go b/src/common/utils/registry/registry.go index b84dabb97..bbd351309 100644 --- a/src/common/utils/registry/registry.go +++ b/src/common/utils/registry/registry.go @@ -129,7 +129,7 @@ func (r *Registry) Catalog() ([]string, error) { // Ping ... 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 { return err } diff --git a/src/common/utils/registry/registry_test.go b/src/common/utils/registry/registry_test.go index 4f62ba595..b4204aaa3 100644 --- a/src/common/utils/registry/registry_test.go +++ b/src/common/utils/registry/registry_test.go @@ -28,7 +28,7 @@ import ( func TestPing(t *testing.T) { server := test.NewServer( &test.RequestHandlerMapping{ - Method: "GET", + Method: http.MethodHead, Pattern: "/v2/", Handler: test.Handler(nil), }) diff --git a/src/common/utils/uaa/client.go b/src/common/utils/uaa/client.go index 8aab55027..07da2e792 100644 --- a/src/common/utils/uaa/client.go +++ b/src/common/utils/uaa/client.go @@ -22,6 +22,7 @@ import ( "fmt" "io/ioutil" "net/http" + "os" "strings" "github.com/vmware/harbor/src/common/utils/log" @@ -179,16 +180,20 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) { InsecureSkipVerify: cfg.SkipTLSVerify, } if !cfg.SkipTLSVerify && len(cfg.CARootPath) > 0 { - content, err := ioutil.ReadFile(cfg.CARootPath) - 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. - if ok := pool.AppendCertsFromPEM(content); !ok { - log.Warningf("Failed to append certificate to cert pool, cert path: %s", cfg.CARootPath) + if _, err := os.Stat(cfg.CARootPath); !os.IsNotExist(err) { + content, err := ioutil.ReadFile(cfg.CARootPath) + 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. + 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 { - 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{ diff --git a/src/common/utils/uaa/client_test.go b/src/common/utils/uaa/client_test.go index 774392106..f56440a56 100644 --- a/src/common/utils/uaa/client_test.go +++ b/src/common/utils/uaa/client_test.go @@ -98,7 +98,7 @@ func TestNewClientWithCACert(t *testing.T) { CARootPath: "/notexist", } _, err := NewDefaultClient(cfg) - assert.NotNil(err) + assert.Nil(err) //Skip if it's malformed. cfg.CARootPath = path.Join(currPath(), "test", "non-ca.pem") _, err = NewDefaultClient(cfg) diff --git a/src/common/utils/utils.go b/src/common/utils/utils.go index 01be8ac45..0cc64b402 100644 --- a/src/common/utils/utils.go +++ b/src/common/utils/utils.go @@ -29,27 +29,24 @@ import ( "github.com/vmware/harbor/src/common/utils/log" ) -// FormatEndpoint formats endpoint -func FormatEndpoint(endpoint string) string { - endpoint = strings.TrimSpace(endpoint) +// ParseEndpoint parses endpoint to a URL +func ParseEndpoint(endpoint string) (*url.URL, error) { + endpoint = strings.Trim(endpoint, " ") endpoint = strings.TrimRight(endpoint, "/") - if !strings.HasPrefix(endpoint, "http://") && - !strings.HasPrefix(endpoint, "https://") { + if len(endpoint) == 0 { + 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 } - return 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 + return url.ParseRequestURI(endpoint) } // ParseRepository splits a repository into two parts: project and rest diff --git a/src/common/utils/utils_test.go b/src/common/utils/utils_test.go index 24539fc54..7dcabc527 100644 --- a/src/common/utils/utils_test.go +++ b/src/common/utils/utils_test.go @@ -23,37 +23,30 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseEndpoint(t *testing.T) { - endpoint := "example.com" - u, err := ParseEndpoint(endpoint) - if err != nil { - t.Fatalf("failed to parse endpoint %s: %v", endpoint, err) + cases := []struct { + input string + err bool + 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" { - t.Errorf("unexpected endpoint: %s != %s", endpoint, "http://example.com") - } - - endpoint = "https://example.com" - u, err = ParseEndpoint(endpoint) - if err != nil { - t.Fatalf("failed to parse endpoint %s: %v", endpoint, err) - } - - 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") + for _, c := range cases { + u, err := ParseEndpoint(c.input) + if c.err { + require.NotNil(t, err) + continue + } + require.Nil(t, err) + assert.Equal(t, c.expected, u.String()) } } diff --git a/src/ui/api/email.go b/src/ui/api/email.go index 9dd3d67b8..66a666a46 100644 --- a/src/ui/api/email.go +++ b/src/ui/api/email.go @@ -106,7 +106,9 @@ func (e *EmailAPI) Ping() { addr := net.JoinHostPort(host, strconv.Itoa(port)) if err := email.Ping(addr, identity, username, password, pingEmailTimeout, ssl, insecure); err != nil { - log.Debugf("ping %s failed: %v", addr, err) - e.CustomAbort(http.StatusBadRequest, err.Error()) + log.Errorf("failed to ping email server: %v", err) + // 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 } } diff --git a/src/ui/api/email_test.go b/src/ui/api/email_test.go index bf5d26111..154fb69e2 100644 --- a/src/ui/api/email_test.go +++ b/src/ui/api/email_test.go @@ -16,7 +16,6 @@ package api import ( "fmt" - "strings" "testing" "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") - //case 2: bad request + //case 2: empty email host settings := `{ "email_host": "" }` @@ -47,7 +46,7 @@ func TestPingEmail(t *testing.T) { 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 settings = `{ @@ -58,18 +57,13 @@ func TestPingEmail(t *testing.T) { "email_ssl": true }` - code, body, err := apiTest.PingEmail(*admin, []byte(settings)) + code, _, err = apiTest.PingEmail(*admin, []byte(settings)) if err != nil { t.Errorf("failed to test ping email server: %v", err) return } - assert.Equal(400, code, "the status code of ping email server should be 400") - - if !strings.Contains(body, "535") { - t.Errorf("unexpected error: %s does not contains 535", body) - return - } + assert.Equal(400, code) //case 4: ping email server whose settings are read from config code, _, err = apiTest.PingEmail(*admin, nil) @@ -78,5 +72,5 @@ func TestPingEmail(t *testing.T) { return } - assert.Equal(400, code, "the status code of ping email server should be 400") + assert.Equal(400, code) } diff --git a/src/ui/api/ldap.go b/src/ui/api/ldap.go index f3f2005a5..42ffb058c 100644 --- a/src/ui/api/ldap.go +++ b/src/ui/api/ldap.go @@ -29,7 +29,7 @@ type LdapAPI struct { } const ( - pingErrorMessage = "LDAP connection test failed!" + pingErrorMessage = "LDAP connection test failed" loadSystemErrorMessage = "Can't load system configuration!" canNotOpenLdapSession = "Can't open LDAP session!" searchLdapFailMessage = "LDAP search failed!" @@ -72,6 +72,7 @@ func (l *LdapAPI) Ping() { if err != nil { 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) return } diff --git a/src/ui/api/target.go b/src/ui/api/target.go index 61f8939eb..603341bf0 100644 --- a/src/ui/api/target.go +++ b/src/ui/api/target.go @@ -16,15 +16,12 @@ package api import ( "fmt" - "net" "net/http" - "net/url" "strconv" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "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/registry" "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) { registry, err := newRegistryClient(endpoint, insecure, username, password) - if err != nil { - // timeout, dns resolve error, connection refused, etc. - 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 == nil { + err = registry.Ping() } - if err = registry.Ping(); err != nil { - if regErr, ok := err.(*registry_error.HTTPError); ok { - t.CustomAbort(regErr.StatusCode, regErr.Detail) - } - - log.Errorf("failed to ping registry %s: %v", registry.Endpoint.String(), err) - t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + if err != nil { + log.Errorf("failed to ping target: %v", err) + // 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 } } @@ -117,7 +102,14 @@ func (t *TargetAPI) Ping() { } 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 { target.Username = *req.Username @@ -129,11 +121,6 @@ func (t *TargetAPI) Ping() { target.Insecure = *req.Insecure } - if len(target.URL) == 0 { - t.HandleBadRequest("empty endpoint") - return - } - t.ping(target.URL, target.Username, target.Password, target.Insecure) } diff --git a/src/ui/auth/authenticator.go b/src/ui/auth/authenticator.go index 111f001aa..eb9da0d06 100644 --- a/src/ui/auth/authenticator.go +++ b/src/ui/auth/authenticator.go @@ -39,6 +39,34 @@ type AuthenticateHelper interface { OnBoardUser(u *models.User) error // Get user information from account repository 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) @@ -79,6 +107,9 @@ func Login(m models.AuthModel) (*models.User, error) { lock.Lock(m.Principal) time.Sleep(frozenTime) } + + authenticator.PostAuthenticate(user) + return user, err } @@ -112,3 +143,12 @@ func SearchUser(username string) (*models.User, error) { } return helper.SearchUser(username) } + +// PostAuthenticate - +func PostAuthenticate(u *models.User) error { + helper, err := getHelper() + if err != nil { + return err + } + return helper.PostAuthenticate(u) +} diff --git a/src/ui/auth/db/db.go b/src/ui/auth/db/db.go index c75f396bf..8cfee4c94 100644 --- a/src/ui/auth/db/db.go +++ b/src/ui/auth/db/db.go @@ -21,7 +21,9 @@ import ( ) // Auth implements Authenticator interface to authenticate user against DB. -type Auth struct{} +type Auth struct { + auth.DefaultAuthenticateHelper +} // Authenticate calls dao to authenticate user. 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 } -// OnBoardUser - Dummy implementation when auth_mod is db_auth -func (d *Auth) OnBoardUser(user *models.User) error { - //No need to create user in local database - return nil -} - // SearchUser - Check if user exist in local db func (d *Auth) SearchUser(username string) (*models.User, error) { var queryCondition = models.User{ diff --git a/src/ui/auth/ldap/ldap.go b/src/ui/auth/ldap/ldap.go index 885ef8824..8ca332a34 100644 --- a/src/ui/auth/ldap/ldap.go +++ b/src/ui/auth/ldap/ldap.go @@ -16,6 +16,7 @@ package ldap import ( "fmt" + "regexp" "strings" "github.com/vmware/harbor/src/common/dao" @@ -26,7 +27,9 @@ import ( ) // 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, // 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.Username = ldapUsers[0].Username - u.Email = ldapUsers[0].Email + u.Email = strings.TrimSpace(ldapUsers[0].Email) u.Realname = ldapUsers[0].Realname 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) 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 - } // 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 } +//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() { auth.Register("ldap_auth", &Auth{}) } diff --git a/src/ui/auth/ldap/ldap_test.go b/src/ui/auth/ldap/ldap_test.go index 6163b6fe7..909e13d2b 100644 --- a/src/ui/auth/ldap/ldap_test.go +++ b/src/ui/auth/ldap/ldap_test.go @@ -19,6 +19,7 @@ import ( "os" "testing" + "github.com/stretchr/testify/assert" "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" @@ -204,3 +205,59 @@ func TestAuthenticateHelperSearchUser(t *testing.T) { 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)) +} diff --git a/src/ui/auth/uaa/uaa.go b/src/ui/auth/uaa/uaa.go index 22557a9bf..b357e0833 100644 --- a/src/ui/auth/uaa/uaa.go +++ b/src/ui/auth/uaa/uaa.go @@ -16,6 +16,7 @@ package uaa import ( "fmt" + "os" "strings" "sync" @@ -38,6 +39,7 @@ func CreateClient() (uaa.Client, error) { ClientSecret: UAASettings.ClientSecret, Endpoint: UAASettings.Endpoint, SkipTLSVerify: !UAASettings.VerifyCert, + CARootPath: os.Getenv("UAA_CA_ROOT"), } return uaa.NewDefaultClient(cfg) } @@ -46,6 +48,7 @@ func CreateClient() (uaa.Client, error) { type Auth struct { sync.Mutex client uaa.Client + auth.DefaultAuthenticateHelper } //Authenticate ... diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index cfc448ae9..e7c4f3397 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.2", + "version": "0.6.6", "description": "Harbor shared UI components based on Clarity and Angular4", "scripts": { "start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json", diff --git a/src/ui_ng/lib/pkg/package.json b/src/ui_ng/lib/pkg/package.json index 3d406f77b..0853af114 100644 --- a/src/ui_ng/lib/pkg/package.json +++ b/src/ui_ng/lib/pkg/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.2", + "version": "0.6.6", "description": "Harbor shared UI components based on Clarity and Angular4", "author": "VMware", "module": "index.js", diff --git a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts index e882ba5c6..086d008e0 100644 --- a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts +++ b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.html.ts @@ -23,7 +23,6 @@ export const CONFIRMATION_DIALOG_TEMPLATE: string = ` - @@ -31,7 +30,7 @@ export const CONFIRMATION_DIALOG_TEMPLATE: string = ` - + diff --git a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.ts b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.ts index 92d549a7c..9216d6356 100644 --- a/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.ts +++ b/src/ui_ng/lib/src/confirmation-dialog/confirmation-dialog.component.ts @@ -98,8 +98,8 @@ export class ConfirmationDialogComponent { this.close(); } - confirm(): void { - if(!this.message){//Inproper condition + delete(): void { + if (!this.message){//Inproper condition this.close(); return; } @@ -118,4 +118,21 @@ export class ConfirmationDialogComponent { ); this.confirmAction.emit(message); } + + confirm(): void { + if (!this.message){//Inproper condition + this.close(); + return; + } + + let data: any = this.message.data ? this.message.data : {}; + let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY; + let message = new ConfirmationAcknowledgement( + ConfirmationState.CONFIRMED, + data, + target + ); + this.confirmAction.emit(message); + this.close(); + } } diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index dca5fb101..16bac5064 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -214,7 +214,7 @@ export class HarborLibraryModule { config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }, config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService }, config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService }, - //Do initializing + // Do initializing TranslateServiceInitializer, { provide: APP_INITIALIZER, diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts index 6b263e84c..f62870012 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts @@ -1,40 +1,28 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = ` -
+
-
- - - +
+ + + +
{{'REPLICATION.NAME' | translate}} - {{'REPLICATION.PROJECT' | translate}} + {{'REPLICATION.PROJECT' | translate}} {{'REPLICATION.DESCRIPTION' | translate}} - {{'REPLICATION.DESTINATION_NAME' | translate}} - {{'REPLICATION.LAST_START_TIME' | translate}} - {{'REPLICATION.ACTIVATION' | translate}} + {{'REPLICATION.DESTINATION_NAME' | translate}} + {{'REPLICATION.SCHEDULE' | translate}} {{'REPLICATION.PLACEHOLDER' | translate }} - - - - {{p.name}} - - - {{p.name}} - + {{p.name}} + + {{p.projects?.length>0 ? p.projects[0].name : ''}} - {{p.project_name}} {{p.description ? p.description : '-'}} - {{p.target_name}} - - - - {{p.start_time | date: 'short'}} - - - {{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}} - + {{p.targets?.length>0 ? p.targets[0].name : ''}} + {{p.trigger ? p.trigger.kind : ''}} {{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'REPLICATION.OF' | translate}} {{pagination.totalItems }} {{'REPLICATION.ITEMS' | translate}} diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts index b76bc3982..96bc4f98d 100644 --- a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts @@ -67,6 +67,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { @Output() toggleOne = new EventEmitter(); @Output() redirect = new EventEmitter(); @Output() openNewRule = new EventEmitter(); + @Output() replicateManual = new EventEmitter(); projectScope: boolean = false; @@ -95,8 +96,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { setInterval(() => ref.markForCheck(), 500); } - public get creationAvailable(): boolean { - return !this.readonly && this.projectId ? true : false; + public get opereateAvailable(): boolean { + return !this.readonly && !this.projectId ? true : false; } @@ -221,6 +222,10 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { this.editOne.emit(rules); } + replicateRule(rule: ReplicationRule) { + this.replicateManual.emit(rule); + } + toggleRule(rule: ReplicationRule) { let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage( rule.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE', diff --git a/src/ui_ng/lib/src/replication/replication.component.css.ts b/src/ui_ng/lib/src/replication/replication.component.css.ts index 473cfc00e..51f755820 100644 --- a/src/ui_ng/lib/src/replication/replication.component.css.ts +++ b/src/ui_ng/lib/src/replication/replication.component.css.ts @@ -21,4 +21,11 @@ export const REPLICATION_STYLE: string = ` .option-right-down { padding-right: 16px; margin-top: 24px; +} + .rightPos{ + position: absolute; + right: 35px; + margin-top: 5px; + z-index: 100; + height: 32px; }`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/replication.component.html.ts b/src/ui_ng/lib/src/replication/replication.component.html.ts index be054f729..e5f7bb9e1 100644 --- a/src/ui_ng/lib/src/replication/replication.component.html.ts +++ b/src/ui_ng/lib/src/replication/replication.component.html.ts @@ -1,13 +1,8 @@ export const REPLICATION_TEMPLATE: string = ` -
-
-
+
+
+
-
- -
@@ -16,8 +11,9 @@ export const REPLICATION_TEMPLATE: string = `
- +
+

{{'REPLICATION.REPLICATION_JOBS' | translate}}
@@ -72,5 +68,4 @@ export const REPLICATION_TEMPLATE: string = `
-
`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/replication.component.spec.ts b/src/ui_ng/lib/src/replication/replication.component.spec.ts index ca875e0ac..7b720ec15 100644 --- a/src/ui_ng/lib/src/replication/replication.component.spec.ts +++ b/src/ui_ng/lib/src/replication/replication.component.spec.ts @@ -260,19 +260,6 @@ describe('Replication Component (inline template)', ()=>{ }); })); - it('Should filter replication rules by status', async(()=>{ - fixture.detectChanges(); - fixture.whenStable().then(()=>{ - fixture.detectChanges(); - comp.doFilterRuleStatus('1' /*Enabled*/); - fixture.detectChanges(); - let el: HTMLElement = deRules.nativeElement; - fixture.detectChanges(); - expect(el).toBeTruthy(); - expect(el.textContent.trim()).toEqual('sync_02'); - }); - })); - it('Should filter replication jobs by keywords', async(()=>{ fixture.detectChanges(); fixture.whenStable().then(()=>{ diff --git a/src/ui_ng/lib/src/replication/replication.component.ts b/src/ui_ng/lib/src/replication/replication.component.ts index 9e6ce673f..96a52d7e3 100644 --- a/src/ui_ng/lib/src/replication/replication.component.ts +++ b/src/ui_ng/lib/src/replication/replication.component.ts @@ -88,6 +88,8 @@ export class ReplicationComponent implements OnInit, OnDestroy { @Input() readonly: boolean; @Output() redirect = new EventEmitter(); + @Output() openCreateRule = new EventEmitter(); + @Output() openEdit = new EventEmitter(); search: SearchOption = new SearchOption(); @@ -111,8 +113,8 @@ export class ReplicationComponent implements OnInit, OnDestroy { @ViewChild(ListReplicationRuleComponent) listReplicationRule: ListReplicationRuleComponent; - @ViewChild(CreateEditRuleComponent) - createEditPolicyComponent: CreateEditRuleComponent; +/* @ViewChild(CreateEditRuleComponent) + createEditPolicyComponent: CreateEditRuleComponent;*/ @ViewChild("replicationLogViewer") replicationLogViewer: JobLogViewerComponent; @@ -134,9 +136,6 @@ export class ReplicationComponent implements OnInit, OnDestroy { private translateService: TranslateService) { } - public get creationAvailable(): boolean { - return !this.readonly && this.projectId ? true : false; - } public get showPaginationIndex(): boolean { return this.totalCount > 0; @@ -155,7 +154,7 @@ export class ReplicationComponent implements OnInit, OnDestroy { } openModal(): void { - this.createEditPolicyComponent.openCreateEditRule(true); + this.openCreateRule.emit(); } openEditRule(rule: ReplicationRule) { @@ -164,7 +163,7 @@ export class ReplicationComponent implements OnInit, OnDestroy { if (rule.enabled === 1) { editable = false; } - this.createEditPolicyComponent.openCreateEditRule(editable, rule.id); + this.openEdit.emit(rule.id); } } @@ -260,6 +259,14 @@ export class ReplicationComponent implements OnInit, OnDestroy { } } + replicateManualRule(rule: ReplicationRule): void { + toPromise(this.replicationService.replicateRule(rule.id)) + .then(response => { + this.refreshJobs(); + }) + .catch(error => this.errorHandler.error(error)); + } + customRedirect(rule: ReplicationRule) { this.redirect.emit(rule); } @@ -269,14 +276,6 @@ export class ReplicationComponent implements OnInit, OnDestroy { this.listReplicationRule.retrieveRules(ruleName); } - doFilterRuleStatus($event: any) { - if ($event && $event.target && $event.target["value"]) { - let status = $event.target["value"]; - this.currentRuleStatus = this.ruleStatus.find((r: any) => r.key === status); - this.listReplicationRule.filterRuleStatus(this.currentRuleStatus.key); - } - } - doFilterJobStatus($event: any) { if ($event && $event.target && $event.target["value"]) { let status = $event.target["value"]; diff --git a/src/ui_ng/lib/src/repository/repository.component.css.ts b/src/ui_ng/lib/src/repository/repository.component.css.ts index 7e4812f15..0b1f34ccf 100644 --- a/src/ui_ng/lib/src/repository/repository.component.css.ts +++ b/src/ui_ng/lib/src/repository/repository.component.css.ts @@ -26,8 +26,25 @@ export const REPOSITORY_STYLE = `.option-right { font-size: 32px; } -pre { - white-space: pre-wrap; +.no-info-div { + 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 { diff --git a/src/ui_ng/lib/src/repository/repository.component.html.ts b/src/ui_ng/lib/src/repository/repository.component.html.ts index a2d021aab..2bb443602 100644 --- a/src/ui_ng/lib/src/repository/repository.component.html.ts +++ b/src/ui_ng/lib/src/repository/repository.component.html.ts @@ -24,12 +24,18 @@ export const REPOSITORY_TEMPLATE = `
- +
-
-

{{'REPOSITORY.NO_INFO' | translate }}

-
{{ imageInfo }}
- +
+
+

{{'REPOSITORY.NO_INFO' | translate }}

+

+
+
{{ imageInfo }}
+
+
+
+
diff --git a/src/ui_ng/lib/src/service/replication.service.ts b/src/ui_ng/lib/src/service/replication.service.ts index 9d722798c..c0f3bce2a 100644 --- a/src/ui_ng/lib/src/service/replication.service.ts +++ b/src/ui_ng/lib/src/service/replication.service.ts @@ -97,6 +97,9 @@ export abstract class ReplicationService { */ abstract disableReplicationRule(ruleId: number | string): Observable | Promise | any; + + abstract replicateRule(ruleId: number | string): Observable | Promise | any; + /** * Get the jobs for the specified replication rule. * Set query parameters through 'queryParams', support: @@ -137,6 +140,7 @@ export abstract class ReplicationService { export class ReplicationDefaultService extends ReplicationService { _ruleBaseUrl: string; _jobBaseUrl: string; + _replicateUrl: string; constructor( private http: Http, @@ -147,6 +151,7 @@ export class ReplicationDefaultService extends ReplicationService { config.replicationRuleEndpoint : '/api/policies/replication'; this._jobBaseUrl = config.replicationJobEndpoint ? config.replicationJobEndpoint : '/api/jobs/replication'; + this._replicateUrl = '/api/replications'; } //Private methods @@ -216,6 +221,17 @@ export class ReplicationDefaultService extends ReplicationService { .catch(error => Promise.reject(error)); } + public replicateRule(ruleId: number | string): Observable | Promise | any { + if (!ruleId) { + return Promise.reject("Bad argument"); + } + + let url: string = `${this._replicateUrl}`; + return this.http.post(url, {policy_id: ruleId}, HTTP_JSON_OPTIONS).toPromise() + .then(response => response) + .catch(error => Promise.reject(error)); + } + public enableReplicationRule(ruleId: number | string, enablement: number): Observable | Promise | any { if (!ruleId || ruleId <= 0) { return Promise.reject('Bad argument'); diff --git a/src/ui_ng/lib/src/shared/shared.module.ts b/src/ui_ng/lib/src/shared/shared.module.ts index 95a1bd85c..365b14ee9 100644 --- a/src/ui_ng/lib/src/shared/shared.module.ts +++ b/src/ui_ng/lib/src/shared/shared.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { HttpModule, Http } from '@angular/http'; import { ClarityModule } from 'clarity-angular'; 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 { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslatorJsonLoader } from '../i18n/local-json.loader'; @@ -30,9 +30,9 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { } /** - * + * * Module for sharing common modules - * + * * @export * @class SharedModule */ @@ -68,4 +68,4 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { providers: [CookieService] }) -export class SharedModule { } \ No newline at end of file +export class SharedModule { } diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 7b608e743..7ee2c24de 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -1,6 +1,6 @@ { "name": "harbor", - "version": "1.2.0", + "version": "1.3.0", "description": "Harbor UI with Clarity", "angular-cli": {}, "scripts": { @@ -31,7 +31,7 @@ "clarity-icons": "^0.10.17", "clarity-ui": "^0.10.17", "core-js": "^2.4.1", - "harbor-ui": "0.6.5", + "harbor-ui": "0.6.9", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", "ngx-cookie": "^1.0.0", diff --git a/src/ui_ng/rollup-config.js b/src/ui_ng/rollup-config.js index 36cedf305..bb5ba936f 100644 --- a/src/ui_ng/rollup-config.js +++ b/src/ui_ng/rollup-config.js @@ -21,7 +21,7 @@ export default { plugins: [ nodeResolve({jsnext: true, module: true, browser: true}), commonjs({ - include: ['node_modules/**'] + include: ['node_modules/**'], }), uglify() ] diff --git a/src/ui_ng/src/app/account/sign-in/sign-in.component.css b/src/ui_ng/src/app/account/sign-in/sign-in.component.css index f9251f47c..ed6572137 100644 --- a/src/ui_ng/src/app/account/sign-in/sign-in.component.css +++ b/src/ui_ng/src/app/account/sign-in/sign-in.component.css @@ -47,4 +47,5 @@ font-size: 14px !important; position: relative; top: -9px; -} \ No newline at end of file +} +.bg{position: absolute;top: 60px; right: 0px;width: 100%; height: 100%; background-size: cover;} \ No newline at end of file diff --git a/src/ui_ng/src/app/account/sign-in/sign-in.component.html b/src/ui_ng/src/app/account/sign-in/sign-in.component.html index e19d06e01..445233195 100644 --- a/src/ui_ng/src/app/account/sign-in/sign-in.component.html +++ b/src/ui_ng/src/app/account/sign-in/sign-in.component.html @@ -1,4 +1,5 @@ - diff --git a/src/ui_ng/src/app/base/navigator/navigator.component.html b/src/ui_ng/src/app/base/navigator/navigator.component.html index 42731159c..2452c61ec 100644 --- a/src/ui_ng/src/app/base/navigator/navigator.component.html +++ b/src/ui_ng/src/app/base/navigator/navigator.component.html @@ -2,7 +2,7 @@ diff --git a/src/ui_ng/src/app/harbor-routing.module.ts b/src/ui_ng/src/app/harbor-routing.module.ts index fa01bf976..6ff812f29 100644 --- a/src/ui_ng/src/app/harbor-routing.module.ts +++ b/src/ui_ng/src/app/harbor-routing.module.ts @@ -50,6 +50,8 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac import { MemberGuard } from './shared/route/member-guard-activate.service'; import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component'; +import { ReplicationRuleComponent} from "./replication/replication-rule/replication-rule.component"; +import {LeavingNewRuleRouteDeactivate} from "./shared/route/leaving-new-rule-deactivate.service"; const harborRoutes: Routes = [ { path: '', redirectTo: 'harbor', pathMatch: 'full' }, @@ -78,25 +80,30 @@ const harborRoutes: Routes = [ component: UserComponent, canActivate: [SystemAdminGuard] }, + { + path: 'registries', + component: DestinationPageComponent, + canActivate: [SystemAdminGuard] + }, { path: 'replications', - component: ReplicationManagementComponent, + component: TotalReplicationPageComponent, canActivate: [SystemAdminGuard], canActivateChild: [SystemAdminGuard], - children: [ - { - path: 'rules', - component: TotalReplicationPageComponent - }, - { - path: 'endpoints', - component: DestinationPageComponent - }, - { - path: '**', - redirectTo: 'endpoints' - } - ] + }, + { + path: 'replications/:id/rule', + component: ReplicationRuleComponent, + canActivate: [SystemAdminGuard], + canActivateChild: [SystemAdminGuard], + canDeactivate: [LeavingNewRuleRouteDeactivate] + }, + { + path: 'replications/new-rule', + component: ReplicationRuleComponent, + canActivate: [SystemAdminGuard], + canActivateChild: [SystemAdminGuard], + canDeactivate: [LeavingNewRuleRouteDeactivate] }, { path: 'tags/:id/:repo', @@ -137,7 +144,6 @@ const harborRoutes: Routes = [ { path: 'replications', component: ReplicationPageComponent, - canActivate: [SystemAdminGuard] }, { path: 'members', @@ -158,6 +164,12 @@ const harborRoutes: Routes = [ component: ConfigurationComponent, canActivate: [SystemAdminGuard], canDeactivate: [LeavingConfigRouteDeactivate] + }, + { + path: 'registry', + component: DestinationPageComponent, + canActivate: [SystemAdminGuard], + canActivateChild: [SystemAdminGuard], } ] }, diff --git a/src/ui_ng/src/app/project/project-detail/project-detail.component.html b/src/ui_ng/src/app/project/project-detail/project-detail.component.html index 3feaa5d1c..9f6cf0939 100644 --- a/src/ui_ng/src/app/project/project-detail/project-detail.component.html +++ b/src/ui_ng/src/app/project/project-detail/project-detail.component.html @@ -13,7 +13,7 @@ -