enable robot full access (#20754)

* have option to enable robot full access

When the system admin enable this option, the robot can be assigned with robot/user/group/quota permissions.

Signed-off-by: wang yan <wangyan@vmware.com>

* robot account permission enhancement

Update codes according to the proposal of https://github.com/goharbor/community/pull/249

Signed-off-by: wang yan <wangyan@vmware.com>

---------

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2024-09-26 19:09:50 +08:00 committed by GitHub
parent d42c34786f
commit e4fe61ddb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 532 additions and 69 deletions

View File

@ -14,7 +14,9 @@
package rbac package rbac
import "github.com/goharbor/harbor/src/pkg/permission/types" import (
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// const action variables // const action variables
const ( const (
@ -81,9 +83,88 @@ const (
ResourceSecurityHub = Resource("security-hub") ResourceSecurityHub = Resource("security-hub")
) )
type scope string
const (
ScopeSystem = scope("System")
ScopeProject = scope("Project")
)
// RobotPermissionProvider defines the permission provider for robot account
type RobotPermissionProvider interface {
GetPermissions(s scope) []*types.Policy
}
// GetPermissionProvider gives the robot permission provider
func GetPermissionProvider() RobotPermissionProvider {
// TODO will determine by the ui configuration
return &NolimitProvider{}
}
// BaseProvider ...
type BaseProvider struct {
}
// GetPermissions ...
func (d *BaseProvider) GetPermissions(s scope) []*types.Policy {
return PoliciesMap[s]
}
// NolimitProvider ...
type NolimitProvider struct {
BaseProvider
}
// GetPermissions ...
func (n *NolimitProvider) GetPermissions(s scope) []*types.Policy {
if s == ScopeSystem {
return append(n.BaseProvider.GetPermissions(ScopeSystem),
&types.Policy{Resource: ResourceRobot, Action: ActionCreate},
&types.Policy{Resource: ResourceRobot, Action: ActionRead},
&types.Policy{Resource: ResourceRobot, Action: ActionUpdate},
&types.Policy{Resource: ResourceRobot, Action: ActionList},
&types.Policy{Resource: ResourceRobot, Action: ActionDelete},
&types.Policy{Resource: ResourceUser, Action: ActionCreate},
&types.Policy{Resource: ResourceUser, Action: ActionRead},
&types.Policy{Resource: ResourceUser, Action: ActionUpdate},
&types.Policy{Resource: ResourceUser, Action: ActionList},
&types.Policy{Resource: ResourceUser, Action: ActionDelete},
&types.Policy{Resource: ResourceLdapUser, Action: ActionCreate},
&types.Policy{Resource: ResourceLdapUser, Action: ActionList},
&types.Policy{Resource: ResourceExportCVE, Action: ActionCreate},
&types.Policy{Resource: ResourceExportCVE, Action: ActionRead},
&types.Policy{Resource: ResourceQuota, Action: ActionUpdate},
&types.Policy{Resource: ResourceUserGroup, Action: ActionCreate},
&types.Policy{Resource: ResourceUserGroup, Action: ActionRead},
&types.Policy{Resource: ResourceUserGroup, Action: ActionUpdate},
&types.Policy{Resource: ResourceUserGroup, Action: ActionList},
&types.Policy{Resource: ResourceUserGroup, Action: ActionDelete})
}
if s == ScopeProject {
return append(n.BaseProvider.GetPermissions(ScopeProject),
&types.Policy{Resource: ResourceRobot, Action: ActionCreate},
&types.Policy{Resource: ResourceRobot, Action: ActionRead},
&types.Policy{Resource: ResourceRobot, Action: ActionUpdate},
&types.Policy{Resource: ResourceRobot, Action: ActionList},
&types.Policy{Resource: ResourceRobot, Action: ActionDelete},
&types.Policy{Resource: ResourceMember, Action: ActionCreate},
&types.Policy{Resource: ResourceMember, Action: ActionRead},
&types.Policy{Resource: ResourceMember, Action: ActionUpdate},
&types.Policy{Resource: ResourceMember, Action: ActionList},
&types.Policy{Resource: ResourceMember, Action: ActionDelete})
}
return []*types.Policy{}
}
var ( var (
PoliciesMap = map[string][]*types.Policy{ PoliciesMap = map[scope][]*types.Policy{
"System": { ScopeSystem: {
{Resource: ResourceAuditLog, Action: ActionList}, {Resource: ResourceAuditLog, Action: ActionList},
{Resource: ResourcePreatInstance, Action: ActionRead}, {Resource: ResourcePreatInstance, Action: ActionRead},
@ -154,7 +235,7 @@ var (
{Resource: ResourceQuota, Action: ActionRead}, {Resource: ResourceQuota, Action: ActionRead},
{Resource: ResourceQuota, Action: ActionList}, {Resource: ResourceQuota, Action: ActionList},
}, },
"Project": { ScopeProject: {
{Resource: ResourceLog, Action: ActionList}, {Resource: ResourceLog, Action: ActionList},
{Resource: ResourceProject, Action: ActionRead}, {Resource: ResourceProject, Action: ActionRead},

View File

@ -0,0 +1,36 @@
package rbac
import (
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/stretchr/testify/assert"
"testing"
)
func TestBaseProvider(t *testing.T) {
permissionProvider := &BaseProvider{}
sysPermissions := permissionProvider.GetPermissions(ScopeSystem)
for _, per := range sysPermissions {
if per.Action == ActionCreate && per.Resource == ResourceRobot {
t.Fail()
}
}
}
func TestNolimitProvider(t *testing.T) {
permissionProvider := &BaseProvider{}
sysPermissions := permissionProvider.GetPermissions(ScopeSystem)
for _, per := range sysPermissions {
if per.Action == ActionCreate && per.Resource == ResourceRobot {
t.Log("no limit provider has the permission of robot account creation")
}
}
}
func TestGetPermissionProvider(t *testing.T) {
defaultPro := GetPermissionProvider()
_, ok := defaultPro.(*NolimitProvider)
assert.True(t, ok)
}

View File

@ -97,10 +97,6 @@ func (d *controller) Count(ctx context.Context, query *q.Query) (int64, error) {
// Create ... // Create ...
func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error) { func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error) {
if err := d.setProject(ctx, r); err != nil {
return 0, "", err
}
var expiresAt int64 var expiresAt int64
if r.Duration == -1 { if r.Duration == -1 {
expiresAt = -1 expiresAt = -1
@ -327,22 +323,6 @@ func (d *controller) populatePermissions(ctx context.Context, r *Robot) error {
return nil return nil
} }
// set the project info if it's a project level robot
func (d *controller) setProject(ctx context.Context, r *Robot) error {
if r == nil {
return nil
}
if r.Level == LEVELPROJECT {
pro, err := d.proMgr.Get(ctx, r.Permissions[0].Namespace)
if err != nil {
return err
}
r.ProjectName = pro.Name
r.ProjectID = pro.ProjectID
}
return nil
}
// convertScope converts the db scope into robot model // convertScope converts the db scope into robot model
// /system => Kind: system Namespace: / // /system => Kind: system Namespace: /
// /project/* => Kind: project Namespace: * // /project/* => Kind: project Namespace: *
@ -394,6 +374,22 @@ func (d *controller) toScope(ctx context.Context, p *Permission) (string, error)
return "", errors.New(nil).WithMessage("unknown robot kind").WithCode(errors.BadRequestCode) return "", errors.New(nil).WithMessage("unknown robot kind").WithCode(errors.BadRequestCode)
} }
// set the project info if it's a project level robot
func SetProject(ctx context.Context, r *Robot) error {
if r == nil {
return nil
}
if r.Level == LEVELPROJECT {
pro, err := project.New().Get(ctx, r.Permissions[0].Namespace)
if err != nil {
return err
}
r.ProjectName = pro.Name
r.ProjectID = pro.ProjectID
}
return nil
}
func CreateSec(salt ...string) (string, string, string, error) { func CreateSec(salt ...string) (string, string, string, error) {
var secret, pwd string var secret, pwd string
options := []retry.Option{ options := []retry.Option{

View File

@ -40,6 +40,7 @@ const (
type Robot struct { type Robot struct {
model.Robot model.Robot
ProjectName string ProjectName string
ProjectNameOrID interface{}
Level string Level string
Editable bool `json:"editable"` Editable bool `json:"editable"`
Permissions []*Permission `json:"permissions"` Permissions []*Permission `json:"permissions"`

View File

@ -867,6 +867,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64
CreatorType: "local", CreatorType: "local",
CreatorRef: int64(0), CreatorRef: int64(0),
}, },
ProjectName: projectName,
Level: robot.LEVELPROJECT, Level: robot.LEVELPROJECT,
Permissions: []*robot.Permission{ Permissions: []*robot.Permission{
{ {

View File

@ -238,6 +238,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
CreatorType: "local", CreatorType: "local",
CreatorRef: int64(0), CreatorRef: int64(0),
}, },
ProjectName: "library",
Level: robot.LEVELPROJECT, Level: robot.LEVELPROJECT,
Permissions: []*robot.Permission{ Permissions: []*robot.Permission{
{ {

View File

@ -79,6 +79,11 @@ export const ACTION_RESOURCE_I18N_MAP = {
'notification-policy': 'ROBOT_ACCOUNT.NOTIFICATION_POLICY', 'notification-policy': 'ROBOT_ACCOUNT.NOTIFICATION_POLICY',
quota: 'ROBOT_ACCOUNT.QUOTA', quota: 'ROBOT_ACCOUNT.QUOTA',
sbom: 'ROBOT_ACCOUNT.SBOM', sbom: 'ROBOT_ACCOUNT.SBOM',
robot: 'ROBOT_ACCOUNT.ROBOT',
user: 'ROBOT_ACCOUNT.USER',
'user-group': 'ROBOT_ACCOUNT.GROUP',
'ldap-user': 'ROBOT_ACCOUNT.LDAPUSER',
member: 'ROBOT_ACCOUNT.MEMBER',
}; };
export function convertKey(key: string) { export function convertKey(key: string) {

View File

@ -423,7 +423,12 @@
"SELECT_PERMISSIONS": "Select Permissions", "SELECT_PERMISSIONS": "Select Permissions",
"SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions",
"SELECT_PROJECT_PERMISSIONS": "Select Project Permissions", "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions",
"SYSTEM_PERMISSIONS": "System Permissions" "SYSTEM_PERMISSIONS": "System Permissions",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "EDIT", "EDIT_BUTTON": "EDIT",

View File

@ -423,7 +423,12 @@
"SELECT_PERMISSIONS": "Select Permissions", "SELECT_PERMISSIONS": "Select Permissions",
"SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions",
"SELECT_PROJECT_PERMISSIONS": "Select Project Permissions", "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions",
"SYSTEM_PERMISSIONS": "System Permissions" "SYSTEM_PERMISSIONS": "System Permissions",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "EDIT", "EDIT_BUTTON": "EDIT",

View File

@ -424,7 +424,12 @@
"SELECT_PERMISSIONS": "Select Permissions", "SELECT_PERMISSIONS": "Select Permissions",
"SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions",
"SELECT_PROJECT_PERMISSIONS": "Select Project Permissions", "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions",
"SYSTEM_PERMISSIONS": "System Permissions" "SYSTEM_PERMISSIONS": "System Permissions",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "EDIT", "EDIT_BUTTON": "EDIT",

View File

@ -423,7 +423,12 @@
"SELECT_PERMISSIONS": "Selectionner les permissions", "SELECT_PERMISSIONS": "Selectionner les permissions",
"SELECT_SYSTEM_PERMISSIONS": "Selectionner les permissions système", "SELECT_SYSTEM_PERMISSIONS": "Selectionner les permissions système",
"SELECT_PROJECT_PERMISSIONS": "Selectionner les permissions projet", "SELECT_PROJECT_PERMISSIONS": "Selectionner les permissions projet",
"SYSTEM_PERMISSIONS": "Permissions système" "SYSTEM_PERMISSIONS": "Permissions système",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "Éditer", "EDIT_BUTTON": "Éditer",

View File

@ -420,7 +420,12 @@
"SELECT_PERMISSIONS": "권한 선택", "SELECT_PERMISSIONS": "권한 선택",
"SELECT_SYSTEM_PERMISSIONS": "시스템 권한 선택", "SELECT_SYSTEM_PERMISSIONS": "시스템 권한 선택",
"SELECT_PROJECT_PERMISSIONS": "프로젝트 권한 선택", "SELECT_PROJECT_PERMISSIONS": "프로젝트 권한 선택",
"SYSTEM_PERMISSIONS": "시스템 권한" "SYSTEM_PERMISSIONS": "시스템 권한",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "편집", "EDIT_BUTTON": "편집",

View File

@ -421,7 +421,12 @@
"SELECT_PERMISSIONS": "Select Permissions", "SELECT_PERMISSIONS": "Select Permissions",
"SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions",
"SELECT_PROJECT_PERMISSIONS": "Select Project Permissions", "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions",
"SYSTEM_PERMISSIONS": "System Permissions" "SYSTEM_PERMISSIONS": "System Permissions",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"GROUP": { "GROUP": {
"GROUP": "Grupo", "GROUP": "Grupo",

View File

@ -423,7 +423,12 @@
"SELECT_PERMISSIONS": "Select Permissions", "SELECT_PERMISSIONS": "Select Permissions",
"SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions",
"SELECT_PROJECT_PERMISSIONS": "Select Project Permissions", "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions",
"SYSTEM_PERMISSIONS": "System Permissions" "SYSTEM_PERMISSIONS": "System Permissions",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "DÜZENLE", "EDIT_BUTTON": "DÜZENLE",

View File

@ -421,7 +421,12 @@
"SELECT_PERMISSIONS": "选择权限", "SELECT_PERMISSIONS": "选择权限",
"SELECT_SYSTEM_PERMISSIONS": "选择系统权限", "SELECT_SYSTEM_PERMISSIONS": "选择系统权限",
"SELECT_PROJECT_PERMISSIONS": "选择项目权限", "SELECT_PROJECT_PERMISSIONS": "选择项目权限",
"SYSTEM_PERMISSIONS": "系统权限" "SYSTEM_PERMISSIONS": "系统权限",
"ROBOT": "机器人账户",
"USER": "用户",
"LDAPUSER": "LDAP 用户",
"GROUP": "用户组",
"MEMBER": "项目成员"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "编辑", "EDIT_BUTTON": "编辑",

View File

@ -422,7 +422,12 @@
"SELECT_PERMISSIONS": "Select Permissions", "SELECT_PERMISSIONS": "Select Permissions",
"SELECT_SYSTEM_PERMISSIONS": "Select System Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System Permissions",
"SELECT_PROJECT_PERMISSIONS": "Select Project Permissions", "SELECT_PROJECT_PERMISSIONS": "Select Project Permissions",
"SYSTEM_PERMISSIONS": "System Permissions" "SYSTEM_PERMISSIONS": "System Permissions",
"ROBOT": "Robot Account",
"USER": "User",
"LDAPUSER": "LDAP User",
"GROUP": "User Group",
"MEMBER": "Project Member"
}, },
"WEBHOOK": { "WEBHOOK": {
"EDIT_BUTTON": "編輯", "EDIT_BUTTON": "編輯",

View File

@ -71,11 +71,12 @@ func (p *permissionsAPI) GetPermissions(ctx context.Context, _ permissions.GetPe
return p.SendError(ctx, errors.ForbiddenError(errors.New("only admins(system and project) can access permissions"))) return p.SendError(ctx, errors.ForbiddenError(errors.New("only admins(system and project) can access permissions")))
} }
provider := rbac.GetPermissionProvider()
sysPermissions := make([]*types.Policy, 0) sysPermissions := make([]*types.Policy, 0)
proPermissions := rbac.PoliciesMap["Project"] proPermissions := provider.GetPermissions(rbac.ScopeProject)
if isSystemAdmin { if isSystemAdmin {
// project admin cannot see the system level permissions // project admin cannot see the system level permissions
sysPermissions = rbac.PoliciesMap["System"] sysPermissions = provider.GetPermissions(rbac.ScopeSystem)
} }
return permissions.NewGetPermissionsOK().WithPayload(p.convertPermissions(sysPermissions, proPermissions)) return permissions.NewGetPermissionsOK().WithPayload(p.convertPermissions(sysPermissions, proPermissions))

View File

@ -31,8 +31,10 @@ import (
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/robot" "github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/lib" "github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/permission/types"
pkg "github.com/goharbor/harbor/src/pkg/robot/model" pkg "github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/goharbor/harbor/src/server/v2.0/handler/model" "github.com/goharbor/harbor/src/server/v2.0/handler/model"
@ -60,12 +62,23 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
if err := rAPI.requireAccess(ctx, params.Robot.Level, params.Robot.Permissions[0].Namespace, rbac.ActionCreate); err != nil { sc, err := rAPI.GetSecurityContext(ctx)
if err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
sc, err := rAPI.GetSecurityContext(ctx) r := &robot.Robot{
if err != nil { Robot: pkg.Robot{
Name: params.Robot.Name,
Description: params.Robot.Description,
Duration: params.Robot.Duration,
Visible: true,
},
Level: params.Robot.Level,
ProjectNameOrID: params.Robot.Permissions[0].Namespace,
}
if err := rAPI.requireAccess(ctx, r, rbac.ActionCreate); err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
@ -78,23 +91,36 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo
default: default:
return rAPI.SendError(ctx, errors.New(nil).WithMessage("invalid security context")) return rAPI.SendError(ctx, errors.New(nil).WithMessage("invalid security context"))
} }
r.CreatorType = sc.Name()
r := &robot.Robot{ r.CreatorRef = creatorRef
Robot: pkg.Robot{
Name: params.Robot.Name,
Description: params.Robot.Description,
Duration: params.Robot.Duration,
Visible: true,
CreatorRef: creatorRef,
CreatorType: sc.Name(),
},
Level: params.Robot.Level,
}
if err := lib.JSONCopy(&r.Permissions, params.Robot.Permissions); err != nil { if err := lib.JSONCopy(&r.Permissions, params.Robot.Permissions); err != nil {
log.Warningf("failed to call JSONCopy on robot permission when CreateRobot, error: %v", err) log.Warningf("failed to call JSONCopy on robot permission when CreateRobot, error: %v", err)
} }
if err := robot.SetProject(ctx, r); err != nil {
return rAPI.SendError(ctx, err)
}
if _, ok := sc.(*robotSc.SecurityContext); ok {
creatorRobots, err := rAPI.robotCtl.List(ctx, q.New(q.KeyWords{
"name": strings.TrimPrefix(sc.GetUsername(), config.RobotPrefix(ctx)),
"project_id": r.ProjectID,
}), &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
if len(creatorRobots) == 0 {
return rAPI.SendError(ctx, errors.DeniedError(nil))
}
if !isValidPermissionScope(params.Robot.Permissions, creatorRobots[0].Permissions) {
return rAPI.SendError(ctx, errors.New(nil).WithMessage("permission scope is invalid. It must be equal to or more restrictive than the creator robot's permissions: %s", creatorRobots[0].Name).WithCode(errors.DENIED))
}
}
rid, pwd, err := rAPI.robotCtl.Create(ctx, r) rid, pwd, err := rAPI.robotCtl.Create(ctx, r)
if err != nil { if err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
@ -125,7 +151,7 @@ func (rAPI *robotAPI) DeleteRobot(ctx context.Context, params operation.DeleteRo
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionDelete); err != nil { if err := rAPI.requireAccess(ctx, r, rbac.ActionDelete); err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
@ -174,7 +200,11 @@ func (rAPI *robotAPI) ListRobot(ctx context.Context, params operation.ListRobotP
} }
query.Keywords["Visible"] = true query.Keywords["Visible"] = true
if err := rAPI.requireAccess(ctx, level, projectID, rbac.ActionList); err != nil { r := &robot.Robot{
ProjectNameOrID: projectID,
Level: level,
}
if err := rAPI.requireAccess(ctx, r, rbac.ActionList); err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
@ -212,7 +242,7 @@ func (rAPI *robotAPI) GetRobotByID(ctx context.Context, params operation.GetRobo
if err != nil { if err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionRead); err != nil { if err := rAPI.requireAccess(ctx, r, rbac.ActionRead); err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
@ -253,7 +283,7 @@ func (rAPI *robotAPI) RefreshSec(ctx context.Context, params operation.RefreshSe
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionUpdate); err != nil { if err := rAPI.requireAccess(ctx, r, rbac.ActionUpdate); err != nil {
return rAPI.SendError(ctx, err) return rAPI.SendError(ctx, err)
} }
@ -282,12 +312,21 @@ func (rAPI *robotAPI) RefreshSec(ctx context.Context, params operation.RefreshSe
return operation.NewRefreshSecOK().WithPayload(robotSec) return operation.NewRefreshSecOK().WithPayload(robotSec)
} }
func (rAPI *robotAPI) requireAccess(ctx context.Context, level string, projectIDOrName interface{}, action rbac.Action) error { func (rAPI *robotAPI) requireAccess(ctx context.Context, r *robot.Robot, action rbac.Action) error {
if level == robot.LEVELSYSTEM { if r.Level == robot.LEVELSYSTEM {
return rAPI.RequireSystemAccess(ctx, action, rbac.ResourceRobot) return rAPI.RequireSystemAccess(ctx, action, rbac.ResourceRobot)
} else if level == robot.LEVELPROJECT { } else if r.Level == robot.LEVELPROJECT {
return rAPI.RequireProjectAccess(ctx, projectIDOrName, action, rbac.ResourceRobot) var ns interface{}
if r.ProjectNameOrID != nil {
ns = r.ProjectNameOrID
} else if r.ProjectID > 0 {
ns = r.ProjectID
} else if r.ProjectName != "" {
ns = r.ProjectName
} }
return rAPI.RequireProjectAccess(ctx, ns, action, rbac.ResourceRobot)
}
return errors.ForbiddenError(nil) return errors.ForbiddenError(nil)
} }
@ -316,17 +355,18 @@ func (rAPI *robotAPI) validate(d int64, level string, permissions []*models.Robo
return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode) return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode)
} }
provider := rbac.GetPermissionProvider()
// to validate the access scope // to validate the access scope
for _, perm := range permissions { for _, perm := range permissions {
if perm.Kind == robot.LEVELSYSTEM { if perm.Kind == robot.LEVELSYSTEM {
polices := rbac.PoliciesMap["System"] polices := provider.GetPermissions(rbac.ScopeSystem)
for _, acc := range perm.Access { for _, acc := range perm.Access {
if !containsAccess(polices, acc) { if !containsAccess(polices, acc) {
return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode) return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode)
} }
} }
} else if perm.Kind == robot.LEVELPROJECT { } else if perm.Kind == robot.LEVELPROJECT {
polices := rbac.PoliciesMap["Project"] polices := provider.GetPermissions(rbac.ScopeProject)
for _, acc := range perm.Access { for _, acc := range perm.Access {
if !containsAccess(polices, acc) { if !containsAccess(polices, acc) {
return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode) return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode)
@ -356,7 +396,8 @@ func (rAPI *robotAPI) updateV2Robot(ctx context.Context, params operation.Update
return errors.BadRequestError(nil).WithMessage("cannot update the project id of robot") return errors.BadRequestError(nil).WithMessage("cannot update the project id of robot")
} }
} }
if err := rAPI.requireAccess(ctx, params.Robot.Level, params.Robot.Permissions[0].Namespace, rbac.ActionUpdate); err != nil { r.ProjectNameOrID = params.Robot.Permissions[0].Namespace
if err := rAPI.requireAccess(ctx, r, rbac.ActionUpdate); err != nil {
return err return err
} }
if params.Robot.Level != r.Level || params.Robot.Name != r.Name { if params.Robot.Level != r.Level || params.Robot.Name != r.Name {
@ -380,6 +421,42 @@ func (rAPI *robotAPI) updateV2Robot(ctx context.Context, params operation.Update
} }
} }
creatorRobot, err := rAPI.robotCtl.Get(ctx, r.CreatorRef, &robot.Option{
WithPermission: true,
})
if err != nil && !errors.IsErr(err, errors.NotFoundCode) {
return err
}
// for nested robot only
if creatorRobot != nil && r.CreatorType == "robot" {
sc, err := rAPI.GetSecurityContext(ctx)
if err != nil {
return err
}
if _, ok := sc.(*robotSc.SecurityContext); ok {
scRobots, err := rAPI.robotCtl.List(ctx, q.New(q.KeyWords{
"name": strings.TrimPrefix(sc.GetUsername(), config.RobotPrefix(ctx)),
"project_id": r.ProjectID,
}), &robot.Option{
WithPermission: true,
})
if err != nil {
return err
}
if len(scRobots) == 0 {
return errors.DeniedError(nil)
}
if scRobots[0].ID != creatorRobot.ID && scRobots[0].ID != r.ID {
return errors.New(nil).WithMessage("as for a nested robot account, only person who has the right permission or the creator robot or nested robot itself has the permission to update").WithCode(errors.DENIED)
}
}
if !isValidPermissionScope(params.Robot.Permissions, creatorRobot.Permissions) {
return errors.New(nil).WithMessage("permission scope is invalid. It must be equal to or more restrictive than the creator robot's permissions: %s", creatorRobot.Name).WithCode(errors.DENIED)
}
}
if err := rAPI.robotCtl.Update(ctx, r, &robot.Option{ if err := rAPI.robotCtl.Update(ctx, r, &robot.Option{
WithPermission: true, WithPermission: true,
}); err != nil { }); err != nil {
@ -414,3 +491,39 @@ func containsAccess(policies []*types.Policy, item *models.Access) bool {
} }
return false return false
} }
// isValidPermissionScope checks if permission slice A is a subset of permission slice B
func isValidPermissionScope(creating []*models.RobotPermission, creator []*robot.Permission) bool {
creatorMap := make(map[string]*robot.Permission)
for _, creatorPerm := range creator {
key := fmt.Sprintf("%s:%s", creatorPerm.Kind, creatorPerm.Namespace)
creatorMap[key] = creatorPerm
}
hasLessThanOrEqualAccess := func(creating []*models.Access, creator []*types.Policy) bool {
creatorMap := make(map[string]*types.Policy)
for _, creatorP := range creator {
key := fmt.Sprintf("%s:%s:%s", creatorP.Resource, creatorP.Action, creatorP.Effect)
creatorMap[key] = creatorP
}
for _, creatingP := range creating {
key := fmt.Sprintf("%s:%s:%s", creatingP.Resource, creatingP.Action, creatingP.Effect)
if _, found := creatorMap[key]; !found {
return false
}
}
return true
}
for _, pCreating := range creating {
key := fmt.Sprintf("%s:%s", pCreating.Kind, pCreating.Namespace)
creatingPerm, found := creatorMap[key]
if !found {
return false
}
if !hasLessThanOrEqualAccess(pCreating.Access, creatingPerm.Access) {
return false
}
}
return true
}

View File

@ -1,10 +1,15 @@
package handler package handler
import ( import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/server/v2.0/models"
"math" "math"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/server/v2.0/models"
) )
func TestValidLevel(t *testing.T) { func TestValidLevel(t *testing.T) {
@ -207,3 +212,181 @@ func TestContainsAccess(t *testing.T) {
}) })
} }
} }
func TestValidPermissionScope(t *testing.T) {
tests := []struct {
name string
creatingPerms []*models.RobotPermission
creatorPerms []*robot.Permission
expected bool
}{
{
name: "Project - subset",
creatingPerms: []*models.RobotPermission{
{
Kind: "project",
Namespace: "testSubset",
Access: []*models.Access{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
creatorPerms: []*robot.Permission{
{
Kind: "project",
Namespace: "testSubset",
Access: []*types.Policy{
{Resource: "repository", Action: "pull", Effect: "allow"},
{Resource: "repository", Action: "push", Effect: "allow"},
},
},
},
expected: true,
},
{
name: "Project - not Subset",
creatingPerms: []*models.RobotPermission{
{
Kind: "project",
Namespace: "testNotSubset",
Access: []*models.Access{
{Resource: "repository", Action: "push", Effect: "allow"},
},
},
},
creatorPerms: []*robot.Permission{
{
Kind: "project",
Namespace: "testNotSubset",
Access: []*types.Policy{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
expected: false,
},
{
name: "Project - equal",
creatingPerms: []*models.RobotPermission{
{
Kind: "project",
Namespace: "library",
Access: []*models.Access{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
creatorPerms: []*robot.Permission{
{
Kind: "project",
Namespace: "library",
Access: []*types.Policy{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
expected: true,
},
{
name: "Project - different",
creatingPerms: []*models.RobotPermission{
{
Kind: "project",
Namespace: "library",
Access: []*models.Access{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
creatorPerms: []*robot.Permission{
{
Kind: "project",
Namespace: "other",
Access: []*types.Policy{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
expected: false,
},
{
name: "Project - empty creator",
creatingPerms: []*models.RobotPermission{
{
Kind: "project",
Namespace: "library",
Access: []*models.Access{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
creatorPerms: []*robot.Permission{},
expected: false,
},
{
name: "Project - empty creating",
creatingPerms: []*models.RobotPermission{},
creatorPerms: []*robot.Permission{
{
Kind: "project",
Namespace: "library",
Access: []*types.Policy{
{Resource: "repository", Action: "pull", Effect: "allow"},
},
},
},
expected: true,
},
{
name: "System - subset",
creatingPerms: []*models.RobotPermission{
{
Kind: "system",
Namespace: "admin",
Access: []*models.Access{
{Resource: "user", Action: "create", Effect: "allow"},
},
},
},
creatorPerms: []*robot.Permission{
{
Kind: "system",
Namespace: "admin",
Access: []*types.Policy{
{Resource: "user", Action: "create", Effect: "allow"},
{Resource: "user", Action: "delete", Effect: "allow"},
},
},
},
expected: true,
},
{
name: "System - not subset",
creatingPerms: []*models.RobotPermission{
{
Kind: "system",
Namespace: "admin",
Access: []*models.Access{
{Resource: "user", Action: "delete", Effect: "allow"},
},
},
},
creatorPerms: []*robot.Permission{
{
Kind: "system",
Namespace: "admin",
Access: []*types.Policy{
{Resource: "user", Action: "create", Effect: "allow"},
},
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isValidPermissionScope(tt.creatingPerms, tt.creatorPerms)
assert.Equal(t, tt.expected, result)
})
}
}