add permission api (#19543)

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 <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2023-11-08 19:47:07 -06:00 committed by GitHub
parent da949bfc3f
commit 5c02fd807e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 387 additions and 15 deletions

View File

@ -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:

View File

@ -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},
},
}
)

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}

View File

@ -70,6 +70,7 @@ func New() http.Handler {
JobserviceAPI: newJobServiceAPI(),
ScheduleAPI: newScheduleAPI(),
SecurityhubAPI: newSecurityAPI(),
PermissionsAPI: newPermissionsAPIAPI(),
})
if err != nil {
log.Fatal(err)

View File

@ -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
}

View File

@ -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))