From 3fbe34ac40aeeb42aa43a37175a814569c4c6a13 Mon Sep 17 00:00:00 2001 From: wang yan Date: Wed, 18 Oct 2023 14:12:31 +0800 Subject: [PATCH] add permission api The permission api targets to return the full set of permissons for robot to use. And only system and project admin have the access Signed-off-by: wang yan --- api/v2.0/swagger.yaml | 39 +++++- src/common/rbac/const.go | 149 +++++++++++++++++++++++ src/controller/member/controller.go | 15 ++- src/controller/member/controller_test.go | 8 ++ src/pkg/cached/project/redis/manager.go | 4 + src/pkg/project/dao/dao.go | 35 ++++-- src/pkg/project/manager.go | 16 ++- src/server/v2.0/handler/handler.go | 1 + src/server/v2.0/handler/permissions.go | 109 +++++++++++++++++ src/testing/pkg/project/manager.go | 26 ++++ 10 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 src/server/v2.0/handler/permissions.go diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 0e4a6d503bf..b9b11074470 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -6140,7 +6140,31 @@ paths: '401': $ref: '#/responses/401' '500': - $ref: '#/responses/500' + $ref: '#/responses/500' + + /permissions: + get: + summary: Get system or project level permissions info. + operationId: getPermissions + description: | + This endpoint is for retrieving resource and action info that only provides for admin user(system admin and project admin). + tags: + - permissions + parameters: + - $ref: '#/parameters/requestId' + responses: + '200': + description: Get permissions successfully. + schema: + $ref: '#/definitions/Permissions' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + $ref: '#/responses/404' + '500': + $ref: '#/responses/500' parameters: query: @@ -9397,6 +9421,19 @@ definitions: action: type: string description: The permission action + Permissions: + type: object + properties: + system: + type: array + description: The system level permissions + items: + $ref: '#/definitions/Permission' + project: + type: array + description: The project level permissions + items: + $ref: '#/definitions/Permission' OIDCCliSecretReq: type: object properties: diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index 282779c4905..32dec240b3a 100644 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -14,6 +14,8 @@ package rbac +import "github.com/goharbor/harbor/src/pkg/permission/types" + // const action variables const ( ActionAll = Action("*") // action match any other actions @@ -77,3 +79,150 @@ const ( ResourceJobServiceMonitor = Resource("jobservice-monitor") ResourceSecurityHub = Resource("security-hub") ) + +var ( + PoliciesMap = map[string][]*types.Policy{ + "System": { + {Resource: ResourceAuditLog, Action: ActionList}, + + {Resource: ResourcePreatPolicy, Action: ActionRead}, + {Resource: ResourcePreatPolicy, Action: ActionCreate}, + {Resource: ResourcePreatPolicy, Action: ActionDelete}, + {Resource: ResourcePreatPolicy, Action: ActionList}, + {Resource: ResourcePreatPolicy, Action: ActionUpdate}, + + {Resource: ResourceProject, Action: ActionList}, + {Resource: ResourceProject, Action: ActionCreate}, + + {Resource: ResourceReplicationPolicy, Action: ActionRead}, + {Resource: ResourceReplicationPolicy, Action: ActionCreate}, + {Resource: ResourceReplicationPolicy, Action: ActionDelete}, + {Resource: ResourceReplicationPolicy, Action: ActionList}, + {Resource: ResourceReplicationPolicy, Action: ActionUpdate}, + + {Resource: ResourceReplication, Action: ActionRead}, + {Resource: ResourceReplication, Action: ActionCreate}, + {Resource: ResourceReplication, Action: ActionDelete}, + {Resource: ResourceReplication, Action: ActionList}, + {Resource: ResourceReplication, Action: ActionUpdate}, + + {Resource: ResourceReplicationAdapter, Action: ActionList}, + + {Resource: ResourceRegistry, Action: ActionRead}, + {Resource: ResourceRegistry, Action: ActionCreate}, + {Resource: ResourceRegistry, Action: ActionDelete}, + {Resource: ResourceRegistry, Action: ActionList}, + {Resource: ResourceRegistry, Action: ActionUpdate}, + + {Resource: ResourceScanAll, Action: ActionRead}, + {Resource: ResourceScanAll, Action: ActionUpdate}, + {Resource: ResourceScanAll, Action: ActionStop}, + {Resource: ResourceScanAll, Action: ActionCreate}, + + {Resource: ResourceSystemVolumes, Action: ActionRead}, + + {Resource: ResourceGarbageCollection, Action: ActionRead}, + {Resource: ResourceGarbageCollection, Action: ActionCreate}, + {Resource: ResourceGarbageCollection, Action: ActionDelete}, + {Resource: ResourceGarbageCollection, Action: ActionList}, + {Resource: ResourceGarbageCollection, Action: ActionUpdate}, + {Resource: ResourceGarbageCollection, Action: ActionStop}, + + {Resource: ResourcePurgeAuditLog, Action: ActionRead}, + {Resource: ResourcePurgeAuditLog, Action: ActionCreate}, + {Resource: ResourcePurgeAuditLog, Action: ActionDelete}, + {Resource: ResourcePurgeAuditLog, Action: ActionList}, + {Resource: ResourcePurgeAuditLog, Action: ActionUpdate}, + {Resource: ResourcePurgeAuditLog, Action: ActionStop}, + + {Resource: ResourceJobServiceMonitor, Action: ActionList}, + {Resource: ResourceJobServiceMonitor, Action: ActionStop}, + + {Resource: ResourceTagRetention, Action: ActionRead}, + {Resource: ResourceTagRetention, Action: ActionCreate}, + {Resource: ResourceTagRetention, Action: ActionDelete}, + {Resource: ResourceTagRetention, Action: ActionList}, + {Resource: ResourceTagRetention, Action: ActionUpdate}, + + {Resource: ResourceScanner, Action: ActionRead}, + {Resource: ResourceScanner, Action: ActionCreate}, + {Resource: ResourceScanner, Action: ActionDelete}, + {Resource: ResourceScanner, Action: ActionList}, + {Resource: ResourceScanner, Action: ActionUpdate}, + + {Resource: ResourceLabel, Action: ActionRead}, + {Resource: ResourceLabel, Action: ActionCreate}, + {Resource: ResourceLabel, Action: ActionDelete}, + {Resource: ResourceLabel, Action: ActionList}, + {Resource: ResourceLabel, Action: ActionUpdate}, + + {Resource: ResourceExportCVE, Action: ActionRead}, + {Resource: ResourceExportCVE, Action: ActionCreate}, + + {Resource: ResourceSecurityHub, Action: ActionRead}, + {Resource: ResourceSecurityHub, Action: ActionList}, + + {Resource: ResourceCatalog, Action: ActionRead}, + }, + "Project": { + {Resource: ResourceLog, Action: ActionList}, + + {Resource: ResourceProject, Action: ActionRead}, + {Resource: ResourceProject, Action: ActionDelete}, + {Resource: ResourceProject, Action: ActionUpdate}, + + {Resource: ResourceMetadata, Action: ActionRead}, + {Resource: ResourceMetadata, Action: ActionCreate}, + {Resource: ResourceMetadata, Action: ActionDelete}, + {Resource: ResourceMetadata, Action: ActionList}, + {Resource: ResourceMetadata, Action: ActionUpdate}, + + {Resource: ResourceRepository, Action: ActionRead}, + {Resource: ResourceRepository, Action: ActionCreate}, + {Resource: ResourceRepository, Action: ActionList}, + {Resource: ResourceRepository, Action: ActionUpdate}, + + {Resource: ResourceArtifact, Action: ActionRead}, + {Resource: ResourceArtifact, Action: ActionCreate}, + {Resource: ResourceArtifact, Action: ActionList}, + {Resource: ResourceArtifact, Action: ActionDelete}, + + {Resource: ResourceScan, Action: ActionCreate}, + {Resource: ResourceScan, Action: ActionRead}, + {Resource: ResourceScan, Action: ActionStop}, + + {Resource: ResourceTag, Action: ActionCreate}, + {Resource: ResourceTag, Action: ActionList}, + {Resource: ResourceTag, Action: ActionDelete}, + + {Resource: ResourceAccessory, Action: ActionList}, + + {Resource: ResourceArtifactAddition, Action: ActionCreate}, + + {Resource: ResourceArtifactLabel, Action: ActionCreate}, + {Resource: ResourceArtifactLabel, Action: ActionDelete}, + + {Resource: ResourceScanner, Action: ActionCreate}, + {Resource: ResourceScanner, Action: ActionRead}, + + {Resource: ResourcePreatPolicy, Action: ActionRead}, + {Resource: ResourcePreatPolicy, Action: ActionCreate}, + {Resource: ResourcePreatPolicy, Action: ActionDelete}, + {Resource: ResourcePreatPolicy, Action: ActionList}, + {Resource: ResourcePreatPolicy, Action: ActionUpdate}, + + {Resource: ResourceImmutableTag, Action: ActionCreate}, + {Resource: ResourceImmutableTag, Action: ActionDelete}, + {Resource: ResourceImmutableTag, Action: ActionList}, + {Resource: ResourceImmutableTag, Action: ActionUpdate}, + + {Resource: ResourceNotificationPolicy, Action: ActionRead}, + {Resource: ResourceNotificationPolicy, Action: ActionCreate}, + {Resource: ResourceNotificationPolicy, Action: ActionDelete}, + {Resource: ResourceNotificationPolicy, Action: ActionList}, + {Resource: ResourceNotificationPolicy, Action: ActionUpdate}, + + {Resource: ResourceRegistry, Action: ActionPush}, + }, + } +) diff --git a/src/controller/member/controller.go b/src/controller/member/controller.go index a50f80cd97f..80212d9c52e 100644 --- a/src/controller/member/controller.go +++ b/src/controller/member/controller.go @@ -32,18 +32,20 @@ import ( // Controller defines the operation related to project member type Controller interface { - // Get get the project member with ID + // Get gets the project member with ID Get(ctx context.Context, projectNameOrID interface{}, memberID int) (*models.Member, error) // Create add project member to project Create(ctx context.Context, projectNameOrID interface{}, req Request) (int, error) // Delete member from project Delete(ctx context.Context, projectNameOrID interface{}, memberID int) error - // List list all project members with condition + // List lists all project members with condition List(ctx context.Context, projectNameOrID interface{}, entityName string, query *q.Query) ([]*models.Member, error) // UpdateRole update the project member role UpdateRole(ctx context.Context, projectNameOrID interface{}, memberID int, role int) error // Count get the total amount of project members Count(ctx context.Context, projectNameOrID interface{}, query *q.Query) (int, error) + // IsProjectAdmin judges if the user is a project admin of any project + IsProjectAdmin(ctx context.Context, memberID int) (bool, error) } // Request - Project Member Request @@ -258,3 +260,12 @@ func (c *controller) Delete(ctx context.Context, projectNameOrID interface{}, me } return c.mgr.Delete(ctx, p.ProjectID, memberID) } + +func (c *controller) IsProjectAdmin(ctx context.Context, memberID int) (bool, error) { + members, err := c.projectMgr.ListAdminRolesOfUser(ctx, memberID) + if err != nil { + return false, err + } + + return len(members) > 0, nil +} diff --git a/src/controller/member/controller_test.go b/src/controller/member/controller_test.go index c278ef3a511..56348166060 100644 --- a/src/controller/member/controller_test.go +++ b/src/controller/member/controller_test.go @@ -15,6 +15,7 @@ package member import ( + "context" "fmt" "testing" @@ -95,6 +96,13 @@ func (suite *MemberControllerTestSuite) TestAddProjectMemberWithUserGroup() { suite.NoError(err) } +func (suite *MemberControllerTestSuite) TestIsProjectAdmin() { + mock.OnAnything(suite.projectMgr, "ListAdminRolesOfUser").Return([]models.Member{models.Member{ID: 2, ProjectID: 2}}, nil) + ok, err := suite.controller.IsProjectAdmin(context.Background(), 2) + suite.NoError(err) + suite.True(ok) +} + func TestMemberControllerTestSuite(t *testing.T) { suite.Run(t, &MemberControllerTestSuite{}) } diff --git a/src/pkg/cached/project/redis/manager.go b/src/pkg/cached/project/redis/manager.go index 8ca1617f5a0..2fe086b5fef 100644 --- a/src/pkg/cached/project/redis/manager.go +++ b/src/pkg/cached/project/redis/manager.go @@ -75,6 +75,10 @@ func (m *Manager) ListRoles(ctx context.Context, projectID int64, userID int, gr return m.delegator.ListRoles(ctx, projectID, userID, groupIDs...) } +func (m *Manager) ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) { + return m.delegator.ListAdminRolesOfUser(ctx, userID) +} + func (m *Manager) Delete(ctx context.Context, id int64) error { p, err := m.Get(ctx, id) if err != nil { diff --git a/src/pkg/project/dao/dao.go b/src/pkg/project/dao/dao.go index 29eca1b5a63..eb88ccc477f 100644 --- a/src/pkg/project/dao/dao.go +++ b/src/pkg/project/dao/dao.go @@ -28,20 +28,22 @@ import ( // DAO is the data access object interface for project type DAO interface { - // Create create a project instance + // Create creates a project instance Create(ctx context.Context, project *models.Project) (int64, error) // Count returns the total count of projects according to the query Count(ctx context.Context, query *q.Query) (total int64, err error) - // Delete delete the project instance by id + // Delete deletes the project instance by id Delete(ctx context.Context, id int64) error - // Get get project instance by id + // Get gets project instance by id Get(ctx context.Context, id int64) (*models.Project, error) // GetByName get project instance by name GetByName(ctx context.Context, name string) (*models.Project, error) - // List list projects + // List lists projects List(ctx context.Context, query *q.Query) ([]*models.Project, error) - // Lists the roles of user for the specific project + // ListRoles the roles of user for the specific project ListRoles(ctx context.Context, projectID int64, userID int, groupIDs ...int) ([]int, error) + // ListAdminRolesOfUser returns the roles of user for the all projects + ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) } // New returns an instance of the default DAO @@ -51,7 +53,7 @@ func New() DAO { type dao struct{} -// Create create a project instance +// Create creates a project instance func (d *dao) Create(ctx context.Context, project *models.Project) (int64, error) { var projectID int64 @@ -105,7 +107,7 @@ func (d *dao) Count(ctx context.Context, query *q.Query) (total int64, err error return qs.Count() } -// Delete delete the project instance by id +// Delete deletes the project instance by id func (d *dao) Delete(ctx context.Context, id int64) error { project, err := d.Get(ctx, id) if err != nil { @@ -124,7 +126,7 @@ func (d *dao) Delete(ctx context.Context, id int64) error { return err } -// Get get project instance by id +// Get gets project instance by id func (d *dao) Get(ctx context.Context, id int64) (*models.Project, error) { o, err := orm.FromContext(ctx) if err != nil { @@ -199,3 +201,20 @@ func (d *dao) ListRoles(ctx context.Context, projectID int64, userID int, groupI return roles, nil } + +func (d *dao) ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) { + o, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } + + sql := `select b.* from project as a left join project_member as b on a.project_id = b.project_id where a.deleted = 'f' and b.entity_id = ? and b.entity_type = 'u' and b.role = 1;` + + var members []models.Member + _, err = o.Raw(sql, userID).QueryRows(&members) + if err != nil { + return nil, err + } + + return members, nil +} diff --git a/src/pkg/project/manager.go b/src/pkg/project/manager.go index 5c072456d4f..3a03d55d3cd 100644 --- a/src/pkg/project/manager.go +++ b/src/pkg/project/manager.go @@ -28,13 +28,13 @@ import ( // Manager is used for project management type Manager interface { - // Create create project instance + // Create creates project instance Create(ctx context.Context, project *models.Project) (int64, error) // Count returns the total count of projects according to the query Count(ctx context.Context, query *q.Query) (total int64, err error) - // Delete delete the project instance by id + // Delete deletes the project instance by id Delete(ctx context.Context, id int64) error // Get the project specified by the ID or name @@ -45,6 +45,9 @@ type Manager interface { // ListRoles returns the roles of user for the specific project ListRoles(ctx context.Context, projectID int64, userID int, groupIDs ...int) ([]int, error) + + // ListAdminRolesOfUser returns the roles of user for the all projects + ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) } // New returns a default implementation of Manager @@ -64,7 +67,7 @@ type manager struct { dao dao.DAO } -// Create create project instance +// Create creates project instance func (m *manager) Create(ctx context.Context, project *models.Project) (int64, error) { if project.OwnerID <= 0 { return 0, errors.BadRequestError(nil).WithMessage("Owner is missing when creating project %s", project.Name) @@ -115,7 +118,12 @@ func (m *manager) List(ctx context.Context, query *q.Query) ([]*models.Project, return m.dao.List(ctx, query) } -// Lists the roles of user for the specific project +// ListRoles the roles of user for the specific project func (m *manager) ListRoles(ctx context.Context, projectID int64, userID int, groupIDs ...int) ([]int, error) { return m.dao.ListRoles(ctx, projectID, userID, groupIDs...) } + +// ListAdminRolesOfUser returns the roles of user for the all projects +func (m *manager) ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) { + return m.dao.ListAdminRolesOfUser(ctx, userID) +} diff --git a/src/server/v2.0/handler/handler.go b/src/server/v2.0/handler/handler.go index 7784b5d9b94..e92dfd1b505 100644 --- a/src/server/v2.0/handler/handler.go +++ b/src/server/v2.0/handler/handler.go @@ -70,6 +70,7 @@ func New() http.Handler { JobserviceAPI: newJobServiceAPI(), ScheduleAPI: newScheduleAPI(), SecurityhubAPI: newSecurityAPI(), + PermissionsAPI: newPermissionsAPIAPI(), }) if err != nil { log.Fatal(err) diff --git a/src/server/v2.0/handler/permissions.go b/src/server/v2.0/handler/permissions.go new file mode 100644 index 00000000000..9d258dc449d --- /dev/null +++ b/src/server/v2.0/handler/permissions.go @@ -0,0 +1,109 @@ +// 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 handler + +import ( + "context" + + "github.com/go-openapi/runtime/middleware" + + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/security" + "github.com/goharbor/harbor/src/controller/member" + "github.com/goharbor/harbor/src/controller/user" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/pkg/permission/types" + "github.com/goharbor/harbor/src/server/v2.0/models" + "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/permissions" +) + +type permissionsAPI struct { + BaseAPI + uc user.Controller + mc member.Controller +} + +func newPermissionsAPIAPI() *permissionsAPI { + return &permissionsAPI{ + uc: user.Ctl, + mc: member.NewController(), + } +} + +func (p *permissionsAPI) GetPermissions(ctx context.Context, params permissions.GetPermissionsParams) middleware.Responder { + secCtx, ok := security.FromContext(ctx) + if !ok { + return p.SendError(ctx, errors.UnauthorizedError(errors.New("security context not found"))) + } + if !secCtx.IsAuthenticated() { + return p.SendError(ctx, errors.UnauthorizedError(nil).WithMessage(secCtx.GetUsername())) + } + + var isSystemAdmin bool + var isProjectAdmin bool + + if secCtx.IsSysAdmin() { + isSystemAdmin = true + } else { + user, err := p.uc.GetByName(ctx, secCtx.GetUsername()) + if err != nil { + return p.SendError(ctx, err) + } + is, err := p.mc.IsProjectAdmin(ctx, user.UserID) + if err != nil { + return p.SendError(ctx, err) + } + isProjectAdmin = is + } + if !isSystemAdmin && !isProjectAdmin { + return p.SendError(ctx, errors.ForbiddenError(errors.New("only admins(system and project) can access permissions"))) + } + + sysPermissions := make([]*types.Policy, 0) + proPermissions := rbac.PoliciesMap["Project"] + if isSystemAdmin { + // project admin cannot see the system level permissions + sysPermissions = rbac.PoliciesMap["System"] + } + + return permissions.NewGetPermissionsOK().WithPayload(p.convertPermissions(sysPermissions, proPermissions)) +} + +func (p *permissionsAPI) convertPermissions(system, project []*types.Policy) *models.Permissions { + res := &models.Permissions{} + if len(system) > 0 { + var sysPermission []*models.Permission + for _, item := range system { + sysPermission = append(sysPermission, &models.Permission{ + Resource: item.Resource.String(), + Action: item.Action.String(), + }) + } + res.System = sysPermission + } + + if len(project) > 0 { + var proPermission []*models.Permission + for _, item := range project { + proPermission = append(proPermission, &models.Permission{ + Resource: item.Resource.String(), + Action: item.Action.String(), + }) + } + res.Project = proPermission + } + + return res +} diff --git a/src/testing/pkg/project/manager.go b/src/testing/pkg/project/manager.go index 169bdec507f..aad036a8e5a 100644 --- a/src/testing/pkg/project/manager.go +++ b/src/testing/pkg/project/manager.go @@ -130,6 +130,32 @@ func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*models.Project, return r0, r1 } +// ListAdminRolesOfUser provides a mock function with given fields: ctx, userID +func (_m *Manager) ListAdminRolesOfUser(ctx context.Context, userID int) ([]models.Member, error) { + ret := _m.Called(ctx, userID) + + var r0 []models.Member + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) ([]models.Member, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) []models.Member); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Member) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListRoles provides a mock function with given fields: ctx, projectID, userID, groupIDs func (_m *Manager) ListRoles(ctx context.Context, projectID int64, userID int, groupIDs ...int) ([]int, error) { _va := make([]interface{}, len(groupIDs))