diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a8b7ddcb2..dba24f3be 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 dbefae98d..c4c14f703 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 34ebe86eb..1515f65e4 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 c37a6ebef..1307fe4c3 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 ada5868e6..0ae5bc1a2 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 4563b41bd..6aa1113ed 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 843686f3b..45d91dcfe 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 45af1b100..740b23663 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 86ad4a772..72840d36b 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 ab4d11f4a..d56433214 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 6d9be17b1..4bcb1f009 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 5f1f59cc5..277b31086 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 b3ee72541..fb1cfebce 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 e309a9560..924c8c294 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 1fe068613..d793ffe72 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 f6f2cb04d..8fc12ae69 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"` +}