diff --git a/Deploy/db/registry.sql b/Deploy/db/registry.sql index 2d32b4cbb..66bb219e6 100644 --- a/Deploy/db/registry.sql +++ b/Deploy/db/registry.sql @@ -93,8 +93,8 @@ create table access_log ( log_id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, project_id int NOT NULL, - repo_name varchar (40), - repo_tag varchar (20), + repo_name varchar (256), + repo_tag varchar (128), GUID varchar(64), operation varchar(20) NOT NULL, op_time timestamp, @@ -159,4 +159,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` ( `version_num` varchar(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -insert into alembic_version values ('0.1.1'); +insert into alembic_version values ('0.2.0'); diff --git a/Deploy/harbor.cfg b/Deploy/harbor.cfg index 7cd7548eb..e1acfa6aa 100644 --- a/Deploy/harbor.cfg +++ b/Deploy/harbor.cfg @@ -38,6 +38,10 @@ self_registration = on #Number of job workers in job service, default is 3 max_job_workers = 3 +#Toggle on and off to tell job service wheter or not verify the ssl cert +#when it tries to access a remote registry +verify_remote_cert = on + #Turn on or off the customize your certificate for registry's token. #If the value is on, the prepare script will generate new root cert and private key #for generating token to access the image in registry. diff --git a/Deploy/prepare b/Deploy/prepare index d0afc365c..d861f58e3 100755 --- a/Deploy/prepare +++ b/Deploy/prepare @@ -47,6 +47,7 @@ crt_organizationalunit = rcp.get("configuration", "crt_organizationalunit") crt_commonname = rcp.get("configuration", "crt_commonname") crt_email = rcp.get("configuration", "crt_email") max_job_workers = rcp.get("configuration", "max_job_workers") +verify_remote_cert = rcp.get("configuration", "verify_remote_cert") ######## ui_secret = ''.join(random.choice(string.ascii_letters+string.digits) for i in range(16)) @@ -122,7 +123,8 @@ render(os.path.join(templates_dir, "jobservice", "env"), db_password=db_password, ui_secret=ui_secret, max_job_workers=max_job_workers, - ui_url=ui_url) + ui_url=ui_url, + verify_remote_cert=verify_remote_cert) def validate_crt_subj(dirty_subj): subj_list = [item for item in dirty_subj.strip().split("/") \ diff --git a/Deploy/templates/jobservice/env b/Deploy/templates/jobservice/env index 079cc3670..beb75a11d 100644 --- a/Deploy/templates/jobservice/env +++ b/Deploy/templates/jobservice/env @@ -3,7 +3,10 @@ MYSQL_PORT=3306 MYSQL_USR=root MYSQL_PWD=$db_password UI_SECRET=$ui_secret -HARBOR_URL=$ui_url +REGISTRY_URL=http://registry:5000 +VERIFY_REMOTE_CERT=$verify_remote_cert MAX_JOB_WORKERS=$max_job_workers LOG_LEVEL=debug GODEBUG=netdns=cgo +EXT_ENDPOINT=$ui_url +TOKEN_URL=http://ui diff --git a/Deploy/templates/ui/env b/Deploy/templates/ui/env index de50c4a76..5098fa1a7 100644 --- a/Deploy/templates/ui/env +++ b/Deploy/templates/ui/env @@ -3,10 +3,11 @@ MYSQL_PORT=3306 MYSQL_USR=root MYSQL_PWD=$db_password REGISTRY_URL=http://registry:5000 +UI_URL=http://ui CONFIG_PATH=/etc/ui/app.conf HARBOR_REG_URL=$hostname HARBOR_ADMIN_PASSWORD=$harbor_admin_password -HARBOR_URL=$hostname +HARBOR_URL=$ui_url AUTH_MODE=$auth_mode LDAP_URL=$ldap_url LDAP_BASE_DN=$ldap_basedn @@ -14,3 +15,5 @@ UI_SECRET=$ui_secret SELF_REGISTRATION=$self_registration LOG_LEVEL=debug GODEBUG=netdns=cgo +EXT_ENDPOINT=$ui_url +TOKEN_URL=http://ui diff --git a/api/jobs/replication.go b/api/jobs/replication.go index d62d4e4fe..0dee3765b 100644 --- a/api/jobs/replication.go +++ b/api/jobs/replication.go @@ -1,16 +1,16 @@ /* - Copyright (c) 2016 VMware, Inc. All Rights Reserved. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ package api @@ -165,7 +165,7 @@ func getRepoList(projectID int64) ([]string, error) { uiPwd = "Harbor12345" } */ - uiURL := config.LocalHarborURL() + uiURL := config.LocalUIURL() client := &http.Client{} req, err := http.NewRequest("GET", uiURL+"/api/repositories?project_id="+strconv.Itoa(int(projectID)), nil) if err != nil { diff --git a/api/project.go b/api/project.go index 3d57abb02..da26b8a0a 100644 --- a/api/project.go +++ b/api/project.go @@ -159,7 +159,7 @@ func (p *ProjectAPI) List() { if len(isPublic) > 0 { public, err = strconv.Atoi(isPublic) if err != nil { - log.Errorf("Error parsing public property: %d, error: %v", isPublic, err) + log.Errorf("Error parsing public property: %v, error: %v", isPublic, err) p.CustomAbort(http.StatusBadRequest, "invalid project Id") } } diff --git a/api/replication_policy.go b/api/replication_policy.go index 4f851a343..be24aa0e9 100644 --- a/api/replication_policy.go +++ b/api/replication_policy.go @@ -1,16 +1,16 @@ /* - Copyright (c) 2016 VMware, Inc. All Rights Reserved. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ package api @@ -121,6 +121,16 @@ func (pa *RepPolicyAPI) Post() { pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID)) } + policies, err := dao.GetRepPolicyByProjectAndTarget(policy.ProjectID, policy.TargetID) + if err != nil { + log.Errorf("failed to get policy [project ID: %d,targetID: %d]: %v", policy.ProjectID, policy.TargetID, err) + pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if len(policies) > 0 { + pa.CustomAbort(http.StatusConflict, "policy already exists with the same project and target") + } + pid, err := dao.AddRepPolicy(*policy) if err != nil { log.Errorf("Failed to add policy to DB, error: %v", err) @@ -159,6 +169,7 @@ func (pa *RepPolicyAPI) Put() { policy.ProjectID = originalPolicy.ProjectID pa.Validate(policy) + // check duplicate name if policy.Name != originalPolicy.Name { po, err := dao.GetRepPolicyByName(policy.Name) if err != nil { @@ -172,6 +183,12 @@ func (pa *RepPolicyAPI) Put() { } if policy.TargetID != originalPolicy.TargetID { + //target of policy can not be modified when the policy is enabled + if originalPolicy.Enabled == 1 { + pa.CustomAbort(http.StatusBadRequest, "target of policy can not be modified when the policy is enabled") + } + + // check the existance of target target, err := dao.GetRepTarget(policy.TargetID) if err != nil { log.Errorf("failed to get target %d: %v", policy.TargetID, err) @@ -181,67 +198,95 @@ func (pa *RepPolicyAPI) Put() { if target == nil { pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID)) } + + // check duplicate policy with the same project and target + policies, err := dao.GetRepPolicyByProjectAndTarget(policy.ProjectID, policy.TargetID) + if err != nil { + log.Errorf("failed to get policy [project ID: %d,targetID: %d]: %v", policy.ProjectID, policy.TargetID, err) + pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if len(policies) > 0 { + pa.CustomAbort(http.StatusConflict, "policy already exists with the same project and target") + } } policy.ID = id - isTargetChanged := !(policy.TargetID == originalPolicy.TargetID) - isEnablementChanged := !(policy.Enabled == policy.Enabled) + /* + isTargetChanged := !(policy.TargetID == originalPolicy.TargetID) + isEnablementChanged := !(policy.Enabled == policy.Enabled) - var shouldStop, shouldTrigger bool + var shouldStop, shouldTrigger bool - // if target and enablement are not changed, do nothing - if !isTargetChanged && !isEnablementChanged { - shouldStop = false - shouldTrigger = false - } else if !isTargetChanged && isEnablementChanged { - // target is not changed, but enablement is changed - if policy.Enabled == 0 { - shouldStop = true - shouldTrigger = false - } else { - shouldStop = false - shouldTrigger = true - } - } else if isTargetChanged && !isEnablementChanged { - // target is changed, but enablement is not changed - if policy.Enabled == 0 { - // enablement is 0, do nothing + // if target and enablement are not changed, do nothing + if !isTargetChanged && !isEnablementChanged { shouldStop = false shouldTrigger = false + } else if !isTargetChanged && isEnablementChanged { + // target is not changed, but enablement is changed + if policy.Enabled == 0 { + shouldStop = true + shouldTrigger = false + } else { + shouldStop = false + shouldTrigger = true + } + } else if isTargetChanged && !isEnablementChanged { + // target is changed, but enablement is not changed + if policy.Enabled == 0 { + // enablement is 0, do nothing + shouldStop = false + shouldTrigger = false + } else { + // enablement is 1, so stop original target's jobs + // and trigger new target's jobs + shouldStop = true + shouldTrigger = true + } } else { - // enablement is 1, so stop original target's jobs - // and trigger new target's jobs - shouldStop = true - shouldTrigger = true - } - } else { - // both target and enablement are changed + // both target and enablement are changed - // enablement: 1 -> 0 - if policy.Enabled == 0 { - shouldStop = true - shouldTrigger = false - } else { - shouldStop = false - shouldTrigger = true + // enablement: 1 -> 0 + if policy.Enabled == 0 { + shouldStop = true + shouldTrigger = false + } else { + shouldStop = false + shouldTrigger = true + } } - } - if shouldStop { - if err := postReplicationAction(id, "stop"); err != nil { - log.Errorf("failed to stop replication of %d: %v", id, err) + if shouldStop { + if err := postReplicationAction(id, "stop"); err != nil { + log.Errorf("failed to stop replication of %d: %v", id, err) + pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + log.Infof("replication of %d has been stopped", id) + } + + if err = dao.UpdateRepPolicy(policy); err != nil { + log.Errorf("failed to update policy %d: %v", id, err) pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } - log.Infof("replication of %d has been stopped", id) - } + + if shouldTrigger { + go func() { + if err := TriggerReplication(id, "", nil, models.RepOpTransfer); err != nil { + log.Errorf("failed to trigger replication of %d: %v", id, err) + } else { + log.Infof("replication of %d triggered", id) + } + }() + } + */ if err = dao.UpdateRepPolicy(policy); err != nil { log.Errorf("failed to update policy %d: %v", id, err) pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } - if shouldTrigger { + if policy.Enabled != originalPolicy.Enabled && policy.Enabled == 1 { go func() { if err := TriggerReplication(id, "", nil, models.RepOpTransfer); err != nil { log.Errorf("failed to trigger replication of %d: %v", id, err) diff --git a/api/repository.go b/api/repository.go index 1296a3e5b..b4b36c090 100644 --- a/api/repository.go +++ b/api/repository.go @@ -255,11 +255,13 @@ func (ra *RepositoryAPI) GetManifests() { func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) { endpoint := os.Getenv("REGISTRY_URL") + // TODO read variable from config file + insecure := true username, password, ok := ra.Ctx.Request.BasicAuth() if ok { - credential := auth.NewBasicAuthCredential(username, password) - return registry.NewRepositoryWithCredential(repoName, endpoint, credential) + return newRepositoryClient(endpoint, insecure, username, password, + repoName, "repository", repoName, "pull", "push", "*") } username, err = ra.getUsername() @@ -267,7 +269,8 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo return nil, err } - return registry.NewRepositoryWithUsername(repoName, endpoint, username) + return cache.NewRepositoryClient(endpoint, insecure, username, repoName, + "repository", repoName, "pull", "push", "*") } func (ra *RepositoryAPI) getUsername() (string, error) { @@ -327,3 +330,21 @@ func (ra *RepositoryAPI) GetTopRepos() { ra.Data["json"] = repos ra.ServeJSON() } + +func newRepositoryClient(endpoint string, insecure bool, username, password, repository, scopeType, scopeName string, + scopeActions ...string) (*registry.Repository, error) { + + credential := auth.NewBasicAuthCredential(username, password) + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/api/target.go b/api/target.go index 089771c2b..2271b142f 100644 --- a/api/target.go +++ b/api/target.go @@ -26,7 +26,7 @@ import ( "github.com/vmware/harbor/models" "github.com/vmware/harbor/utils" "github.com/vmware/harbor/utils/log" - registry_util "github.com/vmware/harbor/utils/registry" + "github.com/vmware/harbor/utils/registry" "github.com/vmware/harbor/utils/registry/auth" registry_error "github.com/vmware/harbor/utils/registry/error" ) @@ -92,8 +92,10 @@ func (t *TargetAPI) Ping() { password = t.GetString("password") } - credential := auth.NewBasicAuthCredential(username, password) - registry, err := registry_util.NewRegistryWithCredential(endpoint, credential) + // TODO read variable from config file + insecure := true + registry, err := newRegistryClient(endpoint, insecure, username, password, + "", "", "") if err != nil { // timeout, dns resolve error, connection refused, etc. if urlErr, ok := err.(*url.Error); ok { @@ -190,6 +192,16 @@ func (t *TargetAPI) Post() { t.CustomAbort(http.StatusConflict, "name is already used") } + ta, err = dao.GetRepTargetByConnInfo(target.URL, target.Username) + if err != nil { + log.Errorf("failed to get target [ %s %s ]: %v", target.URL, target.Username, err) + t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if ta != nil { + t.CustomAbort(http.StatusConflict, "the connection information[ endpoint, username ] is conflict with other target") + } + if len(target.Password) != 0 { target.Password = utils.ReversibleEncrypt(target.Password) } @@ -217,6 +229,24 @@ func (t *TargetAPI) Put() { t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound)) } + policies, err := dao.GetRepPolicyByTarget(id) + if err != nil { + log.Errorf("failed to get policies according target %d: %v", id, err) + t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + hasEnabledPolicy := false + for _, policy := range policies { + if policy.Enabled == 1 { + hasEnabledPolicy = true + break + } + } + + if hasEnabledPolicy { + t.CustomAbort(http.StatusBadRequest, "the target is associated with policy which is enabled") + } + target := &models.RepTarget{} t.DecodeJSONReqAndValidate(target) @@ -232,6 +262,18 @@ func (t *TargetAPI) Put() { } } + if target.URL != originalTarget.URL || target.Username != originalTarget.Username { + ta, err := dao.GetRepTargetByConnInfo(target.URL, target.Username) + if err != nil { + log.Errorf("failed to get target [ %s %s ]: %v", target.URL, target.Username, err) + t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if ta != nil { + t.CustomAbort(http.StatusConflict, "the connection information[ endpoint, username ] is conflict with other target") + } + } + target.ID = id if len(target.Password) != 0 { @@ -273,3 +315,44 @@ func (t *TargetAPI) Delete() { t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } } + +func newRegistryClient(endpoint string, insecure bool, username, password, scopeType, scopeName string, + scopeActions ...string) (*registry.Registry, error) { + credential := auth.NewBasicAuthCredential(username, password) + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} + +// ListPolicies ... +func (t *TargetAPI) ListPolicies() { + id := t.GetIDFromURL() + + target, err := dao.GetRepTarget(id) + if err != nil { + log.Errorf("failed to get target %d: %v", id, err) + t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + if target == nil { + t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + } + + policies, err := dao.GetRepPolicyByTarget(id) + if err != nil { + log.Errorf("failed to get policies according target %d: %v", id, err) + t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + } + + t.Data["json"] = policies + t.ServeJSON() +} diff --git a/controllers/addnew.go b/controllers/addnew.go new file mode 100644 index 000000000..2bf6f1d57 --- /dev/null +++ b/controllers/addnew.go @@ -0,0 +1,32 @@ +package controllers + +import ( + "net/http" + + "github.com/vmware/harbor/dao" + "github.com/vmware/harbor/utils/log" +) + +// AddNewController handles requests to /add_new +type AddNewController struct { + BaseController +} + +// Get renders the add new page +func (anc *AddNewController) Get() { + sessionUserID := anc.GetSession("userId") + anc.Data["AddNew"] = false + if sessionUserID != nil { + isAdmin, err := dao.IsAdminRole(sessionUserID.(int)) + if err != nil { + log.Errorf("Error occurred in IsAdminRole: %v", err) + anc.CustomAbort(http.StatusInternalServerError, "") + } + if isAdmin && anc.AuthMode == "db_auth" { + anc.Data["AddNew"] = true + anc.Forward("Add User", "sign-up.htm") + return + } + } + anc.CustomAbort(http.StatusUnauthorized, "Status Unauthorized.") +} diff --git a/controllers/optionalmenu.go b/controllers/optionalmenu.go index 21118c7f1..ac4cc3098 100644 --- a/controllers/optionalmenu.go +++ b/controllers/optionalmenu.go @@ -18,6 +18,8 @@ func (omc *OptionalMenuController) Get() { sessionUserID := omc.GetSession("userId") var hasLoggedIn bool + var allowAddNew bool + if sessionUserID != nil { hasLoggedIn = true userID := sessionUserID.(int) @@ -31,7 +33,18 @@ func (omc *OptionalMenuController) Get() { omc.CustomAbort(http.StatusUnauthorized, "") } omc.Data["Username"] = u.Username + + isAdmin, err := dao.IsAdminRole(sessionUserID.(int)) + if err != nil { + log.Errorf("Error occurred in IsAdminRole: %v", err) + omc.CustomAbort(http.StatusInternalServerError, "") + } + + if isAdmin && omc.AuthMode == "db_auth" { + allowAddNew = true + } } + omc.Data["AddNew"] = allowAddNew omc.Data["HasLoggedIn"] = hasLoggedIn omc.TplName = "optional-menu.htm" omc.Render() diff --git a/controllers/signin.go b/controllers/signin.go index 4adfd0942..8f70acf32 100644 --- a/controllers/signin.go +++ b/controllers/signin.go @@ -32,6 +32,7 @@ func (sic *SignInController) Get() { } username = u.Username } + sic.Data["AuthMode"] = sic.AuthMode sic.Data["Username"] = username sic.Data["HasLoggedIn"] = hasLoggedIn sic.TplName = "sign-in.htm" diff --git a/controllers/signup.go b/controllers/signup.go index 97214aab1..4f2cd2b4f 100644 --- a/controllers/signup.go +++ b/controllers/signup.go @@ -1,5 +1,9 @@ package controllers +import ( + "net/http" +) + // SignUpController handles requests to /sign_up type SignUpController struct { BaseController @@ -7,5 +11,9 @@ type SignUpController struct { // Get renders sign up page func (suc *SignUpController) Get() { + if suc.AuthMode != "db_auth" { + suc.CustomAbort(http.StatusUnauthorized, "Status unauthorized.") + } + suc.Data["AddNew"] = false suc.Forward("Sign Up", "sign-up.htm") } diff --git a/dao/dao_test.go b/dao/dao_test.go index 1adbce2c0..7742a3dda 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -926,6 +926,21 @@ func TestGetRepPolicyByTarget(t *testing.T) { } } +func TestGetRepPolicyByProjectAndTarget(t *testing.T) { + policies, err := GetRepPolicyByProjectAndTarget(1, targetID) + if err != nil { + t.Fatalf("failed to get policy according project %d and target %d: %v", 1, targetID, err) + } + + if len(policies) == 0 { + t.Fatal("unexpected length of policies 0, expected is >0") + } + + if policies[0].ID != policyID { + t.Fatalf("unexpected policy: %d, expected: %d", policies[0].ID, policyID) + } +} + func TestGetRepPolicyByName(t *testing.T) { policy, err := GetRepPolicy(policyID) if err != nil { diff --git a/dao/replication_job.go b/dao/replication_job.go index f1f533b4d..98842db38 100644 --- a/dao/replication_job.go +++ b/dao/replication_job.go @@ -52,6 +52,20 @@ func GetRepTargetByName(name string) (*models.RepTarget, error) { return &t, err } +// GetRepTargetByConnInfo ... +func GetRepTargetByConnInfo(endpoint, username string) (*models.RepTarget, error) { + o := GetOrmer() + t := models.RepTarget{ + URL: endpoint, + Username: username, + } + err := o.Read(&t, "URL", "Username") + if err == orm.ErrNoRows { + return nil, nil + } + return &t, err +} + // DeleteRepTarget ... func DeleteRepTarget(id int64) error { o := GetOrmer() @@ -206,6 +220,20 @@ func GetRepPolicyByTarget(targetID int64) ([]*models.RepPolicy, error) { return policies, nil } +// GetRepPolicyByProjectAndTarget ... +func GetRepPolicyByProjectAndTarget(projectID, targetID int64) ([]*models.RepPolicy, error) { + o := GetOrmer() + sql := `select * from replication_policy where project_id = ? and target_id = ?` + + var policies []*models.RepPolicy + + if _, err := o.Raw(sql, projectID, targetID).QueryRows(&policies); err != nil { + return nil, err + } + + return policies, nil +} + // UpdateRepPolicy ... func UpdateRepPolicy(policy *models.RepPolicy) error { o := GetOrmer() diff --git a/job/config/config.go b/job/config/config.go index fb5d86fbd..d97292979 100644 --- a/job/config/config.go +++ b/job/config/config.go @@ -1,16 +1,16 @@ /* - Copyright (c) 2016 VMware, Inc. All Rights Reserved. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ package config @@ -26,9 +26,11 @@ import ( const defaultMaxWorkers int = 10 var maxJobWorkers int -var localURL string +var localUIURL string +var localRegURL string var logDir string var uiSecret string +var verifyRemoteCert string func init() { maxWorkersEnv := os.Getenv("MAX_JOB_WORKERS") @@ -39,9 +41,14 @@ func init() { maxJobWorkers = defaultMaxWorkers } - localURL = os.Getenv("HARBOR_URL") - if len(localURL) == 0 { - localURL = "http://registry:5000/" + localRegURL = os.Getenv("REGISTRY_URL") + if len(localRegURL) == 0 { + localRegURL = "http://registry:5000" + } + + localUIURL = os.Getenv("UI_URL") + if len(localUIURL) == 0 { + localUIURL = "http://ui" } logDir = os.Getenv("LOG_DIR") @@ -67,8 +74,15 @@ func init() { panic("UI Secret is not set") } + verifyRemoteCert = os.Getenv("VERIFY_REMOTE_CERT") + if len(verifyRemoteCert) == 0 { + verifyRemoteCert = "on" + } + log.Debugf("config: maxJobWorkers: %d", maxJobWorkers) - log.Debugf("config: localHarborURL: %s", localURL) + log.Debugf("config: localUIURL: %s", localUIURL) + log.Debugf("config: localRegURL: %s", localRegURL) + log.Debugf("config: verifyRemoteCert: %s", verifyRemoteCert) log.Debugf("config: logDir: %s", logDir) log.Debugf("config: uiSecret: ******") } @@ -78,9 +92,14 @@ func MaxJobWorkers() int { return maxJobWorkers } -// LocalHarborURL returns the local registry url, job service will use this URL to pull manifest and repository. -func LocalHarborURL() string { - return localURL +// LocalUIURL returns the local ui url, job service will use this URL to call API hosted on ui process +func LocalUIURL() string { + return localUIURL +} + +// LocalRegURL returns the local registry url, job service will use this URL to pull image from the registry +func LocalRegURL() string { + return localRegURL } // LogDir returns the absolute path to which the log file will be written @@ -92,3 +111,8 @@ func LogDir() string { func UISecret() string { return uiSecret } + +// VerifyRemoteCert return the flag to tell jobservice whether or not verify the cert of remote registry +func VerifyRemoteCert() bool { + return verifyRemoteCert != "off" +} diff --git a/job/replication/delete.go b/job/replication/delete.go index cc4fd0d5a..8bf7813f8 100644 --- a/job/replication/delete.go +++ b/job/replication/delete.go @@ -16,13 +16,10 @@ package replication import ( - "fmt" - "io/ioutil" - "net/http" - "strings" - "github.com/vmware/harbor/models" "github.com/vmware/harbor/utils/log" + "github.com/vmware/harbor/utils/registry" + "github.com/vmware/harbor/utils/registry/auth" ) const ( @@ -39,22 +36,35 @@ type Deleter struct { dstUsr string // username ... dstPwd string // username ... + insecure bool + + dstClient *registry.Repository + logger *log.Logger } // NewDeleter returns a Deleter -func NewDeleter(repository string, tags []string, dstURL, dstUsr, dstPwd string, logger *log.Logger) *Deleter { +func NewDeleter(repository string, tags []string, dstURL, dstUsr, dstPwd string, insecure bool, logger *log.Logger) (*Deleter, error) { + dstCred := auth.NewBasicAuthCredential(dstUsr, dstPwd) + dstClient, err := newRepositoryClient(dstURL, insecure, dstCred, + repository, "repository", repository, "pull", "push", "*") + if err != nil { + return nil, err + } + deleter := &Deleter{ repository: repository, tags: tags, dstURL: dstURL, dstUsr: dstUsr, dstPwd: dstPwd, + insecure: insecure, + dstClient: dstClient, logger: logger, } deleter.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, destination user: %s", deleter.repository, deleter.tags, deleter.dstURL, deleter.dstUsr) - return deleter + return deleter, nil } // Exit ... @@ -64,25 +74,22 @@ func (d *Deleter) Exit() error { // Enter deletes repository or tags func (d *Deleter) Enter() (string, error) { - url := strings.TrimRight(d.dstURL, "/") + "/api/repositories/" - // delete repository if len(d.tags) == 0 { - u := url + "?repo_name=" + d.repository - if err := del(u, d.dstUsr, d.dstPwd); err != nil { - d.logger.Errorf("an error occurred while deleting repository %s on %s with user %s: %v", d.repository, d.dstURL, d.dstUsr, err) + tags, err := d.dstClient.ListTag() + if err != nil { + d.logger.Errorf("an error occurred while listing tags of repository %s on %s with user %s: %v", d.repository, d.dstURL, d.dstUsr, err) return "", err } - d.logger.Infof("repository %s on %s has been deleted", d.repository, d.dstURL) - - return models.JobFinished, nil + d.tags = append(d.tags, tags...) } - // delele tags + d.logger.Infof("tags %v will be deleted", d.tags) + for _, tag := range d.tags { - u := url + "?repo_name=" + d.repository + "&tag=" + tag - if err := del(u, d.dstUsr, d.dstPwd); err != nil { + + if err := d.dstClient.DeleteTag(tag); err != nil { d.logger.Errorf("an error occurred while deleting repository %s:%s on %s with user %s: %v", d.repository, tag, d.dstURL, d.dstUsr, err) return "", err } @@ -92,28 +99,3 @@ func (d *Deleter) Enter() (string, error) { return models.JobFinished, nil } - -func del(url, username, password string) error { - req, err := http.NewRequest("DELETE", url, nil) - if err != nil { - return err - } - - req.SetBasicAuth(username, password) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - if resp.StatusCode == http.StatusOK { - return nil - } - - defer resp.Body.Close() - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - return fmt.Errorf("%d %s", resp.StatusCode, string(b)) -} diff --git a/job/replication/transfer.go b/job/replication/transfer.go index 5e63f9fcb..0851b7fb9 100644 --- a/job/replication/transfer.go +++ b/job/replication/transfer.go @@ -61,6 +61,8 @@ type BaseHandler struct { dstUsr string // username ... dstPwd string // password ... + insecure bool // whether skip secure check when using https + srcClient *registry.Repository dstClient *registry.Repository @@ -75,7 +77,7 @@ type BaseHandler struct { // InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry, // listing tags of the repository if parameter tags is nil. func InitBaseHandler(repository, srcURL, srcSecret, - dstURL, dstUsr, dstPwd string, tags []string, logger *log.Logger) (*BaseHandler, error) { + dstURL, dstUsr, dstPwd string, insecure bool, tags []string, logger *log.Logger) (*BaseHandler, error) { logger.Infof("initializing: repository: %s, tags: %v, source URL: %s, destination URL: %s, destination user: %s", repository, tags, srcURL, dstURL, dstUsr) @@ -96,14 +98,16 @@ func InitBaseHandler(repository, srcURL, srcSecret, c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret} srcCred := auth.NewCookieCredential(c) // srcCred := auth.NewBasicAuthCredential("admin", "Harbor12345") - srcClient, err := registry.NewRepositoryWithCredential(base.repository, base.srcURL, srcCred) + srcClient, err := newRepositoryClient(base.srcURL, base.insecure, srcCred, + base.repository, "repository", base.repository, "pull", "push", "*") if err != nil { return nil, err } base.srcClient = srcClient dstCred := auth.NewBasicAuthCredential(base.dstUsr, base.dstPwd) - dstClient, err := registry.NewRepositoryWithCredential(base.repository, base.dstURL, dstCred) + dstClient, err := newRepositoryClient(base.dstURL, base.insecure, dstCred, + base.repository, "repository", base.repository, "pull", "push", "*") if err != nil { return nil, err } @@ -416,3 +420,34 @@ func (m *ManifestPusher) Enter() (string, error) { return StatePullManifest, nil } + +func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string, + scopeActions ...string) (*registry.Repository, error) { + + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer) + if err != nil { + return nil, err + } + + uam := &userAgentModifier{ + userAgent: "harbor-registry-client", + } + + client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store, uam) + if err != nil { + return nil, err + } + return client, nil +} + +type userAgentModifier struct { + userAgent string +} + +// Modify adds user-agent header to the request +func (u *userAgentModifier) Modify(req *http.Request) error { + req.Header.Set(http.CanonicalHeaderKey("User-Agent"), u.userAgent) + return nil +} diff --git a/job/statemachine.go b/job/statemachine.go index 94c892a70..959a9f797 100644 --- a/job/statemachine.go +++ b/job/statemachine.go @@ -1,16 +1,16 @@ /* - Copyright (c) 2016 VMware, Inc. All Rights Reserved. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ package job @@ -38,6 +38,7 @@ type RepJobParm struct { Tags []string Enabled int Operation string + Insecure bool } // SM is the state machine to handle job, it handles one job at a time. @@ -205,11 +206,12 @@ func (sm *SM) Reset(jid int64) error { return fmt.Errorf("The policy doesn't exist in DB, policy id:%d", job.PolicyID) } sm.Parms = &RepJobParm{ - LocalRegURL: config.LocalHarborURL(), + LocalRegURL: config.LocalRegURL(), Repository: job.Repository, Tags: job.TagList, Enabled: policy.Enabled, Operation: job.Operation, + Insecure: !config.VerifyRemoteCert(), } if policy.Enabled == 0 { //worker will cancel this job @@ -260,7 +262,7 @@ func (sm *SM) Reset(jid int64) error { func addImgTransferTransition(sm *SM) error { base, err := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(), sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword, - sm.Parms.Tags, sm.Logger) + sm.Parms.Insecure, sm.Parms.Tags, sm.Logger) if err != nil { return err } @@ -274,8 +276,11 @@ func addImgTransferTransition(sm *SM) error { } func addImgDeleteTransition(sm *SM) error { - deleter := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL, - sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Logger) + deleter, err := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL, + sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Parms.Insecure, sm.Logger) + if err != nil { + return err + } sm.AddTransition(models.JobRunning, replication.StateDelete, deleter) sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished}) diff --git a/migration/changelog.md b/migration/changelog.md index ab14c17d5..888aeb5a6 100644 --- a/migration/changelog.md +++ b/migration/changelog.md @@ -17,3 +17,11 @@ Changelog for harbor database schema - delete data `AMDRWS` from table `role` - delete data `A` from table `access` +## 0.2.0 + + - create table `replication_policy` + - create table `replication_target` + - create table `replication_job` + - add column `repo_tag` to table `access_log` + - alter column `repo_name` on table `access_log` + - alter column `email` on table `user` diff --git a/migration/db_meta.py b/migration/db_meta.py index dcbdd4311..fecb2aed3 100644 --- a/migration/db_meta.py +++ b/migration/db_meta.py @@ -85,3 +85,42 @@ class Project(Base): deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) owner = relationship(u'User') + +class ReplicationPolicy(Base): + __tablename__ = "replication_policy" + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(256)) + project_id = sa.Column(sa.Integer, nullable=False) + target_id = sa.Column(sa.Integer, nullable=False) + enabled = sa.Column(mysql.TINYINT(1), nullable=False, server_default=sa.text("'1'")) + description = sa.Column(sa.Text) + cron_str = sa.Column(sa.String(256)) + start_time = sa.Column(mysql.TIMESTAMP) + creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP")) + update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")) + +class ReplicationTarget(Base): + __tablename__ = "replication_target" + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(64)) + url = sa.Column(sa.String(64)) + username = sa.Column(sa.String(40)) + password = sa.Column(sa.String(40)) + target_type = sa.Column(mysql.TINYINT(1), nullable=False, server_default=sa.text("'0'")) + creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP")) + update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")) + +class ReplicationJob(Base): + __tablename__ = "replication_job" + + id = sa.Column(sa.Integer, primary_key=True) + status = sa.Column(sa.String(64), nullable=False) + policy_id = sa.Column(sa.Integer, nullable=False) + repository = sa.Column(sa.String(256), nullable=False) + operation = sa.Column(sa.String(64), nullable=False) + tags = sa.Column(sa.String(16384)) + creation_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP")) + update_time = sa.Column(mysql.TIMESTAMP, server_default = sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")) + diff --git a/migration/migration_harbor/versions/0_2_0.py b/migration/migration_harbor/versions/0_2_0.py new file mode 100644 index 000000000..79ff28be9 --- /dev/null +++ b/migration/migration_harbor/versions/0_2_0.py @@ -0,0 +1,52 @@ +# Copyright (c) 2008-2016 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""0.1.1 to 0.2.0 + +Revision ID: 0.1.1 +Revises: + +""" + +# revision identifiers, used by Alembic. +revision = '0.2.0' +down_revision = '0.1.1' +branch_labels = None +depends_on = None + +from alembic import op +from db_meta import * + +from sqlalchemy.dialects import mysql + +def upgrade(): + """ + update schema&data + """ + bind = op.get_bind() + #alter column user.email, alter column access_log.repo_name, and add column access_log.repo_tag + op.alter_column('user', 'email', type_=sa.String(128), existing_type=sa.String(30)) + op.alter_column('access_log', 'repo_name', type_=sa.String(256), existing_type=sa.String(40)) + op.add_column('access_log', sa.Column('repo_tag', sa.String(128))) + + #create tables: replication_policy, replication_target, replication_job + ReplicationPolicy.__table__.create(bind) + ReplicationTarget.__table__.create(bind) + ReplicationJob.__table__.create(bind) + +def downgrade(): + """ + Downgrade has been disabled. + """ + pass diff --git a/service/cache/cache.go b/service/cache/cache.go index 869afc54a..c7b2c005d 100644 --- a/service/cache/cache.go +++ b/service/cache/cache.go @@ -21,17 +21,16 @@ import ( "github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/registry" + "github.com/vmware/harbor/utils/registry/auth" "github.com/astaxie/beego/cache" ) var ( // Cache is the global cache in system. - Cache cache.Cache - endpoint string - username string - registryClient *registry.Registry - repositoryClients map[string]*registry.Repository + Cache cache.Cache + endpoint string + username string ) const catalogKey string = "catalog" @@ -45,23 +44,18 @@ func init() { endpoint = os.Getenv("REGISTRY_URL") username = "admin" - repositoryClients = make(map[string]*registry.Repository, 10) } // RefreshCatalogCache calls registry's API to get repository list and write it to cache. func RefreshCatalogCache() error { log.Debug("refreshing catalog cache...") - if registryClient == nil { - var err error - registryClient, err = registry.NewRegistryWithUsername(endpoint, username) - if err != nil { - log.Errorf("error occurred while initializing registry client used by cache: %v", err) - return err - } + registryClient, err := NewRegistryClient(endpoint, true, username, + "registry", "catalog", "*") + if err != nil { + return err } - var err error rs, err := registryClient.Catalog() if err != nil { return err @@ -70,15 +64,13 @@ func RefreshCatalogCache() error { repos := []string{} for _, repo := range rs { - rc, ok := repositoryClients[repo] - if !ok { - rc, err = registry.NewRepositoryWithUsername(repo, endpoint, username) - if err != nil { - log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err) - continue - } - repositoryClients[repo] = rc + rc, err := NewRepositoryClient(endpoint, true, username, + repo, "repository", repo, "pull", "push", "*") + if err != nil { + log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err) + continue } + tags, err := rc.ListTag() if err != nil { log.Errorf("error occurred while list tag for %s: %v", repo, err) @@ -112,3 +104,38 @@ func GetRepoFromCache() ([]string, error) { } return result.([]string), nil } + +// NewRegistryClient ... +func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string, + scopeActions ...string) (*registry.Registry, error) { + authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} + +// NewRepositoryClient ... +func NewRepositoryClient(endpoint string, insecure bool, username, repository, scopeType, scopeName string, + scopeActions ...string) (*registry.Repository, error) { + + authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/service/notification.go b/service/notification.go index 20efcb42d..2bd391924 100644 --- a/service/notification.go +++ b/service/notification.go @@ -25,7 +25,6 @@ import ( "github.com/vmware/harbor/models" "github.com/vmware/harbor/service/cache" "github.com/vmware/harbor/utils/log" - "github.com/vmware/harbor/utils/registry" "github.com/astaxie/beego" ) @@ -57,7 +56,7 @@ func (n *NotificationHandler) Post() { matched = false } if matched && (strings.HasPrefix(e.Request.UserAgent, "docker") || - strings.ToLower(strings.TrimSpace(e.Request.UserAgent)) == strings.ToLower(registry.UserAgent)) { + strings.ToLower(strings.TrimSpace(e.Request.UserAgent)) == "harbor-registry-client") { username = e.Actor.Name action = e.Action repo = e.Target.Repository diff --git a/static/resources/js/components/optional-menu/optional-menu.directive.js b/static/resources/js/components/optional-menu/optional-menu.directive.js index c477ee15f..1fd270e04 100644 --- a/static/resources/js/components/optional-menu/optional-menu.directive.js +++ b/static/resources/js/components/optional-menu/optional-menu.directive.js @@ -14,13 +14,15 @@ vm.currentLanguage = I18nService().getCurrentLanguage(); vm.languageName = I18nService().getLanguageName(vm.currentLanguage); + I18nService().setCurrentLanguage(vm.currentLanguage); + console.log('current language:' + vm.languageName); vm.supportLanguages = I18nService().getSupportLanguages(); vm.user = currentUser.get(); vm.setLanguage = setLanguage; vm.logOut = logOut; - + function setLanguage(language) { I18nService().setCurrentLanguage(language); $window.location.href = '/language?lang=' + language; diff --git a/static/resources/js/components/replication/create-policy.directive.html b/static/resources/js/components/replication/create-policy.directive.html index b77da9eab..75fd8f35d 100644 --- a/static/resources/js/components/replication/create-policy.directive.html +++ b/static/resources/js/components/replication/create-policy.directive.html @@ -44,13 +44,13 @@
- +
- +
// 'endpoint_is_required' | tr //
@@ -59,7 +59,7 @@
- +
// 'username_is_required' | tr //
@@ -68,7 +68,7 @@
- +
// 'password_is_required' | tr //
diff --git a/static/resources/js/components/replication/create-policy.directive.js b/static/resources/js/components/replication/create-policy.directive.js index 963a1ef58..037ebeedb 100644 --- a/static/resources/js/components/replication/create-policy.directive.js +++ b/static/resources/js/components/replication/create-policy.directive.js @@ -6,9 +6,9 @@ .module('harbor.replication') .directive('createPolicy', createPolicy); - CreatePolicyController.$inject = ['$scope', 'ListReplicationPolicyService', 'ListDestinationService', 'UpdateDestinationService', 'PingDestinationService', 'CreateReplicationPolicyService', 'UpdateReplicationPolicyService', '$location', 'getParameterByName']; + CreatePolicyController.$inject = ['$scope', 'ListReplicationPolicyService', 'ListDestinationService', 'UpdateDestinationService', 'PingDestinationService', 'CreateReplicationPolicyService', 'UpdateReplicationPolicyService', 'ListDestinationPolicyService','$location', 'getParameterByName', '$filter', 'trFilter']; - function CreatePolicyController($scope, ListReplicationPolicyService, ListDestinationService, UpdateDestinationService, PingDestinationService, CreateReplicationPolicyService, UpdateReplicationPolicyService, $location, getParameterByName) { + function CreatePolicyController($scope, ListReplicationPolicyService, ListDestinationService, UpdateDestinationService, PingDestinationService, CreateReplicationPolicyService, UpdateReplicationPolicyService, ListDestinationPolicyService, $location, getParameterByName, $filter, trFilter) { var vm = this; //Since can not set value for textarea by using vm @@ -33,6 +33,8 @@ vm.create = create; vm.update = update; vm.pingDestination = pingDestination; + + vm.targetEditable = true; $scope.$watch('vm.destinations', function(current) { if(current) { @@ -42,24 +44,7 @@ vm1.password = vm1.selection.password; } }); - - $scope.$watch('vm.action+","+vm.policyId', function(current) { - if(current) { - console.log('Current action for replication policy:' + current); - var parts = current.split(','); - vm.action = parts[0]; - vm.policyId = Number(parts[1]); - switch(parts[0]) { - case 'ADD_NEW': - vm.addNew(); - break; - case 'EDIT': - vm.edit(vm.policyId); - break; - } - } - }); - + function selectDestination(item) { vm1.selection = item; vm1.endpoint = item.endpoint; @@ -74,6 +59,8 @@ } function addNew() { + vm.targetEditable = true; + $filter('tr')('add_new_policy', []); vm0.name = ''; vm0.description = ''; vm0.enabled = true; @@ -81,6 +68,9 @@ function edit(policyId) { console.log('Edit policy ID:' + policyId); + vm.policyId = policyId; + vm.targetEditable = true; + $filter('tr')('edit_policy', []); ListReplicationPolicyService(policyId) .success(listReplicationPolicySuccess) .error(listReplicationPolicyFailed); @@ -129,12 +119,37 @@ function listDestinationFailed(data, status) { console.log('Failed list destination:' + data); } + + function listDestinationPolicySuccess(data, status) { + vm.targetEditable = true; + for(var i in data) { + if(data[i].enabled === 1) { + vm.targetEditable = false; + break; + } + } + console.log('current target editable:' + vm.targetEditable + ', policy ID:' + vm.policyId); + } + + function listDestinationPolicyFailed(data, status) { + console.log('Failed list destination policy:' + data); + } + function listReplicationPolicySuccess(data, status) { + console.log(data); var replicationPolicy = data; vm0.name = replicationPolicy.name; vm0.description = replicationPolicy.description; vm0.enabled = replicationPolicy.enabled == 1; vm.targetId = replicationPolicy.target_id; + + if(vm0.enabled) { + vm.targetEditable = false; + }else{ + ListDestinationPolicyService(vm.targetId) + .success(listDestinationPolicySuccess) + .error(listDestinationPolicyFailed); + } } function listReplicationPolicyFailed(data, status) { console.log('Failed list replication policy:' + data); @@ -145,7 +160,7 @@ } function createReplicationPolicyFailed(data, status) { if(status === 409) { - alert('Policy name already exists.'); + alert($filter('tr')('policy_already_exists', [])); } console.log('Failed create replication policy.'); } @@ -163,10 +178,10 @@ console.log('Failed update destination.'); } function pingDestinationSuccess(data, status) { - alert('Successful ping target.'); + alert($filter('tr')('successful_ping_target', [])); } function pingDestinationFailed(data, status) { - alert('Failed ping target:' + data); + alert($filter('tr')('failed_ping_target', []) + ':' + data); } } @@ -190,11 +205,22 @@ function link(scope, element, attr, ctrl) { element.find('#createPolicyModal').on('show.bs.modal', function() { - ctrl.prepareDestination(); + scope.form.$setPristine(); scope.form.$setUntouched(); + + ctrl.prepareDestination(); + switch(ctrl.action) { + case 'ADD_NEW': + ctrl.addNew(); + break; + case 'EDIT': + ctrl.edit(ctrl.policyId); + break; + } + scope.$apply(); }); - + ctrl.save = save; function save(form) { diff --git a/static/resources/js/components/system-management/create-destination.directive.html b/static/resources/js/components/system-management/create-destination.directive.html index fd2565625..9f1e59140 100644 --- a/static/resources/js/components/system-management/create-destination.directive.html +++ b/static/resources/js/components/system-management/create-destination.directive.html @@ -11,7 +11,7 @@
- +
// 'name_is_required' | tr //
@@ -20,7 +20,7 @@
- +
// 'endpoint_is_required' | tr //
@@ -29,7 +29,7 @@
- +
// 'username_is_required' | tr //
@@ -38,7 +38,7 @@
- +
// 'password_is_required' | tr //
diff --git a/static/resources/js/components/system-management/create-destination.directive.js b/static/resources/js/components/system-management/create-destination.directive.js index 301682b58..ff60d3f09 100644 --- a/static/resources/js/components/system-management/create-destination.directive.js +++ b/static/resources/js/components/system-management/create-destination.directive.js @@ -6,9 +6,9 @@ .module('harbor.system.management') .directive('createDestination', createDestination); - CreateDestinationController.$inject = ['$scope', 'ListDestinationService', 'CreateDestinationService', 'UpdateDestinationService', 'PingDestinationService']; + CreateDestinationController.$inject = ['$scope', 'ListDestinationService', 'CreateDestinationService', 'UpdateDestinationService', 'PingDestinationService', 'ListDestinationPolicyService', '$filter', 'trFilter']; - function CreateDestinationController($scope, ListDestinationService, CreateDestinationService, UpdateDestinationService, PingDestinationService) { + function CreateDestinationController($scope, ListDestinationService, CreateDestinationService, UpdateDestinationService, PingDestinationService, ListDestinationPolicyService, $filter, trFilter) { var vm = this; $scope.destination = {}; @@ -20,25 +20,11 @@ vm.update = update; vm.pingDestination = pingDestination; - $scope.$watch('vm.action+","+vm.targetId', function(current) { - if(current) { - var parts = current.split(','); - vm.action = parts[0]; - vm.targetId = parts[1]; - switch(vm.action) { - case 'ADD_NEW': - vm.modalTitle = 'Create destination'; - vm.addNew(); - break; - case 'EDIT': - vm.modalTitle = 'Edit destination'; - vm.edit(vm.targetId); - break; - } - } - }); - + vm.editable = true; + function addNew() { + vm.editable = true; + vm.modalTitle = $filter('tr')('add_new_destination', []); vm0.name = ''; vm0.endpoint = ''; vm0.username = ''; @@ -46,7 +32,11 @@ } function edit(targetId) { - getDestination(targetId); + vm.editable = true; + vm.modalTitle = $filter('tr')('edit_destination', []); + ListDestinationService(targetId) + .success(getDestinationSuccess) + .error(getDestinationFailed); } function create(destination) { @@ -63,7 +53,7 @@ function createDestinationFailed(data, status) { if(status === 409) { - alert('Destination already exists.'); + alert($filter('tr')('destination_already_exists', [])); } console.log('Failed create destination:' + data); } @@ -83,11 +73,6 @@ console.log('Failed update destination.'); } - function getDestination(targetId) { - ListDestinationService(targetId) - .success(getDestinationSuccess) - .error(getDestinationFailed); - } function getDestinationSuccess(data, status) { var destination = data; @@ -95,12 +80,29 @@ vm0.endpoint = destination.endpoint; vm0.username = destination.username; vm0.password = destination.password; + + ListDestinationPolicyService(destination.id) + .success(listDestinationPolicySuccess) + .error(listDestinationPolicyFailed); } function getDestinationFailed(data, status) { console.log('Failed get destination.'); } + function listDestinationPolicySuccess(data, status) { + for(var i in data) { + if(data[i].enabled === 1) { + vm.editable = false; + break; + } + } + } + + function listDestinationPolicyFailed(data, status) { + console.log('Failed list destination policy:' + data); + } + function pingDestination() { var target = { 'name': vm0.name, @@ -113,10 +115,10 @@ .error(pingDestinationFailed); } function pingDestinationSuccess(data, status) { - alert('Successful ping target.'); + alert($filter('tr')('successful_ping_target', [])); } function pingDestinationFailed(data, status) { - alert('Failed ping target:' + data); + alert($filter('tr')('failed_ping_target', []) + ':' + data); } } @@ -139,8 +141,19 @@ function link(scope, element, attrs, ctrl) { element.find('#createDestinationModal').on('show.bs.modal', function() { + scope.form.$setPristine(); scope.form.$setUntouched(); + + switch(ctrl.action) { + case 'ADD_NEW': + ctrl.addNew(); + break; + case 'EDIT': + ctrl.edit(ctrl.targetId); + break; + } + scope.$apply(); }); ctrl.save = save; diff --git a/static/resources/js/components/system-management/destination.directive.js b/static/resources/js/components/system-management/destination.directive.js index 54ecac4b2..a5785f35f 100644 --- a/static/resources/js/components/system-management/destination.directive.js +++ b/static/resources/js/components/system-management/destination.directive.js @@ -6,9 +6,9 @@ .module('harbor.system.management') .directive('destination', destination); - DestinationController.$inject = ['$scope', 'ListDestinationService', 'DeleteDestinationService']; + DestinationController.$inject = ['$scope', 'ListDestinationService', 'DeleteDestinationService', '$filter', 'trFilter']; - function DestinationController($scope, ListDestinationService, DeleteDestinationService) { + function DestinationController($scope, ListDestinationService, DeleteDestinationService, $filter, trFilter) { var vm = this; vm.retrieve = retrieve; @@ -66,6 +66,7 @@ function deleteDestinationFailed(data, status) { console.log('Failed delete destination.'); + alert($filter('tr')('failed_delete_destination', []) + ':' + data); } } diff --git a/static/resources/js/harbor.module.js b/static/resources/js/harbor.module.js index 2ccb547a5..e9120a952 100644 --- a/static/resources/js/harbor.module.js +++ b/static/resources/js/harbor.module.js @@ -7,8 +7,10 @@ 'ngCookies', 'harbor.session', 'harbor.layout.header', + 'harbor.layout.footer', 'harbor.layout.navigation', 'harbor.layout.sign.up', + 'harbor.layout.add.new', 'harbor.layout.account.setting', 'harbor.layout.forgot.password', 'harbor.layout.reset.password', diff --git a/static/resources/js/layout/add-new/add-new.controller.js b/static/resources/js/layout/add-new/add-new.controller.js new file mode 100644 index 000000000..4cc98e738 --- /dev/null +++ b/static/resources/js/layout/add-new/add-new.controller.js @@ -0,0 +1,15 @@ +(function() { + + 'use strict'; + + angular + .module('harbor.layout.add.new') + .controller('AddNewController', AddNewController); + + AddNewController.$inject = []; + + function AddNewController() { + var vm = this; + } + +})(); \ No newline at end of file diff --git a/static/resources/js/layout/add-new/add-new.module.js b/static/resources/js/layout/add-new/add-new.module.js new file mode 100644 index 000000000..a130f6d53 --- /dev/null +++ b/static/resources/js/layout/add-new/add-new.module.js @@ -0,0 +1,8 @@ +(function() { + + 'use strict'; + + angular + .module('harbor.layout.add.new', []); + +})(); \ No newline at end of file diff --git a/static/resources/js/layout/footer/footer.controller.js b/static/resources/js/layout/footer/footer.controller.js new file mode 100644 index 000000000..996216012 --- /dev/null +++ b/static/resources/js/layout/footer/footer.controller.js @@ -0,0 +1,13 @@ +(function() { + + 'use strict'; + + angular + .module('harbor.layout.footer') + .controller('FooterController', FooterController); + + function FooterController() { + var vm = this; + } + +})(); \ No newline at end of file diff --git a/static/resources/js/layout/footer/footer.module.js b/static/resources/js/layout/footer/footer.module.js new file mode 100644 index 000000000..cf1941780 --- /dev/null +++ b/static/resources/js/layout/footer/footer.module.js @@ -0,0 +1,8 @@ +(function() { + + 'use strict'; + + angular + .module('harbor.layout.footer', []); + +})(); \ No newline at end of file diff --git a/static/resources/js/layout/sign-up/sign-up.controller.js b/static/resources/js/layout/sign-up/sign-up.controller.js index e708d170b..b986892bf 100644 --- a/static/resources/js/layout/sign-up/sign-up.controller.js +++ b/static/resources/js/layout/sign-up/sign-up.controller.js @@ -39,7 +39,12 @@ } function confirm() { - $window.location.href = '/'; + if(location.pathname === '/add_new') { + $window.location.href = '/dashboard'; + }else{ + $window.location.href = '/'; + } + } } diff --git a/static/resources/js/services/destination/services.list-destination-policy.js b/static/resources/js/services/destination/services.list-destination-policy.js new file mode 100644 index 000000000..74f289029 --- /dev/null +++ b/static/resources/js/services/destination/services.list-destination-policy.js @@ -0,0 +1,19 @@ +(function() { + + 'use strict'; + + angular + .module('harbor.services.destination') + .factory('ListDestinationPolicyService', ListDestinationPolicyService); + + ListDestinationPolicyService.$inject = ['$http']; + + function ListDestinationPolicyService($http) { + return listDestinationPolicy; + function listDestinationPolicy(targetId) { + return $http + .get('/api/targets/' + targetId + '/policies/'); + } + } + +})(); \ No newline at end of file diff --git a/static/resources/js/services/i18n/locale_messages_en-US.js b/static/resources/js/services/i18n/locale_messages_en-US.js index 2faa498dd..ef33c4923 100644 --- a/static/resources/js/services/i18n/locale_messages_en-US.js +++ b/static/resources/js/services/i18n/locale_messages_en-US.js @@ -160,6 +160,7 @@ var locale_messages = { 'endpoint': 'Endpoint', 'test_connection': 'Test connection', 'add_new_destination': 'New Destination', + 'edit_destination': 'Edit Destination', 'successful_changed_password': 'Password has been changed successfully.', 'change_profile': 'Change Profile', 'successful_changed_profile': 'User profile has been changed successfully.', @@ -172,5 +173,15 @@ var locale_messages = { 'send': 'Send', 'successful_signed_up': 'Signed up successfully.', 'add_new_policy': 'Add New Policy', - 'edit_policy': 'Edit Policy' + 'edit_policy': 'Edit Policy', + 'add_new_title': 'Add User', + 'add_new': 'Add', + 'successful_added': 'Added new user successfully.', + 'copyright': 'Copyright', + 'all_rights_reserved': 'All Rights Reserved.', + 'successful_ping_target': 'Pinged target successfully.', + 'failed_ping_target': 'Pinged target failed:', + 'policy_already_exists': 'Policy alreay exists.', + 'destination_already_exists': 'Destination already exists.', + 'failed_delete_destination': 'Delete destination failed:' }; \ No newline at end of file diff --git a/static/resources/js/services/i18n/locale_messages_zh-CN.js b/static/resources/js/services/i18n/locale_messages_zh-CN.js index 1f0ee77af..5b8288e51 100644 --- a/static/resources/js/services/i18n/locale_messages_zh-CN.js +++ b/static/resources/js/services/i18n/locale_messages_zh-CN.js @@ -159,10 +159,11 @@ var locale_messages = { 'endpoint_is_required': '终端URL为必填项。', 'test_connection': '测试连接', 'add_new_destination': '新建目标', + 'edit_destination': '编辑目标', 'successful_changed_password': '修改密码操作成功。', 'change_profile': '修改个人信息', 'successful_changed_profile': '修改个人信息操作成功。', - 'form_is_invalid': '表单内容无效', + 'form_is_invalid': '表单内容无sign_up效', 'form_is_invalid_message': '表单内容无效,请填写必填字段。', 'administrator': '管理员', 'popular_repositories': '热门镜像仓库', @@ -171,5 +172,15 @@ var locale_messages = { 'send': '发送', 'successful_signed_up': '注册成功。', 'add_new_policy': '新增策略', - 'edit_policy': '修改策略' + 'edit_policy': '修改策略', + 'add_new_title': '新增用户', + 'add_new': '新增', + 'successful_added': '新增用户成功。', + 'copyright': '版权所有', + 'all_rights_reserved': '保留所有权利。', + 'successful_ping_target': 'Ping 目标成功。', + 'failed_ping_target': 'Ping 目标失败:', + 'policy_already_exists': '策略已存在。', + 'destination_already_exists': '目标已存在。', + 'failed_delete_destination': '删除目标失败:' }; \ No newline at end of file diff --git a/static/resources/js/services/i18n/services.i18n.js b/static/resources/js/services/i18n/services.i18n.js index 8a2f64bf2..84a0ba74c 100644 --- a/static/resources/js/services/i18n/services.i18n.js +++ b/static/resources/js/services/i18n/services.i18n.js @@ -9,9 +9,11 @@ I18nService.$inject = ['$cookies', '$window']; function I18nService($cookies, $window) { + var cookieOptions = {'path': '/'}; + var messages = $.extend(true, {}, eval('locale_messages')); - var defaultLanguage = navigator.language || 'en-US'; + var defaultLanguage = 'en-US'; var supportLanguages = { 'en-US': 'English', 'zh-CN': '中文' @@ -24,6 +26,7 @@ } return false; }; + return tr; function tr() { @@ -45,6 +48,7 @@ if(!angular.isDefined(language) || !isSupportLanguage(language)) { language = defaultLanguage; } + $cookies.put('language', language, cookieOptions); return supportLanguages[language]; }, 'getSupportLanguages': function() { diff --git a/ui/router.go b/ui/router.go index a58996dab..4fe8a4184 100644 --- a/ui/router.go +++ b/ui/router.go @@ -35,6 +35,7 @@ func initRouters() { beego.Router("/project", &controllers.ProjectController{}) beego.Router("/repository", &controllers.RepositoryController{}) beego.Router("/sign_up", &controllers.SignUpController{}) + beego.Router("/add_new", &controllers.AddNewController{}) beego.Router("/account_setting", &controllers.AccountSettingController{}) beego.Router("/admin_option", &controllers.AdminOptionController{}) beego.Router("/forgot_password", &controllers.ForgotPasswordController{}) @@ -76,6 +77,7 @@ func initRouters() { beego.Router("/api/targets/", &api.TargetAPI{}, "get:List") beego.Router("/api/targets/", &api.TargetAPI{}, "post:Post") beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{}) + beego.Router("/api/targets/:id([0-9]+)/policies/", &api.TargetAPI{}, "get:ListPolicies") beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping") beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos") diff --git a/utils/registry/auth/authorizer.go b/utils/registry/auth/authorizer.go index 26ea177a5..8dd4ef862 100644 --- a/utils/registry/auth/authorizer.go +++ b/utils/registry/auth/authorizer.go @@ -16,40 +16,63 @@ package auth import ( + "crypto/tls" + "fmt" "net/http" au "github.com/docker/distribution/registry/client/auth" + "github.com/vmware/harbor/utils/registry/utils" ) -// Handler authorizes requests according to the schema -type Handler interface { +// Authorizer authorizes requests according to the schema +type Authorizer interface { // Scheme : basic, bearer Scheme() string - //AuthorizeRequest adds basic auth or token auth to the header of request - AuthorizeRequest(req *http.Request, params map[string]string) error + //Authorize adds basic auth or token auth to the header of request + Authorize(req *http.Request, params map[string]string) error } -// RequestAuthorizer holds a handler list, which will authorize request. -// Implements interface RequestModifier -type RequestAuthorizer struct { - handlers []Handler - challenges []au.Challenge +// AuthorizerStore holds a authorizer list, which will authorize request. +// And it implements interface Modifier +type AuthorizerStore struct { + authorizers []Authorizer + challenges []au.Challenge } -// NewRequestAuthorizer ... -func NewRequestAuthorizer(handlers []Handler, challenges []au.Challenge) *RequestAuthorizer { - return &RequestAuthorizer{ - handlers: handlers, - challenges: challenges, +// NewAuthorizerStore ... +func NewAuthorizerStore(endpoint string, insecure bool, authorizers ...Authorizer) (*AuthorizerStore, error) { + endpoint = utils.FormatEndpoint(endpoint) + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, + }, } + + resp, err := client.Get(buildPingURL(endpoint)) + if err != nil { + return nil, err + } + + challenges := ParseChallengeFromResponse(resp) + return &AuthorizerStore{ + authorizers: authorizers, + challenges: challenges, + }, nil } -// ModifyRequest adds authorization to the request -func (r *RequestAuthorizer) ModifyRequest(req *http.Request) error { - for _, challenge := range r.challenges { - for _, handler := range r.handlers { - if handler.Scheme() == challenge.Scheme { - if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { +func buildPingURL(endpoint string) string { + return fmt.Sprintf("%s/v2/", endpoint) +} + +// Modify adds authorization to the request +func (a *AuthorizerStore) Modify(req *http.Request) error { + for _, challenge := range a.challenges { + for _, authorizer := range a.authorizers { + if authorizer.Scheme() == challenge.Scheme { + if err := authorizer.Authorize(req, challenge.Parameters); err != nil { return err } } diff --git a/utils/registry/auth/challenge.go b/utils/registry/auth/challenge.go index 523fc1341..322ea8238 100644 --- a/utils/registry/auth/challenge.go +++ b/utils/registry/auth/challenge.go @@ -19,14 +19,11 @@ import ( "net/http" au "github.com/docker/distribution/registry/client/auth" - "github.com/vmware/harbor/utils/log" ) // ParseChallengeFromResponse ... func ParseChallengeFromResponse(resp *http.Response) []au.Challenge { challenges := au.ResponseChallenges(resp) - log.Debugf("challenges: %v", challenges) - return challenges } diff --git a/utils/registry/auth/tokenhandler.go b/utils/registry/auth/tokenauthorizer.go similarity index 66% rename from utils/registry/auth/tokenhandler.go rename to utils/registry/auth/tokenauthorizer.go index 8aa5d3cbb..0abb72fb7 100644 --- a/utils/registry/auth/tokenhandler.go +++ b/utils/registry/auth/tokenauthorizer.go @@ -16,11 +16,13 @@ package auth import ( + "crypto/tls" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" + "os" "strings" "sync" "time" @@ -42,8 +44,8 @@ func (s *scope) string() string { type tokenGenerator func(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) -// Implements interface Handler -type tokenHandler struct { +// Implements interface Authorizer +type tokenAuthorizer struct { scope *scope tg tokenGenerator cache string // cached token @@ -53,12 +55,12 @@ type tokenHandler struct { } // Scheme returns the scheme that the handler can handle -func (t *tokenHandler) Scheme() string { +func (t *tokenAuthorizer) Scheme() string { return "bearer" } // AuthorizeRequest will add authorization header which contains a token before the request is sent -func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { +func (t *tokenAuthorizer) Authorize(req *http.Request, params map[string]string) error { var scopes []*scope var token string @@ -100,26 +102,23 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str if !hasFrom { t.updateCachedToken(to, expiresIn, issuedAt) - log.Debug("add token to cache") } } else { token = cachedToken - log.Debug("get token from cache") } req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token)) - log.Debugf("add token to request: %s %s", req.Method, req.URL.String()) return nil } -func (t *tokenHandler) getCachedToken() (string, int, *time.Time) { +func (t *tokenAuthorizer) getCachedToken() (string, int, *time.Time) { t.Lock() defer t.Unlock() return t.cache, t.expiresIn, t.issuedAt } -func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) { +func (t *tokenAuthorizer) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) { t.Lock() defer t.Unlock() t.cache = token @@ -127,38 +126,45 @@ func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt * t.issuedAt = issuedAt } -// Implements interface Handler -type standardTokenHandler struct { - tokenHandler +// Implements interface Authorizer +type standardTokenAuthorizer struct { + tokenAuthorizer client *http.Client credential Credential } -// NewStandardTokenHandler returns a standard token handler. The handler will request a token +// NewStandardTokenAuthorizer returns a standard token authorizer. The authorizer will request a token // from token server and add it to the origin request -// TODO deal with https -func NewStandardTokenHandler(credential Credential, scopeType, scopeName string, scopeActions ...string) Handler { - handler := &standardTokenHandler{ +func NewStandardTokenAuthorizer(credential Credential, insecure bool, scopeType, scopeName string, scopeActions ...string) Authorizer { + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, + } + + authorizer := &standardTokenAuthorizer{ client: &http.Client{ - Transport: http.DefaultTransport, + Transport: t, }, credential: credential, } if len(scopeType) != 0 || len(scopeName) != 0 { - handler.scope = &scope{ + authorizer.scope = &scope{ Type: scopeType, Name: scopeName, Actions: scopeActions, } } - handler.tg = handler.generateToken + authorizer.tg = authorizer.generateToken - return handler + return authorizer } -func (s *standardTokenHandler) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { +func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { + realm = tokenURL(realm) + u, err := url.Parse(realm) if err != nil { return @@ -217,37 +223,50 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str } } - log.Debug("get token from token server") - return } +// when the registry client is used inside Harbor, the token request +// can be posted to token service directly rather than going through nginx. +// this solution can resolve two problems: +// 1. performance issue +// 2. the realm field returned by registry is an IP which can not reachable +// inside Harbor +func tokenURL(realm string) string { + extEndpoint := os.Getenv("EXT_ENDPOINT") + tokenURL := os.Getenv("TOKEN_URL") + if len(extEndpoint) != 0 && len(tokenURL) != 0 && + strings.Contains(realm, extEndpoint) { + realm = strings.TrimRight(tokenURL, "/") + "/service/token" + } + return realm +} + // Implements interface Handler -type usernameTokenHandler struct { - tokenHandler +type usernameTokenAuthorizer struct { + tokenAuthorizer username string } -// NewUsernameTokenHandler returns a handler which will generate a token according to +// NewUsernameTokenAuthorizer returns a authorizer which will generate a token according to // the user's privileges -func NewUsernameTokenHandler(username string, scopeType, scopeName string, scopeActions ...string) Handler { - handler := &usernameTokenHandler{ +func NewUsernameTokenAuthorizer(username string, scopeType, scopeName string, scopeActions ...string) Authorizer { + authorizer := &usernameTokenAuthorizer{ username: username, } - handler.scope = &scope{ + authorizer.scope = &scope{ Type: scopeType, Name: scopeName, Actions: scopeActions, } - handler.tg = handler.generateToken + authorizer.tg = authorizer.generateToken - return handler + return authorizer } -func (u *usernameTokenHandler) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { +func (u *usernameTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { token, expiresIn, issuedAt, err = token_util.GenTokenForUI(u.username, service, scopes) - log.Debug("get token by calling GenTokenForUI directly") return } diff --git a/utils/registry/modifier.go b/utils/registry/modifier.go new file mode 100644 index 000000000..938be4f9e --- /dev/null +++ b/utils/registry/modifier.go @@ -0,0 +1,25 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "net/http" +) + +// Modifier modifies request +type Modifier interface { + Modify(*http.Request) error +} diff --git a/utils/registry/registry.go b/utils/registry/registry.go index feb6c5fca..75ad5bfa3 100644 --- a/utils/registry/registry.go +++ b/utils/registry/registry.go @@ -16,21 +16,15 @@ package registry import ( + "crypto/tls" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" - "strings" - "github.com/vmware/harbor/utils/log" - "github.com/vmware/harbor/utils/registry/auth" registry_error "github.com/vmware/harbor/utils/registry/error" -) - -const ( - // UserAgent is used to decorate the request so it can be identified by webhook. - UserAgent string = "registry-client" + "github.com/vmware/harbor/utils/registry/utils" ) // Registry holds information of a registry entity @@ -41,9 +35,7 @@ type Registry struct { // NewRegistry returns an instance of registry func NewRegistry(endpoint string, client *http.Client) (*Registry, error) { - endpoint = strings.TrimRight(endpoint, "/") - - u, err := url.Parse(endpoint) + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } @@ -53,64 +45,30 @@ func NewRegistry(endpoint string, client *http.Client) (*Registry, error) { client: client, } - log.Debugf("initialized a registry client: %s", endpoint) - return registry, nil } -// NewRegistryWithUsername returns a Registry instance which will authorize the request -// according to the privileges of user -func NewRegistryWithUsername(endpoint, username string) (*Registry, error) { - endpoint = strings.TrimRight(endpoint, "/") - - u, err := url.Parse(endpoint) +// NewRegistryWithModifiers returns an instance of Registry according to the modifiers +func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modifier) (*Registry, error) { + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } - client, err := newClient(endpoint, username, nil, "registry", "catalog", "*") - if err != nil { - return nil, err + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, } - registry := &Registry{ + transport := NewTransport(t, modifiers...) + + return &Registry{ Endpoint: u, - client: client, - } - - log.Debugf("initialized a registry client with username: %s %s", endpoint, username) - - return registry, nil -} - -// NewRegistryWithCredential returns a Registry instance which associate to a crendential. -// And Credential is essentially a decorator for client to docorate the request before sending it to the registry. -func NewRegistryWithCredential(endpoint string, credential auth.Credential) (*Registry, error) { - endpoint = strings.TrimSpace(endpoint) - endpoint = strings.TrimRight(endpoint, "/") - if !strings.HasPrefix(endpoint, "http://") && - !strings.HasPrefix(endpoint, "https://") { - endpoint = "http://" + endpoint - } - - u, err := url.Parse(endpoint) - if err != nil { - return nil, err - } - - client, err := newClient(endpoint, "", credential, "", "", "") - if err != nil { - return nil, err - } - - registry := &Registry{ - Endpoint: u, - client: client, - } - - log.Debugf("initialized a registry client with credential: %s", endpoint) - - return registry, nil + client: &http.Client{ + Transport: transport, + }, + }, nil } // Catalog ... @@ -163,16 +121,6 @@ func (r *Registry) Ping() error { resp, err := r.client.Do(req) if err != nil { - // if urlErr, ok := err.(*url.Error); ok { - // if regErr, ok := urlErr.Err.(*registry_error.Error); ok { - // return ®istry_error.Error{ - // StatusCode: regErr.StatusCode, - // Detail: regErr.Detail, - // } - // } - // return urlErr.Err - // } - return parseError(err) } @@ -196,32 +144,3 @@ func (r *Registry) Ping() error { func buildCatalogURL(endpoint string) string { return fmt.Sprintf("%s/v2/_catalog", endpoint) } - -func newClient(endpoint, username string, credential auth.Credential, - scopeType, scopeName string, scopeActions ...string) (*http.Client, error) { - - endpoint = strings.TrimRight(endpoint, "/") - resp, err := http.Get(buildPingURL(endpoint)) - if err != nil { - return nil, err - } - - var handlers []auth.Handler - var handler auth.Handler - if credential != nil { - handler = auth.NewStandardTokenHandler(credential, scopeType, scopeName, scopeActions...) - } else { - handler = auth.NewUsernameTokenHandler(username, scopeType, scopeName, scopeActions...) - } - - handlers = append(handlers, handler) - - challenges := auth.ParseChallengeFromResponse(resp) - authorizer := auth.NewRequestAuthorizer(handlers, challenges) - headerModifier := NewHeaderModifier(map[string]string{http.CanonicalHeaderKey("User-Agent"): UserAgent}) - - transport := NewTransport(http.DefaultTransport, []RequestModifier{authorizer, headerModifier}) - return &http.Client{ - Transport: transport, - }, nil -} diff --git a/utils/registry/repository.go b/utils/registry/repository.go index c514413d8..c4f48e04a 100644 --- a/utils/registry/repository.go +++ b/utils/registry/repository.go @@ -17,6 +17,7 @@ package registry import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "io" @@ -28,9 +29,9 @@ import ( "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" - "github.com/vmware/harbor/utils/log" - "github.com/vmware/harbor/utils/registry/auth" + registry_error "github.com/vmware/harbor/utils/registry/error" + "github.com/vmware/harbor/utils/registry/utils" ) // Repository holds information of a repository entity @@ -40,14 +41,11 @@ type Repository struct { client *http.Client } -// TODO add agent to header of request, notifications need it - // NewRepository returns an instance of Repository func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) { name = strings.TrimSpace(name) - endpoint = strings.TrimRight(endpoint, "/") - u, err := url.Parse(endpoint) + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } @@ -61,55 +59,30 @@ func NewRepository(name, endpoint string, client *http.Client) (*Repository, err return repository, nil } -// NewRepositoryWithCredential returns a Repository instance which will authorize the request -// according to the credenttial -func NewRepositoryWithCredential(name, endpoint string, credential auth.Credential) (*Repository, error) { +// NewRepositoryWithModifiers returns an instance of Repository according to the modifiers +func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) { name = strings.TrimSpace(name) - endpoint = strings.TrimRight(endpoint, "/") - u, err := url.Parse(endpoint) + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } - client, err := newClient(endpoint, "", credential, "repository", name, "pull", "push") - if err != nil { - return nil, err + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, } - repository := &Repository{ + transport := NewTransport(t, modifiers...) + + return &Repository{ Name: name, Endpoint: u, - client: client, - } - - log.Debugf("initialized a repository client with credential: %s %s", endpoint, name) - - return repository, nil -} - -// NewRepositoryWithUsername returns a Repository instance which will authorize the request -// according to the privileges of user -func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, error) { - name = strings.TrimSpace(name) - endpoint = strings.TrimRight(endpoint, "/") - - u, err := url.Parse(endpoint) - if err != nil { - return nil, err - } - - client, err := newClient(endpoint, username, nil, "repository", name, "pull", "push") - - repository := &Repository{ - Name: name, - Endpoint: u, - client: client, - } - - log.Debugf("initialized a repository client with username: %s %s %s", endpoint, name, username) - - return repository, nil + client: &http.Client{ + Transport: transport, + }, + }, nil } func parseError(err error) error { diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go index 2b75e1540..f1c4c3c2d 100644 --- a/utils/registry/repository_test.go +++ b/utils/registry/repository_test.go @@ -26,7 +26,7 @@ import ( "time" "github.com/vmware/harbor/utils/registry/auth" - "github.com/vmware/harbor/utils/registry/error" + registry_error "github.com/vmware/harbor/utils/registry/error" ) var ( @@ -139,7 +139,8 @@ func serveToken(w http.ResponseWriter, r *http.Request) { } func TestListTag(t *testing.T) { - client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + client, err := newRepositoryClient(registryServer.URL, true, credential, + repo, "repository", repo, "pull", "push", "*") if err != nil { t.Error(err) } @@ -158,13 +159,14 @@ func TestListTag(t *testing.T) { func TestListTagWithInvalidCredential(t *testing.T) { credential := auth.NewBasicAuthCredential(username, "wrong_password") - client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + client, err := newRepositoryClient(registryServer.URL, true, credential, + repo, "repository", repo, "pull", "push", "*") if err != nil { t.Error(err) } if _, err = client.ListTag(); err != nil { - e, ok := err.(*error.Error) + e, ok := err.(*registry_error.Error) if ok && e.StatusCode == http.StatusUnauthorized { return } @@ -173,3 +175,20 @@ func TestListTagWithInvalidCredential(t *testing.T) { return } } + +func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string, + scopeActions ...string) (*Repository, error) { + + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, true, authorizer) + if err != nil { + return nil, err + } + + client, err := NewRepositoryWithModifiers(repository, endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/utils/registry/transport.go b/utils/registry/transport.go index 9c9e6b8e4..c1798450d 100644 --- a/utils/registry/transport.go +++ b/utils/registry/transport.go @@ -21,39 +21,14 @@ import ( "github.com/vmware/harbor/utils/log" ) -// RequestModifier modifies request -type RequestModifier interface { - ModifyRequest(*http.Request) error -} - -// HeaderModifier adds headers to request -type HeaderModifier struct { - headers map[string]string -} - -// NewHeaderModifier ... -func NewHeaderModifier(headers map[string]string) *HeaderModifier { - return &HeaderModifier{ - headers: headers, - } -} - -// ModifyRequest adds headers to the request -func (h *HeaderModifier) ModifyRequest(req *http.Request) error { - for key, value := range h.headers { - req.Header.Add(key, value) - } - return nil -} - // Transport holds information about base transport and modifiers type Transport struct { transport http.RoundTripper - modifiers []RequestModifier + modifiers []Modifier } // NewTransport ... -func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Transport { +func NewTransport(transport http.RoundTripper, modifiers ...Modifier) *Transport { return &Transport{ transport: transport, modifiers: modifiers, @@ -63,7 +38,7 @@ func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Tra // RoundTrip ... func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { for _, modifier := range t.modifiers { - if err := modifier.ModifyRequest(req); err != nil { + if err := modifier.Modify(req); err != nil { return nil, err } } diff --git a/utils/registry/utils/utils.go b/utils/registry/utils/utils.go new file mode 100644 index 000000000..dc62c4c13 --- /dev/null +++ b/utils/registry/utils/utils.go @@ -0,0 +1,44 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package utils + +import ( + "net/url" + "strings" +) + +// FormatEndpoint formats endpoint +func FormatEndpoint(endpoint string) string { + endpoint = strings.TrimSpace(endpoint) + endpoint = strings.TrimRight(endpoint, "/") + if !strings.HasPrefix(endpoint, "http://") && + !strings.HasPrefix(endpoint, "https://") { + 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 +} diff --git a/views/forgot-password.htm b/views/forgot-password.htm index 8af64cab4..baa0918a8 100644 --- a/views/forgot-password.htm +++ b/views/forgot-password.htm @@ -6,7 +6,7 @@

// 'forgot_password' | tr //

-
+
diff --git a/views/optional-menu.htm b/views/optional-menu.htm index f09e21424..757449dcd 100644 --- a/views/optional-menu.htm +++ b/views/optional-menu.htm @@ -4,6 +4,9 @@ {{ .Username }}