From ebd26c0105a8685a4c98fdecf6eefa9c898f3065 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Wed, 16 Jan 2019 16:08:17 +0800 Subject: [PATCH 01/45] Implement current security interfaces using ram Signed-off-by: He Weiwei --- src/common/ram/namespace.go | 57 +++++++++++++ src/common/ram/namespace_test.go | 45 ++++++++++ src/common/ram/parser.go | 50 +++++++++++ src/common/ram/parser_test.go | 39 +++++++++ src/common/ram/project/const.go | 33 ++++++++ src/common/ram/project/util.go | 59 +++++++++++++ src/common/ram/project/visitor.go | 82 ++++++++++++++++++ src/common/ram/project/visitor_role.go | 79 +++++++++++++++++ src/common/ram/project/visitor_role_test.go | 44 ++++++++++ src/common/ram/project/visitor_test.go | 93 +++++++++++++++++++++ src/common/ram/ram.go | 13 +++ src/common/ram/ram_test.go | 35 ++++++++ src/common/security/admiral/context.go | 77 +++++------------ src/common/security/local/context.go | 73 +++++----------- src/common/security/secret/context.go | 9 ++ 15 files changed, 680 insertions(+), 108 deletions(-) create mode 100644 src/common/ram/namespace.go create mode 100644 src/common/ram/namespace_test.go create mode 100644 src/common/ram/parser.go create mode 100644 src/common/ram/parser_test.go create mode 100644 src/common/ram/project/const.go create mode 100644 src/common/ram/project/util.go create mode 100644 src/common/ram/project/visitor.go create mode 100644 src/common/ram/project/visitor_role.go create mode 100644 src/common/ram/project/visitor_role_test.go create mode 100644 src/common/ram/project/visitor_test.go diff --git a/src/common/ram/namespace.go b/src/common/ram/namespace.go new file mode 100644 index 0000000000..daf5e5891d --- /dev/null +++ b/src/common/ram/namespace.go @@ -0,0 +1,57 @@ +// Copyright Project Harbor Authors +// +// 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 ram + +import ( + "fmt" +) + +// Namespace the namespace interface +type Namespace interface { + // Kind returns the kind of namespace + Kind() string + // Resource returns new resource for subresources with the namespace + Resource(subresources ...Resource) Resource + // Identity returns identity attached with namespace + Identity() interface{} + // IsPublic returns true if namespace is public + IsPublic() bool +} + +type projectNamespace struct { + projectIDOrName interface{} + isPublic bool +} + +func (ns *projectNamespace) Kind() string { + return "project" +} + +func (ns *projectNamespace) Resource(subresources ...Resource) Resource { + return Resource(fmt.Sprintf("/project/%v", ns.projectIDOrName)).Subresource(subresources...) +} + +func (ns *projectNamespace) Identity() interface{} { + return ns.projectIDOrName +} + +func (ns *projectNamespace) IsPublic() bool { + return ns.isPublic +} + +// NewProjectNamespace returns namespace for project +func NewProjectNamespace(projectIDOrName interface{}, isPublic bool) Namespace { + return &projectNamespace{projectIDOrName: projectIDOrName, isPublic: isPublic} +} diff --git a/src/common/ram/namespace_test.go b/src/common/ram/namespace_test.go new file mode 100644 index 0000000000..d3b8dff76d --- /dev/null +++ b/src/common/ram/namespace_test.go @@ -0,0 +1,45 @@ +// Copyright Project Harbor Authors +// +// 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 ram + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type ProjectNamespaceTestSuite struct { + suite.Suite +} + +func (suite *ProjectNamespaceTestSuite) TestResource() { + var namespace Namespace + + namespace = &projectNamespace{projectIDOrName: int64(1)} + + suite.Equal(namespace.Resource(Resource("image")), Resource("/project/1/image")) +} + +func (suite *ProjectNamespaceTestSuite) TestIdentity() { + namespace, _ := Resource("/project/1/image").GetNamespace() + suite.Equal(namespace.Identity(), int64(1)) + + namespace, _ = Resource("/project/library/image").GetNamespace() + suite.Equal(namespace.Identity(), "library") +} + +func TestProjectNamespaceTestSuite(t *testing.T) { + suite.Run(t, new(ProjectNamespaceTestSuite)) +} diff --git a/src/common/ram/parser.go b/src/common/ram/parser.go new file mode 100644 index 0000000000..22355b02e3 --- /dev/null +++ b/src/common/ram/parser.go @@ -0,0 +1,50 @@ +// Copyright Project Harbor Authors +// +// 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 ram + +import ( + "errors" + "regexp" + "strconv" +) + +var ( + namespaceParsers = map[string]namespaceParser{ + "project": projectNamespaceParser, + } +) + +type namespaceParser func(resource Resource) (Namespace, error) + +func projectNamespaceParser(resource Resource) (Namespace, error) { + parserRe := regexp.MustCompile("^/project/([^/]*)/?") + + matches := parserRe.FindStringSubmatch(resource.String()) + + if len(matches) <= 1 { + return nil, errors.New("not support resource") + } + + var projectIDOrName interface{} + + id, err := strconv.ParseInt(matches[1], 10, 64) + if err == nil { + projectIDOrName = id + } else { + projectIDOrName = matches[1] + } + + return &projectNamespace{projectIDOrName: projectIDOrName}, nil +} diff --git a/src/common/ram/parser_test.go b/src/common/ram/parser_test.go new file mode 100644 index 0000000000..3869297b74 --- /dev/null +++ b/src/common/ram/parser_test.go @@ -0,0 +1,39 @@ +// Copyright Project Harbor Authors +// +// 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 ram + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type ProjectParserTestSuite struct { + suite.Suite +} + +func (suite *ProjectParserTestSuite) TestParse() { + namespace, err := projectNamespaceParser(Resource("/project/1/image")) + suite.Equal(namespace, &projectNamespace{projectIDOrName: int64(1)}) + suite.Nil(err) + + namespace, err = projectNamespaceParser(Resource("/fake/1/image")) + suite.Nil(namespace) + suite.Error(err) +} + +func TestProjectParserTestSuite(t *testing.T) { + suite.Run(t, new(ProjectParserTestSuite)) +} diff --git a/src/common/ram/project/const.go b/src/common/ram/project/const.go new file mode 100644 index 0000000000..5d54561cf5 --- /dev/null +++ b/src/common/ram/project/const.go @@ -0,0 +1,33 @@ +// Copyright Project Harbor Authors +// +// 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 project + +import ( + "github.com/goharbor/harbor/src/common/ram" +) + +// const action variables +const ( + ActionAll = ram.Action("*") + ActionPull = ram.Action("pull") + ActionPush = ram.Action("push") + ActionPushPull = ram.Action("push+pull") +) + +// const resource variables +const ( + ResourceAll = ram.Resource("*") + ResourceImage = ram.Resource("image") +) diff --git a/src/common/ram/project/util.go b/src/common/ram/project/util.go new file mode 100644 index 0000000000..d073577771 --- /dev/null +++ b/src/common/ram/project/util.go @@ -0,0 +1,59 @@ +// Copyright Project Harbor Authors +// +// 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 project + +import ( + "github.com/goharbor/harbor/src/common/ram" +) + +var ( + // subresource policies for public project + publicProjectPolicies = []*ram.Policy{ + {Resource: ResourceImage, Action: ActionPull}, + } + + // subresource policies for system admin visitor + systemAdminProjectPolicies = []*ram.Policy{ + {Resource: ResourceAll, Action: ActionAll}, + } +) + +func policiesForPublicProject(namespace ram.Namespace) []*ram.Policy { + policies := []*ram.Policy{} + + for _, policy := range publicProjectPolicies { + policies = append(policies, &ram.Policy{ + Resource: namespace.Resource(policy.Resource), + Action: policy.Action, + Effect: policy.Effect, + }) + } + + return policies +} + +func policiesForSystemAdmin(namespace ram.Namespace) []*ram.Policy { + policies := []*ram.Policy{} + + for _, policy := range systemAdminProjectPolicies { + policies = append(policies, &ram.Policy{ + Resource: namespace.Resource(policy.Resource), + Action: policy.Action, + Effect: policy.Effect, + }) + } + + return policies +} diff --git a/src/common/ram/project/visitor.go b/src/common/ram/project/visitor.go new file mode 100644 index 0000000000..8a7e5263dc --- /dev/null +++ b/src/common/ram/project/visitor.go @@ -0,0 +1,82 @@ +// Copyright Project Harbor Authors +// +// 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 project + +import ( + "github.com/goharbor/harbor/src/common/ram" +) + +// visitorContext the context interface for the project visitor +type visitorContext interface { + IsAuthenticated() bool + // GetUsername returns the username of user related to the context + GetUsername() string + // IsSysAdmin returns whether the user is system admin + IsSysAdmin() bool +} + +// visitor implement the ram.User interface for project visitor +type visitor struct { + ctx visitorContext + namespace ram.Namespace + projectRoles []int +} + +// GetUserName returns username of the visitor +func (v *visitor) GetUserName() string { + // anonymous username for unauthenticated Visitor + if !v.ctx.IsAuthenticated() { + return "anonymous" + } + + return v.ctx.GetUsername() +} + +// GetPolicies returns policies of the visitor +func (v *visitor) GetPolicies() []*ram.Policy { + if v.ctx.IsSysAdmin() { + return policiesForSystemAdmin(v.namespace) + } + + if v.namespace.IsPublic() { + return policiesForPublicProject(v.namespace) + } + + return nil +} + +// GetRoles returns roles of the visitor +func (v *visitor) GetRoles() []ram.Role { + if !v.ctx.IsAuthenticated() { + return nil + } + + roles := []ram.Role{} + + for _, roleID := range v.projectRoles { + roles = append(roles, &visitorRole{roleID: roleID, namespace: v.namespace}) + } + + return roles +} + +// NewUser returns ram.User interface for the project visitor +func NewUser(ctx visitorContext, namespace ram.Namespace, projectRoles ...int) ram.User { + return &visitor{ + ctx: ctx, + namespace: namespace, + projectRoles: projectRoles, + } +} diff --git a/src/common/ram/project/visitor_role.go b/src/common/ram/project/visitor_role.go new file mode 100644 index 0000000000..20b18f236d --- /dev/null +++ b/src/common/ram/project/visitor_role.go @@ -0,0 +1,79 @@ +// Copyright Project Harbor Authors +// +// 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 project + +import ( + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/common/ram" +) + +var ( + rolePoliciesMap = map[string][]*ram.Policy{ + "projectAdmin": { + {Resource: ResourceImage, Action: ActionPushPull}, // compatible with security all perm of project + {Resource: ResourceImage, Action: ActionPush}, + {Resource: ResourceImage, Action: ActionPull}, + }, + + "developer": { + {Resource: ResourceImage, Action: ActionPush}, + {Resource: ResourceImage, Action: ActionPull}, + }, + + "guest": { + {Resource: ResourceImage, Action: ActionPull}, + }, + } +) + +// visitorRole implement the ram.Role interface +type visitorRole struct { + namespace ram.Namespace + roleID int +} + +// GetRoleName returns role name for the visitor role +func (role *visitorRole) GetRoleName() string { + switch role.roleID { + case common.RoleProjectAdmin: + return "projectAdmin" + case common.RoleDeveloper: + return "developer" + case common.RoleGuest: + return "guest" + default: + return "" + } +} + +// GetPolicies returns policies for the visitor role +func (role *visitorRole) GetPolicies() []*ram.Policy { + policies := []*ram.Policy{} + + roleName := role.GetRoleName() + if roleName == "" { + return policies + } + + for _, policy := range rolePoliciesMap[roleName] { + policies = append(policies, &ram.Policy{ + Resource: role.namespace.Resource(policy.Resource), + Action: policy.Action, + Effect: policy.Effect, + }) + } + + return policies +} diff --git a/src/common/ram/project/visitor_role_test.go b/src/common/ram/project/visitor_role_test.go new file mode 100644 index 0000000000..b1f22d24ac --- /dev/null +++ b/src/common/ram/project/visitor_role_test.go @@ -0,0 +1,44 @@ +// Copyright Project Harbor Authors +// +// 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 project + +import ( + "testing" + + "github.com/goharbor/harbor/src/common" + "github.com/stretchr/testify/suite" +) + +type VisitorRoleTestSuite struct { + suite.Suite +} + +func (suite *VisitorRoleTestSuite) TestGetRoleName() { + projectAdmin := visitorRole{roleID: common.RoleProjectAdmin} + suite.Equal(projectAdmin.GetRoleName(), "projectAdmin") + + developer := visitorRole{roleID: common.RoleDeveloper} + suite.Equal(developer.GetRoleName(), "developer") + + guest := visitorRole{roleID: common.RoleGuest} + suite.Equal(guest.GetRoleName(), "guest") + + unknow := visitorRole{roleID: 404} + suite.Equal(unknow.GetRoleName(), "") +} + +func TestVisitorRoleTestSuite(t *testing.T) { + suite.Run(t, new(VisitorRoleTestSuite)) +} diff --git a/src/common/ram/project/visitor_test.go b/src/common/ram/project/visitor_test.go new file mode 100644 index 0000000000..c1b3450ccf --- /dev/null +++ b/src/common/ram/project/visitor_test.go @@ -0,0 +1,93 @@ +// Copyright Project Harbor Authors +// +// 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 project + +import ( + "testing" + + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/common/ram" + "github.com/stretchr/testify/suite" +) + +type fakeVisitorContext struct { + username string + isSysAdmin bool +} + +func (ctx *fakeVisitorContext) IsAuthenticated() bool { + return ctx.username != "" +} + +func (ctx *fakeVisitorContext) GetUsername() string { + return ctx.username +} + +func (ctx *fakeVisitorContext) IsSysAdmin() bool { + return ctx.IsAuthenticated() && ctx.isSysAdmin +} + +var ( + anonymousCtx = &fakeVisitorContext{} + authenticatedCtx = &fakeVisitorContext{username: "user"} + sysAdminCtx = &fakeVisitorContext{username: "admin", isSysAdmin: true} +) + +type VisitorTestSuite struct { + suite.Suite +} + +func (suite *VisitorTestSuite) TestGetPolicies() { + namespace := ram.NewProjectNamespace("library", false) + publicNamespace := ram.NewProjectNamespace("library", true) + + anonymous := NewUser(anonymousCtx, namespace) + suite.Nil(anonymous.GetPolicies()) + + anonymousForPublicProject := NewUser(anonymousCtx, publicNamespace) + suite.Equal(anonymousForPublicProject.GetPolicies(), policiesForPublicProject(publicNamespace)) + + authenticated := NewUser(authenticatedCtx, namespace) + suite.Nil(authenticated.GetPolicies()) + + authenticatedForPublicProject := NewUser(authenticatedCtx, publicNamespace) + suite.Equal(authenticatedForPublicProject.GetPolicies(), policiesForPublicProject(publicNamespace)) + + systemAdmin := NewUser(sysAdminCtx, namespace) + suite.Equal(systemAdmin.GetPolicies(), policiesForSystemAdmin(namespace)) + + systemAdminForPublicProject := NewUser(sysAdminCtx, publicNamespace) + suite.Equal(systemAdminForPublicProject.GetPolicies(), policiesForSystemAdmin(publicNamespace)) +} + +func (suite *VisitorTestSuite) TestGetRoles() { + namespace := ram.NewProjectNamespace("library", false) + + anonymous := NewUser(anonymousCtx, namespace) + suite.Nil(anonymous.GetRoles()) + + authenticated := NewUser(authenticatedCtx, namespace) + suite.Empty(authenticated.GetRoles()) + + authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin) + suite.Len(authenticated.GetRoles(), 1) + + authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin, common.RoleDeveloper) + suite.Len(authenticated.GetRoles(), 2) +} + +func TestVisitorTestSuite(t *testing.T) { + suite.Run(t, new(VisitorTestSuite)) +} diff --git a/src/common/ram/ram.go b/src/common/ram/ram.go index c7d8a23035..76a76b2b1c 100644 --- a/src/common/ram/ram.go +++ b/src/common/ram/ram.go @@ -15,6 +15,7 @@ package ram import ( + "fmt" "path" ) @@ -43,6 +44,18 @@ func (res Resource) Subresource(resources ...Resource) Resource { return Resource(path.Join(elements...)) } +// GetNamespace returns namespace from resource +func (res Resource) GetNamespace() (Namespace, error) { + for _, parser := range namespaceParsers { + namespace, err := parser(res) + if err == nil { + return namespace, nil + } + } + + return nil, fmt.Errorf("no namespace found for %s", res) +} + // Action the type of action type Action string diff --git a/src/common/ram/ram_test.go b/src/common/ram/ram_test.go index cd435c369d..84a8acaa51 100644 --- a/src/common/ram/ram_test.go +++ b/src/common/ram/ram_test.go @@ -15,6 +15,7 @@ package ram import ( + "reflect" "testing" ) @@ -355,3 +356,37 @@ func TestResource_Subresource(t *testing.T) { }) } } + +func TestResource_GetNamespace(t *testing.T) { + tests := []struct { + name string + res Resource + want Namespace + wantErr bool + }{ + { + name: "project namespace", + res: Resource("/project/1"), + want: &projectNamespace{int64(1), false}, + wantErr: false, + }, + { + name: "unknow namespace", + res: Resource("/unknow/1"), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.res.GetNamespace() + if (err != nil) != tt.wantErr { + t.Errorf("Resource.GetNamespace() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Resource.GetNamespace() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/common/security/admiral/context.go b/src/common/security/admiral/context.go index 3b3b5476b4..09856d3f57 100644 --- a/src/common/security/admiral/context.go +++ b/src/common/security/admiral/context.go @@ -15,10 +15,10 @@ package admiral import ( - "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/ram/project" "github.com/goharbor/harbor/src/common/security/admiral/authcontext" - "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/promgr" ) @@ -71,70 +71,33 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - public, err := s.pm.IsPublic(projectIDOrName) - if err != nil { - log.Errorf("failed to check the public of project %v: %v", - projectIDOrName, err) - return false - } - if public { - return true - } - - // private project - if !s.IsAuthenticated() { - return false - } - - // system admin - if s.IsSysAdmin() { - return true - } - - roles := s.GetProjectRoles(projectIDOrName) - - return len(roles) > 0 + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - if !s.IsAuthenticated() { - return false - } - - // system admin - if s.IsSysAdmin() { - return true - } - - roles := s.GetProjectRoles(projectIDOrName) - for _, role := range roles { - switch role { - case common.RoleProjectAdmin, - common.RoleDeveloper: - return true - } - } - - return false + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPush, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - if !s.IsAuthenticated() { - return false - } + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPushPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) +} - // system admin - if s.IsSysAdmin() { - return true - } - - roles := s.GetProjectRoles(projectIDOrName) - for _, role := range roles { - switch role { - case common.RoleProjectAdmin: - return true +// Can returns whether the user can do action on resource +func (s *SecurityContext) Can(action ram.Action, resource ram.Resource) bool { + ns, err := resource.GetNamespace() + if err == nil { + switch ns.Kind() { + case "project": + projectIDOrName := ns.Identity() + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + projectNamespace := ram.NewProjectNamespace(projectIDOrName, isPublicProject) + user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...) + return ram.HasPermission(user, resource, action) } } diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index 48af21b302..2cf469237d 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -19,6 +19,8 @@ import ( "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao/group" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/ram/project" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/promgr" ) @@ -67,67 +69,36 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - // public project - public, err := s.pm.IsPublic(projectIDOrName) - if err != nil { - log.Errorf("failed to check the public of project %v: %v", - projectIDOrName, err) - return false - } - if public { - return true - } - - // private project - if !s.IsAuthenticated() { - return false - } - - // system admin - if s.IsSysAdmin() { - return true - } - - roles := s.GetProjectRoles(projectIDOrName) - return len(roles) > 0 + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - if !s.IsAuthenticated() { - return false - } - // system admin - if s.IsSysAdmin() { - return true - } - roles := s.GetProjectRoles(projectIDOrName) - for _, role := range roles { - switch role { - case common.RoleProjectAdmin, - common.RoleDeveloper: - return true - } - } - return false + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPush, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - if !s.IsAuthenticated() { - return false - } - // system admin - if s.IsSysAdmin() { - return true - } - roles := s.GetProjectRoles(projectIDOrName) - for _, role := range roles { - switch role { - case common.RoleProjectAdmin: - return true + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPushPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) +} + +// Can returns whether the user can do action on resource +func (s *SecurityContext) Can(action ram.Action, resource ram.Resource) bool { + ns, err := resource.GetNamespace() + if err == nil { + switch ns.Kind() { + case "project": + projectIDOrName := ns.Identity() + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + projectNamespace := ram.NewProjectNamespace(projectIDOrName, isPublicProject) + user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...) + return ram.HasPermission(user, resource, action) } } + return false } diff --git a/src/common/security/secret/context.go b/src/common/security/secret/context.go index ac4a5b2e5f..62ee716024 100644 --- a/src/common/security/secret/context.go +++ b/src/common/security/secret/context.go @@ -19,6 +19,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/ram" "github.com/goharbor/harbor/src/common/secret" "github.com/goharbor/harbor/src/common/utils/log" ) @@ -97,6 +98,14 @@ func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser } +// Can returns whether the user can do action on resource +func (s *SecurityContext) Can(action ram.Action, resource ram.Resource) bool { + if s.store == nil { + return false + } + return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser +} + // GetMyProjects ... func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) { return nil, fmt.Errorf("GetMyProjects is unsupported") From be4455ec1bf4f48c36ab9b1475dede6770b27354 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 11 Jan 2019 12:53:34 +0800 Subject: [PATCH 02/45] Implement the unified health check API The commit implements an unified health check API for all Harbor services Signed-off-by: Wenkai Yin --- docs/swagger.yaml | 36 ++++ src/common/const.go | 3 + src/core/api/base.go | 1 + src/core/api/harborapi_test.go | 1 + src/core/api/health.go | 323 +++++++++++++++++++++++++++++++++ src/core/api/health_test.go | 134 ++++++++++++++ src/core/config/config.go | 33 ++++ src/core/router.go | 1 + 8 files changed, 532 insertions(+) create mode 100644 src/core/api/health.go create mode 100644 src/core/api/health_test.go diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b33e9ebc17..eef1cce21a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -19,6 +19,18 @@ securityDefinitions: security: - basicAuth: [] paths: + /health: + get: + summary: 'Health check API' + description: | + The endpoint returns the health stauts of the system. + tags: + - Products + responses: + '200': + description: The system health status. + schema: + $ref: '#/definitions/OverallHealthStatus' /search: get: summary: 'Search for projects, repositories and helm charts' @@ -4514,3 +4526,27 @@ definitions: description: A list of label items: $ref: '#/definitions/Label' + OverallHealthStatus: + type: object + description: The system health status + properties: + status: + type: string + description: The overall health status. It is "healthy" only when all the components' status are "healthy" + components: + type: array + items: + $ref: '#/definitions/ComponentHealthStatus' + ComponentHealthStatus: + type: object + description: The health status of component + properties: + name: + type: string + description: The component name + status: + type: string + description: The health status of component + error: + type: string + description: (optional) The error message when the status is "unhealthy" diff --git a/src/common/const.go b/src/common/const.go index 4cb2d1c840..5dc0325e14 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -115,6 +115,9 @@ const ( WithChartMuseum = "with_chartmuseum" ChartRepoURL = "chart_repository_url" DefaultChartRepoURL = "http://chartmuseum:9999" + DefaultPortalURL = "http://portal" + DefaultRegistryCtlURL = "http://registryctl:8080" + DefaultClairHealthCheckServerURL = "http://clair:6061" ) // Shared variable, not allowed to modify diff --git a/src/core/api/base.go b/src/core/api/base.go index 4e8a8ad57e..22bc2c059a 100644 --- a/src/core/api/base.go +++ b/src/core/api/base.go @@ -125,6 +125,7 @@ func (b *BaseController) WriteYamlData(object interface{}) { // Init related objects/configurations for the API controllers func Init() error { + registerHealthCheckers() // If chart repository is not enabled then directly return if !config.WithChartMuseum() { return nil diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 17e1a1e58c..9b501f108d 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -96,6 +96,7 @@ func init() { filter.Init() beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter) + beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth") beego.Router("/api/search/", &SearchAPI{}) beego.Router("/api/projects/", &ProjectAPI{}, "get:List;post:Post;head:Head") beego.Router("/api/projects/:id", &ProjectAPI{}, "delete:Delete;get:Get;put:Put") diff --git a/src/core/api/health.go b/src/core/api/health.go new file mode 100644 index 0000000000..1a43ab68e2 --- /dev/null +++ b/src/core/api/health.go @@ -0,0 +1,323 @@ +// Copyright 2019 Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "sync" + "time" + + "github.com/goharbor/harbor/src/common/utils" + + "github.com/goharbor/harbor/src/common/dao" + httputil "github.com/goharbor/harbor/src/common/http" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + + "github.com/docker/distribution/health" + "github.com/gomodule/redigo/redis" +) + +var ( + timeout = 60 * time.Second + healthCheckerRegistry = map[string]health.Checker{} +) + +type overallHealthStatus struct { + Status string `json:"status"` + Components []*componentHealthStatus `json:"components"` +} + +type componentHealthStatus struct { + Name string `json:"name"` + Status string `json:"status"` + Error string `json:"error,omitempty"` +} + +type healthy bool + +func (h healthy) String() string { + if h { + return "healthy" + } + return "unhealthy" +} + +// HealthAPI handles the request for "/api/health" +type HealthAPI struct { + BaseController +} + +// CheckHealth checks the health of system +func (h *HealthAPI) CheckHealth() { + var isHealthy healthy = true + components := []*componentHealthStatus{} + c := make(chan *componentHealthStatus, len(healthCheckerRegistry)) + for name, checker := range healthCheckerRegistry { + go check(name, checker, timeout, c) + } + for i := 0; i < len(healthCheckerRegistry); i++ { + componentStatus := <-c + if len(componentStatus.Error) != 0 { + isHealthy = false + } + components = append(components, componentStatus) + } + status := &overallHealthStatus{} + status.Status = isHealthy.String() + status.Components = components + if !isHealthy { + log.Debugf("unhealthy system status: %v", status) + } + h.WriteJSONData(status) +} + +func check(name string, checker health.Checker, + timeout time.Duration, c chan *componentHealthStatus) { + statusChan := make(chan *componentHealthStatus) + go func() { + err := checker.Check() + var healthy healthy = err == nil + status := &componentHealthStatus{ + Name: name, + Status: healthy.String(), + } + if !healthy { + status.Error = err.Error() + } + statusChan <- status + }() + + select { + case status := <-statusChan: + c <- status + case <-time.After(timeout): + var healthy healthy = false + c <- &componentHealthStatus{ + Name: name, + Status: healthy.String(), + Error: "failed to check the health status: timeout", + } + } +} + +// HTTPStatusCodeHealthChecker implements a Checker to check that the HTTP status code +// returned matches the expected one +func HTTPStatusCodeHealthChecker(method string, url string, header http.Header, + timeout time.Duration, statusCode int) health.Checker { + return health.CheckFunc(func() error { + req, err := http.NewRequest(method, url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) + } + for key, values := range header { + for _, value := range values { + req.Header.Add(key, value) + } + } + + client := httputil.NewClient(&http.Client{ + Timeout: timeout, + }) + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to check health: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != statusCode { + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Debugf("failed to read response body: %v", err) + } + return fmt.Errorf("received unexpected status code: %d %s", resp.StatusCode, string(data)) + } + + return nil + }) +} + +type updater struct { + sync.Mutex + status error +} + +func (u *updater) Check() error { + u.Lock() + defer u.Unlock() + + return u.status +} + +func (u *updater) update(status error) { + u.Lock() + defer u.Unlock() + + u.status = status +} + +// PeriodicHealthChecker implements a Checker to check status periodically +func PeriodicHealthChecker(checker health.Checker, period time.Duration) health.Checker { + u := &updater{ + // init the "status" as "unknown status" error to avoid returning nil error(which means healthy) + // before the first health check request finished + status: errors.New("unknown status"), + } + + go func() { + ticker := time.NewTicker(period) + for { + u.update(checker.Check()) + <-ticker.C + } + }() + + return u +} + +func coreHealthChecker() health.Checker { + return health.CheckFunc(func() error { + return nil + }) +} + +func portalHealthChecker() health.Checker { + url := config.GetPortalURL() + timeout := 60 * time.Second + period := 10 * time.Second + checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK) + return PeriodicHealthChecker(checker, period) +} + +func jobserviceHealthChecker() health.Checker { + url := config.InternalJobServiceURL() + "/api/v1/stats" + timeout := 60 * time.Second + period := 10 * time.Second + checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK) + return PeriodicHealthChecker(checker, period) +} + +func registryHealthChecker() health.Checker { + url := getRegistryURL() + "/v2" + timeout := 60 * time.Second + period := 10 * time.Second + checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusUnauthorized) + return PeriodicHealthChecker(checker, period) +} + +func registryCtlHealthChecker() health.Checker { + url := config.GetRegistryCtlURL() + "/api/health" + timeout := 60 * time.Second + period := 10 * time.Second + checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK) + return PeriodicHealthChecker(checker, period) +} + +func chartmuseumHealthChecker() health.Checker { + url, err := config.GetChartMuseumEndpoint() + if err != nil { + log.Errorf("failed to get the URL of chartmuseum: %v", err) + } + url = url + "/health" + timeout := 60 * time.Second + period := 10 * time.Second + checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK) + return PeriodicHealthChecker(checker, period) +} + +func clairHealthChecker() health.Checker { + url := config.GetClairHealthCheckServerURL() + "/health" + timeout := 60 * time.Second + period := 10 * time.Second + checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK) + return PeriodicHealthChecker(checker, period) +} + +func notaryHealthChecker() health.Checker { + url := config.InternalNotaryEndpoint() + "/_notary_server/health" + timeout := 60 * time.Second + period := 10 * time.Second + checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK) + return PeriodicHealthChecker(checker, period) +} + +func databaseHealthChecker() health.Checker { + period := 10 * time.Second + checker := health.CheckFunc(func() error { + _, err := dao.GetOrmer().Raw("SELECT 1").Exec() + if err != nil { + return fmt.Errorf("failed to run SQL \"SELECT 1\": %v", err) + } + return nil + }) + return PeriodicHealthChecker(checker, period) +} + +func redisHealthChecker() health.Checker { + url := config.GetRedisOfRegURL() + timeout := 60 * time.Second + period := 10 * time.Second + checker := health.CheckFunc(func() error { + conn, err := redis.DialURL(url, + redis.DialConnectTimeout(timeout*time.Second), + redis.DialReadTimeout(timeout*time.Second), + redis.DialWriteTimeout(timeout*time.Second)) + if err != nil { + return fmt.Errorf("failed to establish connection with Redis: %v", err) + } + defer conn.Close() + _, err = conn.Do("PING") + if err != nil { + return fmt.Errorf("failed to run \"PING\": %v", err) + } + return nil + }) + return PeriodicHealthChecker(checker, period) +} + +func registerHealthCheckers() { + healthCheckerRegistry["core"] = coreHealthChecker() + healthCheckerRegistry["portal"] = portalHealthChecker() + healthCheckerRegistry["jobservice"] = jobserviceHealthChecker() + healthCheckerRegistry["registry"] = registryHealthChecker() + healthCheckerRegistry["registryctl"] = registryCtlHealthChecker() + healthCheckerRegistry["database"] = databaseHealthChecker() + healthCheckerRegistry["redis"] = redisHealthChecker() + if config.WithChartMuseum() { + healthCheckerRegistry["chartmuseum"] = chartmuseumHealthChecker() + } + if config.WithClair() { + healthCheckerRegistry["clair"] = clairHealthChecker() + } + if config.WithNotary() { + healthCheckerRegistry["notary"] = notaryHealthChecker() + } +} + +func getRegistryURL() string { + endpoint, err := config.RegistryURL() + if err != nil { + log.Errorf("failed to get the URL of registry: %v", err) + return "" + } + url, err := utils.ParseEndpoint(endpoint) + if err != nil { + log.Errorf("failed to parse the URL of registry: %v", err) + return "" + } + return url.String() +} diff --git a/src/core/api/health_test.go b/src/core/api/health_test.go new file mode 100644 index 0000000000..8426a74b1f --- /dev/null +++ b/src/core/api/health_test.go @@ -0,0 +1,134 @@ +// Copyright 2019 Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "errors" + "net/http" + "testing" + "time" + + "github.com/docker/distribution/health" + "github.com/goharbor/harbor/src/common/utils/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStringOfHealthy(t *testing.T) { + var isHealthy healthy = true + assert.Equal(t, "healthy", isHealthy.String()) + isHealthy = false + assert.Equal(t, "unhealthy", isHealthy.String()) +} + +func TestUpdater(t *testing.T) { + updater := &updater{} + assert.Equal(t, nil, updater.Check()) + updater.status = errors.New("unhealthy") + assert.Equal(t, "unhealthy", updater.Check().Error()) +} + +func TestHTTPStatusCodeHealthChecker(t *testing.T) { + handler := &test.RequestHandlerMapping{ + Method: http.MethodGet, + Pattern: "/health", + Handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, + } + server := test.NewServer(handler) + defer server.Close() + + url := server.URL + "/health" + checker := HTTPStatusCodeHealthChecker( + http.MethodGet, url, map[string][]string{ + "key": {"value"}, + }, 5*time.Second, http.StatusOK) + assert.Equal(t, nil, checker.Check()) + + checker = HTTPStatusCodeHealthChecker( + http.MethodGet, url, nil, 5*time.Second, http.StatusUnauthorized) + assert.NotEqual(t, nil, checker.Check()) +} + +func TestPeriodicHealthChecker(t *testing.T) { + firstCheck := true + checkFunc := func() error { + time.Sleep(2 * time.Second) + if firstCheck { + firstCheck = false + return nil + } + return errors.New("unhealthy") + } + + checker := PeriodicHealthChecker(health.CheckFunc(checkFunc), 1*time.Second) + assert.Equal(t, "unknown status", checker.Check().Error()) + time.Sleep(3 * time.Second) + assert.Equal(t, nil, checker.Check()) + time.Sleep(3 * time.Second) + assert.Equal(t, "unhealthy", checker.Check().Error()) +} + +func fakeHealthChecker(healthy bool) health.Checker { + return health.CheckFunc(func() error { + if healthy { + return nil + } + return errors.New("unhealthy") + }) +} +func TestCheckHealth(t *testing.T) { + // component01: healthy, component02: healthy => status: healthy + healthCheckerRegistry = map[string]health.Checker{} + healthCheckerRegistry["component01"] = fakeHealthChecker(true) + healthCheckerRegistry["component02"] = fakeHealthChecker(true) + status := map[string]interface{}{} + err := handleAndParse(&testingRequest{ + method: http.MethodGet, + url: "/api/health", + }, &status) + require.Nil(t, err) + assert.Equal(t, "healthy", status["status"].(string)) + + // component01: healthy, component02: unhealthy => status: unhealthy + healthCheckerRegistry = map[string]health.Checker{} + healthCheckerRegistry["component01"] = fakeHealthChecker(true) + healthCheckerRegistry["component02"] = fakeHealthChecker(false) + status = map[string]interface{}{} + err = handleAndParse(&testingRequest{ + method: http.MethodGet, + url: "/api/health", + }, &status) + require.Nil(t, err) + assert.Equal(t, "unhealthy", status["status"].(string)) +} + +func TestCoreHealthChecker(t *testing.T) { + checker := coreHealthChecker() + assert.Equal(t, nil, checker.Check()) +} + +func TestDatabaseHealthChecker(t *testing.T) { + checker := databaseHealthChecker() + time.Sleep(1 * time.Second) + assert.Equal(t, nil, checker.Check()) +} + +func TestRegisterHealthCheckers(t *testing.T) { + healthCheckerRegistry = map[string]health.Checker{} + registerHealthCheckers() + assert.NotNil(t, healthCheckerRegistry["core"]) +} diff --git a/src/core/config/config.go b/src/core/config/config.go index 0d503b71ab..c71ce1574f 100644 --- a/src/core/config/config.go +++ b/src/core/config/config.go @@ -572,3 +572,36 @@ func GetChartMuseumEndpoint() (string, error) { return chartEndpoint, nil } + +// GetRedisOfRegURL returns the URL of Redis used by registry +func GetRedisOfRegURL() string { + return os.Getenv("_REDIS_URL_REG") +} + +// GetPortalURL returns the URL of portal +func GetPortalURL() string { + url := os.Getenv("PORTAL_URL") + if len(url) == 0 { + return common.DefaultPortalURL + } + return url +} + +// GetRegistryCtlURL returns the URL of registryctl +func GetRegistryCtlURL() string { + url := os.Getenv("REGISTRYCTL_URL") + if len(url) == 0 { + return common.DefaultRegistryCtlURL + } + return url +} + +// GetClairHealthCheckServerURL returns the URL of +// the health check server of Clair +func GetClairHealthCheckServerURL() string { + url := os.Getenv("CLAIR_HEALTH_CHECK_SERVER_URL") + if len(url) == 0 { + return common.DefaultClairHealthCheckServerURL + } + return url +} diff --git a/src/core/router.go b/src/core/router.go index 2c629ba01a..734fd84b6e 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -56,6 +56,7 @@ func initRouters() { } // API + beego.Router("/api/health", &api.HealthAPI{}, "get:CheckHealth") beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping") beego.Router("/api/search", &api.SearchAPI{}) beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") From bacfe64979712145b5582465031cd83108673c0d Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Wed, 16 Jan 2019 18:09:14 +0800 Subject: [PATCH 03/45] Rename ram to rbac Signed-off-by: He Weiwei --- src/common/{ram => rbac}/casbin.go | 2 +- src/common/{ram => rbac}/namespace.go | 2 +- src/common/{ram => rbac}/namespace_test.go | 2 +- src/common/{ram => rbac}/parser.go | 2 +- src/common/{ram => rbac}/parser_test.go | 2 +- src/common/{ram => rbac}/project/const.go | 14 +++++++------- src/common/{ram => rbac}/project/util.go | 18 +++++++++--------- src/common/{ram => rbac}/project/visitor.go | 16 ++++++++-------- .../{ram => rbac}/project/visitor_role.go | 14 +++++++------- .../{ram => rbac}/project/visitor_role_test.go | 0 .../{ram => rbac}/project/visitor_test.go | 8 ++++---- src/common/{ram/ram.go => rbac/rbac.go} | 6 +++--- .../{ram/ram_test.go => rbac/rbac_test.go} | 2 +- src/common/security/admiral/context.go | 16 ++++++++-------- src/common/security/local/context.go | 16 ++++++++-------- src/common/security/secret/context.go | 4 ++-- 16 files changed, 62 insertions(+), 62 deletions(-) rename src/common/{ram => rbac}/casbin.go (99%) rename src/common/{ram => rbac}/namespace.go (99%) rename src/common/{ram => rbac}/namespace_test.go (99%) rename src/common/{ram => rbac}/parser.go (99%) rename src/common/{ram => rbac}/parser_test.go (98%) rename src/common/{ram => rbac}/project/const.go (71%) rename src/common/{ram => rbac}/project/util.go (74%) rename src/common/{ram => rbac}/project/visitor.go (81%) rename src/common/{ram => rbac}/project/visitor_role.go (85%) rename src/common/{ram => rbac}/project/visitor_role_test.go (100%) rename src/common/{ram => rbac}/project/visitor_test.go (92%) rename src/common/{ram/ram.go => rbac/rbac.go} (97%) rename src/common/{ram/ram_test.go => rbac/rbac_test.go} (99%) diff --git a/src/common/ram/casbin.go b/src/common/rbac/casbin.go similarity index 99% rename from src/common/ram/casbin.go rename to src/common/rbac/casbin.go index d90294d9ad..306997f542 100644 --- a/src/common/ram/casbin.go +++ b/src/common/rbac/casbin.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ram +package rbac import ( "errors" diff --git a/src/common/ram/namespace.go b/src/common/rbac/namespace.go similarity index 99% rename from src/common/ram/namespace.go rename to src/common/rbac/namespace.go index daf5e5891d..ad85cc5e66 100644 --- a/src/common/ram/namespace.go +++ b/src/common/rbac/namespace.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ram +package rbac import ( "fmt" diff --git a/src/common/ram/namespace_test.go b/src/common/rbac/namespace_test.go similarity index 99% rename from src/common/ram/namespace_test.go rename to src/common/rbac/namespace_test.go index d3b8dff76d..5fddad0e41 100644 --- a/src/common/ram/namespace_test.go +++ b/src/common/rbac/namespace_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ram +package rbac import ( "testing" diff --git a/src/common/ram/parser.go b/src/common/rbac/parser.go similarity index 99% rename from src/common/ram/parser.go rename to src/common/rbac/parser.go index 22355b02e3..bb65943e68 100644 --- a/src/common/ram/parser.go +++ b/src/common/rbac/parser.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ram +package rbac import ( "errors" diff --git a/src/common/ram/parser_test.go b/src/common/rbac/parser_test.go similarity index 98% rename from src/common/ram/parser_test.go rename to src/common/rbac/parser_test.go index 3869297b74..cf23d517b4 100644 --- a/src/common/ram/parser_test.go +++ b/src/common/rbac/parser_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ram +package rbac import ( "testing" diff --git a/src/common/ram/project/const.go b/src/common/rbac/project/const.go similarity index 71% rename from src/common/ram/project/const.go rename to src/common/rbac/project/const.go index 5d54561cf5..dbefae98d2 100644 --- a/src/common/ram/project/const.go +++ b/src/common/rbac/project/const.go @@ -15,19 +15,19 @@ package project import ( - "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/rbac" ) // const action variables const ( - ActionAll = ram.Action("*") - ActionPull = ram.Action("pull") - ActionPush = ram.Action("push") - ActionPushPull = ram.Action("push+pull") + ActionAll = rbac.Action("*") + ActionPull = rbac.Action("pull") + ActionPush = rbac.Action("push") + ActionPushPull = rbac.Action("push+pull") ) // const resource variables const ( - ResourceAll = ram.Resource("*") - ResourceImage = ram.Resource("image") + ResourceAll = rbac.Resource("*") + ResourceImage = rbac.Resource("image") ) diff --git a/src/common/ram/project/util.go b/src/common/rbac/project/util.go similarity index 74% rename from src/common/ram/project/util.go rename to src/common/rbac/project/util.go index d073577771..34ebe86ebc 100644 --- a/src/common/ram/project/util.go +++ b/src/common/rbac/project/util.go @@ -15,26 +15,26 @@ package project import ( - "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/rbac" ) var ( // subresource policies for public project - publicProjectPolicies = []*ram.Policy{ + publicProjectPolicies = []*rbac.Policy{ {Resource: ResourceImage, Action: ActionPull}, } // subresource policies for system admin visitor - systemAdminProjectPolicies = []*ram.Policy{ + systemAdminProjectPolicies = []*rbac.Policy{ {Resource: ResourceAll, Action: ActionAll}, } ) -func policiesForPublicProject(namespace ram.Namespace) []*ram.Policy { - policies := []*ram.Policy{} +func policiesForPublicProject(namespace rbac.Namespace) []*rbac.Policy { + policies := []*rbac.Policy{} for _, policy := range publicProjectPolicies { - policies = append(policies, &ram.Policy{ + policies = append(policies, &rbac.Policy{ Resource: namespace.Resource(policy.Resource), Action: policy.Action, Effect: policy.Effect, @@ -44,11 +44,11 @@ func policiesForPublicProject(namespace ram.Namespace) []*ram.Policy { return policies } -func policiesForSystemAdmin(namespace ram.Namespace) []*ram.Policy { - policies := []*ram.Policy{} +func policiesForSystemAdmin(namespace rbac.Namespace) []*rbac.Policy { + policies := []*rbac.Policy{} for _, policy := range systemAdminProjectPolicies { - policies = append(policies, &ram.Policy{ + policies = append(policies, &rbac.Policy{ Resource: namespace.Resource(policy.Resource), Action: policy.Action, Effect: policy.Effect, diff --git a/src/common/ram/project/visitor.go b/src/common/rbac/project/visitor.go similarity index 81% rename from src/common/ram/project/visitor.go rename to src/common/rbac/project/visitor.go index 8a7e5263dc..c37a6ebef6 100644 --- a/src/common/ram/project/visitor.go +++ b/src/common/rbac/project/visitor.go @@ -15,7 +15,7 @@ package project import ( - "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/rbac" ) // visitorContext the context interface for the project visitor @@ -27,10 +27,10 @@ type visitorContext interface { IsSysAdmin() bool } -// visitor implement the ram.User interface for project visitor +// visitor implement the rbac.User interface for project visitor type visitor struct { ctx visitorContext - namespace ram.Namespace + namespace rbac.Namespace projectRoles []int } @@ -45,7 +45,7 @@ func (v *visitor) GetUserName() string { } // GetPolicies returns policies of the visitor -func (v *visitor) GetPolicies() []*ram.Policy { +func (v *visitor) GetPolicies() []*rbac.Policy { if v.ctx.IsSysAdmin() { return policiesForSystemAdmin(v.namespace) } @@ -58,12 +58,12 @@ func (v *visitor) GetPolicies() []*ram.Policy { } // GetRoles returns roles of the visitor -func (v *visitor) GetRoles() []ram.Role { +func (v *visitor) GetRoles() []rbac.Role { if !v.ctx.IsAuthenticated() { return nil } - roles := []ram.Role{} + roles := []rbac.Role{} for _, roleID := range v.projectRoles { roles = append(roles, &visitorRole{roleID: roleID, namespace: v.namespace}) @@ -72,8 +72,8 @@ func (v *visitor) GetRoles() []ram.Role { return roles } -// NewUser returns ram.User interface for the project visitor -func NewUser(ctx visitorContext, namespace ram.Namespace, projectRoles ...int) ram.User { +// NewUser returns rbac.User interface for the project visitor +func NewUser(ctx visitorContext, namespace rbac.Namespace, projectRoles ...int) rbac.User { return &visitor{ ctx: ctx, namespace: namespace, diff --git a/src/common/ram/project/visitor_role.go b/src/common/rbac/project/visitor_role.go similarity index 85% rename from src/common/ram/project/visitor_role.go rename to src/common/rbac/project/visitor_role.go index 20b18f236d..ada5868e6e 100644 --- a/src/common/ram/project/visitor_role.go +++ b/src/common/rbac/project/visitor_role.go @@ -16,11 +16,11 @@ package project import ( "github.com/goharbor/harbor/src/common" - "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/rbac" ) var ( - rolePoliciesMap = map[string][]*ram.Policy{ + rolePoliciesMap = map[string][]*rbac.Policy{ "projectAdmin": { {Resource: ResourceImage, Action: ActionPushPull}, // compatible with security all perm of project {Resource: ResourceImage, Action: ActionPush}, @@ -38,9 +38,9 @@ var ( } ) -// visitorRole implement the ram.Role interface +// visitorRole implement the rbac.Role interface type visitorRole struct { - namespace ram.Namespace + namespace rbac.Namespace roleID int } @@ -59,8 +59,8 @@ func (role *visitorRole) GetRoleName() string { } // GetPolicies returns policies for the visitor role -func (role *visitorRole) GetPolicies() []*ram.Policy { - policies := []*ram.Policy{} +func (role *visitorRole) GetPolicies() []*rbac.Policy { + policies := []*rbac.Policy{} roleName := role.GetRoleName() if roleName == "" { @@ -68,7 +68,7 @@ func (role *visitorRole) GetPolicies() []*ram.Policy { } for _, policy := range rolePoliciesMap[roleName] { - policies = append(policies, &ram.Policy{ + policies = append(policies, &rbac.Policy{ Resource: role.namespace.Resource(policy.Resource), Action: policy.Action, Effect: policy.Effect, diff --git a/src/common/ram/project/visitor_role_test.go b/src/common/rbac/project/visitor_role_test.go similarity index 100% rename from src/common/ram/project/visitor_role_test.go rename to src/common/rbac/project/visitor_role_test.go diff --git a/src/common/ram/project/visitor_test.go b/src/common/rbac/project/visitor_test.go similarity index 92% rename from src/common/ram/project/visitor_test.go rename to src/common/rbac/project/visitor_test.go index c1b3450ccf..4563b41bd8 100644 --- a/src/common/ram/project/visitor_test.go +++ b/src/common/rbac/project/visitor_test.go @@ -18,7 +18,7 @@ import ( "testing" "github.com/goharbor/harbor/src/common" - "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/rbac" "github.com/stretchr/testify/suite" ) @@ -50,8 +50,8 @@ type VisitorTestSuite struct { } func (suite *VisitorTestSuite) TestGetPolicies() { - namespace := ram.NewProjectNamespace("library", false) - publicNamespace := ram.NewProjectNamespace("library", true) + namespace := rbac.NewProjectNamespace("library", false) + publicNamespace := rbac.NewProjectNamespace("library", true) anonymous := NewUser(anonymousCtx, namespace) suite.Nil(anonymous.GetPolicies()) @@ -73,7 +73,7 @@ func (suite *VisitorTestSuite) TestGetPolicies() { } func (suite *VisitorTestSuite) TestGetRoles() { - namespace := ram.NewProjectNamespace("library", false) + namespace := rbac.NewProjectNamespace("library", false) anonymous := NewUser(anonymousCtx, namespace) suite.Nil(anonymous.GetRoles()) diff --git a/src/common/ram/ram.go b/src/common/rbac/rbac.go similarity index 97% rename from src/common/ram/ram.go rename to src/common/rbac/rbac.go index 76a76b2b1c..843686f3b1 100644 --- a/src/common/ram/ram.go +++ b/src/common/rbac/rbac.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ram +package rbac import ( "fmt" @@ -87,14 +87,14 @@ func (p *Policy) GetEffect() string { return eft.String() } -// Role the interface of ram role +// Role the interface of rbac role type Role interface { // GetRoleName returns the role identity, if empty string role's policies will be ignore GetRoleName() string GetPolicies() []*Policy } -// User the interface of ram user +// User the interface of rbac user type User interface { // GetUserName returns the user identity, if empty string user's all policies will be ignore GetUserName() string diff --git a/src/common/ram/ram_test.go b/src/common/rbac/rbac_test.go similarity index 99% rename from src/common/ram/ram_test.go rename to src/common/rbac/rbac_test.go index 84a8acaa51..45af1b100f 100644 --- a/src/common/ram/ram_test.go +++ b/src/common/rbac/rbac_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ram +package rbac import ( "reflect" diff --git a/src/common/security/admiral/context.go b/src/common/security/admiral/context.go index 09856d3f57..86ad4a7722 100644 --- a/src/common/security/admiral/context.go +++ b/src/common/security/admiral/context.go @@ -16,8 +16,8 @@ package admiral import ( "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/ram" - "github.com/goharbor/harbor/src/common/ram/project" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/common/security/admiral/authcontext" "github.com/goharbor/harbor/src/core/promgr" ) @@ -72,32 +72,32 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPush, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPushPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // Can returns whether the user can do action on resource -func (s *SecurityContext) Can(action ram.Action, resource ram.Resource) bool { +func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { ns, err := resource.GetNamespace() if err == nil { switch ns.Kind() { case "project": projectIDOrName := ns.Identity() isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - projectNamespace := ram.NewProjectNamespace(projectIDOrName, isPublicProject) + projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject) user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...) - return ram.HasPermission(user, resource, action) + return rbac.HasPermission(user, resource, action) } } diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index 2cf469237d..e7c2bc5571 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -19,8 +19,8 @@ import ( "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao/group" "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/ram" - "github.com/goharbor/harbor/src/common/ram/project" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/promgr" ) @@ -70,32 +70,32 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPush, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPushPull, ram.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) } // Can returns whether the user can do action on resource -func (s *SecurityContext) Can(action ram.Action, resource ram.Resource) bool { +func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { ns, err := resource.GetNamespace() if err == nil { switch ns.Kind() { case "project": projectIDOrName := ns.Identity() isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - projectNamespace := ram.NewProjectNamespace(projectIDOrName, isPublicProject) + projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject) user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...) - return ram.HasPermission(user, resource, action) + return rbac.HasPermission(user, resource, action) } } diff --git a/src/common/security/secret/context.go b/src/common/security/secret/context.go index 62ee716024..0155320d85 100644 --- a/src/common/security/secret/context.go +++ b/src/common/security/secret/context.go @@ -19,7 +19,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/ram" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/secret" "github.com/goharbor/harbor/src/common/utils/log" ) @@ -99,7 +99,7 @@ func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { } // Can returns whether the user can do action on resource -func (s *SecurityContext) Can(action ram.Action, resource ram.Resource) bool { +func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { if s.store == nil { return false } From 1af0f3c3b9ab2977d10dc8bc2d1ca6eabac9dc7c Mon Sep 17 00:00:00 2001 From: Yan Date: Wed, 16 Jan 2019 00:04:07 +0800 Subject: [PATCH 04/45] Add API implementation of robot account Add API implementation of robot account 1. POST /api/project/pid/robots 2, GET /api/project/pid/robots/id? 3, PUT /api/project/pid/robots/id 4, DELETE /api/project/pid/robots/id Signed-off-by: wang yan --- src/common/dao/robot.go | 5 + src/common/models/robot.go | 26 ++- src/common/models/token.go | 6 + src/core/api/api_test.go | 27 ++- src/core/api/harborapi_test.go | 3 + src/core/api/robot.go | 207 ++++++++++++++++++++++ src/core/api/robot_test.go | 314 +++++++++++++++++++++++++++++++++ src/core/router.go | 4 + 8 files changed, 589 insertions(+), 3 deletions(-) create mode 100644 src/core/api/robot.go create mode 100644 src/core/api/robot_test.go diff --git a/src/common/dao/robot.go b/src/common/dao/robot.go index 873f89c204..51673eeebd 100644 --- a/src/common/dao/robot.go +++ b/src/common/dao/robot.go @@ -79,6 +79,11 @@ func getRobotQuerySetter(query *models.RobotQuery) orm.QuerySeter { return qs } +// CountRobot ... +func CountRobot(query *models.RobotQuery) (int64, error) { + return getRobotQuerySetter(query).Count() +} + // UpdateRobot ... func UpdateRobot(robot *models.Robot) error { robot.UpdateTime = time.Now() diff --git a/src/common/models/robot.go b/src/common/models/robot.go index da81a5025a..f5e9f585e3 100644 --- a/src/common/models/robot.go +++ b/src/common/models/robot.go @@ -15,6 +15,7 @@ package models import ( + "github.com/astaxie/beego/validation" "time" ) @@ -42,7 +43,30 @@ type RobotQuery struct { Pagination } +// RobotReq ... +type RobotReq struct { + Name string `json:"name"` + Description string `json:"description"` + Disabled bool `json:"disabled"` + Access []*ResourceActions `json:"access"` +} + +// Valid put request validation +func (rq *RobotReq) Valid(v *validation.Validation) { + switch rq.Disabled { + case true, false: + default: + v.SetError("disabled", "must be in [true, false]") + } +} + +// RobotRep ... +type RobotRep struct { + Name string + Token string +} + // TableName ... -func (u *Robot) TableName() string { +func (r *Robot) TableName() string { return RobotTable } diff --git a/src/common/models/token.go b/src/common/models/token.go index 1b9122b006..f5bbd797b7 100644 --- a/src/common/models/token.go +++ b/src/common/models/token.go @@ -20,3 +20,9 @@ type Token struct { ExpiresIn int `json:"expires_in"` IssuedAt string `json:"issued_at"` } + +// ResourceActions ... +type ResourceActions struct { + Name string `json:"name"` + Actions []string `json:"actions"` +} diff --git a/src/core/api/api_test.go b/src/core/api/api_test.go index 8b7da6a75b..8b9d3bfaf5 100644 --- a/src/core/api/api_test.go +++ b/src/core/api/api_test.go @@ -40,8 +40,8 @@ import ( ) var ( - nonSysAdminID, projAdminID, projDeveloperID, projGuestID int64 - projAdminPMID, projDeveloperPMID, projGuestPMID int + nonSysAdminID, projAdminID, projDeveloperID, projGuestID, projAdminRobotID int64 + projAdminPMID, projDeveloperPMID, projGuestPMID, projAdminRobotPMID int // The following users/credentials are registered and assigned roles at the beginning of // running testing and cleaned up at the end. // Do not try to change the system and project roles that the users have during @@ -67,6 +67,10 @@ var ( Name: "proj_guest", Passwd: "Harbor12345", } + projAdmin4Robot = &usrInfo{ + Name: "proj_admin_robot", + Passwd: "Harbor12345", + } ) type testingRequest struct { @@ -240,6 +244,25 @@ func prepare() error { return err } + // register projAdminRobots and assign project admin role + projAdminRobotID, err = dao.Register(models.User{ + Username: projAdmin4Robot.Name, + Password: projAdmin4Robot.Passwd, + Email: projAdmin4Robot.Name + "@test.com", + }) + if err != nil { + return err + } + + if projAdminRobotPMID, err = project.AddProjectMember(models.Member{ + ProjectID: 1, + Role: models.PROJECTADMIN, + EntityID: int(projAdminRobotID), + EntityType: common.UserMember, + }); err != nil { + return err + } + // register projDeveloper and assign project developer role projDeveloperID, err = dao.Register(models.User{ Username: projDeveloper.Name, diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 9b501f108d..5f1f59cc58 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -153,6 +153,9 @@ func init() { beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog") beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post") + beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List") + beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete") + // Charts are controlled under projects chartRepositoryAPIType := &ChartRepositoryAPI{} beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus") diff --git a/src/core/api/robot.go b/src/core/api/robot.go new file mode 100644 index 0000000000..d373082a0d --- /dev/null +++ b/src/core/api/robot.go @@ -0,0 +1,207 @@ +// Copyright 2018 Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + "net/http" + "strconv" +) + +// User this prefix to distinguish harbor user, +// The prefix contains a specific character($), so it cannot be registered as a harbor user. +const robotPrefix = "robot$" + +// RobotAPI ... +type RobotAPI struct { + BaseController + project *models.Project + robot *models.Robot +} + +// Prepare ... +func (r *RobotAPI) Prepare() { + r.BaseController.Prepare() + method := r.Ctx.Request.Method + + if !r.SecurityCtx.IsAuthenticated() { + r.HandleUnauthorized() + return + } + + pid, err := r.GetInt64FromPath(":pid") + if err != nil || pid <= 0 { + text := "invalid project ID: " + if err != nil { + text += err.Error() + } else { + text += fmt.Sprintf("%d", pid) + } + r.HandleBadRequest(text) + return + } + project, err := r.ProjectMgr.Get(pid) + if err != nil { + r.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err) + return + } + if project == nil { + r.HandleNotFound(fmt.Sprintf("project %d not found", pid)) + return + } + r.project = project + + if method == http.MethodPut || method == http.MethodDelete { + id, err := r.GetInt64FromPath(":id") + if err != nil || id <= 0 { + r.HandleBadRequest("invalid robot ID") + return + } + + robot, err := dao.GetRobotByID(id) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err)) + return + } + + if robot == nil { + r.HandleNotFound(fmt.Sprintf("robot %d not found", id)) + return + } + + r.robot = robot + } + + if !(r.Ctx.Input.IsGet() && r.SecurityCtx.HasReadPerm(pid) || + r.SecurityCtx.HasAllPerm(pid)) { + r.HandleForbidden(r.SecurityCtx.GetUsername()) + return + } + +} + +// Post ... +func (r *RobotAPI) Post() { + var robotReq models.RobotReq + r.DecodeJSONReq(&robotReq) + + createdName := robotPrefix + robotReq.Name + + robot := models.Robot{ + Name: createdName, + Description: robotReq.Description, + ProjectID: r.project.ProjectID, + // TODO: use token service to generate token per access information + Token: "this is a placeholder", + } + + robotQuery := models.RobotQuery{ + Name: createdName, + ProjectID: r.project.ProjectID, + } + robots, err := dao.ListRobots(&robotQuery) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to list robot account: %v", err)) + return + } + if len(robots) > 0 { + r.HandleConflict() + return + } + + id, err := dao.AddRobot(&robot) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to create robot account: %v", err)) + return + } + + robotRep := models.RobotRep{ + Name: robot.Name, + Token: robot.Token, + } + + r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10)) + r.Data["json"] = robotRep + r.ServeJSON() +} + +// List list all the robots of a project +func (r *RobotAPI) List() { + query := models.RobotQuery{ + ProjectID: r.project.ProjectID, + } + + count, err := dao.CountRobot(&query) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to count robots %v", err)) + return + } + query.Page, query.Size = r.GetPaginationParams() + + robots, err := dao.ListRobots(&query) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get robots %v", err)) + return + } + + r.SetPaginationHeader(count, query.Page, query.Size) + r.Data["json"] = robots + r.ServeJSON() +} + +// Get get robot by id +func (r *RobotAPI) Get() { + id, err := r.GetInt64FromPath(":id") + if err != nil || id <= 0 { + r.HandleBadRequest(fmt.Sprintf("invalid robot ID: %s", r.GetStringFromPath(":id"))) + return + } + + robot, err := dao.GetRobotByID(id) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err)) + return + } + if robot == nil { + r.HandleNotFound(fmt.Sprintf("robot %d not found", id)) + return + } + + r.Data["json"] = robot + r.ServeJSON() +} + +// Put disable or enable a robot account +func (r *RobotAPI) Put() { + var robotReq models.RobotReq + r.DecodeJSONReqAndValidate(&robotReq) + r.robot.Disabled = robotReq.Disabled + + if err := dao.UpdateRobot(r.robot); err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to update robot %d: %v", r.robot.ID, err)) + return + } + +} + +// Delete delete robot by id +func (r *RobotAPI) Delete() { + if err := dao.DeleteRobot(r.robot.ID); err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to delete robot %d: %v", r.robot.ID, err)) + return + } +} diff --git a/src/core/api/robot_test.go b/src/core/api/robot_test.go new file mode 100644 index 0000000000..f69791e717 --- /dev/null +++ b/src/core/api/robot_test.go @@ -0,0 +1,314 @@ +// Copyright 2018 Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "github.com/goharbor/harbor/src/common/models" + "net/http" + "testing" +) + +var ( + robotPath = "/api/projects/1/robots" + robotID int64 +) + +func TestRobotAPIPost(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodPost, + url: robotPath, + }, + code: http.StatusUnauthorized, + }, + + // 403 + { + request: &testingRequest{ + method: http.MethodPost, + url: robotPath, + bodyJSON: &models.Robot{}, + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + // 201 + { + request: &testingRequest{ + method: http.MethodPost, + url: robotPath, + bodyJSON: &models.Robot{ + Name: "test", + Description: "test desc", + }, + credential: projAdmin4Robot, + }, + code: http.StatusCreated, + }, + // 403 -- developer + { + request: &testingRequest{ + method: http.MethodPost, + url: robotPath, + bodyJSON: &models.Robot{ + Name: "test2", + Description: "test2 desc", + }, + credential: projDeveloper, + }, + code: http.StatusForbidden, + }, + + // 409 + { + request: &testingRequest{ + method: http.MethodPost, + url: robotPath, + bodyJSON: &models.Robot{ + Name: "test", + Description: "test desc", + ProjectID: 1, + }, + credential: projAdmin4Robot, + }, + code: http.StatusConflict, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestRobotAPIGet(t *testing.T) { + cases := []*codeCheckingCase{ + // 400 + { + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", robotPath, 0), + }, + code: http.StatusUnauthorized, + }, + + // 404 + { + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", robotPath, 1000), + credential: projDeveloper, + }, + code: http.StatusNotFound, + }, + + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", robotPath, 1), + credential: projDeveloper, + }, + code: http.StatusOK, + }, + + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("%s/%d", robotPath, 1), + credential: projAdmin4Robot, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestRobotAPIList(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodGet, + url: robotPath, + }, + code: http.StatusUnauthorized, + }, + + // 400 + { + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/0/robots", + credential: projAdmin4Robot, + }, + code: http.StatusBadRequest, + }, + + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: robotPath, + credential: projDeveloper, + }, + code: http.StatusOK, + }, + + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: robotPath, + credential: projAdmin4Robot, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestRobotAPIPut(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", robotPath, 1), + }, + code: http.StatusUnauthorized, + }, + + // 400 + { + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", robotPath, 0), + credential: projAdmin4Robot, + }, + code: http.StatusBadRequest, + }, + + // 404 + { + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", robotPath, 10000), + credential: projAdmin4Robot, + }, + code: http.StatusNotFound, + }, + + // 403 non-member user + { + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", robotPath, 1), + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + + // 403 developer + { + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", robotPath, 1), + credential: projDeveloper, + }, + code: http.StatusForbidden, + }, + + // 200 + { + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("%s/%d", robotPath, 1), + bodyJSON: &models.Robot{ + Disabled: true, + }, + credential: projAdmin4Robot, + }, + code: http.StatusOK, + }, + } + + runCodeCheckingCases(t, cases...) +} + +func TestRobotAPIDelete(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", robotPath, 1), + }, + code: http.StatusUnauthorized, + }, + + // 400 + { + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", robotPath, 0), + credential: projAdmin4Robot, + }, + code: http.StatusBadRequest, + }, + + // 404 + { + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", robotPath, 10000), + credential: projAdmin4Robot, + }, + code: http.StatusNotFound, + }, + + // 403 non-member user + { + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", robotPath, 1), + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + + // 403 developer + { + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", robotPath, 1), + credential: projDeveloper, + }, + code: http.StatusForbidden, + }, + + // 200 + { + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("%s/%d", robotPath, 1), + credential: projAdmin4Robot, + }, + code: http.StatusOK, + }, + } + + runCodeCheckingCases(t, cases...) +} diff --git a/src/core/router.go b/src/core/router.go index 734fd84b6e..1fe0686139 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -65,6 +65,10 @@ func initRouters() { beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get") beego.Router("/api/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post") beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &api.MetadataAPI{}, "put:Put;delete:Delete") + + beego.Router("/api/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List") + beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &api.RobotAPI{}, "get:Get;put:Put;delete:Delete") + beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get") beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll") beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put") From 4cde11892afebca71159da81cc30a0ab4602a12e Mon Sep 17 00:00:00 2001 From: wang yan Date: Wed, 16 Jan 2019 18:20:38 +0800 Subject: [PATCH 05/45] update the conflict check with DB unique constrain error message Signed-off-by: wang yan --- src/common/dao/base.go | 4 ++++ src/common/dao/robot.go | 10 +++++++++- src/core/api/robot.go | 18 ++++-------------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/common/dao/base.go b/src/common/dao/base.go index 4de0f8648a..2c397c16cc 100644 --- a/src/common/dao/base.go +++ b/src/common/dao/base.go @@ -15,6 +15,7 @@ package dao import ( + "errors" "fmt" "strconv" "strings" @@ -32,6 +33,9 @@ const ( ClairDBAlias = "clair-db" ) +// ErrDupRows is returned by DAO when inserting failed with error "duplicate key value violates unique constraint" +var ErrDupRows = errors.New("sql: duplicate row in DB") + // Database is an interface of different databases type Database interface { // Name returns the name of database diff --git a/src/common/dao/robot.go b/src/common/dao/robot.go index 51673eeebd..0d8b5c7f14 100644 --- a/src/common/dao/robot.go +++ b/src/common/dao/robot.go @@ -17,6 +17,7 @@ package dao import ( "github.com/astaxie/beego/orm" "github.com/goharbor/harbor/src/common/models" + "strings" "time" ) @@ -25,7 +26,14 @@ func AddRobot(robot *models.Robot) (int64, error) { now := time.Now() robot.CreationTime = now robot.UpdateTime = now - return GetOrmer().Insert(robot) + id, err := GetOrmer().Insert(robot) + if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return 0, ErrDupRows + } + return 0, err + } + return id, nil } // GetRobotByID ... diff --git a/src/core/api/robot.go b/src/core/api/robot.go index d373082a0d..436cecb76d 100644 --- a/src/core/api/robot.go +++ b/src/core/api/robot.go @@ -109,22 +109,12 @@ func (r *RobotAPI) Post() { Token: "this is a placeholder", } - robotQuery := models.RobotQuery{ - Name: createdName, - ProjectID: r.project.ProjectID, - } - robots, err := dao.ListRobots(&robotQuery) - if err != nil { - r.HandleInternalServerError(fmt.Sprintf("failed to list robot account: %v", err)) - return - } - if len(robots) > 0 { - r.HandleConflict() - return - } - id, err := dao.AddRobot(&robot) if err != nil { + if err == dao.ErrDupRows { + r.HandleConflict() + return + } r.HandleInternalServerError(fmt.Sprintf("failed to create robot account: %v", err)) return } From 903e15235ee3c877be67992d048c8de558edee99 Mon Sep 17 00:00:00 2001 From: wang yan Date: Thu, 17 Jan 2019 15:33:05 +0800 Subject: [PATCH 06/45] Update validation and error message per comments --- src/common/models/robot.go | 6 +----- src/core/api/robot.go | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/common/models/robot.go b/src/common/models/robot.go index f5e9f585e3..78c4d21b8e 100644 --- a/src/common/models/robot.go +++ b/src/common/models/robot.go @@ -53,11 +53,7 @@ type RobotReq struct { // Valid put request validation func (rq *RobotReq) Valid(v *validation.Validation) { - switch rq.Disabled { - case true, false: - default: - v.SetError("disabled", "must be in [true, false]") - } + // ToDo: add validation for access info. } // RobotRep ... diff --git a/src/core/api/robot.go b/src/core/api/robot.go index 436cecb76d..7e2ba2b91b 100644 --- a/src/core/api/robot.go +++ b/src/core/api/robot.go @@ -45,13 +45,13 @@ func (r *RobotAPI) Prepare() { pid, err := r.GetInt64FromPath(":pid") if err != nil || pid <= 0 { - text := "invalid project ID: " + var errMsg string if err != nil { - text += err.Error() + errMsg = "failed to get project ID " + err.Error() } else { - text += fmt.Sprintf("%d", pid) + errMsg = "invalid project ID: " + fmt.Sprintf("%d", pid) } - r.HandleBadRequest(text) + r.HandleBadRequest(errMsg) return } project, err := r.ProjectMgr.Get(pid) @@ -137,7 +137,7 @@ func (r *RobotAPI) List() { count, err := dao.CountRobot(&query) if err != nil { - r.HandleInternalServerError(fmt.Sprintf("failed to count robots %v", err)) + r.HandleInternalServerError(fmt.Sprintf("failed to list robots on project: %d, %v", r.project.ProjectID, err)) return } query.Page, query.Size = r.GetPaginationParams() From f29960628a280cecf620c00b74761c42320e78f6 Mon Sep 17 00:00:00 2001 From: danfengliu Date: Wed, 16 Jan 2019 13:39:36 +0800 Subject: [PATCH 07/45] Keyword has modified, but verify.robot didn't adapt this keyword. Modifications: 1.Add paramenter hasimage in data.json 2.Modify Harbor-Pages\Verify.robot to pass it to 3.Add loop and exception-catch in to prevent using Sleep and failure by exception(this exception was caused by short time of paga loading) Signed-off-by: danfengliu --- .../Harbor-Pages/Project-Members.robot | 17 ++++++++---- tests/resources/Harbor-Pages/Verify.robot | 26 ++++++++++++------- tests/robot-cases/Group3-Upgrade/data.json | 2 ++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/resources/Harbor-Pages/Project-Members.robot b/tests/resources/Harbor-Pages/Project-Members.robot index 5b34e41626..d246bd9902 100644 --- a/tests/resources/Harbor-Pages/Project-Members.robot +++ b/tests/resources/Harbor-Pages/Project-Members.robot @@ -22,14 +22,21 @@ ${HARBOR_VERSION} v1.1.1 *** Keywords *** Go Into Project [Arguments] ${project} ${has_image}=${true} - Wait Until Element Is Visible ${search_input} + Wait Until Element Is Visible And Enabled ${search_input} Input Text ${search_input} ${project} Wait Until Page Contains ${project} - Wait Until Element Is Visible xpath=//*[@id='project-results']//clr-dg-cell[contains(.,'${project}')]/a + Wait Until Element Is Visible And Enabled xpath=//*[@id='project-results']//clr-dg-cell[contains(.,'${project}')]/a Click Element xpath=//*[@id='project-results']//clr-dg-cell[contains(.,'${project}')]/a - Run Keyword If ${has_image}==${false} Wait Until Element Is Visible xpath=//clr-dg-placeholder[contains(.,\"We couldn\'t find any repositories!\")] - ... ELSE Wait Until Element Is Visible xpath=//clr-dg-cell[contains(.,'${project}/')] - Capture Page Screenshot gointo_${project}.png + #To prevent waiting for a fixed-period of time for page loading and failure caused by exception, we add loop to re-run when + # exception was caught. + :For ${n} IN RANGE 1 5 + \ ${out} Run Keyword If ${has_image}==${false} Run Keyword And Ignore Error Wait Until Element Is Visible And Enabled xpath=//clr-dg-placeholder[contains(.,\"We couldn\'t find any repositories!\")] + \ ... ELSE Run Keyword And Ignore Error Wait Until Element Is Visible And Enabled xpath=//clr-dg-cell[contains(.,'${project}/')] + \ Log To Console ${out[0]} + \ ${result} Set Variable If '${out[0]}'=='PASS' ${true} ${false} + \ Run Keyword If ${result} == ${true} Exit For Loop + \ Sleep 1 + Should Be Equal ${result} ${true} Add User To Project Admin [Arguments] ${project} ${user} diff --git a/tests/resources/Harbor-Pages/Verify.robot b/tests/resources/Harbor-Pages/Verify.robot index 1d0aeee29e..99625844cc 100644 --- a/tests/resources/Harbor-Pages/Verify.robot +++ b/tests/resources/Harbor-Pages/Verify.robot @@ -35,7 +35,9 @@ Verify Image Tag Init Chrome Driver Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} :FOR ${project} IN @{project} - \ Go Into Project ${project} + \ @{out_has_image}= Get Value From Json ${json} $.projects[?(@.name=${project})].has_image + \ ${has_image} Set Variable If @{out_has_image}[0] == ${true} ${true} ${false} + \ Go Into Project ${project} has_image=${has_image} \ @{repo}= Get Value From Json ${json} $.projects[?(@name=${project})]..repo..name \ Loop Image Repo @{repo} \ Back To Projects @@ -52,7 +54,9 @@ Verify Member Exist Init Chrome Driver Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} :For ${project} In @{project} - \ Go Into Project ${project} + \ @{out_has_image}= Get Value From Json ${json} $.projects[?(@.name=${project})].has_image + \ ${has_image} Set Variable If @{out_has_image}[0] == ${true} ${true} ${false} + \ Go Into Project ${project} has_image=${has_image} \ Switch To Member \ @{members}= Get Value From Json ${json} $.projects[?(@name=${project})].member..name \ Loop Member @{members} @@ -91,12 +95,14 @@ Verify Project Label Init Chrome Driver Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} :For ${project} In @{project} - \ Go Into Project ${project} - \ Switch To Project Label - \ @{projectlabel}= Get Value From Json ${json} $.projects[?(@.name=${project})]..labels..name - \ :For ${label} In @{label} - \ \ Page Should Contain ${projectlabel} - \ Back To Projects + \ @{out_has_image}= Get Value From Json ${json} $.projects[?(@.name=${project})].has_image + \ ${has_image} Set Variable If @{out_has_image}[0] == ${true} ${true} ${false} + \ Go Into Project ${project} has_image=${has_image} + \ Switch To Project Label + \ @{projectlabel}= Get Value From Json ${json} $.projects[?(@.name=${project})]..labels..name + \ :For ${label} In @{label} + \ \ Page Should Contain ${projectlabel} + \ Back To Projects Close Browser Verify Endpoint @@ -129,7 +135,9 @@ Verify Project Setting \ ${scanonpush}= Get Value From Json ${json} $.projects[?(@.name=${project})]..automatically_scan_images_on_push \ Init Chrome Driver \ Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} - \ Go Into Project ${project} + \ @{out_has_image}= Get Value From Json ${json} $.projects[?(@.name=${project})].has_image + \ ${has_image} Set Variable If @{out_has_image}[0] == ${true} ${true} ${false} + \ Go Into Project ${project} has_image=${has_image} \ Goto Project Config \ Run Keyword If ${public} == "public" Checkbox Should Be Checked //clr-checkbox-wrapper[@name='public']//label \ Run Keyword If ${contenttrust} == "true" Checkbox Should Be Checked //clr-checkbox-wrapper[@name='content-trust']//label diff --git a/tests/robot-cases/Group3-Upgrade/data.json b/tests/robot-cases/Group3-Upgrade/data.json index a0a2858d2f..20f27cf844 100644 --- a/tests/robot-cases/Group3-Upgrade/data.json +++ b/tests/robot-cases/Group3-Upgrade/data.json @@ -92,6 +92,7 @@ "projects":[ { "name":"project1", + "has_image":true, "accesslevel":"public", "repocounts":2, "repo":[ @@ -151,6 +152,7 @@ }, { "name":"project2", + "has_image":false, "accesslevel":"public", "repocounts":2, "repo":[ From 276202cfa9540fea6d5ee4e2924c1022cfcd5af5 Mon Sep 17 00:00:00 2001 From: Qian Deng Date: Fri, 18 Jan 2019 13:34:32 +0800 Subject: [PATCH 08/45] Fix issue data not reload when global serach going to a location that has the same url Just add a reload option on router module. This feature is introduced in angular5, we can solve this issue easily. Signed-off-by: Qian Deng --- src/portal/package-lock.json | 2 +- src/portal/src/app/harbor-routing.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/portal/package-lock.json b/src/portal/package-lock.json index 494219bcea..39788b6aeb 100644 --- a/src/portal/package-lock.json +++ b/src/portal/package-lock.json @@ -1,6 +1,6 @@ { "name": "harbor", - "version": "1.7.0", + "version": "1.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index a6f01fbe25..5eab2b3c14 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -199,7 +199,7 @@ const harborRoutes: Routes = [ @NgModule({ imports: [ - RouterModule.forRoot(harborRoutes) + RouterModule.forRoot(harborRoutes, {onSameUrlNavigation: 'reload'}) ], exports: [RouterModule] }) From ae061482aef64f6f2c05c5ac616191d17f20dbb6 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Wed, 23 Jan 2019 14:32:37 +0800 Subject: [PATCH 09/45] Add Can method to securty.Context interface (#6779) * Add Can method to securty.Context interface Signed-off-by: He Weiwei * Improve mockSecurityContext Can method Signed-off-by: He Weiwei --- src/common/security/context.go | 3 ++ src/core/api/chart_repository_test.go | 48 +++++++++++++++------------ src/core/service/token/token_test.go | 4 +++ 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/common/security/context.go b/src/common/security/context.go index 7a6ea56f04..d1b9af92bd 100644 --- a/src/common/security/context.go +++ b/src/common/security/context.go @@ -16,6 +16,7 @@ package security import ( "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" ) // Context abstracts the operations related with authN and authZ @@ -38,4 +39,6 @@ type Context interface { GetMyProjects() ([]*models.Project, error) // Get user's role in provided project GetProjectRoles(projectIDOrName interface{}) []int + // Can returns whether the user can do action on resource + Can(action rbac.Action, resource rbac.Resource) bool } diff --git a/src/core/api/chart_repository_test.go b/src/core/api/chart_repository_test.go index 030fa85a83..6d9be17b18 100644 --- a/src/core/api/chart_repository_test.go +++ b/src/core/api/chart_repository_test.go @@ -8,6 +8,8 @@ import ( "github.com/goharbor/harbor/src/chartserver" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/core/promgr/metamgr" ) @@ -311,32 +313,12 @@ func (msc *mockSecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - if projectIDOrName == nil { - return false - } - - if ns, ok := projectIDOrName.(string); ok { - if ns == "library" { - return true - } - } - - return false + return msc.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceImage)) } // HasWritePerm returns whether the user has write permission to the project func (msc *mockSecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - if projectIDOrName == nil { - return false - } - - if ns, ok := projectIDOrName.(string); ok { - if ns == "library" { - return true - } - } - - return false + return msc.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceImage)) } // HasAllPerm returns whether the user has all permissions to the project @@ -344,6 +326,28 @@ func (msc *mockSecurityContext) HasAllPerm(projectIDOrName interface{}) bool { return msc.HasReadPerm(projectIDOrName) && msc.HasWritePerm(projectIDOrName) } +// Can returns whether the user can do action on resource +func (msc *mockSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { + namespace, err := resource.GetNamespace() + if err != nil || namespace.Kind() != "project" { + return false + } + + projectIDOrName := namespace.Identity() + + if projectIDOrName == nil { + return false + } + + if ns, ok := projectIDOrName.(string); ok { + if ns == "library" { + return true + } + } + + return false +} + // Get current user's all project func (msc *mockSecurityContext) GetMyProjects() ([]*models.Project, error) { return []*models.Project{{ProjectID: 0, Name: "library"}}, nil diff --git a/src/core/service/token/token_test.go b/src/core/service/token/token_test.go index 5c0ca8f18b..3869eb1050 100644 --- a/src/core/service/token/token_test.go +++ b/src/core/service/token/token_test.go @@ -30,6 +30,7 @@ import ( "testing" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/core/config" ) @@ -260,6 +261,9 @@ func (f *fakeSecurityContext) HasWritePerm(projectIDOrName interface{}) bool { func (f *fakeSecurityContext) HasAllPerm(projectIDOrName interface{}) bool { return false } +func (f *fakeSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { + return false +} func (f *fakeSecurityContext) GetMyProjects() ([]*models.Project, error) { return nil, nil } From 3f8e06a8bc3c8414e2c74dca8383dbc58e1f13c0 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Wed, 23 Jan 2019 14:56:23 +0800 Subject: [PATCH 10/45] Support master role for project member create and update apis (#6780) * Support master role for project member create and update apis Signed-off-by: He Weiwei * Fix description for role_id in swagger.yaml Signed-off-by: He Weiwei --- docs/swagger.yaml | 4 ++-- .../postgresql/0005_add_master_role.up.sql | 1 + src/common/const.go | 1 + src/common/dao/project.go | 5 +++-- src/common/security/local/context.go | 2 ++ src/core/api/projectmember.go | 4 ++-- src/core/api/projectmember_test.go | 12 ++++++++++++ 7 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 make/migrations/postgresql/0005_add_master_role.up.sql diff --git a/docs/swagger.yaml b/docs/swagger.yaml index eef1cce21a..a8b7ddcb2a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -4182,7 +4182,7 @@ definitions: properties: role_id: type: integer - description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest' + description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for master' member_user: $ref: '#/definitions/UserEntity' member_group: @@ -4192,7 +4192,7 @@ definitions: properties: role_id: type: integer - description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest' + description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for master' UserEntity: type: object properties: diff --git a/make/migrations/postgresql/0005_add_master_role.up.sql b/make/migrations/postgresql/0005_add_master_role.up.sql new file mode 100644 index 0000000000..d24d3f5e68 --- /dev/null +++ b/make/migrations/postgresql/0005_add_master_role.up.sql @@ -0,0 +1 @@ +INSERT INTO role (role_code, name) VALUES ('DRWS', 'master'); \ No newline at end of file diff --git a/src/common/const.go b/src/common/const.go index 5dc0325e14..d6ecda41bb 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -28,6 +28,7 @@ const ( RoleProjectAdmin = 1 RoleDeveloper = 2 RoleGuest = 3 + RoleMaster = 4 LabelLevelSystem = "s" LabelLevelUser = "u" diff --git a/src/common/dao/project.go b/src/common/dao/project.go index 80751a35fe..423b6b23bc 100644 --- a/src/common/dao/project.go +++ b/src/common/dao/project.go @@ -249,7 +249,8 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac roleID = 2 case common.RoleGuest: roleID = 3 - + case common.RoleMaster: + roleID = 4 } params = append(params, roleID) } @@ -299,7 +300,7 @@ func GetRolesByLDAPGroup(projectID int64, groupDNCondition string) ([]int, error } o := GetOrmer() // Because an LDAP user can be memberof multiple groups, - // the role is in descent order (1-admin, 2-developer, 3-guest), use min to select the max privilege role. + // the role is in descent order (1-admin, 2-developer, 3-guest, 4-master), use min to select the max privilege role. sql := fmt.Sprintf( `select min(pm.role) from project_member pm left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index e7c2bc5571..ab4d11f4ab 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -138,6 +138,8 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { switch role.RoleCode { case "MDRWS": roles = append(roles, common.RoleProjectAdmin) + case "DRWS": + roles = append(roles, common.RoleMaster) case "RWS": roles = append(roles, common.RoleDeveloper) case "RS": diff --git a/src/core/api/projectmember.go b/src/core/api/projectmember.go index 6dfef750a9..d94ae1e0f1 100644 --- a/src/core/api/projectmember.go +++ b/src/core/api/projectmember.go @@ -160,7 +160,7 @@ func (pma *ProjectMemberAPI) Put() { pmID := pma.id var req models.Member pma.DecodeJSONReq(&req) - if req.Role < 1 || req.Role > 3 { + if req.Role < 1 || req.Role > 4 { pma.HandleBadRequest(fmt.Sprintf("Invalid role id %v", req.Role)) return } @@ -226,7 +226,7 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) { return 0, ErrDuplicateProjectMember } - if member.Role < 1 || member.Role > 3 { + if member.Role < 1 || member.Role > 4 { // Return invalid role error return 0, ErrInvalidRole } diff --git a/src/core/api/projectmember_test.go b/src/core/api/projectmember_test.go index 8de569c10b..e440ce0e92 100644 --- a/src/core/api/projectmember_test.go +++ b/src/core/api/projectmember_test.go @@ -209,6 +209,18 @@ func TestProjectMemberAPI_PutAndDelete(t *testing.T) { }, code: http.StatusOK, }, + // 200 + { + request: &testingRequest{ + method: http.MethodPut, + url: URL, + bodyJSON: &models.Member{ + Role: 4, + }, + credential: admin, + }, + code: http.StatusOK, + }, // 400 { request: &testingRequest{ From 257eebfebe5a3926ca11fbd501d8450d426ca57a Mon Sep 17 00:00:00 2001 From: danfengliu Date: Thu, 24 Jan 2019 09:38:46 +0800 Subject: [PATCH 11/45] Add space adaptor for regexp when create a new project, we loose the criteria to match the project name because the return json datat may be shown in different lines . (#6768) Signed-off-by: danfengliu --- tests/resources/Harbor-Pages/Project.robot | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/resources/Harbor-Pages/Project.robot b/tests/resources/Harbor-Pages/Project.robot index b359ac9ad7..51a431528c 100644 --- a/tests/resources/Harbor-Pages/Project.robot +++ b/tests/resources/Harbor-Pages/Project.robot @@ -34,10 +34,12 @@ Create An New Project ${element}= Set Variable xpath=//button[contains(.,'OK')] Wait Until Element Is Visible And Enabled ${element} Click Element ${element} + #Try to get Project Infomation 5 times at most and sleep 1 second each time if we fail to get it. ${found_project}= Set Variable ${false} :For ${n} IN RANGE 1 5 \ ${rc} ${output}= Run And Return Rc And Output curl -u ${HARBOR_ADMIN}:${HARBOR_PASSWORD} -k -X GET --header 'Accept: application/json' ${HARBOR_URL}/api/projects?name=${projectname} - \ ${match} ${regexp_project_name} Should Match Regexp ${output} ,\"name\":\"(\\w+)\",\"creation_time\": + \ Log To Console ${output} + \ ${match} ${regexp_project_name} Should Match Regexp ${output} \"name\"\\s*:\\s*\"(\\w+)\"\\s*, \ ${found_project} Set Variable If '${rc}' == '0' and '${regexp_project_name}' == '${projectname}' ${true} \ Run Keyword If ${found_project} == ${true} Exit For Loop \ Sleep 1 From b0c1adab8dea4817b065fdb40e0e2e0212eff47d Mon Sep 17 00:00:00 2001 From: danfengliu Date: Thu, 24 Jan 2019 17:09:32 +0800 Subject: [PATCH 12/45] add retry for make swagger client (#6813) Signed-off-by: danfengliu --- tests/resources/APITest-Util.robot | 8 ++++++-- tests/resources/Util.robot | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/resources/APITest-Util.robot b/tests/resources/APITest-Util.robot index 8055b4ab0c..c7ac739f62 100644 --- a/tests/resources/APITest-Util.robot +++ b/tests/resources/APITest-Util.robot @@ -1,8 +1,12 @@ *** Keywords *** -Setup API Test +Make Swagger Client ${rc} ${output}= Run And Return Rc And Output make swagger_client Log ${output} - Should Be Equal As Integers ${rc} 0 + [Return] ${rc} + +Setup API Test + Retry Keyword When Error Make Swagger Client + Harbor API Test [Arguments] ${testcase_name} ${current_dir}= Run pwd diff --git a/tests/resources/Util.robot b/tests/resources/Util.robot index bfb2475f62..7384cdcd08 100644 --- a/tests/resources/Util.robot +++ b/tests/resources/Util.robot @@ -78,3 +78,13 @@ Wait Unitl Vul Data Ready \ Exit For Loop If ${contains} \ Sleep ${interval} Run Keyword If ${i+1}==${n} Fail The vul data is not ready + +Retry Keyword When Error + [Arguments] ${keyword} ${times}=6 + :For ${n} IN RANGE 1 ${times} + \ Log To Console Attampt to ${keyword} ${n} times ... + \ ${out} Run Keyword And Ignore Error ${keyword} + \ Log To Console Return value is ${out} + \ Exit For Loop If '${out[0]}'=='PASS' + \ Sleep 3 + Should Be Equal As Strings '${out[0]}' 'PASS' \ No newline at end of file From 923432a17231cc1a6635f152a77b5fa8b1c4e04e Mon Sep 17 00:00:00 2001 From: danfengliu Date: Fri, 25 Jan 2019 10:26:14 +0800 Subject: [PATCH 13/45] resort test case, 2 test cases failed due to unfinish chart data preparation, so resort the test cases, move the verification test case ahead of the 2 failed cases. (#6818) Signed-off-by: danfengliu --- tests/robot-cases/Group1-Nightly/Common.robot | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/tests/robot-cases/Group1-Nightly/Common.robot b/tests/robot-cases/Group1-Nightly/Common.robot index fcd8b4077a..4b2b28ae47 100644 --- a/tests/robot-cases/Group1-Nightly/Common.robot +++ b/tests/robot-cases/Group1-Nightly/Common.robot @@ -593,20 +593,6 @@ Test Case - Manual Scan All Summary Chart Should Display latest Close Browser -Test Case - View Scan Results - Init Chrome Driver - ${d}= get current date result_format=%m%s - - Sign In Harbor ${HARBOR_URL} user025 Test1@34 - Create An New Project project${d} - Push Image ${ip} user025 Test1@34 project${d} tomcat - Go Into Project project${d} - Go Into Repo project${d}/tomcat - Scan Repo latest Succeed - Summary Chart Should Display latest - View Repo Scan Details - Close Browser - Test Case - View Scan Error Init Chrome Driver ${d}= get current date result_format=%m%s @@ -620,21 +606,6 @@ Test Case - View Scan Error View Scan Error Log Close Browser -Test Case - Project Level Image Serverity Policy - Init Chrome Driver - Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} - ${d}= get current date result_format=%m%s - Create An New Project project${d} - Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} haproxy - Go Into Project project${d} - Go Into Repo haproxy - Scan Repo latest Succeed - Back To Projects - Go Into Project project${d} - Set Vulnerabilty Serverity 0 - Cannot pull image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} haproxy - Close Browser - Test Case - List Helm Charts Init Chrome Driver ${d}= Get Current Date result_format=%m%s @@ -683,20 +654,6 @@ Test Case - Admin Push Signed Image Should Be Equal As Integers ${rc} 0 Should Contain ${output} sha256 -Test Case - Scan Image On Push - Wait Unitl Vul Data Ready ${HARBOR_URL} 7200 30 - Init Chrome Driver - Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} - Go Into Project library - Goto Project Config - Enable Scan On Push - Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library memcached - Back To Projects - Go Into Project library - Go Into Repo memcached - Summary Chart Should Display latest - Close Browser - Test Case - Retag A Image Tag Init Chrome Driver ${random_num1}= Get Current Date result_format=%m%s @@ -722,3 +679,46 @@ Test Case - Retag A Image Tag Sleep 1 Page Should Contain Element xpath=${tag_value_xpath} Close Browser + +Test Case - Scan Image On Push + Wait Unitl Vul Data Ready ${HARBOR_URL} 7200 30 + Init Chrome Driver + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + Go Into Project library + Goto Project Config + Enable Scan On Push + Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library memcached + Back To Projects + Go Into Project library + Go Into Repo memcached + Summary Chart Should Display latest + Close Browser + +Test Case - View Scan Results + Init Chrome Driver + ${d}= get current date result_format=%m%s + + Sign In Harbor ${HARBOR_URL} user025 Test1@34 + Create An New Project project${d} + Push Image ${ip} user025 Test1@34 project${d} tomcat + Go Into Project project${d} + Go Into Repo project${d}/tomcat + Scan Repo latest Succeed + Summary Chart Should Display latest + View Repo Scan Details + Close Browser + +Test Case - Project Level Image Serverity Policy + Init Chrome Driver + Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} + ${d}= get current date result_format=%m%s + Create An New Project project${d} + Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} haproxy + Go Into Project project${d} + Go Into Repo haproxy + Scan Repo latest Succeed + Back To Projects + Go Into Project project${d} + Set Vulnerabilty Serverity 0 + Cannot pull image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} haproxy + Close Browser From ebda1cda2290f7daa783c49f19134221b36dc0b7 Mon Sep 17 00:00:00 2001 From: Yogi_Wang Date: Fri, 18 Jan 2019 17:50:34 +0800 Subject: [PATCH 14/45] style_change Signed-off-by: Yogi_Wang --- .../src/helm-chart/helm-chart.component.scss | 1 + src/portal/lib/src/label/label.component.scss | 1 - .../list-replication-rule.component.html | 4 +-- .../replication/replication.component.scss | 2 +- src/portal/lib/src/tag/tag.component.scss | 3 +- .../src/app/log/audit-log.component.html | 2 +- src/portal/src/app/log/audit-log.component.ts | 32 +++++++++++-------- .../member/add-group/add-group.component.scss | 4 ++- 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/portal/lib/src/helm-chart/helm-chart.component.scss b/src/portal/lib/src/helm-chart/helm-chart.component.scss index b03ebe643e..cbbb50b7ed 100644 --- a/src/portal/lib/src/helm-chart/helm-chart.component.scss +++ b/src/portal/lib/src/helm-chart/helm-chart.component.scss @@ -13,6 +13,7 @@ $size60:60px; overflow: hidden; .rightPos { @include grid-left-top-pos; + margin-top: 20px; .filter-divider { display: inline-block; height: 16px; diff --git a/src/portal/lib/src/label/label.component.scss b/src/portal/lib/src/label/label.component.scss index c798ac5c47..f1ec7bb476 100644 --- a/src/portal/lib/src/label/label.component.scss +++ b/src/portal/lib/src/label/label.component.scss @@ -7,7 +7,6 @@ position: absolute; z-index: 100; right: 35px; - margin-top: 14px; height: 24px; .option-right { padding-right: 16px; diff --git a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html index 479c5cc52e..88d5a28cdc 100644 --- a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html +++ b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html @@ -1,5 +1,5 @@
- + @@ -13,7 +13,7 @@ {{'REPLICATION.DESTINATION_NAME' | translate}} {{'REPLICATION.TRIGGER_MODE' | translate}} {{'REPLICATION.PLACEHOLDER' | translate }} - + {{p.name}}
diff --git a/src/portal/lib/src/replication/replication.component.scss b/src/portal/lib/src/replication/replication.component.scss index c7664c936a..16104b61f0 100644 --- a/src/portal/lib/src/replication/replication.component.scss +++ b/src/portal/lib/src/replication/replication.component.scss @@ -16,7 +16,7 @@ .rightPos{ position: absolute; right: 35px; - margin-top: 15px; + margin-top: 20px; z-index: 100; height: 32px; } diff --git a/src/portal/lib/src/tag/tag.component.scss b/src/portal/lib/src/tag/tag.component.scss index b4e75260c1..4234bfa1b7 100644 --- a/src/portal/lib/src/tag/tag.component.scss +++ b/src/portal/lib/src/tag/tag.component.scss @@ -65,7 +65,8 @@ position: relative; padding-left: .5rem; padding-right: .5rem; - line-height: 1.0 + line-height: 1.0; + height: 1.2rem; } .dropdown-menu input { diff --git a/src/portal/src/app/log/audit-log.component.html b/src/portal/src/app/log/audit-log.component.html index b8cee16bc8..f891de38fc 100644 --- a/src/portal/src/app/log/audit-log.component.html +++ b/src/portal/src/app/log/audit-log.component.html @@ -30,7 +30,7 @@
- + {{'AUDIT_LOG.USERNAME' | translate}} {{'AUDIT_LOG.REPOSITORY_NAME' | translate}} {{'AUDIT_LOG.TAGS' | translate}} diff --git a/src/portal/src/app/log/audit-log.component.ts b/src/portal/src/app/log/audit-log.component.ts index 17e567cb0d..6b08170240 100644 --- a/src/portal/src/app/log/audit-log.component.ts +++ b/src/portal/src/app/log/audit-log.component.ts @@ -95,31 +95,35 @@ export class AuditLogComponent implements OnInit { } - retrieve(state?: State): void { - if (state) { - this.queryParam.page = Math.ceil((state.page.to + 1) / this.pageSize); - this.currentPage = this.queryParam.page; - } + private retrieve(): void { this.auditLogService .listAuditLogs(this.queryParam) .subscribe( - response => { - this.totalRecordCount = Number.parseInt(response.headers.get('x-total-count')); - this.auditLogs = response.json(); - }, - error => { - this.router.navigate(['/harbor', 'projects']); - this.messageHandlerService.handleError(error); - } + response => { + this.totalRecordCount = Number.parseInt(response.headers.get('x-total-count')); + this.auditLogs = response.json(); + }, + error => { + this.router.navigate(['/harbor', 'projects']); + this.messageHandlerService.handleError(error); + } ); } + retrievePage(state: State) { + if (state && state.page) { + this.queryParam.page = Math.ceil((state.page.to + 1) / this.pageSize); + this.currentPage = this.queryParam.page; + this.retrieve(); + } + } + doSearchAuditLogs(searchUsername: string): void { this.queryParam.username = searchUsername; this.retrieve(); } -doSearchByStartTime(fromTimestamp: string): void { + doSearchByStartTime(fromTimestamp: string): void { this.queryParam.begin_timestamp = fromTimestamp; this.retrieve(); } diff --git a/src/portal/src/app/project/member/add-group/add-group.component.scss b/src/portal/src/app/project/member/add-group/add-group.component.scss index 1d9d5d98be..575e651df6 100644 --- a/src/portal/src/app/project/member/add-group/add-group.component.scss +++ b/src/portal/src/app/project/member/add-group/add-group.component.scss @@ -7,7 +7,9 @@ clr-datagrid { .row { margin-top: 12px; } - +.flex-items-xs-between{ + display: flex; +} .modeSelectradios { margin-top: 21px; } From fe5820efee258d426186dcbbcf6eee38757c01e8 Mon Sep 17 00:00:00 2001 From: FangyuanCheng Date: Sun, 27 Jan 2019 23:24:51 +0800 Subject: [PATCH 15/45] The tag column sets the minimum width Signed-off-by: FangyuanCheng --- src/portal/lib/src/tag/tag.component.html | 36 +++++++++++------------ src/portal/lib/src/tag/tag.component.scss | 20 ++----------- 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/src/portal/lib/src/tag/tag.component.html b/src/portal/lib/src/tag/tag.component.html index 5cb4099765..10a450d548 100644 --- a/src/portal/lib/src/tag/tag.component.html +++ b/src/portal/lib/src/tag/tag.component.html @@ -78,29 +78,29 @@ - {{'REPOSITORY.TAG' | translate}} - {{'REPOSITORY.SIZE' | translate}} - {{'REPOSITORY.PULL_COMMAND' | translate}} - {{'REPOSITORY.VULNERABILITY' | translate}} - {{'REPOSITORY.SIGNED' | translate}} - {{'REPOSITORY.AUTHOR' | translate}} - {{'REPOSITORY.CREATED' | translate}} - {{'REPOSITORY.DOCKER_VERSION' | translate}} - {{'REPOSITORY.LABELS' | translate}} + {{'REPOSITORY.TAG' | translate}} + {{'REPOSITORY.SIZE' | translate}} + {{'REPOSITORY.PULL_COMMAND' | translate}} + {{'REPOSITORY.VULNERABILITY' | translate}} + {{'REPOSITORY.SIGNED' | translate}} + {{'REPOSITORY.AUTHOR' | translate}} + {{'REPOSITORY.CREATED' | translate}} + {{'REPOSITORY.DOCKER_VERSION' | translate}} + {{'REPOSITORY.LABELS' | translate}} {{'TAG.PLACEHOLDER' | translate }} - + {{t.name}} {{t.name}} - {{sizeTransform(t.size)}} - + {{sizeTransform(t.size)}} + - + - + @@ -108,10 +108,10 @@ {{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}} - {{t.author}} - {{t.created | date: 'short'}} - {{t.docker_version}} - + {{t.author}} + {{t.created | date: 'short'}} + {{t.docker_version}} +
diff --git a/src/portal/lib/src/tag/tag.component.scss b/src/portal/lib/src/tag/tag.component.scss index b4e75260c1..67da43d413 100644 --- a/src/portal/lib/src/tag/tag.component.scss +++ b/src/portal/lib/src/tag/tag.component.scss @@ -227,25 +227,9 @@ hbr-image-name-input { position: relative; } -.width-100 { - width: 100px; -} - -.width-130 { - width: 130px; -} - -.width-160 { - width: 160px; -} - .datagrid-top { - .flex-width { - min-width: 100px; - max-width: 220px; - } - .flex-min-width { - min-width: 130px; + .flex-max-width { + max-width: 220px } } From 20db0e737b5703cc69af50f08dd9d7a91eb69185 Mon Sep 17 00:00:00 2001 From: Daniel Jiang Date: Fri, 11 Jan 2019 18:16:50 +0800 Subject: [PATCH 16/45] Provide HTTP authenticator An HTTP authenticator verifies the credentials by sending a POST request to an HTTP endpoint. After successful authentication he will be onboarded to Harbor's local DB and assigned a role in a project. This commit provides the initial implementation. Currently one limitation is that we don't have clear definition about how we would "search" a user via this HTTP authenticator, a flag for "alway onboard" is provided to skip the search, otherwise, a user has to login first before he can be assigned a role in Harbor. Signed-off-by: Daniel Jiang --- src/common/const.go | 1 + src/core/auth/authenticator.go | 2 +- src/core/auth/authproxy/auth.go | 143 ++++++++++++++++++++++++ src/core/auth/authproxy/auth_test.go | 144 +++++++++++++++++++++++++ src/core/auth/authproxy/test/server.go | 49 +++++++++ src/core/auth/uaa/uaa.go | 2 +- src/core/main.go | 1 + 7 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 src/core/auth/authproxy/auth.go create mode 100644 src/core/auth/authproxy/auth_test.go create mode 100644 src/core/auth/authproxy/test/server.go diff --git a/src/common/const.go b/src/common/const.go index d6ecda41bb..8bb1297d68 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -19,6 +19,7 @@ const ( DBAuth = "db_auth" LDAPAuth = "ldap_auth" UAAAuth = "uaa_auth" + HTTPAuth = "http_auth" ProCrtRestrEveryone = "everyone" ProCrtRestrAdmOnly = "adminonly" LDAPScopeBase = 0 diff --git a/src/core/auth/authenticator.go b/src/core/auth/authenticator.go index 83393f0c02..48641b37ba 100644 --- a/src/core/auth/authenticator.go +++ b/src/core/auth/authenticator.go @@ -123,7 +123,7 @@ func Register(name string, h AuthenticateHelper) { return } registry[name] = h - log.Debugf("Registered authencation helper for auth mode: %s", name) + log.Debugf("Registered authentication helper for auth mode: %s", name) } // Login authenticates user credentials based on setting. diff --git a/src/core/auth/authproxy/auth.go b/src/core/auth/authproxy/auth.go new file mode 100644 index 0000000000..a87e567d70 --- /dev/null +++ b/src/core/auth/authproxy/auth.go @@ -0,0 +1,143 @@ +// Copyright Project Harbor Authors +// +// 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 authproxy + +import ( + "crypto/tls" + "fmt" + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/auth" + "io/ioutil" + "net/http" + "os" + "strings" + "sync" +) + +// Auth implements HTTP authenticator the required attributes. +// The attribute Endpoint is the HTTP endpoint to which the POST request should be issued for authentication +type Auth struct { + auth.DefaultAuthenticateHelper + sync.Mutex + Endpoint string + SkipCertVerify bool + AlwaysOnboard bool + client *http.Client +} + +// Authenticate issues http POST request to Endpoint if it returns 200 the authentication is considered success. +func (a *Auth) Authenticate(m models.AuthModel) (*models.User, error) { + a.ensure() + req, err := http.NewRequest(http.MethodPost, a.Endpoint, nil) + if err != nil { + return nil, fmt.Errorf("failed to send request, error: %v", err) + } + req.SetBasicAuth(m.Principal, m.Password) + resp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return &models.User{Username: m.Principal}, nil + } else if resp.StatusCode == http.StatusUnauthorized { + return nil, auth.ErrAuth{} + } else { + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Warningf("Failed to read response body, error: %v", err) + } + return nil, fmt.Errorf("failed to authenticate, status code: %d, text: %s", resp.StatusCode, string(data)) + } + +} + +// OnBoardUser delegates to dao pkg to insert/update data in DB. +func (a *Auth) OnBoardUser(u *models.User) error { + return dao.OnBoardUser(u) +} + +// PostAuthenticate generates the user model and on board the user. +func (a *Auth) PostAuthenticate(u *models.User) error { + if res, _ := dao.GetUser(*u); res != nil { + return nil + } + if err := a.fillInModel(u); err != nil { + return err + } + return a.OnBoardUser(u) +} + +// SearchUser - TODO: Remove this workaround when #6767 is fixed. +// When the flag is set it always return the default model without searching +func (a *Auth) SearchUser(username string) (*models.User, error) { + a.ensure() + var queryCondition = models.User{ + Username: username, + } + u, err := dao.GetUser(queryCondition) + if err != nil { + return nil, err + } + if a.AlwaysOnboard && u == nil { + u = &models.User{Username: username} + if err := a.fillInModel(u); err != nil { + return nil, err + } + } + return u, nil +} + +func (a *Auth) fillInModel(u *models.User) error { + if strings.TrimSpace(u.Username) == "" { + return fmt.Errorf("username cannot be empty") + } + u.Realname = u.Username + u.Password = "1234567ab" + u.Comment = "By Authproxy" + if strings.Contains(u.Username, "@") { + u.Email = u.Username + } else { + u.Email = fmt.Sprintf("%s@placeholder.com", u.Username) + } + return nil +} + +func (a *Auth) ensure() { + a.Lock() + defer a.Unlock() + if a.Endpoint == "" { + a.Endpoint = os.Getenv("AUTHPROXY_ENDPOINT") + a.SkipCertVerify = strings.EqualFold(os.Getenv("AUTHPROXY_SKIP_CERT_VERIFY"), "true") + a.AlwaysOnboard = strings.EqualFold(os.Getenv("AUTHPROXY_ALWAYS_ONBOARD"), "true") + } + if a.client == nil { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: a.SkipCertVerify, + }, + } + a.client = &http.Client{ + Transport: tr, + } + } +} + +func init() { + auth.Register(common.HTTPAuth, &Auth{}) +} diff --git a/src/core/auth/authproxy/auth_test.go b/src/core/auth/authproxy/auth_test.go new file mode 100644 index 0000000000..9c0c81cbd6 --- /dev/null +++ b/src/core/auth/authproxy/auth_test.go @@ -0,0 +1,144 @@ +// Copyright Project Harbor Authors +// +// 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 authproxy + +import ( + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/core/auth" + "github.com/goharbor/harbor/src/core/auth/authproxy/test" + "github.com/stretchr/testify/assert" + "net/http/httptest" + "os" + "testing" +) + +var mockSvr *httptest.Server +var a *Auth +var pwd = "1234567ab" +var cmt = "By Authproxy" + +func TestMain(m *testing.M) { + mockSvr = test.NewMockServer(map[string]string{"jt": "pp", "Admin@vsphere.local": "Admin!23"}) + defer mockSvr.Close() + a = &Auth{ + Endpoint: mockSvr.URL + "/test/login", + SkipCertVerify: true, + } + rc := m.Run() + if rc != 0 { + os.Exit(rc) + } +} + +func TestAuth_Authenticate(t *testing.T) { + t.Log("auth endpoint: ", a.Endpoint) + type output struct { + user models.User + err error + } + type tc struct { + input models.AuthModel + expect output + } + suite := []tc{ + { + input: models.AuthModel{ + Principal: "jt", Password: "pp"}, + expect: output{ + user: models.User{ + Username: "jt", + }, + err: nil, + }, + }, + { + input: models.AuthModel{ + Principal: "Admin@vsphere.local", + Password: "Admin!23", + }, + expect: output{ + user: models.User{ + Username: "Admin@vsphere.local", + // Email: "Admin@placeholder.com", + // Password: pwd, + // Comment: fmt.Sprintf(cmtTmpl, path.Join(mockSvr.URL, "/test/login")), + }, + err: nil, + }, + }, + { + input: models.AuthModel{ + Principal: "jt", + Password: "ppp", + }, + expect: output{ + err: auth.ErrAuth{}, + }, + }, + } + assert := assert.New(t) + for _, c := range suite { + r, e := a.Authenticate(c.input) + if c.expect.err == nil { + assert.Nil(e) + assert.Equal(c.expect.user, *r) + } else { + assert.Nil(r) + assert.NotNil(e) + if _, ok := e.(auth.ErrAuth); ok { + assert.IsType(auth.ErrAuth{}, e) + } + } + } +} + +/* TODO: Enable this case after adminserver refactor is merged. +func TestAuth_PostAuthenticate(t *testing.T) { + type tc struct { + input *models.User + expect models.User + } + suite := []tc{ + { + input: &models.User{ + Username: "jt", + }, + expect: models.User{ + Username: "jt", + Email: "jt@placeholder.com", + Realname: "jt", + Password: pwd, + Comment: fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"), + }, + }, + { + input: &models.User{ + Username: "Admin@vsphere.local", + }, + expect: models.User{ + Username: "Admin@vsphere.local", + Email: "jt@placeholder.com", + Realname: "Admin@vsphere.local", + Password: pwd, + Comment: fmt.Sprintf(cmtTmpl, mockSvr.URL+"/test/login"), + }, + }, + } + for _, c := range suite { + a.PostAuthenticate(c.input) + assert.Equal(t, c.expect, *c.input) + } +} +*/ diff --git a/src/core/auth/authproxy/test/server.go b/src/core/auth/authproxy/test/server.go new file mode 100644 index 0000000000..b11ec17aa7 --- /dev/null +++ b/src/core/auth/authproxy/test/server.go @@ -0,0 +1,49 @@ +// Copyright Project Harbor Authors +// +// 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 test + +import ( + "net/http" + "net/http/httptest" +) + +type authHandler struct { + m map[string]string +} + +// ServeHTTP handles HTTP requests +func (ah *authHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, "", http.StatusMethodNotAllowed) + } + if u, p, ok := req.BasicAuth(); !ok { + // Simulate a service error + http.Error(rw, "", http.StatusInternalServerError) + } else if pass, ok := ah.m[u]; !ok || pass != p { + http.Error(rw, "", http.StatusUnauthorized) + } else { + _, e := rw.Write([]byte(`{"session_id": "hgx59wuWI3b0jcbtidv5mU1YCp-DOQ9NKR1iYKACdKCvbVn7"}`)) + if e != nil { + panic(e) + } + } +} + +// NewMockServer creates the mock server for testing +func NewMockServer(creds map[string]string) *httptest.Server { + mux := http.NewServeMux() + mux.Handle("/test/login", &authHandler{m: creds}) + return httptest.NewTLSServer(mux) +} diff --git a/src/core/auth/uaa/uaa.go b/src/core/auth/uaa/uaa.go index 0b3bb92439..b4889302c5 100644 --- a/src/core/auth/uaa/uaa.go +++ b/src/core/auth/uaa/uaa.go @@ -63,7 +63,7 @@ func (u *Auth) Authenticate(m models.AuthModel) (*models.User, error) { func (u *Auth) OnBoardUser(user *models.User) error { user.Username = strings.TrimSpace(user.Username) if len(user.Username) == 0 { - return fmt.Errorf("The Username is empty") + return fmt.Errorf("the Username is empty") } if len(user.Password) == 0 { user.Password = "1234567ab" diff --git a/src/core/main.go b/src/core/main.go index a348786683..b9c2dea666 100644 --- a/src/core/main.go +++ b/src/core/main.go @@ -28,6 +28,7 @@ import ( "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/api" + _ "github.com/goharbor/harbor/src/core/auth/authproxy" _ "github.com/goharbor/harbor/src/core/auth/db" _ "github.com/goharbor/harbor/src/core/auth/ldap" _ "github.com/goharbor/harbor/src/core/auth/uaa" From 9aeb626fe31787a9917f5579c2a183b99c063cac Mon Sep 17 00:00:00 2001 From: danfengliu Date: Mon, 28 Jan 2019 16:32:45 +0800 Subject: [PATCH 17/45] The former way to verify project creation is by API calling, but it's not a GUI operation, which is not a goal for operator, so I replace it with GUI verification (#6816) Signed-off-by: danfengliu --- tests/resources/Harbor-Pages/Project.robot | 52 +++++++++---------- .../Harbor-Pages/Project_Elements.robot | 7 ++- tests/resources/Harbor-Pages/ToolKit.robot | 1 - tests/resources/Harbor-Pages/Verify.robot | 6 +-- tests/resources/Util.robot | 12 +++++ tests/robot-cases/Group1-Nightly/Common.robot | 19 ++++--- tests/robot-cases/Group1-Nightly/LDAP.robot | 2 +- .../robot-cases/Group1-Nightly/Nightly.robot | 8 +-- 8 files changed, 62 insertions(+), 45 deletions(-) diff --git a/tests/resources/Harbor-Pages/Project.robot b/tests/resources/Harbor-Pages/Project.robot index 51a431528c..480b04fd0d 100644 --- a/tests/resources/Harbor-Pages/Project.robot +++ b/tests/resources/Harbor-Pages/Project.robot @@ -22,28 +22,21 @@ ${HARBOR_VERSION} v1.1.1 *** Keywords *** Create An New Project [Arguments] ${projectname} ${public}=false - ${element}= Set Variable css=${create_project_button_css} - Wait Until Element Is Visible And Enabled ${element} - Click Button ${element} + Navigate To Projects + ${element_create_project_button}= Set Variable xpath=${create_project_button_xpath} + Wait Until Element Is Visible And Enabled ${element_create_project_button} + Click Button ${element_create_project_button} Log To Console Project Name: ${projectname} - ${element}= Set Variable xpath=${project_name_xpath} - Wait Until Element Is Visible And Enabled ${element} - Input Text ${element} ${projectname} - ${element}= Set Variable xpath=${project_public_xpath} - Run Keyword If '${public}' == 'true' Run Keywords Wait Until Element Is Visible And Enabled ${element} AND Click Element ${element} - ${element}= Set Variable xpath=//button[contains(.,'OK')] - Wait Until Element Is Visible And Enabled ${element} - Click Element ${element} - #Try to get Project Infomation 5 times at most and sleep 1 second each time if we fail to get it. - ${found_project}= Set Variable ${false} - :For ${n} IN RANGE 1 5 - \ ${rc} ${output}= Run And Return Rc And Output curl -u ${HARBOR_ADMIN}:${HARBOR_PASSWORD} -k -X GET --header 'Accept: application/json' ${HARBOR_URL}/api/projects?name=${projectname} - \ Log To Console ${output} - \ ${match} ${regexp_project_name} Should Match Regexp ${output} \"name\"\\s*:\\s*\"(\\w+)\"\\s*, - \ ${found_project} Set Variable If '${rc}' == '0' and '${regexp_project_name}' == '${projectname}' ${true} - \ Run Keyword If ${found_project} == ${true} Exit For Loop - \ Sleep 1 - Should Be Equal ${found_project} ${true} + ${elemen_project_name}= Set Variable xpath=${project_name_xpath} + Wait Until Element Is Visible And Enabled ${elemen_project_name} + Input Text ${elemen_project_name} ${projectname} + ${element_project_public}= Set Variable xpath=${project_public_xpath} + Run Keyword If '${public}' == 'true' Run Keywords Wait Until Element Is Visible And Enabled ${element_project_public} AND Click Element ${element_project_public} + ${element_create_project_OK_button_xpath}= Set Variable ${create_project_OK_button_xpath} + Wait Until Element Is Visible And Enabled ${element_create_project_OK_button_xpath} + Click Element ${element_create_project_OK_button_xpath} + Wait Until Page Does Not Contain Element ${create_project_CANCEL_button_xpath} + Go Into Project ${projectname} has_image=${false} Create An New Project With New User [Arguments] ${url} ${username} ${email} ${realname} ${newPassword} ${comment} ${projectname} ${public} @@ -72,7 +65,7 @@ Switch To Replication Click Element xpath=${project_replication_xpath} Sleep 1 -Back To Projects +Navigate To Projects ${element}= Set Variable xpath=${projects_xpath} Wait Until Element Is Visible And Enabled ${element} Click Element ${element} @@ -118,11 +111,15 @@ Make Project Public Delete Repo [Arguments] ${projectname} - Click Element xpath=//clr-dg-row[contains(.,"${projectname}")]//clr-checkbox-wrapper//label - Wait Until Element Is Enabled //button[contains(.,"Delete")] - Click Element xpath=//button[contains(.,"Delete")] - Wait Until Element Is Visible //clr-modal//button[2] - Click Element xpath=//clr-modal//button[2] + ${element_repo_checkbox}= Set Variable xpath=//clr-dg-row[contains(.,"${projectname}")]//clr-checkbox-wrapper//label + Retry Keyword With Element When Error Wait Until Element Is Visible And Enabled ${element_repo_checkbox} + Retry Keyword With Element When Error Click Element ${element_repo_checkbox} + ${element_delete_btn}= Set Variable xpath=//button[contains(.,"Delete")] + Retry Keyword With Element When Error Wait Until Element Is Visible And Enabled ${element_delete_btn} + Retry Keyword With Element When Error Click Element ${element_delete_btn} + ${element_delete_confirm_btn}= Set Variable xpath=//clr-modal//button[2] + Retry Keyword With Element When Error Wait Until Element Is Visible And Enabled ${element_delete_confirm_btn} + Retry Keyword With Element When Error Click Element ${element_delete_confirm_btn} Delete Repo on CardView [Arguments] ${reponame} @@ -134,6 +131,7 @@ Delete Repo on CardView Delete Project [Arguments] ${projectname} + Navigate To Projects Sleep 1 Click Element xpath=//clr-dg-row[contains(.,"${projectname}")]//clr-checkbox-wrapper//label Sleep 1 diff --git a/tests/resources/Harbor-Pages/Project_Elements.robot b/tests/resources/Harbor-Pages/Project_Elements.robot index 276ca1cfa2..5ca8032638 100644 --- a/tests/resources/Harbor-Pages/Project_Elements.robot +++ b/tests/resources/Harbor-Pages/Project_Elements.robot @@ -16,7 +16,7 @@ Documentation This resource provides any keywords related to the Harbor private registry appliance *** Variables *** -${create_project_button_css} .btn +${create_project_button_xpath} //clr-main-container//button[contains(., 'New Project')] ${project_name_xpath} //*[@id="create_project_name"] ${project_public_xpath} //input[@name='public']/..//label ${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-container.main-container div.content-container div.content-area.content-area-override project div.row div.col-lg-12.col-md-12.col-sm-12.col-xs-12 div.row.flex-items-xs-between div.option-left create-project clr-modal div.modal div.modal-dialog div.modal-content div.modal-footer button.btn.btn-primary @@ -25,3 +25,8 @@ ${projects_xpath} //clr-main-container//clr-vertical-nav//a[contains(.,'Project ${project_replication_xpath} //project-detail//a[contains(.,'Replication')] ${project_log_xpath} //project-detail//li[contains(.,'Logs')] ${project_member_xpath} //project-detail//li[contains(.,'Members')] + +${create_project_OK_button_xpath} xpath=//button[contains(.,'OK')] +${create_project_CANCEL_button_xpath} xpath=//button[contains(.,'CANCEL')] +${project_statistics_private_repository_icon} xpath=//project/div/div/div[1]/div/statistics-panel/div/div[2]/div[1]/div[2]/div[2]/statistics/div/span[1] + diff --git a/tests/resources/Harbor-Pages/ToolKit.robot b/tests/resources/Harbor-Pages/ToolKit.robot index 87da0520d5..fc71d7b48c 100644 --- a/tests/resources/Harbor-Pages/ToolKit.robot +++ b/tests/resources/Harbor-Pages/ToolKit.robot @@ -59,7 +59,6 @@ Multi-delete Object \ ${element}= Set Variable xpath=//clr-dg-row[contains(.,'${obj}')]//label \ Wait Until Element Is Visible And Enabled ${element} \ Click Element ${element} - Capture Page Screenshot ${element}= Set Variable xpath=//button[contains(.,'Delete')] Wait Until Element Is Visible And Enabled ${element} diff --git a/tests/resources/Harbor-Pages/Verify.robot b/tests/resources/Harbor-Pages/Verify.robot index 99625844cc..0690abb203 100644 --- a/tests/resources/Harbor-Pages/Verify.robot +++ b/tests/resources/Harbor-Pages/Verify.robot @@ -40,7 +40,7 @@ Verify Image Tag \ Go Into Project ${project} has_image=${has_image} \ @{repo}= Get Value From Json ${json} $.projects[?(@name=${project})]..repo..name \ Loop Image Repo @{repo} - \ Back To Projects + \ Navigate To Projects Close Browser Loop Image Repo @@ -60,7 +60,7 @@ Verify Member Exist \ Switch To Member \ @{members}= Get Value From Json ${json} $.projects[?(@name=${project})].member..name \ Loop Member @{members} - \ Back To Projects + \ Navigate To Projects Close Browser Loop Member @@ -102,7 +102,7 @@ Verify Project Label \ @{projectlabel}= Get Value From Json ${json} $.projects[?(@.name=${project})]..labels..name \ :For ${label} In @{label} \ \ Page Should Contain ${projectlabel} - \ Back To Projects + \ Navigate To Projects Close Browser Verify Endpoint diff --git a/tests/resources/Util.robot b/tests/resources/Util.robot index 7384cdcd08..7b6ae01e24 100644 --- a/tests/resources/Util.robot +++ b/tests/resources/Util.robot @@ -87,4 +87,16 @@ Retry Keyword When Error \ Log To Console Return value is ${out} \ Exit For Loop If '${out[0]}'=='PASS' \ Sleep 3 + Should Be Equal As Strings '${out[0]}' 'PASS' + +Retry Keyword With Element When Error + [Arguments] ${keyword} ${element} ${times}=6 + #To prevent waiting for a fixed-period of time for page loading and failure caused by exception, we add loop to re-run when + # exception was caught. + :For ${n} IN RANGE 1 ${times} + \ Log To Console Attampt to wait for ${n} times ... + \ ${out} Run Keyword And Ignore Error ${keyword} ${element} + \ Log To Console Return value is ${out} + \ Exit For Loop If '${out[0]}'=='PASS' + \ Sleep 2 Should Be Equal As Strings '${out[0]}' 'PASS' \ No newline at end of file diff --git a/tests/robot-cases/Group1-Nightly/Common.robot b/tests/robot-cases/Group1-Nightly/Common.robot index 4b2b28ae47..b8271da85a 100644 --- a/tests/robot-cases/Group1-Nightly/Common.robot +++ b/tests/robot-cases/Group1-Nightly/Common.robot @@ -83,7 +83,7 @@ Test Case - Delete A Project Project Should Not Be Deleted project${d} Go Into Project project${d} Delete Repo project${d} - Back To Projects + Navigate To Projects Project Should Be Deleted project${d} Close Browser @@ -115,7 +115,8 @@ Test Case - Staticsinfo Init Chrome Driver ${d}= Get Current Date result_format=%m%s Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} - Wait Until Element Is Visible //project/div/div/div[1]/div/statistics-panel/div/div[2]/div[1]/div[2]/div[2]/statistics/div/span[1] + ${element}= Set Variable ${project_statistics_private_repository_icon} + Wait Until Element Is Visible ${element} ${privaterepocount1}= Get Statics Private Repo ${privateprojcount1}= Get Statics Private Project ${publicrepocount1}= Get Statics Public Repo @@ -133,9 +134,10 @@ Test Case - Staticsinfo ${publicrepocount}= evaluate ${publicrepocount1}+1 ${totalrepocount}= evaluate ${totalrepocount1}+2 ${totalprojcount}= evaluate ${totalprojcount1}+2 - Wait Until Element Is Visible //project/div/div/div[1]/div/statistics-panel/div/div[2]/div[1]/div[2]/div[2]/statistics/div/span[1] + Navigate To Projects + Wait Until Element Is Visible ${element} ${privaterepocountStr}= Convert To String ${privaterepocount} - Wait Until Element Contains //project/div/div/div[1]/div/statistics-panel/div/div[2]/div[1]/div[2]/div[2]/statistics/div/span[1] ${privaterepocountStr} + Wait Until Element Contains ${element} ${privaterepocountStr} ${privaterepocount2}= Get Statics Private Repo ${privateprojcount2}= get statics private project ${publicrepocount2}= get statics public repo @@ -418,6 +420,7 @@ Test Case - Delete Multi Project Create An New Project projecta${d} Create An New Project projectb${d} Push Image ${ip} user012 Test1@34 projecta${d} hello-world + Navigate To Projects Filter Object project Wait Until Element Is Not Visible //clr-datagrid/div/div[2] Multi-delete Object projecta projectb @@ -587,7 +590,7 @@ Test Case - Manual Scan All Switch To Configure Go To Vulnerability Config Trigger Scan Now - Back To Projects + Navigate To Projects Go Into Project library Go Into Repo redis Summary Chart Should Display latest @@ -671,7 +674,7 @@ Test Case - Retag A Image Tag Retag Image ${image_tag} project${random_num2} ${target_image_name} ${target_tag_value} Wait Until Element Is Not Visible css=${modal-dialog} - Back To Projects + Navigate To Projects Go Into Project project${random_num2} Sleep 1 Page Should Contain ${target_image_name} @@ -688,7 +691,7 @@ Test Case - Scan Image On Push Goto Project Config Enable Scan On Push Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library memcached - Back To Projects + Navigate To Projects Go Into Project library Go Into Repo memcached Summary Chart Should Display latest @@ -717,7 +720,7 @@ Test Case - Project Level Image Serverity Policy Go Into Project project${d} Go Into Repo haproxy Scan Repo latest Succeed - Back To Projects + Navigate To Projects Go Into Project project${d} Set Vulnerabilty Serverity 0 Cannot pull image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} project${d} haproxy diff --git a/tests/robot-cases/Group1-Nightly/LDAP.robot b/tests/robot-cases/Group1-Nightly/LDAP.robot index 5492c380b7..37dc3458f7 100644 --- a/tests/robot-cases/Group1-Nightly/LDAP.robot +++ b/tests/robot-cases/Group1-Nightly/LDAP.robot @@ -45,7 +45,7 @@ Test Case - System Admin On-board New Member Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} Switch To User Tag Page Should Not Contain mike02 - Back To Projects + Navigate To Projects Create An New Project project${d} Go Into Project project${d} has_image=${false} Switch To Member diff --git a/tests/robot-cases/Group1-Nightly/Nightly.robot b/tests/robot-cases/Group1-Nightly/Nightly.robot index 24d5a7ff5b..3d78ab74bb 100644 --- a/tests/robot-cases/Group1-Nightly/Nightly.robot +++ b/tests/robot-cases/Group1-Nightly/Nightly.robot @@ -455,7 +455,7 @@ Test Case - Manual Scan All Switch To Configure Go To Vulnerability Config Trigger Scan Now - Back To Projects + Navigate To Projects Go Into Project library Go Into Repo redis Summary Chart Should Display latest @@ -468,7 +468,7 @@ Test Case - Project Level Image Serverity Policy Go Into Project library Go Into Repo haproxy Scan Repo latest Succeed - Back To Projects + Navigate To Projects Go Into Project library Set Vulnerabilty Serverity 0 Cannot pull image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library haproxy @@ -524,7 +524,7 @@ Test Case - Delete A Project Project Should Not Be Deleted project${d} Go Into Project project${d} Delete Repo project${d} - Back To Projects + Navigate To Projects Project Should Be Deleted project${d} Close Browser @@ -680,7 +680,7 @@ Test Case - Scan Image On Push Goto Project Config Enable Scan On Push Push Image ${ip} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} library memcached - Back To Projects + Navigate To Projects Go Into Project library Go Into Repo memcached Summary Chart Should Display latest From 71f37fb8206cba2f891a0bb82d0379ca805c2b32 Mon Sep 17 00:00:00 2001 From: Yan Date: Thu, 24 Jan 2019 19:11:45 +0800 Subject: [PATCH 18/45] * Add robot account authn & authz implementation. This commit is to add the jwt token service, and do the authn & authz for robot account. Signed-off-by: wang yan --- .travis.yml | 1 + src/common/const.go | 2 + src/common/models/robot.go | 9 +- src/common/rbac/project/robot.go | 61 +++++++ src/common/rbac/project/robot_test.go | 38 +++++ src/common/rbac/project/util.go | 20 +++ src/common/security/robot/context.go | 112 ++++++++++++ src/common/security/robot/context_test.go | 197 ++++++++++++++++++++++ src/common/token/claims.go | 30 ++++ src/common/token/claims_test.go | 68 ++++++++ src/common/token/htoken.go | 78 +++++++++ src/common/token/htoken_test.go | 89 ++++++++++ src/common/token/options.go | 83 +++++++++ src/common/token/options_test.go | 23 +++ src/core/api/robot.go | 36 ++-- src/core/config/config_test.go | 5 + src/core/filter/security.go | 50 ++++++ src/core/filter/security_test.go | 17 ++ tests/private_key.pem | 51 ++++++ 19 files changed, 954 insertions(+), 16 deletions(-) create mode 100644 src/common/rbac/project/robot.go create mode 100644 src/common/rbac/project/robot_test.go create mode 100644 src/common/security/robot/context.go create mode 100644 src/common/security/robot/context_test.go create mode 100644 src/common/token/claims.go create mode 100644 src/common/token/claims_test.go create mode 100644 src/common/token/htoken.go create mode 100644 src/common/token/htoken_test.go create mode 100644 src/common/token/options.go create mode 100644 src/common/token/options_test.go create mode 100644 tests/private_key.pem diff --git a/.travis.yml b/.travis.yml index e0099564eb..f3201e3b70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,6 +36,7 @@ env: - REDIS_HOST: localhost - REG_VERSION: v2.6.2 - UI_BUILDER_VERSION: 1.6.0 + - TOKEN_PRIVATE_KEY_PATH: "/home/travis/gopath/src/github.com/goharbor/harbor/tests/private_key.pem" addons: apt: sources: diff --git a/src/common/const.go b/src/common/const.go index d6ecda41bb..f9ff20e030 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -119,6 +119,8 @@ const ( DefaultPortalURL = "http://portal" DefaultRegistryCtlURL = "http://registryctl:8080" DefaultClairHealthCheckServerURL = "http://clair:6061" + // Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user. + RobotPrefix = "robot$" ) // Shared variable, not allowed to modify diff --git a/src/common/models/robot.go b/src/common/models/robot.go index 78c4d21b8e..b2d8baa713 100644 --- a/src/common/models/robot.go +++ b/src/common/models/robot.go @@ -16,6 +16,7 @@ package models import ( "github.com/astaxie/beego/validation" + "github.com/goharbor/harbor/src/common/rbac" "time" ) @@ -45,10 +46,10 @@ type RobotQuery struct { // RobotReq ... type RobotReq struct { - Name string `json:"name"` - Description string `json:"description"` - Disabled bool `json:"disabled"` - Access []*ResourceActions `json:"access"` + Name string `json:"name"` + Description string `json:"description"` + Disabled bool `json:"disabled"` + Policy []*rbac.Policy `json:"access"` } // Valid put request validation diff --git a/src/common/rbac/project/robot.go b/src/common/rbac/project/robot.go new file mode 100644 index 0000000000..ccd0460882 --- /dev/null +++ b/src/common/rbac/project/robot.go @@ -0,0 +1,61 @@ +package project + +import "github.com/goharbor/harbor/src/common/rbac" + +// robotContext the context interface for the robot +type robotContext interface { + // Index whether the robot is authenticated + IsAuthenticated() bool + // GetUsername returns the name of robot + GetUsername() string + // GetPolicy get the rbac policies from security context + GetPolicies() []*rbac.Policy +} + +// robot implement the rbac.User interface for project robot account +type robot struct { + ctx robotContext + namespace rbac.Namespace +} + +// GetUserName get the robot name. +func (r *robot) GetUserName() string { + return r.ctx.GetUsername() +} + +// GetPolicies ... +func (r *robot) GetPolicies() []*rbac.Policy { + policies := []*rbac.Policy{} + + var publicProjectPolicies []*rbac.Policy + if r.namespace.IsPublic() { + publicProjectPolicies = policiesForPublicProjectRobot(r.namespace) + } + if len(publicProjectPolicies) > 0 { + for _, policy := range publicProjectPolicies { + policies = append(policies, policy) + } + } + + tokenPolicies := r.ctx.GetPolicies() + if len(tokenPolicies) > 0 { + for _, policy := range tokenPolicies { + policies = append(policies, policy) + } + } + + return policies +} + +// GetRoles robot has no definition of role, always return nil here. +func (r *robot) GetRoles() []rbac.Role { + return nil +} + +// NewRobot ... +func NewRobot(ctx robotContext, namespace rbac.Namespace) rbac.User { + return &robot{ + ctx: ctx, + namespace: namespace, + } +} diff --git a/src/common/rbac/project/robot_test.go b/src/common/rbac/project/robot_test.go new file mode 100644 index 0000000000..d8316eeb7c --- /dev/null +++ b/src/common/rbac/project/robot_test.go @@ -0,0 +1,38 @@ +package project + +import ( + "github.com/goharbor/harbor/src/common/rbac" + "github.com/stretchr/testify/assert" + "testing" +) + +type fakeRobotContext struct { + username string + isSysAdmin bool +} + +var ( + robotCtx = &fakeRobotContext{username: "robot$tester", isSysAdmin: true} +) + +func (ctx *fakeRobotContext) IsAuthenticated() bool { + return ctx.username != "" +} + +func (ctx *fakeRobotContext) GetUsername() string { + return ctx.username +} + +func (ctx *fakeRobotContext) IsSysAdmin() bool { + return ctx.IsAuthenticated() && ctx.isSysAdmin +} + +func (ctx *fakeRobotContext) GetPolicies() []*rbac.Policy { + return nil +} + +func TestGetPolicies(t *testing.T) { + namespace := rbac.NewProjectNamespace("library", false) + robot := NewRobot(robotCtx, namespace) + assert.NotNil(t, robot.GetPolicies()) +} diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index 34ebe86ebc..55f718896f 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -19,6 +19,12 @@ import ( ) var ( + // subresource policies for public project + // robot account can only access docker pull for the public project. + publicProjectPoliciesRobot = []*rbac.Policy{ + {Resource: ResourceImage, Action: ActionPull}, + } + // subresource policies for public project publicProjectPolicies = []*rbac.Policy{ {Resource: ResourceImage, Action: ActionPull}, @@ -30,6 +36,20 @@ var ( } ) +func policiesForPublicProjectRobot(namespace rbac.Namespace) []*rbac.Policy { + policies := []*rbac.Policy{} + + for _, policy := range publicProjectPoliciesRobot { + policies = append(policies, &rbac.Policy{ + Resource: namespace.Resource(policy.Resource), + Action: policy.Action, + Effect: policy.Effect, + }) + } + + return policies +} + func policiesForPublicProject(namespace rbac.Namespace) []*rbac.Policy { policies := []*rbac.Policy{} diff --git a/src/common/security/robot/context.go b/src/common/security/robot/context.go new file mode 100644 index 0000000000..d8a9fd1d46 --- /dev/null +++ b/src/common/security/robot/context.go @@ -0,0 +1,112 @@ +// Copyright Project Harbor Authors +// +// 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 robot + +import ( + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/rbac/project" + "github.com/goharbor/harbor/src/core/promgr" +) + +// SecurityContext implements security.Context interface based on database +type SecurityContext struct { + robot *models.Robot + pm promgr.ProjectManager + policy []*rbac.Policy +} + +// NewSecurityContext ... +func NewSecurityContext(robot *models.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext { + return &SecurityContext{ + robot: robot, + pm: pm, + policy: policy, + } +} + +// IsAuthenticated returns true if the user has been authenticated +func (s *SecurityContext) IsAuthenticated() bool { + return s.robot != nil +} + +// GetUsername returns the username of the authenticated user +// It returns null if the user has not been authenticated +func (s *SecurityContext) GetUsername() string { + if !s.IsAuthenticated() { + return "" + } + return s.robot.Name +} + +// IsSysAdmin robot cannot be a system admin +func (s *SecurityContext) IsSysAdmin() bool { + return false +} + +// IsSolutionUser robot cannot be a system admin +func (s *SecurityContext) IsSolutionUser() bool { + return false +} + +// HasReadPerm returns whether the user has read permission to the project +func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) +} + +// HasWritePerm returns whether the user has write permission to the project +func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) +} + +// HasAllPerm returns whether the user has all permissions to the project +func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) +} + +// GetMyProjects no implementation +func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) { + return nil, nil +} + +// GetPolicies get access infor from the token and convert it to the rbac policy +func (s *SecurityContext) GetPolicies() []*rbac.Policy { + return s.policy +} + +// GetProjectRoles no implementation +func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { + return nil +} + +// Can returns whether the robot can do action on resource +func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { + ns, err := resource.GetNamespace() + if err == nil { + switch ns.Kind() { + case "project": + projectIDOrName := ns.Identity() + isPublicProject, _ := s.pm.IsPublic(projectIDOrName) + projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject) + robot := project.NewRobot(s, projectNamespace) + return rbac.HasPermission(robot, resource, action) + } + } + + return false +} diff --git a/src/common/security/robot/context_test.go b/src/common/security/robot/context_test.go new file mode 100644 index 0000000000..3a729efaab --- /dev/null +++ b/src/common/security/robot/context_test.go @@ -0,0 +1,197 @@ +// Copyright Project Harbor Authors +// +// 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 robot + +import ( + "os" + "testing" + + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/promgr" + "github.com/goharbor/harbor/src/core/promgr/pmsdriver/local" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "strconv" +) + +var ( + private = &models.Project{ + Name: "testrobot", + OwnerID: 1, + } + pm = promgr.NewDefaultProjectManager(local.NewDriver(), true) +) + +func TestMain(m *testing.M) { + dbHost := os.Getenv("POSTGRESQL_HOST") + if len(dbHost) == 0 { + log.Fatalf("environment variable POSTGRES_HOST is not set") + } + dbUser := os.Getenv("POSTGRESQL_USR") + if len(dbUser) == 0 { + log.Fatalf("environment variable POSTGRES_USR is not set") + } + dbPortStr := os.Getenv("POSTGRESQL_PORT") + if len(dbPortStr) == 0 { + log.Fatalf("environment variable POSTGRES_PORT is not set") + } + dbPort, err := strconv.Atoi(dbPortStr) + if err != nil { + log.Fatalf("invalid POSTGRESQL_PORT: %v", err) + } + + dbPassword := os.Getenv("POSTGRESQL_PWD") + dbDatabase := os.Getenv("POSTGRESQL_DATABASE") + if len(dbDatabase) == 0 { + log.Fatalf("environment variable POSTGRESQL_DATABASE is not set") + } + + database := &models.Database{ + Type: "postgresql", + PostGreSQL: &models.PostGreSQL{ + Host: dbHost, + Port: dbPort, + Username: dbUser, + Password: dbPassword, + Database: dbDatabase, + }, + } + + log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword) + + if err := dao.InitDatabase(database); err != nil { + log.Fatalf("failed to initialize database: %v", err) + } + + // add project + id, err := dao.AddProject(*private) + if err != nil { + log.Fatalf("failed to add project: %v", err) + } + private.ProjectID = id + defer dao.DeleteProject(id) + + os.Exit(m.Run()) +} + +func TestIsAuthenticated(t *testing.T) { + // unauthenticated + ctx := NewSecurityContext(nil, nil, nil) + assert.False(t, ctx.IsAuthenticated()) + + // authenticated + ctx = NewSecurityContext(&models.Robot{ + Name: "test", + Disabled: false, + }, nil, nil) + assert.True(t, ctx.IsAuthenticated()) +} + +func TestGetUsername(t *testing.T) { + // unauthenticated + ctx := NewSecurityContext(nil, nil, nil) + assert.Equal(t, "", ctx.GetUsername()) + + // authenticated + ctx = NewSecurityContext(&models.Robot{ + Name: "test", + Disabled: false, + }, nil, nil) + assert.Equal(t, "test", ctx.GetUsername()) +} + +func TestIsSysAdmin(t *testing.T) { + // unauthenticated + ctx := NewSecurityContext(nil, nil, nil) + assert.False(t, ctx.IsSysAdmin()) + + // authenticated, non admin + ctx = NewSecurityContext(&models.Robot{ + Name: "test", + Disabled: false, + }, nil, nil) + assert.False(t, ctx.IsSysAdmin()) +} + +func TestIsSolutionUser(t *testing.T) { + ctx := NewSecurityContext(nil, nil, nil) + assert.False(t, ctx.IsSolutionUser()) +} + +func TestHasReadPerm(t *testing.T) { + + rbacPolicy := &rbac.Policy{ + Resource: "/project/testrobot/image", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + robot := &models.Robot{ + Name: "test_robot_1", + Description: "desc", + } + + ctx := NewSecurityContext(robot, pm, policies) + assert.True(t, ctx.HasReadPerm(private.Name)) +} + +func TestHasWritePerm(t *testing.T) { + + rbacPolicy := &rbac.Policy{ + Resource: "/project/testrobot/image", + Action: "push", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + robot := &models.Robot{ + Name: "test_robot_2", + Description: "desc", + } + + ctx := NewSecurityContext(robot, pm, policies) + assert.True(t, ctx.HasWritePerm(private.Name)) +} + +func TestHasAllPerm(t *testing.T) { + rbacPolicy := &rbac.Policy{ + Resource: "/project/testrobot/image", + Action: "push+pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + robot := &models.Robot{ + Name: "test_robot_3", + Description: "desc", + } + + ctx := NewSecurityContext(robot, pm, policies) + assert.True(t, ctx.HasAllPerm(private.Name)) +} + +func TestGetMyProjects(t *testing.T) { + ctx := NewSecurityContext(nil, nil, nil) + projects, err := ctx.GetMyProjects() + require.Nil(t, err) + assert.Nil(t, projects) +} + +func TestGetProjectRoles(t *testing.T) { + ctx := NewSecurityContext(nil, nil, nil) + roles := ctx.GetProjectRoles("test") + assert.Nil(t, roles) +} diff --git a/src/common/token/claims.go b/src/common/token/claims.go new file mode 100644 index 0000000000..b273a8aead --- /dev/null +++ b/src/common/token/claims.go @@ -0,0 +1,30 @@ +package token + +import ( + "github.com/dgrijalva/jwt-go" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/pkg/errors" +) + +// RobotClaims implements the interface of jwt.Claims +type RobotClaims struct { + jwt.StandardClaims + TokenID int64 `json:"id"` + ProjectID int64 `json:"pid"` + Policy []*rbac.Policy `json:"access"` +} + +// Valid valid the claims "tokenID, projectID and access". +func (rc RobotClaims) Valid() error { + + if rc.TokenID < 0 { + return errors.New("Token id must an valid INT") + } + if rc.ProjectID < 0 { + return errors.New("Project id must an valid INT") + } + if rc.Policy == nil { + return errors.New("The access info cannot be nil") + } + return nil +} diff --git a/src/common/token/claims_test.go b/src/common/token/claims_test.go new file mode 100644 index 0000000000..5a20a0375c --- /dev/null +++ b/src/common/token/claims_test.go @@ -0,0 +1,68 @@ +package token + +import ( + "github.com/goharbor/harbor/src/common/rbac" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestValid(t *testing.T) { + + rbacPolicy := &rbac.Policy{ + Resource: "/project/libray/repository", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + + rClaims := &RobotClaims{ + TokenID: 1, + ProjectID: 2, + Policy: policies, + } + assert.Nil(t, rClaims.Valid()) +} + +func TestUnValidTokenID(t *testing.T) { + + rbacPolicy := &rbac.Policy{ + Resource: "/project/libray/repository", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + + rClaims := &RobotClaims{ + TokenID: -1, + ProjectID: 2, + Policy: policies, + } + assert.NotNil(t, rClaims.Valid()) +} + +func TestUnValidProjectID(t *testing.T) { + + rbacPolicy := &rbac.Policy{ + Resource: "/project/libray/repository", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + + rClaims := &RobotClaims{ + TokenID: 1, + ProjectID: -2, + Policy: policies, + } + assert.NotNil(t, rClaims.Valid()) +} + +func TestUnValidPolicy(t *testing.T) { + + rClaims := &RobotClaims{ + TokenID: 1, + ProjectID: 2, + Policy: nil, + } + assert.NotNil(t, rClaims.Valid()) +} diff --git a/src/common/token/htoken.go b/src/common/token/htoken.go new file mode 100644 index 0000000000..686e6d021f --- /dev/null +++ b/src/common/token/htoken.go @@ -0,0 +1,78 @@ +package token + +import ( + "crypto/ecdsa" + "crypto/rsa" + "errors" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/goharbor/harbor/src/common/utils/log" + "time" +) + +// HToken ... +type HToken struct { + jwt.Token +} + +// NewWithClaims ... +func NewWithClaims(claims *RobotClaims) *HToken { + rClaims := &RobotClaims{ + TokenID: claims.TokenID, + ProjectID: claims.ProjectID, + Policy: claims.Policy, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(DefaultOptions.TTL).Unix(), + Issuer: DefaultOptions.Issuer, + }, + } + return &HToken{ + Token: *jwt.NewWithClaims(DefaultOptions.SignMethod, rClaims), + } +} + +// SignedString get the SignedString. +func (htk *HToken) SignedString() (string, error) { + key, err := DefaultOptions.GetKey() + if err != nil { + return "", nil + } + raw, err := htk.Token.SignedString(key) + if err != nil { + log.Debugf(fmt.Sprintf("failed to issue token %v", err)) + return "", err + } + return raw, err +} + +// ParseWithClaims ... +func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) { + key, err := DefaultOptions.GetKey() + if err != nil { + return nil, err + } + token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) { + if token.Method.Alg() != DefaultOptions.SignMethod.Alg() { + return nil, errors.New("invalid signing method") + } + switch k := key.(type) { + case *rsa.PrivateKey: + return &k.PublicKey, nil + case *ecdsa.PrivateKey: + return &k.PublicKey, nil + default: + return key, nil + } + }) + if err != nil { + log.Errorf(fmt.Sprintf("parse token error, %v", err)) + return nil, err + } + if !token.Valid { + log.Errorf(fmt.Sprintf("invalid jwt token, %v", token)) + return nil, err + } + return &HToken{ + Token: *token, + }, nil +} diff --git a/src/common/token/htoken_test.go b/src/common/token/htoken_test.go new file mode 100644 index 0000000000..b6376c108c --- /dev/null +++ b/src/common/token/htoken_test.go @@ -0,0 +1,89 @@ +package token + +import ( + "fmt" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/common/utils/test" + "github.com/goharbor/harbor/src/core/config" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestMain(m *testing.M) { + server, err := test.NewAdminserver(nil) + if err != nil { + panic(err) + } + defer server.Close() + + if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil { + panic(err) + } + + if err := config.Init(); err != nil { + panic(err) + } + + result := m.Run() + if result != 0 { + os.Exit(result) + } +} + +func TestNewWithClaims(t *testing.T) { + rbacPolicy := &rbac.Policy{ + Resource: "/project/libray/repository", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + + policy := &RobotClaims{ + TokenID: 123, + ProjectID: 321, + Policy: policies, + } + token := NewWithClaims(policy) + + assert.Equal(t, token.Header["alg"], "RS256") + assert.Equal(t, token.Header["typ"], "JWT") + +} + +func TestSignedString(t *testing.T) { + rbacPolicy := &rbac.Policy{ + Resource: "/project/library/repository", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + + policy := &RobotClaims{ + TokenID: 123, + ProjectID: 321, + Policy: policies, + } + + keyPath, err := DefaultOptions.GetKey() + if err != nil { + log.Infof(fmt.Sprintf("get key error, %v", err)) + } + log.Infof(fmt.Sprintf("get the key path, %s, ", keyPath)) + + token := NewWithClaims(policy) + rawTk, err := token.SignedString() + + assert.Nil(t, err) + assert.NotNil(t, rawTk) +} + +func TestParseWithClaims(t *testing.T) { + rawTk := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTIzLCJQcm9qZWN0SUQiOjAsIkFjY2VzcyI6W3siUmVzb3VyY2UiOiIvcHJvamVjdC9saWJyYXkvcmVwb3NpdG9yeSIsIkFjdGlvbiI6InB1bGwiLCJFZmZlY3QiOiIifV0sIlN0YW5kYXJkQ2xhaW1zIjp7ImV4cCI6MTU0ODE0MDIyOSwiaXNzIjoiaGFyYm9yLXRva2VuLWlzc3VlciJ9fQ.Jc3qSKN4SJVUzAvBvemVpRcSOZaHlu0Avqms04qzPm4ru9-r9IRIl3mnSkI6m9XkzLUeJ7Kiwyw63ghngnVKw_PupeclOGC6s3TK5Cfmo4h-lflecXjZWwyy-dtH_e7Us_ItS-R3nXDJtzSLEpsGHCcAj-1X2s93RB2qD8LNSylvYeDezVkTzqRzzfawPJheKKh9JTrz-3eUxCwQard9-xjlwvfUYULoHTn9npNAUq4-jqhipW4uE8HL-ym33AGF57la8U0RO11hmDM5K8-PiYknbqJ_oONeS3HBNym2pEFeGjtTv2co213wl4T5lemlg4SGolMBuJ03L7_beVZ0o-MKTkKDqDwJalb6_PM-7u3RbxC9IzJMiwZKIPnD3FvV10iPxUUQHaH8Jz5UZ2pFIhi_8BNnlBfT0JOPFVYATtLjHMczZelj2YvAeR1UHBzq3E0jPpjjwlqIFgaHCaN_KMwEvadTo_Fi2sEH4pNGP7M3yehU_72oLJQgF4paJarsmEoij6ZtPs6xekBz1fccVitq_8WNIz9aeCUdkUBRwI5QKw1RdW4ua-w74ld5MZStWJA8veyoLkEb_Q9eq2oAj5KWFjJbW5-ltiIfM8gxKflsrkWAidYGcEIYcuXr7UdqEKXxtPiWM0xb3B91ovYvO5402bn3f9-UGtlcestxNHA" + rClaims := &RobotClaims{} + _, _ = ParseWithClaims(rawTk, rClaims) + assert.Equal(t, int64(123), rClaims.TokenID) + assert.Equal(t, int64(0), rClaims.ProjectID) + assert.Equal(t, "/project/libray/repository", rClaims.Policy[0].Resource.String()) +} diff --git a/src/common/token/options.go b/src/common/token/options.go new file mode 100644 index 0000000000..a3328d82e1 --- /dev/null +++ b/src/common/token/options.go @@ -0,0 +1,83 @@ +package token + +import ( + "crypto/rsa" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "io/ioutil" + "time" +) + +const ( + ttl = 60 * time.Minute + issuer = "harbor-token-issuer" + signedMethod = "RS256" +) + +var ( + privateKey = config.TokenPrivateKeyPath() + // DefaultOptions ... + DefaultOptions = NewOptions() +) + +// Options ... +type Options struct { + SignMethod jwt.SigningMethod + PublicKey []byte + PrivateKey []byte + TTL time.Duration + Issuer string +} + +// NewOptions ... +func NewOptions() *Options { + privateKey, err := ioutil.ReadFile(privateKey) + if err != nil { + log.Errorf(fmt.Sprintf("failed to read private key %v", err)) + return nil + } + opt := &Options{ + SignMethod: jwt.GetSigningMethod(signedMethod), + PrivateKey: privateKey, + Issuer: issuer, + TTL: ttl, + } + return opt +} + +// GetKey ... +func (o *Options) GetKey() (interface{}, error) { + var err error + var privateKey *rsa.PrivateKey + var publicKey *rsa.PublicKey + + switch o.SignMethod.(type) { + case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS: + if len(o.PrivateKey) > 0 { + privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(o.PrivateKey) + if err != nil { + return nil, err + } + } + if len(o.PublicKey) > 0 { + publicKey, err = jwt.ParseRSAPublicKeyFromPEM(o.PublicKey) + if err != nil { + return nil, err + } + } + if privateKey == nil { + if publicKey != nil { + return publicKey, nil + } + return nil, fmt.Errorf("key is provided") + } + if publicKey != nil && publicKey.E != privateKey.E && publicKey.N.Cmp(privateKey.N) != 0 { + return nil, fmt.Errorf("the public key and private key are not match") + } + return privateKey, nil + default: + return nil, fmt.Errorf(fmt.Sprintf("unsupported sign method, %s", o.SignMethod)) + } +} diff --git a/src/common/token/options_test.go b/src/common/token/options_test.go new file mode 100644 index 0000000000..660975fff7 --- /dev/null +++ b/src/common/token/options_test.go @@ -0,0 +1,23 @@ +package token + +import ( + "github.com/dgrijalva/jwt-go" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestNewOptions(t *testing.T) { + defaultOpt := DefaultOptions + assert.NotNil(t, defaultOpt) + assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256")) + assert.Equal(t, defaultOpt.Issuer, "harbor-token-issuer") + assert.Equal(t, defaultOpt.TTL, 60*time.Minute) +} + +func TestGetKey(t *testing.T) { + defaultOpt := DefaultOptions + key, err := defaultOpt.GetKey() + assert.Nil(t, err) + assert.NotNil(t, key) +} diff --git a/src/core/api/robot.go b/src/core/api/robot.go index 7e2ba2b91b..2b8d3c9103 100644 --- a/src/core/api/robot.go +++ b/src/core/api/robot.go @@ -16,16 +16,14 @@ package api import ( "fmt" + "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/token" "net/http" "strconv" ) -// User this prefix to distinguish harbor user, -// The prefix contains a specific character($), so it cannot be registered as a harbor user. -const robotPrefix = "robot$" - // RobotAPI ... type RobotAPI struct { BaseController @@ -98,17 +96,14 @@ func (r *RobotAPI) Prepare() { func (r *RobotAPI) Post() { var robotReq models.RobotReq r.DecodeJSONReq(&robotReq) + createdName := common.RobotPrefix + robotReq.Name - createdName := robotPrefix + robotReq.Name - + // first to add a robot account, and get its id. robot := models.Robot{ Name: createdName, Description: robotReq.Description, ProjectID: r.project.ProjectID, - // TODO: use token service to generate token per access information - Token: "this is a placeholder", } - id, err := dao.AddRobot(&robot) if err != nil { if err == dao.ErrDupRows { @@ -119,11 +114,28 @@ func (r *RobotAPI) Post() { return } - robotRep := models.RobotRep{ - Name: robot.Name, - Token: robot.Token, + // generate the token, and return it with response data. + // token is not stored in the database. + rClaims := &token.RobotClaims{ + TokenID: id, + ProjectID: r.project.ProjectID, + Policy: robotReq.Policy, + } + token := token.NewWithClaims(rClaims) + rawTk, err := token.SignedString() + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to create token for robot account, %v", err)) + err := dao.DeleteRobot(id) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err)) + } + return } + robotRep := models.RobotRep{ + Name: robot.Name, + Token: rawTk, + } r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10)) r.Data["json"] = robotRep r.ServeJSON() diff --git a/src/core/config/config_test.go b/src/core/config/config_test.go index 2e0e0dd653..4c0dd2014e 100644 --- a/src/core/config/config_test.go +++ b/src/core/config/config_test.go @@ -53,6 +53,11 @@ func TestConfig(t *testing.T) { if err := os.Setenv("KEY_PATH", secretKeyPath); err != nil { t.Fatalf("failed to set env %s: %v", "KEY_PATH", err) } + oriKeyPath := os.Getenv("TOKEN_PRIVATE_KEY_PATH") + if err := os.Setenv("TOKEN_PRIVATE_KEY_PATH", ""); err != nil { + t.Fatalf("failed to set env %s: %v", "TOKEN_PRIVATE_KEY_PATH", err) + } + defer os.Setenv("TOKEN_PRIVATE_KEY_PATH", oriKeyPath) if err := Init(); err != nil { t.Fatalf("failed to initialize configurations: %v", err) diff --git a/src/core/filter/security.go b/src/core/filter/security.go index 2374bdc65a..62286b46b1 100644 --- a/src/core/filter/security.go +++ b/src/core/filter/security.go @@ -22,18 +22,23 @@ import ( beegoctx "github.com/astaxie/beego/context" "github.com/docker/distribution/reference" + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" secstore "github.com/goharbor/harbor/src/common/secret" "github.com/goharbor/harbor/src/common/security" admr "github.com/goharbor/harbor/src/common/security/admiral" "github.com/goharbor/harbor/src/common/security/admiral/authcontext" "github.com/goharbor/harbor/src/common/security/local" + robotCtx "github.com/goharbor/harbor/src/common/security/robot" "github.com/goharbor/harbor/src/common/security/secret" + "github.com/goharbor/harbor/src/common/token" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/auth" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/promgr" "github.com/goharbor/harbor/src/core/promgr/pmsdriver/admiral" + "strings" ) // ContextValueKey for content value @@ -95,6 +100,7 @@ func Init() { // standalone reqCtxModifiers = []ReqCtxModifier{ &secretReqCtxModifier{config.SecretStore}, + &robotAuthReqCtxModifier{}, &basicAuthReqCtxModifier{}, &sessionReqCtxModifier{}, &unauthorizedReqCtxModifier{}} @@ -147,6 +153,50 @@ func (s *secretReqCtxModifier) Modify(ctx *beegoctx.Context) bool { return true } +type robotAuthReqCtxModifier struct{} + +func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool { + robotName, robotTk, ok := ctx.Request.BasicAuth() + if !ok { + return false + } + log.Debug("got robot information via token auth") + if !strings.HasPrefix(robotName, common.RobotPrefix) { + return false + } + rClaims := &token.RobotClaims{} + htk := &token.HToken{} + htk, err := token.ParseWithClaims(robotTk, rClaims) + if err != nil { + log.Errorf("failed to decrypt robot token, %v", err) + return false + } + log.Infof(fmt.Sprintf("got robot token header, %v", htk.Header)) + // Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable. + robot, err := dao.GetRobotByID(htk.Claims.(*token.RobotClaims).TokenID) + if err != nil { + log.Errorf("failed to get robot %s: %v", robotName, err) + return false + } + if robot == nil { + log.Error("the token is not valid.") + return false + } + if robotName != robot.Name { + log.Errorf("failed to authenticate : %v", robotName) + return false + } + if robot.Disabled { + log.Errorf("the robot account %s is disabled", robot.Name) + return false + } + log.Debug("creating robot account security context...") + pm := config.GlobalProjectMgr + securCtx := robotCtx.NewSecurityContext(robot, pm, htk.Claims.(*token.RobotClaims).Policy) + setSecurCtxAndPM(ctx.Request, securCtx, pm) + return true +} + type basicAuthReqCtxModifier struct{} func (b *basicAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool { diff --git a/src/core/filter/security_test.go b/src/core/filter/security_test.go index 3512c61a25..403c769546 100644 --- a/src/core/filter/security_test.go +++ b/src/core/filter/security_test.go @@ -122,6 +122,23 @@ func TestSecretReqCtxModifier(t *testing.T) { assert.NotNil(t, projectManager(ctx)) } +func TestRobotReqCtxModifier(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, + "http://127.0.0.1/api/projects/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", req) + } + req.SetBasicAuth("robot$test1", "Harbor12345") + ctx, err := newContext(req) + if err != nil { + t.Fatalf("failed to crate context: %v", err) + } + + modifier := &robotAuthReqCtxModifier{} + modified := modifier.Modify(ctx) + assert.False(t, modified) +} + func TestBasicAuthReqCtxModifier(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/api/projects/", nil) diff --git a/tests/private_key.pem b/tests/private_key.pem new file mode 100644 index 0000000000..d2dc85dd1c --- /dev/null +++ b/tests/private_key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAtpMvyv153iSmwm6TrFpUOzsIGBEDbGtOOEZMEm08D8IC2n1G +d6/XOZ5FxPAD6gIpE0EAcMojY5O0Hl4CDoyV3e/iKcBqFOgYtpogNtan7yT5J8gw +KsPbU/8nBkK75GOq56nfvq4t9GVAclIDtHbuvmlh6O2n+fxtR0M9LbuotbSBdXYU +hzXqiSsMclBvLyIk/z327VP5l0nUNOzPuKIwQjuxYKDkvq1oGy98oVlE6wl0ldh2 +ZYZLGAYbVhqBVUT1Un/PYqi9Nofa2RI5n1WOkUJQp87vb+PUPFhVOdvH/oAzV6/b +9dzyhA5paDM06lj2gsg9hQWxCgbFh1x39c6pSI8hmVe6x2d4tAtSyOm3Qwz+zO2l +bPDvkY8Svh5nxUYObrNreoO8wHr8MC6TGUQLnUt/RfdVKe5fYPFl6VYqJP/L3LDn +Xj771nFq6PKiYbhBwJw3TM49gpKNS/Of70TP2m7nVlyuyMdE5T1j3xyXNkixXqqn +JuSMqX/3Bmm0On9KEbemwn7KRYF/bqc50+RcGUdKNcOkN6vuMVZei4GbxALnVqac +s+/UQAiQP4212UO7iZFwMaCNJ3r/b4GOlyalI1yEA4odoZov7k5zVOzHu8O6QmCj +3R5TVOudpGiUh+lumRRpNqxDgjngLljvaWU6ttyIbjnAwCjnJoppZM2lkRkCAwEA +AQKCAgAvsvCPlf2a3fR7Y6xNISRUfS22K+u7DaXX6fXB8qv4afWY45Xfex89vG35 +78L2Bi55C0h0LztjrpkmPeVHq88TtrJduhl88M5UFpxH93jUb9JwZErBQX4xyb2G +UzUHjEqAT89W3+a9rR5TP74cDd59/MZJtp1mIF7keVqochi3sDsKVxkx4hIuWALe +csk5hTApRyUWCBRzRCSe1yfF0wnMpA/JcP+SGXfTcmqbNNlelo/Q/kaga59+3UmT +C0Wy41s8fIvP+MnGT2QLxkkrqYyfwrWTweqoTtuKEIHjpdnwUcoYJKfQ6jKp8aH0 +STyP5UIyFOKNuFjyh6ZfoPbuT1nGW+YKlUnK4hQ9N/GE0oMoecTaHTbqM+psQvbj +6+CG/1ukA5ZTQyogNyuOApArFBQ+RRmVudPKA3JYygIhwctuB2oItsVEOEZMELCn +g2aVFAVXGfGRDXvpa8oxs3Pc6RJEp/3tON6+w7cMCx0lwN/Jk2Ie6RgTzUycT3k6 +MoTQJRoO6/ZHcx3hTut/CfnrWiltyAUZOsefLuLg+Pwf9GHhOycLRI6gHfgSwdIV +S77UbbELWdscVr1EoPIasUm1uYWBBcFRTturRW+GHJ8TZX+mcWSBcWwBhp15LjEl +tJf+9U6lWMOSB2LvT+vFmR0M9q56fo7UeKFIR7mo7/GpiVu5AQKCAQEA6Qs7G9mw +N/JZOSeQO6xIQakC+sKApPyXO58fa7WQzri+l2UrLNp0DEQfZCujqDgwys6OOzR/ +xg8ZKQWVoad08Ind3ZwoJgnLn6QLENOcE6PpWxA/JjnVGP4JrXCYR98cP0sf9jEI +xkR1qT50GbeqU3RDFliI4kGRvbZ8cekzuWppfQcjstSBPdvuxqAcUVmTnTw83nvD +FmBbhlLiEgI3iKtJ97UB7480ivnWnOuusduk7FO4jF3hkrOa+YRidinTCi8JBo0Y +jx4Ci3Y5x6nvwkXhKzXapd7YmPNisUc5xA7/a+W71cyC0IKUwRc/8pYWLL3R3CpR +YiV8gf6gwzOckQKCAQEAyI9CSNoAQH4zpS8B9PF8zILqEEuun8m1f5JB3hQnfWzm +7uz/zg6I0TkcCE0AJVSKPHQm1V9+TRbF9+DiOWHEYYzPmK8h63SIufaWxZPqai4E +PUj6eQWykBUVJ96n6/AW0JHRZ+WrJ5RXBqCLuY7NP6wDhORrCJjBwaGMohNpbKPS +H3QewsoxCh+CEXKdKyy+/yU/f4E89PlHapkW1/bDJ5u7puSD+KvmiDDIXSBncdOO +uFT8n+XH5IwgjdXFSDim15rQ8jD2l2xLcwKboTpx5GeRl8oB1VGm0fUbBn1dvGPG +4WfHGyrp9VNZtP160WoHr+vRVPqvHNkoeAlCfEwQCQKCAQBN1dtzLN0HgqE8TrOE +ysEDdTCykj4nXNoiJr522hi4gsndhQPLolb6NdKKQW0S5Vmekyi8K4e1nhtYMS5N +5MFRCasZtmtOcR0af87WWucZRDjPmniNCunaxBZ1YFLsRl+H4E6Xir8UgY8O7PYY +FNkFsKIrl3x4nU/RHl8oKKyG9Dyxbq4Er6dPAuMYYiezIAkGjjUCVjHNindnQM2T +GDx2IEe/PSydV6ZD+LguhyU88FCAQmI0N7L8rZJIXmgIcWW0VAterceTHYHaFK2t +u1uB9pcDOKSDnA+Z3kiLT2/CxQOYhQ2clgbnH4YRi/Nm0awsW2X5dATklAKm5GXL +bLSRAoIBAQClaNnPQdTBXBR2IN3pSZ2XAkXPKMwdxvtk+phOc6raHA4eceLL7FrU +y9gd1HvRTfcwws8gXcDKDYU62gNaNhMELWEt2QsNqS/2x7Qzwbms1sTyUpUZaSSL +BohLOKyfv4ThgdIGcXoGi6Z2tcRnRqpq4BCK8uR/05TBgN5+8amaS0ZKYLfaCW4G +nlPk1fVgHWhtAChtnYZLuKg494fKmB7+NMfAbmmVlxjrq+gkPkxyqXvk9Vrg+V8y +VIuozu0Fkouv+GRpyw4ldtCHS1hV0eEK8ow2dwmqCMygDxm58X10mYn2b2PcOTl5 +9sNerUw1GNC8O66K+rGgBk4FKgXmg8kZAoIBABBcuisK250fXAfjAWXGqIMs2+Di +vqAdT041SNZEOJSGNFsLJbhd/3TtCLf29PN/YXtnvBmC37rqryTsqjSbx/YT2Jbr +Bk3jOr9JVbmcoSubXl8d/uzf7IGs91qaCgBwPZHgeH+kK13FCLexz+U9zYMZ78fF +/yO82CpoekT+rcl1jzYn43b6gIklHABQU1uCD6MMyMhJ9Op2WmbDk3X+py359jMc ++Cr2zfzdHAIVff2dOV3OL+ZHEWbwtnn3htKUdOmjoTJrciFx0xNZJS5Q7QYHMONj +yPqbajyhopiN01aBQpCSGF1F1uRpWeIjTrAZPbrwLl9YSYXz0AT05QeFEFk= +-----END RSA PRIVATE KEY----- From 8b5e68073d0e23f9dbb3dd728c430bce420fa766 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Mon, 28 Jan 2019 18:06:52 +0800 Subject: [PATCH 19/45] Implement api for get current user permissions Signed-off-by: He Weiwei --- docs/swagger.yaml | 41 ++++++ src/common/rbac/project/const.go | 40 +++++- src/common/rbac/project/util.go | 82 +++++++++++- src/common/rbac/project/visitor.go | 5 +- src/common/rbac/project/visitor_role.go | 171 +++++++++++++++++++++++- src/common/rbac/project/visitor_test.go | 4 +- src/common/rbac/rbac.go | 23 ++++ src/common/rbac/rbac_test.go | 47 +++++++ src/common/security/admiral/context.go | 6 +- src/common/security/local/context.go | 6 +- src/core/api/chart_repository_test.go | 4 +- src/core/api/harborapi_test.go | 19 +++ src/core/api/user.go | 53 ++++++++ src/core/api/user_test.go | 25 ++++ src/core/router.go | 1 + tests/apitests/apilib/user.go | 6 + 16 files changed, 503 insertions(+), 30 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a8b7ddcb2a..dba24f3be2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -717,6 +717,37 @@ paths: $ref: '#/definitions/User' '401': description: User need to log in first. + /users/current/permissions: + get: + summary: Get current user permissions. + description: | + This endpoint is to get the current user permissions. + parameters: + - name: scope + in: query + type: string + required: false + description: Get permissions of the scope + - name: relative + in: query + type: boolean + required: false + description: | + If true, the resources in the response are relative to the scope, + eg for resource '/project/1/repository' if relative is 'true' then the resource in response will be 'repository'. + tags: + - Products + responses: + '200': + description: Get current user permission successfully. + schema: + type: array + items: + $ref: '#/definitions/Permission' + '401': + description: User need to log in first. + '500': + description: Internal errors. '/users/{user_id}': get: summary: Get a user's profile. @@ -4550,3 +4581,13 @@ definitions: error: type: string description: (optional) The error message when the status is "unhealthy" + Permission: + type: object + description: The permission + properties: + resource: + type: string + description: The permission resoruce + action: + type: string + description: The permission action \ No newline at end of file diff --git a/src/common/rbac/project/const.go b/src/common/rbac/project/const.go index dbefae98d2..c4c14f7033 100644 --- a/src/common/rbac/project/const.go +++ b/src/common/rbac/project/const.go @@ -20,14 +20,42 @@ import ( // const action variables const ( - ActionAll = rbac.Action("*") - ActionPull = rbac.Action("pull") - ActionPush = rbac.Action("push") - ActionPushPull = rbac.Action("push+pull") + ActionAll = rbac.Action("*") // action match any other actions + + ActionPull = rbac.Action("pull") // pull repository tag + ActionPush = rbac.Action("push") // push repository tag + ActionPushPull = rbac.Action("push+pull") // compatible with security all perm of project + + // create, read, update, delete, list actions compatible with restful api methods + ActionCreate = rbac.Action("create") + ActionRead = rbac.Action("read") + ActionUpdate = rbac.Action("update") + ActionDelete = rbac.Action("delete") + ActionList = rbac.Action("list") + + // execute replication for the replication policy (replication rule) + ActionExecute = rbac.Action("execute") + + // vulnerabilities scan for repository tag (aka, image tag) + ActionScan = rbac.Action("scan") ) // const resource variables const ( - ResourceAll = rbac.Resource("*") - ResourceImage = rbac.Resource("image") + ResourceAll = rbac.Resource("*") // resource match any other resources + ResourceSelf = rbac.Resource("") // subresource for project self + ResourceMember = rbac.Resource("member") + ResourceLog = rbac.Resource("log") + ResourceReplication = rbac.Resource("replication") + ResourceLabel = rbac.Resource("label") + ResourceRepository = rbac.Resource("repository") + ResourceRepositoryTag = rbac.Resource("repository-tag") + ResourceRepositoryTagManifest = rbac.Resource("repository-tag-manifest") + ResourceRepositoryTagVulnerability = rbac.Resource("repository-tag-vulnerability") + ResourceRepositoryTagLabel = rbac.Resource("repository-tag-label") + ResourceHelmChart = rbac.Resource("helm-chart") + ResourceHelmChartVersion = rbac.Resource("helm-chart-version") + ResourceHelmChartVersionLabel = rbac.Resource("helm-chart-version-label") + ResourceConfiguration = rbac.Resource("configuration") // compatible for portal only + ResourceRobot = rbac.Resource("robot") ) diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index 34ebe86ebc..1515f65e4e 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -21,12 +21,81 @@ import ( var ( // subresource policies for public project publicProjectPolicies = []*rbac.Policy{ - {Resource: ResourceImage, Action: ActionPull}, + {Resource: ResourceSelf, Action: ActionRead}, + + {Resource: ResourceRepository, Action: ActionList}, + {Resource: ResourceRepository, Action: ActionPull}, + + {Resource: ResourceHelmChart, Action: ActionRead}, + {Resource: ResourceHelmChart, Action: ActionList}, + + {Resource: ResourceHelmChartVersion, Action: ActionRead}, + {Resource: ResourceHelmChartVersion, Action: ActionList}, } - // subresource policies for system admin visitor - systemAdminProjectPolicies = []*rbac.Policy{ - {Resource: ResourceAll, Action: ActionAll}, + // all policies for the projects + allPolicies = []*rbac.Policy{ + {Resource: ResourceSelf, Action: ActionRead}, + {Resource: ResourceSelf, Action: ActionUpdate}, + {Resource: ResourceSelf, Action: ActionDelete}, + + {Resource: ResourceMember, Action: ActionCreate}, + {Resource: ResourceMember, Action: ActionUpdate}, + {Resource: ResourceMember, Action: ActionDelete}, + {Resource: ResourceMember, Action: ActionList}, + + {Resource: ResourceLog, Action: ActionList}, + + {Resource: ResourceReplication, Action: ActionList}, + {Resource: ResourceReplication, Action: ActionCreate}, + {Resource: ResourceReplication, Action: ActionUpdate}, + {Resource: ResourceReplication, Action: ActionDelete}, + {Resource: ResourceReplication, Action: ActionExecute}, + + {Resource: ResourceLabel, Action: ActionCreate}, + {Resource: ResourceLabel, Action: ActionUpdate}, + {Resource: ResourceLabel, Action: ActionDelete}, + {Resource: ResourceLabel, Action: ActionList}, + + {Resource: ResourceRepository, Action: ActionCreate}, + {Resource: ResourceRepository, Action: ActionUpdate}, + {Resource: ResourceRepository, Action: ActionDelete}, + {Resource: ResourceRepository, Action: ActionList}, + {Resource: ResourceRepository, Action: ActionPushPull}, // compatible with security all perm of project + {Resource: ResourceRepository, Action: ActionPush}, + {Resource: ResourceRepository, Action: ActionPull}, + + {Resource: ResourceRepositoryTag, Action: ActionDelete}, + {Resource: ResourceRepositoryTag, Action: ActionList}, + {Resource: ResourceRepositoryTag, Action: ActionScan}, + + {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + + {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + + {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, + {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + + {Resource: ResourceHelmChart, Action: ActionCreate}, + {Resource: ResourceHelmChart, Action: ActionRead}, + {Resource: ResourceHelmChart, Action: ActionDelete}, + {Resource: ResourceHelmChart, Action: ActionList}, + + {Resource: ResourceHelmChartVersion, Action: ActionRead}, + {Resource: ResourceHelmChartVersion, Action: ActionDelete}, + {Resource: ResourceHelmChartVersion, Action: ActionList}, + + {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, + {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + + {Resource: ResourceConfiguration, Action: ActionRead}, + {Resource: ResourceConfiguration, Action: ActionUpdate}, + + {Resource: ResourceRobot, Action: ActionCreate}, + {Resource: ResourceRobot, Action: ActionRead}, + {Resource: ResourceRobot, Action: ActionUpdate}, + {Resource: ResourceRobot, Action: ActionDelete}, + {Resource: ResourceRobot, Action: ActionList}, } ) @@ -44,10 +113,11 @@ func policiesForPublicProject(namespace rbac.Namespace) []*rbac.Policy { return policies } -func policiesForSystemAdmin(namespace rbac.Namespace) []*rbac.Policy { +// GetAllPolicies returns all policies for namespace of the project +func GetAllPolicies(namespace rbac.Namespace) []*rbac.Policy { policies := []*rbac.Policy{} - for _, policy := range systemAdminProjectPolicies { + for _, policy := range allPolicies { policies = append(policies, &rbac.Policy{ Resource: namespace.Resource(policy.Resource), Action: policy.Action, diff --git a/src/common/rbac/project/visitor.go b/src/common/rbac/project/visitor.go index c37a6ebef6..1307fe4c37 100644 --- a/src/common/rbac/project/visitor.go +++ b/src/common/rbac/project/visitor.go @@ -47,7 +47,7 @@ func (v *visitor) GetUserName() string { // GetPolicies returns policies of the visitor func (v *visitor) GetPolicies() []*rbac.Policy { if v.ctx.IsSysAdmin() { - return policiesForSystemAdmin(v.namespace) + return GetAllPolicies(v.namespace) } if v.namespace.IsPublic() { @@ -59,7 +59,8 @@ func (v *visitor) GetPolicies() []*rbac.Policy { // GetRoles returns roles of the visitor func (v *visitor) GetRoles() []rbac.Role { - if !v.ctx.IsAuthenticated() { + // Ignore roles when visitor is anonymous or system admin + if !v.ctx.IsAuthenticated() || v.ctx.IsSysAdmin() { return nil } diff --git a/src/common/rbac/project/visitor_role.go b/src/common/rbac/project/visitor_role.go index ada5868e6e..0ae5bc1a29 100644 --- a/src/common/rbac/project/visitor_role.go +++ b/src/common/rbac/project/visitor_role.go @@ -22,18 +22,175 @@ import ( var ( rolePoliciesMap = map[string][]*rbac.Policy{ "projectAdmin": { - {Resource: ResourceImage, Action: ActionPushPull}, // compatible with security all perm of project - {Resource: ResourceImage, Action: ActionPush}, - {Resource: ResourceImage, Action: ActionPull}, + {Resource: ResourceSelf, Action: ActionRead}, + {Resource: ResourceSelf, Action: ActionUpdate}, + {Resource: ResourceSelf, Action: ActionDelete}, + + {Resource: ResourceMember, Action: ActionCreate}, + {Resource: ResourceMember, Action: ActionUpdate}, + {Resource: ResourceMember, Action: ActionDelete}, + {Resource: ResourceMember, Action: ActionList}, + + {Resource: ResourceLog, Action: ActionList}, + + {Resource: ResourceReplication, Action: ActionRead}, + {Resource: ResourceReplication, Action: ActionList}, + + {Resource: ResourceLabel, Action: ActionCreate}, + {Resource: ResourceLabel, Action: ActionUpdate}, + {Resource: ResourceLabel, Action: ActionDelete}, + {Resource: ResourceLabel, Action: ActionList}, + + {Resource: ResourceRepository, Action: ActionCreate}, + {Resource: ResourceRepository, Action: ActionUpdate}, + {Resource: ResourceRepository, Action: ActionDelete}, + {Resource: ResourceRepository, Action: ActionList}, + {Resource: ResourceRepository, Action: ActionPushPull}, // compatible with security all perm of project + {Resource: ResourceRepository, Action: ActionPush}, + {Resource: ResourceRepository, Action: ActionPull}, + + {Resource: ResourceRepositoryTag, Action: ActionDelete}, + {Resource: ResourceRepositoryTag, Action: ActionList}, + {Resource: ResourceRepositoryTag, Action: ActionScan}, + + {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + + {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + + {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, + {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + + {Resource: ResourceHelmChart, Action: ActionCreate}, // upload helm chart + {Resource: ResourceHelmChart, Action: ActionRead}, // download helm chart + {Resource: ResourceHelmChart, Action: ActionDelete}, + {Resource: ResourceHelmChart, Action: ActionList}, + + {Resource: ResourceHelmChartVersion, Action: ActionCreate}, // upload helm chart version + {Resource: ResourceHelmChartVersion, Action: ActionRead}, // read and download helm chart version + {Resource: ResourceHelmChartVersion, Action: ActionDelete}, + {Resource: ResourceHelmChartVersion, Action: ActionList}, + + {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, + {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + + {Resource: ResourceConfiguration, Action: ActionRead}, + {Resource: ResourceConfiguration, Action: ActionUpdate}, + + {Resource: ResourceRobot, Action: ActionCreate}, + {Resource: ResourceRobot, Action: ActionRead}, + {Resource: ResourceRobot, Action: ActionUpdate}, + {Resource: ResourceRobot, Action: ActionDelete}, + {Resource: ResourceRobot, Action: ActionList}, + }, + + "master": { + {Resource: ResourceSelf, Action: ActionRead}, + + {Resource: ResourceMember, Action: ActionList}, + + {Resource: ResourceLog, Action: ActionList}, + + {Resource: ResourceReplication, Action: ActionRead}, + {Resource: ResourceReplication, Action: ActionList}, + + {Resource: ResourceLabel, Action: ActionCreate}, + {Resource: ResourceLabel, Action: ActionUpdate}, + {Resource: ResourceLabel, Action: ActionDelete}, + {Resource: ResourceLabel, Action: ActionList}, + + {Resource: ResourceRepository, Action: ActionCreate}, + {Resource: ResourceRepository, Action: ActionUpdate}, + {Resource: ResourceRepository, Action: ActionDelete}, + {Resource: ResourceRepository, Action: ActionList}, + {Resource: ResourceRepository, Action: ActionPush}, + {Resource: ResourceRepository, Action: ActionPull}, + + {Resource: ResourceRepositoryTag, Action: ActionDelete}, + {Resource: ResourceRepositoryTag, Action: ActionList}, + {Resource: ResourceRepositoryTag, Action: ActionScan}, + + {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + + {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + + {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, + {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + + {Resource: ResourceHelmChart, Action: ActionCreate}, + {Resource: ResourceHelmChart, Action: ActionRead}, + {Resource: ResourceHelmChart, Action: ActionDelete}, + {Resource: ResourceHelmChart, Action: ActionList}, + + {Resource: ResourceHelmChartVersion, Action: ActionCreate}, + {Resource: ResourceHelmChartVersion, Action: ActionRead}, + {Resource: ResourceHelmChartVersion, Action: ActionDelete}, + {Resource: ResourceHelmChartVersion, Action: ActionList}, + + {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, + {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + + {Resource: ResourceConfiguration, Action: ActionRead}, + {Resource: ResourceConfiguration, Action: ActionUpdate}, }, "developer": { - {Resource: ResourceImage, Action: ActionPush}, - {Resource: ResourceImage, Action: ActionPull}, + {Resource: ResourceSelf, Action: ActionRead}, + + {Resource: ResourceMember, Action: ActionList}, + + {Resource: ResourceLog, Action: ActionList}, + + {Resource: ResourceRepository, Action: ActionCreate}, + {Resource: ResourceRepository, Action: ActionList}, + {Resource: ResourceRepository, Action: ActionPush}, + {Resource: ResourceRepository, Action: ActionPull}, + + {Resource: ResourceRepositoryTag, Action: ActionList}, + + {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + + {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + + {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, + {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + + {Resource: ResourceHelmChart, Action: ActionCreate}, + {Resource: ResourceHelmChart, Action: ActionRead}, + {Resource: ResourceHelmChart, Action: ActionList}, + + {Resource: ResourceHelmChartVersion, Action: ActionCreate}, + {Resource: ResourceHelmChartVersion, Action: ActionRead}, + {Resource: ResourceHelmChartVersion, Action: ActionList}, + + {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, + {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + + {Resource: ResourceConfiguration, Action: ActionRead}, }, "guest": { - {Resource: ResourceImage, Action: ActionPull}, + {Resource: ResourceSelf, Action: ActionRead}, + + {Resource: ResourceMember, Action: ActionList}, + + {Resource: ResourceLog, Action: ActionList}, + + {Resource: ResourceRepository, Action: ActionList}, + {Resource: ResourceRepository, Action: ActionPull}, + + {Resource: ResourceRepositoryTag, Action: ActionList}, + + {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + + {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + + {Resource: ResourceHelmChart, Action: ActionRead}, + {Resource: ResourceHelmChart, Action: ActionList}, + + {Resource: ResourceHelmChartVersion, Action: ActionRead}, + {Resource: ResourceHelmChartVersion, Action: ActionList}, + + {Resource: ResourceConfiguration, Action: ActionRead}, }, } ) @@ -49,6 +206,8 @@ func (role *visitorRole) GetRoleName() string { switch role.roleID { case common.RoleProjectAdmin: return "projectAdmin" + case common.RoleMaster: + return "master" case common.RoleDeveloper: return "developer" case common.RoleGuest: diff --git a/src/common/rbac/project/visitor_test.go b/src/common/rbac/project/visitor_test.go index 4563b41bd8..6aa1113ed6 100644 --- a/src/common/rbac/project/visitor_test.go +++ b/src/common/rbac/project/visitor_test.go @@ -66,10 +66,10 @@ func (suite *VisitorTestSuite) TestGetPolicies() { suite.Equal(authenticatedForPublicProject.GetPolicies(), policiesForPublicProject(publicNamespace)) systemAdmin := NewUser(sysAdminCtx, namespace) - suite.Equal(systemAdmin.GetPolicies(), policiesForSystemAdmin(namespace)) + suite.Equal(systemAdmin.GetPolicies(), GetAllPolicies(namespace)) systemAdminForPublicProject := NewUser(sysAdminCtx, publicNamespace) - suite.Equal(systemAdminForPublicProject.GetPolicies(), policiesForSystemAdmin(publicNamespace)) + suite.Equal(systemAdminForPublicProject.GetPolicies(), GetAllPolicies(publicNamespace)) } func (suite *VisitorTestSuite) TestGetRoles() { diff --git a/src/common/rbac/rbac.go b/src/common/rbac/rbac.go index 843686f3b1..45d91dcfeb 100644 --- a/src/common/rbac/rbac.go +++ b/src/common/rbac/rbac.go @@ -15,8 +15,10 @@ package rbac import ( + "errors" "fmt" "path" + "strings" ) const ( @@ -29,6 +31,27 @@ const ( // Resource the type of resource type Resource string +// RelativeTo returns relative resource to other resource +func (res Resource) RelativeTo(other Resource) (Resource, error) { + prefix := other.String() + str := res.String() + + if !strings.HasPrefix(str, prefix) { + return Resource(""), errors.New("value error") + } + + relative := strings.TrimPrefix(str, prefix) + if strings.HasPrefix(relative, "/") { + relative = relative[1:] + } + + if relative == "" { + relative = "." + } + + return Resource(relative), nil +} + func (res Resource) String() string { return string(res) } diff --git a/src/common/rbac/rbac_test.go b/src/common/rbac/rbac_test.go index 45af1b100f..740b236630 100644 --- a/src/common/rbac/rbac_test.go +++ b/src/common/rbac/rbac_test.go @@ -390,3 +390,50 @@ func TestResource_GetNamespace(t *testing.T) { }) } } + +func TestResource_RelativeTo(t *testing.T) { + type args struct { + other Resource + } + tests := []struct { + name string + res Resource + args args + want Resource + wantErr bool + }{ + { + name: "/project/1/image", + res: Resource("/project/1/image"), + args: args{other: Resource("/project/1")}, + want: Resource("image"), + wantErr: false, + }, + { + name: "/project/1", + res: Resource("/project/1"), + args: args{other: Resource("/project/1")}, + want: Resource("."), + wantErr: false, + }, + { + name: "/project/1", + res: Resource("/project/1"), + args: args{other: Resource("/system")}, + want: Resource(""), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.res.RelativeTo(tt.args.other) + if (err != nil) != tt.wantErr { + t.Errorf("Resource.RelativeTo() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Resource.RelativeTo() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/common/security/admiral/context.go b/src/common/security/admiral/context.go index 86ad4a7722..72840d36b0 100644 --- a/src/common/security/admiral/context.go +++ b/src/common/security/admiral/context.go @@ -72,19 +72,19 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) } // Can returns whether the user can do action on resource diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index ab4d11f4ab..d564332142 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -70,19 +70,19 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) } // Can returns whether the user can do action on resource diff --git a/src/core/api/chart_repository_test.go b/src/core/api/chart_repository_test.go index 6d9be17b18..4bcb1f009c 100644 --- a/src/core/api/chart_repository_test.go +++ b/src/core/api/chart_repository_test.go @@ -313,12 +313,12 @@ func (msc *mockSecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - return msc.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceImage)) + return msc.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceRepository)) } // HasWritePerm returns whether the user has write permission to the project func (msc *mockSecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - return msc.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceImage)) + return msc.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceRepository)) } // HasAllPerm returns whether the user has all permissions to the project diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 5f1f59cc58..277b31086d 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -34,6 +34,7 @@ import ( "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/filter" "github.com/goharbor/harbor/tests/apitests/apilib" + // "strconv" // "strings" @@ -103,6 +104,7 @@ func init() { beego.Router("/api/users/:id", &UserAPI{}, "get:Get") beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put") beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword") + beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions") beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs") beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable") @@ -996,6 +998,23 @@ func (a testapi) UsersUpdatePassword(userID int, password apilib.Password, authI return httpStatusCode, err } +func (a testapi) UsersGetPermissions(userID interface{}, scope string, authInfo usrInfo) (int, []apilib.Permission, error) { + _sling := sling.New().Get(a.basePath) + // create path and map variables + path := fmt.Sprintf("/api/users/%v/permissions", userID) + _sling = _sling.Path(path) + type QueryParams struct { + Scope string `url:"scope,omitempty"` + } + _sling = _sling.QueryStruct(&QueryParams{Scope: scope}) + httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo) + var successPayLoad []apilib.Permission + if 200 == httpStatusCode && nil == err { + err = json.Unmarshal(body, &successPayLoad) + } + return httpStatusCode, successPayLoad, err +} + // Mark a registered user as be removed. func (a testapi) UsersDelete(userID int, authInfo usrInfo) (int, error) { _sling := sling.New().Delete(a.basePath) diff --git a/src/core/api/user.go b/src/core/api/user.go index b3ee72541f..fb1cfebcec 100644 --- a/src/core/api/user.go +++ b/src/core/api/user.go @@ -24,6 +24,8 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/config" @@ -339,6 +341,57 @@ func (ua *UserAPI) ToggleUserAdminRole() { } } +// ListUserPermissions handles GET to /api/users/{}/permissions +func (ua *UserAPI) ListUserPermissions() { + if ua.userID != ua.currentUserID { + log.Warningf("Current user, id: %d can not view other user's permissions", ua.currentUserID) + ua.RenderError(http.StatusForbidden, "User does not have permission") + return + } + + relative := ua.Ctx.Input.Query("relative") == "true" + + scope := rbac.Resource(ua.Ctx.Input.Query("scope")) + policies := []*rbac.Policy{} + + namespace, err := scope.GetNamespace() + if err == nil { + switch namespace.Kind() { + case "project": + for _, policy := range project.GetAllPolicies(namespace) { + if ua.SecurityCtx.Can(policy.Action, policy.Resource) { + policies = append(policies, policy) + } + } + } + } + + results := []map[string]string{} + for _, policy := range policies { + var resource rbac.Resource + + // for resource `/project/1/repository` if `relative` is `true` then the resource in response will be `repository` + if relative { + relativeResource, err := policy.Resource.RelativeTo(scope) + if err != nil { + continue + } + resource = relativeResource + } else { + resource = policy.Resource + } + + results = append(results, map[string]string{ + "resource": resource.String(), + "action": policy.Action.String(), + }) + } + + ua.Data["json"] = results + ua.ServeJSON() + return +} + // modifiable returns whether the modify is allowed based on current auth mode and context func (ua *UserAPI) modifiable() bool { if ua.AuthMode == common.DBAuth { diff --git a/src/core/api/user_test.go b/src/core/api/user_test.go index e309a95601..924c8c2943 100644 --- a/src/core/api/user_test.go +++ b/src/core/api/user_test.go @@ -572,3 +572,28 @@ func TestModifiable(t *testing.T) { } assert.True(ua4.modifiable()) } + +func TestUsersCurrentPermissions(t *testing.T) { + fmt.Println("Testing Get Users Current Permissions") + + assert := assert.New(t) + apiTest := newHarborAPI() + + httpStatusCode, permissions, err := apiTest.UsersGetPermissions("current", "/project/library", *projAdmin) + assert.Nil(err) + assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200") + assert.NotEmpty(permissions, "permissions should not be empty") + + httpStatusCode, permissions, err = apiTest.UsersGetPermissions("current", "/unsupport-scope", *projAdmin) + assert.Nil(err) + assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200") + assert.Empty(permissions, "permissions should be empty") + + httpStatusCode, _, err = apiTest.UsersGetPermissions(projAdminID, "/project/library", *projAdmin) + assert.Nil(err) + assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200") + + httpStatusCode, _, err = apiTest.UsersGetPermissions(projDeveloperID, "/project/library", *projAdmin) + assert.Nil(err) + assert.Equal(int(403), httpStatusCode, "httpStatusCode should be 403") +} diff --git a/src/core/router.go b/src/core/router.go index 1fe0686139..d793ffe72c 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -46,6 +46,7 @@ func initRouters() { beego.Router("/api/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put") beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post") beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword") + beego.Router("/api/users/:id/permissions", &api.UserAPI{}, "get:ListUserPermissions") beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{}) beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping") diff --git a/tests/apitests/apilib/user.go b/tests/apitests/apilib/user.go index f6f2cb04de..8fc12ae69f 100644 --- a/tests/apitests/apilib/user.go +++ b/tests/apitests/apilib/user.go @@ -53,3 +53,9 @@ type User struct { UpdateTime string `json:"update_time,omitempty"` } + +// Permission the permission type +type Permission struct { + Resource string `json:"resource,omitempty"` + Action string `json:"action,omitempty"` +} From 2d7ea9c383f601b4cfd2b771ecaafbbc50c2afe9 Mon Sep 17 00:00:00 2001 From: wang yan Date: Mon, 28 Jan 2019 16:46:52 +0800 Subject: [PATCH 20/45] update codes per review comments Signed-off-by: wang yan --- src/common/models/robot.go | 2 +- src/common/rbac/project/robot.go | 61 ------------------------- src/common/rbac/project/robot_test.go | 38 --------------- src/common/rbac/project/util.go | 23 +--------- src/common/rbac/project/visitor.go | 2 +- src/common/rbac/project/visitor_test.go | 4 +- src/common/security/robot/context.go | 7 +-- src/common/security/robot/robot.go | 42 +++++++++++++++++ src/common/security/robot/robot_test.go | 27 +++++++++++ src/common/token/claims.go | 5 +- src/common/token/claims_test.go | 8 ++-- src/common/token/htoken.go | 25 ++++++---- src/common/token/htoken_test.go | 36 +++++---------- src/core/api/robot.go | 20 ++++---- src/core/api/robot_test.go | 20 ++++++-- src/core/filter/security.go | 7 +-- 16 files changed, 139 insertions(+), 188 deletions(-) delete mode 100644 src/common/rbac/project/robot.go delete mode 100644 src/common/rbac/project/robot_test.go create mode 100644 src/common/security/robot/robot.go create mode 100644 src/common/security/robot/robot_test.go diff --git a/src/common/models/robot.go b/src/common/models/robot.go index b2d8baa713..52a7c2975e 100644 --- a/src/common/models/robot.go +++ b/src/common/models/robot.go @@ -49,7 +49,7 @@ type RobotReq struct { Name string `json:"name"` Description string `json:"description"` Disabled bool `json:"disabled"` - Policy []*rbac.Policy `json:"access"` + Access []*rbac.Policy `json:"access"` } // Valid put request validation diff --git a/src/common/rbac/project/robot.go b/src/common/rbac/project/robot.go deleted file mode 100644 index ccd0460882..0000000000 --- a/src/common/rbac/project/robot.go +++ /dev/null @@ -1,61 +0,0 @@ -package project - -import "github.com/goharbor/harbor/src/common/rbac" - -// robotContext the context interface for the robot -type robotContext interface { - // Index whether the robot is authenticated - IsAuthenticated() bool - // GetUsername returns the name of robot - GetUsername() string - // GetPolicy get the rbac policies from security context - GetPolicies() []*rbac.Policy -} - -// robot implement the rbac.User interface for project robot account -type robot struct { - ctx robotContext - namespace rbac.Namespace -} - -// GetUserName get the robot name. -func (r *robot) GetUserName() string { - return r.ctx.GetUsername() -} - -// GetPolicies ... -func (r *robot) GetPolicies() []*rbac.Policy { - policies := []*rbac.Policy{} - - var publicProjectPolicies []*rbac.Policy - if r.namespace.IsPublic() { - publicProjectPolicies = policiesForPublicProjectRobot(r.namespace) - } - if len(publicProjectPolicies) > 0 { - for _, policy := range publicProjectPolicies { - policies = append(policies, policy) - } - } - - tokenPolicies := r.ctx.GetPolicies() - if len(tokenPolicies) > 0 { - for _, policy := range tokenPolicies { - policies = append(policies, policy) - } - } - - return policies -} - -// GetRoles robot has no definition of role, always return nil here. -func (r *robot) GetRoles() []rbac.Role { - return nil -} - -// NewRobot ... -func NewRobot(ctx robotContext, namespace rbac.Namespace) rbac.User { - return &robot{ - ctx: ctx, - namespace: namespace, - } -} diff --git a/src/common/rbac/project/robot_test.go b/src/common/rbac/project/robot_test.go deleted file mode 100644 index d8316eeb7c..0000000000 --- a/src/common/rbac/project/robot_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package project - -import ( - "github.com/goharbor/harbor/src/common/rbac" - "github.com/stretchr/testify/assert" - "testing" -) - -type fakeRobotContext struct { - username string - isSysAdmin bool -} - -var ( - robotCtx = &fakeRobotContext{username: "robot$tester", isSysAdmin: true} -) - -func (ctx *fakeRobotContext) IsAuthenticated() bool { - return ctx.username != "" -} - -func (ctx *fakeRobotContext) GetUsername() string { - return ctx.username -} - -func (ctx *fakeRobotContext) IsSysAdmin() bool { - return ctx.IsAuthenticated() && ctx.isSysAdmin -} - -func (ctx *fakeRobotContext) GetPolicies() []*rbac.Policy { - return nil -} - -func TestGetPolicies(t *testing.T) { - namespace := rbac.NewProjectNamespace("library", false) - robot := NewRobot(robotCtx, namespace) - assert.NotNil(t, robot.GetPolicies()) -} diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index 55f718896f..f176de0b51 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -19,12 +19,6 @@ import ( ) var ( - // subresource policies for public project - // robot account can only access docker pull for the public project. - publicProjectPoliciesRobot = []*rbac.Policy{ - {Resource: ResourceImage, Action: ActionPull}, - } - // subresource policies for public project publicProjectPolicies = []*rbac.Policy{ {Resource: ResourceImage, Action: ActionPull}, @@ -36,21 +30,8 @@ var ( } ) -func policiesForPublicProjectRobot(namespace rbac.Namespace) []*rbac.Policy { - policies := []*rbac.Policy{} - - for _, policy := range publicProjectPoliciesRobot { - policies = append(policies, &rbac.Policy{ - Resource: namespace.Resource(policy.Resource), - Action: policy.Action, - Effect: policy.Effect, - }) - } - - return policies -} - -func policiesForPublicProject(namespace rbac.Namespace) []*rbac.Policy { +// PoliciesForPublicProject ... +func PoliciesForPublicProject(namespace rbac.Namespace) []*rbac.Policy { policies := []*rbac.Policy{} for _, policy := range publicProjectPolicies { diff --git a/src/common/rbac/project/visitor.go b/src/common/rbac/project/visitor.go index c37a6ebef6..0169422302 100644 --- a/src/common/rbac/project/visitor.go +++ b/src/common/rbac/project/visitor.go @@ -51,7 +51,7 @@ func (v *visitor) GetPolicies() []*rbac.Policy { } if v.namespace.IsPublic() { - return policiesForPublicProject(v.namespace) + return PoliciesForPublicProject(v.namespace) } return nil diff --git a/src/common/rbac/project/visitor_test.go b/src/common/rbac/project/visitor_test.go index 4563b41bd8..e68ed01ee1 100644 --- a/src/common/rbac/project/visitor_test.go +++ b/src/common/rbac/project/visitor_test.go @@ -57,13 +57,13 @@ func (suite *VisitorTestSuite) TestGetPolicies() { suite.Nil(anonymous.GetPolicies()) anonymousForPublicProject := NewUser(anonymousCtx, publicNamespace) - suite.Equal(anonymousForPublicProject.GetPolicies(), policiesForPublicProject(publicNamespace)) + suite.Equal(anonymousForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace)) authenticated := NewUser(authenticatedCtx, namespace) suite.Nil(authenticated.GetPolicies()) authenticatedForPublicProject := NewUser(authenticatedCtx, publicNamespace) - suite.Equal(authenticatedForPublicProject.GetPolicies(), policiesForPublicProject(publicNamespace)) + suite.Equal(authenticatedForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace)) systemAdmin := NewUser(sysAdminCtx, namespace) suite.Equal(systemAdmin.GetPolicies(), policiesForSystemAdmin(namespace)) diff --git a/src/common/security/robot/context.go b/src/common/security/robot/context.go index d8a9fd1d46..9e73dc5570 100644 --- a/src/common/security/robot/context.go +++ b/src/common/security/robot/context.go @@ -84,11 +84,6 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) { return nil, nil } -// GetPolicies get access infor from the token and convert it to the rbac policy -func (s *SecurityContext) GetPolicies() []*rbac.Policy { - return s.policy -} - // GetProjectRoles no implementation func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { return nil @@ -103,7 +98,7 @@ func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { projectIDOrName := ns.Identity() isPublicProject, _ := s.pm.IsPublic(projectIDOrName) projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject) - robot := project.NewRobot(s, projectNamespace) + robot := NewRobot(s.GetUsername(), projectNamespace, s.policy) return rbac.HasPermission(robot, resource, action) } } diff --git a/src/common/security/robot/robot.go b/src/common/security/robot/robot.go new file mode 100644 index 0000000000..9bfec53a9e --- /dev/null +++ b/src/common/security/robot/robot.go @@ -0,0 +1,42 @@ +package robot + +import ( + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/rbac/project" +) + +// robot implement the rbac.User interface for project robot account +type robot struct { + username string + namespace rbac.Namespace + policy []*rbac.Policy +} + +// GetUserName get the robot name. +func (r *robot) GetUserName() string { + return r.username +} + +// GetPolicies ... +func (r *robot) GetPolicies() []*rbac.Policy { + policies := []*rbac.Policy{} + if r.namespace.IsPublic() { + policies = append(policies, project.PoliciesForPublicProject(r.namespace)...) + } + policies = append(policies, r.policy...) + return policies +} + +// GetRoles robot has no definition of role, always return nil here. +func (r *robot) GetRoles() []rbac.Role { + return nil +} + +// NewRobot ... +func NewRobot(username string, namespace rbac.Namespace, policy []*rbac.Policy) rbac.User { + return &robot{ + username: username, + namespace: namespace, + policy: policy, + } +} diff --git a/src/common/security/robot/robot_test.go b/src/common/security/robot/robot_test.go new file mode 100644 index 0000000000..62acbe11f6 --- /dev/null +++ b/src/common/security/robot/robot_test.go @@ -0,0 +1,27 @@ +package robot + +import ( + "github.com/goharbor/harbor/src/common/rbac" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetPolicies(t *testing.T) { + + rbacPolicy := &rbac.Policy{ + Resource: "/project/libray/repository", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + + robot := robot{ + username: "test", + namespace: rbac.NewProjectNamespace("library", false), + policy: policies, + } + + assert.Equal(t, robot.GetUserName(), "test") + assert.NotNil(t, robot.GetPolicies()) + assert.Nil(t, robot.GetRoles()) +} diff --git a/src/common/token/claims.go b/src/common/token/claims.go index b273a8aead..4739f9d216 100644 --- a/src/common/token/claims.go +++ b/src/common/token/claims.go @@ -11,19 +11,18 @@ type RobotClaims struct { jwt.StandardClaims TokenID int64 `json:"id"` ProjectID int64 `json:"pid"` - Policy []*rbac.Policy `json:"access"` + Access []*rbac.Policy `json:"access"` } // Valid valid the claims "tokenID, projectID and access". func (rc RobotClaims) Valid() error { - if rc.TokenID < 0 { return errors.New("Token id must an valid INT") } if rc.ProjectID < 0 { return errors.New("Project id must an valid INT") } - if rc.Policy == nil { + if rc.Access == nil { return errors.New("The access info cannot be nil") } return nil diff --git a/src/common/token/claims_test.go b/src/common/token/claims_test.go index 5a20a0375c..dc25a120a1 100644 --- a/src/common/token/claims_test.go +++ b/src/common/token/claims_test.go @@ -18,7 +18,7 @@ func TestValid(t *testing.T) { rClaims := &RobotClaims{ TokenID: 1, ProjectID: 2, - Policy: policies, + Access: policies, } assert.Nil(t, rClaims.Valid()) } @@ -35,7 +35,7 @@ func TestUnValidTokenID(t *testing.T) { rClaims := &RobotClaims{ TokenID: -1, ProjectID: 2, - Policy: policies, + Access: policies, } assert.NotNil(t, rClaims.Valid()) } @@ -52,7 +52,7 @@ func TestUnValidProjectID(t *testing.T) { rClaims := &RobotClaims{ TokenID: 1, ProjectID: -2, - Policy: policies, + Access: policies, } assert.NotNil(t, rClaims.Valid()) } @@ -62,7 +62,7 @@ func TestUnValidPolicy(t *testing.T) { rClaims := &RobotClaims{ TokenID: 1, ProjectID: 2, - Policy: nil, + Access: nil, } assert.NotNil(t, rClaims.Valid()) } diff --git a/src/common/token/htoken.go b/src/common/token/htoken.go index 686e6d021f..ac90678207 100644 --- a/src/common/token/htoken.go +++ b/src/common/token/htoken.go @@ -6,33 +6,40 @@ import ( "errors" "fmt" "github.com/dgrijalva/jwt-go" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" "time" ) -// HToken ... +// HToken htoken is a jwt token for harbor robot account, +// which contains the robot ID, project ID and the access permission for the project. +// It used for authn/authz for robot account in Harbor. type HToken struct { jwt.Token } -// NewWithClaims ... -func NewWithClaims(claims *RobotClaims) *HToken { +// New ... +func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) { rClaims := &RobotClaims{ - TokenID: claims.TokenID, - ProjectID: claims.ProjectID, - Policy: claims.Policy, + TokenID: tokenID, + ProjectID: projectID, + Access: access, StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(DefaultOptions.TTL).Unix(), Issuer: DefaultOptions.Issuer, }, } + err := rClaims.Valid() + if err != nil { + return nil, err + } return &HToken{ Token: *jwt.NewWithClaims(DefaultOptions.SignMethod, rClaims), - } + }, nil } -// SignedString get the SignedString. -func (htk *HToken) SignedString() (string, error) { +// Raw get the Raw string of token +func (htk *HToken) Raw() (string, error) { key, err := DefaultOptions.GetKey() if err != nil { return "", nil diff --git a/src/common/token/htoken_test.go b/src/common/token/htoken_test.go index b6376c108c..58a853d948 100644 --- a/src/common/token/htoken_test.go +++ b/src/common/token/htoken_test.go @@ -1,9 +1,7 @@ package token import ( - "fmt" "github.com/goharbor/harbor/src/common/rbac" - "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/core/config" "github.com/stretchr/testify/assert" @@ -32,7 +30,7 @@ func TestMain(m *testing.M) { } } -func TestNewWithClaims(t *testing.T) { +func TestNew(t *testing.T) { rbacPolicy := &rbac.Policy{ Resource: "/project/libray/repository", Action: "pull", @@ -40,19 +38,17 @@ func TestNewWithClaims(t *testing.T) { policies := []*rbac.Policy{} policies = append(policies, rbacPolicy) - policy := &RobotClaims{ - TokenID: 123, - ProjectID: 321, - Policy: policies, - } - token := NewWithClaims(policy) + tokenID := int64(123) + projectID := int64(321) + token, err := New(tokenID, projectID, policies) + assert.Nil(t, err) assert.Equal(t, token.Header["alg"], "RS256") assert.Equal(t, token.Header["typ"], "JWT") } -func TestSignedString(t *testing.T) { +func TestRaw(t *testing.T) { rbacPolicy := &rbac.Policy{ Resource: "/project/library/repository", Action: "pull", @@ -60,21 +56,13 @@ func TestSignedString(t *testing.T) { policies := []*rbac.Policy{} policies = append(policies, rbacPolicy) - policy := &RobotClaims{ - TokenID: 123, - ProjectID: 321, - Policy: policies, - } + tokenID := int64(123) + projectID := int64(321) - keyPath, err := DefaultOptions.GetKey() - if err != nil { - log.Infof(fmt.Sprintf("get key error, %v", err)) - } - log.Infof(fmt.Sprintf("get the key path, %s, ", keyPath)) - - token := NewWithClaims(policy) - rawTk, err := token.SignedString() + token, err := New(tokenID, projectID, policies) + assert.Nil(t, err) + rawTk, err := token.Raw() assert.Nil(t, err) assert.NotNil(t, rawTk) } @@ -85,5 +73,5 @@ func TestParseWithClaims(t *testing.T) { _, _ = ParseWithClaims(rawTk, rClaims) assert.Equal(t, int64(123), rClaims.TokenID) assert.Equal(t, int64(0), rClaims.ProjectID) - assert.Equal(t, "/project/libray/repository", rClaims.Policy[0].Resource.String()) + assert.Equal(t, "/project/libray/repository", rClaims.Access[0].Resource.String()) } diff --git a/src/core/api/robot.go b/src/core/api/robot.go index 2b8d3c9103..920ce312ca 100644 --- a/src/core/api/robot.go +++ b/src/core/api/robot.go @@ -116,15 +116,19 @@ func (r *RobotAPI) Post() { // generate the token, and return it with response data. // token is not stored in the database. - rClaims := &token.RobotClaims{ - TokenID: id, - ProjectID: r.project.ProjectID, - Policy: robotReq.Policy, - } - token := token.NewWithClaims(rClaims) - rawTk, err := token.SignedString() + jwtToken, err := token.New(id, r.project.ProjectID, robotReq.Access) if err != nil { - r.HandleInternalServerError(fmt.Sprintf("failed to create token for robot account, %v", err)) + r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err)) + err := dao.DeleteRobot(id) + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err)) + } + return + } + + rawTk, err := jwtToken.Raw() + if err != nil { + r.HandleInternalServerError(fmt.Sprintf("failed to sign token for robot account, %v", err)) err := dao.DeleteRobot(id) if err != nil { r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err)) diff --git a/src/core/api/robot_test.go b/src/core/api/robot_test.go index f69791e717..0ece3a6674 100644 --- a/src/core/api/robot_test.go +++ b/src/core/api/robot_test.go @@ -17,6 +17,7 @@ package api import ( "fmt" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "net/http" "testing" ) @@ -27,6 +28,14 @@ var ( ) func TestRobotAPIPost(t *testing.T) { + + rbacPolicy := &rbac.Policy{ + Resource: "/project/libray/repository", + Action: "pull", + } + policies := []*rbac.Policy{} + policies = append(policies, rbacPolicy) + cases := []*codeCheckingCase{ // 401 { @@ -42,7 +51,7 @@ func TestRobotAPIPost(t *testing.T) { request: &testingRequest{ method: http.MethodPost, url: robotPath, - bodyJSON: &models.Robot{}, + bodyJSON: &models.RobotReq{}, credential: nonSysAdmin, }, code: http.StatusForbidden, @@ -52,9 +61,10 @@ func TestRobotAPIPost(t *testing.T) { request: &testingRequest{ method: http.MethodPost, url: robotPath, - bodyJSON: &models.Robot{ + bodyJSON: &models.RobotReq{ Name: "test", Description: "test desc", + Access: policies, }, credential: projAdmin4Robot, }, @@ -65,7 +75,7 @@ func TestRobotAPIPost(t *testing.T) { request: &testingRequest{ method: http.MethodPost, url: robotPath, - bodyJSON: &models.Robot{ + bodyJSON: &models.RobotReq{ Name: "test2", Description: "test2 desc", }, @@ -79,10 +89,10 @@ func TestRobotAPIPost(t *testing.T) { request: &testingRequest{ method: http.MethodPost, url: robotPath, - bodyJSON: &models.Robot{ + bodyJSON: &models.RobotReq{ Name: "test", Description: "test desc", - ProjectID: 1, + Access: policies, }, credential: projAdmin4Robot, }, diff --git a/src/core/filter/security.go b/src/core/filter/security.go index 62286b46b1..e7a380ca8a 100644 --- a/src/core/filter/security.go +++ b/src/core/filter/security.go @@ -160,18 +160,15 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool { if !ok { return false } - log.Debug("got robot information via token auth") if !strings.HasPrefix(robotName, common.RobotPrefix) { return false } rClaims := &token.RobotClaims{} - htk := &token.HToken{} htk, err := token.ParseWithClaims(robotTk, rClaims) if err != nil { log.Errorf("failed to decrypt robot token, %v", err) return false } - log.Infof(fmt.Sprintf("got robot token header, %v", htk.Header)) // Do authn for robot account, as Harbor only stores the token ID, just validate the ID and disable. robot, err := dao.GetRobotByID(htk.Claims.(*token.RobotClaims).TokenID) if err != nil { @@ -179,7 +176,7 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool { return false } if robot == nil { - log.Error("the token is not valid.") + log.Error("the token provided doesn't exist.") return false } if robotName != robot.Name { @@ -192,7 +189,7 @@ func (r *robotAuthReqCtxModifier) Modify(ctx *beegoctx.Context) bool { } log.Debug("creating robot account security context...") pm := config.GlobalProjectMgr - securCtx := robotCtx.NewSecurityContext(robot, pm, htk.Claims.(*token.RobotClaims).Policy) + securCtx := robotCtx.NewSecurityContext(robot, pm, htk.Claims.(*token.RobotClaims).Access) setSecurCtxAndPM(ctx.Request, securCtx, pm) return true } From 0ab7c93e16cfb4b56646e1216b4f1936766056a3 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Tue, 29 Jan 2019 01:26:38 +0800 Subject: [PATCH 21/45] Replace casbin builtin keyMatch2 with custom match func Signed-off-by: He Weiwei --- src/common/rbac/casbin.go | 32 +++++++++++++++++- src/common/rbac/casbin_test.go | 59 ++++++++++++++++++++++++++++++++++ src/common/rbac/rbac_test.go | 6 ++-- 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/common/rbac/casbin_test.go diff --git a/src/common/rbac/casbin.go b/src/common/rbac/casbin.go index 306997f542..4ec835bd2f 100644 --- a/src/common/rbac/casbin.go +++ b/src/common/rbac/casbin.go @@ -17,10 +17,13 @@ package rbac import ( "errors" "fmt" + "regexp" + "strings" "github.com/casbin/casbin" "github.com/casbin/casbin/model" "github.com/casbin/casbin/persist" + "github.com/casbin/casbin/util" ) var ( @@ -50,6 +53,30 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == '*') ` +// keyMatch2 determines whether key1 matches the pattern of key2, its behavior most likely the builtin KeyMatch2 +// except that the match of ("/project/1/robot", "/project/1") will return false +func keyMatch2(key1 string, key2 string) bool { + key2 = strings.Replace(key2, "/*", "/.*", -1) + + re := regexp.MustCompile(`(.*):[^/]+(.*)`) + for { + if !strings.Contains(key2, "/:") { + break + } + + key2 = re.ReplaceAllString(key2, "$1[^/]+$2") + } + + return util.RegexMatch(key1, "^"+key2+"$") +} + +func keyMatch2Func(args ...interface{}) (interface{}, error) { + name1 := args[0].(string) + name2 := args[1].(string) + + return bool(keyMatch2(name1, name2)), nil +} + type userAdapter struct { User } @@ -134,5 +161,8 @@ func (a *userAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex func enforcerForUser(user User) *casbin.Enforcer { m := model.Model{} m.LoadModelFromText(modelText) - return casbin.NewEnforcer(m, &userAdapter{User: user}) + + e := casbin.NewEnforcer(m, &userAdapter{User: user}) + e.AddFunction("keyMatch2", keyMatch2Func) + return e } diff --git a/src/common/rbac/casbin_test.go b/src/common/rbac/casbin_test.go new file mode 100644 index 0000000000..01c6f04f86 --- /dev/null +++ b/src/common/rbac/casbin_test.go @@ -0,0 +1,59 @@ +// Copyright Project Harbor Authors +// +// 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 rbac + +import ( + "testing" +) + +func Test_keyMatch2(t *testing.T) { + type args struct { + key1 string + key2 string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "match /project/1/robot, /project/1", + args: args{"/project/1/robot", "/project/1"}, + want: false, + }, + { + name: "match /project/1/robot, /project/:pid", + args: args{"/project/1/robot", "/project/:pid"}, + want: false, + }, + { + name: "match /project/1/robot, /project/1/*", + args: args{"/project/1/robot", "/project/1/*"}, + want: true, + }, + { + name: "match /project/1/robot, /project/:pid/robot", + args: args{"/project/1/robot", "/project/:pid/robot"}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := keyMatch2(tt.args.key1, tt.args.key2); got != tt.want { + t.Errorf("keyMatch2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/common/rbac/rbac_test.go b/src/common/rbac/rbac_test.go index 740b236630..e8881b4f45 100644 --- a/src/common/rbac/rbac_test.go +++ b/src/common/rbac/rbac_test.go @@ -127,7 +127,7 @@ func TestHasPermissionUserWithoutRoles(t *testing.T) { { name: "project create for user without roles", args: args{ - &userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}}, + &userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}}, "/project", "create", }, @@ -136,7 +136,7 @@ func TestHasPermissionUserWithoutRoles(t *testing.T) { { name: "project delete test for user without roles", args: args{ - &userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}}, + &userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}}, "/project", "delete", }, @@ -168,7 +168,7 @@ func TestHasPermissionUsernameEmpty(t *testing.T) { { name: "project create for user without roles", args: args{ - &userWithoutRoles{Username: "", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}}, + &userWithoutRoles{Username: "", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}}, "/project", "create", }, From 4c9b98a463f602e96a88c82fff0459b534a96443 Mon Sep 17 00:00:00 2001 From: Brett Johnson Date: Thu, 10 Jan 2019 07:21:18 +1100 Subject: [PATCH 22/45] Updated clair db import for Harbor 1.6+ Signed-off-by: Brett Johnson --- docs/import_vulnerability_data.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/import_vulnerability_data.md b/docs/import_vulnerability_data.md index e39edda360..797ad11da6 100644 --- a/docs/import_vulnerability_data.md +++ b/docs/import_vulnerability_data.md @@ -2,13 +2,13 @@ Harbor has integrated with Clair to scan vulnerabilities in images. When Harbor is installed in an environment without internet connection, Clair cannot fetch data from the public vulnerability database. Under this circumstance, Harbor administrator needs to manually update the Clair database. -This document provides step-by-step instructions on updating Clair vulnerability database in Harbor v1.2. +This document provides step-by-step instructions on updating Clair vulnerability database in Harbor. **NOTE:** Harbor does not ship with any vulnerability data. For this reason, if Harbor cannot connect to Internet, the administrator must manually import vulnerability data to Harbor by using instructions given in this document. ### Preparation -A. You need to install an instance of Clair 2.0.1 with internet connection. If you have another instance of Harbor v1.2 with internet access, it also works. +A. You need to install an instance of Clair with internet connection. If you have another instance of Harbor with internet access, it also works. B. Check whether your Clair instance has already updated the vulnerability database to the latest version. If it has not, wait for Clair to get the data from public endpoints. @@ -29,28 +29,39 @@ B. Check whether your Clair instance has already updated the vulnerability datab ``` - The phrase "finished fetching" indicates that Clair has finished a round of vulnerability update from an endpoint. Please make sure all five endpoints (rhel, alpine, oracle, debian, ubuntu) are updated correctly. +## Harbor version < 1.6 + +If you're using a version of Harbor prior to 1.6, you can access the correct instructions for your version using the following URL. +https://github.com/goharbor/harbor/blob/v\/docs/import_vulnerability_data.md + +## Harbor version >= 1.6 + +Databased were consolidated in version 1.6 which moved the clair database to the harbor-db container and removed the clair-db container. + ### Dumping vulnerability data - Log in to the host (that is connected to Internet) where Clair database (Postgres) is running. - Dump Clair's vulnerability database by the following commands, two files (`vulnerability.sql` and `clear.sql`) are generated: +_NOTE: The container name 'clair-db' is a placeholder for the db container used by the internet connected instance of clair_ + ``` - $ docker exec clair-db /bin/bash -c "pg_dump -U postgres -a -t feature -t keyvalue -t namespace -t schema_migrations -t vulnerability -t vulnerability_fixedin_feature" > vulnerability.sql - $ docker exec clair-db /bin/bash -c "pg_dump -U postgres -c -s" > clear.sql + $ docker exec clair-db /bin/sh -c "pg_dump -U postgres -a -t feature -t keyvalue -t namespace -t schema_migrations -t vulnerability -t vulnerability_fixedin_feature" > vulnerability.sql + $ docker exec clair-db /bin/sh -c "pg_dump -U postgres -c -s" > clear.sql ``` ### Back up Harbor's Clair database Before importing the data, it is strongly recommended to back up the Clair database in Harbor. ``` - $ docker exec clair-db /bin/bash -c "pg_dump -U postgres -c" > all.sql + $ docker exec harbor-db /bin/sh -c "pg_dump -U postgres -c" > all.sql ``` ### Update Harbor's Clair database Copy the `vulnerability.sql` and `clear.sql` to the host where Harbor is running on. Run the below commands to import the data to Harbor's Clair database: ``` - $ docker exec -i clair-db psql -U postgres < clear.sql - $ docker exec -i clair-db psql -U postgres < vulnerability.sql + $ docker exec -i harbor-db psql -U postgres < clear.sql + $ docker exec -i harbor-db psql -U postgres < vulnerability.sql ``` ### Rescanning images From 6e95b98108e17155cccfd1d53ae53ce1bef557a5 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Tue, 29 Jan 2019 11:58:35 +0800 Subject: [PATCH 23/45] Standard actions for rbac Signed-off-by: He Weiwei --- src/common/rbac/const.go | 53 ++++++ src/common/rbac/project/const.go | 61 ------ src/common/rbac/project/util.go | 114 ++++++------ src/common/rbac/project/visitor_role.go | 238 +++++++++++++----------- src/common/security/admiral/context.go | 6 +- src/common/security/local/context.go | 6 +- src/core/api/chart_repository_test.go | 5 +- 7 files changed, 246 insertions(+), 237 deletions(-) create mode 100644 src/common/rbac/const.go delete mode 100644 src/common/rbac/project/const.go diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go new file mode 100644 index 0000000000..e0894d763a --- /dev/null +++ b/src/common/rbac/const.go @@ -0,0 +1,53 @@ +// Copyright Project Harbor Authors +// +// 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 rbac + +// const action variables +const ( + ActionAll = Action("*") // action match any other actions + + ActionPull = Action("pull") // pull repository tag + ActionPush = Action("push") // push repository tag + ActionPushPull = Action("push+pull") // compatible with security all perm of project + + // create, read, update, delete, list actions compatible with restful api methods + ActionCreate = Action("create") + ActionRead = Action("read") + ActionUpdate = Action("update") + ActionDelete = Action("delete") + ActionList = Action("list") +) + +// const resource variables +const ( + ResourceAll = Resource("*") // resource match any other resources + ResourceConfiguration = Resource("configuration") // project configuration compatible for portal only + ResourceHelmChart = Resource("helm-chart") + ResourceHelmChartVersion = Resource("helm-chart-version") + ResourceHelmChartVersionLabel = Resource("helm-chart-version-label") + ResourceLabel = Resource("label") + ResourceLog = Resource("log") + ResourceMember = Resource("member") + ResourceReplication = Resource("replication") + ResourceReplicationJob = Resource("replication-job") + ResourceRepository = Resource("repository") + ResourceRepositoryTag = Resource("repository-tag") + ResourceRepositoryTagLabel = Resource("repository-tag-label") + ResourceRepositoryTagManifest = Resource("repository-tag-manifest") + ResourceRepositoryTagScanJob = Resource("repository-tag-scan-job") + ResourceRepositoryTagVulnerability = Resource("repository-tag-vulnerability") + ResourceRobot = Resource("robot") + ResourceSelf = Resource("") // subresource for self +) diff --git a/src/common/rbac/project/const.go b/src/common/rbac/project/const.go deleted file mode 100644 index c4c14f7033..0000000000 --- a/src/common/rbac/project/const.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright Project Harbor Authors -// -// 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 project - -import ( - "github.com/goharbor/harbor/src/common/rbac" -) - -// const action variables -const ( - ActionAll = rbac.Action("*") // action match any other actions - - ActionPull = rbac.Action("pull") // pull repository tag - ActionPush = rbac.Action("push") // push repository tag - ActionPushPull = rbac.Action("push+pull") // compatible with security all perm of project - - // create, read, update, delete, list actions compatible with restful api methods - ActionCreate = rbac.Action("create") - ActionRead = rbac.Action("read") - ActionUpdate = rbac.Action("update") - ActionDelete = rbac.Action("delete") - ActionList = rbac.Action("list") - - // execute replication for the replication policy (replication rule) - ActionExecute = rbac.Action("execute") - - // vulnerabilities scan for repository tag (aka, image tag) - ActionScan = rbac.Action("scan") -) - -// const resource variables -const ( - ResourceAll = rbac.Resource("*") // resource match any other resources - ResourceSelf = rbac.Resource("") // subresource for project self - ResourceMember = rbac.Resource("member") - ResourceLog = rbac.Resource("log") - ResourceReplication = rbac.Resource("replication") - ResourceLabel = rbac.Resource("label") - ResourceRepository = rbac.Resource("repository") - ResourceRepositoryTag = rbac.Resource("repository-tag") - ResourceRepositoryTagManifest = rbac.Resource("repository-tag-manifest") - ResourceRepositoryTagVulnerability = rbac.Resource("repository-tag-vulnerability") - ResourceRepositoryTagLabel = rbac.Resource("repository-tag-label") - ResourceHelmChart = rbac.Resource("helm-chart") - ResourceHelmChartVersion = rbac.Resource("helm-chart-version") - ResourceHelmChartVersionLabel = rbac.Resource("helm-chart-version-label") - ResourceConfiguration = rbac.Resource("configuration") // compatible for portal only - ResourceRobot = rbac.Resource("robot") -) diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index 1515f65e4e..ac09110268 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -21,81 +21,87 @@ import ( var ( // subresource policies for public project publicProjectPolicies = []*rbac.Policy{ - {Resource: ResourceSelf, Action: ActionRead}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, - {Resource: ResourceRepository, Action: ActionList}, - {Resource: ResourceRepository, Action: ActionPull}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, - {Resource: ResourceHelmChart, Action: ActionRead}, - {Resource: ResourceHelmChart, Action: ActionList}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, - {Resource: ResourceHelmChartVersion, Action: ActionRead}, - {Resource: ResourceHelmChartVersion, Action: ActionList}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, } // all policies for the projects allPolicies = []*rbac.Policy{ - {Resource: ResourceSelf, Action: ActionRead}, - {Resource: ResourceSelf, Action: ActionUpdate}, - {Resource: ResourceSelf, Action: ActionDelete}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionDelete}, - {Resource: ResourceMember, Action: ActionCreate}, - {Resource: ResourceMember, Action: ActionUpdate}, - {Resource: ResourceMember, Action: ActionDelete}, - {Resource: ResourceMember, Action: ActionList}, + {Resource: rbac.ResourceMember, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceMember, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceMember, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceMember, Action: rbac.ActionList}, - {Resource: ResourceLog, Action: ActionList}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, - {Resource: ResourceReplication, Action: ActionList}, - {Resource: ResourceReplication, Action: ActionCreate}, - {Resource: ResourceReplication, Action: ActionUpdate}, - {Resource: ResourceReplication, Action: ActionDelete}, - {Resource: ResourceReplication, Action: ActionExecute}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionList}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionDelete}, - {Resource: ResourceLabel, Action: ActionCreate}, - {Resource: ResourceLabel, Action: ActionUpdate}, - {Resource: ResourceLabel, Action: ActionDelete}, - {Resource: ResourceLabel, Action: ActionList}, + {Resource: rbac.ResourceReplicationJob, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead}, + {Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList}, - {Resource: ResourceRepository, Action: ActionCreate}, - {Resource: ResourceRepository, Action: ActionUpdate}, - {Resource: ResourceRepository, Action: ActionDelete}, - {Resource: ResourceRepository, Action: ActionList}, - {Resource: ResourceRepository, Action: ActionPushPull}, // compatible with security all perm of project - {Resource: ResourceRepository, Action: ActionPush}, - {Resource: ResourceRepository, Action: ActionPull}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTag, Action: ActionDelete}, - {Resource: ResourceRepositoryTag, Action: ActionList}, - {Resource: ResourceRepositoryTag, Action: ActionScan}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project + {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, - {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + {Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead}, - {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, - {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + {Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList}, - {Resource: ResourceHelmChart, Action: ActionCreate}, - {Resource: ResourceHelmChart, Action: ActionRead}, - {Resource: ResourceHelmChart, Action: ActionDelete}, - {Resource: ResourceHelmChart, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead}, - {Resource: ResourceHelmChartVersion, Action: ActionRead}, - {Resource: ResourceHelmChartVersion, Action: ActionDelete}, - {Resource: ResourceHelmChartVersion, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, - {Resource: ResourceConfiguration, Action: ActionRead}, - {Resource: ResourceConfiguration, Action: ActionUpdate}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, - {Resource: ResourceRobot, Action: ActionCreate}, - {Resource: ResourceRobot, Action: ActionRead}, - {Resource: ResourceRobot, Action: ActionUpdate}, - {Resource: ResourceRobot, Action: ActionDelete}, - {Resource: ResourceRobot, Action: ActionList}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete}, + + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate}, + + {Resource: rbac.ResourceRobot, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionList}, } ) diff --git a/src/common/rbac/project/visitor_role.go b/src/common/rbac/project/visitor_role.go index 0ae5bc1a29..ac499887d6 100644 --- a/src/common/rbac/project/visitor_role.go +++ b/src/common/rbac/project/visitor_role.go @@ -22,175 +22,187 @@ import ( var ( rolePoliciesMap = map[string][]*rbac.Policy{ "projectAdmin": { - {Resource: ResourceSelf, Action: ActionRead}, - {Resource: ResourceSelf, Action: ActionUpdate}, - {Resource: ResourceSelf, Action: ActionDelete}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionDelete}, - {Resource: ResourceMember, Action: ActionCreate}, - {Resource: ResourceMember, Action: ActionUpdate}, - {Resource: ResourceMember, Action: ActionDelete}, - {Resource: ResourceMember, Action: ActionList}, + {Resource: rbac.ResourceMember, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceMember, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceMember, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceMember, Action: rbac.ActionList}, - {Resource: ResourceLog, Action: ActionList}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, - {Resource: ResourceReplication, Action: ActionRead}, - {Resource: ResourceReplication, Action: ActionList}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionRead}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionList}, - {Resource: ResourceLabel, Action: ActionCreate}, - {Resource: ResourceLabel, Action: ActionUpdate}, - {Resource: ResourceLabel, Action: ActionDelete}, - {Resource: ResourceLabel, Action: ActionList}, + {Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead}, + {Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList}, - {Resource: ResourceRepository, Action: ActionCreate}, - {Resource: ResourceRepository, Action: ActionUpdate}, - {Resource: ResourceRepository, Action: ActionDelete}, - {Resource: ResourceRepository, Action: ActionList}, - {Resource: ResourceRepository, Action: ActionPushPull}, // compatible with security all perm of project - {Resource: ResourceRepository, Action: ActionPush}, - {Resource: ResourceRepository, Action: ActionPull}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTag, Action: ActionDelete}, - {Resource: ResourceRepositoryTag, Action: ActionList}, - {Resource: ResourceRepositoryTag, Action: ActionScan}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project + {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, - {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + {Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead}, - {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, - {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + {Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList}, - {Resource: ResourceHelmChart, Action: ActionCreate}, // upload helm chart - {Resource: ResourceHelmChart, Action: ActionRead}, // download helm chart - {Resource: ResourceHelmChart, Action: ActionDelete}, - {Resource: ResourceHelmChart, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead}, - {Resource: ResourceHelmChartVersion, Action: ActionCreate}, // upload helm chart version - {Resource: ResourceHelmChartVersion, Action: ActionRead}, // read and download helm chart version - {Resource: ResourceHelmChartVersion, Action: ActionDelete}, - {Resource: ResourceHelmChartVersion, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, // upload helm chart + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, // download helm chart + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, - {Resource: ResourceConfiguration, Action: ActionRead}, - {Resource: ResourceConfiguration, Action: ActionUpdate}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, // upload helm chart version + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, // read and download helm chart version + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, - {Resource: ResourceRobot, Action: ActionCreate}, - {Resource: ResourceRobot, Action: ActionRead}, - {Resource: ResourceRobot, Action: ActionUpdate}, - {Resource: ResourceRobot, Action: ActionDelete}, - {Resource: ResourceRobot, Action: ActionList}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete}, + + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate}, + + {Resource: rbac.ResourceRobot, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionList}, }, "master": { - {Resource: ResourceSelf, Action: ActionRead}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, - {Resource: ResourceMember, Action: ActionList}, + {Resource: rbac.ResourceMember, Action: rbac.ActionList}, - {Resource: ResourceLog, Action: ActionList}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, - {Resource: ResourceReplication, Action: ActionRead}, - {Resource: ResourceReplication, Action: ActionList}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionRead}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionList}, - {Resource: ResourceLabel, Action: ActionCreate}, - {Resource: ResourceLabel, Action: ActionUpdate}, - {Resource: ResourceLabel, Action: ActionDelete}, - {Resource: ResourceLabel, Action: ActionList}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, - {Resource: ResourceRepository, Action: ActionCreate}, - {Resource: ResourceRepository, Action: ActionUpdate}, - {Resource: ResourceRepository, Action: ActionDelete}, - {Resource: ResourceRepository, Action: ActionList}, - {Resource: ResourceRepository, Action: ActionPush}, - {Resource: ResourceRepository, Action: ActionPull}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, - {Resource: ResourceRepositoryTag, Action: ActionDelete}, - {Resource: ResourceRepositoryTag, Action: ActionList}, - {Resource: ResourceRepositoryTag, Action: ActionScan}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead}, - {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + {Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, - {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + {Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead}, - {Resource: ResourceHelmChart, Action: ActionCreate}, - {Resource: ResourceHelmChart, Action: ActionRead}, - {Resource: ResourceHelmChart, Action: ActionDelete}, - {Resource: ResourceHelmChart, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, - {Resource: ResourceHelmChartVersion, Action: ActionCreate}, - {Resource: ResourceHelmChartVersion, Action: ActionRead}, - {Resource: ResourceHelmChartVersion, Action: ActionDelete}, - {Resource: ResourceHelmChartVersion, Action: ActionList}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, - {Resource: ResourceConfiguration, Action: ActionRead}, - {Resource: ResourceConfiguration, Action: ActionUpdate}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete}, + + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate}, }, "developer": { - {Resource: ResourceSelf, Action: ActionRead}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, - {Resource: ResourceMember, Action: ActionList}, + {Resource: rbac.ResourceMember, Action: rbac.ActionList}, - {Resource: ResourceLog, Action: ActionList}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, - {Resource: ResourceRepository, Action: ActionCreate}, - {Resource: ResourceRepository, Action: ActionList}, - {Resource: ResourceRepository, Action: ActionPush}, - {Resource: ResourceRepository, Action: ActionPull}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, - {Resource: ResourceRepositoryTag, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + {Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead}, - {Resource: ResourceRepositoryTagLabel, Action: ActionCreate}, - {Resource: ResourceRepositoryTagLabel, Action: ActionDelete}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, - {Resource: ResourceHelmChart, Action: ActionCreate}, - {Resource: ResourceHelmChart, Action: ActionRead}, - {Resource: ResourceHelmChart, Action: ActionList}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, - {Resource: ResourceHelmChartVersion, Action: ActionCreate}, - {Resource: ResourceHelmChartVersion, Action: ActionRead}, - {Resource: ResourceHelmChartVersion, Action: ActionList}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionCreate}, - {Resource: ResourceHelmChartVersionLabel, Action: ActionDelete}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete}, - {Resource: ResourceConfiguration, Action: ActionRead}, + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, }, "guest": { - {Resource: ResourceSelf, Action: ActionRead}, + {Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, - {Resource: ResourceMember, Action: ActionList}, + {Resource: rbac.ResourceMember, Action: rbac.ActionList}, - {Resource: ResourceLog, Action: ActionList}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, - {Resource: ResourceRepository, Action: ActionList}, - {Resource: ResourceRepository, Action: ActionPull}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, - {Resource: ResourceRepositoryTag, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagVulnerability, Action: ActionList}, + {Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList}, - {Resource: ResourceRepositoryTagManifest, Action: ActionRead}, + {Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead}, - {Resource: ResourceHelmChart, Action: ActionRead}, - {Resource: ResourceHelmChart, Action: ActionList}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, - {Resource: ResourceHelmChartVersion, Action: ActionRead}, - {Resource: ResourceHelmChartVersion, Action: ActionList}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, - {Resource: ResourceConfiguration, Action: ActionRead}, + {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, }, } ) diff --git a/src/common/security/admiral/context.go b/src/common/security/admiral/context.go index 72840d36b0..9abc5faea9 100644 --- a/src/common/security/admiral/context.go +++ b/src/common/security/admiral/context.go @@ -72,19 +72,19 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) + return s.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) + return s.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) + return s.Can(rbac.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // Can returns whether the user can do action on resource diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index d564332142..f0d33ceedd 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -70,19 +70,19 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) + return s.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) + return s.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceRepository)) + return s.Can(rbac.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // Can returns whether the user can do action on resource diff --git a/src/core/api/chart_repository_test.go b/src/core/api/chart_repository_test.go index 4bcb1f009c..05a3c138fd 100644 --- a/src/core/api/chart_repository_test.go +++ b/src/core/api/chart_repository_test.go @@ -9,7 +9,6 @@ import ( "github.com/goharbor/harbor/src/chartserver" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/rbac" - "github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/core/promgr/metamgr" ) @@ -313,12 +312,12 @@ func (msc *mockSecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - return msc.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceRepository)) + return msc.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, false).Resource(rbac.ResourceRepository)) } // HasWritePerm returns whether the user has write permission to the project func (msc *mockSecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - return msc.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, false).Resource(project.ResourceRepository)) + return msc.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, false).Resource(rbac.ResourceRepository)) } // HasAllPerm returns whether the user has all permissions to the project From 404ee307f35125bffa2322d2efa98fc42464395a Mon Sep 17 00:00:00 2001 From: FangyuanCheng Date: Thu, 17 Jan 2019 17:16:07 +0800 Subject: [PATCH 24/45] Support Robot account in Harbor Signed-off-by: FangyuanCheng --- src/portal/lib/src/_mixin.scss | 2 +- .../src/helm-chart/helm-chart.component.scss | 2 +- .../helm-chart-version.component.scss | 2 +- .../repository-gridview.component.scss | 2 +- src/portal/lib/src/shared/shared.const.ts | 1 + src/portal/src/app/config/config.module.ts | 45 ++-- src/portal/src/app/harbor-routing.module.ts | 5 + .../project-detail.component.html | 3 + src/portal/src/app/project/project.module.ts | 9 +- .../add-robot/add-robot.component.html | 102 +++++++++ .../add-robot/add-robot.component.scss | 31 +++ .../add-robot/add-robot.component.spec.ts | 25 +++ .../add-robot/add-robot.component.ts | 191 +++++++++++++++++ .../robot-account.component.html | 74 +++++++ .../robot-account.component.scss | 28 +++ .../robot-account.component.spec.ts | 25 +++ .../robot-account/robot-account.component.ts | 201 ++++++++++++++++++ .../robot-account/robot-account.service.ts | 64 ++++++ .../robot-account/robot.api.repository.ts | 44 ++++ .../src/app/project/robot-account/robot.ts | 20 ++ .../confirmation-dialog.component.scss | 1 + src/portal/src/app/shared/shared.const.ts | 1 + src/portal/src/i18n/lang/en-us-lang.json | 29 ++- src/portal/src/i18n/lang/es-es-lang.json | 29 ++- src/portal/src/i18n/lang/fr-fr-lang.json | 29 ++- src/portal/src/i18n/lang/pt-br-lang.json | 29 ++- src/portal/src/i18n/lang/zh-cn-lang.json | 29 ++- src/portal/src/styles.css | 8 + 28 files changed, 1000 insertions(+), 31 deletions(-) create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.html create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts create mode 100644 src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.html create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.scss create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.spec.ts create mode 100644 src/portal/src/app/project/robot-account/robot-account.component.ts create mode 100644 src/portal/src/app/project/robot-account/robot-account.service.ts create mode 100644 src/portal/src/app/project/robot-account/robot.api.repository.ts create mode 100644 src/portal/src/app/project/robot-account/robot.ts diff --git a/src/portal/lib/src/_mixin.scss b/src/portal/lib/src/_mixin.scss index ea15edcc61..a29a9c3250 100644 --- a/src/portal/lib/src/_mixin.scss +++ b/src/portal/lib/src/_mixin.scss @@ -12,7 +12,7 @@ @include text-overflow; } -@mixin grid-left-top-pos{ +@mixin grid-right-top-pos{ position: absolute; z-index: 100; right: 35px; diff --git a/src/portal/lib/src/helm-chart/helm-chart.component.scss b/src/portal/lib/src/helm-chart/helm-chart.component.scss index cbbb50b7ed..80a7590f98 100644 --- a/src/portal/lib/src/helm-chart/helm-chart.component.scss +++ b/src/portal/lib/src/helm-chart/helm-chart.component.scss @@ -12,7 +12,7 @@ $size60:60px; .toolbar { overflow: hidden; .rightPos { - @include grid-left-top-pos; + @include grid-right-top-pos; margin-top: 20px; .filter-divider { display: inline-block; diff --git a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss index 27a21a5d1c..2459e977b0 100644 --- a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss +++ b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.scss @@ -16,7 +16,7 @@ .toolbar { overflow: hidden; .rightPos { - @include grid-left-top-pos; + @include grid-right-top-pos; .filter-divider { display: inline-block; height: 16px; diff --git a/src/portal/lib/src/repository-gridview/repository-gridview.component.scss b/src/portal/lib/src/repository-gridview/repository-gridview.component.scss index f345da7449..8ee3cbb316 100644 --- a/src/portal/lib/src/repository-gridview/repository-gridview.component.scss +++ b/src/portal/lib/src/repository-gridview/repository-gridview.component.scss @@ -1,7 +1,7 @@ @import '../mixin'; .rightPos{ - @include grid-left-top-pos; + @include grid-right-top-pos; } .toolbar { diff --git a/src/portal/lib/src/shared/shared.const.ts b/src/portal/lib/src/shared/shared.const.ts index bb91dbc1ce..3c6c83fee0 100644 --- a/src/portal/lib/src/shared/shared.const.ts +++ b/src/portal/lib/src/shared/shared.const.ts @@ -33,6 +33,7 @@ export const enum ConfirmationTargets { PROJECT, PROJECT_MEMBER, USER, + ROBOT_ACCOUNT, POLICY, TOGGLE_CONFIRM, TARGET, diff --git a/src/portal/src/app/config/config.module.ts b/src/portal/src/app/config/config.module.ts index cecf8f6fab..7556251fce 100644 --- a/src/portal/src/app/config/config.module.ts +++ b/src/portal/src/app/config/config.module.ts @@ -11,27 +11,24 @@ // 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. -import { NgModule } from '@angular/core'; -import { CoreModule } from '../core/core.module'; -import { SharedModule } from '../shared/shared.module'; - -import { ConfigurationComponent } from './config.component'; -import { ConfigurationService } from './config.service'; -import { ConfirmMessageHandler } from './config.msg.utils'; -import { ConfigurationAuthComponent } from './auth/config-auth.component'; -import { ConfigurationEmailComponent } from './email/config-email.component'; -import { GcComponent } from './gc/gc.component'; -import { GcRepoService } from './gc/gc.service'; -import { GcApiRepository } from './gc/gc.api.repository'; -import { GcViewModelFactory } from './gc/gc.viewmodel.factory'; -import { GcUtility } from './gc/gc.utility'; +import { NgModule } from "@angular/core"; +import { CoreModule } from "../core/core.module"; +import { SharedModule } from "../shared/shared.module"; +import { ConfigurationComponent } from "./config.component"; +import { ConfigurationService } from "./config.service"; +import { ConfirmMessageHandler } from "./config.msg.utils"; +import { ConfigurationAuthComponent } from "./auth/config-auth.component"; +import { ConfigurationEmailComponent } from "./email/config-email.component"; +import { GcComponent } from "./gc/gc.component"; +import { GcRepoService } from "./gc/gc.service"; +import { GcApiRepository } from "./gc/gc.api.repository"; +import { RobotApiRepository } from "../project/robot-account/robot.api.repository"; +import { GcViewModelFactory } from "./gc/gc.viewmodel.factory"; +import { GcUtility } from "./gc/gc.utility"; @NgModule({ - imports: [ - CoreModule, - SharedModule - ], + imports: [CoreModule, SharedModule], declarations: [ ConfigurationComponent, ConfigurationAuthComponent, @@ -39,6 +36,14 @@ import { GcUtility } from './gc/gc.utility'; GcComponent ], exports: [ConfigurationComponent], - providers: [ConfigurationService, GcRepoService, GcApiRepository, GcViewModelFactory, GcUtility, ConfirmMessageHandler] + providers: [ + ConfigurationService, + GcRepoService, + GcApiRepository, + GcViewModelFactory, + GcUtility, + ConfirmMessageHandler, + RobotApiRepository + ] }) -export class ConfigurationModule { } +export class ConfigurationModule {} diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index 5eab2b3c14..4ec78ac7e9 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -44,6 +44,7 @@ import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-reposit import { ProjectComponent } from './project/project.component'; import { ProjectDetailComponent } from './project/project-detail/project-detail.component'; import { MemberComponent } from './project/member/member.component'; +import { RobotAccountComponent } from './project/robot-account/robot-account.component'; import { ProjectLabelComponent } from "./project/project-label/project-label.component"; import { ProjectConfigComponent } from './project/project-config/project-config.component'; import { ProjectRoutingResolver } from './project/project-routing-resolver.service'; @@ -178,6 +179,10 @@ const harborRoutes: Routes = [ { path: 'configs', component: ProjectConfigComponent + }, + { + path: 'robot-account', + component: RobotAccountComponent } ] }, diff --git a/src/portal/src/app/project/project-detail/project-detail.component.html b/src/portal/src/app/project/project-detail/project-detail.component.html index 0b6853a707..7b43be2cad 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.html +++ b/src/portal/src/app/project/project-detail/project-detail.component.html @@ -22,6 +22,9 @@ + diff --git a/src/portal/src/app/project/project.module.ts b/src/portal/src/app/project/project.module.ts index cacb0b4e91..96ef8aa88e 100644 --- a/src/portal/src/app/project/project.module.ts +++ b/src/portal/src/app/project/project.module.ts @@ -30,6 +30,7 @@ import { AddGroupComponent } from './member/add-group/add-group.component'; import { ProjectService } from './project.service'; import { MemberService } from './member/member.service'; +import { RobotService } from './robot-account/robot-account.service'; import { ProjectRoutingResolver } from './project-routing-resolver.service'; import { TargetExistsValidatorDirective } from '../shared/target-exists-directive'; @@ -37,6 +38,8 @@ import { ProjectLabelComponent } from "../project/project-label/project-label.co import { ListChartsComponent } from './list-charts/list-charts.component'; import { ListChartVersionsComponent } from './list-chart-versions/list-chart-versions.component'; import { ChartDetailComponent } from './chart-detail/chart-detail.component'; +import { RobotAccountComponent } from './robot-account/robot-account.component'; +import { AddRobotComponent } from './robot-account/add-robot/add-robot.component'; @NgModule({ imports: [ @@ -58,10 +61,12 @@ import { ChartDetailComponent } from './chart-detail/chart-detail.component'; AddGroupComponent, ListChartsComponent, ListChartVersionsComponent, - ChartDetailComponent + ChartDetailComponent, + RobotAccountComponent, + AddRobotComponent ], exports: [ProjectComponent, ListProjectComponent], - providers: [ProjectRoutingResolver, ProjectService, MemberService] + providers: [ProjectRoutingResolver, ProjectService, MemberService, RobotService] }) export class ProjectModule { diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.html b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.html new file mode 100644 index 0000000000..87bc66d208 --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.html @@ -0,0 +1,102 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss new file mode 100644 index 0000000000..7271c7727b --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.scss @@ -0,0 +1,31 @@ +.rule-width { + width: 100%; +} + +.input-width { + width: 200px; +} + +.copy-token { + .success-icon { + color: #318700; + } + .show-info { + .robot-name { + margin: 30px 0; + + label { + margin-right: 30px; + } + } + .robot-token { + margin-bottom: 20px; + label { + margin-right: 24px; + } + .copy-input { + display: inline-block; + } + } + } +} diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts new file mode 100644 index 0000000000..0f45bd6c27 --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddRobotComponent } from './add-robot.component'; + +describe('AddRobotComponent', () => { + let component: AddRobotComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddRobotComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddRobotComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts new file mode 100644 index 0000000000..61922c4e14 --- /dev/null +++ b/src/portal/src/app/project/robot-account/add-robot/add-robot.component.ts @@ -0,0 +1,191 @@ +import { + Component, + OnInit, + Input, + ViewChild, + OnDestroy, + Output, + EventEmitter, + ChangeDetectorRef +} from "@angular/core"; +import { Robot } from "../robot"; +import { NgForm } from "@angular/forms"; +import { Subject } from "rxjs"; +import { debounceTime, finalize } from "rxjs/operators"; +import { RobotService } from "../robot-account.service"; +import { TranslateService } from "@ngx-translate/core"; +import { ErrorHandler } from "@harbor/ui"; +import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service"; +import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component"; + +@Component({ + selector: "add-robot", + templateUrl: "./add-robot.component.html", + styleUrls: ["./add-robot.component.scss"] +}) +export class AddRobotComponent implements OnInit, OnDestroy { + addRobotOpened: boolean; + copyToken: boolean; + robotToken: string; + robotAccount: string; + isSubmitOnGoing = false; + closable: boolean = false; + staticBackdrop: boolean = true; + isPull: boolean; + isPush: boolean; + createSuccess: string; + isRobotNameValid: boolean = true; + checkOnGoing: boolean = false; + robot: Robot = new Robot(); + robotNameChecker: Subject = new Subject(); + nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME"; + robotForm: NgForm; + @Input() projectId: number; + @Input() projectName: string; + @Output() create = new EventEmitter(); + @ViewChild("robotForm") currentForm: NgForm; + @ViewChild("copyAlert") copyAlert: InlineAlertComponent; + constructor( + private robotService: RobotService, + private translate: TranslateService, + private errorHandler: ErrorHandler, + private cdr: ChangeDetectorRef, + private messageHandlerService: MessageHandlerService + ) {} + + ngOnInit(): void { + this.robotNameChecker.pipe(debounceTime(800)).subscribe((name: string) => { + let cont = this.currentForm.controls["robot_name"]; + if (cont) { + this.isRobotNameValid = cont.valid; + if (this.isRobotNameValid) { + this.checkOnGoing = true; + this.robotService + .listRobotAccount(this.projectId) + .pipe( + finalize(() => { + this.checkOnGoing = false; + let hnd = setInterval(() => this.cdr.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 2000); + }) + ) + .subscribe( + response => { + if (response && response.length) { + if ( + response.find(target => { + return target.name === "robot$" + cont.value; + }) + ) { + this.isRobotNameValid = false; + this.nameTooltipText = "ROBOT_ACCOUNT.ACCOUNT_EXISTING"; + } + } + }, + error => { + this.errorHandler.error(error); + } + ); + } else { + this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME"; + } + } + }); + } + + openAddRobotModal(): void { + if (this.isSubmitOnGoing) { + return; + } + this.robot.name = ""; + this.robot.description = ""; + this.addRobotOpened = true; + this.isRobotNameValid = true; + this.robot = new Robot(); + this.nameTooltipText = "ROBOT_ACCOUNT.ROBOT_NAME"; + } + + onCancel(): void { + this.addRobotOpened = false; + } + + ngOnDestroy(): void { + this.robotNameChecker.unsubscribe(); + } + + onSubmit(): void { + if (this.isSubmitOnGoing) { + return; + } + this.isSubmitOnGoing = true; + this.robotService + .addRobotAccount( + this.projectId, + this.robot.name, + this.robot.description, + this.projectName, + this.robot.access.isPull, + this.robot.access.isPush + ) + .subscribe( + response => { + this.isSubmitOnGoing = false; + this.robotToken = response.Token; + this.robotAccount = response.Name; + this.copyToken = true; + this.create.emit(true); + this.translate + .get("ROBOT_ACCOUNT.CREATED_SUCCESS", { param: this.robotAccount }) + .subscribe((res: string) => { + this.createSuccess = res; + }); + this.addRobotOpened = false; + }, + error => { + this.isSubmitOnGoing = false; + this.errorHandler.error(error); + } + ); + } + + isValid(): boolean { + return ( + this.currentForm && + this.currentForm.valid && + !this.isSubmitOnGoing && + this.isRobotNameValid && + !this.checkOnGoing + ); + } + get shouldDisable(): boolean { + if (this.robot && this.robot.access) { + return ( + !this.isValid() || + (!this.robot.access.isPush && !this.robot.access.isPull) + ); + } + } + + // Handle the form validation + handleValidation(): void { + let cont = this.currentForm.controls["robot_name"]; + if (cont) { + this.robotNameChecker.next(cont.value); + } + } + + onCpError($event: any): void { + if (this.copyAlert) { + this.copyAlert.showInlineError("PUSH_IMAGE.COPY_ERROR"); + } + } + + onCpSuccess($event: any): void { + this.copyToken = false; + this.translate + .get("ROBOT_ACCOUNT.COPY_SUCCESS", { param: this.robotAccount }) + .subscribe((res: string) => { + this.messageHandlerService.showSuccess(res); + }); + } +} diff --git a/src/portal/src/app/project/robot-account/robot-account.component.html b/src/portal/src/app/project/robot-account/robot-account.component.html new file mode 100644 index 0000000000..a1a55a4408 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.html @@ -0,0 +1,74 @@ +
+
+
+
+
+
+ + + + +
+
+
+
+ + + + {{'MEMBER.ACTION' | translate}} + + + + + + + + + {{'ROBOT_ACCOUNT.NAME' | translate}} + {{'ROBOT_ACCOUNT.ENABLED_STATE' | translate}} + {{'ROBOT_ACCOUNT.DESCRIPTION' | translate}} + + {{r.name}} + + + + + {{r.description}} + + + {{pagination.firstItem + 1}} + - + {{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' | + translate}} + {{pagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}} + + + +
+ +
diff --git a/src/portal/src/app/project/robot-account/robot-account.component.scss b/src/portal/src/app/project/robot-account/robot-account.component.scss new file mode 100644 index 0000000000..9278019c0d --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.scss @@ -0,0 +1,28 @@ +@import "../../../../lib/src/mixin"; + +.robot-space { + margin-top: 12px; + position: relative; + + clr-icon.red-position { + margin-left: 2px; + } + + .rightPos { + @include grid-right-top-pos; + + .option-left { + padding-left: 16px; + position: relative; + top: 10px; + } + + .option-right { + padding-right: 16px; + + .refresh-btn { + cursor: pointer; + } + } + } +} diff --git a/src/portal/src/app/project/robot-account/robot-account.component.spec.ts b/src/portal/src/app/project/robot-account/robot-account.component.spec.ts new file mode 100644 index 0000000000..2a1d3c3035 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RobotAccountComponent } from './robot-account.component'; + +describe('RobotAccountComponent', () => { + let component: RobotAccountComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RobotAccountComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RobotAccountComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/project/robot-account/robot-account.component.ts b/src/portal/src/app/project/robot-account/robot-account.component.ts new file mode 100644 index 0000000000..006a59d819 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.component.ts @@ -0,0 +1,201 @@ +import { + Component, + OnInit, + ViewChild, + OnDestroy, + ChangeDetectorRef +} from "@angular/core"; +import { AddRobotComponent } from "./add-robot/add-robot.component"; +import { ActivatedRoute, Router } from "@angular/router"; +import { Robot } from "./robot"; +import { Project } from "./../project"; +import { finalize, catchError, map } from "rxjs/operators"; +import { TranslateService } from "@ngx-translate/core"; +import { Subscription, forkJoin, Observable, throwError } from "rxjs"; +import { MessageHandlerService } from "../../shared/message-handler/message-handler.service"; +import { RobotService } from "./robot-account.service"; +import { ConfirmationMessage } from "../../shared/confirmation-dialog/confirmation-message"; +import { + ConfirmationTargets, + ConfirmationState, + ConfirmationButtons +} from "../../shared/shared.const"; +import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service"; +import { + operateChanges, + OperateInfo, + OperationService, + OperationState +} from "@harbor/ui"; + +@Component({ + selector: "app-robot-account", + templateUrl: "./robot-account.component.html", + styleUrls: ["./robot-account.component.scss"] +}) +export class RobotAccountComponent implements OnInit, OnDestroy { + @ViewChild(AddRobotComponent) + addRobotComponent: AddRobotComponent; + selectedRow: Robot[] = []; + robotsCopy: Robot[] = []; + loading = false; + searchRobot: string; + projectName: string; + timerHandler: any; + batchChangeInfos: {}; + isDisabled: boolean; + isDisabledTip: string = "ROBOT_ACCOUNT.DISABLE_ACCOUNT"; + robots: Robot[]; + projectId: number; + subscription: Subscription; + constructor( + private route: ActivatedRoute, + private robotService: RobotService, + private OperateDialogService: ConfirmationDialogService, + private operationService: OperationService, + private translate: TranslateService, + private ref: ChangeDetectorRef, + private messageHandlerService: MessageHandlerService + ) { + this.subscription = OperateDialogService.confirmationConfirm$.subscribe( + message => { + if ( + message && + message.state === ConfirmationState.CONFIRMED && + message.source === ConfirmationTargets.ROBOT_ACCOUNT + ) { + this.delRobots(message.data); + } + } + ); + this.forceRefreshView(2000); + } + + ngOnInit(): void { + this.projectId = +this.route.snapshot.parent.params["id"]; + let resolverData = this.route.snapshot.parent.data; + if (resolverData) { + let project = resolverData["projectResolver"]; + this.projectName = project.name; + } + this.searchRobot = ""; + this.retrieve(); + } + + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + if (this.timerHandler) { + clearInterval(this.timerHandler); + this.timerHandler = null; + } + } + + openAddRobotModal(): void { + this.addRobotComponent.openAddRobotModal(); + } + + openDeleteRobotsDialog(robots: Robot[]) { + let robotNames = robots.map(robot => robot.name).join(","); + let deletionMessage = new ConfirmationMessage( + "ROBOT_ACCOUNT.DELETION_TITLE", + "ROBOT_ACCOUNT.DELETION_SUMMARY", + robotNames, + robots, + ConfirmationTargets.ROBOT_ACCOUNT, + ConfirmationButtons.DELETE_CANCEL + ); + this.OperateDialogService.openComfirmDialog(deletionMessage); + } + + delRobots(robots: Robot[]): void { + if (robots && robots.length < 1) { + return; + } + let robotsDelete$ = robots.map(robot => this.delOperate(robot)); + forkJoin(robotsDelete$) + .pipe( + catchError(err => throwError(err)), + finalize(() => { + this.retrieve(); + this.selectedRow = []; + }) + ) + .subscribe(() => {}); + } + + delOperate(robot: Robot) { + // init operation info + let operMessage = new OperateInfo(); + operMessage.name = "OPERATION.DELETE_ROBOT"; + operMessage.data.id = robot.id; + operMessage.state = OperationState.progressing; + operMessage.data.name = robot.name; + this.operationService.publishInfo(operMessage); + + return this.robotService + .deleteRobotAccount(this.projectId, robot.id) + .pipe( + map( + () => operateChanges(operMessage, OperationState.success), + err => operateChanges(operMessage, OperationState.failure, err) + ) + ); + } + + createAccount(created: boolean): void { + if (created) { + this.retrieve(); + } + } + + forceRefreshView(duration: number): void { + // Reset timer + if (this.timerHandler) { + clearInterval(this.timerHandler); + } + this.timerHandler = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => { + if (this.timerHandler) { + clearInterval(this.timerHandler); + this.timerHandler = null; + } + }, duration); + } + + doSearch(value: string): void { + this.searchRobot = value; + this.retrieve(); + } + + retrieve(): void { + this.loading = true; + this.selectedRow = []; + this.robotService + .listRobotAccount(this.projectId) + .pipe(finalize(() => (this.loading = false))) + .subscribe( + response => { + this.robots = response.filter(x => + x.name.split('$')[1].includes(this.searchRobot) + ); + this.robotsCopy = response.map(x => Object.assign({}, x)); + this.forceRefreshView(2000); + }, + error => { + this.messageHandlerService.handleError(error); + } + ); + } + + changeAccountStatus(robots: Robot): void { + let id: number | string = robots[0].id; + this.isDisabled = robots[0].disabled ? false : true; + this.robotService + .toggleDisabledAccount(this.projectId, id, this.isDisabled) + .subscribe(response => { + this.retrieve(); + }); + } +} diff --git a/src/portal/src/app/project/robot-account/robot-account.service.ts b/src/portal/src/app/project/robot-account/robot-account.service.ts new file mode 100644 index 0000000000..0edd89390e --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot-account.service.ts @@ -0,0 +1,64 @@ +import { throwError as observableThrowError, Observable } from "rxjs"; + +import { map, catchError } from "rxjs/operators"; +// 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. +import { Injectable } from "@angular/core"; +import { Http } from "@angular/http"; +import { RobotApiRepository } from "./robot.api.repository"; +@Injectable() +export class RobotService { + constructor( + private http: Http, + private robotApiRepository: RobotApiRepository + ) {} + public addRobotAccount(projecId, name, description, projectName, isPull, isPush): Observable { + let access = []; + if ( isPull ) { + access.push({"resource": "/project/" + projecId + "/repository", "action": "pull"}); + access.push({"resource": "/project/" + projectName + "/repository", "action": "pull"}); + } + if ( isPush ) { + access.push({"resource": "/project/" + projecId + "/repository", "action": "push"}); + access.push({"resource": "/project/" + projectName + "/repository", "action": "push"}); + } + + let param = { + name: name, + description: description, + access: access + }; + + return this.robotApiRepository.postRobot(projecId, param); + } + + public deleteRobotAccount(projecId, id): Observable { + return this.robotApiRepository.deleteRobot(projecId, id); + } + + public listRobotAccount(projecId): Observable { + return this.robotApiRepository.listRobot(projecId); + } + + public getRobotAccount(projecId, id): Observable { + return this.robotApiRepository.getRobot(projecId, id); + } + + public toggleDisabledAccount(projecId, id, isDisabled): Observable { + let data = { + Disabled: isDisabled + }; + return this.robotApiRepository.toggleDisabledAccount(projecId, id, data); + } +} diff --git a/src/portal/src/app/project/robot-account/robot.api.repository.ts b/src/portal/src/app/project/robot-account/robot.api.repository.ts new file mode 100644 index 0000000000..308e658cb0 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot.api.repository.ts @@ -0,0 +1,44 @@ +import { Injectable } from "@angular/core"; +import { Http } from "@angular/http"; +import { throwError as observableThrowError, Observable, pipe } from "rxjs"; +import { catchError, map } from "rxjs/operators"; +import { Robot } from './robot'; +import { HTTP_JSON_OPTIONS } from "../../shared/shared.utils"; + +@Injectable() +export class RobotApiRepository { + constructor(private http: Http) {} + + public postRobot(projectId, param): Observable { + return this.http + .post(`/api/projects/${projectId}/robots`, param) + .pipe(map(response => response.json())) + .pipe(catchError(error => observableThrowError(error))); + } + + public deleteRobot(projectId, id): Observable { + return this.http + .delete(`/api/projects/${projectId}/robots/${id}`) + .pipe(catchError(error => observableThrowError(error))); + } + + public listRobot(projectId): Observable { + return this.http + .get(`/api/projects/${projectId}/robots`) + .pipe(map(response => response.json() as Robot[])) + .pipe(catchError(error => observableThrowError(error))); + } + + public getRobot(projectId, id): Observable { + return this.http + .get(`/api/projects/${projectId}/robots/${id}`) + .pipe(map(response => response.json() as Robot[])) + .pipe(catchError(error => observableThrowError(error))); + } + + public toggleDisabledAccount(projectId, id, data): Observable { + return this.http + .put(`/api/projects/${projectId}/robots/${id}`, data) + .pipe(catchError(error => observableThrowError(error))); + } +} diff --git a/src/portal/src/app/project/robot-account/robot.ts b/src/portal/src/app/project/robot-account/robot.ts new file mode 100644 index 0000000000..f3ff1ffef7 --- /dev/null +++ b/src/portal/src/app/project/robot-account/robot.ts @@ -0,0 +1,20 @@ +export class Robot { + project_id: number; + id: number; + name: string; + description: string; + disabled: boolean; + access: { + isPull: boolean; + isPush: boolean; + }; + + + constructor () { + this.access = {}; + // this.access[0].action = true; + this.access.isPull = true; + this.access.isPush = true; + } +} + diff --git a/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss b/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss index da03240da9..4ed1cbdbee 100644 --- a/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss +++ b/src/portal/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss @@ -17,6 +17,7 @@ vertical-align: middle; width: 80%; white-space: pre-wrap; + word-break: break-all; } .batchInfoUl{ padding: 20px; list-style-type: none; diff --git a/src/portal/src/app/shared/shared.const.ts b/src/portal/src/app/shared/shared.const.ts index ac263e44d6..7cefb9dec3 100644 --- a/src/portal/src/app/shared/shared.const.ts +++ b/src/portal/src/app/shared/shared.const.ts @@ -35,6 +35,7 @@ export const enum ConfirmationTargets { EMPTY, PROJECT, PROJECT_MEMBER, + ROBOT_ACCOUNT, USER, POLICY, TOGGLE_CONFIRM, diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 907c94bd4a..5ea73b524f 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -194,7 +194,8 @@ "LABELS": "Labels", "PROJECTS": "Projects", "CONFIG": "Configuration", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Project registry", @@ -258,6 +259,31 @@ "SET_ROLE": "SET ROLE", "REMOVE": "Remove" }, + "ROBOT_ACCOUNT": { + "NAME": "Name", + "TOKEN": "Token", + "NEW_ROBOT_ACCOUNT": "NEW ROBOT ACCOUNT", + "ENABLED_STATE": "Enabled state", + "DESCRIPTION": "Description", + "ACTION": "Action", + "EDIT": "Edit", + "ITEMS": "items", + "OF": "of", + "DISABLE_ACCOUNT": "Disable Account", + "ENABLE_ACCOUNT": "Enable Account", + "DELETE": "Delete", + "CREAT_ROBOT_ACCOUNT": "Creat Robot Account", + "PULL_PERMISSION": "Permission for Pull", + "PUSH_PERMISSION": "Permission for Push", + "FILTER_PLACEHOLDER": "Filter Robot Accounts", + "ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.", + "ACCOUNT_EXISTING": "Robot Account is already exists.", + "ALERT_TEXT": "This is the only time to copy your personal access token.You wont't have another opportunity", + "CREATED_SUCCESS": "Created '{{param}}' successfully.", + "COPY_SUCCESS": "Copy token successfully of '{{param}}'", + "DELETION_TITLE": "Confirm removal of robot accounts", + "DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?" + }, "GROUP": { "GROUP": "Group", "GROUPS": "Groups", @@ -802,6 +828,7 @@ "DELETE_REPO": "Delete repository", "DELETE_TAG": "Delete tag", "DELETE_USER": "Delete user", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Delete registry", "DELETE_REPLICATION": "Delete replication", "DELETE_MEMBER": "Delete user member", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 62a5f50612..b286dd804d 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -194,7 +194,8 @@ "LABELS": "Labels", "PROJECTS": "Proyectos", "CONFIG": "Configuración", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Registro de proyectos", @@ -258,6 +259,31 @@ "SET_ROLE": "SET ROLE", "REMOVE": "Remove" }, + "ROBOT_ACCOUNT": { + "NAME": "Name", + "TOKEN": "Token", + "NEW_ROBOT_ACCOUNT": "NEW ROBOT ACCOUNT", + "ENABLED_STATE": "Enabled state", + "DESCRIPTION": "Description", + "ACTION": "Action", + "EDIT": "Edit", + "ITEMS": "items", + "OF": "of", + "DISABLE_ACCOUNT": "Disable Account", + "ENABLE_ACCOUNT": "Enable Account", + "DELETE": "Delete", + "CREAT_ROBOT_ACCOUNT": "Creat Robot Account", + "PULL_PERMISSION": "Permission for Pull", + "PUSH_PERMISSION": "Permission for Push", + "FILTER_PLACEHOLDER": "Filter Robot Accounts", + "ROBOT_NAME": "Cannot contain special characters(~#$%) and maximum length should be 255 characters.", + "ACCOUNT_EXISTING": "Robot Account is already exists.", + "ALERT_TEXT": "This is the only time to copy your personal access token.You wont't have another opportunity", + "CREATED_SUCCESS": "Created '{{param}}' successfully.", + "COPY_SUCCESS": "Copy token successfully of '{{param}}'", + "DELETION_TITLE": "Confirm removal of robot accounts", + "DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?" + }, "GROUP": { "GROUP": "Group", "GROUPS": "Groups", @@ -802,6 +828,7 @@ "DELETE_REPO": "Delete repository", "DELETE_TAG": "Delete tag", "DELETE_USER": "Delete user", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Delete registry", "DELETE_REPLICATION": "Delete replication", "DELETE_MEMBER": "Delete user member", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 0efefb39a9..13c03ec0be 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -180,7 +180,8 @@ "LABELS": "Labels", "PROJECTS": "Projets", "CONFIG": "Configuration", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Dépôt du Projet", @@ -242,6 +243,31 @@ "SET_ROLE": "SET ROLE", "REMOVE": "Remove" }, + "ROBOT_ACCOUNT": { + "NAME": "Nom", + "TOKEN": "gage ", + "NEW_ROBOT_ACCOUNT": "nouveau robot compte ", + "ENABLED_STATE": "état d 'activation", + "DESCRIPTION": "Description", + "ACTION": "Action", + "EDIT": "Edit", + "ITEMS": "items", + "OF": "of", + "DISABLE_ACCOUNT": "désactiver le compte ", + "ENABLE_ACCOUNT": "permettre à compte ", + "DELETE": "Supprimer", + "CREAT_ROBOT_ACCOUNT": "créat robot compte ", + "PULL_PERMISSION": "Permission for Pull", + "PUSH_PERMISSION": "Permission for Push", + "FILTER_PLACEHOLDER": "Filter Robot Accounts", + "ROBOT_NAME": "ne peut pas contenir de caractères spéciaux(~#$%) et la longueur maximale devrait être de 255 caractères.", + "ACCOUNT_EXISTING": "le robot est existe déjà.", + "ALERT_TEXT": "This is the only time to copy your personal access token.You wont't have another opportunity", + "CREATED_SUCCESS": "Created '{{param}}' successfully.", + "COPY_SUCCESS": "Copy token successfully of '{{param}}'", + "DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ", + "DELETION_SUMMARY": "Voulez-vous supprimer la règle {{param}}?" + }, "GROUP": { "Group": "Group", "GROUPS": "Groups", @@ -765,6 +791,7 @@ "DELETE_REPO": "Delete repository", "DELETE_TAG": "Delete tag", "DELETE_USER": "Delete user", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Delete registry", "DELETE_REPLICATION": "Delete replication", "DELETE_MEMBER": "Delete member", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 072781b5f4..7cd47fef4c 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -192,7 +192,8 @@ "LABELS": "Etiquetas", "PROJECTS": "Projetos", "CONFIG": "Configuração", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "Robot Accounts" }, "PROJECT_CONFIG": { "REGISTRY": "Registro do Projeto", @@ -256,6 +257,31 @@ "SET_ROLE": "DEFINIR FUNÇÃO", "REMOVE": "Remover" }, + "ROBOT_ACCOUNT": { + "NAME": "Nome", + "TOKEN": "Token", + "NEW_ROBOT_ACCOUNT": "Novo robô conta", + "ENABLED_STATE": "Enabled state", + "DESCRIPTION": "Descrição", + "ACTION": "AÇÃO", + "EDIT": "Editar", + "ITEMS": "itens", + "OF": "de", + "DISABLE_ACCOUNT": "Desactivar a conta", + "ENABLE_ACCOUNT": "Ativar conta", + "DELETE": "Remover", + "CREAT_ROBOT_ACCOUNT": "CRIA robô conta", + "PULL_PERMISSION": "Permission for Pull", + "PUSH_PERMISSION": "Permission for Push", + "FILTER_PLACEHOLDER": "Filtro robot accounts", + "ROBOT_NAME": "Não Pode conter caracteres especiais(~#$%) e comprimento máximo deveria ser 255 caracteres.", + "ACCOUNT_EXISTING": "Robô conta já existe.", + "ALERT_TEXT": "É só copiar o token de acesso Pessoal não VAI ter outra oportunidade.", + "CREATED_SUCCESS": "Created '{{param}}' successfully.", + "COPY_SUCCESS": "Copy token successfully of '{{param}}'", + "DELETION_TITLE": "Confirmar a remoção do robô Contas", + "DELETION_SUMMARY": "Você quer remover a regra {{param}}?" + }, "GROUP": { "GROUP": "Grupo", "GROUPS": "Grupos", @@ -792,6 +818,7 @@ "DELETE_REPO": "Remover repositório", "DELETE_TAG": "Remover tag", "DELETE_USER": "Remover usuário", + "DELETE_ROBOT": "Delete robot", "DELETE_REGISTRY": "Remover registry", "DELETE_REPLICATION": "Remover replicação", "DELETE_MEMBER": "Remover usuário membro", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 57c8bb19a3..8766b43c45 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -193,7 +193,8 @@ "LABELS": "标签", "PROJECTS": "项目", "CONFIG": "配置管理", - "HELMCHART": "Helm Charts" + "HELMCHART": "Helm Charts", + "ROBOT_ACCOUNTS": "机器人账户" }, "PROJECT_CONFIG": { "REGISTRY": "项目仓库", @@ -257,6 +258,31 @@ "SET_ROLE": "设置角色", "REMOVE": "移除成员" }, + "ROBOT_ACCOUNT": { + "NAME": "姓名", + "TOKEN": "令牌", + "NEW_ROBOT_ACCOUNT": "添加机器人账户", + "ENABLED_STATE": "启用状态", + "DESCRIPTION": "描述", + "ACTION": "操作", + "EDIT": "编辑", + "OF": "共计", + "ITEMS": "条记录", + "DISABLE_ACCOUNT": "禁用账户", + "ENABLE_ACCOUNT": "启用账户", + "DELETE": "删除", + "CREAT_ROBOT_ACCOUNT": "创建机器人账户", + "PULL_PERMISSION": "Pull 权限", + "PUSH_PERMISSION": "Push 权限", + "FILTER_PLACEHOLDER": "过滤机器人账户", + "ROBOT_NAME": "不能包含特殊字符(~#$%)且长度不能超过255.", + "ACCOUNT_EXISTING": "机器人账户已经存在.", + "ALERT_TEXT": "这是唯一一次复制您的个人访问令牌的机会", + "CREATED_SUCCESS": "创建账户 '{{param}}' 成功.", + "COPY_SUCCESS": "成功复制 '{{param}}' 的令牌", + "DELETION_TITLE": "删除账户确认", + "DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?" + }, "GROUP": { "GROUP": "组", "GROUPS": "组", @@ -800,6 +826,7 @@ "DELETE_REPO": "删除仓库", "DELETE_TAG": "删除镜像标签", "DELETE_USER": "删除用户", + "DELETE_ROBOT": "删除账户", "DELETE_REGISTRY": "删除Registry", "DELETE_REPLICATION": "删除复制", "DELETE_MEMBER": "删除用户成员", diff --git a/src/portal/src/styles.css b/src/portal/src/styles.css index 431c0710bd..ec72024cfa 100644 --- a/src/portal/src/styles.css +++ b/src/portal/src/styles.css @@ -78,4 +78,12 @@ body { .datagrid-header{ z-index: 1 !important; +} + +.color-green { + color: #1D5100; +} + +.color-red { + color: red; } \ No newline at end of file From f4f45353049066784b3354788a687cfafa791413 Mon Sep 17 00:00:00 2001 From: wang yan Date: Tue, 29 Jan 2019 16:22:50 +0800 Subject: [PATCH 25/45] Fix action and resouce of RBAC change Signed-off-by: wang yan --- src/common/security/robot/context.go | 7 +++---- src/common/security/robot/context_test.go | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/common/security/robot/context.go b/src/common/security/robot/context.go index 9e73dc5570..3b48b91bc0 100644 --- a/src/common/security/robot/context.go +++ b/src/common/security/robot/context.go @@ -17,7 +17,6 @@ package robot import ( "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/rbac" - "github.com/goharbor/harbor/src/common/rbac/project" "github.com/goharbor/harbor/src/core/promgr" ) @@ -64,19 +63,19 @@ func (s *SecurityContext) IsSolutionUser() bool { // HasReadPerm returns whether the user has read permission to the project func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // HasWritePerm returns whether the user has write permission to the project func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // HasAllPerm returns whether the user has all permissions to the project func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(project.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(project.ResourceImage)) + return s.Can(rbac.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) } // GetMyProjects no implementation diff --git a/src/common/security/robot/context_test.go b/src/common/security/robot/context_test.go index 3a729efaab..df7869a904 100644 --- a/src/common/security/robot/context_test.go +++ b/src/common/security/robot/context_test.go @@ -136,7 +136,7 @@ func TestIsSolutionUser(t *testing.T) { func TestHasReadPerm(t *testing.T) { rbacPolicy := &rbac.Policy{ - Resource: "/project/testrobot/image", + Resource: "/project/testrobot/repository", Action: "pull", } policies := []*rbac.Policy{} @@ -153,7 +153,7 @@ func TestHasReadPerm(t *testing.T) { func TestHasWritePerm(t *testing.T) { rbacPolicy := &rbac.Policy{ - Resource: "/project/testrobot/image", + Resource: "/project/testrobot/repository", Action: "push", } policies := []*rbac.Policy{} @@ -169,7 +169,7 @@ func TestHasWritePerm(t *testing.T) { func TestHasAllPerm(t *testing.T) { rbacPolicy := &rbac.Policy{ - Resource: "/project/testrobot/image", + Resource: "/project/testrobot/repository", Action: "push+pull", } policies := []*rbac.Policy{} From dacc0bd6bcc6f4527ad8668e375269c08433468a Mon Sep 17 00:00:00 2001 From: FangyuanCheng Date: Wed, 30 Jan 2019 10:29:49 +0800 Subject: [PATCH 26/45] Fixed display problems caused by formatting Signed-off-by: FangyuanCheng --- .../src/app/project/robot-account/robot-account.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/portal/src/app/project/robot-account/robot-account.component.html b/src/portal/src/app/project/robot-account/robot-account.component.html index a1a55a4408..77bd257fa8 100644 --- a/src/portal/src/app/project/robot-account/robot-account.component.html +++ b/src/portal/src/app/project/robot-account/robot-account.component.html @@ -4,8 +4,7 @@
- From a77684bf1ddd26bbde60fa444fa32986ae546670 Mon Sep 17 00:00:00 2001 From: Yan Date: Wed, 30 Jan 2019 14:10:26 +0800 Subject: [PATCH 27/45] add API doc of robot account (#6846) Signed-off-by: wang yan --- docs/swagger.yaml | 210 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index dba24f3be2..2adb9265f7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3179,6 +3179,160 @@ paths: $ref: '#/definitions/NotFoundChartAPIError' '500': $ref: '#/definitions/InternalChartAPIError' + '/projects/{project_id}/robots': + get: + summary: Get all robot accounts of specified project + description: Get all robot accounts of specified project + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + tags: + - Products + - Robot Account + responses: + '200': + description: Get project robot accounts successfully. + schema: + type: array + items: + $ref: '#/definitions/RobotAccount' + '400': + description: The project id is invalid. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the project. + '404': + description: Project ID does not exist. + '500': + description: Unexpected internal errors. + post: + summary: Create a robot account for project + description: Create a robot account for project + tags: + - Products + - Robot Account + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: robot + in: body + description: Request body of creating a robot account. + required: true + schema: + $ref: '#/definitions/RobotAccountCreate' + responses: + '201': + description: Project member created successfully. + '400': + description: Project id is not valid. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the project. + '409': + description: An robot account with same name already exist in the project. + '500': + description: Unexpected internal errors. + '/projects/{project_id}/robots/{robot_id}': + get: + summary: Return the infor of the specified robot account. + description: Return the infor of the specified robot account. + tags: + - Products + - Robot Account + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: robot_id + in: path + type: integer + format: int64 + required: true + description: The ID of robot account. + responses: + '200': + description: '#/definitions/RobotAccount' + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the project. + '404': + description: The robot account is not found. + '500': + description: Unexpected internal errors. + put: + summary: Update status of robot account. + description: Used to disable/enable a specified robot account. + tags: + - Products + - Robot Account + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: robot_id + in: path + type: integer + format: int64 + required: true + description: The ID of robot account. + - name: robot + in: body + description: Request body of enable/disable a robot account. + required: true + schema: + $ref: '#/definitions/RobotAccountUpdate' + responses: + '200': + description: Robot account has been modified success. + '500': + description: Unexpected internal errors. + delete: + summary: Delete the specified robot account + description: Delete the specified robot account + tags: + - Products + - Robot Account + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: robot_id + in: path + type: integer + format: int64 + required: true + description: The ID of robot account. + responses: + '200': + description: The specified robot account is successfully deleted. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the project. + '404': + description: The robot account is not found. + '500': + description: Unexpected internal errors. responses: UnsupportedMediaType: description: 'The Media Type of the request is not supported, it has to be "application/json"' @@ -4581,6 +4735,60 @@ definitions: error: type: string description: (optional) The error message when the status is "unhealthy" + RobotAccount: + type: object + description: The object of robot account + properties: + id: + type: integer + description: The id of robot account + name: + type: string + description: The name of robot account + description: + type: string + description: The description of robot account + project_id: + type: integer + description: The project id of robot account + disabled: + type: boolean + description: The robot account is disable or enable + creation_time: + type: string + description: The creation time of the robot account + update_time: + type: string + description: The update time of the robot account + RobotAccountCreate: + type: object + properties: + name: + type: string + description: The name of robot account + description: + type: string + description: The description of robot account + access: + type: array + description: The permission of robot account + items: + $ref: '#/definitions/RobotAccountAccess' + RobotAccountAccess: + type: object + properties: + resource: + type: string + description: the resource of harbor + action: + type: string + description: the action to resource that perdefined in harbor rbac + RobotAccountUpdate: + type: object + properties: + disable: + type: boolean + description: The robot account is disable or enable Permission: type: object description: The permission @@ -4590,4 +4798,4 @@ definitions: description: The permission resoruce action: type: string - description: The permission action \ No newline at end of file + description: The permission action From e12fd13c563cd76387da40723edbca2631372f74 Mon Sep 17 00:00:00 2001 From: danfengliu Date: Wed, 30 Jan 2019 14:22:31 +0800 Subject: [PATCH 28/45] Encapsulate keyword and add it into keyword , this porblem caused case failure of randomly. (#6834) Signed-off-by: danfengliu --- tests/resources/Admiral-Util.robot | 12 +-- .../Harbor-Pages/Administration-Users.robot | 4 +- .../Harbor-Pages/Configuration.robot | 86 +++++++-------- .../Harbor-Pages/Configuration_Elements.robot | 20 ++-- tests/resources/Harbor-Pages/HomePage.robot | 12 +-- .../Harbor-Pages/HomePage_Elements.robot | 14 +-- .../Harbor-Pages/Project-Helmcharts.robot | 15 ++- .../Project-Helmcharts_Elements.robot | 28 ++--- .../Harbor-Pages/Project-Members.robot | 8 +- .../Project-Members_Elements.robot | 4 +- .../Harbor-Pages/Project-Retag.robot | 2 +- tests/resources/Harbor-Pages/Project.robot | 101 +++++++++--------- .../Harbor-Pages/Project_Elements.robot | 5 +- .../resources/Harbor-Pages/Replication.robot | 18 ++-- .../Harbor-Pages/Replication_Elements.robot | 18 ++-- tests/resources/Harbor-Pages/ToolKit.robot | 4 +- .../Harbor-Pages/ToolKit_Elements.robot | 2 +- .../resources/Harbor-Pages/UserProfile.robot | 10 +- tests/resources/Harbor-Pages/Verify.robot | 12 +-- .../Harbor-Pages/Vulnerability.robot | 22 ++-- tests/resources/Util.robot | 51 ++++++--- 21 files changed, 231 insertions(+), 217 deletions(-) diff --git a/tests/resources/Admiral-Util.robot b/tests/resources/Admiral-Util.robot index 3319675f6a..e9d9b82cfb 100644 --- a/tests/resources/Admiral-Util.robot +++ b/tests/resources/Admiral-Util.robot @@ -49,9 +49,9 @@ Login To Admiral Add Host To Admiral [Arguments] ${address} ${credentials}=${EMPTY} - Wait Until Element Is Visible css=a[data-cmd="navigation-hosts"] - Wait Until Element Is Enabled css=a[data-cmd="navigation-hosts"] - Click Element css=a[data-cmd="navigation-hosts"] + Wait Until Element Is Visible css=a[data-cmd='navigation-hosts'] + Wait Until Element Is Enabled css=a[data-cmd='navigation-hosts'] + Click Element css=a[data-cmd='navigation-hosts'] Wait Until Element Is Visible css=div.query-search-input-controls.form-control Wait Until Element Is Enabled css=div.query-search-input-controls.form-control @@ -87,9 +87,9 @@ Add Host To Admiral Add Project to Admiral [Arguments] ${name} - Wait Until Element Is Visible css=a[data-cmd="navigation-placements"] - Wait Until Element Is Enabled css=a[data-cmd="navigation-placements"] - Click Element css=a[data-cmd="navigation-placements"] + Wait Until Element Is Visible css=a[data-cmd='navigation-placements'] + Wait Until Element Is Enabled css=a[data-cmd='navigation-placements'] + Click Element css=a[data-cmd='navigation-placements'] Wait Until Element Is Visible css=div.right-context-panel > div.toolbar > div:nth-child(2) > a Wait Until Element Is Enabled css=div.right-context-panel > div.toolbar > div:nth-child(2) > a diff --git a/tests/resources/Harbor-Pages/Administration-Users.robot b/tests/resources/Harbor-Pages/Administration-Users.robot index f44324480f..c5d6358a09 100644 --- a/tests/resources/Harbor-Pages/Administration-Users.robot +++ b/tests/resources/Harbor-Pages/Administration-Users.robot @@ -26,9 +26,9 @@ Assign User Admin Input Text xpath=//harbor-user//hbr-filter//input ${user} Sleep 2 #select checkbox - Click Element //clr-dg-row[contains(.,"${user}")]//label + Click Element //clr-dg-row[contains(.,'${user}')]//label #click assign admin - Click Element //*[@id="set-admin"] + Click Element //*[@id='set-admin'] Sleep 1 Switch to User Tag diff --git a/tests/resources/Harbor-Pages/Configuration.robot b/tests/resources/Harbor-Pages/Configuration.robot index 28043c942b..a443c3ae3b 100644 --- a/tests/resources/Harbor-Pages/Configuration.robot +++ b/tests/resources/Harbor-Pages/Configuration.robot @@ -24,17 +24,17 @@ Init LDAP ${rc} ${output}= Run And Return Rc And Output ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}' Log ${output} Sleep 2 - Input Text xpath=//*[@id="ldapUrl"] ldaps://${output} + Input Text xpath=//*[@id='ldapUrl'] ldaps://${output} Sleep 1 - Input Text xpath=//*[@id="ldapSearchDN"] cn=admin,dc=example,dc=com + Input Text xpath=//*[@id='ldapSearchDN'] cn=admin,dc=example,dc=com Sleep 1 - Input Text xpath=//*[@id="ldapSearchPwd"] admin + Input Text xpath=//*[@id='ldapSearchPwd'] admin Sleep 1 - Input Text xpath=//*[@id="ldapBaseDN"] dc=example,dc=com + Input Text xpath=//*[@id='ldapBaseDN'] dc=example,dc=com Sleep 1 - Input Text xpath=//*[@id="ldapFilter"] (&(objectclass=inetorgperson)(memberof=cn=harbor_users,ou=groups,dc=example,dc=com)) + Input Text xpath=//*[@id='ldapFilter'] (&(objectclass=inetorgperson)(memberof=cn=harbor_users,ou=groups,dc=example,dc=com)) Sleep 1 - Input Text xpath=//*[@id="ldapUid"] cn + Input Text xpath=//*[@id='ldapUid'] cn Sleep 1 Capture Page Screenshot Disable Ldap Verify Cert Checkbox @@ -52,15 +52,15 @@ Test Ldap Connection ${rc} ${output}= Run And Return Rc And Output ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}' Log ${output} Sleep 2 - Input Text xpath=//*[@id="ldapUrl"] ldaps://${output} + Input Text xpath=//*[@id='ldapUrl'] ldaps://${output} Sleep 1 - Input Text xpath=//*[@id="ldapSearchDN"] cn=admin,dc=example,dc=com + Input Text xpath=//*[@id='ldapSearchDN'] cn=admin,dc=example,dc=com Sleep 1 - Input Text xpath=//*[@id="ldapSearchPwd"] admin + Input Text xpath=//*[@id='ldapSearchPwd'] admin Sleep 1 - Input Text xpath=//*[@id="ldapBaseDN"] dc=example,dc=com + Input Text xpath=//*[@id='ldapBaseDN'] dc=example,dc=com Sleep 1 - Input Text xpath=//*[@id="ldapUid"] cn + Input Text xpath=//*[@id='ldapUid'] cn Sleep 1 # default is checked, click test connection to verify fail as no cert. @@ -81,14 +81,14 @@ Test LDAP Server Success Wait Until Page Contains Connection to LDAP server is verified timeout=15 Disable Ldap Verify Cert Checkbox - Mouse Down xpath=//*[@id="clr-checkbox-ldapVerifyCert"] - Mouse Up xpath=//*[@id="clr-checkbox-ldapVerifyCert"] + Mouse Down xpath=//*[@id='clr-checkbox-ldapVerifyCert'] + Mouse Up xpath=//*[@id='clr-checkbox-ldapVerifyCert'] Sleep 2 Capture Page Screenshot Ldap Verify Cert Checkbox Should Be Disabled Ldap Verify Cert Checkbox Should Be Disabled - Checkbox Should Not Be Selected xpath=//*[@id="clr-checkbox-ldapVerifyCert"] + Checkbox Should Not Be Selected xpath=//*[@id='clr-checkbox-ldapVerifyCert'] Set Pro Create Admin Only #set limit to admin only @@ -96,8 +96,8 @@ Set Pro Create Admin Only Sleep 2 Click Element xpath=${system_config_xpath} Sleep 1 - Click Element xpath=//select[@id="proCreation"] - Click Element xpath=//select[@id="proCreation"]//option[@value="adminonly"] + Click Element xpath=//select[@id='proCreation'] + Click Element xpath=//select[@id='proCreation']//option[@value='adminonly'] Sleep 1 Click Element xpath=${config_system_save_button_xpath} Capture Page Screenshot AdminCreateOnly.png @@ -108,8 +108,8 @@ Set Pro Create Every One #set limit to Every One Click Element xpath=${system_config_xpath} Sleep 1 - Click Element xpath=//select[@id="proCreation"] - Click Element xpath=//select[@id="proCreation"]//option[@value="everyone"] + Click Element xpath=//select[@id='proCreation'] + Click Element xpath=//select[@id='proCreation']//option[@value='everyone'] Sleep 1 Click Element xpath=${config_system_save_button_xpath} Sleep 2 @@ -153,18 +153,18 @@ Switch To System Settings Click Element xpath=${system_config_xpath} Modify Token Expiration [Arguments] ${minutes} - Input Text xpath=//*[@id="tokenExpiration"] ${minutes} + Input Text xpath=//*[@id='tokenExpiration'] ${minutes} Click Button xpath=${config_system_save_button_xpath} Sleep 1 Token Must Be Match [Arguments] ${minutes} - Textfield Value Should Be xpath=//*[@id="tokenExpiration"] ${minutes} + Textfield Value Should Be xpath=//*[@id='tokenExpiration'] ${minutes} ## Replication Check Verify Remote Cert - Mouse Down xpath=//*[@id="clr-checkbox-verifyRemoteCert"] - Mouse Up xpath=//*[@id="clr-checkbox-verifyRemoteCert"] + Mouse Down xpath=//*[@id='clr-checkbox-verifyRemoteCert'] + Mouse Up xpath=//*[@id='clr-checkbox-verifyRemoteCert'] Click Element xpath=${config_save_button_xpath} Capture Page Screenshot RemoteCert.png Sleep 1 @@ -172,39 +172,39 @@ Check Verify Remote Cert Switch To System Replication Sleep 1 Switch To Configure - Click Element xpath=//*[@id="config-replication"] + Click Element xpath=//*[@id='config-replication'] Sleep 1 Should Verify Remote Cert Be Enabled - Checkbox Should Not Be Selected xpath=//*[@id="clr-checkbox-verifyRemoteCert"] + Checkbox Should Not Be Selected xpath=//*[@id='clr-checkbox-verifyRemoteCert'] ## Email Switch To Email Switch To Configure - Click Element xpath=//*[@id="config-email"] + Click Element xpath=//*[@id='config-email'] Sleep 1 Config Email - Input Text xpath=//*[@id="mailServer"] smtp.vmware.com - Input Text xpath=//*[@id="emailPort"] 25 - Input Text xpath=//*[@id="emailUsername"] example@vmware.com - Input Text xpath=//*[@id="emailPassword"] example - Input Text xpath=//*[@id="emailFrom"] example + Input Text xpath=//*[@id='mailServer'] smtp.vmware.com + Input Text xpath=//*[@id='emailPort'] 25 + Input Text xpath=//*[@id='emailUsername'] example@vmware.com + Input Text xpath=//*[@id='emailPassword'] example + Input Text xpath=//*[@id='emailFrom'] example Sleep 1 - Click Element xpath=//clr-checkbox-wrapper[@id="emailSSL-wrapper"]//label + Click Element xpath=//clr-checkbox-wrapper[@id='emailSSL-wrapper']//label Sleep 1 - Click Element xpath=//clr-checkbox-wrapper[@id="emailInsecure-wrapper"]//label + Click Element xpath=//clr-checkbox-wrapper[@id='emailInsecure-wrapper']//label Sleep 1 Click Element xpath=${config_email_save_button_xpath} Sleep 6 Verify Email - Textfield Value Should Be xpath=//*[@id="mailServer"] smtp.vmware.com - Textfield Value Should Be xpath=//*[@id="emailPort"] 25 - Textfield Value Should Be xpath=//*[@id="emailUsername"] example@vmware.com - Textfield Value Should Be xpath=//*[@id="emailFrom"] example - Checkbox Should Be Selected xpath=//*[@id="emailSSL"] - Checkbox Should Not Be Selected xpath=//*[@id="emailInsecure"] + Textfield Value Should Be xpath=//*[@id='mailServer'] smtp.vmware.com + Textfield Value Should Be xpath=//*[@id='emailPort'] 25 + Textfield Value Should Be xpath=//*[@id='emailUsername'] example@vmware.com + Textfield Value Should Be xpath=//*[@id='emailFrom'] example + Checkbox Should Be Selected xpath=//*[@id='emailSSL'] + Checkbox Should Not Be Selected xpath=//*[@id='emailInsecure'] Set Scan All To None click element //vulnerability-config//select @@ -236,19 +236,19 @@ Disable Read Only Switch To System Labels Sleep 1 Click Element xpath=${configuration_xpath} - Click Element xpath=//*[@id="config-label"] + Click Element xpath=//*[@id='config-label'] Create New Labels [Arguments] ${labelname} Click Element xpath=//button[contains(.,'New Label')] Sleep 1 - Input Text xpath=//*[@id="name"] ${labelname} + Input Text xpath=//*[@id='name'] ${labelname} Sleep 1 Click Element xpath=//hbr-create-edit-label//clr-dropdown/clr-icon Sleep 1 Click Element xpath=//hbr-create-edit-label//clr-dropdown-menu/label[1] Sleep 1 - Input Text xpath=//*[@id="description"] global + Input Text xpath=//*[@id='description'] global Click Element xpath=//div/form/section/label[4]/button[2] Capture Page Screenshot Wait Until Page Contains ${labelname} @@ -259,7 +259,7 @@ Update A Label Sleep 1 Click Element xpath=//button[contains(.,'Edit')] Sleep 1 - Input Text xpath=//*[@id="name"] ${labelname}1 + Input Text xpath=//*[@id='name'] ${labelname}1 Sleep 1 Click Element xpath=//hbr-create-edit-label//form/section//button[2] Capture Page Screenshot @@ -273,7 +273,7 @@ Delete A Label Sleep 3 Capture Page Screenshot Click Element xpath=//clr-modal//div//button[contains(.,'DELETE')] - Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${labelname}')]/../div/clr-icon[@shape="success-standard"] + Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${labelname}')]/../div/clr-icon[@shape='success-standard'] ## Garbage Collection Switch To Garbage Collection diff --git a/tests/resources/Harbor-Pages/Configuration_Elements.robot b/tests/resources/Harbor-Pages/Configuration_Elements.robot index ce557e2927..bfe072ec0e 100644 --- a/tests/resources/Harbor-Pages/Configuration_Elements.robot +++ b/tests/resources/Harbor-Pages/Configuration_Elements.robot @@ -17,15 +17,15 @@ Documentation This resource provides any keywords related to the Harbor private *** Variables *** ${project_create_xpath} //clr-dg-action-bar//button[contains(.,'New')] -${self_reg_xpath} //input[@id="selfReg"] -${test_ldap_xpath} //*[@id="authentication"]/config-auth/div/button[3] +${self_reg_xpath} //input[@id='selfReg'] +${test_ldap_xpath} //*[@id='authentication']/config-auth/div/button[3] ${config_save_button_xpath} //config//div/button[contains(.,'SAVE')] -${config_email_save_button_xpath} //*[@id="config_email_save"] -${config_auth_save_button_xpath} //*[@id="config_auth_save"] -${config_system_save_button_xpath} //*[@id="config_system_save"] -${vulnerbility_save_button_xpath} //*[@id="config_vulnerbility_save"] +${config_email_save_button_xpath} //*[@id='config_email_save'] +${config_auth_save_button_xpath} //*[@id='config_auth_save'] +${config_system_save_button_xpath} //*[@id='config_system_save'] +${vulnerbility_save_button_xpath} //*[@id='config_vulnerbility_save'] ${configuration_xpath} //clr-vertical-nav-group-children/a[contains(.,'Configuration')] -${system_config_xpath} //*[@id="config-system"] -${garbage_collection_xpath} //*[@id="config-gc"] -${gc_now_xpath} //*[@id="gc"]/gc-config/button -${gc_log_details_xpath} //*[@id="clr-dg-row26"]/clr-dg-cell[6]/a \ No newline at end of file +${system_config_xpath} //*[@id='config-system'] +${garbage_collection_xpath} //*[@id='config-gc'] +${gc_now_xpath} //*[@id='gc']/gc-config/button +${gc_log_details_xpath} //*[@id='clr-dg-row26']/clr-dg-cell[6]/a \ No newline at end of file diff --git a/tests/resources/Harbor-Pages/HomePage.robot b/tests/resources/Harbor-Pages/HomePage.robot index 745a9c95f2..9095a397aa 100644 --- a/tests/resources/Harbor-Pages/HomePage.robot +++ b/tests/resources/Harbor-Pages/HomePage.robot @@ -23,15 +23,15 @@ ${HARBOR_VERSION} v1.1.1 Sign In Harbor [Arguments] ${url} ${user} ${pw} Go To ${url} - Wait Until Element Is Enabled ${harbor_span_title} - Wait Until Element Is Visible ${login_name} - Wait Until Element Is Visible ${login_pwd} + Retry Wait Element ${harbor_span_title} + Retry Wait Element ${login_name} + Retry Wait Element ${login_pwd} Input Text ${login_name} ${user} Input Text ${login_pwd} ${pw} - Wait Until Element Is Visible ${login_btn} - Click button ${login_btn} + Retry Wait Element ${login_btn} + Retry Button Click ${login_btn} Log To Console ${user} - Wait Until Element Is Visible xpath=//span[contains(., '${user}')] + Retry Wait Element xpath=//span[contains(., '${user}')] Capture Screenshot And Source Capture Page Screenshot diff --git a/tests/resources/Harbor-Pages/HomePage_Elements.robot b/tests/resources/Harbor-Pages/HomePage_Elements.robot index 9d94f1e40b..91035a400b 100644 --- a/tests/resources/Harbor-Pages/HomePage_Elements.robot +++ b/tests/resources/Harbor-Pages/HomePage_Elements.robot @@ -17,13 +17,13 @@ Documentation This resource provides any keywords related to the Harbor private *** Variables *** ${sign_up_for_an_account_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/sign-in/div/form/div[1]/a -${sign_up_button_xpath} //a[@class="signup"] -${username_xpath} //*[@id="username"] -${email_xpath} //*[@id="email"] -${realname_xpath} //*[@id="realname"] -${newPassword_xpath} //*[@id="newPassword"] -${confirmPassword_xpath} //*[@id="confirmPassword"] -${comment_xpath} //*[@id="comment"] +${sign_up_button_xpath} //a[@class='signup'] +${username_xpath} //*[@id='username'] +${email_xpath} //*[@id='email'] +${realname_xpath} //*[@id='realname'] +${newPassword_xpath} //*[@id='newPassword'] +${confirmPassword_xpath} //*[@id='confirmPassword'] +${comment_xpath} //*[@id='comment'] ${signup_xpath} //clr-modal/div/div[1]/div/div/div[3]/button[2] ${signup_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/sign-in/sign-up/clr-modal/div/div[1]/div/div[1]/div/div[3]/button[2] diff --git a/tests/resources/Harbor-Pages/Project-Helmcharts.robot b/tests/resources/Harbor-Pages/Project-Helmcharts.robot index 5de53c6cdd..556ddd88ae 100644 --- a/tests/resources/Harbor-Pages/Project-Helmcharts.robot +++ b/tests/resources/Harbor-Pages/Project-Helmcharts.robot @@ -33,19 +33,18 @@ Upload Chart files Go Into Chart Version [Arguments] ${chart_name} - Click Element xpath=//hbr-helm-chart//a[contains(., "${chart_name}")] + Click Element xpath=//hbr-helm-chart//a[contains(., '${chart_name}')] Capture Page Screenshot viewchartversion.png Go Into Chart Detail [Arguments] ${version_name} - Click Element xpath=//hbr-helm-chart-version//a[contains(., "${version_name}")] + Click Element xpath=//hbr-helm-chart-version//a[contains(., '${version_name}')] Sleep 2 Page Should Contain Element ${chart_detail} Go Back To Versions And Delete - Click Element xpath=${version_bread_crumbs} - Sleep 2 - Click Element xpath=${version_checkbox} - Click Element xpath=${version_delete} - Click Element xpath=${version_confirm_delete} - Wait Until Page Contains Element xpath=${helmchart_content} \ No newline at end of file + Retry Element Click xpath=${version_bread_crumbs} + Retry Element Click xpath=${version_checkbox} + Retry Element Click xpath=${version_delete} + Retry Element Click xpath=${version_confirm_delete} + Retry Keyword When Error Wait Until Page Contains Element element=xpath=${helmchart_content} \ No newline at end of file diff --git a/tests/resources/Harbor-Pages/Project-Helmcharts_Elements.robot b/tests/resources/Harbor-Pages/Project-Helmcharts_Elements.robot index dd6c7f9b01..71bb947488 100644 --- a/tests/resources/Harbor-Pages/Project-Helmcharts_Elements.robot +++ b/tests/resources/Harbor-Pages/Project-Helmcharts_Elements.robot @@ -3,10 +3,10 @@ Documentation This resource provides any keywords related to the Harbor private *** Variables *** -${upload_chart_button} //button[contains(.,"Upload")] -${chart_file_browse} //*[@id="chart"] -${chart_prov_browse} //*[@id="prov"] -${upload_action_button} //clr-modal//form/div/button[contains(.,"Upload")] +${upload_chart_button} //button[contains(.,'Upload')] +${chart_file_browse} //*[@id='chart'] +${chart_prov_browse} //*[@id='prov'] +${upload_action_button} //clr-modal//form/div/button[contains(.,'Upload')] ${harbor_chart_name} harbor ${harbor_chart_filename} harbor-0.2.0.tgz @@ -18,19 +18,19 @@ ${prometheus_chart_name} prometheus ${prometheus_chart_filename} prometheus-7.0.2.tgz ${prometheus_chart_version} 7.0.2 ${prometheus_chart_file_url} https://storage.googleapis.com/harbor-builds/helm-chart-test-files/prometheus-7.0.2.tgz -${prometheus_version} //hbr-helm-chart//a[contains(.,"prometheus")] +${prometheus_version} //hbr-helm-chart//a[contains(.,'prometheus')] ${chart_detail} //hbr-chart-detail -${summary_markdown} //*[@id="summary-content"]//div[contains(@class,'md-div')] -${summary_container} //*[@id="summary-content"]//div[contains(@class,'summary-container')] -${detail_dependency} //*[@id="depend-link"] -${dependency_content} //*[@id="depend-content"]/hbr-chart-detail-dependency -${detail_value} //*[@id="value-link"] -${value_content} //*[@id="value-content"]/hbr-chart-detail-value +${summary_markdown} //*[@id='summary-content']//div[contains(@class,'md-div')] +${summary_container} //*[@id='summary-content']//div[contains(@class,'summary-container')] +${detail_dependency} //*[@id='depend-link'] +${dependency_content} //*[@id='depend-content']/hbr-chart-detail-dependency +${detail_value} //*[@id='value-link'] +${value_content} //*[@id='value-content']/hbr-chart-detail-value -${version_bread_crumbs} //project-chart-detail//a[contains(.,"Versions")] +${version_bread_crumbs} //project-chart-detail//a[contains(.,'Versions')] ${version_checkbox} //clr-dg-row//clr-checkbox-wrapper/label -${version_delete} //clr-dg-action-bar/button[contains(.,"DELETE")] -${version_confirm_delete} //clr-modal//button[contains(.,"DELETE")] +${version_delete} //clr-dg-action-bar/button[contains(.,'DELETE')] +${version_confirm_delete} //clr-modal//button[contains(.,'DELETE')] ${helmchart_content} //project-detail/project-list-charts/hbr-helm-chart \ No newline at end of file diff --git a/tests/resources/Harbor-Pages/Project-Members.robot b/tests/resources/Harbor-Pages/Project-Members.robot index d246bd9902..b01bdb6195 100644 --- a/tests/resources/Harbor-Pages/Project-Members.robot +++ b/tests/resources/Harbor-Pages/Project-Members.robot @@ -56,7 +56,7 @@ Search Project Member [Arguments] ${project} ${user} Go Into Project ${project} Sleep 2 - Click Element xpath=//clr-dg-cell//a[contains(.,"${project}")] + Click Element xpath=//clr-dg-cell//a[contains(.,'${project}')] Sleep 1 Click Element xpath=${project_member_search_button_xpath} Sleep 1 @@ -66,14 +66,14 @@ Search Project Member Change Project Member Role [Arguments] ${project} ${user} ${role} - Click Element xpath=//clr-dg-cell//a[contains(.,"${project}")] + Click Element xpath=//clr-dg-cell//a[contains(.,'${project}')] Sleep 2 Click Element xpath=${project_member_tag_xpath} Sleep 1 Click Element xpath=//project-detail//clr-dg-row[contains(.,'${user}')]//label Sleep 1 #change role - Click Element //*[@id="member-action"] + Click Element //*[@id='member-action'] Click Element //button[contains(.,'${role}')] Sleep 2 Wait Until Page Contains ${role} @@ -149,7 +149,7 @@ Manage Project Member Change User Role In Project [Arguments] ${admin} ${pwd} ${project} ${user} ${role} Sign In Harbor ${HARBOR_URL} ${admin} ${pwd} - Wait Until Element Is Visible //clr-dg-cell//a[contains(.,"${project}")] + Wait Until Element Is Visible //clr-dg-cell//a[contains(.,'${project}')] Change Project Member Role ${project} ${user} ${role} Logout Harbor diff --git a/tests/resources/Harbor-Pages/Project-Members_Elements.robot b/tests/resources/Harbor-Pages/Project-Members_Elements.robot index 13640ddf3b..8699c1ae2a 100644 --- a/tests/resources/Harbor-Pages/Project-Members_Elements.robot +++ b/tests/resources/Harbor-Pages/Project-Members_Elements.robot @@ -18,7 +18,7 @@ Documentation This resource provides any keywords related to the Harbor private *** Variables *** ${project_member_tag_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/nav/ul/li[3]/a ${project_member_add_button_xpath} //project-detail//button[contains(.,'User')] -${project_member_add_username_xpath} //*[@id="member_name"] +${project_member_add_username_xpath} //*[@id='member_name'] ${project_member_add_admin_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/ng-component/div/div[1]/div/div[1]/add-member/clr-modal/div/div[1]/div/div[1]/div/div[2]/form/section/div[2]/div[1]/label ${project_member_add_save_button_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/ng-component/div/div[1]/div/div[1]/add-member/clr-modal/div/div[1]/div/div[1]/div/div[3]/button[2] ${project_member_search_button_xpath} //project-detail//hbr-filter/span/clr-icon @@ -27,4 +27,4 @@ ${project_member_add_confirmation_ok_xpath} //project-detail//add-member//butto ${project_member_search_button_xpath2} //button[contains(.,'New')] ${project_member_add_button_xpath2} //project-detail//add-member//button[2] ${project_member_guest_radio_checkbox} //project-detail//form//input[@id='checkrads_guest'] -${project_member_delete_button_xpath} //button[contains(.,"REMOVE")] +${project_member_delete_button_xpath} //button[contains(.,'REMOVE')] diff --git a/tests/resources/Harbor-Pages/Project-Retag.robot b/tests/resources/Harbor-Pages/Project-Retag.robot index 493f1e985c..8a0f75db46 100644 --- a/tests/resources/Harbor-Pages/Project-Retag.robot +++ b/tests/resources/Harbor-Pages/Project-Retag.robot @@ -5,7 +5,7 @@ Resource ../../resources/Util.robot Retag Image [Arguments] ${tag} ${projectname} ${reponame} ${tagname} - Click Element xpath=//clr-dg-row[contains(.,"${tag}")]//label + Click Element xpath=//clr-dg-row[contains(.,'${tag}')]//label Sleep 1 Click Element xpath=${retag_btn} Sleep 1 diff --git a/tests/resources/Harbor-Pages/Project.robot b/tests/resources/Harbor-Pages/Project.robot index 480b04fd0d..9b60966f8b 100644 --- a/tests/resources/Harbor-Pages/Project.robot +++ b/tests/resources/Harbor-Pages/Project.robot @@ -93,7 +93,7 @@ Make Project Private Click Element xpath=//project-detail//a[contains(.,'Configuration')] Sleep 1 Checkbox Should Be Selected xpath=//input[@name='public'] - Click Element //div[@id="clr-wrapper-public"]//label + Click Element //div[@id='clr-wrapper-public']//label Wait Until Element Is Enabled //button[contains(.,'SAVE')] Click Element //button[contains(.,'SAVE')] Wait Until Page Contains Configuration has been successfully saved @@ -104,22 +104,17 @@ Make Project Public Sleep 1 Click Element xpath=//project-detail//a[contains(.,'Configuration')] Checkbox Should Not Be Selected xpath=//input[@name='public'] - Click Element //div[@id="clr-wrapper-public"]//label + Click Element //div[@id='clr-wrapper-public']//label Wait Until Element Is Enabled //button[contains(.,'SAVE')] Click Element //button[contains(.,'SAVE')] Wait Until Page Contains Configuration has been successfully saved Delete Repo [Arguments] ${projectname} - ${element_repo_checkbox}= Set Variable xpath=//clr-dg-row[contains(.,"${projectname}")]//clr-checkbox-wrapper//label - Retry Keyword With Element When Error Wait Until Element Is Visible And Enabled ${element_repo_checkbox} - Retry Keyword With Element When Error Click Element ${element_repo_checkbox} - ${element_delete_btn}= Set Variable xpath=//button[contains(.,"Delete")] - Retry Keyword With Element When Error Wait Until Element Is Visible And Enabled ${element_delete_btn} - Retry Keyword With Element When Error Click Element ${element_delete_btn} - ${element_delete_confirm_btn}= Set Variable xpath=//clr-modal//button[2] - Retry Keyword With Element When Error Wait Until Element Is Visible And Enabled ${element_delete_confirm_btn} - Retry Keyword With Element When Error Click Element ${element_delete_confirm_btn} + ${element_repo_checkbox}= Set Variable xpath=//clr-dg-row[contains(.,'${projectname}')]//clr-checkbox-wrapper//label + Retry Element Click ${element_repo_checkbox} + Retry Element Click ${repo_delete_btn} + Retry Element Click ${repo_delete_confirm_btn} Delete Repo on CardView [Arguments] ${reponame} @@ -133,9 +128,9 @@ Delete Project [Arguments] ${projectname} Navigate To Projects Sleep 1 - Click Element xpath=//clr-dg-row[contains(.,"${projectname}")]//clr-checkbox-wrapper//label + Click Element xpath=//clr-dg-row[contains(.,'${projectname}')]//clr-checkbox-wrapper//label Sleep 1 - Click Element xpath=//button[contains(.,"Delete")] + Click Element xpath=//button[contains(.,'Delete')] Sleep 2 Click Element //clr-modal//button[contains(.,'DELETE')] Sleep 1 @@ -144,56 +139,56 @@ Project Should Not Be Deleted [Arguments] ${projname} Delete Project ${projname} Sleep 1 - Page Should Contain Element //clr-tab-content//div[contains(.,'${projname}')]/../div/clr-icon[@shape="error-standard"] + Page Should Contain Element //clr-tab-content//div[contains(.,'${projname}')]/../div/clr-icon[@shape='error-standard'] Project Should Be Deleted [Arguments] ${projname} Delete Project ${projname} Sleep 2 - Page Should Contain Element //clr-tab-content//div[contains(.,'${projname}')]/../div/clr-icon[@shape="success-standard"] + Page Should Contain Element //clr-tab-content//div[contains(.,'${projname}')]/../div/clr-icon[@shape='success-standard'] Advanced Search Should Display - Page Should Contain Element xpath=//audit-log//div[@class="flex-xs-middle"]/button + Page Should Contain Element xpath=//audit-log//div[@class='flex-xs-middle']/button # it's not a common keywords, only used into log case. Do Log Advanced Search Capture Page Screenshot LogAdvancedSearch.png Sleep 1 - Page Should Contain Element xpath=//clr-dg-row[contains(.,"pull")] - Page Should Contain Element xpath=//clr-dg-row[contains(.,"push")] - Page Should Contain Element xpath=//clr-dg-row[contains(.,"create")] - Page Should Contain Element xpath=//clr-dg-row[contains(.,"delete")] + Page Should Contain Element xpath=//clr-dg-row[contains(.,'pull')] + Page Should Contain Element xpath=//clr-dg-row[contains(.,'push')] + Page Should Contain Element xpath=//clr-dg-row[contains(.,'create')] + Page Should Contain Element xpath=//clr-dg-row[contains(.,'delete')] Sleep 1 - Click Element xpath=//audit-log//div[@class="flex-xs-middle"]/button + Click Element xpath=//audit-log//div[@class='flex-xs-middle']/button Sleep 1 Click Element xpath=//project-detail//audit-log//clr-dropdown/button Sleep 1 #pull log Sleep 1 - Click Element xpath=//audit-log//clr-dropdown//a[contains(.,"Pull")] + Click Element xpath=//audit-log//clr-dropdown//a[contains(.,'Pull')] Sleep 1 - Page Should Not Contain Element xpath=//clr-dg-row[contains(.,"pull")] + Page Should Not Contain Element xpath=//clr-dg-row[contains(.,'pull')] #push log Click Element xpath=//audit-log//clr-dropdown/button Sleep 1 - Click Element xpath=//audit-log//clr-dropdown//a[contains(.,"Push")] + Click Element xpath=//audit-log//clr-dropdown//a[contains(.,'Push')] Sleep 1 - Page Should Not Contain Element xpath=//clr-dg-row[contains(.,"push")] + Page Should Not Contain Element xpath=//clr-dg-row[contains(.,'push')] #create log Click Element xpath=//audit-log//clr-dropdown/button Sleep 1 - Click Element xpath=//audit-log//clr-dropdown//a[contains(.,"Create")] + Click Element xpath=//audit-log//clr-dropdown//a[contains(.,'Create')] Sleep 1 - Page Should Not Contain Element xpath=//clr-dg-row[contains(.,"create")] + Page Should Not Contain Element xpath=//clr-dg-row[contains(.,'create')] #delete log Click Element xpath=//audit-log//clr-dropdown/button Sleep 1 - Click Element xpath=//audit-log//clr-dropdown//a[contains(.,"Delete")] + Click Element xpath=//audit-log//clr-dropdown//a[contains(.,'Delete')] Sleep 1 - Page Should Not Contain Element xpath=//clr-dg-row[contains(.,"delete")] + Page Should Not Contain Element xpath=//clr-dg-row[contains(.,'delete')] #others Click Element xpath=//audit-log//clr-dropdown/button - Click Element xpath=//audit-log//clr-dropdown//a[contains(.,"Others")] + Click Element xpath=//audit-log//clr-dropdown//a[contains(.,'Others')] Sleep 1 Click Element xpath=//audit-log//hbr-filter//clr-icon Input Text xpath=//audit-log//hbr-filter//input harbor @@ -225,21 +220,21 @@ Expand Repo Sleep 1 Edit Repo Info - Click Element //*[@id="repo-info"] + Click Element //*[@id='repo-info'] Sleep 1 - Page Should Contain Element //*[@id="info"]/form/div[2] + Page Should Contain Element //*[@id='info']/form/div[2] # Cancel input - Click Element xpath=//*[@id="info-edit-button"]/button - Input Text xpath=//*[@id="info"]/form/div[2]/textarea test_description_info - Click Element xpath=//*[@id="info"]/form/div[3]/button[2] + Click Element xpath=//*[@id='info-edit-button']/button + Input Text xpath=//*[@id='info']/form/div[2]/textarea test_description_info + Click Element xpath=//*[@id='info']/form/div[3]/button[2] Sleep 1 - Click Element xpath=//*[@id="info"]/form/confirmation-dialog/clr-modal/div/div[1]/div[1]/div/div[3]/button[2] + Click Element xpath=//*[@id='info']/form/confirmation-dialog/clr-modal/div/div[1]/div[1]/div/div[3]/button[2] Sleep 1 - Page Should Contain Element //*[@id="info"]/form/div[2] + Page Should Contain Element //*[@id='info']/form/div[2] # Confirm input - Click Element xpath=//*[@id="info-edit-button"]/button - Input Text xpath=//*[@id="info"]/form/div[2]/textarea test_description_info - Click Element xpath=//*[@id="info"]/form/div[3]/button[1] + Click Element xpath=//*[@id='info-edit-button']/button + Input Text xpath=//*[@id='info']/form/div[2]/textarea test_description_info + Click Element xpath=//*[@id='info']/form/div[3]/button[1] Sleep 1 Page Should Contain test_description_info Capture Page Screenshot RepoInfo.png @@ -254,35 +249,35 @@ Switch To Project Repo Add Labels To Tag [Arguments] ${tagName} ${labelName} - Click Element xpath=//clr-dg-row[contains(.,"${tagName}")]//label + Click Element xpath=//clr-dg-row[contains(.,'${tagName}')]//label Capture Page Screenshot add_${labelName}.png Sleep 1 Click Element xpath=//clr-dg-action-bar//clr-dropdown//button Sleep 1 - Click Element xpath=//clr-dropdown//div//label[contains(.,"${labelName}")] + Click Element xpath=//clr-dropdown//div//label[contains(.,'${labelName}')] Sleep 3 - Page Should Contain Element xpath=//clr-dg-row//label[contains(.,"${labelName}")] + Page Should Contain Element xpath=//clr-dg-row//label[contains(.,'${labelName}')] Filter Labels In Tags [Arguments] ${labelName1} ${labelName2} Sleep 2 - Click Element xpath=//*[@id="filterArea"]//hbr-filter/span/clr-icon + Click Element xpath=//*[@id='filterArea']//hbr-filter/span/clr-icon Sleep 2 - Page Should Contain Element xpath=//*[@id="filterArea"]//div//button[contains(.,"${labelName1}")] - Click Element xpath=//*[@id="filterArea"]//div//button[contains(.,"${labelName1}")] + Page Should Contain Element xpath=//*[@id='filterArea']//div//button[contains(.,'${labelName1}')] + Click Element xpath=//*[@id='filterArea']//div//button[contains(.,'${labelName1}')] Sleep 2 - Click Element xpath=//*[@id="filterArea"]//hbr-filter/span/clr-icon - Page Should Contain Element xpath=//clr-datagrid//label[contains(.,"${labelName1}")] + Click Element xpath=//*[@id='filterArea']//hbr-filter/span/clr-icon + Page Should Contain Element xpath=//clr-datagrid//label[contains(.,'${labelName1}')] - Click Element xpath=//*[@id="filterArea"]//hbr-filter/span/clr-icon + Click Element xpath=//*[@id='filterArea']//hbr-filter/span/clr-icon Sleep 2 - Click Element xpath=//*[@id="filterArea"]//div//button[contains(.,"${labelName2}")] + Click Element xpath=//*[@id='filterArea']//div//button[contains(.,'${labelName2}')] Sleep 2 - Click Element xpath=//*[@id="filterArea"]//hbr-filter/span/clr-icon + Click Element xpath=//*[@id='filterArea']//hbr-filter/span/clr-icon Sleep 2 Capture Page Screenshot filter_${labelName2}.png - Page Should Contain Element xpath=//clr-dg-row[contains(.,"${labelName2}")] - Page Should Not Contain Element xpath=//clr-dg-row[contains(.,"${labelName1}")] + Page Should Contain Element xpath=//clr-dg-row[contains(.,'${labelName2}')] + Page Should Not Contain Element xpath=//clr-dg-row[contains(.,'${labelName1}')] Get Statics Private Repo ${privaterepo}= Get Text //project/div/div/div[1]/div/statistics-panel/div/div[2]/div[1]/div[2]/div[2]/statistics/div/span[1] diff --git a/tests/resources/Harbor-Pages/Project_Elements.robot b/tests/resources/Harbor-Pages/Project_Elements.robot index 5ca8032638..0b5f5fcee9 100644 --- a/tests/resources/Harbor-Pages/Project_Elements.robot +++ b/tests/resources/Harbor-Pages/Project_Elements.robot @@ -17,7 +17,7 @@ Documentation This resource provides any keywords related to the Harbor private *** Variables *** ${create_project_button_xpath} //clr-main-container//button[contains(., 'New Project')] -${project_name_xpath} //*[@id="create_project_name"] +${project_name_xpath} //*[@id='create_project_name'] ${project_public_xpath} //input[@name='public']/..//label ${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-container.main-container div.content-container div.content-area.content-area-override project div.row div.col-lg-12.col-md-12.col-sm-12.col-xs-12 div.row.flex-items-xs-between div.option-left create-project clr-modal div.modal div.modal-dialog div.modal-content div.modal-footer button.btn.btn-primary ${log_xpath} //clr-main-container//clr-vertical-nav//a[contains(.,'Logs')] @@ -29,4 +29,5 @@ ${project_member_xpath} //project-detail//li[contains(.,'Members')] ${create_project_OK_button_xpath} xpath=//button[contains(.,'OK')] ${create_project_CANCEL_button_xpath} xpath=//button[contains(.,'CANCEL')] ${project_statistics_private_repository_icon} xpath=//project/div/div/div[1]/div/statistics-panel/div/div[2]/div[1]/div[2]/div[2]/statistics/div/span[1] - +${repo_delete_btn} xpath=//button[contains(.,'Delete')] +${repo_delete_confirm_btn} xpath=//clr-modal//button[2] diff --git a/tests/resources/Harbor-Pages/Replication.robot b/tests/resources/Harbor-Pages/Replication.robot index bdd17f847b..583edc1af0 100644 --- a/tests/resources/Harbor-Pages/Replication.robot +++ b/tests/resources/Harbor-Pages/Replication.robot @@ -62,8 +62,8 @@ Create A Rule With Existing Endpoint Click Element //select[@id='ruleTarget']//option[contains(.,'${endpoint}')] #set trigger Click Element ${rule_trigger_select} - Wait Until Element Is Visible //select[@id="ruleTrigger"]//option[contains(.,'${mode}')] - Click Element //select[@id="ruleTrigger"]//option[contains(.,'${mode}')] + Wait Until Element Is Visible //select[@id='ruleTrigger']//option[contains(.,'${mode}')] + Click Element //select[@id='ruleTrigger']//option[contains(.,'${mode}')] Run Keyword If '${mode}' == 'Scheduled' Setting Replicaiton Schedule ${plan} ${weekday} ${time} #click save Click Element ${rule_save_button} @@ -88,8 +88,8 @@ Project Create A Rule With Existing Endpoint Click Element //select[@id='ruleTarget']//option[contains(.,'${endpoint}')] #set trigger Click Element ${rule_trigger_select} - Wait Until Element Is Visible //select[@id="ruleTrigger"]//option[contains(.,'${mode}')] - Click Element //select[@id="ruleTrigger"]//option[contains(.,'${mode}')] + Wait Until Element Is Visible //select[@id='ruleTrigger']//option[contains(.,'${mode}')] + Click Element //select[@id='ruleTrigger']//option[contains(.,'${mode}')] Run Keyword If '${mode}' == 'Scheduled' Setting Replicaiton Schedule ${plan} ${weekday} ${time} #click save Click Element ${rule_save_button} @@ -97,16 +97,16 @@ Project Create A Rule With Existing Endpoint Setting Replication Schedule [Arguments] ${plan} ${weekday}=1 ${time}=0800a Click Element ${schedule_type_select} - Wait Until Element Is Visible //select[@name="scheduleType"]/option[@value="${plan}"] - Click Element //select[@name="scheduleType"]/option[@value="${plan}"] + Wait Until Element Is Visible //select[@name='scheduleType']/option[@value='${plan}'] + Click Element //select[@name='scheduleType']/option[@value='${plan}'] Run Keyword If '${plan}' == 'Weekly' Setting Replication Weekday ${weekday} Input Text ${shcedule_time} ${time} Setting Replication Weekday [arguments] ${day} Click Element ${schedule_day_select} - Wait Until Element Is Visible //select[@name="scheduleDay"]/option[@value='${day}'] - Click Element //select[@name="scheduleDay"]/option[@value='${day}'] + Wait Until Element Is Visible //select[@name='scheduleDay']/option[@value='${day}'] + Click Element //select[@name='scheduleDay']/option[@value='${day}'] Endpoint Is Unpingable Click Element ${ping_test_button} @@ -146,7 +146,7 @@ Trigger Replication Manual Mouse Down ${dialog_replicate} Mouse Up ${dialog_replicate} Sleep 2 - Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${rule}')]/../div/clr-icon[@shape="success-standard"] + Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${rule}')]/../div/clr-icon[@shape='success-standard'] Sleep 1 Rename Rule diff --git a/tests/resources/Harbor-Pages/Replication_Elements.robot b/tests/resources/Harbor-Pages/Replication_Elements.robot index 0d614ad68c..44fd26d9fb 100644 --- a/tests/resources/Harbor-Pages/Replication_Elements.robot +++ b/tests/resources/Harbor-Pages/Replication_Elements.robot @@ -17,8 +17,8 @@ Documentation This resource provides any keywords related to the Harbor private *** Variables *** ${new_name_xpath} //hbr-list-replication-rule//button[contains(.,'New')] -${policy_name_xpath} //*[@id="policy_name"] -${policy_description_xpath} //*[@id="policy_description"] +${policy_name_xpath} //*[@id='policy_name'] +${policy_description_xpath} //*[@id='policy_description'] ${policy_enable_checkbox} //input[@id='policy_enable']/../label ${policy_endpoint_checkbox} //input[@id='check_new']/../label ${destination_name_xpath} //*[@id='destination_name'] @@ -27,22 +27,22 @@ ${destination_username_xpath} //*[@id='destination_username'] ${destination_password_xpath} //*[@id='destination_password'] ${replication_save_xpath} //button[contains(.,'OK')] ${replication_xpath} //clr-vertical-nav-group-children/a[contains(.,'Replication')] -${destination_insecure_xpath} //label[@id="destination_insecure_checkbox"] +${destination_insecure_xpath} //label[@id='destination_insecure_checkbox'] ${new_replication-rule_button} //button[contains(.,'New Replication Rule')] ${link_to_registries} //clr-modal//span[contains(.,'Endpoint')] ${new_endpoint_button} //hbr-endpoint//button[contains(.,'New')] ${rule_name} //input[@id='ruleName'] ${source_project} //input[@value='name'] -${source_image_filter_add} //hbr-create-edit-rule/clr-modal//clr-icon[@id="add-label-list"] +${source_image_filter_add} //hbr-create-edit-rule/clr-modal//clr-icon[@id='add-label-list'] ${source_iamge_repo_filter} //hbr-create-edit-rule//section/div[4]/div/div[1]/div/label/input ${source_image_tag_filter} //hbr-create-edit-rule//section/div[4]/div/div[2]/div/label/input ${rule_target_select} //select[@id='ruleTarget'] -${rule_trigger_select} //select[@id="ruleTrigger"] -${schedule_type_select} //select[@name="scheduleType"] -${schedule_day_select} //select[@name="scheduleDay"] -${shcedule_time} //input[@type="time"] -${destination_insecure_checkbox} //hbr-create-edit-endpoint/clr-modal//input[@id="destination_insecure"] +${rule_trigger_select} //select[@id='ruleTrigger'] +${schedule_type_select} //select[@name='scheduleType'] +${schedule_day_select} //select[@name='scheduleDay'] +${shcedule_time} //input[@type='time'] +${destination_insecure_checkbox} //hbr-create-edit-endpoint/clr-modal//input[@id='destination_insecure'] ${ping_test_button} //button[contains(.,'Test')] ${nav_to_registries} //clr-vertical-nav//span[contains(.,'Registries')] ${nav_to_replications} //clr-vertical-nav//span[contains(.,'Replications')] diff --git a/tests/resources/Harbor-Pages/ToolKit.robot b/tests/resources/Harbor-Pages/ToolKit.robot index fc71d7b48c..a2222d387e 100644 --- a/tests/resources/Harbor-Pages/ToolKit.robot +++ b/tests/resources/Harbor-Pages/ToolKit.robot @@ -23,14 +23,14 @@ ${HARBOR_VERSION} v1.1.1 Delete Success [Arguments] @{obj} :For ${obj} in @{obj} - \ Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${obj}')]/../div/clr-icon[@shape="success-standard"] + \ Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${obj}')]/../div/clr-icon[@shape='success-standard'] Sleep 1 Capture Page Screenshot Delete Fail [Arguments] @{obj} :For ${obj} in @{obj} - \ Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${obj}')]/../div/clr-icon[@shape="error-standard"] + \ Wait Until Page Contains Element //clr-tab-content//div[contains(.,'${obj}')]/../div/clr-icon[@shape='error-standard'] Sleep 1 Capture Page Screenshot diff --git a/tests/resources/Harbor-Pages/ToolKit_Elements.robot b/tests/resources/Harbor-Pages/ToolKit_Elements.robot index b19fb0450b..17d2ceb131 100644 --- a/tests/resources/Harbor-Pages/ToolKit_Elements.robot +++ b/tests/resources/Harbor-Pages/ToolKit_Elements.robot @@ -16,5 +16,5 @@ Documentation This resource provides any keywords related to the Harbor private registry appliance *** Variables *** -${member_action_xpath} //*[@id="member-action"] +${member_action_xpath} //*[@id='member-action'] ${delete_action_xpath} //clr-dropdown/clr-dropdown-menu/button[4] diff --git a/tests/resources/Harbor-Pages/UserProfile.robot b/tests/resources/Harbor-Pages/UserProfile.robot index 6b64cdd2b2..6be351bc04 100644 --- a/tests/resources/Harbor-Pages/UserProfile.robot +++ b/tests/resources/Harbor-Pages/UserProfile.robot @@ -25,9 +25,9 @@ Change Password Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/navigator/clr-header/div[3]/clr-dropdown[2]/button/span Click Element xpath=//clr-main-container//clr-dropdown//a[2] Sleep 2 - Input Text xpath=//*[@id="oldPassword"] ${cur_pw} - Input Text xpath=//*[@id="newPassword"] ${new_pw} - Input Text xpath=//*[@id="reNewPassword"] ${new_pw} + Input Text xpath=//*[@id='oldPassword'] ${cur_pw} + Input Text xpath=//*[@id='newPassword'] ${new_pw} + Input Text xpath=//*[@id='reNewPassword'] ${new_pw} Sleep 1 Click Element xpath=//password-setting/clr-modal//button[2] Sleep 2 @@ -39,7 +39,7 @@ Update User Comment Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/navigator/clr-header/div[3]/clr-dropdown[2]/button/span Click Element xpath=//clr-main-container//clr-dropdown//a[1] Sleep 2 - Input Text xpath=//*[@id="account_settings_comments"] ${new_comment} + Input Text xpath=//*[@id='account_settings_comments'] ${new_comment} Sleep 1 Click Element xpath=//account-settings-modal/clr-modal//button[2] Sleep 2 @@ -54,4 +54,4 @@ Logout Harbor Sleep 1 Capture Page Screenshot Logout.png Sleep 2 - Wait Until Keyword Succeeds 5x 1 Page Should Contain Element xpath=//sign-in//form//*[@class="title"] + Wait Until Keyword Succeeds 5x 1 Page Should Contain Element xpath=//sign-in//form//*[@class='title'] diff --git a/tests/resources/Harbor-Pages/Verify.robot b/tests/resources/Harbor-Pages/Verify.robot index 0690abb203..10ed042290 100644 --- a/tests/resources/Harbor-Pages/Verify.robot +++ b/tests/resources/Harbor-Pages/Verify.robot @@ -163,13 +163,13 @@ Verify System Setting Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD} Switch To Configure Page Should Contain @{authtype}[0] - Run Keyword If @{selfreg}[0] == "True" Checkbox Should Be Checked //clr-checkbox-wrapper[@id='selfReg']//label - Run Keyword If @{selfreg}[0] == "False" Checkbox Should Not Be Checked //clr-checkbox-wrapper[@id='selfReg']//label + Run Keyword If @{selfreg}[0] == 'True' Checkbox Should Be Checked //clr-checkbox-wrapper[@id='selfReg']//label + Run Keyword If @{selfreg}[0] == 'False' Checkbox Should Not Be Checked //clr-checkbox-wrapper[@id='selfReg']//label Switch To Email - Textfield Value Should Be xpath=//*[@id="mailServer"] @{emailserver}[0] - Textfield Value Should Be xpath=//*[@id="emailPort"] @{emailport}[0] - Textfield Value Should Be xpath=//*[@id="emailUsername"] @{emailuser}[0] - Textfield Value Should Be xpath=//*[@id="emailFrom"] @{emailfrom}[0] + Textfield Value Should Be xpath=//*[@id='mailServer'] @{emailserver}[0] + Textfield Value Should Be xpath=//*[@id='emailPort'] @{emailport}[0] + Textfield Value Should Be xpath=//*[@id='emailUsername'] @{emailuser}[0] + Textfield Value Should Be xpath=//*[@id='emailFrom'] @{emailfrom}[0] Switch To System Settings Page Should Contain @{creation}[0] Token Must Be Match @{token}[0] diff --git a/tests/resources/Harbor-Pages/Vulnerability.robot b/tests/resources/Harbor-Pages/Vulnerability.robot index 4e4ed5e311..98618b8248 100644 --- a/tests/resources/Harbor-Pages/Vulnerability.robot +++ b/tests/resources/Harbor-Pages/Vulnerability.robot @@ -7,10 +7,10 @@ Resource ../../resources/Util.robot *** Keywords *** Disable Scan Schedule - Click Element //vulnerability-config//button[@id="editSchedule"] - Click Element //vulnerability-config//select[@id="scanAllPolicy"] - Click Element //vulnerability-config//select[@id="scanAllPolicy"]//option[contains(.,'None')] - Click Element //button[@id="config_vulnerbility_save"] + Click Element //vulnerability-config//button[@id='editSchedule'] + Click Element //vulnerability-config//select[@id='scanAllPolicy'] + Click Element //vulnerability-config//select[@id='scanAllPolicy']//option[contains(.,'None')] + Click Element //button[@id='config_vulnerbility_save'] Go To Vulnerability Config Click Element //config//button[contains(.,'Vulnerability')] @@ -24,8 +24,8 @@ Set Vulnerabilty Serverity [Arguments] ${level} Goto Project Config #enable first - Click Element //project-detail//div[@id="prevent-vulenrability-image"]//label - Checkbox Should Be Selected //project-detail//clr-checkbox-wrapper//input[@name="prevent-vulenrability-image-input"] + Click Element //project-detail//div[@id='prevent-vulenrability-image']//label + Checkbox Should Be Selected //project-detail//clr-checkbox-wrapper//input[@name='prevent-vulenrability-image-input'] Click Element //project-detail//select #wait for dropdown popup Sleep 1 @@ -45,7 +45,7 @@ Scan Repo #use fail for image clair can not scan, otherwise use success [Arguments] ${tagname} ${status} #select one tag - Click Element //clr-dg-row[contains(.,"${tagname}")]//label + Click Element //clr-dg-row[contains(.,'${tagname}')]//label Click Element //button[contains(.,'Scan')] Run Keyword If '${status}' == 'Succeed' Wait Until Element Is Visible //hbr-vulnerability-bar//hbr-vulnerability-summary-chart 300 Run Keyword If '${status}' == 'Fail' Wait Until Element Is Visible //hbr-vulnerability-bar//a 300 @@ -56,14 +56,14 @@ Summary Chart Should Display Page Should Contain Element //clr-dg-row[contains(.,'${tagname}')]//hbr-vulnerability-bar//hbr-vulnerability-summary-chart Enable Scan On Push - Checkbox Should Not Be Selected //clr-checkbox-wrapper[@id="scan-image-on-push-wrapper"]//input - Click Element //clr-checkbox-wrapper[@id="scan-image-on-push-wrapper"]//label - Checkbox Should Be Selected //clr-checkbox-wrapper[@id="scan-image-on-push-wrapper"]//input + Checkbox Should Not Be Selected //clr-checkbox-wrapper[@id='scan-image-on-push-wrapper']//input + Click Element //clr-checkbox-wrapper[@id='scan-image-on-push-wrapper']//label + Checkbox Should Be Selected //clr-checkbox-wrapper[@id='scan-image-on-push-wrapper']//input Click Element //hbr-project-policy-config//button[contains(.,'SAVE')] Sleep 10 Vulnerability Not Ready Project Hint - ${element}= Set Variable xpath=//span[contains(@class, "db-status-warning")] + ${element}= Set Variable xpath=//span[contains(@class, 'db-status-warning')] Wait Until Element Is Visible And Enabled ${element} Vulnerability Not Ready Config Hint diff --git a/tests/resources/Util.robot b/tests/resources/Util.robot index 7b6ae01e24..84a7741acc 100644 --- a/tests/resources/Util.robot +++ b/tests/resources/Util.robot @@ -67,6 +67,36 @@ Wait Until Element Is Visible And Enabled Wait Until Element Is Visible ${element} Wait Until Element Is Enabled ${element} +Retry Action Keyword + [Arguments] ${keyword} ${element_xpath} + Retry Keyword When Error ${keyword} element=${element_xpath} + +Retry Element Click + [Arguments] ${element_xpath} + Retry Action Keyword Element Click ${element_xpath} + +Retry Wait Element + [Arguments] ${element_xpath} + Retry Action Keyword Wait Element ${element_xpath} + +Retry Button Click + [Arguments] ${element_xpath} + Retry Action Keyword Button Click ${element_xpath} + +Element Click + [Arguments] ${element_xpath} + Wait Until Element Is Visible And Enabled ${element_xpath} + Click Element ${element_xpath} + +Wait Element + [Arguments] ${element_xpath} + Wait Until Element Is Visible And Enabled ${element_xpath} + +Button Click + [Arguments] ${element_xpath} + Wait Until Element Is Visible And Enabled ${element_xpath} + Click button ${element_xpath} + Wait Unitl Vul Data Ready [Arguments] ${url} ${timeout} ${interval} ${n}= Evaluate ${timeout}/${interval} @@ -80,23 +110,12 @@ Wait Unitl Vul Data Ready Run Keyword If ${i+1}==${n} Fail The vul data is not ready Retry Keyword When Error - [Arguments] ${keyword} ${times}=6 + [Arguments] ${keyword} ${element}=${None} ${times}=6 :For ${n} IN RANGE 1 ${times} \ Log To Console Attampt to ${keyword} ${n} times ... - \ ${out} Run Keyword And Ignore Error ${keyword} - \ Log To Console Return value is ${out} + \ ${out} Run Keyword If "${element}"=="${None}" Run Keyword And Ignore Error ${keyword} + \ ... ELSE Run Keyword And Ignore Error ${keyword} ${element} + \ Log To Console Return value is ${out[0]} \ Exit For Loop If '${out[0]}'=='PASS' - \ Sleep 3 - Should Be Equal As Strings '${out[0]}' 'PASS' - -Retry Keyword With Element When Error - [Arguments] ${keyword} ${element} ${times}=6 - #To prevent waiting for a fixed-period of time for page loading and failure caused by exception, we add loop to re-run when - # exception was caught. - :For ${n} IN RANGE 1 ${times} - \ Log To Console Attampt to wait for ${n} times ... - \ ${out} Run Keyword And Ignore Error ${keyword} ${element} - \ Log To Console Return value is ${out} - \ Exit For Loop If '${out[0]}'=='PASS' - \ Sleep 2 + \ Sleep 1 Should Be Equal As Strings '${out[0]}' 'PASS' \ No newline at end of file From 5d6a28d73e3a1424fd03e148230dd8310fa78f8e Mon Sep 17 00:00:00 2001 From: wang yan Date: Wed, 30 Jan 2019 23:56:20 +0800 Subject: [PATCH 29/45] Remove the token attribute for robot table This commit is to remove the token attribute as harbor doesn't store the token in DB. Signed-off-by: wang yan --- make/migrations/postgresql/0004_add_robot_account.up.sql | 4 ---- src/common/dao/robot_test.go | 6 ------ src/common/models/robot.go | 1 - 3 files changed, 11 deletions(-) diff --git a/make/migrations/postgresql/0004_add_robot_account.up.sql b/make/migrations/postgresql/0004_add_robot_account.up.sql index 8255b37889..c2e3d273ba 100644 --- a/make/migrations/postgresql/0004_add_robot_account.up.sql +++ b/make/migrations/postgresql/0004_add_robot_account.up.sql @@ -1,10 +1,6 @@ CREATE TABLE robot ( id SERIAL PRIMARY KEY NOT NULL, name varchar(255), - /* - The maximum length of token is 7k -*/ - token varchar(7168), description varchar(1024), project_id int, disabled boolean DEFAULT false NOT NULL, diff --git a/src/common/dao/robot_test.go b/src/common/dao/robot_test.go index ea68d880dc..0ffbcf0813 100644 --- a/src/common/dao/robot_test.go +++ b/src/common/dao/robot_test.go @@ -27,7 +27,6 @@ func TestAddRobot(t *testing.T) { robotName := "test1" robot := &models.Robot{ Name: robotName, - Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q1", Description: "test1 description", ProjectID: 1, } @@ -46,7 +45,6 @@ func TestGetRobot(t *testing.T) { robotName := "test2" robot := &models.Robot{ Name: robotName, - Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q2", Description: "test2 description", ProjectID: 1, } @@ -66,7 +64,6 @@ func TestListRobots(t *testing.T) { robotName := "test3" robot := &models.Robot{ Name: robotName, - Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q3", Description: "test3 description", ProjectID: 1, } @@ -86,7 +83,6 @@ func TestDisableRobot(t *testing.T) { robotName := "test4" robot := &models.Robot{ Name: robotName, - Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q4", Description: "test4 description", ProjectID: 1, } @@ -111,7 +107,6 @@ func TestEnableRobot(t *testing.T) { robotName := "test5" robot := &models.Robot{ Name: robotName, - Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q5", Description: "test5 description", Disabled: true, ProjectID: 1, @@ -137,7 +132,6 @@ func TestDeleteRobot(t *testing.T) { robotName := "test6" robot := &models.Robot{ Name: robotName, - Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q6", Description: "test6 description", ProjectID: 1, } diff --git a/src/common/models/robot.go b/src/common/models/robot.go index 52a7c2975e..70e9906f0a 100644 --- a/src/common/models/robot.go +++ b/src/common/models/robot.go @@ -27,7 +27,6 @@ const RobotTable = "robot" type Robot struct { ID int64 `orm:"pk;auto;column(id)" json:"id"` Name string `orm:"column(name)" json:"name"` - Token string `orm:"column(token)" json:"token"` Description string `orm:"column(description)" json:"description"` ProjectID int64 `orm:"column(project_id)" json:"project_id"` Disabled bool `orm:"column(disabled)" json:"disabled"` From e5efbfe490fd880625ee2cae1590c1609afbbcd3 Mon Sep 17 00:00:00 2001 From: danfengliu Date: Thu, 31 Jan 2019 16:08:58 +0800 Subject: [PATCH 30/45] add retry keyword for docker pull and push image command. (#6857) Signed-off-by: danfengliu --- tests/resources/Docker-Util.robot | 99 ++++++------------- .../Harbor-Pages/Project-Members.robot | 2 +- .../Harbor-Pages/Project_Elements.robot | 1 + tests/resources/Util.robot | 13 +++ tests/robot-cases/Group1-Nightly/Common.robot | 3 +- 5 files changed, 48 insertions(+), 70 deletions(-) diff --git a/tests/resources/Docker-Util.robot b/tests/resources/Docker-Util.robot index fa29e8daf1..5cf5d83c39 100644 --- a/tests/resources/Docker-Util.robot +++ b/tests/resources/Docker-Util.robot @@ -20,17 +20,13 @@ Library Process *** Keywords *** Run Docker Info [Arguments] ${docker-params} - ${rc}= Run And Return Rc docker ${docker-params} info - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success docker ${docker-params} info Pull image [Arguments] ${ip} ${user} ${pwd} ${project} ${image} Log To Console \nRunning docker pull ${image}... - ${rc} ${output}= Run And Return Rc And Output docker login -u ${user} -p ${pwd} ${ip} - Should Be Equal As Integers ${rc} 0 - ${rc} ${output}= Run And Return Rc And Output docker pull ${ip}/${project}/${image} - Log ${output} - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success docker login -u ${user} -p ${pwd} ${ip} + ${output}= Wait Unitl Command Success docker pull ${ip}/${project}/${image} Should Contain ${output} Digest: Should Contain ${output} Status: Should Not Contain ${output} No such image: @@ -38,62 +34,41 @@ Pull image Push image [Arguments] ${ip} ${user} ${pwd} ${project} ${image} Log To Console \nRunning docker push ${image}... - ${rc} ${output}= Run And Return Rc And Output docker pull ${image} - Log ${output} - Should Be Equal As Integers ${rc} 0 - ${rc} ${output}= Run And Return Rc And Output docker login -u ${user} -p ${pwd} ${ip} - Log ${output} - Should Be Equal As Integers ${rc} 0 - ${rc}= Run And Return Rc docker tag ${image} ${ip}/${project}/${image} - ${rc} ${output}= Run And Return Rc And Output docker push ${ip}/${project}/${image} - Log ${output} - Should Be Equal As Integers ${rc} 0 - ${rc}= Run And Return Rc docker logout ${ip} + Wait Unitl Command Success docker pull ${image} + Wait Unitl Command Success docker login -u ${user} -p ${pwd} ${ip} + Wait Unitl Command Success docker tag ${image} ${ip}/${project}/${image} + Wait Unitl Command Success docker push ${ip}/${project}/${image} + Wait Unitl Command Success docker logout ${ip} Push Image With Tag #tag1 is tag of image on docker hub,default latest,use a version existing if you do not want to use latest [Arguments] ${ip} ${user} ${pwd} ${project} ${image} ${tag} ${tag1}=latest Log To Console \nRunning docker push ${image}... - ${rc} ${output}= Run And Return Rc And Output docker pull ${image}:${tag1} - Log ${output} - Should Be Equal As Integers ${rc} 0 - ${rc} ${output}= Run And Return Rc And Output docker login -u ${user} -p ${pwd} ${ip} - Log ${output} - Should Be Equal As Integers ${rc} 0 - ${rc}= Run And Return Rc docker tag ${image}:${tag1} ${ip}/${project}/${image}:${tag} - ${rc} ${output}= Run And Return Rc And Output docker push ${ip}/${project}/${image}:${tag} - Log ${output} - Should Be Equal As Integers ${rc} 0 - ${rc}= Run And Return Rc docker logout ${ip} + Wait Unitl Command Success docker pull ${image}:${tag1} + Wait Unitl Command Success docker login -u ${user} -p ${pwd} ${ip} + Wait Unitl Command Success docker tag ${image}:${tag1} ${ip}/${project}/${image}:${tag} + Wait Unitl Command Success docker push ${ip}/${project}/${image}:${tag} + Wait Unitl Command Success docker logout ${ip} Cannot Pull image [Arguments] ${ip} ${user} ${pwd} ${project} ${image} - ${rc} ${output}= Run And Return Rc And Output docker login -u ${user} -p ${pwd} ${ip} - Should Be Equal As Integers ${rc} 0 - ${rc} ${output}= Run And Return Rc And Output docker pull ${ip}/${project}/${image} - Log ${output} - Should Not Be Equal As Integers ${rc} 0 + Wait Unitl Command Success docker login -u ${user} -p ${pwd} ${ip} + Wait Unitl Command Success docker pull ${ip}/${project}/${image} positive=${false} Cannot Pull Unsigned Image [Arguments] ${ip} ${user} ${pass} ${proj} ${imagewithtag} - ${rc} ${output}= Run And Return Rc And Output docker login -u ${user} -p ${pass} ${ip} - Should Be Equal As Integers ${rc} 0 - ${rc} ${output}= Run And Return Rc And Output docker pull ${ip}/${proj}/${imagewithtag} + Wait Unitl Command Success docker login -u ${user} -p ${pass} ${ip} + ${output}= Wait Unitl Command Success docker pull ${ip}/${proj}/${imagewithtag} positive=${false} Should Contain ${output} The image is not signed in Notary - Should Not Be Equal As Integers ${rc} 0 Cannot Push image [Arguments] ${ip} ${user} ${pwd} ${project} ${image} Log To Console \nRunning docker push ${image}... - ${rc}= Run And Return Rc docker pull ${image} - ${rc} ${output}= Run And Return Rc And Output docker login -u ${user} -p ${pwd} ${ip} - Log ${output} - Should Be Equal As Integers ${rc} 0 - ${rc}= Run And Return Rc docker tag ${image} ${ip}/${project}/${image} - ${rc} ${output}= Run And Return Rc And Output docker push ${ip}/${project}/${image} - Log ${output} - Should Not Be Equal As Integers ${rc} 0 - ${rc}= Run And Return Rc docker logout ${ip} + Wait Unitl Command Success docker pull ${image} + Wait Unitl Command Success docker login -u ${user} -p ${pwd} ${ip} + Wait Unitl Command Success docker tag ${image} ${ip}/${project}/${image} + Wait Unitl Command Success docker push ${ip}/${project}/${image} positive=${false} + Wait Unitl Command Success docker logout ${ip} Wait Until Container Stops [Arguments] ${container} @@ -106,13 +81,11 @@ Wait Until Container Stops Hit Nginx Endpoint [Arguments] ${vch-ip} ${port} - ${rc} ${output}= Run And Return Rc And Output wget ${vch-ip}:${port} - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success wget ${vch-ip}:${port} Get Container IP [Arguments] ${docker-params} ${id} ${network}=default ${dockercmd}=docker - ${rc} ${ip}= Run And Return Rc And Output ${dockercmd} ${docker-params} network inspect ${network} | jq '.[0].Containers."${id}".IPv4Address' | cut -d \\" -f 2 | cut -d \\/ -f 1 - Should Be Equal As Integers ${rc} 0 + ${ip}= Wait Unitl Command Success ${dockercmd} ${docker-params} network inspect ${network} | jq '.[0].Containers."${id}".IPv4Address' | cut -d \\" -f 2 | cut -d \\/ -f 1 [Return] ${ip} # The local dind version is embedded in Dockerfile @@ -133,42 +106,34 @@ Start Docker Daemon Locally Prepare Docker Cert [Arguments] ${ip} - ${rc} ${out}= Run And Return Rc And Output mkdir -p /etc/docker/certs.d/${ip} - Should Be Equal As Integers ${rc} 0 - ${rc} ${out}= Run And Return Rc And Output cp harbor_ca.crt /etc/docker/certs.d/${ip} - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success mkdir -p /etc/docker/certs.d/${ip} + Wait Unitl Command Success cp harbor_ca.crt /etc/docker/certs.d/${ip} Kill Local Docker Daemon [Arguments] ${handle} ${dockerd-pid} Terminate Process ${handle} Process Should Be Stopped ${handle} - ${rc}= Run And Return Rc kill -9 ${dockerd-pid} - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success kill -9 ${dockerd-pid} Docker Login Fail [Arguments] ${ip} ${user} ${pwd} Log To Console \nRunning docker login ${ip} ... - ${rc} ${output}= Run And Return Rc And Output docker login -u ${user} -p ${pwd} ${ip} - Should Not Be Equal As Integers ${rc} 0 + ${output}= Wait Unitl Command Success docker login -u ${user} -p ${pwd} ${ip} positive=${false} Should Contain ${output} unauthorized: authentication required Should Not Contain ${output} 500 Internal Server Error Docker Login [Arguments] ${server} ${username} ${password} - ${rc} ${output}= Run And Return Rc And Output docker login -u ${username} -p ${password} ${server} - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success docker login -u ${username} -p ${password} ${server} Docker Pull [Arguments] ${image} - ${rc} ${output}= Run And Return Rc And Output docker pull ${image} - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success docker pull ${image} Docker Tag [Arguments] ${src_image} ${dst_image} - ${rc} ${output}= Run And Return Rc And Output docker tag ${src_image} ${dst_image} - Should Be Equal As Integers ${rc} 0 + Wait Unitl Command Success docker tag ${src_image} ${dst_image} Docker Push [Arguments] ${image} - ${rc} ${output}= Run And Return Rc And Output docker push ${image} - Should Be Equal As Integers ${rc} 0 \ No newline at end of file + Wait Unitl Command Success docker push ${image} \ No newline at end of file diff --git a/tests/resources/Harbor-Pages/Project-Members.robot b/tests/resources/Harbor-Pages/Project-Members.robot index b01bdb6195..1760c1849c 100644 --- a/tests/resources/Harbor-Pages/Project-Members.robot +++ b/tests/resources/Harbor-Pages/Project-Members.robot @@ -134,7 +134,7 @@ User Should Not Be A Member Of Project Project Should Not Display ${project} Logout Harbor Cannot Pull image ${ip} ${user} ${pwd} ${project} ${ip}/${project}/hello-world - Cannot Push image ${ip} ${user} ${pwd} ${project} ${ip}/${project}/hello-world + Cannot Push image ${ip} ${user} ${pwd} ${project} hello-world Manage Project Member [Arguments] ${admin} ${pwd} ${project} ${user} ${op} ${has_image}=${true} diff --git a/tests/resources/Harbor-Pages/Project_Elements.robot b/tests/resources/Harbor-Pages/Project_Elements.robot index 0b5f5fcee9..58d3d32c88 100644 --- a/tests/resources/Harbor-Pages/Project_Elements.robot +++ b/tests/resources/Harbor-Pages/Project_Elements.robot @@ -31,3 +31,4 @@ ${create_project_CANCEL_button_xpath} xpath=//button[contains(.,'CANCEL')] ${project_statistics_private_repository_icon} xpath=//project/div/div/div[1]/div/statistics-panel/div/div[2]/div[1]/div[2]/div[2]/statistics/div/span[1] ${repo_delete_btn} xpath=//button[contains(.,'Delete')] ${repo_delete_confirm_btn} xpath=//clr-modal//button[2] +${repo_retag_confirm_dlg} css=${modal-dialog} diff --git a/tests/resources/Util.robot b/tests/resources/Util.robot index 84a7741acc..f9796002e6 100644 --- a/tests/resources/Util.robot +++ b/tests/resources/Util.robot @@ -109,6 +109,19 @@ Wait Unitl Vul Data Ready \ Sleep ${interval} Run Keyword If ${i+1}==${n} Fail The vul data is not ready +Wait Unitl Command Success + [Arguments] ${cmd} ${times}=8 ${positive}=${true} + :FOR ${n} IN RANGE 1 ${times} + \ Log Trying ${cmd}: ${n} ... console=True + \ ${rc} ${output}= Run And Return Rc And Output ${cmd} + \ Run Keyword If ${positive} == ${true} Exit For Loop If '${rc}'=='0' + \ ... ELSE Exit For Loop If '${rc}'!='0' + \ Sleep 2 + Log ${output} + Run Keyword If ${positive} == ${true} Should Be Equal As Strings '${rc}' '0' + ... ELSE Should Not Be Equal As Strings '${rc}' '0' + [Return] ${output} + Retry Keyword When Error [Arguments] ${keyword} ${element}=${None} ${times}=6 :For ${n} IN RANGE 1 ${times} diff --git a/tests/robot-cases/Group1-Nightly/Common.robot b/tests/robot-cases/Group1-Nightly/Common.robot index b8271da85a..be1bb2dacd 100644 --- a/tests/robot-cases/Group1-Nightly/Common.robot +++ b/tests/robot-cases/Group1-Nightly/Common.robot @@ -672,8 +672,7 @@ Test Case - Retag A Image Tag Sleep 1 Go Into Repo project${random_num1}/redis Retag Image ${image_tag} project${random_num2} ${target_image_name} ${target_tag_value} - - Wait Until Element Is Not Visible css=${modal-dialog} + Retry Keyword When Error Wait Until Element Is Not Visible element=${repo_retag_confirm_dlg} Navigate To Projects Go Into Project project${random_num2} Sleep 1 From 47530cd62e9a9702ec175f84be369def73d48d7c Mon Sep 17 00:00:00 2001 From: danfengliu Date: Thu, 31 Jan 2019 17:58:05 +0800 Subject: [PATCH 31/45] remove some sleep and add retry common keyword (#6875) Signed-off-by: danfengliu --- tests/resources/Util.robot | 10 +++++----- tests/robot-cases/Group1-Nightly/Common.robot | 8 ++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/resources/Util.robot b/tests/resources/Util.robot index f9796002e6..f918bf1357 100644 --- a/tests/resources/Util.robot +++ b/tests/resources/Util.robot @@ -77,7 +77,11 @@ Retry Element Click Retry Wait Element [Arguments] ${element_xpath} - Retry Action Keyword Wait Element ${element_xpath} + Retry Action Keyword Wait Until Element Is Visible And Enabled ${element_xpath} + +Retry Wait Element Not Visible + [Arguments] ${element_xpath} + Retry Action Keyword Wait Until Element Is Not Visible ${element_xpath} Retry Button Click [Arguments] ${element_xpath} @@ -88,10 +92,6 @@ Element Click Wait Until Element Is Visible And Enabled ${element_xpath} Click Element ${element_xpath} -Wait Element - [Arguments] ${element_xpath} - Wait Until Element Is Visible And Enabled ${element_xpath} - Button Click [Arguments] ${element_xpath} Wait Until Element Is Visible And Enabled ${element_xpath} diff --git a/tests/robot-cases/Group1-Nightly/Common.robot b/tests/robot-cases/Group1-Nightly/Common.robot index be1bb2dacd..4003ea325b 100644 --- a/tests/robot-cases/Group1-Nightly/Common.robot +++ b/tests/robot-cases/Group1-Nightly/Common.robot @@ -422,7 +422,7 @@ Test Case - Delete Multi Project Push Image ${ip} user012 Test1@34 projecta${d} hello-world Navigate To Projects Filter Object project - Wait Until Element Is Not Visible //clr-datagrid/div/div[2] + Retry Wait Element Not Visible //clr-datagrid/div/div[2] Multi-delete Object projecta projectb # Verify delete project with image should not be deleted directly Delete Fail projecta${d} @@ -452,7 +452,6 @@ Test Case - Delete Multi Tag Create An New Project project${d} Push Image With Tag ${ip} user014 Test1@34 project${d} redis 3.2.10-alpine 3.2.10-alpine Push Image With Tag ${ip} user014 Test1@34 project${d} redis 4.0.7-alpine 4.0.7-alpine - Sleep 2 Go Into Project project${d} Go Into Repo redis Multi-delete object 3.2.10-alpine 4.0.7-alpine @@ -467,7 +466,6 @@ Test Case - Delete Repo on CardView Create An New Project project${d} Push Image ${ip} user015 Test1@34 project${d} hello-world Push Image ${ip} user015 Test1@34 project${d} busybox - Sleep 2 Go Into Project project${d} Switch To CardView Delete Repo on CardView busybox @@ -511,7 +509,6 @@ Test Case - Project Admin Add Labels To Repo Create An New Project project${d} Push Image With Tag ${ip} user020 Test1@34 project${d} redis 3.2.10-alpine 3.2.10-alpine Push Image With Tag ${ip} user020 Test1@34 project${d} redis 4.0.7-alpine 4.0.7-alpine - Go Into Project project${d} Sleep 2 # Add labels @@ -551,7 +548,6 @@ Test Case - Scan A Tag In The Repo Create An New Project project${d} Go Into Project project${d} has_image=${false} Push Image ${ip} user023 Test1@34 project${d} hello-world - Sleep 5 Go Into Project project${d} Go Into Repo project${d}/hello-world Scan Repo latest Succeed @@ -672,7 +668,7 @@ Test Case - Retag A Image Tag Sleep 1 Go Into Repo project${random_num1}/redis Retag Image ${image_tag} project${random_num2} ${target_image_name} ${target_tag_value} - Retry Keyword When Error Wait Until Element Is Not Visible element=${repo_retag_confirm_dlg} + Retry Wait Element Not Visible ${repo_retag_confirm_dlg} Navigate To Projects Go Into Project project${random_num2} Sleep 1 From 1a551690d3a9401f91b71d6aa86060f6678ef1a8 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Wed, 9 Jan 2019 16:08:56 +0800 Subject: [PATCH 32/45] promission reset Signed-off-by: Yogi_Wang --- .../lib/src/endpoint/endpoint.component.ts | 47 +-- src/portal/lib/src/harbor-library.module.ts | 19 +- .../src/helm-chart/helm-chart.component.html | 6 +- .../src/helm-chart/helm-chart.component.ts | 87 +++--- .../helm-chart-version.component.html | 10 +- .../versions/helm-chart-version.component.ts | 26 +- src/portal/lib/src/label/label.component.html | 6 +- src/portal/lib/src/label/label.component.ts | 43 +-- .../list-replication-rule.component.html | 8 +- .../list-replication-rule.component.ts | 44 +-- .../project-policy-config.component.html | 12 +- .../project-policy-config.component.spec.ts | 5 +- .../project-policy-config.component.ts | 13 +- .../replication/replication.component.html | 9 +- .../src/replication/replication.component.ts | 4 + .../repository-gridview.component.html | 4 +- .../repository-gridview.component.spec.ts | 19 +- .../repository-gridview.component.ts | 111 ++++--- .../src/repository/repository.component.html | 2 +- .../repository/repository.component.spec.ts | 2 + src/portal/lib/src/service/index.ts | 2 + src/portal/lib/src/service/interface.ts | 29 +- .../lib/src/service/permission-static.ts | 134 +++++++++ .../lib/src/service/permission.service.ts | 76 +++++ src/portal/lib/src/shared/shared.const.ts | 4 +- .../lib/src/tag/tag-detail.component.html | 6 +- .../lib/src/tag/tag-detail.component.spec.ts | 18 +- .../lib/src/tag/tag-detail.component.ts | 27 +- src/portal/lib/src/tag/tag.component.html | 6 +- src/portal/lib/src/tag/tag.component.spec.ts | 29 +- src/portal/lib/src/tag/tag.component.ts | 280 +++++++++--------- src/portal/lib/src/utils.ts | 8 + .../result-grid.component.html | 2 +- .../result-grid.component.spec.ts | 4 +- .../result-grid.component.ts | 22 +- .../result-tip.component.spec.ts | 4 +- src/portal/package-lock.json | 60 ++-- src/portal/package.json | 6 +- .../src/app/config/config.component.html | 5 +- .../list-chart-versions.component.html | 2 - .../list-chart-versions.component.ts | 4 - .../list-charts/list-charts.component.html | 1 - .../list-charts/list-charts.component.ts | 2 - .../add-member/add-member.component.html | 4 + .../app/project/member/member.component.html | 13 +- .../app/project/member/member.component.ts | 85 +++--- .../project-config.component.html | 2 +- .../project-config.component.ts | 2 - .../project-detail.component.html | 16 +- .../project-detail.component.ts | 68 ++++- .../project-label.component.html | 9 +- .../project-label/project-label.component.ts | 29 +- .../robot-account.component.html | 31 +- .../robot-account/robot-account.component.ts | 28 +- .../replication-page.component.html | 11 +- .../replication/replication-page.component.ts | 63 ++-- .../total-replication-page.component.html | 11 +- .../tag-detail/tag-detail-page.component.html | 10 +- .../tag-detail/tag-detail-page.component.ts | 4 - .../message-handler.service.ts | 4 +- .../route/member-guard-activate.service.ts | 2 +- .../route/sign-in-guard-activate.service.ts | 5 +- src/portal/src/app/shared/session.service.ts | 10 +- src/portal/src/app/shared/shared.const.ts | 9 +- src/portal/src/app/user/user.component.ts | 36 +-- src/portal/src/i18n/lang/en-us-lang.json | 1 + src/portal/src/i18n/lang/es-es-lang.json | 1 + src/portal/src/i18n/lang/fr-fr-lang.json | 1 + src/portal/src/i18n/lang/pt-br-lang.json | 1 + src/portal/src/i18n/lang/zh-cn-lang.json | 1 + .../Harbor-Pages/Project-Config.robot | 1 + tests/resources/Harbor-Pages/Project.robot | 11 +- .../Harbor-Pages/ToolKit_Elements.robot | 4 +- .../Harbor-Pages/Vulnerability.robot | 1 + 74 files changed, 1128 insertions(+), 554 deletions(-) create mode 100644 src/portal/lib/src/service/permission-static.ts create mode 100644 src/portal/lib/src/service/permission.service.ts diff --git a/src/portal/lib/src/endpoint/endpoint.component.ts b/src/portal/lib/src/endpoint/endpoint.component.ts index ad52138105..fd77076ff1 100644 --- a/src/portal/lib/src/endpoint/endpoint.component.ts +++ b/src/portal/lib/src/endpoint/endpoint.component.ts @@ -19,8 +19,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef } from "@angular/core"; -import { Subscription} from "rxjs"; -import {forkJoin} from "rxjs"; +import { Subscription } from "rxjs"; +import { forkJoin } from "rxjs"; import { TranslateService } from "@ngx-translate/core"; import { Comparator } from "../service/interface"; @@ -29,9 +29,9 @@ import { EndpointService } from "../service/endpoint.service"; import { ErrorHandler } from "../error-handler/index"; -import {ConfirmationMessage} from "../confirmation-dialog/confirmation-message"; -import {ConfirmationAcknowledgement} from "../confirmation-dialog/confirmation-state-message"; -import {ConfirmationDialogComponent} from "../confirmation-dialog/confirmation-dialog.component"; +import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message"; +import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message"; +import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component"; import { ConfirmationTargets, @@ -42,8 +42,9 @@ import { import { CreateEditEndpointComponent } from "../create-edit-endpoint/create-edit-endpoint.component"; import { toPromise, CustomComparator } from "../utils"; -import {operateChanges, OperateInfo, OperationState} from "../operation/operate"; -import {OperationService} from "../operation/operation.service"; +import { operateChanges, OperateInfo, OperationState } from "../operation/operate"; +import { OperationService } from "../operation/operation.service"; + @Component({ selector: "hbr-endpoint", @@ -86,10 +87,10 @@ export class EndpointComponent implements OnInit, OnDestroy { } constructor(private endpointService: EndpointService, - private errorHandler: ErrorHandler, - private translateService: TranslateService, - private operationService: OperationService, - private ref: ChangeDetectorRef) { + private errorHandler: ErrorHandler, + private translateService: TranslateService, + private operationService: OperationService, + private ref: ChangeDetectorRef) { this.forceRefreshView(1000); } @@ -208,18 +209,18 @@ export class EndpointComponent implements OnInit, OnDestroy { operateChanges(operMessage, OperationState.success); }); }).catch( - error => { - if (error && error.status === 412) { - forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), - this.translateService.get('DESTINATION.FAILED_TO_DELETE_TARGET_IN_USED')).subscribe(res => { - operateChanges(operMessage, OperationState.failure, res[1]); - }); - } else { - this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { - operateChanges(operMessage, OperationState.failure, res); - }); - } - }); + error => { + if (error && error.status === 412) { + forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), + this.translateService.get('DESTINATION.FAILED_TO_DELETE_TARGET_IN_USED')).subscribe(res => { + operateChanges(operMessage, OperationState.failure, res[1]); + }); + } else { + this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { + operateChanges(operMessage, OperationState.failure, res); + }); + } + }); } // Forcely refresh the view diff --git a/src/portal/lib/src/harbor-library.module.ts b/src/portal/lib/src/harbor-library.module.ts index 2c014e762b..2c7370f892 100644 --- a/src/portal/lib/src/harbor-library.module.ts +++ b/src/portal/lib/src/harbor-library.module.ts @@ -29,7 +29,6 @@ import { CREATE_EDIT_LABEL_DIRECTIVES } from "./create-edit-label/index"; import { LABEL_PIECE_DIRECTIVES } from "./label-piece/index"; import { HELMCHART_DIRECTIVE } from "./helm-chart/index"; import { IMAGE_NAME_INPUT_DIRECTIVES } from "./image-name-input/index"; - import { SystemInfoService, SystemInfoDefaultService, @@ -56,7 +55,9 @@ import { HelmChartService, HelmChartDefaultService, RetagService, - RetagDefaultService + RetagDefaultService, + UserPermissionService, + UserPermissionDefaultService } from './service/index'; import { ErrorHandler, @@ -68,7 +69,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { TranslateServiceInitializer } from './i18n/index'; import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils'; import { ChannelService } from './channel/index'; -import { OperationService } from './operation/operation.service'; +import { OperationService } from './operation/operation.service'; /** * Declare default service configuration; all the endpoints will be defined in @@ -151,6 +152,8 @@ export interface HarborModuleConfig { // Service implementation for helmchart helmChartService?: Provider; + // Service implementation for userPermission + userPermissionService?: Provider; } /** @@ -248,8 +251,9 @@ export class HarborLibraryModule { config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }, config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService }, config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService }, - config.labelService || {provide: LabelService, useClass: LabelDefaultService}, - config.helmChartService || {provide: HelmChartService, useClass: HelmChartDefaultService}, + config.labelService || { provide: LabelService, useClass: LabelDefaultService }, + config.helmChartService || { provide: HelmChartService, useClass: HelmChartDefaultService }, + config.userPermissionService || { provide: UserPermissionService, useClass: UserPermissionDefaultService }, // Do initializing TranslateServiceInitializer, { @@ -281,8 +285,9 @@ export class HarborLibraryModule { config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }, config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService }, config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService }, - config.labelService || {provide: LabelService, useClass: LabelDefaultService}, - config.helmChartService || {provide: HelmChartService, useClass: HelmChartDefaultService}, + config.labelService || { provide: LabelService, useClass: LabelDefaultService }, + config.helmChartService || { provide: HelmChartService, useClass: HelmChartDefaultService }, + config.userPermissionService || { provide: UserPermissionService, useClass: UserPermissionDefaultService }, ChannelService, OperationService ] diff --git a/src/portal/lib/src/helm-chart/helm-chart.component.html b/src/portal/lib/src/helm-chart/helm-chart.component.html index bade63a46e..a9d1b75c1e 100644 --- a/src/portal/lib/src/helm-chart/helm-chart.component.html +++ b/src/portal/lib/src/helm-chart/helm-chart.component.html @@ -23,14 +23,14 @@
- - - diff --git a/src/portal/lib/src/helm-chart/helm-chart.component.ts b/src/portal/lib/src/helm-chart/helm-chart.component.ts index f3d6832ecb..792eb93331 100644 --- a/src/portal/lib/src/helm-chart/helm-chart.component.ts +++ b/src/portal/lib/src/helm-chart/helm-chart.component.ts @@ -17,9 +17,11 @@ import { SystemInfo, SystemInfoService, HelmChartItem } from "../service/index"; import { ErrorHandler } from "../error-handler/error-handler"; import { toPromise, DEFAULT_PAGE_SIZE, downloadFile } from "../utils"; import { HelmChartService } from "../service/helm-chart.service"; -import { DefaultHelmIcon} from "../shared/shared.const"; +import { DefaultHelmIcon } from "../shared/shared.const"; import { Roles } from './../shared/shared.const'; import { OperationService } from "./../operation/operation.service"; +import { UserPermissionService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; import { OperateInfo, OperationState, @@ -45,7 +47,6 @@ export class HelmChartComponent implements OnInit { @Input() urlPrefix: string; @Input() hasSignedIn: boolean; @Input() projectRoleID = Roles.OTHER; - @Input() hasProjectAdminRole: boolean; @Output() chartClickEvt = new EventEmitter(); @Output() chartDownloadEve = new EventEmitter(); @Input() chartDefaultIcon: string = DefaultHelmIcon; @@ -76,24 +77,23 @@ export class HelmChartComponent implements OnInit { @ViewChild('chartUploadForm') uploadForm: NgForm; @ViewChild("confirmationDialog") confirmationDialog: ConfirmationDialogComponent; - + hasUploadHelmChartsPermission: boolean; + hasDownloadHelmChartsPermission: boolean; + hasDeleteHelmChartsPermission: boolean; constructor( private errorHandler: ErrorHandler, private translateService: TranslateService, private systemInfoService: SystemInfoService, private helmChartService: HelmChartService, + private userPermissionService: UserPermissionService, private operationService: OperationService, private cdr: ChangeDetectorRef, - ) {} + ) { } public get registryUrl(): string { return this.systemInfo ? this.systemInfo.registry_url : ""; } - public get developerRoleOrAbove(): boolean { - return this.projectRoleID === Roles.DEVELOPER || this.hasProjectAdminRole; - } - ngOnInit(): void { // Get system info for tag views toPromise(this.systemInfoService.getSystemInfo()) @@ -101,8 +101,21 @@ export class HelmChartComponent implements OnInit { .catch(error => this.errorHandler.error(error)); this.lastFilteredChartName = ""; this.refresh(); + this.getHelmPermissionRule(this.projectId); + } + getHelmPermissionRule(projectId: number): void { + let hasUploadHelmChartsPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.HELM_CHART.KEY, USERSTATICPERMISSION.HELM_CHART.VALUE.UPLOAD); + let hasDownloadHelmChartsPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.HELM_CHART.KEY, USERSTATICPERMISSION.HELM_CHART.VALUE.DOWNLOAD); + let hasDeleteHelmChartsPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.HELM_CHART.KEY, USERSTATICPERMISSION.HELM_CHART.VALUE.DELETE); + forkJoin(hasUploadHelmChartsPermission, hasDownloadHelmChartsPermission, hasDeleteHelmChartsPermission).subscribe(permissions => { + this.hasUploadHelmChartsPermission = permissions[0] as boolean; + this.hasDownloadHelmChartsPermission = permissions[1] as boolean; + this.hasDeleteHelmChartsPermission = permissions[2] as boolean; + }, error => this.errorHandler.error(error)); } - updateFilterValue(value: string) { this.lastFilteredChartName = value; this.refresh(); @@ -111,22 +124,22 @@ export class HelmChartComponent implements OnInit { refresh() { this.loading = true; this.helmChartService - .getHelmCharts(this.projectName) - .pipe(finalize(() => { + .getHelmCharts(this.projectName) + .pipe(finalize(() => { let hnd = setInterval(() => this.cdr.markForCheck(), 100); setTimeout(() => clearInterval(hnd), 3000); this.loading = false; - })) - .subscribe( - charts => { - this.charts = charts.filter(x => x.name.includes(this.lastFilteredChartName)); - this.chartsCopy = charts.map(x => Object.assign({}, x)); - this.totalCount = charts.length; - }, - err => { - this.errorHandler.error(err); - } - ); + })) + .subscribe( + charts => { + this.charts = charts.filter(x => x.name.includes(this.lastFilteredChartName)); + this.chartsCopy = charts.map(x => Object.assign({}, x)); + this.totalCount = charts.length; + }, + err => { + this.errorHandler.error(err); + } + ); } onChartClick(item: HelmChartItem) { @@ -163,10 +176,10 @@ export class HelmChartComponent implements OnInit { this.refresh(); })) .subscribe(() => { - this.translateService - .get("HELM_CHART.FILE_UPLOADED") - .subscribe(res => this.errorHandler.info(res)); - }, + this.translateService + .get("HELM_CHART.FILE_UPLOADED") + .subscribe(res => this.errorHandler.info(res)); + }, err => this.errorHandler.error(err) ); } @@ -192,23 +205,23 @@ export class HelmChartComponent implements OnInit { this.operationService.publishInfo(operateMsg); return this.helmChartService.deleteHelmChart(this.projectName, chartName) - .pipe(map( - () => operateChanges(operateMsg, OperationState.success), - err => operateChanges(operateMsg, OperationState.failure, err) - )); + .pipe(map( + () => operateChanges(operateMsg, OperationState.success), + err => operateChanges(operateMsg, OperationState.failure, err) + )); } deleteCharts(charts: HelmChartItem[]) { if (charts && charts.length < 1) { return; } let chartsDelete$ = charts.map(chart => this.deleteChart(chart.name)); forkJoin(chartsDelete$) - .pipe( - catchError(err => throwError(err)), - finalize(() => { - this.refresh(); - this.selectedRows = []; - })) - .subscribe(() => {}); + .pipe( + catchError(err => throwError(err)), + finalize(() => { + this.refresh(); + this.selectedRows = []; + })) + .subscribe(() => { }); } downloadLatestVersion(evt?: Event, item?: HelmChartItem) { diff --git a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.html b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.html index 1cd96ab41b..929cdc64e5 100644 --- a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.html +++ b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.html @@ -38,18 +38,18 @@ - @@ -144,7 +144,7 @@
diff --git a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.ts b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.ts index 276f5dbd6e..c92e724ca8 100644 --- a/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.ts +++ b/src/portal/lib/src/helm-chart/versions/helm-chart-version.component.ts @@ -26,6 +26,8 @@ import { ErrorHandler } from "./../../error-handler/error-handler"; import { toPromise, DEFAULT_PAGE_SIZE, downloadFile } from "../../utils"; import { OperationService } from "./../../operation/operation.service"; import { HelmChartService } from "./../../service/helm-chart.service"; +import { UserPermissionService } from "../../service/permission.service"; +import { USERSTATICPERMISSION } from "../../service/permission-static"; import { ConfirmationAcknowledgement, ConfirmationDialogComponent, ConfirmationMessage } from "./../../confirmation-dialog"; import { OperateInfo, @@ -49,13 +51,11 @@ import { }) export class ChartVersionComponent implements OnInit { signedCon: { [key: string]: any | string[] } = {}; - @Input() projectRoleID: number; @Input() projectId: number; @Input() projectName: string; @Input() chartName: string; @Input() roleName: string; @Input() hasSignedIn: boolean; - @Input() hasProjectAdminRole: boolean; @Input() chartDefaultIcon: string = DefaultHelmIcon; @Output() versionClickEvt = new EventEmitter(); @Output() backEvt = new EventEmitter(); @@ -85,12 +85,15 @@ export class ChartVersionComponent implements OnInit { @ViewChild("confirmationDialog") confirmationDialog: ConfirmationDialogComponent; - + hasAddRemoveHelmChartVersionPermission: boolean; + hasDownloadHelmChartVersionPermission: boolean; + hasDeleteHelmChartVersionPermission: boolean; constructor( private errorHandler: ErrorHandler, private systemInfoService: SystemInfoService, private helmChartService: HelmChartService, private resrouceLabelService: LabelService, + public userPermissionService: UserPermissionService, private cdr: ChangeDetectorRef, private operationService: OperationService, ) { } @@ -107,6 +110,7 @@ export class ChartVersionComponent implements OnInit { this.refresh(); this.getLabels(); this.lastFilteredVersionName = ""; + this.getHelmChartVersionPermission(this.projectId); } updateFilterValue(value: string) { @@ -326,7 +330,19 @@ export class ChartVersionComponent implements OnInit { }); } - public get developerRoleOrAbove(): boolean { - return this.projectRoleID === Roles.DEVELOPER || this.hasProjectAdminRole; + getHelmChartVersionPermission(projectId: number): void { + + let hasAddRemoveHelmChartVersionPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.HELM_CHART_VERSION_LABEL.KEY, USERSTATICPERMISSION.HELM_CHART_VERSION_LABEL.VALUE.CREATE); + let hasDownloadHelmChartVersionPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.HELM_CHART_VERSION.KEY, USERSTATICPERMISSION.HELM_CHART_VERSION.VALUE.READ); + let hasDeleteHelmChartVersionPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.HELM_CHART_VERSION.KEY, USERSTATICPERMISSION.HELM_CHART_VERSION.VALUE.DELETE); + forkJoin(hasAddRemoveHelmChartVersionPermission, hasDownloadHelmChartVersionPermission, hasDeleteHelmChartVersionPermission) + .subscribe(permissions => { + this.hasAddRemoveHelmChartVersionPermission = permissions[0] as boolean; + this.hasDownloadHelmChartVersionPermission = permissions[1] as boolean; + this.hasDeleteHelmChartVersionPermission = permissions[2] as boolean; + }, error => this.errorHandler.error(error)); } } diff --git a/src/portal/lib/src/label/label.component.html b/src/portal/lib/src/label/label.component.html index 7749f6bb28..717f9faf9c 100644 --- a/src/portal/lib/src/label/label.component.html +++ b/src/portal/lib/src/label/label.component.html @@ -11,9 +11,9 @@
- - - + + +
diff --git a/src/portal/lib/src/label/label.component.ts b/src/portal/lib/src/label/label.component.ts index 7a3f3c374c..1867440d61 100644 --- a/src/portal/lib/src/label/label.component.ts +++ b/src/portal/lib/src/label/label.component.ts @@ -19,22 +19,22 @@ import { ChangeDetectorRef, Input } from "@angular/core"; -import {Label} from "../service/interface"; -import {LabelService} from "../service/label.service"; -import {toPromise} from "../utils"; -import {ErrorHandler} from "../error-handler/error-handler"; -import {CreateEditLabelComponent} from "../create-edit-label/create-edit-label.component"; -import {ConfirmationMessage} from "../confirmation-dialog/confirmation-message"; +import { Label } from "../service/interface"; +import { LabelService } from "../service/label.service"; +import { toPromise } from "../utils"; +import { ErrorHandler } from "../error-handler/error-handler"; +import { CreateEditLabelComponent } from "../create-edit-label/create-edit-label.component"; +import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message"; import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const"; -import {ConfirmationAcknowledgement} from "../confirmation-dialog/confirmation-state-message"; -import {TranslateService} from "@ngx-translate/core"; -import {ConfirmationDialogComponent} from "../confirmation-dialog/confirmation-dialog.component"; -import {operateChanges, OperateInfo, OperationState} from "../operation/operate"; -import {OperationService} from "../operation/operation.service"; +import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message"; +import { TranslateService } from "@ngx-translate/core"; +import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component"; +import { operateChanges, OperateInfo, OperationState } from "../operation/operate"; +import { OperationService } from "../operation/operation.service"; @Component({ selector: "hbr-label", @@ -51,7 +51,9 @@ export class LabelComponent implements OnInit { @Input() scope: string; @Input() projectId = 0; - @Input() hasProjectAdminRole: boolean; + @Input() hasCreateLabelPermission: boolean; + @Input() hasUpdateLabelPermission: boolean; + @Input() hasDeleteLabelPermission: boolean; @ViewChild(CreateEditLabelComponent) createEditLabel: CreateEditLabelComponent; @@ -59,10 +61,10 @@ export class LabelComponent implements OnInit { confirmationDialogComponent: ConfirmationDialogComponent; constructor(private labelService: LabelService, - private errorHandler: ErrorHandler, - private translateService: TranslateService, - private operationService: OperationService, - private ref: ChangeDetectorRef) { + private errorHandler: ErrorHandler, + private translateService: TranslateService, + private operationService: OperationService, + private ref: ChangeDetectorRef) { } ngOnInit(): void { @@ -162,11 +164,11 @@ export class LabelComponent implements OnInit { operateChanges(operMessage, OperationState.success); }); }).catch( - error => { - this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { - operateChanges(operMessage, OperationState.failure, res); + error => { + this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { + operateChanges(operMessage, OperationState.failure, res); + }); }); - }); } // Forcely refresh the view @@ -183,4 +185,5 @@ export class LabelComponent implements OnInit { } }, duration); } + } diff --git a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html index 88d5a28cdc..83188bda56 100644 --- a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html +++ b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.html @@ -1,10 +1,10 @@
- - - - + + + + {{'REPLICATION.NAME' | translate}} {{'REPLICATION.STATUS' | translate}} diff --git a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts index 4107dc194c..f593ff25ad 100644 --- a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts +++ b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts @@ -24,28 +24,29 @@ import { SimpleChange, SimpleChanges } from "@angular/core"; -import { forkJoin} from "rxjs"; +import { forkJoin } from "rxjs"; import { Comparator } from "../service/interface"; import { TranslateService } from "@ngx-translate/core"; -import {ReplicationService} from "../service/replication.service"; +import { ReplicationService } from "../service/replication.service"; + import { ReplicationJob, ReplicationJobItem, ReplicationRule } from "../service/interface"; -import {ConfirmationDialogComponent} from "../confirmation-dialog/confirmation-dialog.component"; -import {ConfirmationMessage} from "../confirmation-dialog/confirmation-message"; -import {ConfirmationAcknowledgement} from "../confirmation-dialog/confirmation-state-message"; +import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component"; +import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message"; +import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message"; import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from "../shared/shared.const"; -import {ErrorHandler} from "../error-handler/error-handler"; -import {toPromise, CustomComparator} from "../utils"; -import {operateChanges, OperateInfo, OperationState} from "../operation/operate"; -import {OperationService} from "../operation/operation.service"; +import { ErrorHandler } from "../error-handler/error-handler"; +import { toPromise, CustomComparator } from "../utils"; +import { operateChanges, OperateInfo, OperationState } from "../operation/operate"; +import { OperationService } from "../operation/operation.service"; @Component({ @@ -58,12 +59,14 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { nullTime = "0001-01-01T00:00:00Z"; @Input() projectId: number; - @Input() isSystemAdmin: boolean; @Input() selectedId: number | string; @Input() withReplicationJob: boolean; @Input() loading = false; - + @Input() hasCreateReplicationPermission: boolean; + @Input() hasUpdateReplicationPermission: boolean; + @Input() hasDeleteReplicationPermission: boolean; + @Input() hasExecuteReplicationPermission: boolean; @Output() reload = new EventEmitter(); @Output() selectOne = new EventEmitter(); @Output() editOne = new EventEmitter(); @@ -92,10 +95,10 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { enabledComparator: Comparator = new CustomComparator("enabled", "number"); constructor(private replicationService: ReplicationService, - private translateService: TranslateService, - private errorHandler: ErrorHandler, - private operationService: OperationService, - private ref: ChangeDetectorRef) { + private translateService: TranslateService, + private errorHandler: ErrorHandler, + private operationService: OperationService, + private ref: ChangeDetectorRef) { setInterval(() => ref.markForCheck(), 500); } @@ -113,7 +116,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { this.retrieveRules(); } } - ngOnChanges(changes: SimpleChanges): void { let proIdChange: SimpleChange = changes["projectId"]; if (proIdChange) { @@ -156,7 +158,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { let count = 0; rule.filters.forEach((data: any) => { if (data.kind === 'label' && data.value.deleted) { - count ++; + count++; } }); if (count === 0) { @@ -258,8 +260,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { if (!this.canDeleteRule) { forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), this.translateService.get('REPLICATION.DELETION_SUMMARY_FAILURE')).subscribe(res => { - operateChanges(operMessage, OperationState.failure, res[1]); - }); + operateChanges(operMessage, OperationState.failure, res[1]); + }); return null; } @@ -273,8 +275,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { if (error && error.status === 412) { forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), this.translateService.get('REPLICATION.FAILED_TO_DELETE_POLICY_ENABLED')).subscribe(res => { - operateChanges(operMessage, OperationState.failure, res[1]); - }); + operateChanges(operMessage, OperationState.failure, res[1]); + }); } else { this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { operateChanges(operMessage, OperationState.failure, res); diff --git a/src/portal/lib/src/project-policy-config/project-policy-config.component.html b/src/portal/lib/src/project-policy-config/project-policy-config.component.html index c9aa2458f2..17263599f1 100644 --- a/src/portal/lib/src/project-policy-config/project-policy-config.component.html +++ b/src/portal/lib/src/project-policy-config/project-policy-config.component.html @@ -5,7 +5,7 @@
+ [disabled]="!hasChangeConfigRole" /> @@ -19,7 +19,7 @@
- + @@ -28,7 +28,7 @@
+ [disabled]="!hasChangeConfigRole" /> @@ -52,16 +52,16 @@
-
- - diff --git a/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts b/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts index 640fb69130..4983c9f134 100644 --- a/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts +++ b/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts @@ -8,7 +8,7 @@ import { ProjectService, ProjectDefaultService} from '../service/project.service import { SERVICE_CONFIG, IServiceConfig} from '../service.config'; import { SystemInfo } from '../service/interface'; import { Project } from './project'; - +import { UserPermissionService, UserPermissionDefaultService } from '../service/permission.service'; describe('ProjectPolicyConfigComponent', () => { let systemInfoService: SystemInfoService; @@ -102,7 +102,8 @@ describe('ProjectPolicyConfigComponent', () => { ErrorHandler, { provide: SERVICE_CONFIG, useValue: config }, { provide: ProjectService, useClass: ProjectDefaultService }, - { provide: SystemInfoService, useClass: SystemInfoDefaultService} + { provide: SystemInfoService, useClass: SystemInfoDefaultService}, + { provide: UserPermissionService, useClass: UserPermissionDefaultService}, ] }) .compileComponents(); diff --git a/src/portal/lib/src/project-policy-config/project-policy-config.component.ts b/src/portal/lib/src/project-policy-config/project-policy-config.component.ts index e97f4429b8..650e9b5d3d 100644 --- a/src/portal/lib/src/project-policy-config/project-policy-config.component.ts +++ b/src/portal/lib/src/project-policy-config/project-policy-config.component.ts @@ -13,6 +13,8 @@ import { TranslateService } from '@ngx-translate/core'; import { Project } from './project'; import {SystemInfo, SystemInfoService} from '../service/index'; +import { UserPermissionService } from '../service/permission.service'; +import { USERSTATICPERMISSION } from '../service/permission-static'; export class ProjectPolicy { Public: boolean; @@ -56,7 +58,7 @@ export class ProjectPolicyConfigComponent implements OnInit { systemInfo: SystemInfo; orgProjectPolicy = new ProjectPolicy(); projectPolicy = new ProjectPolicy(); - + hasChangeConfigRole: boolean; severityOptions = [ {severity: 'high', severityLevel: 'VULNERABILITY.SEVERITY.HIGH'}, {severity: 'medium', severityLevel: 'VULNERABILITY.SEVERITY.MEDIUM'}, @@ -69,6 +71,7 @@ export class ProjectPolicyConfigComponent implements OnInit { private translate: TranslateService, private projectService: ProjectService, private systemInfoService: SystemInfoService, + private userPermission: UserPermissionService ) {} ngOnInit(): void { @@ -85,8 +88,14 @@ export class ProjectPolicyConfigComponent implements OnInit { // retrive project level policy data this.retrieve(); + this.getPermission(); + } + private getPermission(): void { + this.userPermission.getPermission(this.projectId, + USERSTATICPERMISSION.CONFIGURATION.KEY, USERSTATICPERMISSION.CONFIGURATION.VALUE.UPDATE).subscribe(permissins => { + this.hasChangeConfigRole = permissins as boolean; + }); } - public get withNotary(): boolean { return this.systemInfo ? this.systemInfo.with_notary : false; } diff --git a/src/portal/lib/src/replication/replication.component.html b/src/portal/lib/src/replication/replication.component.html index 512d0b864c..f4d9a2a6c2 100644 --- a/src/portal/lib/src/replication/replication.component.html +++ b/src/portal/lib/src/replication/replication.component.html @@ -11,9 +11,14 @@
- + (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)" + [hasCreateReplicationPermission]="hasCreateReplicationPermission" + [hasUpdateReplicationPermission]="hasUpdateReplicationPermission" + [hasDeleteReplicationPermission]="hasDeleteReplicationPermission" + [hasExecuteReplicationPermission]="hasExecuteReplicationPermission" + >
diff --git a/src/portal/lib/src/replication/replication.component.ts b/src/portal/lib/src/replication/replication.component.ts index d16bc80c32..ece3c95da6 100644 --- a/src/portal/lib/src/replication/replication.component.ts +++ b/src/portal/lib/src/replication/replication.component.ts @@ -104,6 +104,10 @@ export class ReplicationComponent implements OnInit, OnDestroy { @Input() isSystemAdmin: boolean; @Input() withAdmiral: boolean; @Input() withReplicationJob: boolean; + @Input() hasCreateReplicationPermission: boolean; + @Input() hasUpdateReplicationPermission: boolean; + @Input() hasDeleteReplicationPermission: boolean; + @Input() hasExecuteReplicationPermission: boolean; @Output() redirect = new EventEmitter(); @Output() openCreateRule = new EventEmitter(); diff --git a/src/portal/lib/src/repository-gridview/repository-gridview.component.html b/src/portal/lib/src/repository-gridview/repository-gridview.component.html index 6fd137c12c..fc1fecde0f 100644 --- a/src/portal/lib/src/repository-gridview/repository-gridview.component.html +++ b/src/portal/lib/src/repository-gridview/repository-gridview.component.html @@ -7,7 +7,7 @@ {{'CONFIG.REGISTRY_CERTIFICATE' | translate | uppercase}} - + @@ -27,7 +27,7 @@ - + {{'REPOSITORY.NAME' | translate}} {{'REPOSITORY.TAGS_COUNT' | translate}} diff --git a/src/portal/lib/src/repository-gridview/repository-gridview.component.spec.ts b/src/portal/lib/src/repository-gridview/repository-gridview.component.spec.ts index e1fa5c128b..8f0d068581 100644 --- a/src/portal/lib/src/repository-gridview/repository-gridview.component.spec.ts +++ b/src/portal/lib/src/repository-gridview/repository-gridview.component.spec.ts @@ -24,13 +24,16 @@ import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index'; import { LabelPieceComponent } from "../label-piece/label-piece.component"; import { OperationService } from "../operation/operation.service"; import {ProjectDefaultService, ProjectService, RetagDefaultService, RetagService} from "../service"; - +import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; +import { of } from "rxjs"; describe('RepositoryComponentGridview (inline template)', () => { let compRepo: RepositoryGridviewComponent; let fixtureRepo: ComponentFixture; let repositoryService: RepositoryService; let systemInfoService: SystemInfoService; + let userPermissionService: UserPermissionService; let spyRepos: jasmine.Spy; let spySystemInfo: jasmine.Spy; @@ -72,7 +75,8 @@ describe('RepositoryComponentGridview (inline template)', () => { metadata: {xTotalCount: 2}, data: mockRepoData }; - + let mockHasCreateRepositoryPermission: boolean = true; + let mockHasDeleteRepositoryPermission: boolean = true; // let mockTagData: Tag[] = [ // { // "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", @@ -120,6 +124,7 @@ describe('RepositoryComponentGridview (inline template)', () => { { provide: ProjectService, useClass: ProjectDefaultService }, { provide: RetagService, useClass: RetagDefaultService }, { provide: SystemInfoService, useClass: SystemInfoDefaultService }, + { provide: UserPermissionService, useClass: UserPermissionDefaultService }, { provide: OperationService } ] }); @@ -136,9 +141,17 @@ describe('RepositoryComponentGridview (inline template)', () => { spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo)); spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo)); + + + userPermissionService = fixtureRepo.debugElement.injector.get(UserPermissionService); + spyOn(userPermissionService, "getPermission") + .withArgs(compRepo.projectId, + USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.CREATE ) + .and.returnValue(of(mockHasCreateRepositoryPermission)) + .withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.DELETE ) + .and.returnValue(of(mockHasDeleteRepositoryPermission)); fixtureRepo.detectChanges(); }); - it('should create', () => { expect(compRepo).toBeTruthy(); }); diff --git a/src/portal/lib/src/repository-gridview/repository-gridview.component.ts b/src/portal/lib/src/repository-gridview/repository-gridview.component.ts index 904d6c8c1d..baf7f9fce0 100644 --- a/src/portal/lib/src/repository-gridview/repository-gridview.component.ts +++ b/src/portal/lib/src/repository-gridview/repository-gridview.component.ts @@ -14,8 +14,8 @@ import { import { Router } from "@angular/router"; import { forkJoin } from "rxjs"; import { finalize } from "rxjs/operators"; -import {TranslateService} from "@ngx-translate/core"; -import {Comparator, State} from "../service/interface"; +import { TranslateService } from "@ngx-translate/core"; +import { Comparator, State } from "../service/interface"; import { Repository, @@ -26,17 +26,19 @@ import { RepositoryItem, TagService } from '../service/index'; -import {ErrorHandler} from '../error-handler/error-handler'; -import {toPromise, CustomComparator, DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting, clone} from '../utils'; -import {ConfirmationState, ConfirmationTargets, ConfirmationButtons} from '../shared/shared.const'; -import {ConfirmationDialogComponent} from '../confirmation-dialog/confirmation-dialog.component'; -import {ConfirmationMessage} from '../confirmation-dialog/confirmation-message'; -import {ConfirmationAcknowledgement} from '../confirmation-dialog/confirmation-state-message'; -import {Tag} from '../service/interface'; -import {GridViewComponent} from '../gridview/grid-view.component'; -import {OperationService} from "../operation/operation.service"; -import {OperateInfo, OperationState, operateChanges} from "../operation/operate"; -import {SERVICE_CONFIG, IServiceConfig, downloadUrl } from '../service.config'; +import { ErrorHandler } from '../error-handler/error-handler'; +import { toPromise, CustomComparator, DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting, clone } from '../utils'; +import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const'; +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; +import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; +import { Tag } from '../service/interface'; +import { GridViewComponent } from '../gridview/grid-view.component'; +import { OperationService } from "../operation/operation.service"; +import { UserPermissionService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; +import { OperateInfo, OperationState, operateChanges } from "../operation/operate"; +import { SERVICE_CONFIG, IServiceConfig, downloadUrl } from '../service.config'; @Component({ selector: "hbr-repository-gridview", templateUrl: "./repository-gridview.component.html", @@ -80,19 +82,21 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { confirmationDialog: ConfirmationDialogComponent; @ViewChild("gridView") gridView: GridViewComponent; - + hasCreateRepositoryPermission: boolean; + hasDeleteRepositoryPermission: boolean; constructor(@Inject(SERVICE_CONFIG) private configInfo: IServiceConfig, - private errorHandler: ErrorHandler, - private translateService: TranslateService, - private repositoryService: RepositoryService, - private systemInfoService: SystemInfoService, - private tagService: TagService, - private operationService: OperationService, - private ref: ChangeDetectorRef, - private router: Router) { - if (this.configInfo && this.configInfo.systemInfoEndpoint) { - this.downloadLink = this.configInfo.systemInfoEndpoint + "/getcert"; - } + private errorHandler: ErrorHandler, + private translateService: TranslateService, + private repositoryService: RepositoryService, + private systemInfoService: SystemInfoService, + private tagService: TagService, + private operationService: OperationService, + public userPermissionService: UserPermissionService, + private ref: ChangeDetectorRef, + private router: Router) { + if (this.configInfo && this.configInfo.systemInfoEndpoint) { + this.downloadLink = this.configInfo.systemInfoEndpoint + "/getcert"; + } } public get registryUrl(): string { @@ -142,6 +146,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { } this.lastFilteredRepoName = ""; + this.getHelmChartVersionPermission(this.projectId); } confirmDeletion(message: ConfirmationAcknowledgement) { @@ -182,8 +187,8 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { if (this.signedCon[repo.name].length !== 0) { forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED')).subscribe(res => { - operateChanges(operMessage, OperationState.failure, res[1]); - }); + operateChanges(operMessage, OperationState.failure, res[1]); + }); } else { return toPromise(this.repositoryService .deleteRepository(repo.name)) @@ -193,24 +198,24 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { operateChanges(operMessage, OperationState.success); }); }).catch(error => { - if (error.status === "412") { - forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), - this.translateService.get('REPOSITORY.TAGS_SIGNED')).subscribe(res => { - operateChanges(operMessage, OperationState.failure, res[1]); + if (error.status === "412") { + forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), + this.translateService.get('REPOSITORY.TAGS_SIGNED')).subscribe(res => { + operateChanges(operMessage, OperationState.failure, res[1]); + }); + return; + } + if (error.status === 503) { + forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), + this.translateService.get('REPOSITORY.TAGS_NO_DELETE')).subscribe(res => { + operateChanges(operMessage, OperationState.failure, res[1]); + }); + return; + } + this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { + operateChanges(operMessage, OperationState.failure, res); }); - return; - } - if (error.status === 503) { - forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), - this.translateService.get('REPOSITORY.TAGS_NO_DELETE')).subscribe(res => { - operateChanges(operMessage, OperationState.failure, res[1]); - }); - return; - } - this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { - operateChanges(operMessage, OperationState.failure, res); }); - }); } } @@ -219,7 +224,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { this.currentPage = 1; let st: State = this.currentState; if (!st) { - st = {page: {}}; + st = { page: {} }; } st.page.size = this.pageSize; st.page.from = 0; @@ -299,8 +304,8 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { return toPromise(this.tagService.getTags(repo.name)) .then(items => { if (items.some((t: Tag) => { - return t.name === 'latest'; - })) { + return t.name === 'latest'; + })) { return true; } else { return false; @@ -449,7 +454,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { let st: State = this.currentState; if (!st) { - st = {page: {}}; + st = { page: {} }; } st.page.size = this.pageSize; st.page.from = (targetPageNumber - 1) * this.pageSize; @@ -497,4 +502,16 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { return this.listHover; } } + + getHelmChartVersionPermission(projectId: number): void { + + let hasCreateRepositoryPermission = this.userPermissionService.getPermission(this.projectId, + USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.CREATE); + let hasDeleteRepositoryPermission = this.userPermissionService.getPermission(this.projectId, + USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.DELETE); + forkJoin(hasCreateRepositoryPermission, hasDeleteRepositoryPermission).subscribe(permissions => { + this.hasCreateRepositoryPermission = permissions[0] as boolean; + this.hasDeleteRepositoryPermission = permissions[1] as boolean; + }, error => this.errorHandler.error(error)); + } } diff --git a/src/portal/lib/src/repository/repository.component.html b/src/portal/lib/src/repository/repository.component.html index 207ea6d698..a5216ed331 100644 --- a/src/portal/lib/src/repository/repository.component.html +++ b/src/portal/lib/src/repository/repository.component.html @@ -55,7 +55,7 @@
diff --git a/src/portal/lib/src/repository/repository.component.spec.ts b/src/portal/lib/src/repository/repository.component.spec.ts index 58ff2b9326..7b90f93875 100644 --- a/src/portal/lib/src/repository/repository.component.spec.ts +++ b/src/portal/lib/src/repository/repository.component.spec.ts @@ -27,6 +27,7 @@ import { LabelPieceComponent } from "../label-piece/label-piece.component"; import { LabelDefaultService, LabelService } from "../service/label.service"; import { OperationService } from "../operation/operation.service"; import { ProjectDefaultService, ProjectService, RetagDefaultService, RetagService } from "../service"; +import { UserPermissionDefaultService, UserPermissionService } from "../service/permission.service"; class RouterStub { @@ -178,6 +179,7 @@ describe('RepositoryComponent (inline template)', () => { { provide: ProjectService, useClass: ProjectDefaultService }, { provide: RetagService, useClass: RetagDefaultService }, { provide: LabelService, useClass: LabelDefaultService}, + { provide: UserPermissionService, useClass: UserPermissionDefaultService}, { provide: ChannelService}, { provide: OperationService } ] diff --git a/src/portal/lib/src/service/index.ts b/src/portal/lib/src/service/index.ts index afc38321a9..1772c00546 100644 --- a/src/portal/lib/src/service/index.ts +++ b/src/portal/lib/src/service/index.ts @@ -13,3 +13,5 @@ export * from "./project.service"; export * from "./label.service"; export * from "./helm-chart.service"; export * from "./retag.service"; +export * from "./permission.service"; +export * from "./permission-static"; diff --git a/src/portal/lib/src/service/interface.ts b/src/portal/lib/src/service/interface.ts index 0b07259d27..7bd1c8993c 100644 --- a/src/portal/lib/src/service/interface.ts +++ b/src/portal/lib/src/service/interface.ts @@ -119,8 +119,8 @@ export class Trigger { schedule_param: | any | { - [key: string]: any | any[]; - }; + [key: string]: any | any[]; + }; constructor(kind: string, param: any | { [key: string]: any | any[] }) { this.kind = kind; this.schedule_param = param; @@ -395,8 +395,8 @@ export interface HelmChartSignature { * interface Manifest */ export interface Manifest { - manifset: Object; - config: string; + manifset: Object; + config: string; } export interface RetagRequest { @@ -426,10 +426,23 @@ export interface ClrDatagridFilterInterface { } /** @deprecated since 0.11 */ -export interface Comparator extends ClrDatagridComparatorInterface {} +export interface Comparator extends ClrDatagridComparatorInterface { } /** @deprecated since 0.11 */ -export interface ClrFilter extends ClrDatagridFilterInterface {} +export interface ClrFilter extends ClrDatagridFilterInterface { } /** @deprecated since 0.11 */ -export interface State extends ClrDatagridStateInterface {} -export interface Modal extends ClrModal {} +export interface State extends ClrDatagridStateInterface { } +export interface Modal extends ClrModal { } export const Modal = ClrModal; + +/** + * The access user privilege from serve. + * + ** + * interface UserPrivilegeServe + */ +export interface UserPrivilegeServeItem { + [key: string]: any | any[]; + resource: string; + action: string; +} + diff --git a/src/portal/lib/src/service/permission-static.ts b/src/portal/lib/src/service/permission-static.ts new file mode 100644 index 0000000000..da0aaf62c5 --- /dev/null +++ b/src/portal/lib/src/service/permission-static.ts @@ -0,0 +1,134 @@ +export const USERSTATICPERMISSION = { + "PROJECT": { + 'KEY': 'project', + 'VALUE': { + "DELETE": "delete" + } + }, + "MEMBER": { + 'KEY': 'member', + 'VALUE': { + "CREATE": "create", + "UPDATE": "update", + "DELETE": "delete", + "LIST": "list" + } + }, + "LOG": { + 'KEY': 'log', + 'VALUE': { + "LIST": "list" + } + }, + "REPLICATION": { + 'KEY': 'replication', + 'VALUE': { + "CREATE": "create", + "UPDATE": "update", + "DELETE": "delete", + "LIST": "list", + } + }, + "REPLICATION_JOB": { + 'KEY': 'replication-job', + 'VALUE': { + "CREATE": "create", + } + }, + "LABEL": { + 'KEY': 'label', + 'VALUE': { + "CREATE": "create", + "UPDATE": "update", + "DELETE": "delete", + "LIST": "list", + } + }, + "CONFIGURATION": { + 'KEY': 'configuration', + 'VALUE': { + "UPDATE": "update", + "READ": "read", + } + }, + "REPOSITORY": { + 'KEY': 'repository', + 'VALUE': { + "CREATE": "create", + "UPDATE": "update", + "DELETE": "delete", + "LIST": "list", + "PUSH": "push", + "PULL": "pull", + } + }, + "REPOSITORY_TAG": { + 'KEY': 'repository-tag', + 'VALUE': { + "DELETE": "delete", + "LIST": "list", + } + }, + "REPOSITORY_TAG_SCAN_JOB": { + 'KEY': 'repository-tag-scan-job', + 'VALUE': { + "CREATE": "create", + "READ": "read", + "LIST": "list", + } + }, + "REPOSITORY_TAG_VULNERABILITY": { + 'KEY': 'repository-tag-vulnerability', + 'VALUE': { + "LIST": "list", + } + }, + "REPOSITORY_TAG_LABEL": { + 'KEY': 'repository-tag-label', + 'VALUE': { + "CREATE": "create", + "DELETE": "delete", + } + }, + "REPOSITORY_TAG_MANIFEST": { + 'KEY': 'repository-tag-manifest', + 'VALUE': { + "READ": "read", + } + }, + "HELM_CHART": { + 'KEY': 'helm-chart', + 'VALUE': { + "UPLOAD": "create", + "DOWNLOAD": "read", + "DELETE": "delete", + "LIST": "list", + } + }, + "HELM_CHART_VERSION": { + 'KEY': 'helm-chart-version', + 'VALUE': { + "DELETE": "delete", + "LIST": "list", + "READ": "read", + } + }, + "HELM_CHART_VERSION_LABEL": { + 'KEY': 'helm-chart-version-label', + 'VALUE': { + "CREATE": "create", + "DELETE": "delete", + } + }, + "ROBOT": { + 'KEY': 'robot', + 'VALUE': { + "CREATE": "create", + "UPDATE": "update", + "DELETE": "delete", + "LIST": "list", + "READ": "read", + } + }, +}; + diff --git a/src/portal/lib/src/service/permission.service.ts b/src/portal/lib/src/service/permission.service.ts new file mode 100644 index 0000000000..0130c20af7 --- /dev/null +++ b/src/portal/lib/src/service/permission.service.ts @@ -0,0 +1,76 @@ +// 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. + +import { Injectable } from '@angular/core'; +import { Observable, throwError as observableThrowError } from "rxjs"; +import { map, catchError, shareReplay } from "rxjs/operators"; +import { UserPrivilegeServeItem } from './interface'; +import { HttpClient } from '@angular/common/http'; + + + +const CACHE_SIZE = 1; +/** + * Get System privilege about current backend server. + * @abstract + * class UserPermissionService + */ + +export abstract class UserPermissionService { + /** + * Get user privilege information. + * @abstract + * returns + */ + abstract getPermission(projectId, resource, action); + abstract clearPermissionCache(); +} + +@Injectable() +export class UserPermissionDefaultService extends UserPermissionService { + constructor( + private http: HttpClient, + ) { + super(); + } + private permissionCache: Observable; + private getPermissionFromBackend(projectId): Observable { + const userPermissionUrl = `/api/users/current/permissions?scope=/project/${projectId}&relative=true`; + return this.http.get(userPermissionUrl); + } + private processingPermissionResult(responsePermission, resource, action): boolean { + const permissionList = responsePermission as UserPrivilegeServeItem[]; + for (const privilegeItem of permissionList) { + if (privilegeItem.resource === resource && privilegeItem.action === action) { + return true; + } + } + return false; + } + public getPermission(projectId, resource, action): Observable { + + if (!this.permissionCache) { + this.permissionCache = this.getPermissionFromBackend(projectId).pipe( + shareReplay(CACHE_SIZE)); + } + return this.permissionCache.pipe(map(response => { + return this.processingPermissionResult(response, resource, action); + })) + .pipe(catchError(error => observableThrowError(error) + )); + } + public clearPermissionCache() { + this.permissionCache = null; + } +} diff --git a/src/portal/lib/src/shared/shared.const.ts b/src/portal/lib/src/shared/shared.const.ts index 3c6c83fee0..f180073ad2 100644 --- a/src/portal/lib/src/shared/shared.const.ts +++ b/src/portal/lib/src/shared/shared.const.ts @@ -91,12 +91,14 @@ export const LabelColor = [ { 'color': '#F57600', 'textColor': 'black' }, { 'color': '#FFDC0B', 'textColor': 'black' }, ]; -export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' }; +export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', 'master': 'MEMBER.PROJECT_MASTER', +'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' }; export const DefaultHelmIcon = '/images/helm-gray.svg'; export enum Roles { PROJECT_ADMIN = 1, + PROJECT_MASTER = 4, DEVELOPER = 2, GUEST = 3, OTHER = 0, diff --git a/src/portal/lib/src/tag/tag-detail.component.html b/src/portal/lib/src/tag/tag-detail.component.html index 70929ddd8e..9fb78661a9 100644 --- a/src/portal/lib/src/tag/tag-detail.component.html +++ b/src/portal/lib/src/tag/tag-detail.component.html @@ -91,15 +91,15 @@ - + - + - {{ 'REPOSITORY.BUILD_HISTORY' | diff --git a/src/portal/lib/src/tag/tag-detail.component.spec.ts b/src/portal/lib/src/tag/tag-detail.component.spec.ts index 17c59bef1e..64a95b26bf 100644 --- a/src/portal/lib/src/tag/tag-detail.component.spec.ts +++ b/src/portal/lib/src/tag/tag-detail.component.spec.ts @@ -25,15 +25,19 @@ import { VULNERABILITY_SCAN_STATUS } from "../utils"; import { VULNERABILITY_DIRECTIVES } from "../vulnerability-scanning/index"; import { LabelPieceComponent } from "../label-piece/label-piece.component"; import { ChannelService } from "../channel/channel.service"; +import { of } from "rxjs"; import { JobLogService, JobLogDefaultService } from "../service/job-log.service"; +import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; describe("TagDetailComponent (inline template)", () => { let comp: TagDetailComponent; let fixture: ComponentFixture; let tagService: TagService; + let userPermissionService: UserPermissionService; let scanningService: ScanningResultService; let spy: jasmine.Spy; let vulSpy: jasmine.Spy; @@ -83,13 +87,13 @@ describe("TagDetailComponent (inline template)", () => { let config: IServiceConfig = { repositoryBaseEndpoint: "/api/repositories/testing" }; - + let mockHasVulnerabilitiesListPermission: boolean = false; + let mockHasBuildHistoryPermission: boolean = true; let mockManifest: Manifest = { manifset: {}, // tslint:disable-next-line:max-line-length config: `{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"],"ArgsEscaped":true,"Image":"sha256:fbef17698ac8605733924d5662f0cbfc0b27a51e83ab7d7a4b8d8a9a9fe0d1c2","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"30e1a2427aa2325727a092488d304505780501585a6ccf5a6a53c4d83a826101","container_config":{"Hostname":"30e1a2427aa2","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\\"/bin/sh\\"]"],"ArgsEscaped":true,"Image":"sha256:fbef17698ac8605733924d5662f0cbfc0b27a51e83ab7d7a4b8d8a9a9fe0d1c2","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2018-01-09T21:10:58.579708634Z","docker_version":"17.06.2-ce","history":[{"created":"2018-01-09T21:10:58.365737589Z","created_by":"/bin/sh -c #(nop) ADD file:093f0723fa46f6cdbd6f7bd146448bb70ecce54254c35701feeceb956414622f in / "},{"created":"2018-01-09T21:10:58.579708634Z","created_by":"/bin/sh -c #(nop) CMD [\\"/bin/sh\\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:cd7100a72410606589a54b932cabd804a17f9ae5b42a1882bd56d263e02b6215"]}}` }; - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [SharedModule], @@ -108,6 +112,7 @@ describe("TagDetailComponent (inline template)", () => { { provide: JobLogService, useClass: JobLogDefaultService }, { provide: SERVICE_CONFIG, useValue: config }, { provide: TagService, useClass: TagDefaultService }, + { provide: UserPermissionService, useClass: UserPermissionDefaultService }, { provide: ScanningResultService, useClass: ScanningResultDefaultService @@ -122,6 +127,8 @@ describe("TagDetailComponent (inline template)", () => { comp.tagId = "mock_tag"; comp.repositoryId = "mock_repo"; + comp.projectId = 1; + tagService = fixture.debugElement.injector.get(TagService); spy = spyOn(tagService, "getTag").and.returnValues( @@ -153,7 +160,14 @@ describe("TagDetailComponent (inline template)", () => { manifestSpy = spyOn(tagService, "getManifest").and.returnValues( Promise.resolve(mockManifest) ); + userPermissionService = fixture.debugElement.injector.get(UserPermissionService); + spyOn(userPermissionService, "getPermission") + .withArgs(comp.projectId, + USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.VALUE.LIST ) + .and.returnValue(of(mockHasVulnerabilitiesListPermission)) + .withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.VALUE.READ ) + .and.returnValue(of(mockHasBuildHistoryPermission)); fixture.detectChanges(); }); diff --git a/src/portal/lib/src/tag/tag-detail.component.ts b/src/portal/lib/src/tag/tag-detail.component.ts index af92d942ab..79e001722d 100644 --- a/src/portal/lib/src/tag/tag-detail.component.ts +++ b/src/portal/lib/src/tag/tag-detail.component.ts @@ -4,6 +4,9 @@ import { TagService, Tag, VulnerabilitySeverity } from "../service/index"; import { toPromise } from "../utils"; import { ErrorHandler } from "../error-handler/index"; import { Label } from "../service/interface"; +import { forkJoin } from "rxjs"; +import { UserPermissionService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; const TabLinkContentMap: { [index: string]: string } = { "tag-history": "history", @@ -32,8 +35,6 @@ export class TagDetailComponent implements OnInit { withAdmiral: boolean; @Input() withClair: boolean; - @Input() - withAdminRole: boolean; tagDetails: Tag = { name: "--", size: "--", @@ -51,11 +52,14 @@ export class TagDetailComponent implements OnInit { backEvt: EventEmitter = new EventEmitter(); currentTabID = "tag-vulnerability"; - + hasVulnerabilitiesListPermission: boolean; + hasBuildHistoryPermission: boolean; + @Input() projectId: number; constructor( private tagService: TagService, - private errorHandler: ErrorHandler - ) {} + private errorHandler: ErrorHandler, + private userPermissionService: UserPermissionService, + ) { } ngOnInit(): void { if (this.repositoryId && this.tagId) { @@ -90,6 +94,7 @@ export class TagDetailComponent implements OnInit { }) .catch(error => this.errorHandler.error(error)); } + this.getTagPermissions(this.projectId); } onBack(): void { @@ -173,4 +178,16 @@ export class TagDetailComponent implements OnInit { tabLinkClick(tabID: string) { this.currentTabID = tabID; } + + getTagPermissions(projectId: number): void { + + const hasVulnerabilitiesListPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.VALUE.LIST); + const hasBuildHistoryPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.VALUE.READ); + forkJoin(hasVulnerabilitiesListPermission, hasBuildHistoryPermission).subscribe(permissions => { + this.hasVulnerabilitiesListPermission = permissions[0] as boolean; + this.hasBuildHistoryPermission = permissions[1] as boolean; + }, error => this.errorHandler.error(error)); + } } diff --git a/src/portal/lib/src/tag/tag.component.html b/src/portal/lib/src/tag/tag.component.html index 10a450d548..dd0f462a9c 100644 --- a/src/portal/lib/src/tag/tag.component.html +++ b/src/portal/lib/src/tag/tag.component.html @@ -60,7 +60,7 @@ - +
@@ -75,8 +75,8 @@
- - + + {{'REPOSITORY.TAG' | translate}} {{'REPOSITORY.SIZE' | translate}} diff --git a/src/portal/lib/src/tag/tag.component.spec.ts b/src/portal/lib/src/tag/tag.component.spec.ts index 0e9429b41f..22811acc34 100644 --- a/src/portal/lib/src/tag/tag.component.spec.ts +++ b/src/portal/lib/src/tag/tag.component.spec.ts @@ -20,16 +20,21 @@ import { ChannelService } from "../channel/index"; import { CopyInputComponent } from "../push-image/copy-input.component"; import { LabelPieceComponent } from "../label-piece/label-piece.component"; import { LabelDefaultService, LabelService } from "../service/label.service"; +import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; import { OperationService } from "../operation/operation.service"; +import { Observable, of } from "rxjs"; describe("TagComponent (inline template)", () => { let comp: TagComponent; let fixture: ComponentFixture; let tagService: TagService; + let userPermissionService: UserPermissionService; let spy: jasmine.Spy; let spyLabels: jasmine.Spy; let spyLabels1: jasmine.Spy; + let mockTags: Tag[] = [ { "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", @@ -95,7 +100,10 @@ describe("TagComponent (inline template)", () => { let config: IServiceConfig = { repositoryBaseEndpoint: "/api/repositories/testing" }; - + let mockHasAddLabelImagePermission: boolean = true; + let mockHasRetagImagePermission: boolean = true; + let mockHasDeleteImagePermission: boolean = true; + let mockHasScanImagePermission: boolean = true; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -119,6 +127,7 @@ describe("TagComponent (inline template)", () => { { provide: RetagService, useClass: RetagDefaultService }, { provide: ScanningResultService, useClass: ScanningResultDefaultService }, { provide: LabelService, useClass: LabelDefaultService }, + { provide: UserPermissionService, useClass: UserPermissionDefaultService }, { provide: OperationService } ] }); @@ -130,10 +139,12 @@ describe("TagComponent (inline template)", () => { comp.projectId = 1; comp.repoName = "library/nginx"; - comp.hasProjectAdminRole = true; + comp.hasDeleteImagePermission = true; + comp.hasScanImagePermission = true; comp.hasSignedIn = true; comp.registryUrl = "http://registry.testing.com"; comp.withNotary = false; + comp.withAdmiral = false; let labelService: LabelService; @@ -141,6 +152,17 @@ describe("TagComponent (inline template)", () => { tagService = fixture.debugElement.injector.get(TagService); spy = spyOn(tagService, "getTags").and.returnValues(Promise.resolve(mockTags)); + userPermissionService = fixture.debugElement.injector.get(UserPermissionService); + + spyOn(userPermissionService, "getPermission") + .withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE ) + .and.returnValue(of(mockHasAddLabelImagePermission)) + .withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL ) + .and.returnValue(of(mockHasRetagImagePermission)) + .withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE ) + .and.returnValue(of(mockHasDeleteImagePermission)) + .withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE) + .and.returnValue(of(mockHasScanImagePermission)); labelService = fixture.debugElement.injector.get(LabelService); @@ -149,7 +171,6 @@ describe("TagComponent (inline template)", () => { fixture.detectChanges(); }); - it("should load data", async(() => { expect(spy.calls.any).toBeTruthy(); })); @@ -169,3 +190,5 @@ describe("TagComponent (inline template)", () => { })); }); + + diff --git a/src/portal/lib/src/tag/tag.component.ts b/src/portal/lib/src/tag/tag.component.ts index 69b389e53c..09ef90dda5 100644 --- a/src/portal/lib/src/tag/tag.component.ts +++ b/src/portal/lib/src/tag/tag.component.ts @@ -21,8 +21,8 @@ import { ChangeDetectorRef, ElementRef, AfterViewInit } from "@angular/core"; -import {Subject, forkJoin} from "rxjs"; -import { debounceTime , distinctUntilChanged, finalize} from 'rxjs/operators'; +import { Subject, forkJoin } from "rxjs"; +import { debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators'; import { TranslateService } from "@ngx-translate/core"; import { State, Comparator } from "../service/interface"; @@ -30,9 +30,9 @@ import { TagService, RetagService, VulnerabilitySeverity, RequestQueryParams } f import { ErrorHandler } from "../error-handler/error-handler"; import { ChannelService } from "../channel/index"; import { - ConfirmationTargets, - ConfirmationState, - ConfirmationButtons, Roles + ConfirmationTargets, + ConfirmationState, + ConfirmationButtons, Roles } from "../shared/shared.const"; import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component"; @@ -54,6 +54,8 @@ import { import { CopyInputComponent } from "../push-image/copy-input.component"; import { LabelService } from "../service/label.service"; +import { UserPermissionService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; import { operateChanges, OperateInfo, OperationState } from "../operation/operate"; import { OperationService } from "../operation/operation.service"; import { ImageNameInputComponent } from "../image-name-input/image-name-input.component"; @@ -71,14 +73,13 @@ export interface LabelState { }) export class TagComponent implements OnInit, AfterViewInit { - signedCon: {[key: string]: any | string[]} = {}; + signedCon: { [key: string]: any | string[] } = {}; @Input() projectId: number; @Input() memberRoleID: number; @Input() repoName: string; @Input() isEmbedded: boolean; @Input() hasSignedIn: boolean; - @Input() hasProjectAdminRole: boolean; @Input() isGuest: boolean; @Input() registryUrl: string; @Input() withNotary: boolean; @@ -116,8 +117,8 @@ export class TagComponent implements OnInit, AfterViewInit { labelListOpen = false; selectedTag: Tag[]; - labelNameFilter: Subject = new Subject (); - stickLabelNameFilter: Subject = new Subject (); + labelNameFilter: Subject = new Subject(); + stickLabelNameFilter: Subject = new Subject(); filterOnGoing: boolean; stickName = ''; filterName = ''; @@ -144,10 +145,15 @@ export class TagComponent implements OnInit, AfterViewInit { totalCount = 0; currentState: State; + hasAddLabelImagePermission: boolean; + hasRetagImagePermission: boolean; + hasDeleteImagePermission: boolean; + hasScanImagePermission: boolean; constructor( private errorHandler: ErrorHandler, private tagService: TagService, private retagService: RetagService, + private userPermissionService: UserPermissionService, private labelService: LabelService, private translateService: TranslateService, private ref: ChangeDetectorRef, @@ -164,50 +170,50 @@ export class TagComponent implements OnInit, AfterViewInit { this.errorHandler.error("Repo name cannot be unset."); return; } - this.retrieve(); this.lastFilteredTagName = ''; this.labelNameFilter - .pipe(debounceTime(500)) - .pipe(distinctUntilChanged()) - .subscribe((name: string) => { - if (this.filterName.length) { - this.filterOnGoing = true; + .pipe(debounceTime(500)) + .pipe(distinctUntilChanged()) + .subscribe((name: string) => { + if (this.filterName.length) { + this.filterOnGoing = true; - this.imageFilterLabels.forEach(data => { - if (data.label.name.indexOf(this.filterName) !== -1) { - data.show = true; - } else { - data.show = false; - } - }); - setTimeout(() => { - setInterval(() => this.ref.markForCheck(), 200); - }, 1000); - } - }); + this.imageFilterLabels.forEach(data => { + if (data.label.name.indexOf(this.filterName) !== -1) { + data.show = true; + } else { + data.show = false; + } + }); + setTimeout(() => { + setInterval(() => this.ref.markForCheck(), 200); + }, 1000); + } + }); this.stickLabelNameFilter - .pipe(debounceTime(500)) - .pipe(distinctUntilChanged()) - .subscribe((name: string) => { - if (this.stickName.length) { - this.filterOnGoing = true; + .pipe(debounceTime(500)) + .pipe(distinctUntilChanged()) + .subscribe((name: string) => { + if (this.stickName.length) { + this.filterOnGoing = true; - this.imageStickLabels.forEach(data => { - if (data.label.name.indexOf(this.stickName) !== -1) { - data.show = true; - } else { - data.show = false; - } - }); - setTimeout(() => { - setInterval(() => this.ref.markForCheck(), 200); - }, 1000); - } - }); + this.imageStickLabels.forEach(data => { + if (data.label.name.indexOf(this.stickName) !== -1) { + data.show = true; + } else { + data.show = false; + } + }); + setTimeout(() => { + setInterval(() => this.ref.markForCheck(), 200); + }, 1000); + } + }); + this.getImagePermissionRule(this.projectId); } ngAfterViewInit() { @@ -219,7 +225,7 @@ export class TagComponent implements OnInit, AfterViewInit { public get filterLabelPieceWidth() { let len = this.lastFilteredTagName.length ? this.lastFilteredTagName.length * 6 + 60 : 115; return len > 210 ? 210 : len; -} + } doSearchTagNames(tagName: string) { this.lastFilteredTagName = tagName; @@ -234,9 +240,9 @@ export class TagComponent implements OnInit, AfterViewInit { st.page.to = this.pageSize - 1; let selectedLab = this.imageFilterLabels.find(label => label.iconsShow === true); if (selectedLab) { - st.filters = [{property: 'name', value: this.lastFilteredTagName}, {property: 'labels.id', value: selectedLab.label.id}]; + st.filters = [{ property: 'name', value: this.lastFilteredTagName }, { property: 'labels.id', value: selectedLab.label.id }]; } else { - st.filters = [{property: 'name', value: this.lastFilteredTagName}]; + st.filters = [{ property: 'name', value: this.lastFilteredTagName }]; } this.clrLoad(st); @@ -286,14 +292,14 @@ export class TagComponent implements OnInit, AfterViewInit { toPromise(this.labelService.getGLabels()).then((res: Label[]) => { if (res.length) { res.forEach(data => { - this.imageLabels.push({'iconsShow': false, 'label': data, 'show': true}); + this.imageLabels.push({ 'iconsShow': false, 'label': data, 'show': true }); }); } toPromise(this.labelService.getPLabels(this.projectId)).then((res1: Label[]) => { if (res1.length) { res1.forEach(data => { - this.imageLabels.push({'iconsShow': false, 'label': data, 'show': true}); + this.imageLabels.push({ 'iconsShow': false, 'label': data, 'show': true }); }); } this.imageFilterLabels = clone(this.imageLabels); @@ -372,15 +378,15 @@ export class TagComponent implements OnInit, AfterViewInit { } unSelectLabel(labelInfo: LabelState): void { - if (!this.inprogress) { - this.inprogress = true; - let labelId = labelInfo.label.id; - this.selectedRow = this.selectedTag; - toPromise(this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => { - this.refresh(); + if (!this.inprogress) { + this.inprogress = true; + let labelId = labelInfo.label.id; + this.selectedRow = this.selectedTag; + toPromise(this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => { + this.refresh(); - // insert the unselected label to groups with the same icons - this.sortOperation(this.imageStickLabels, labelInfo); + // insert the unselected label to groups with the same icons + this.sortOperation(this.imageStickLabels, labelInfo); labelInfo.iconsShow = false; this.inprogress = false; }).catch(err => { @@ -417,26 +423,26 @@ export class TagComponent implements OnInit, AfterViewInit { data.iconsShow = true; } }); - this.imageFilterLabels.splice(this.imageFilterLabels.indexOf(labelInfo), 1); - this.imageFilterLabels.unshift(labelInfo); - this.filterOneLabel = labelInfo.label; + this.imageFilterLabels.splice(this.imageFilterLabels.indexOf(labelInfo), 1); + this.imageFilterLabels.unshift(labelInfo); + this.filterOneLabel = labelInfo.label; - // reload data - this.currentPage = 1; - let st: State = this.currentState; - if (!st) { - st = { page: {} }; - } - st.page.size = this.pageSize; - st.page.from = 0; - st.page.to = this.pageSize - 1; - if (this.lastFilteredTagName) { - st.filters = [{property: 'name', value: this.lastFilteredTagName}, {property: 'labels.id', value: labelId}]; - } else { - st.filters = [{property: 'labels.id', value: labelId}]; - } + // reload data + this.currentPage = 1; + let st: State = this.currentState; + if (!st) { + st = { page: {} }; + } + st.page.size = this.pageSize; + st.page.from = 0; + st.page.to = this.pageSize - 1; + if (this.lastFilteredTagName) { + st.filters = [{ property: 'name', value: this.lastFilteredTagName }, { property: 'labels.id', value: labelId }]; + } else { + st.filters = [{ property: 'labels.id', value: labelId }]; + } - this.clrLoad(st); + this.clrLoad(st); } unFilterLabel(labelInfo: LabelState): void { @@ -456,7 +462,7 @@ export class TagComponent implements OnInit, AfterViewInit { st.page.from = 0; st.page.to = this.pageSize - 1; if (this.lastFilteredTagName) { - st.filters = [{property: 'name', value: this.lastFilteredTagName}]; + st.filters = [{ property: 'name', value: this.lastFilteredTagName }]; } else { st.filters = []; } @@ -480,7 +486,7 @@ export class TagComponent implements OnInit, AfterViewInit { data.show = false; } }); - } else { + } else { this.openLabelFilterPanel = false; this.openLabelFilterPiece = false; } @@ -523,7 +529,7 @@ export class TagComponent implements OnInit, AfterViewInit { retrieve() { this.tags = []; - let signatures: string[] = [] ; + let signatures: string[] = []; this.loading = true; toPromise(this.tagService @@ -539,15 +545,15 @@ export class TagComponent implements OnInit, AfterViewInit { components: { total: 0, summary: [] - } - }; - } - if (t.signature !== null) { - signatures.push(t.name); - } - }); - this.tags = items; - let signedName: {[key: string]: string[]} = {}; + } + }; + } + if (t.signature !== null) { + signatures.push(t.name); + } + }); + this.tags = items; + let signedName: { [key: string]: string[] } = {}; signedName[this.repoName] = signatures; this.signatureOutput.emit(signedName); this.loading = false; @@ -568,9 +574,9 @@ export class TagComponent implements OnInit, AfterViewInit { if (Math.pow(1024, 1) <= size && size < Math.pow(1024, 2)) { return (size / Math.pow(1024, 1)).toFixed(2) + "KB"; } else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) { - return (size / Math.pow(1024, 2)).toFixed(2) + "MB"; + return (size / Math.pow(1024, 2)).toFixed(2) + "MB"; } else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) { - return (size / Math.pow(1024, 3)).toFixed(2) + "GB"; + return (size / Math.pow(1024, 3)).toFixed(2) + "GB"; } else { return size + "B"; } @@ -578,8 +584,8 @@ export class TagComponent implements OnInit, AfterViewInit { retag(tags: Tag[]) { if (tags && tags.length) { - this.retagDialogOpened = true; - this.retagSrcImage = this.repoName + ":" + tags[0].digest; + this.retagDialogOpened = true; + this.retagSrcImage = this.repoName + ":" + tags[0].digest; } else { this.errorHandler.error("One tag should be selected before retag."); } @@ -587,23 +593,23 @@ export class TagComponent implements OnInit, AfterViewInit { onRetag() { this.retagService.retag({ - targetProject: this.imageNameInput.projectName.value, - targetRepo: this.imageNameInput.repoName.value, - targetTag: this.imageNameInput.tagName.value, - srcImage: this.retagSrcImage, - override: true - }) - .pipe(finalize(() => { + targetProject: this.imageNameInput.projectName.value, + targetRepo: this.imageNameInput.repoName.value, + targetTag: this.imageNameInput.tagName.value, + srcImage: this.retagSrcImage, + override: true + }) + .pipe(finalize(() => { this.retagDialogOpened = false; this.imageNameInput.form.reset(); - })) - .subscribe(response => { - this.translateService.get('RETAG.MSG_SUCCESS').subscribe((res: string) => { - this.errorHandler.info(res); - }); - }, error => { + })) + .subscribe(response => { + this.translateService.get('RETAG.MSG_SUCCESS').subscribe((res: string) => { + this.errorHandler.info(res); + }); + }, error => { this.errorHandler.error(error); - }); + }); } deleteTags(tags: Tag[]) { @@ -631,8 +637,8 @@ export class TagComponent implements OnInit, AfterViewInit { confirmDeletion(message: ConfirmationAcknowledgement) { if (message && - message.source === ConfirmationTargets.TAG - && message.state === ConfirmationState.CONFIRMED) { + message.source === ConfirmationTargets.TAG + && message.state === ConfirmationState.CONFIRMED) { let tags: Tag[] = message.data; if (tags && tags.length) { let promiseLists: any[] = []; @@ -660,27 +666,27 @@ export class TagComponent implements OnInit, AfterViewInit { if (tag.signature) { forkJoin(this.translateService.get("BATCH.DELETED_FAILURE"), this.translateService.get("REPOSITORY.DELETION_SUMMARY_TAG_DENIED")).subscribe(res => { - let wrongInfo: string = res[1] + "notary -s https://" + this.registryUrl + + let wrongInfo: string = res[1] + "notary -s https://" + this.registryUrl + ":4443 -d ~/.docker/trust remove -p " + this.registryUrl + "/" + this.repoName + " " + name; - operateChanges(operMessage, OperationState.failure, wrongInfo); - }); + operateChanges(operMessage, OperationState.failure, wrongInfo); + }); } else { return toPromise(this.tagService - .deleteTag(this.repoName, tag.name)) - .then( - response => { - this.translateService.get("BATCH.DELETED_SUCCESS") - .subscribe(res => { - operateChanges(operMessage, OperationState.success); - }); - }).catch(error => { + .deleteTag(this.repoName, tag.name)) + .then( + response => { + this.translateService.get("BATCH.DELETED_SUCCESS") + .subscribe(res => { + operateChanges(operMessage, OperationState.success); + }); + }).catch(error => { if (error.status === 503) { forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), - this.translateService.get('REPOSITORY.TAGS_NO_DELETE')).subscribe(res => { - operateChanges(operMessage, OperationState.failure, res[1]); - }); + this.translateService.get('REPOSITORY.TAGS_NO_DELETE')).subscribe(res => { + operateChanges(operMessage, OperationState.failure, res[1]); + }); return; } this.translateService.get("BATCH.DELETED_FAILURE").subscribe(res => { @@ -744,13 +750,29 @@ export class TagComponent implements OnInit, AfterViewInit { // Whether show the 'scan now' menu canScanNow(t: Tag[]): boolean { if (!this.withClair) { return false; } - if (!this.hasProjectAdminRole) { return false; } - let st: string = this.scanStatus(t[0]); + if (!this.hasScanImagePermission) { return false; } + let st: string = this.scanStatus(t[0]); return st !== VULNERABILITY_SCAN_STATUS.pending && st !== VULNERABILITY_SCAN_STATUS.running; } - + getImagePermissionRule(projectId: number): void { + let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, + USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE); + let hasRetagImagePermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL); + let hasDeleteImagePermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE); + let hasScanImagePermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE); + forkJoin(hasAddLabelImagePermission, hasRetagImagePermission, hasDeleteImagePermission, hasScanImagePermission) + .subscribe(permissions => { + this.hasAddLabelImagePermission = permissions[0] as boolean; + this.hasRetagImagePermission = permissions[1] as boolean; + this.hasDeleteImagePermission = permissions[2] as boolean; + this.hasScanImagePermission = permissions[3] as boolean; + }, error => this.errorHandler.error(error) ); + } // Trigger scan scanNow(t: Tag[]): void { if (t && t.length) { @@ -763,14 +785,6 @@ export class TagComponent implements OnInit, AfterViewInit { // pull command onCpError($event: any): void { - this.copyInput.setPullCommendShow(); - } - - public get developerRoleOrAbove(): boolean { - return this.memberRoleID === Roles.DEVELOPER || this.hasProjectAdminRole; - } - - public get guestRoleOrAbove(): boolean { - return this.memberRoleID === Roles.GUEST || this.memberRoleID === Roles.DEVELOPER || this.hasProjectAdminRole; + this.copyInput.setPullCommendShow(); } } diff --git a/src/portal/lib/src/utils.ts b/src/portal/lib/src/utils.ts index cbbd312311..b90d3984d4 100644 --- a/src/portal/lib/src/utils.ts +++ b/src/portal/lib/src/utils.ts @@ -56,6 +56,14 @@ export const HTTP_GET_OPTIONS: RequestOptions = new RequestOptions({ "Pragma": 'no-cache' }) }); +export const HTTP_GET_OPTIONS_CACHE: RequestOptions = new RequestOptions({ + headers: new Headers({ + "Content-Type": 'application/json', + "Accept": 'application/json', + "Cache-Control": 'no-cache', + "Pragma": 'no-cache', + }) +}); export const FILE_UPLOAD_OPTION: RequestOptions = new RequestOptions({ headers: new Headers({ diff --git a/src/portal/lib/src/vulnerability-scanning/result-grid.component.html b/src/portal/lib/src/vulnerability-scanning/result-grid.component.html index 70e0e1c9ab..cbc3c96bb7 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-grid.component.html +++ b/src/portal/lib/src/vulnerability-scanning/result-grid.component.html @@ -10,7 +10,7 @@
- + {{'VULNERABILITY.GRID.COLUMN_ID' | translate}} {{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}} diff --git a/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts b/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts index 920644c390..3b3948660c 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts @@ -8,6 +8,7 @@ import { ErrorHandler } from '../error-handler/index'; import { SharedModule } from '../shared/shared.module'; import { FilterComponent } from '../filter/index'; import {ChannelService} from "../channel/channel.service"; +import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service"; describe('ResultGridComponent (inline template)', () => { let component: ResultGridComponent; @@ -29,7 +30,8 @@ describe('ResultGridComponent (inline template)', () => { ErrorHandler, ChannelService, { provide: SERVICE_CONFIG, useValue: testConfig }, - { provide: ScanningResultService, useClass: ScanningResultDefaultService } + { provide: ScanningResultService, useClass: ScanningResultDefaultService }, + { provide: UserPermissionService, useClass: UserPermissionDefaultService } ] }); diff --git a/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts b/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts index 2eb1bf692d..4283fdfb06 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts @@ -5,10 +5,12 @@ import { VulnerabilitySeverity } from '../service/index'; import { ErrorHandler } from '../error-handler/index'; +import { forkJoin } from "rxjs"; import { toPromise } from '../utils'; -import {ChannelService} from "../channel/channel.service"; - +import { ChannelService } from "../channel/channel.service"; +import { UserPermissionService } from "../service/permission.service"; +import { USERSTATICPERMISSION } from "../service/permission-static"; @Component({ selector: 'hbr-vulnerabilities-grid', templateUrl: './result-grid.component.html', @@ -20,12 +22,12 @@ export class ResultGridComponent implements OnInit { @Input() tagId: string; @Input() repositoryId: string; - @Input() withAdminRole: boolean; - + hasScanImagePermission: boolean; constructor( private scanningService: ScanningResultService, private channel: ChannelService, - private errorHandler: ErrorHandler + private userPermissionService: UserPermissionService, + private errorHandler: ErrorHandler, ) { } ngOnInit(): void { @@ -79,4 +81,14 @@ export class ResultGridComponent implements OnInit { scanNow(): void { this.channel.publishScanEvent(this.repositoryId + "/" + this.tagId); } + getScanPermissions(projectId: number): void { + + const hasScanImagePermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE); + forkJoin(hasScanImagePermission).subscribe(permissions => { + this.hasScanImagePermission = permissions[0] as boolean; + }, error => { + this.errorHandler.error(error); + }); + } } diff --git a/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts b/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts index b1f41ab778..1c29e487e9 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts @@ -6,6 +6,7 @@ import { SharedModule } from '../shared/shared.module'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { VULNERABILITY_SCAN_STATUS } from '../utils'; +import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service"; describe('ResultTipComponent (inline template)', () => { let component: ResultTipComponent; @@ -41,7 +42,8 @@ describe('ResultTipComponent (inline template)', () => { SharedModule ], declarations: [ResultTipComponent], - providers: [{ provide: SERVICE_CONFIG, useValue: testConfig }] + providers: [{ provide: SERVICE_CONFIG, useValue: testConfig }, + { provide: UserPermissionService, useClass: UserPermissionDefaultService }] }); })); diff --git a/src/portal/package-lock.json b/src/portal/package-lock.json index 39788b6aeb..b0e9571a3b 100644 --- a/src/portal/package-lock.json +++ b/src/portal/package-lock.json @@ -134,13 +134,15 @@ "version": "1.37.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", - "dev": true + "dev": true, + "optional": true }, "mime-types": { "version": "2.1.21", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", "dev": true, + "optional": true, "requires": { "mime-db": "~1.37.0" } @@ -2757,9 +2759,9 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, "@types/jasmine": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", - "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==", + "version": "3.3.8", + "resolved": "http://registry.npm.taobao.org/@types/jasmine/download/@types/jasmine-3.3.8.tgz", + "integrity": "sha1-/Gq9kvcSFDFoXsmG8OyPd7Q5Cj4=", "dev": true }, "@types/jasminewd2": { @@ -6284,7 +6286,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -6305,12 +6308,14 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6325,17 +6330,20 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6452,7 +6460,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -6464,6 +6473,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6478,6 +6488,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6485,12 +6496,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6509,6 +6522,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6589,7 +6603,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6601,6 +6616,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -6686,7 +6702,8 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -6722,6 +6739,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6741,6 +6759,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6784,12 +6803,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "optional": true } } }, @@ -8703,10 +8724,9 @@ } }, "jasmine-core": { - "version": "2.99.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", - "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", - "dev": true + "version": "3.3.0", + "resolved": "http://registry.npm.taobao.org/jasmine-core/download/jasmine-core-3.3.0.tgz", + "integrity": "sha1-3qHNxjS8k8fg1K0nGF3zD6lxsQ4=" }, "jasmine-diff": { "version": "0.1.3", @@ -8965,7 +8985,7 @@ }, "karma-jasmine": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", + "resolved": "http://registry.npm.taobao.org/karma-jasmine/download/karma-jasmine-1.1.2.tgz", "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=", "dev": true }, diff --git a/src/portal/package.json b/src/portal/package.json index 0111b1224d..fc0b21174c 100644 --- a/src/portal/package.json +++ b/src/portal/package.json @@ -42,6 +42,7 @@ "buffer": "^5.2.1", "core-js": "^2.5.4", "intl": "^1.2.5", + "jasmine-core": "^3.3.0", "jquery": "^3.3.1", "mutationobserver-shim": "^0.3.2", "ng-packagr": "^4.1.1", @@ -65,18 +66,17 @@ "@angular/compiler-cli": "^7.1.3", "@angular/language-service": "^7.1.3", "@types/core-js": "^0.9.41", - "@types/jasmine": "~2.8.6", + "@types/jasmine": "^3.3.1", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", "codelyzer": "~4.2.1", "enhanced-resolve": "^3.0.0", - "jasmine-core": "~2.99.1", "jasmine-spec-reporter": "~4.2.1", "karma": "~1.7.1", "karma-chrome-launcher": "~2.2.0", "karma-cli": "^1.0.1", "karma-coverage-istanbul-reporter": "~2.0.0", - "karma-jasmine": "~1.1.1", + "karma-jasmine": "^1.1.2", "karma-jasmine-html-reporter": "^0.2.2", "karma-mocha-reporter": "^2.2.4", "karma-remap-istanbul": "^0.6.0", diff --git a/src/portal/src/app/config/config.component.html b/src/portal/src/app/config/config.component.html index 2fa843afc9..4e0b6a34b9 100644 --- a/src/portal/src/app/config/config.component.html +++ b/src/portal/src/app/config/config.component.html @@ -25,7 +25,10 @@ - + diff --git a/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.html b/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.html index b870f47af5..6e5bbdcdf3 100644 --- a/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.html +++ b/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.html @@ -10,8 +10,6 @@ [chartName]='chartName' [roleName]='roleName' [hasSignedIn]='hasSignedIn' - [projectRoleID]='project_member_role_id' - [hasProjectAdminRole]='hasProjectAdminRole' (versionClickEvt)='onVersionClick($event)' (backEvt)='gotoChartList()'> diff --git a/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.ts b/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.ts index a4315d97a7..79d17c86e5 100644 --- a/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.ts +++ b/src/portal/src/app/project/list-chart-versions/list-chart-versions.component.ts @@ -21,9 +21,7 @@ export class ListChartVersionsComponent implements OnInit { roleName: string; hasSignedIn: boolean; - hasProjectAdminRole: boolean; currentUser: SessionUser; - project_member_role_id: number; constructor( private route: ActivatedRoute, @@ -39,10 +37,8 @@ export class ListChartVersionsComponent implements OnInit { let resolverData = this.route.snapshot.data; if (resolverData) { let project = (resolverData["projectResolver"]); - this.hasProjectAdminRole = project.has_project_admin_role; this.roleName = project.role_name; this.projectName = project.name; - this.project_member_role_id = project.current_user_role_id; } } diff --git a/src/portal/src/app/project/list-charts/list-charts.component.html b/src/portal/src/app/project/list-charts/list-charts.component.html index 9f6c590981..7f17ab609b 100644 --- a/src/portal/src/app/project/list-charts/list-charts.component.html +++ b/src/portal/src/app/project/list-charts/list-charts.component.html @@ -4,6 +4,5 @@ [urlPrefix]='urlPrefix' [hasSignedIn]='hasSignedIn' [projectRoleID]='project_member_role_id' - [hasProjectAdminRole]='hasProjectAdminRole' (chartClickEvt)='onChartClick($event)'> diff --git a/src/portal/src/app/project/list-charts/list-charts.component.ts b/src/portal/src/app/project/list-charts/list-charts.component.ts index 3f8452a73a..5bb48e2c8a 100644 --- a/src/portal/src/app/project/list-charts/list-charts.component.ts +++ b/src/portal/src/app/project/list-charts/list-charts.component.ts @@ -17,7 +17,6 @@ export class ListChartsComponent implements OnInit { projectName: string; urlPrefix: string; hasSignedIn: boolean; - hasProjectAdminRole: boolean; project_member_role_id: number; currentUser: SessionUser; @@ -35,7 +34,6 @@ export class ListChartsComponent implements OnInit { if (resolverData) { let project = (resolverData["projectResolver"]); this.projectName = project.name; - this.hasProjectAdminRole = project.has_project_admin_role; this.project_member_role_id = project.current_user_role_id; } } diff --git a/src/portal/src/app/project/member/add-member/add-member.component.html b/src/portal/src/app/project/member/add-member/add-member.component.html index 3fc49c83d3..25d6cf17e2 100644 --- a/src/portal/src/app/project/member/add-member/add-member.component.html +++ b/src/portal/src/app/project/member/add-member/add-member.component.html @@ -32,6 +32,10 @@
+
+ + +
diff --git a/src/portal/src/app/project/member/member.component.html b/src/portal/src/app/project/member/member.component.html index f60bbb8ec2..1b6c152555 100644 --- a/src/portal/src/app/project/member/member.component.html +++ b/src/portal/src/app/project/member/member.component.html @@ -13,21 +13,22 @@
- - {{'MEMBER.ACTION' | translate}} - - - + + + + - + diff --git a/src/portal/src/app/project/member/member.component.ts b/src/portal/src/app/project/member/member.component.ts index f2f141a388..85eb416825 100644 --- a/src/portal/src/app/project/member/member.component.ts +++ b/src/portal/src/app/project/member/member.component.ts @@ -1,5 +1,5 @@ -import {finalize} from 'rxjs/operators'; +import { finalize } from 'rxjs/operators'; // Copyright (c) 2017 VMware, Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +15,9 @@ import {finalize} from 'rxjs/operators'; // limitations under the License. import { Component, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subscription } from "rxjs"; -import {TranslateService} from "@ngx-translate/core"; -import {operateChanges, OperateInfo, OperationService, OperationState} from "@harbor/ui"; +import { Subscription, forkJoin } from "rxjs"; +import { TranslateService } from "@ngx-translate/core"; +import { operateChanges, OperateInfo, OperationService, OperationState } from "@harbor/ui"; import { MessageHandlerService } from "../../shared/message-handler/message-handler.service"; import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from "../../shared/shared.const"; @@ -31,7 +31,8 @@ import { SessionUser } from "../../shared/session-user"; import { AddGroupComponent } from './add-group/add-group.component'; import { MemberService } from "./member.service"; import { AddMemberComponent } from "./add-member/add-member.component"; -import {AppConfigService} from "../../app-config.service"; +import { AppConfigService } from "../../app-config.service"; +import { UserPermissionService, USERSTATICPERMISSION, ErrorHandler } from "@harbor/ui"; @Component({ templateUrl: "member.component.html", @@ -46,7 +47,6 @@ export class MemberComponent implements OnInit, OnDestroy { delSub: Subscription; currentUser: SessionUser; - hasProjectAdminRole: boolean; batchOps = 'delete'; searchMember: string; @@ -65,7 +65,9 @@ export class MemberComponent implements OnInit, OnDestroy { @ViewChild(AddGroupComponent) addGroupComponent: AddGroupComponent; - + hasCreateMemberPermission: boolean; + hasUpdateMemberPermission: boolean; + hasDeleteMemberPermission: boolean; constructor( private route: ActivatedRoute, private router: Router, @@ -76,6 +78,8 @@ export class MemberComponent implements OnInit, OnDestroy { private session: SessionService, private operationService: OperationService, private appConfigService: AppConfigService, + private userPermissionService: UserPermissionService, + private errorHandler: ErrorHandler, private ref: ChangeDetectorRef) { this.delSub = OperateDialogService.confirmationConfirm$.subscribe(message => { @@ -102,14 +106,12 @@ export class MemberComponent implements OnInit, OnDestroy { this.projectId = +this.route.snapshot.parent.params["id"]; // Get current user from registered resolver. this.currentUser = this.session.getCurrentUser(); - let resolverData = this.route.snapshot.parent.data; - if (resolverData) { - this.hasProjectAdminRole = (resolverData["projectResolver"]).has_project_admin_role; - } this.retrieve(this.projectId, ""); if (this.appConfigService.isLdapMode()) { this.isLdapMode = true; } + // get member permission rule + this.getMemberPermissionRule(this.projectId); } doSearch(searchMember: string) { @@ -126,23 +128,23 @@ export class MemberComponent implements OnInit, OnDestroy { this.selectedRow = []; this.memberService .listMembers(projectId, username).pipe( - finalize(() => this.loading = false)) + finalize(() => this.loading = false)) .subscribe( - response => { - this.members = response; - let hnd = setInterval(() => this.ref.markForCheck(), 100); - setTimeout(() => clearInterval(hnd), 1000); - }, - error => { - this.router.navigate(["/harbor", "projects"]); - this.messageHandlerService.handleError(error); - }); + response => { + this.members = response; + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 1000); + }, + error => { + this.router.navigate(["/harbor", "projects"]); + this.messageHandlerService.handleError(error); + }); } get onlySelf(): boolean { if (this.selectedRow.length === 1 && this.selectedRow[0].entity_type === 'u' && - this.selectedRow[0].entity_id === this.currentUser.user_id) { + this.selectedRow[0].entity_id === this.currentUser.user_id) { return true; } return false; @@ -173,7 +175,7 @@ export class MemberComponent implements OnInit, OnDestroy { addedGroup(result: boolean) { this.searchMember = ""; this.retrieve(this.projectId, ""); - } + } changeMembersRole(members: Member[], roleId: number) { if (!members) { @@ -182,9 +184,9 @@ export class MemberComponent implements OnInit, OnDestroy { let changeOperate = (projectId: number, member: Member, ) => { return this.memberService - .changeMemberRole(projectId, member.id, roleId) - .then( () => this.batchChangeRoleInfos[member.id] = 'done') - .catch(error => this.messageHandlerService.handleError(error + ": " + member.entity_name)); + .changeMemberRole(projectId, member.id, roleId) + .then(() => this.batchChangeRoleInfos[member.id] = 'done') + .catch(error => this.messageHandlerService.handleError(error + ": " + member.entity_name)); }; // Preparation for members role change @@ -223,7 +225,7 @@ export class MemberComponent implements OnInit, OnDestroy { ConfirmationTargets.PROJECT_MEMBER, ConfirmationButtons.DELETE_CANCEL ); - this.OperateDialogService.openComfirmDialog(deletionMessage); + this.OperateDialogService.openComfirmDialog(deletionMessage); } } @@ -250,15 +252,15 @@ export class MemberComponent implements OnInit, OnDestroy { return this.memberService .deleteMember(projectId, member.id) .then(response => { - this.translate.get("BATCH.DELETED_SUCCESS").subscribe(res => { - operateChanges(operMessage, OperationState.success); - }); - }) - .catch(error => { - this.translate.get("BATCH.DELETED_FAILURE").subscribe(res => { - operateChanges(operMessage, OperationState.failure, res); - }); + this.translate.get("BATCH.DELETED_SUCCESS").subscribe(res => { + operateChanges(operMessage, OperationState.success); }); + }) + .catch(error => { + this.translate.get("BATCH.DELETED_FAILURE").subscribe(res => { + operateChanges(operMessage, OperationState.failure, res); + }); + }); }; // Deleting member then wating for results @@ -270,4 +272,17 @@ export class MemberComponent implements OnInit, OnDestroy { this.retrieve(this.projectId, ""); }); } + getMemberPermissionRule(projectId: number): void { + let hasCreateMemberPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.MEMBER.KEY, USERSTATICPERMISSION.MEMBER.VALUE.CREATE); + let hasUpdateMemberPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.MEMBER.KEY, USERSTATICPERMISSION.MEMBER.VALUE.UPDATE); + let hasDeleteMemberPermission = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.MEMBER.KEY, USERSTATICPERMISSION.MEMBER.VALUE.DELETE); + forkJoin(hasCreateMemberPermission, hasUpdateMemberPermission, hasDeleteMemberPermission).subscribe(MemberRule => { + this.hasCreateMemberPermission = MemberRule[0] as boolean; + this.hasUpdateMemberPermission = MemberRule[1] as boolean; + this.hasDeleteMemberPermission = MemberRule[2] as boolean; + }, error => this.errorHandler.error(error)); + } } diff --git a/src/portal/src/app/project/project-config/project-config.component.html b/src/portal/src/app/project/project-config/project-config.component.html index d51542b48e..bbfbc4254d 100644 --- a/src/portal/src/app/project/project-config/project-config.component.html +++ b/src/portal/src/app/project/project-config/project-config.component.html @@ -1,5 +1,5 @@
- +
\ No newline at end of file diff --git a/src/portal/src/app/project/project-config/project-config.component.ts b/src/portal/src/app/project/project-config/project-config.component.ts index d12a850579..5499aaf405 100644 --- a/src/portal/src/app/project/project-config/project-config.component.ts +++ b/src/portal/src/app/project/project-config/project-config.component.ts @@ -28,7 +28,6 @@ export class ProjectConfigComponent implements OnInit { projectName: string; currentUser: SessionUser; hasSignedIn: boolean; - hasProjectAdminRole: boolean; constructor( private route: ActivatedRoute, @@ -42,7 +41,6 @@ export class ProjectConfigComponent implements OnInit { let resolverData = this.route.snapshot.parent.data; if (resolverData) { let pro: Project = resolverData['projectResolver']; - this.hasProjectAdminRole = pro.has_project_admin_role; this.projectName = pro.name; } } diff --git a/src/portal/src/app/project/project-detail/project-detail.component.html b/src/portal/src/app/project/project-detail/project-detail.component.html index 7b43be2cad..5d3ea84c8d 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.html +++ b/src/portal/src/app/project/project-detail/project-detail.component.html @@ -4,28 +4,28 @@

{{currentProject.name}} {{roleName | translate}}

+ + \ No newline at end of file diff --git a/src/portal/src/app/project/robot-account/robot-account.component.ts b/src/portal/src/app/project/robot-account/robot-account.component.ts index 006a59d819..d0f2d0cac9 100644 --- a/src/portal/src/app/project/robot-account/robot-account.component.ts +++ b/src/portal/src/app/project/robot-account/robot-account.component.ts @@ -25,7 +25,10 @@ import { operateChanges, OperateInfo, OperationService, - OperationState + OperationState, + UserPermissionService, + USERSTATICPERMISSION, + ErrorHandler } from "@harbor/ui"; @Component({ @@ -48,12 +51,17 @@ export class RobotAccountComponent implements OnInit, OnDestroy { robots: Robot[]; projectId: number; subscription: Subscription; + hasRobotCreatePermission: boolean; + hasRobotUpdatePermission: boolean; + hasRobotDeletePermission: boolean; constructor( private route: ActivatedRoute, private robotService: RobotService, private OperateDialogService: ConfirmationDialogService, private operationService: OperationService, private translate: TranslateService, + private userPermissionService: UserPermissionService, + private errorHandler: ErrorHandler, private ref: ChangeDetectorRef, private messageHandlerService: MessageHandlerService ) { @@ -80,8 +88,24 @@ export class RobotAccountComponent implements OnInit, OnDestroy { } this.searchRobot = ""; this.retrieve(); + this.getPermissionsList(this.projectId); } + getPermissionsList(projectId: number): void { + let permissionsList = []; + permissionsList.push(this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.CREATE)); + permissionsList.push(this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.UPDATE)); + permissionsList.push(this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.DELETE)); + forkJoin(...permissionsList).subscribe(Rules => { + this.hasRobotCreatePermission = Rules[0] as boolean; + this.hasRobotUpdatePermission = Rules[1] as boolean; + this.hasRobotDeletePermission = Rules[2] as boolean; + + }, error => this.errorHandler.error(error)); + } ngOnDestroy(): void { if (this.subscription) { this.subscription.unsubscribe(); @@ -122,7 +146,7 @@ export class RobotAccountComponent implements OnInit, OnDestroy { this.selectedRow = []; }) ) - .subscribe(() => {}); + .subscribe(() => { }); } delOperate(robot: Robot) { diff --git a/src/portal/src/app/replication/replication-page.component.html b/src/portal/src/app/replication/replication-page.component.html index 579d1d5b13..e7efd3ce47 100644 --- a/src/portal/src/app/replication/replication-page.component.html +++ b/src/portal/src/app/replication/replication-page.component.html @@ -1,3 +1,12 @@
- +
\ No newline at end of file diff --git a/src/portal/src/app/replication/replication-page.component.ts b/src/portal/src/app/replication/replication-page.component.ts index b43aa878e0..2995ba9dd7 100644 --- a/src/portal/src/app/replication/replication-page.component.ts +++ b/src/portal/src/app/replication/replication-page.component.ts @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'; -import {ActivatedRoute, Router} from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; -import { ReplicationComponent } from '@harbor/ui'; - -import {SessionService} from "../shared/session.service"; -import {Project} from "../project/project"; -import {ProjectService} from "../project/project.service"; +import { SessionService } from "../shared/session.service"; +import { Project } from "../project/project"; +import { ProjectService } from "../project/project.service"; +import { ReplicationComponent, UserPermissionService, USERSTATICPERMISSION, ErrorHandler } from "@harbor/ui"; +import { forkJoin } from 'rxjs'; @Component({ selector: 'replication', @@ -28,25 +28,30 @@ export class ReplicationPageComponent implements OnInit, AfterViewInit { projectIdentify: string | number; @ViewChild("replicationView") replicationView: ReplicationComponent; projectName: string; - + hasCreateReplicationPermission: boolean; + hasUpdateReplicationPermission: boolean; + hasDeleteReplicationPermission: boolean; + hasExecuteReplicationPermission: boolean; constructor(private route: ActivatedRoute, - private router: Router, - private proService: ProjectService, - private session: SessionService) { } + private router: Router, + private proService: ProjectService, + private userPermissionService: UserPermissionService, + private errorHandler: ErrorHandler, + private session: SessionService) { } ngOnInit(): void { this.projectIdentify = +this.route.snapshot.parent.params['id']; - + this.getReplicationPermissions(this.projectIdentify); this.proService.listProjects("", undefined).toPromise() - .then(response => { - let projects = response.json() as Project[]; - if (projects.length) { - let project = projects.find(data => data.project_id === this.projectIdentify); - if (project) { - this.projectName = project.name; - } + .then(response => { + let projects = response.json() as Project[]; + if (projects.length) { + let project = projects.find(data => data.project_id === this.projectIdentify); + if (project) { + this.projectName = project.name; } - }); + } + }); } public get isSystemAdmin(): boolean { @@ -66,4 +71,24 @@ export class ReplicationPageComponent implements OnInit, AfterViewInit { goRegistry(): void { this.router.navigate(['/harbor', 'registries']); } + + getReplicationPermissions(projectId: number): void { + + let permissionsCreate = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPLICATION.KEY, USERSTATICPERMISSION.REPLICATION.VALUE.CREATE); + let permissionsUpdate = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPLICATION.KEY, USERSTATICPERMISSION.REPLICATION.VALUE.UPDATE); + let permissionsDelete = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPLICATION.KEY, USERSTATICPERMISSION.REPLICATION.VALUE.DELETE); + let permissionsExecute = this.userPermissionService.getPermission(projectId, + USERSTATICPERMISSION.REPLICATION_JOB.KEY, USERSTATICPERMISSION.REPLICATION_JOB.VALUE.CREATE); + forkJoin(permissionsCreate, permissionsUpdate, permissionsDelete, permissionsExecute).subscribe(permissions => { + this.hasCreateReplicationPermission = permissions[0] as boolean; + this.hasUpdateReplicationPermission = permissions[1] as boolean; + this.hasDeleteReplicationPermission = permissions[2] as boolean; + this.hasExecuteReplicationPermission = permissions[3] as boolean; + }, error => { + this.errorHandler.error(error); + }); + } } diff --git a/src/portal/src/app/replication/total-replication/total-replication-page.component.html b/src/portal/src/app/replication/total-replication/total-replication-page.component.html index 13be7aad57..a823aac157 100644 --- a/src/portal/src/app/replication/total-replication/total-replication-page.component.html +++ b/src/portal/src/app/replication/total-replication/total-replication-page.component.html @@ -1,4 +1,13 @@

{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}

- +
\ No newline at end of file diff --git a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.html b/src/portal/src/app/repository/tag-detail/tag-detail-page.component.html index ca6f724010..6eb680c31f 100644 --- a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.html +++ b/src/portal/src/app/repository/tag-detail/tag-detail-page.component.html @@ -5,9 +5,9 @@ < {{repositoryId}} + [tagId]="tagId" + [withClair]="withClair" + [withAdmiral]="withAdmiral" + [projectId]="projectId" + [repositoryId]="repositoryId"> \ No newline at end of file diff --git a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.ts b/src/portal/src/app/repository/tag-detail/tag-detail-page.component.ts index 9232789e6f..f4c1d1f34d 100644 --- a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.ts +++ b/src/portal/src/app/repository/tag-detail/tag-detail-page.component.ts @@ -48,10 +48,6 @@ export class TagDetailPageComponent implements OnInit { return this.appConfigService.getConfig().with_clair; } - get withAdminRole(): boolean { - return this.session.getCurrentUser().has_admin_role; - } - goBack(tag: string): void { this.router.navigate(["harbor", "projects", this.projectId, "repositories", tag]); } diff --git a/src/portal/src/app/shared/message-handler/message-handler.service.ts b/src/portal/src/app/shared/message-handler/message-handler.service.ts index ed19f08752..efbc12642e 100644 --- a/src/portal/src/app/shared/message-handler/message-handler.service.ts +++ b/src/portal/src/app/shared/message-handler/message-handler.service.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { ErrorHandler } from '@harbor/ui'; +import { ErrorHandler, UserPermissionService } from '@harbor/ui'; import { MessageService } from '../../global-message/message.service'; import { AlertType, httpStatusCode } from '../../shared/shared.const'; @@ -27,6 +27,7 @@ export class MessageHandlerService implements ErrorHandler { constructor( private msgService: MessageService, private translate: TranslateService, + private userPermissionService: UserPermissionService, private session: SessionService) { } // Handle the error and map it to the suitable message @@ -46,6 +47,7 @@ export class MessageHandlerService implements ErrorHandler { this.msgService.announceAppLevelMessage(code, msg, AlertType.DANGER); // Session is invalid now, clare session cache this.session.clear(); + this.userPermissionService.clearPermissionCache(); } else { this.msgService.announceMessage(code, msg, AlertType.DANGER); } diff --git a/src/portal/src/app/shared/route/member-guard-activate.service.ts b/src/portal/src/app/shared/route/member-guard-activate.service.ts index 9ff813ea62..05b907dce1 100644 --- a/src/portal/src/app/shared/route/member-guard-activate.service.ts +++ b/src/portal/src/app/shared/route/member-guard-activate.service.ts @@ -57,7 +57,7 @@ export class MemberGuard implements CanActivate, CanActivateChild { () => { // Add exception for repository in project detail router activation. this.projectService.getProject(projectId).subscribe(project => { - if (project.public === 1) { + if (project.metadata && project.metadata.public === 'true') { return resolve(true); } this.router.navigate([CommonRoutes.HARBOR_DEFAULT]); diff --git a/src/portal/src/app/shared/route/sign-in-guard-activate.service.ts b/src/portal/src/app/shared/route/sign-in-guard-activate.service.ts index 2cd39c961f..4f6a72133a 100644 --- a/src/portal/src/app/shared/route/sign-in-guard-activate.service.ts +++ b/src/portal/src/app/shared/route/sign-in-guard-activate.service.ts @@ -20,10 +20,11 @@ import { } from '@angular/router'; import { SessionService } from '../../shared/session.service'; import { CommonRoutes } from '../../shared/shared.const'; +import { UserPermissionService } from "@harbor/ui"; @Injectable() export class SignInGuard implements CanActivate, CanActivateChild { - constructor(private authService: SessionService, private router: Router) { } + constructor(private authService: SessionService, private router: Router, private userPermission: UserPermissionService) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean { // If user has logged in, should not login again @@ -34,6 +35,8 @@ export class SignInGuard implements CanActivate, CanActivateChild { this.authService.signOff() .then(() => { this.authService.clear(); // Destroy session cache + this.userPermission.clearPermissionCache(); + return resolve(true); }) .catch(error => { diff --git a/src/portal/src/app/shared/session.service.ts b/src/portal/src/app/shared/session.service.ts index c74e7e5b81..e03b8c8d00 100644 --- a/src/portal/src/app/shared/session.service.ts +++ b/src/portal/src/app/shared/session.service.ts @@ -20,7 +20,7 @@ import { Member } from '../project/member/member'; import { SignInCredential } from './sign-in-credential'; import { enLang } from '../shared/shared.const'; -import {HTTP_FORM_OPTIONS, HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS} from "./shared.utils"; +import { HTTP_FORM_OPTIONS, HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS } from "./shared.utils"; const signInUrl = '/c/login'; const currentUserEndpoint = "/api/users/current"; @@ -67,7 +67,7 @@ export class SessionService { signIn(signInCredential: SignInCredential): Promise { // Build the form package let queryParam: string = 'principal=' + encodeURIComponent(signInCredential.principal) + - '&password=' + encodeURIComponent(signInCredential.password); + '&password=' + encodeURIComponent(signInCredential.password); // Trigger Http return this.http.post(signInUrl, queryParam, HTTP_FORM_OPTIONS) @@ -144,9 +144,9 @@ export class SessionService { return Promise.reject("Invalid account settings"); } return this.http.post(renameAdminEndpoint, JSON.stringify({}), HTTP_JSON_OPTIONS) - .toPromise() - .then(() => null) - .catch(error => this.handleError(error)); + .toPromise() + .then(() => null) + .catch(error => this.handleError(error)); } /** diff --git a/src/portal/src/app/shared/shared.const.ts b/src/portal/src/app/shared/shared.const.ts index 7cefb9dec3..1bc2ddcf2e 100644 --- a/src/portal/src/app/shared/shared.const.ts +++ b/src/portal/src/app/shared/shared.const.ts @@ -78,16 +78,19 @@ export const enum ConfirmationButtons { } export const ProjectTypes = { 0: 'PROJECT.ALL_PROJECTS', 1: 'PROJECT.PRIVATE_PROJECTS', 2: 'PROJECT.PUBLIC_PROJECTS' }; -export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST' }; -export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' }; +export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST', 4: 'MEMBER.PROJECT_MASTER' }; +export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', +'master': 'MEMBER.PROJECT_MASTER', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' }; export const ProjectRoles = [ { id: 1, value: "MEMBER.PROJECT_ADMIN" }, { id: 2, value: "MEMBER.DEVELOPER" }, - { id: 3, value: "MEMBER.GUEST" } + { id: 3, value: "MEMBER.GUEST" }, + { id: 4, value: "MEMBER.PROJECT_MASTER" }, ]; export enum Roles { PROJECT_ADMIN = 1, + PROJECT_MASTER = 4, DEVELOPER = 2, GUEST = 3, OTHER = 0, diff --git a/src/portal/src/app/user/user.component.ts b/src/portal/src/app/user/user.component.ts index 5123ac3825..3a3e6e2992 100644 --- a/src/portal/src/app/user/user.component.ts +++ b/src/portal/src/app/user/user.component.ts @@ -26,8 +26,8 @@ import { AppConfigService } from '../app-config.service'; import { NewUserModalComponent } from './new-user-modal.component'; import { UserService } from './user.service'; import { User } from './user'; -import {ChangePasswordComponent} from "./change-password/change-password.component"; -import {operateChanges, OperateInfo, OperationService, OperationState} from "@harbor/ui"; +import { ChangePasswordComponent } from "./change-password/change-password.component"; +import { operateChanges, OperateInfo, OperationService, OperationState } from "@harbor/ui"; /** * NOTES: * Pagination for this component is a temporary workaround solution. It will be replaced in future release. @@ -153,7 +153,7 @@ export class UserComponent implements OnInit, OnDestroy { return this.onGoing; } - ngOnInit(): void {} + ngOnInit(): void { } ngOnDestroy(): void { if (this.deletionSubscription) { @@ -223,15 +223,15 @@ export class UserComponent implements OnInit, OnDestroy { } } - Promise.all(promiseLists).then(() => { - this.selectedRow = []; - this.refresh(); - }) + Promise.all(promiseLists).then(() => { + this.selectedRow = []; + this.refresh(); + }) .catch(error => { - this.selectedRow = []; - this.msgHandler.handleError(error); - }); - } + this.selectedRow = []; + this.msgHandler.handleError(error); + }); + } } // Delete the specified user @@ -298,7 +298,7 @@ export class UserComponent implements OnInit, OnDestroy { this.translate.get('BATCH.DELETED_FAILURE').subscribe(res => { operateChanges(operMessage, OperationState.failure, res); }); - }); + }); } // Refresh the user list @@ -310,15 +310,15 @@ export class UserComponent implements OnInit, OnDestroy { this.originalUsers = this.userService.getUsers(); this.originalUsers.then(users => { - this.onGoing = false; + this.onGoing = false; - this.totalCount = users.length; - this.users = users.slice(from, to); // First page + this.totalCount = users.length; + this.users = users.slice(from, to); // First page - this.forceRefreshView(5000); + this.forceRefreshView(5000); - return users; - }) + return users; + }) .catch(error => { this.onGoing = false; this.msgHandler.handleError(error); diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 5ea73b524f..ebdeff096c 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -219,6 +219,7 @@ "ROLE": "Role", "SYS_ADMIN": "System Admin", "PROJECT_ADMIN": "Project Admin", + "PROJECT_MASTER": "Master", "DEVELOPER": "Developer", "GUEST": "Guest", "DELETE": "Delete", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index b286dd804d..181433c6e5 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -219,6 +219,7 @@ "ROLE": "Rol", "SYS_ADMIN": "Administrador del sistema", "PROJECT_ADMIN": "Administrador del proyecto", + "PROJECT_MASTER": "Mantenedor", "DEVELOPER": "Desarrollador", "GUEST": "Invitado", "DELETE": "Eliminar", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 13c03ec0be..af362a39ce 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -224,6 +224,7 @@ "USER_TYPE": "User", "SYS_ADMIN": "System Admin", "PROJECT_ADMIN": "Project Admin", + "PROJECT_MASTER": "préposé à la maintenance", "DEVELOPER": "Développeur", "GUEST": "Invité", "DELETE": "Supprimer", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 7cd47fef4c..684cff4755 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -217,6 +217,7 @@ "ROLE": "Função", "SYS_ADMIN": "Administrador do Sistema", "PROJECT_ADMIN": "Administrador do Projeto", + "PROJECT_MASTER": "Mantenedor", "DEVELOPER": "Desenvolvedor", "GUEST": "Visitante", "DELETE": "Remover", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 8766b43c45..a4475b4dcf 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -219,6 +219,7 @@ "ROLE": "角色", "SYS_ADMIN": "系统管理员", "PROJECT_ADMIN": "项目管理员", + "PROJECT_MASTER": "维护人员", "DEVELOPER": "开发人员", "GUEST": "访客", "DELETE": "删除", diff --git a/tests/resources/Harbor-Pages/Project-Config.robot b/tests/resources/Harbor-Pages/Project-Config.robot index 0452383866..4d5467a7b5 100644 --- a/tests/resources/Harbor-Pages/Project-Config.robot +++ b/tests/resources/Harbor-Pages/Project-Config.robot @@ -8,6 +8,7 @@ ${HARBOR_VERSION} V1.1.1 *** Keywords *** Goto Project Config + Sleep 3 Click Element //project-detail//ul/li[contains(.,'Configuration')] Sleep 2 diff --git a/tests/resources/Harbor-Pages/Project.robot b/tests/resources/Harbor-Pages/Project.robot index 9b60966f8b..2a8f797995 100644 --- a/tests/resources/Harbor-Pages/Project.robot +++ b/tests/resources/Harbor-Pages/Project.robot @@ -52,6 +52,7 @@ Go To Project Log Sleep 2 Switch To Member + Sleep 3 Click Element xpath=${project_member_xpath} Sleep 1 @@ -89,22 +90,22 @@ Search Private Projects Make Project Private [Arguments] ${projectname} Go Into Project ${project name} - Sleep 1 + Sleep 2 Click Element xpath=//project-detail//a[contains(.,'Configuration')] Sleep 1 Checkbox Should Be Selected xpath=//input[@name='public'] - Click Element //div[@id='clr-wrapper-public']//label + Click Element //div[@id="clr-wrapper-public"]//label[1] Wait Until Element Is Enabled //button[contains(.,'SAVE')] Click Element //button[contains(.,'SAVE')] Wait Until Page Contains Configuration has been successfully saved Make Project Public [Arguments] ${projectname} - Go Into Project ${project name} - Sleep 1 + Go Into Project ${project name} + Sleep 2 Click Element xpath=//project-detail//a[contains(.,'Configuration')] Checkbox Should Not Be Selected xpath=//input[@name='public'] - Click Element //div[@id='clr-wrapper-public']//label + Click Element //div[@id="clr-wrapper-public"]//label[1] Wait Until Element Is Enabled //button[contains(.,'SAVE')] Click Element //button[contains(.,'SAVE')] Wait Until Page Contains Configuration has been successfully saved diff --git a/tests/resources/Harbor-Pages/ToolKit_Elements.robot b/tests/resources/Harbor-Pages/ToolKit_Elements.robot index 17d2ceb131..27db34257b 100644 --- a/tests/resources/Harbor-Pages/ToolKit_Elements.robot +++ b/tests/resources/Harbor-Pages/ToolKit_Elements.robot @@ -16,5 +16,5 @@ Documentation This resource provides any keywords related to the Harbor private registry appliance *** Variables *** -${member_action_xpath} //*[@id='member-action'] -${delete_action_xpath} //clr-dropdown/clr-dropdown-menu/button[4] +${member_action_xpath} //*[@id="member-action"] +${delete_action_xpath} //clr-dropdown/clr-dropdown-menu/button[5] diff --git a/tests/resources/Harbor-Pages/Vulnerability.robot b/tests/resources/Harbor-Pages/Vulnerability.robot index 98618b8248..0e377229b5 100644 --- a/tests/resources/Harbor-Pages/Vulnerability.robot +++ b/tests/resources/Harbor-Pages/Vulnerability.robot @@ -63,6 +63,7 @@ Enable Scan On Push Sleep 10 Vulnerability Not Ready Project Hint + Sleep 2 ${element}= Set Variable xpath=//span[contains(@class, 'db-status-warning')] Wait Until Element Is Visible And Enabled ${element} From 1c4b9aa3460e3523737e74ca04bc24878b87e44f Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Fri, 1 Feb 2019 18:55:06 +0800 Subject: [PATCH 33/45] Protect API using rbac Signed-off-by: He Weiwei --- src/common/rbac/const.go | 3 + src/common/rbac/namespace.go | 8 +- src/common/rbac/project/util.go | 34 ++++++- src/common/rbac/project/visitor_role.go | 59 ++++++++++- src/common/security/admiral/context.go | 18 ---- src/common/security/context.go | 6 -- src/common/security/local/context.go | 18 ---- src/common/security/local/context_test.go | 41 +++++--- src/common/security/robot/context.go | 18 ---- src/common/security/robot/context_test.go | 11 ++- src/common/security/secret/context.go | 33 +------ src/common/security/secret/context_test.go | 28 ++++-- src/core/api/chart_label.go | 26 +++-- src/core/api/chart_repository.go | 110 ++++++++------------- src/core/api/chart_repository_test.go | 40 +------- src/core/api/label.go | 75 ++++++++------ src/core/api/label_resource.go | 17 ---- src/core/api/metadata.go | 50 ++++++---- src/core/api/project.go | 64 +++++------- src/core/api/projectmember.go | 39 ++++++-- src/core/api/replication_job.go | 7 +- src/core/api/replication_policy.go | 7 +- src/core/api/repository.go | 36 ++++--- src/core/api/repository_label.go | 89 ++++++++++++----- src/core/api/robot.go | 35 ++++++- src/core/api/scan_job.go | 5 +- src/core/service/token/creator.go | 14 +-- src/core/service/token/token_test.go | 9 -- 28 files changed, 493 insertions(+), 407 deletions(-) diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index e0894d763a..b0e1de59b8 100644 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -38,11 +38,14 @@ const ( ResourceHelmChartVersion = Resource("helm-chart-version") ResourceHelmChartVersionLabel = Resource("helm-chart-version-label") ResourceLabel = Resource("label") + ResourceLabelResource = Resource("label-resource") ResourceLog = Resource("log") ResourceMember = Resource("member") + ResourceMetadata = Resource("metadata") ResourceReplication = Resource("replication") ResourceReplicationJob = Resource("replication-job") ResourceRepository = Resource("repository") + ResourceRepositoryLabel = Resource("repository-label") ResourceRepositoryTag = Resource("repository-tag") ResourceRepositoryTagLabel = Resource("repository-tag-label") ResourceRepositoryTagManifest = Resource("repository-tag-manifest") diff --git a/src/common/rbac/namespace.go b/src/common/rbac/namespace.go index ad85cc5e66..7f4f0f6a37 100644 --- a/src/common/rbac/namespace.go +++ b/src/common/rbac/namespace.go @@ -52,6 +52,10 @@ func (ns *projectNamespace) IsPublic() bool { } // NewProjectNamespace returns namespace for project -func NewProjectNamespace(projectIDOrName interface{}, isPublic bool) Namespace { - return &projectNamespace{projectIDOrName: projectIDOrName, isPublic: isPublic} +func NewProjectNamespace(projectIDOrName interface{}, isPublic ...bool) Namespace { + isPublicNamespace := false + if len(isPublic) > 0 { + isPublicNamespace = isPublic[0] + } + return &projectNamespace{projectIDOrName: projectIDOrName, isPublic: isPublicNamespace} } diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index 447e7c9d13..75dd8b13d0 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -23,9 +23,23 @@ var ( publicProjectPolicies = []*rbac.Policy{ {Resource: rbac.ResourceSelf, Action: rbac.ActionRead}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionRead}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, + + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, + + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList}, + + {Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList}, + + {Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead}, + {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, @@ -44,10 +58,16 @@ var ( {Resource: rbac.ResourceMember, Action: rbac.ActionDelete}, {Resource: rbac.ResourceMember, Action: rbac.ActionList}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionRead}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, {Resource: rbac.ResourceReplication, Action: rbac.ActionList}, {Resource: rbac.ResourceReplication, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceReplication, Action: rbac.ActionRead}, {Resource: rbac.ResourceReplication, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceReplication, Action: rbac.ActionDelete}, @@ -56,17 +76,25 @@ var ( {Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList}, {Resource: rbac.ResourceLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionRead}, {Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceLabel, Action: rbac.ActionDelete}, {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceLabelResource, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceRepository, Action: rbac.ActionDelete}, {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, - {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project - {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project + + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete}, @@ -81,12 +109,14 @@ var ( {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionList}, + {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete}, {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, diff --git a/src/common/rbac/project/visitor_role.go b/src/common/rbac/project/visitor_role.go index ac499887d6..97aeae87f8 100644 --- a/src/common/rbac/project/visitor_role.go +++ b/src/common/rbac/project/visitor_role.go @@ -31,6 +31,11 @@ var ( {Resource: rbac.ResourceMember, Action: rbac.ActionDelete}, {Resource: rbac.ResourceMember, Action: rbac.ActionList}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionRead}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, {Resource: rbac.ResourceReplication, Action: rbac.ActionRead}, @@ -40,18 +45,25 @@ var ( {Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList}, {Resource: rbac.ResourceLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionRead}, {Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceLabel, Action: rbac.ActionDelete}, {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceLabelResource, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceRepository, Action: rbac.ActionDelete}, {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, - {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project - {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, - {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project + + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete}, @@ -66,6 +78,7 @@ var ( {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, // upload helm chart {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, // download helm chart @@ -95,23 +108,34 @@ var ( {Resource: rbac.ResourceMember, Action: rbac.ActionList}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionRead}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate}, + {Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceLog, Action: rbac.ActionList}, {Resource: rbac.ResourceReplication, Action: rbac.ActionRead}, {Resource: rbac.ResourceReplication, Action: rbac.ActionList}, {Resource: rbac.ResourceLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionRead}, {Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceLabel, Action: rbac.ActionDelete}, {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceRepository, Action: rbac.ActionDelete}, {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, @@ -125,6 +149,7 @@ var ( {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, @@ -140,7 +165,9 @@ var ( {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete}, {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, - {Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate}, + + {Resource: rbac.ResourceRobot, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionList}, }, "developer": { @@ -150,11 +177,20 @@ var ( {Resource: rbac.ResourceLog, Action: rbac.ActionList}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionRead}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate}, {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPush}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, @@ -164,6 +200,7 @@ var ( {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate}, {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, {Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, @@ -177,6 +214,9 @@ var ( {Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete}, {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, + + {Resource: rbac.ResourceRobot, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionList}, }, "guest": { @@ -186,12 +226,20 @@ var ( {Resource: rbac.ResourceLog, Action: rbac.ActionList}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionRead}, + {Resource: rbac.ResourceLabel, Action: rbac.ActionList}, + + {Resource: rbac.ResourceRepository, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepository, Action: rbac.ActionList}, {Resource: rbac.ResourceRepository, Action: rbac.ActionPull}, + {Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead}, {Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList}, + {Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList}, {Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead}, @@ -203,6 +251,9 @@ var ( {Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList}, {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, + + {Resource: rbac.ResourceRobot, Action: rbac.ActionRead}, + {Resource: rbac.ResourceRobot, Action: rbac.ActionList}, }, } ) diff --git a/src/common/security/admiral/context.go b/src/common/security/admiral/context.go index 9abc5faea9..962a6dafb7 100644 --- a/src/common/security/admiral/context.go +++ b/src/common/security/admiral/context.go @@ -69,24 +69,6 @@ func (s *SecurityContext) IsSolutionUser() bool { return false } -// HasReadPerm returns whether the user has read permission to the project -func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - -// HasWritePerm returns whether the user has write permission to the project -func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - -// HasAllPerm returns whether the user has all permissions to the project -func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - // Can returns whether the user can do action on resource func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { ns, err := resource.GetNamespace() diff --git a/src/common/security/context.go b/src/common/security/context.go index d1b9af92bd..4b879a2184 100644 --- a/src/common/security/context.go +++ b/src/common/security/context.go @@ -29,12 +29,6 @@ type Context interface { IsSysAdmin() bool // IsSolutionUser returns whether the user is solution user IsSolutionUser() bool - // HasReadPerm returns whether the user has read permission to the project - HasReadPerm(projectIDOrName interface{}) bool - // HasWritePerm returns whether the user has write permission to the project - HasWritePerm(projectIDOrName interface{}) bool - // HasAllPerm returns whether the user has all permissions to the project - HasAllPerm(projectIDOrName interface{}) bool // Get current user's all project GetMyProjects() ([]*models.Project, error) // Get user's role in provided project diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index f0d33ceedd..655fe34b19 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -67,24 +67,6 @@ func (s *SecurityContext) IsSolutionUser() bool { return false } -// HasReadPerm returns whether the user has read permission to the project -func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - -// HasWritePerm returns whether the user has write permission to the project -func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - -// HasAllPerm returns whether the user has all permissions to the project -func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - // Can returns whether the user can do action on resource func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { ns, err := resource.GetNamespace() diff --git a/src/common/security/local/context_test.go b/src/common/security/local/context_test.go index 976237fd6e..80b40818be 100644 --- a/src/common/security/local/context_test.go +++ b/src/common/security/local/context_test.go @@ -23,6 +23,7 @@ import ( "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao/project" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/promgr" "github.com/goharbor/harbor/src/core/promgr/pmsdriver/local" @@ -210,66 +211,73 @@ func TestIsSolutionUser(t *testing.T) { func TestHasReadPerm(t *testing.T) { // public project ctx := NewSecurityContext(nil, pm) - assert.True(t, ctx.HasReadPerm("library")) + + resource := rbac.NewProjectNamespace("library").Resource(rbac.ResourceRepository) + assert.True(t, ctx.Can(rbac.ActionPull, resource)) // private project, unauthenticated ctx = NewSecurityContext(nil, pm) - assert.False(t, ctx.HasReadPerm(private.Name)) + resource = rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository) + assert.False(t, ctx.Can(rbac.ActionPull, resource)) // private project, authenticated, has no perm ctx = NewSecurityContext(&models.User{ Username: "test", }, pm) - assert.False(t, ctx.HasReadPerm(private.Name)) + assert.False(t, ctx.Can(rbac.ActionPull, resource)) // private project, authenticated, has read perm ctx = NewSecurityContext(guestUser, pm) - assert.True(t, ctx.HasReadPerm(private.Name)) + assert.True(t, ctx.Can(rbac.ActionPull, resource)) // private project, authenticated, system admin ctx = NewSecurityContext(&models.User{ Username: "admin", HasAdminRole: true, }, pm) - assert.True(t, ctx.HasReadPerm(private.Name)) + assert.True(t, ctx.Can(rbac.ActionPull, resource)) } func TestHasWritePerm(t *testing.T) { + resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository) + // unauthenticated ctx := NewSecurityContext(nil, pm) - assert.False(t, ctx.HasWritePerm(private.Name)) + assert.False(t, ctx.Can(rbac.ActionPush, resource)) // authenticated, has read perm ctx = NewSecurityContext(guestUser, pm) - assert.False(t, ctx.HasWritePerm(private.Name)) + assert.False(t, ctx.Can(rbac.ActionPush, resource)) // authenticated, has write perm ctx = NewSecurityContext(developerUser, pm) - assert.True(t, ctx.HasWritePerm(private.Name)) + assert.True(t, ctx.Can(rbac.ActionPush, resource)) // authenticated, system admin ctx = NewSecurityContext(&models.User{ Username: "admin", HasAdminRole: true, }, pm) - assert.True(t, ctx.HasReadPerm(private.Name)) + assert.True(t, ctx.Can(rbac.ActionPush, resource)) } func TestHasAllPerm(t *testing.T) { + resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository) + // unauthenticated ctx := NewSecurityContext(nil, pm) - assert.False(t, ctx.HasAllPerm(private.Name)) + assert.False(t, ctx.Can(rbac.ActionPushPull, resource)) // authenticated, has all perms ctx = NewSecurityContext(projectAdminUser, pm) - assert.True(t, ctx.HasAllPerm(private.Name)) + assert.True(t, ctx.Can(rbac.ActionPushPull, resource)) // authenticated, system admin ctx = NewSecurityContext(&models.User{ Username: "admin", HasAdminRole: true, }, pm) - assert.True(t, ctx.HasAllPerm(private.Name)) + assert.True(t, ctx.Can(rbac.ActionPushPull, resource)) } func TestHasAllPermWithGroup(t *testing.T) { @@ -285,10 +293,13 @@ func TestHasAllPermWithGroup(t *testing.T) { developer.GroupList = []*models.UserGroup{ {GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"}, } + + resource := rbac.NewProjectNamespace(project.Name).Resource(rbac.ResourceRepository) + ctx := NewSecurityContext(developer, pm) - assert.False(t, ctx.HasAllPerm(project.Name)) - assert.True(t, ctx.HasWritePerm(project.Name)) - assert.True(t, ctx.HasReadPerm(project.Name)) + assert.False(t, ctx.Can(rbac.ActionPushPull, resource)) + assert.True(t, ctx.Can(rbac.ActionPush, resource)) + assert.True(t, ctx.Can(rbac.ActionPull, resource)) } func TestGetMyProjects(t *testing.T) { diff --git a/src/common/security/robot/context.go b/src/common/security/robot/context.go index 3b48b91bc0..49d80ef350 100644 --- a/src/common/security/robot/context.go +++ b/src/common/security/robot/context.go @@ -60,24 +60,6 @@ func (s *SecurityContext) IsSolutionUser() bool { return false } -// HasReadPerm returns whether the user has read permission to the project -func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - -// HasWritePerm returns whether the user has write permission to the project -func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - -// HasAllPerm returns whether the user has all permissions to the project -func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - isPublicProject, _ := s.pm.IsPublic(projectIDOrName) - return s.Can(rbac.ActionPushPull, rbac.NewProjectNamespace(projectIDOrName, isPublicProject).Resource(rbac.ResourceRepository)) -} - // GetMyProjects no implementation func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) { return nil, nil diff --git a/src/common/security/robot/context_test.go b/src/common/security/robot/context_test.go index df7869a904..46225b52aa 100644 --- a/src/common/security/robot/context_test.go +++ b/src/common/security/robot/context_test.go @@ -16,6 +16,7 @@ package robot import ( "os" + "strconv" "testing" "github.com/goharbor/harbor/src/common/dao" @@ -26,7 +27,6 @@ import ( "github.com/goharbor/harbor/src/core/promgr/pmsdriver/local" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "strconv" ) var ( @@ -147,7 +147,8 @@ func TestHasReadPerm(t *testing.T) { } ctx := NewSecurityContext(robot, pm, policies) - assert.True(t, ctx.HasReadPerm(private.Name)) + resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository) + assert.True(t, ctx.Can(rbac.ActionPull, resource)) } func TestHasWritePerm(t *testing.T) { @@ -164,7 +165,8 @@ func TestHasWritePerm(t *testing.T) { } ctx := NewSecurityContext(robot, pm, policies) - assert.True(t, ctx.HasWritePerm(private.Name)) + resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository) + assert.True(t, ctx.Can(rbac.ActionPush, resource)) } func TestHasAllPerm(t *testing.T) { @@ -180,7 +182,8 @@ func TestHasAllPerm(t *testing.T) { } ctx := NewSecurityContext(robot, pm, policies) - assert.True(t, ctx.HasAllPerm(private.Name)) + resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository) + assert.True(t, ctx.Can(rbac.ActionPushPull, resource)) } func TestGetMyProjects(t *testing.T) { diff --git a/src/common/security/secret/context.go b/src/common/security/secret/context.go index 0155320d85..5dc06137af 100644 --- a/src/common/security/secret/context.go +++ b/src/common/security/secret/context.go @@ -71,34 +71,9 @@ func (s *SecurityContext) IsSolutionUser() bool { return s.IsAuthenticated() } -// HasReadPerm returns true if the corresponding user of the secret -// is jobservice or core service, otherwise returns false -func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - if s.store == nil { - return false - } - return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser -} - -// HasWritePerm returns true if the corresponding user of the secret -// is jobservice or core service, otherwise returns false -func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - if s.store == nil { - return false - } - return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser -} - -// HasAllPerm returns true if the corresponding user of the secret -// is jobservice or core service, otherwise returns false -func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - if s.store == nil { - return false - } - return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser -} - // Can returns whether the user can do action on resource +// returns true if the corresponding user of the secret +// is jobservice or core service, otherwise returns false func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { if s.store == nil { return false @@ -114,7 +89,9 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) { // GetProjectRoles return guest role if has read permission, otherwise return nil func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { roles := []int{} - if s.HasReadPerm(projectIDOrName) { + if s.store != nil && + (s.store.GetUsername(s.secret) == secret.JobserviceUser || + s.store.GetUsername(s.secret) == secret.CoreUser) { roles = append(roles, common.RoleGuest) } return roles diff --git a/src/common/security/secret/context_test.go b/src/common/security/secret/context_test.go index ace3d5dc65..2e743da2b5 100644 --- a/src/common/security/secret/context_test.go +++ b/src/common/security/secret/context_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/secret" "github.com/stretchr/testify/assert" ) @@ -96,9 +97,11 @@ func TestIsSolutionUser(t *testing.T) { } func TestHasReadPerm(t *testing.T) { + readAction := rbac.Action("pull") + resource := rbac.Resource("/project/project_name/repository") // secret store is null context := NewSecurityContext("", nil) - hasReadPerm := context.HasReadPerm("project_name") + hasReadPerm := context.Can(readAction, resource) assert.False(t, hasReadPerm) // invalid secret @@ -106,7 +109,7 @@ func TestHasReadPerm(t *testing.T) { secret.NewStore(map[string]string{ "jobservice_secret": secret.JobserviceUser, })) - hasReadPerm = context.HasReadPerm("project_name") + hasReadPerm = context.Can(readAction, resource) assert.False(t, hasReadPerm) // valid secret, project name @@ -114,11 +117,12 @@ func TestHasReadPerm(t *testing.T) { secret.NewStore(map[string]string{ "jobservice_secret": secret.JobserviceUser, })) - hasReadPerm = context.HasReadPerm("project_name") + hasReadPerm = context.Can(readAction, resource) assert.True(t, hasReadPerm) // valid secret, project ID - hasReadPerm = context.HasReadPerm(1) + resource = rbac.Resource("/project/1/repository") + hasReadPerm = context.Can(readAction, resource) assert.True(t, hasReadPerm) } @@ -128,12 +132,16 @@ func TestHasWritePerm(t *testing.T) { "secret": "username", })) + writeAction := rbac.Action("push") + // project name - hasWritePerm := context.HasWritePerm("project_name") + resource := rbac.Resource("/project/project_name/repository") + hasWritePerm := context.Can(writeAction, resource) assert.False(t, hasWritePerm) // project ID - hasWritePerm = context.HasWritePerm(1) + resource = rbac.Resource("/project/1/repository") + hasWritePerm = context.Can(writeAction, resource) assert.False(t, hasWritePerm) } @@ -143,12 +151,16 @@ func TestHasAllPerm(t *testing.T) { "secret": "username", })) + allAction := rbac.Action("push+pull") + // project name - hasAllPerm := context.HasAllPerm("project_name") + resource := rbac.Resource("/project/project_name/repository") + hasAllPerm := context.Can(allAction, resource) assert.False(t, hasAllPerm) // project ID - hasAllPerm = context.HasAllPerm(1) + resource = rbac.Resource("/project/1/repository") + hasAllPerm = context.Can(allAction, resource) assert.False(t, hasAllPerm) } diff --git a/src/core/api/chart_label.go b/src/core/api/chart_label.go index 2b6f697e04..f3de48c9cf 100644 --- a/src/core/api/chart_label.go +++ b/src/core/api/chart_label.go @@ -6,6 +6,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" ) const ( @@ -45,12 +46,6 @@ func (cla *ChartLabelAPI) Prepare() { } cla.project = existingProject - // Check permission - if !cla.checkPermissions(project) { - cla.SendForbiddenError(errors.New(cla.SecurityCtx.GetUsername())) - return - } - // Check the existence of target chart chartName := cla.GetStringFromPath(nameParam) version := cla.GetStringFromPath(versionParam) @@ -62,8 +57,23 @@ func (cla *ChartLabelAPI) Prepare() { cla.chartFullName = fmt.Sprintf("%s/%s:%s", project, chartName, version) } +func (cla *ChartLabelAPI) requireAccess(action rbac.Action) bool { + resource := rbac.NewProjectNamespace(cla.project.ProjectID).Resource(rbac.ResourceHelmChartVersionLabel) + + if !cla.SecurityCtx.Can(action, resource) { + cla.HandleForbidden(cla.SecurityCtx.GetUsername()) + return false + } + + return true +} + // MarkLabel handles the request of marking label to chart. func (cla *ChartLabelAPI) MarkLabel() { + if !cla.requireAccess(rbac.ActionCreate) { + return + } + l := &models.Label{} cla.DecodeJSONReq(l) @@ -83,6 +93,10 @@ func (cla *ChartLabelAPI) MarkLabel() { // RemoveLabel handles the request of removing label from chart. func (cla *ChartLabelAPI) RemoveLabel() { + if !cla.requireAccess(rbac.ActionDelete) { + return + } + lID, err := cla.GetInt64FromPath(idParam) if err != nil { cla.SendInternalServerError(err) diff --git a/src/core/api/chart_repository.go b/src/core/api/chart_repository.go index c595a790bb..927bf2c093 100644 --- a/src/core/api/chart_repository.go +++ b/src/core/api/chart_repository.go @@ -15,6 +15,7 @@ import ( "github.com/goharbor/harbor/src/core/label" "github.com/goharbor/harbor/src/chartserver" + "github.com/goharbor/harbor/src/common/rbac" hlog "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/config" ) @@ -84,10 +85,35 @@ func (cra *ChartRepositoryAPI) Prepare() { cra.labelManager = &label.BaseManager{} } +func (cra *ChartRepositoryAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool { + if len(subresource) == 0 { + subresource = append(subresource, rbac.ResourceHelmChart) + } + resource := rbac.NewProjectNamespace(cra.namespace).Resource(subresource...) + + if !cra.SecurityCtx.Can(action, resource) { + if !cra.SecurityCtx.IsAuthenticated() { + cra.SendUnAuthorizedError(errors.New("Unauthorized")) + } else { + cra.HandleForbidden(cra.SecurityCtx.GetUsername()) + } + + return false + } + + return true +} + // GetHealthStatus handles GET /api/chartrepo/health func (cra *ChartRepositoryAPI) GetHealthStatus() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelSystem) { + if !cra.SecurityCtx.IsAuthenticated() { + cra.SendUnAuthorizedError(errors.New("Unauthorized")) + return + } + + if !cra.SecurityCtx.IsSysAdmin() { + cra.HandleForbidden(cra.SecurityCtx.GetUsername()) return } @@ -98,7 +124,7 @@ func (cra *ChartRepositoryAPI) GetHealthStatus() { // GetIndexByRepo handles GET /:repo/index.yaml func (cra *ChartRepositoryAPI) GetIndexByRepo() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelRead) { + if !cra.requireAccess(rbac.ActionRead) { return } @@ -109,7 +135,13 @@ func (cra *ChartRepositoryAPI) GetIndexByRepo() { // GetIndex handles GET /index.yaml func (cra *ChartRepositoryAPI) GetIndex() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelSystem) { + if !cra.SecurityCtx.IsAuthenticated() { + cra.SendUnAuthorizedError(errors.New("Unauthorized")) + return + } + + if !cra.SecurityCtx.IsSysAdmin() { + cra.HandleForbidden(cra.SecurityCtx.GetUsername()) return } @@ -136,7 +168,7 @@ func (cra *ChartRepositoryAPI) GetIndex() { // DownloadChart handles GET /:repo/charts/:filename func (cra *ChartRepositoryAPI) DownloadChart() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelRead) { + if !cra.requireAccess(rbac.ActionRead) { return } @@ -147,7 +179,7 @@ func (cra *ChartRepositoryAPI) DownloadChart() { // ListCharts handles GET /api/:repo/charts func (cra *ChartRepositoryAPI) ListCharts() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelRead) { + if !cra.requireAccess(rbac.ActionList) { return } @@ -163,7 +195,7 @@ func (cra *ChartRepositoryAPI) ListCharts() { // ListChartVersions GET /api/:repo/charts/:name func (cra *ChartRepositoryAPI) ListChartVersions() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelRead) { + if !cra.requireAccess(rbac.ActionList, rbac.ResourceHelmChartVersion) { return } @@ -191,7 +223,7 @@ func (cra *ChartRepositoryAPI) ListChartVersions() { // GetChartVersion handles GET /api/:repo/charts/:name/:version func (cra *ChartRepositoryAPI) GetChartVersion() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelRead) { + if !cra.requireAccess(rbac.ActionRead, rbac.ResourceHelmChartVersion) { return } @@ -219,7 +251,7 @@ func (cra *ChartRepositoryAPI) GetChartVersion() { // DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version func (cra *ChartRepositoryAPI) DeleteChartVersion() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelAll) { + if !cra.requireAccess(rbac.ActionDelete, rbac.ResourceHelmChartVersion) { return } @@ -244,7 +276,7 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() { hlog.Debugf("Header of request of uploading chart: %#v, content-len=%d", cra.Ctx.Request.Header, cra.Ctx.Request.ContentLength) // Check access - if !cra.requireAccess(cra.namespace, accessLevelWrite) { + if !cra.requireAccess(rbac.ActionCreate, rbac.ResourceHelmChartVersion) { return } @@ -272,7 +304,7 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() { // UploadChartProvFile handles POST /api/:repo/prov func (cra *ChartRepositoryAPI) UploadChartProvFile() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelWrite) { + if !cra.requireAccess(rbac.ActionCreate) { return } @@ -297,7 +329,7 @@ func (cra *ChartRepositoryAPI) UploadChartProvFile() { // DeleteChart deletes all the chart versions of the specified chart. func (cra *ChartRepositoryAPI) DeleteChart() { // Check access - if !cra.requireAccess(cra.namespace, accessLevelWrite) { + if !cra.requireAccess(rbac.ActionDelete) { return } @@ -365,62 +397,6 @@ func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool { return true } -// Check if the related access match the expected requirement -// If with right access, return true -// If without right access, return false -func (cra *ChartRepositoryAPI) requireAccess(namespace string, accessLevel uint) bool { - if accessLevel == accessLevelPublic { - return true // do nothing - } - - theLevel := accessLevel - // If repo is empty, system admin role must be required - if len(namespace) == 0 { - theLevel = accessLevelSystem - } - - var err error - - switch theLevel { - // Should be system admin role - case accessLevelSystem: - if !cra.SecurityCtx.IsSysAdmin() { - err = errors.New("permission denied: system admin role is required") - } - case accessLevelAll: - if !cra.SecurityCtx.HasAllPerm(namespace) { - err = errors.New("permission denied: project admin or higher role is required") - } - case accessLevelWrite: - if !cra.SecurityCtx.HasWritePerm(namespace) { - err = errors.New("permission denied: developer or higher role is required") - } - case accessLevelRead: - if !cra.SecurityCtx.HasReadPerm(namespace) { - err = errors.New("permission denied: guest or higher role is required") - } - default: - // access rejected for invalid scope - cra.SendForbiddenError(errors.New("unrecognized access scope")) - return false - } - - // Access is not granted, check if user has authenticated - if err != nil { - // Unauthenticated, return 401 - if !cra.SecurityCtx.IsAuthenticated() { - cra.SendUnAuthorizedError(errors.New("Unauthorized")) - return false - } - - // Authenticated, return 403 - cra.SendForbiddenError(err) - return false - } - - return true -} - // formFile is used to represent the uploaded files in the form type formFile struct { // form field key contains the form file diff --git a/src/core/api/chart_repository_test.go b/src/core/api/chart_repository_test.go index 05a3c138fd..d095ca71ff 100644 --- a/src/core/api/chart_repository_test.go +++ b/src/core/api/chart_repository_test.go @@ -17,29 +17,6 @@ var ( crMockServer *httptest.Server ) -// Test access checking -func TestRequireAccess(t *testing.T) { - chartAPI := &ChartRepositoryAPI{} - chartAPI.SecurityCtx = &mockSecurityContext{} - - ns := "library" - if !chartAPI.requireAccess(ns, accessLevelPublic) { - t.Fatal("expect true result (public access level is granted) but got false") - } - if !chartAPI.requireAccess(ns, accessLevelAll) { - t.Fatal("expect true result (admin has all perm) but got false") - } - if !chartAPI.requireAccess(ns, accessLevelRead) { - t.Fatal("expect true result (admin has read perm) but got false") - } - if !chartAPI.requireAccess(ns, accessLevelWrite) { - t.Fatal("expect true result (admin has write perm) but got false") - } - if !chartAPI.requireAccess(ns, accessLevelSystem) { - t.Fatal("expect true result (admin has system perm) but got false") - } -} - func TestIsMultipartFormData(t *testing.T) { req, err := createRequest(http.MethodPost, "/api/chartrepo/charts") if err != nil { @@ -205,7 +182,7 @@ func TestDeleteChart(t *testing.T) { request: &testingRequest{ url: "/api/chartrepo/library/charts/harbor", method: http.MethodDelete, - credential: projDeveloper, + credential: projAdmin, }, code: http.StatusOK, }) @@ -310,21 +287,6 @@ func (msc *mockSecurityContext) IsSolutionUser() bool { return false } -// HasReadPerm returns whether the user has read permission to the project -func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - return msc.Can(rbac.ActionPull, rbac.NewProjectNamespace(projectIDOrName, false).Resource(rbac.ResourceRepository)) -} - -// HasWritePerm returns whether the user has write permission to the project -func (msc *mockSecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - return msc.Can(rbac.ActionPush, rbac.NewProjectNamespace(projectIDOrName, false).Resource(rbac.ResourceRepository)) -} - -// HasAllPerm returns whether the user has all permissions to the project -func (msc *mockSecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - return msc.HasReadPerm(projectIDOrName) && msc.HasWritePerm(projectIDOrName) -} - // Can returns whether the user can do action on resource func (msc *mockSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { namespace, err := resource.GetNamespace() diff --git a/src/core/api/label.go b/src/core/api/label.go index 1bf0b18c24..702e3cc371 100644 --- a/src/core/api/label.go +++ b/src/core/api/label.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/replication" "github.com/goharbor/harbor/src/replication/core" rep_models "github.com/goharbor/harbor/src/replication/models" @@ -65,15 +66,36 @@ func (l *LabelAPI) Prepare() { return } - if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() || - label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) { - l.HandleForbidden(l.SecurityCtx.GetUsername()) - return - } l.label = label } } +func (l *LabelAPI) requireAccess(label *models.Label, action rbac.Action, subresources ...rbac.Resource) bool { + var hasPermission bool + + switch label.Scope { + case common.LabelScopeGlobal: + hasPermission = l.SecurityCtx.IsSysAdmin() + case common.LabelScopeProject: + if len(subresources) == 0 { + subresources = append(subresources, rbac.ResourceLabel) + } + resource := rbac.NewProjectNamespace(label.ProjectID).Resource(subresources...) + hasPermission = l.SecurityCtx.Can(action, resource) + } + + if !hasPermission { + if !l.SecurityCtx.IsAuthenticated() { + l.HandleUnauthorized() + } else { + l.HandleForbidden(l.SecurityCtx.GetUsername()) + } + return false + } + + return true +} + // Post creates a label func (l *LabelAPI) Post() { label := &models.Label{} @@ -82,10 +104,6 @@ func (l *LabelAPI) Post() { switch label.Scope { case common.LabelScopeGlobal: - if !l.SecurityCtx.IsSysAdmin() { - l.HandleForbidden(l.SecurityCtx.GetUsername()) - return - } label.ProjectID = 0 case common.LabelScopeProject: exist, err := l.ProjectMgr.Exists(label.ProjectID) @@ -98,10 +116,10 @@ func (l *LabelAPI) Post() { l.HandleNotFound(fmt.Sprintf("project %d not found", label.ProjectID)) return } - if !l.SecurityCtx.HasAllPerm(label.ProjectID) { - l.HandleForbidden(l.SecurityCtx.GetUsername()) - return - } + } + + if !l.requireAccess(label, rbac.ActionCreate) { + return } labels, err := dao.ListLabels(&models.LabelQuery{ @@ -147,15 +165,8 @@ func (l *LabelAPI) Get() { return } - if label.Scope == common.LabelScopeProject { - if !l.SecurityCtx.HasReadPerm(label.ProjectID) { - if !l.SecurityCtx.IsAuthenticated() { - l.HandleUnauthorized() - return - } - l.HandleForbidden(l.SecurityCtx.GetUsername()) - return - } + if !l.requireAccess(label, rbac.ActionRead) { + return } l.Data["json"] = label @@ -189,7 +200,8 @@ func (l *LabelAPI) List() { return } - if !l.SecurityCtx.HasReadPerm(projectID) { + resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceLabel) + if !l.SecurityCtx.Can(rbac.ActionList, resource) { if !l.SecurityCtx.IsAuthenticated() { l.HandleUnauthorized() return @@ -221,6 +233,10 @@ func (l *LabelAPI) List() { // Put updates the label func (l *LabelAPI) Put() { + if !l.requireAccess(l.label, rbac.ActionUpdate) { + return + } + label := &models.Label{} l.DecodeJSONReq(label) @@ -259,6 +275,10 @@ func (l *LabelAPI) Put() { // Delete the label func (l *LabelAPI) Delete() { + if !l.requireAccess(l.label, rbac.ActionDelete) { + return + } + id := l.label.ID if err := dao.DeleteResourceLabelByLabel(id); err != nil { l.HandleInternalServerError(fmt.Sprintf("failed to delete resource label mappings of label %d: %v", id, err)) @@ -272,11 +292,6 @@ func (l *LabelAPI) Delete() { // ListResources lists the resources that the label is referenced by func (l *LabelAPI) ListResources() { - if !l.SecurityCtx.IsAuthenticated() { - l.HandleUnauthorized() - return - } - id, err := l.GetInt64FromPath(":id") if err != nil || id <= 0 { l.HandleBadRequest("invalid label ID") @@ -294,9 +309,7 @@ func (l *LabelAPI) ListResources() { return } - if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() || - label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) { - l.HandleForbidden(l.SecurityCtx.GetUsername()) + if !l.requireAccess(label, rbac.ActionList, rbac.ResourceLabelResource) { return } diff --git a/src/core/api/label_resource.go b/src/core/api/label_resource.go index 4fc96637b0..807b11029d 100644 --- a/src/core/api/label_resource.go +++ b/src/core/api/label_resource.go @@ -22,23 +22,6 @@ func (lra *LabelResourceAPI) Prepare() { lra.labelManager = &label.BaseManager{} } -func (lra *LabelResourceAPI) checkPermissions(project string) bool { - if lra.Ctx.Request.Method == http.MethodPost || - lra.Ctx.Request.Method == http.MethodDelete { - if lra.SecurityCtx.HasWritePerm(project) { - return true - } - } - - if lra.Ctx.Request.Method == http.MethodGet { - if lra.SecurityCtx.HasReadPerm(project) { - return true - } - } - - return false -} - func (lra *LabelResourceAPI) getLabelsOfResource(rType string, rIDOrName interface{}) { labels, err := lra.labelManager.GetLabelsOfResource(rType, rIDOrName) if err != nil { diff --git a/src/core/api/metadata.go b/src/core/api/metadata.go index 090b7ca5c1..146d6de094 100644 --- a/src/core/api/metadata.go +++ b/src/core/api/metadata.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/promgr/metamgr" ) @@ -72,24 +73,6 @@ func (m *MetadataAPI) Prepare() { m.project = project - switch m.Ctx.Request.Method { - case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete: - if !(m.Ctx.Request.Method == http.MethodGet && project.IsPublic()) { - if !m.SecurityCtx.IsAuthenticated() { - m.HandleUnauthorized() - return - } - if !m.SecurityCtx.HasReadPerm(project.ProjectID) { - m.HandleForbidden(m.SecurityCtx.GetUsername()) - return - } - } - default: - log.Debugf("%s method not allowed", m.Ctx.Request.Method) - m.RenderError(http.StatusMethodNotAllowed, "") - return - } - name := m.GetStringFromPath(":name") if len(name) > 0 { m.name = name @@ -105,8 +88,27 @@ func (m *MetadataAPI) Prepare() { } } +func (m *MetadataAPI) requireAccess(action rbac.Action) bool { + resource := rbac.NewProjectNamespace(m.project.ProjectID).Resource(rbac.ResourceMetadata) + + if !m.SecurityCtx.Can(action, resource) { + if !m.SecurityCtx.IsAuthenticated() { + m.HandleUnauthorized() + } else { + m.HandleForbidden(m.SecurityCtx.GetUsername()) + } + return false + } + + return true +} + // Get ... func (m *MetadataAPI) Get() { + if !m.requireAccess(rbac.ActionRead) { + return + } + var metas map[string]string var err error if len(m.name) > 0 { @@ -125,6 +127,10 @@ func (m *MetadataAPI) Get() { // Post ... func (m *MetadataAPI) Post() { + if !m.requireAccess(rbac.ActionCreate) { + return + } + var metas map[string]string m.DecodeJSONReq(&metas) @@ -161,6 +167,10 @@ func (m *MetadataAPI) Post() { // Put ... func (m *MetadataAPI) Put() { + if !m.requireAccess(rbac.ActionUpdate) { + return + } + var metas map[string]string m.DecodeJSONReq(&metas) @@ -188,6 +198,10 @@ func (m *MetadataAPI) Put() { // Delete ... func (m *MetadataAPI) Delete() { + if !m.requireAccess(rbac.ActionDelete) { + return + } + if err := m.metaMgr.Delete(m.project.ProjectID, m.name); err != nil { m.HandleInternalServerError(fmt.Sprintf("failed to delete metadata %s of project %d: %v", m.name, m.project.ProjectID, err)) return diff --git a/src/core/api/project.go b/src/core/api/project.go index ac9ce2ce0e..7771b7d774 100644 --- a/src/core/api/project.go +++ b/src/core/api/project.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils" errutil "github.com/goharbor/harbor/src/common/utils/error" "github.com/goharbor/harbor/src/common/utils/log" @@ -77,6 +78,25 @@ func (p *ProjectAPI) Prepare() { } } +func (p *ProjectAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool { + if len(subresource) == 0 { + subresource = append(subresource, rbac.ResourceSelf) + } + resource := rbac.NewProjectNamespace(p.project.ProjectID).Resource(subresource...) + + if !p.SecurityCtx.Can(action, resource) { + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + } else { + p.HandleForbidden(p.SecurityCtx.GetUsername()) + } + + return false + } + + return true +} + // Post ... func (p *ProjectAPI) Post() { if !p.SecurityCtx.IsAuthenticated() { @@ -187,16 +207,8 @@ func (p *ProjectAPI) Head() { // Get ... func (p *ProjectAPI) Get() { - if !p.project.IsPublic() { - if !p.SecurityCtx.IsAuthenticated() { - p.HandleUnauthorized() - return - } - - if !p.SecurityCtx.HasReadPerm(p.project.ProjectID) { - p.HandleForbidden(p.SecurityCtx.GetUsername()) - return - } + if !p.requireAccess(rbac.ActionRead) { + return } p.populateProperties(p.project) @@ -207,13 +219,7 @@ func (p *ProjectAPI) Get() { // Delete ... func (p *ProjectAPI) Delete() { - if !p.SecurityCtx.IsAuthenticated() { - p.HandleUnauthorized() - return - } - - if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) { - p.HandleForbidden(p.SecurityCtx.GetUsername()) + if !p.requireAccess(rbac.ActionDelete) { return } @@ -248,13 +254,7 @@ func (p *ProjectAPI) Delete() { // Deletable ... func (p *ProjectAPI) Deletable() { - if !p.SecurityCtx.IsAuthenticated() { - p.HandleUnauthorized() - return - } - - if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) { - p.HandleForbidden(p.SecurityCtx.GetUsername()) + if !p.requireAccess(rbac.ActionDelete) { return } @@ -433,13 +433,7 @@ func (p *ProjectAPI) populateProperties(project *models.Project) { // Put ... func (p *ProjectAPI) Put() { - if !p.SecurityCtx.IsAuthenticated() { - p.HandleUnauthorized() - return - } - - if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) { - p.HandleForbidden(p.SecurityCtx.GetUsername()) + if !p.requireAccess(rbac.ActionUpdate) { return } @@ -458,13 +452,7 @@ func (p *ProjectAPI) Put() { // Logs ... func (p *ProjectAPI) Logs() { - if !p.SecurityCtx.IsAuthenticated() { - p.HandleUnauthorized() - return - } - - if !p.SecurityCtx.HasReadPerm(p.project.ProjectID) { - p.HandleForbidden(p.SecurityCtx.GetUsername()) + if !p.requireAccess(rbac.ActionList, rbac.ResourceLog) { return } diff --git a/src/core/api/projectmember.go b/src/core/api/projectmember.go index d94ae1e0f1..f4e52d6726 100644 --- a/src/core/api/projectmember.go +++ b/src/core/api/projectmember.go @@ -24,6 +24,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao/project" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/auth" ) @@ -73,12 +74,6 @@ func (pma *ProjectMemberAPI) Prepare() { } pma.project = project - if !(pma.Ctx.Input.IsGet() && pma.SecurityCtx.HasReadPerm(pid) || - pma.SecurityCtx.HasAllPerm(pid)) { - pma.HandleForbidden(pma.SecurityCtx.GetUsername()) - return - } - pmid, err := pma.GetInt64FromPath(":pmid") if err != nil { log.Warningf("Failed to get pmid from path, error %v", err) @@ -90,6 +85,22 @@ func (pma *ProjectMemberAPI) Prepare() { pma.id = int(pmid) } +func (pma *ProjectMemberAPI) requireAccess(action rbac.Action) bool { + resource := rbac.NewProjectNamespace(pma.project.ProjectID).Resource(rbac.ResourceMember) + + if !pma.SecurityCtx.Can(action, resource) { + if !pma.SecurityCtx.IsAuthenticated() { + pma.HandleUnauthorized() + } else { + pma.HandleForbidden(pma.SecurityCtx.GetUsername()) + } + + return false + } + + return true +} + // Get ... func (pma *ProjectMemberAPI) Get() { projectID := pma.project.ProjectID @@ -97,6 +108,9 @@ func (pma *ProjectMemberAPI) Get() { queryMember.ProjectID = projectID pma.Data["json"] = make([]models.Member, 0) if pma.id == 0 { + if !pma.requireAccess(rbac.ActionList) { + return + } entityname := pma.GetString("entityname") memberList, err := project.SearchMemberByName(projectID, entityname) if err != nil { @@ -119,6 +133,10 @@ func (pma *ProjectMemberAPI) Get() { pma.HandleNotFound(fmt.Sprintf("The project member does not exit, pmid:%v", pma.id)) return } + + if !pma.requireAccess(rbac.ActionRead) { + return + } pma.Data["json"] = memberList[0] } pma.ServeJSON() @@ -126,6 +144,9 @@ func (pma *ProjectMemberAPI) Get() { // Post ... Add a project member func (pma *ProjectMemberAPI) Post() { + if !pma.requireAccess(rbac.ActionCreate) { + return + } projectID := pma.project.ProjectID var request models.MemberReq pma.DecodeJSONReq(&request) @@ -156,6 +177,9 @@ func (pma *ProjectMemberAPI) Post() { // Put ... Update an exist project member func (pma *ProjectMemberAPI) Put() { + if !pma.requireAccess(rbac.ActionUpdate) { + return + } pid := pma.project.ProjectID pmID := pma.id var req models.Member @@ -173,6 +197,9 @@ func (pma *ProjectMemberAPI) Put() { // Delete ... func (pma *ProjectMemberAPI) Delete() { + if !pma.requireAccess(rbac.ActionDelete) { + return + } pmid := pma.id err := project.DeleteProjectMemberByID(pmid) if err != nil { diff --git a/src/core/api/replication_job.go b/src/core/api/replication_job.go index c871f6f142..32ee1a7b5f 100644 --- a/src/core/api/replication_job.go +++ b/src/core/api/replication_job.go @@ -24,6 +24,7 @@ import ( common_http "github.com/goharbor/harbor/src/common/http" common_job "github.com/goharbor/harbor/src/common/job" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" api_models "github.com/goharbor/harbor/src/core/api/models" "github.com/goharbor/harbor/src/core/utils" @@ -80,7 +81,8 @@ func (ra *RepJobAPI) List() { return } - if !ra.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) { + resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob) + if !ra.SecurityCtx.Can(rbac.ActionList, resource) { ra.HandleForbidden(ra.SecurityCtx.GetUsername()) return } @@ -190,7 +192,8 @@ func (ra *RepJobAPI) GetLog() { return } - if !ra.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) { + resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob) + if !ra.SecurityCtx.Can(rbac.ActionRead, resource) { ra.HandleForbidden(ra.SecurityCtx.GetUsername()) return } diff --git a/src/core/api/replication_policy.go b/src/core/api/replication_policy.go index 642cf35d70..ac45fbdad8 100644 --- a/src/core/api/replication_policy.go +++ b/src/core/api/replication_policy.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" api_models "github.com/goharbor/harbor/src/core/api/models" "github.com/goharbor/harbor/src/core/promgr" @@ -63,7 +64,8 @@ func (pa *RepPolicyAPI) Get() { return } - if !pa.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) { + resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication) + if !pa.SecurityCtx.Can(rbac.ActionRead, resource) { pa.HandleForbidden(pa.SecurityCtx.GetUsername()) return } @@ -105,7 +107,8 @@ func (pa *RepPolicyAPI) List() { if result != nil { total = result.Total for _, policy := range result.Policies { - if !pa.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) { + resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication) + if !pa.SecurityCtx.Can(rbac.ActionRead, resource) { continue } ply, err := convertFromRepPolicy(pa.ProjectMgr, *policy) diff --git a/src/core/api/repository.go b/src/core/api/repository.go index 4633653358..a96fdc8986 100644 --- a/src/core/api/repository.go +++ b/src/core/api/repository.go @@ -30,6 +30,7 @@ import ( "github.com/goharbor/harbor/src/common/dao" commonhttp "github.com/goharbor/harbor/src/common/http" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils/clair" "github.com/goharbor/harbor/src/common/utils/log" @@ -131,7 +132,8 @@ func (ra *RepositoryAPI) Get() { return } - if !ra.SecurityCtx.HasReadPerm(projectID) { + resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository) + if !ra.SecurityCtx.Can(rbac.ActionList, resource) { if !ra.SecurityCtx.IsAuthenticated() { ra.HandleUnauthorized() return @@ -247,7 +249,8 @@ func (ra *RepositoryAPI) Delete() { return } - if !ra.SecurityCtx.HasAllPerm(projectName) { + resource := rbac.NewProjectNamespace(project.ProjectID).Resource(rbac.ResourceRepository) + if !ra.SecurityCtx.Can(rbac.ActionDelete, resource) { ra.HandleForbidden(ra.SecurityCtx.GetUsername()) return } @@ -393,7 +396,8 @@ func (ra *RepositoryAPI) GetTag() { return } project, _ := utils.ParseRepository(repository) - if !ra.SecurityCtx.HasReadPerm(project) { + resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTag) + if !ra.SecurityCtx.Can(rbac.ActionRead, resource) { if !ra.SecurityCtx.IsAuthenticated() { ra.HandleUnauthorized() return @@ -488,14 +492,16 @@ func (ra *RepositoryAPI) Retag() { } // Check whether user has read permission to source project - if !ra.SecurityCtx.HasReadPerm(srcImage.Project) { + srcResource := rbac.NewProjectNamespace(srcImage.Project).Resource(rbac.ResourceRepository) + if !ra.SecurityCtx.Can(rbac.ActionPull, srcResource) { log.Errorf("user has no read permission to project '%s'", srcImage.Project) ra.HandleForbidden(fmt.Sprintf("%s has no read permission to project %s", ra.SecurityCtx.GetUsername(), srcImage.Project)) return } // Check whether user has write permission to target project - if !ra.SecurityCtx.HasWritePerm(project) { + destResource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository) + if !ra.SecurityCtx.Can(rbac.ActionPush, destResource) { log.Errorf("user has no write permission to project '%s'", project) ra.HandleForbidden(fmt.Sprintf("%s has no write permission to project %s", ra.SecurityCtx.GetUsername(), project)) return @@ -533,7 +539,8 @@ func (ra *RepositoryAPI) GetTags() { return } - if !ra.SecurityCtx.HasReadPerm(projectName) { + resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTag) + if !ra.SecurityCtx.Can(rbac.ActionList, resource) { if !ra.SecurityCtx.IsAuthenticated() { ra.HandleUnauthorized() return @@ -741,7 +748,8 @@ func (ra *RepositoryAPI) GetManifests() { return } - if !ra.SecurityCtx.HasReadPerm(projectName) { + resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagManifest) + if !ra.SecurityCtx.Can(rbac.ActionRead, resource) { if !ra.SecurityCtx.IsAuthenticated() { ra.HandleUnauthorized() return @@ -872,7 +880,8 @@ func (ra *RepositoryAPI) Put() { } project, _ := utils.ParseRepository(name) - if !ra.SecurityCtx.HasWritePerm(project) { + resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository) + if !ra.SecurityCtx.Can(rbac.ActionUpdate, resource) { ra.HandleForbidden(ra.SecurityCtx.GetUsername()) return } @@ -906,7 +915,8 @@ func (ra *RepositoryAPI) GetSignatures() { return } - if !ra.SecurityCtx.HasReadPerm(projectName) { + resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository) + if !ra.SecurityCtx.Can(rbac.ActionRead, resource) { if !ra.SecurityCtx.IsAuthenticated() { ra.HandleUnauthorized() return @@ -949,7 +959,9 @@ func (ra *RepositoryAPI) ScanImage() { ra.HandleUnauthorized() return } - if !ra.SecurityCtx.HasAllPerm(projectName) { + + resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob) + if !ra.SecurityCtx.Can(rbac.ActionCreate, resource) { ra.HandleForbidden(ra.SecurityCtx.GetUsername()) return } @@ -980,7 +992,9 @@ func (ra *RepositoryAPI) VulnerabilityDetails() { return } project, _ := utils.ParseRepository(repository) - if !ra.SecurityCtx.HasReadPerm(project) { + + resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTagVulnerability) + if !ra.SecurityCtx.Can(rbac.ActionList, resource) { if !ra.SecurityCtx.IsAuthenticated() { ra.HandleUnauthorized() return diff --git a/src/core/api/repository_label.go b/src/core/api/repository_label.go index 547fe5c82c..5b658e4241 100644 --- a/src/core/api/repository_label.go +++ b/src/core/api/repository_label.go @@ -22,7 +22,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/common/rbac" coreutils "github.com/goharbor/harbor/src/core/utils" ) @@ -45,12 +45,6 @@ func (r *RepositoryLabelAPI) Prepare() { } repository := r.GetString(":splat") - project, _ := utils.ParseRepository(repository) - if !r.checkPermissions(project) { - r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername())) - return - } - repo, err := dao.GetRepositoryByName(repository) if err != nil { r.SendInternalServerError(fmt.Errorf("failed to get repository %s: %v", repository, err)) @@ -77,25 +71,6 @@ func (r *RepositoryLabelAPI) Prepare() { r.tag = tag } - if r.Ctx.Request.Method == http.MethodPost { - p, err := r.ProjectMgr.Get(project) - if err != nil { - r.SendInternalServerError(err) - return - } - - l := &models.Label{} - r.DecodeJSONReq(l) - - label, ok := r.validate(l.ID, p.ProjectID) - if !ok { - return - } - r.label = label - - return - } - if r.Ctx.Request.Method == http.MethodDelete { labelID, err := r.GetInt64FromPath(":id") if err != nil { @@ -112,13 +87,59 @@ func (r *RepositoryLabelAPI) Prepare() { } } +func (r *RepositoryLabelAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool { + if len(subresource) == 0 { + subresource = append(subresource, rbac.ResourceRepositoryLabel) + } + resource := rbac.NewProjectNamespace(r.repository.ProjectID).Resource(rbac.ResourceRepositoryLabel) + + if !r.SecurityCtx.Can(action, resource) { + if !r.SecurityCtx.IsAuthenticated() { + r.SendUnAuthorizedError(errors.New("UnAuthorized")) + } else { + r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername())) + } + + return false + } + + return true +} + +func (r *RepositoryLabelAPI) isValidLabelReq() bool { + p, err := r.ProjectMgr.Get(r.repository.ProjectID) + if err != nil { + r.SendInternalServerError(err) + return false + } + + l := &models.Label{} + r.DecodeJSONReq(l) + + label, ok := r.validate(l.ID, p.ProjectID) + if !ok { + return false + } + r.label = label + + return true +} + // GetOfImage returns labels of an image func (r *RepositoryLabelAPI) GetOfImage() { + if !r.requireAccess(rbac.ActionList, rbac.ResourceRepositoryTagLabel) { + return + } + r.getLabelsOfResource(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag)) } // AddToImage adds the label to an image func (r *RepositoryLabelAPI) AddToImage() { + if !r.requireAccess(rbac.ActionCreate, rbac.ResourceRepositoryTagLabel) || !r.isValidLabelReq() { + return + } + rl := &models.ResourceLabel{ LabelID: r.label.ID, ResourceType: common.ResourceTypeImage, @@ -129,17 +150,29 @@ func (r *RepositoryLabelAPI) AddToImage() { // RemoveFromImage removes the label from an image func (r *RepositoryLabelAPI) RemoveFromImage() { + if !r.requireAccess(rbac.ActionDelete, rbac.ResourceRepositoryTagLabel) { + return + } + r.removeLabelFromResource(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag), r.label.ID) } // GetOfRepository returns labels of a repository func (r *RepositoryLabelAPI) GetOfRepository() { + if !r.requireAccess(rbac.ActionList) { + return + } + r.getLabelsOfResource(common.ResourceTypeRepository, r.repository.RepositoryID) } // AddToRepository adds the label to a repository func (r *RepositoryLabelAPI) AddToRepository() { + if !r.requireAccess(rbac.ActionCreate) || !r.isValidLabelReq() { + return + } + rl := &models.ResourceLabel{ LabelID: r.label.ID, ResourceType: common.ResourceTypeRepository, @@ -150,6 +183,10 @@ func (r *RepositoryLabelAPI) AddToRepository() { // RemoveFromRepository removes the label from a repository func (r *RepositoryLabelAPI) RemoveFromRepository() { + if !r.requireAccess(rbac.ActionDelete) { + return + } + r.removeLabelFromResource(common.ResourceTypeRepository, r.repository.RepositoryID, r.label.ID) } diff --git a/src/core/api/robot.go b/src/core/api/robot.go index 920ce312ca..03850f90f0 100644 --- a/src/core/api/robot.go +++ b/src/core/api/robot.go @@ -16,12 +16,14 @@ package api import ( "fmt" + "net/http" + "strconv" + "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/token" - "net/http" - "strconv" ) // RobotAPI ... @@ -83,17 +85,24 @@ func (r *RobotAPI) Prepare() { r.robot = robot } +} - if !(r.Ctx.Input.IsGet() && r.SecurityCtx.HasReadPerm(pid) || - r.SecurityCtx.HasAllPerm(pid)) { +func (r *RobotAPI) requireAccess(action rbac.Action) bool { + resource := rbac.NewProjectNamespace(r.project.ProjectID).Resource(rbac.ResourceRobot) + if !r.SecurityCtx.Can(action, resource) { r.HandleForbidden(r.SecurityCtx.GetUsername()) - return + return false } + return true } // Post ... func (r *RobotAPI) Post() { + if !r.requireAccess(rbac.ActionCreate) { + return + } + var robotReq models.RobotReq r.DecodeJSONReq(&robotReq) createdName := common.RobotPrefix + robotReq.Name @@ -147,6 +156,10 @@ func (r *RobotAPI) Post() { // List list all the robots of a project func (r *RobotAPI) List() { + if !r.requireAccess(rbac.ActionList) { + return + } + query := models.RobotQuery{ ProjectID: r.project.ProjectID, } @@ -171,6 +184,10 @@ func (r *RobotAPI) List() { // Get get robot by id func (r *RobotAPI) Get() { + if !r.requireAccess(rbac.ActionRead) { + return + } + id, err := r.GetInt64FromPath(":id") if err != nil || id <= 0 { r.HandleBadRequest(fmt.Sprintf("invalid robot ID: %s", r.GetStringFromPath(":id"))) @@ -193,6 +210,10 @@ func (r *RobotAPI) Get() { // Put disable or enable a robot account func (r *RobotAPI) Put() { + if !r.requireAccess(rbac.ActionUpdate) { + return + } + var robotReq models.RobotReq r.DecodeJSONReqAndValidate(&robotReq) r.robot.Disabled = robotReq.Disabled @@ -206,6 +227,10 @@ func (r *RobotAPI) Put() { // Delete delete robot by id func (r *RobotAPI) Delete() { + if !r.requireAccess(rbac.ActionDelete) { + return + } + if err := dao.DeleteRobot(r.robot.ID); err != nil { r.HandleInternalServerError(fmt.Sprintf("failed to delete robot %d: %v", r.robot.ID, err)) return diff --git a/src/core/api/scan_job.go b/src/core/api/scan_job.go index 9f3110cc49..9489d57edc 100644 --- a/src/core/api/scan_job.go +++ b/src/core/api/scan_job.go @@ -17,6 +17,7 @@ package api import ( "github.com/goharbor/harbor/src/common/dao" common_http "github.com/goharbor/harbor/src/common/http" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/utils" @@ -54,7 +55,9 @@ func (sj *ScanJobAPI) Prepare() { sj.CustomAbort(http.StatusInternalServerError, "Failed to get Job data") } projectName := strings.SplitN(data.Repository, "/", 2)[0] - if !sj.SecurityCtx.HasReadPerm(projectName) { + + resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob) + if !sj.SecurityCtx.Can(rbac.ActionRead, resource) { log.Errorf("User does not have read permission for project: %s", projectName) sj.HandleForbidden(sj.SecurityCtx.GetUsername()) } diff --git a/src/core/service/token/creator.go b/src/core/service/token/creator.go index c847980019..fbd2297832 100644 --- a/src/core/service/token/creator.go +++ b/src/core/service/token/creator.go @@ -22,6 +22,7 @@ import ( "github.com/docker/distribution/registry/auth/token" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/config" @@ -158,24 +159,25 @@ func (rep repositoryFilter) filter(ctx security.Context, pm promgr.ProjectManage if err != nil { return err } - project := img.namespace + projectName := img.namespace permission := "" - exist, err := pm.Exists(project) + exist, err := pm.Exists(projectName) if err != nil { return err } if !exist { - log.Debugf("project %s does not exist, set empty permission", project) + log.Debugf("project %s does not exist, set empty permission", projectName) a.Actions = []string{} return nil } - if ctx.HasAllPerm(project) { + resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository) + if ctx.Can(rbac.ActionPush, resource) && ctx.Can(rbac.ActionPull, resource) { permission = "RWM" - } else if ctx.HasWritePerm(project) { + } else if ctx.Can(rbac.ActionPush, resource) { permission = "RW" - } else if ctx.HasReadPerm(project) { + } else if ctx.Can(rbac.ActionPull, resource) { permission = "R" } diff --git a/src/core/service/token/token_test.go b/src/core/service/token/token_test.go index 3869eb1050..7337357fdd 100644 --- a/src/core/service/token/token_test.go +++ b/src/core/service/token/token_test.go @@ -252,15 +252,6 @@ func (f *fakeSecurityContext) IsSysAdmin() bool { func (f *fakeSecurityContext) IsSolutionUser() bool { return false } -func (f *fakeSecurityContext) HasReadPerm(projectIDOrName interface{}) bool { - return false -} -func (f *fakeSecurityContext) HasWritePerm(projectIDOrName interface{}) bool { - return false -} -func (f *fakeSecurityContext) HasAllPerm(projectIDOrName interface{}) bool { - return false -} func (f *fakeSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool { return false } From 1b80e79ec447e02589c026b0ba01b2187fb22c19 Mon Sep 17 00:00:00 2001 From: Yogi_Wang Date: Fri, 1 Feb 2019 17:00:01 +0800 Subject: [PATCH 34/45] fixPermissionIssue Signed-off-by: Yogi_Wang --- .../list-replication-rule.component.ts | 6 +++-- .../project-policy-config.component.spec.ts | 10 +++++++- .../repository-gridview.component.html | 4 ++-- .../lib/src/service/permission.service.ts | 5 +++- .../lib/src/tag/tag-detail.component.html | 6 ++--- src/portal/lib/src/tag/tag.component.spec.ts | 2 +- src/portal/lib/src/tag/tag.component.ts | 24 +++++-------------- .../result-grid.component.spec.ts | 15 +++++++++++- .../result-grid.component.ts | 2 ++ 9 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts index f593ff25ad..91c8820c91 100644 --- a/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts +++ b/src/portal/lib/src/list-replication-rule/list-replication-rule.component.ts @@ -179,8 +179,10 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges { } selectRule(rule: ReplicationRule): void { - this.selectedId = rule.id || ""; - this.selectOne.emit(rule); + if (rule) { + this.selectedId = rule.id || ""; + this.selectOne.emit(rule); + } } redirectTo(rule: ReplicationRule): void { diff --git a/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts b/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts index 4983c9f134..8b82c8e05d 100644 --- a/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts +++ b/src/portal/lib/src/project-policy-config/project-policy-config.component.spec.ts @@ -9,14 +9,17 @@ import { SERVICE_CONFIG, IServiceConfig} from '../service.config'; import { SystemInfo } from '../service/interface'; import { Project } from './project'; import { UserPermissionService, UserPermissionDefaultService } from '../service/permission.service'; +import { USERSTATICPERMISSION } from '../service/permission-static'; +import { of } from 'rxjs'; describe('ProjectPolicyConfigComponent', () => { let systemInfoService: SystemInfoService; let projectPolicyService: ProjectService; + let userPermissionService: UserPermissionService; let spySystemInfo: jasmine.Spy; let spyProjectPolicies: jasmine.Spy; - + let mockHasChangeConfigRole: boolean = true; let mockSystemInfo: SystemInfo[] = [ { 'with_clair': true, @@ -121,6 +124,11 @@ describe('ProjectPolicyConfigComponent', () => { spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo[0])); spyProjectPolicies = spyOn(projectPolicyService, 'getProject').and.returnValues(Promise.resolve(mockPorjectPolicies[0])); + userPermissionService = fixture.debugElement.injector.get(UserPermissionService); + spyOn(userPermissionService, "getPermission") + .withArgs(component.projectId, + USERSTATICPERMISSION.CONFIGURATION.KEY, USERSTATICPERMISSION.CONFIGURATION.VALUE.UPDATE ) + .and.returnValue(of(mockHasChangeConfigRole)); fixture.detectChanges(); }); diff --git a/src/portal/lib/src/repository-gridview/repository-gridview.component.html b/src/portal/lib/src/repository-gridview/repository-gridview.component.html index fc1fecde0f..52f46561e4 100644 --- a/src/portal/lib/src/repository-gridview/repository-gridview.component.html +++ b/src/portal/lib/src/repository-gridview/repository-gridview.component.html @@ -87,7 +87,7 @@