From bcb7337f4f5b0fc6223123ac3fed96b1972463d3 Mon Sep 17 00:00:00 2001 From: wang yan Date: Mon, 15 Jul 2024 17:14:45 +0800 Subject: [PATCH 1/2] 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 --- src/common/const.go | 3 + src/common/rbac/const.go | 93 ++++++++++++++++++- src/common/rbac/const_test.go | 54 +++++++++++ src/lib/config/metadata/metadatalist.go | 2 + src/lib/config/userconfig.go | 5 + .../system-robot-util.ts | 5 + src/portal/src/i18n/lang/de-de-lang.json | 7 +- src/portal/src/i18n/lang/en-us-lang.json | 7 +- src/portal/src/i18n/lang/es-es-lang.json | 7 +- src/portal/src/i18n/lang/fr-fr-lang.json | 7 +- src/portal/src/i18n/lang/ko-kr-lang.json | 7 +- src/portal/src/i18n/lang/pt-br-lang.json | 7 +- src/portal/src/i18n/lang/tr-tr-lang.json | 7 +- src/portal/src/i18n/lang/zh-cn-lang.json | 7 +- src/portal/src/i18n/lang/zh-tw-lang.json | 7 +- src/server/v2.0/handler/permissions.go | 5 +- src/server/v2.0/handler/robot.go | 11 ++- 17 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 src/common/rbac/const_test.go diff --git a/src/common/const.go b/src/common/const.go index a8166cea3a2..67aff0a1879 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -247,4 +247,7 @@ const ( // Global Leeway used for token validation JwtLeeway = 60 * time.Second + + // Global Leeway used for token validation + EnableRobotFullAccess = "enable_robot_full_access" ) diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index a783e71d4ac..d8ce1260314 100644 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -14,7 +14,12 @@ package rbac -import "github.com/goharbor/harbor/src/pkg/permission/types" +import ( + "context" + + "github.com/goharbor/harbor/src/lib/config" + "github.com/goharbor/harbor/src/pkg/permission/types" +) // const action variables const ( @@ -81,9 +86,89 @@ const ( 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(ctx context.Context) RobotPermissionProvider { + var permissionProvider RobotPermissionProvider + permissionProvider = &BaseProvider{} + if config.RobotFullAccess(ctx) { + permissionProvider = &NolimitProvider{} + } + return permissionProvider +} + +// 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: 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 ( - PoliciesMap = map[string][]*types.Policy{ - "System": { + PoliciesMap = map[scope][]*types.Policy{ + ScopeSystem: { {Resource: ResourceAuditLog, Action: ActionList}, {Resource: ResourcePreatInstance, Action: ActionRead}, @@ -154,7 +239,7 @@ var ( {Resource: ResourceQuota, Action: ActionRead}, {Resource: ResourceQuota, Action: ActionList}, }, - "Project": { + ScopeProject: { {Resource: ResourceLog, Action: ActionList}, {Resource: ResourceProject, Action: ActionRead}, diff --git a/src/common/rbac/const_test.go b/src/common/rbac/const_test.go new file mode 100644 index 00000000000..34979bbc5c7 --- /dev/null +++ b/src/common/rbac/const_test.go @@ -0,0 +1,54 @@ +package rbac + +import ( + "context" + + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/lib/config" + _ "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) { + cfg := map[string]interface{}{ + common.EnableRobotFullAccess: "false", + } + config.InitWithSettings(cfg) + + defaultPro := GetPermissionProvider(context.Background()) + _, ok := defaultPro.(*BaseProvider) + assert.True(t, ok) + + cfg = map[string]interface{}{ + common.EnableRobotFullAccess: "true", + } + config.InitWithSettings(cfg) + defaultPro = GetPermissionProvider(context.Background()) + _, ok = defaultPro.(*NolimitProvider) + assert.True(t, ok) + +} diff --git a/src/lib/config/metadata/metadatalist.go b/src/lib/config/metadata/metadatalist.go index aab4919fd89..bfa1b59632a 100644 --- a/src/lib/config/metadata/metadatalist.go +++ b/src/lib/config/metadata/metadatalist.go @@ -201,5 +201,7 @@ var ( {Name: common.BeegoMaxMemoryBytes, Scope: SystemScope, Group: BasicGroup, EnvKey: "BEEGO_MAX_MEMORY_BYTES", DefaultValue: fmt.Sprintf("%d", common.DefaultBeegoMaxMemoryBytes), ItemType: &Int64Type{}, Editable: false, Description: `The bytes for limiting the beego max memory, default is 128GB`}, {Name: common.BeegoMaxUploadSizeBytes, Scope: SystemScope, Group: BasicGroup, EnvKey: "BEEGO_MAX_UPLOAD_SIZE_BYTES", DefaultValue: fmt.Sprintf("%d", common.DefaultBeegoMaxUploadSizeBytes), ItemType: &Int64Type{}, Editable: false, Description: `The bytes for limiting the beego max upload size, default it 128GB`}, + + {Name: common.EnableRobotFullAccess, Scope: SystemScope, Group: BasicGroup, EnvKey: "ENABLE_ROBOT_FULL_ACCESS", DefaultValue: "false", ItemType: &BoolType{}, Editable: true, Description: `The flag indicates if a robot is able to access full entry points of harbor`}, } ) diff --git a/src/lib/config/userconfig.go b/src/lib/config/userconfig.go index 4012097c9e3..0f28dff95d2 100644 --- a/src/lib/config/userconfig.go +++ b/src/lib/config/userconfig.go @@ -257,6 +257,11 @@ func ScannerSkipUpdatePullTime(ctx context.Context) bool { return DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool() } +// RobotFullAccess returns a bool to indicate if the robot can access full entry points +func RobotFullAccess(ctx context.Context) bool { + return DefaultMgr().Get(ctx, common.EnableRobotFullAccess).GetBool() +} + // BannerMessage returns the customized banner message func BannerMessage(ctx context.Context) string { return DefaultMgr().Get(ctx, common.BannerMessage).GetString() diff --git a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts index 6e3b4097c56..627d1543da2 100644 --- a/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts +++ b/src/portal/src/app/base/left-side-nav/system-robot-accounts/system-robot-util.ts @@ -79,6 +79,11 @@ export const ACTION_RESOURCE_I18N_MAP = { 'notification-policy': 'ROBOT_ACCOUNT.NOTIFICATION_POLICY', quota: 'ROBOT_ACCOUNT.QUOTA', 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) { diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index b81356b40e3..bde3bbebd07 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -423,7 +423,12 @@ "SELECT_PERMISSIONS": "Select Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System 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": { "EDIT_BUTTON": "EDIT", diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 1117353d3bb..78b4261a88a 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -423,7 +423,12 @@ "SELECT_PERMISSIONS": "Select Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System 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": { "EDIT_BUTTON": "EDIT", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 21560b47094..722d26cdb48 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -424,7 +424,12 @@ "SELECT_PERMISSIONS": "Select Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System 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": { "EDIT_BUTTON": "EDIT", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index fdd45c0dd63..5d9f01c9f13 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -423,7 +423,12 @@ "SELECT_PERMISSIONS": "Selectionner les permissions", "SELECT_SYSTEM_PERMISSIONS": "Selectionner les permissions système", "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": { "EDIT_BUTTON": "Éditer", diff --git a/src/portal/src/i18n/lang/ko-kr-lang.json b/src/portal/src/i18n/lang/ko-kr-lang.json index e082fba2793..54ae4e64e4d 100644 --- a/src/portal/src/i18n/lang/ko-kr-lang.json +++ b/src/portal/src/i18n/lang/ko-kr-lang.json @@ -420,7 +420,12 @@ "SELECT_PERMISSIONS": "권한 선택", "SELECT_SYSTEM_PERMISSIONS": "시스템 권한 선택", "SELECT_PROJECT_PERMISSIONS": "프로젝트 권한 선택", - "SYSTEM_PERMISSIONS": "시스템 권한" + "SYSTEM_PERMISSIONS": "시스템 권한", + "ROBOT": "Robot Account", + "USER": "User", + "LDAPUSER": "LDAP User", + "GROUP": "User Group", + "MEMBER": "Project Member" }, "WEBHOOK": { "EDIT_BUTTON": "편집", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 35debc986c4..2d1aa01ce64 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -421,7 +421,12 @@ "SELECT_PERMISSIONS": "Select Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System 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": "Grupo", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 6e6b2f3feec..040949e74ae 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -423,7 +423,12 @@ "SELECT_PERMISSIONS": "Select Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System 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": { "EDIT_BUTTON": "DÜZENLE", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index a3a976bf94e..bd775c248bc 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -421,7 +421,12 @@ "SELECT_PERMISSIONS": "选择权限", "SELECT_SYSTEM_PERMISSIONS": "选择系统权限", "SELECT_PROJECT_PERMISSIONS": "选择项目权限", - "SYSTEM_PERMISSIONS": "系统权限" + "SYSTEM_PERMISSIONS": "系统权限", + "ROBOT": "机器人账户", + "USER": "用户", + "LDAPUSER": "LDAP 用户", + "GROUP": "用户组", + "MEMBER": "项目成员" }, "WEBHOOK": { "EDIT_BUTTON": "编辑", diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index 9252bf151c6..f2b3f1ba025 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -422,7 +422,12 @@ "SELECT_PERMISSIONS": "Select Permissions", "SELECT_SYSTEM_PERMISSIONS": "Select System 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": { "EDIT_BUTTON": "編輯", diff --git a/src/server/v2.0/handler/permissions.go b/src/server/v2.0/handler/permissions.go index 0189abf2379..530f299df9c 100644 --- a/src/server/v2.0/handler/permissions.go +++ b/src/server/v2.0/handler/permissions.go @@ -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"))) } + provider := rbac.GetPermissionProvider(ctx) sysPermissions := make([]*types.Policy, 0) - proPermissions := rbac.PoliciesMap["Project"] + proPermissions := provider.GetPermissions(rbac.ScopeProject) if isSystemAdmin { // 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)) diff --git a/src/server/v2.0/handler/robot.go b/src/server/v2.0/handler/robot.go index 8137775e3a4..191bce7a3af 100644 --- a/src/server/v2.0/handler/robot.go +++ b/src/server/v2.0/handler/robot.go @@ -56,7 +56,7 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo return rAPI.SendError(ctx, err) } - if err := rAPI.validate(params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { + if err := rAPI.validate(ctx, params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { return rAPI.SendError(ctx, err) } @@ -292,7 +292,7 @@ func (rAPI *robotAPI) requireAccess(ctx context.Context, level string, projectID } // more validation -func (rAPI *robotAPI) validate(d int64, level string, permissions []*models.RobotPermission) error { +func (rAPI *robotAPI) validate(ctx context.Context, d int64, level string, permissions []*models.RobotPermission) error { if !isValidDuration(d) { return errors.New(nil).WithMessage("bad request error duration input: %d, duration must be either -1(Never) or a positive integer", d).WithCode(errors.BadRequestCode) } @@ -316,17 +316,18 @@ func (rAPI *robotAPI) validate(d int64, level string, permissions []*models.Robo return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode) } + provider := rbac.GetPermissionProvider(ctx) // to validate the access scope for _, perm := range permissions { if perm.Kind == robot.LEVELSYSTEM { - polices := rbac.PoliciesMap["System"] + polices := provider.GetPermissions(rbac.ScopeSystem) for _, acc := range perm.Access { if !containsAccess(polices, acc) { return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode) } } } else if perm.Kind == robot.LEVELPROJECT { - polices := rbac.PoliciesMap["Project"] + polices := provider.GetPermissions(rbac.ScopeProject) for _, acc := range perm.Access { if !containsAccess(polices, acc) { return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode) @@ -344,7 +345,7 @@ func (rAPI *robotAPI) updateV2Robot(ctx context.Context, params operation.Update if params.Robot.Duration == nil { params.Robot.Duration = &r.Duration } - if err := rAPI.validate(*params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { + if err := rAPI.validate(ctx, *params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { return err } if r.Level != robot.LEVELSYSTEM { From 150756fe94f56301377d97ba3aeb46cc5f7bb4e3 Mon Sep 17 00:00:00 2001 From: wang yan Date: Fri, 16 Aug 2024 17:47:27 +0800 Subject: [PATCH 2/2] robot account permission enhancement Update codes according to the proposal of https://github.com/goharbor/community/pull/249 Signed-off-by: wang yan --- src/common/const.go | 3 - src/common/rbac/const.go | 16 +- src/common/rbac/const_test.go | 22 +-- src/controller/robot/controller.go | 36 ++-- src/controller/robot/model.go | 9 +- src/controller/scan/base_controller.go | 3 +- src/controller/scan/base_controller_test.go | 3 +- src/lib/config/metadata/metadatalist.go | 2 - src/lib/config/userconfig.go | 5 - src/server/v2.0/handler/permissions.go | 2 +- src/server/v2.0/handler/robot.go | 168 +++++++++++++++--- src/server/v2.0/handler/robot_test.go | 187 +++++++++++++++++++- 12 files changed, 359 insertions(+), 97 deletions(-) diff --git a/src/common/const.go b/src/common/const.go index 67aff0a1879..a8166cea3a2 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -247,7 +247,4 @@ const ( // Global Leeway used for token validation JwtLeeway = 60 * time.Second - - // Global Leeway used for token validation - EnableRobotFullAccess = "enable_robot_full_access" ) diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index d8ce1260314..7594efb997c 100644 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -15,9 +15,6 @@ package rbac import ( - "context" - - "github.com/goharbor/harbor/src/lib/config" "github.com/goharbor/harbor/src/pkg/permission/types" ) @@ -99,13 +96,9 @@ type RobotPermissionProvider interface { } // GetPermissionProvider gives the robot permission provider -func GetPermissionProvider(ctx context.Context) RobotPermissionProvider { - var permissionProvider RobotPermissionProvider - permissionProvider = &BaseProvider{} - if config.RobotFullAccess(ctx) { - permissionProvider = &NolimitProvider{} - } - return permissionProvider +func GetPermissionProvider() RobotPermissionProvider { + // TODO will determine by the ui configuration + return &NolimitProvider{} } // BaseProvider ... @@ -141,6 +134,9 @@ func (n *NolimitProvider) GetPermissions(s scope) []*types.Policy { &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}, diff --git a/src/common/rbac/const_test.go b/src/common/rbac/const_test.go index 34979bbc5c7..9a794b86465 100644 --- a/src/common/rbac/const_test.go +++ b/src/common/rbac/const_test.go @@ -1,10 +1,6 @@ package rbac import ( - "context" - - "github.com/goharbor/harbor/src/common" - "github.com/goharbor/harbor/src/lib/config" _ "github.com/goharbor/harbor/src/pkg/config/inmemory" "github.com/stretchr/testify/assert" @@ -34,21 +30,7 @@ func TestNolimitProvider(t *testing.T) { } func TestGetPermissionProvider(t *testing.T) { - cfg := map[string]interface{}{ - common.EnableRobotFullAccess: "false", - } - config.InitWithSettings(cfg) - - defaultPro := GetPermissionProvider(context.Background()) - _, ok := defaultPro.(*BaseProvider) + defaultPro := GetPermissionProvider() + _, ok := defaultPro.(*NolimitProvider) assert.True(t, ok) - - cfg = map[string]interface{}{ - common.EnableRobotFullAccess: "true", - } - config.InitWithSettings(cfg) - defaultPro = GetPermissionProvider(context.Background()) - _, ok = defaultPro.(*NolimitProvider) - assert.True(t, ok) - } diff --git a/src/controller/robot/controller.go b/src/controller/robot/controller.go index 21b17afdf64..0132eba5cd4 100644 --- a/src/controller/robot/controller.go +++ b/src/controller/robot/controller.go @@ -97,10 +97,6 @@ func (d *controller) Count(ctx context.Context, query *q.Query) (int64, error) { // Create ... 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 if r.Duration == -1 { expiresAt = -1 @@ -327,22 +323,6 @@ func (d *controller) populatePermissions(ctx context.Context, r *Robot) error { 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 // /system => Kind: system 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) } +// 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) { var secret, pwd string options := []retry.Option{ diff --git a/src/controller/robot/model.go b/src/controller/robot/model.go index 32d6a0c8a07..375483cdd84 100644 --- a/src/controller/robot/model.go +++ b/src/controller/robot/model.go @@ -39,10 +39,11 @@ const ( // Robot ... type Robot struct { model.Robot - ProjectName string - Level string - Editable bool `json:"editable"` - Permissions []*Permission `json:"permissions"` + ProjectName string + ProjectNameOrID interface{} + Level string + Editable bool `json:"editable"` + Permissions []*Permission `json:"permissions"` } // IsSysLevel, true is a system level robot, others are project level. diff --git a/src/controller/scan/base_controller.go b/src/controller/scan/base_controller.go index 3b087315fb8..04213eb4c3c 100644 --- a/src/controller/scan/base_controller.go +++ b/src/controller/scan/base_controller.go @@ -867,7 +867,8 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64 CreatorType: "local", CreatorRef: int64(0), }, - Level: robot.LEVELPROJECT, + ProjectName: projectName, + Level: robot.LEVELPROJECT, Permissions: []*robot.Permission{ { Kind: "project", diff --git a/src/controller/scan/base_controller_test.go b/src/controller/scan/base_controller_test.go index 97b2530b905..4133babf67b 100644 --- a/src/controller/scan/base_controller_test.go +++ b/src/controller/scan/base_controller_test.go @@ -238,7 +238,8 @@ func (suite *ControllerTestSuite) SetupSuite() { CreatorType: "local", CreatorRef: int64(0), }, - Level: robot.LEVELPROJECT, + ProjectName: "library", + Level: robot.LEVELPROJECT, Permissions: []*robot.Permission{ { Kind: "project", diff --git a/src/lib/config/metadata/metadatalist.go b/src/lib/config/metadata/metadatalist.go index bfa1b59632a..aab4919fd89 100644 --- a/src/lib/config/metadata/metadatalist.go +++ b/src/lib/config/metadata/metadatalist.go @@ -201,7 +201,5 @@ var ( {Name: common.BeegoMaxMemoryBytes, Scope: SystemScope, Group: BasicGroup, EnvKey: "BEEGO_MAX_MEMORY_BYTES", DefaultValue: fmt.Sprintf("%d", common.DefaultBeegoMaxMemoryBytes), ItemType: &Int64Type{}, Editable: false, Description: `The bytes for limiting the beego max memory, default is 128GB`}, {Name: common.BeegoMaxUploadSizeBytes, Scope: SystemScope, Group: BasicGroup, EnvKey: "BEEGO_MAX_UPLOAD_SIZE_BYTES", DefaultValue: fmt.Sprintf("%d", common.DefaultBeegoMaxUploadSizeBytes), ItemType: &Int64Type{}, Editable: false, Description: `The bytes for limiting the beego max upload size, default it 128GB`}, - - {Name: common.EnableRobotFullAccess, Scope: SystemScope, Group: BasicGroup, EnvKey: "ENABLE_ROBOT_FULL_ACCESS", DefaultValue: "false", ItemType: &BoolType{}, Editable: true, Description: `The flag indicates if a robot is able to access full entry points of harbor`}, } ) diff --git a/src/lib/config/userconfig.go b/src/lib/config/userconfig.go index 0f28dff95d2..4012097c9e3 100644 --- a/src/lib/config/userconfig.go +++ b/src/lib/config/userconfig.go @@ -257,11 +257,6 @@ func ScannerSkipUpdatePullTime(ctx context.Context) bool { return DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool() } -// RobotFullAccess returns a bool to indicate if the robot can access full entry points -func RobotFullAccess(ctx context.Context) bool { - return DefaultMgr().Get(ctx, common.EnableRobotFullAccess).GetBool() -} - // BannerMessage returns the customized banner message func BannerMessage(ctx context.Context) string { return DefaultMgr().Get(ctx, common.BannerMessage).GetString() diff --git a/src/server/v2.0/handler/permissions.go b/src/server/v2.0/handler/permissions.go index 530f299df9c..192e88a2758 100644 --- a/src/server/v2.0/handler/permissions.go +++ b/src/server/v2.0/handler/permissions.go @@ -71,7 +71,7 @@ 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"))) } - provider := rbac.GetPermissionProvider(ctx) + provider := rbac.GetPermissionProvider() sysPermissions := make([]*types.Policy, 0) proPermissions := provider.GetPermissions(rbac.ScopeProject) if isSystemAdmin { diff --git a/src/server/v2.0/handler/robot.go b/src/server/v2.0/handler/robot.go index 191bce7a3af..119c29fce23 100644 --- a/src/server/v2.0/handler/robot.go +++ b/src/server/v2.0/handler/robot.go @@ -31,8 +31,10 @@ import ( "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/controller/robot" "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/log" + "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg/permission/types" pkg "github.com/goharbor/harbor/src/pkg/robot/model" "github.com/goharbor/harbor/src/server/v2.0/handler/model" @@ -56,16 +58,27 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo return rAPI.SendError(ctx, err) } - if err := rAPI.validate(ctx, params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { + if err := rAPI.validate(params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { 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) } - sc, err := rAPI.GetSecurityContext(ctx) - if err != nil { + r := &robot.Robot{ + 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) } @@ -78,23 +91,36 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo default: return rAPI.SendError(ctx, errors.New(nil).WithMessage("invalid security context")) } - - r := &robot.Robot{ - 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, - } + r.CreatorType = sc.Name() + r.CreatorRef = creatorRef 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) } + 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) if err != nil { return rAPI.SendError(ctx, err) @@ -125,7 +151,7 @@ func (rAPI *robotAPI) DeleteRobot(ctx context.Context, params operation.DeleteRo 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) } @@ -174,7 +200,11 @@ func (rAPI *robotAPI) ListRobot(ctx context.Context, params operation.ListRobotP } 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) } @@ -212,7 +242,7 @@ func (rAPI *robotAPI) GetRobotByID(ctx context.Context, params operation.GetRobo if err != nil { 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) } @@ -253,7 +283,7 @@ func (rAPI *robotAPI) RefreshSec(ctx context.Context, params operation.RefreshSe 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) } @@ -282,17 +312,26 @@ func (rAPI *robotAPI) RefreshSec(ctx context.Context, params operation.RefreshSe return operation.NewRefreshSecOK().WithPayload(robotSec) } -func (rAPI *robotAPI) requireAccess(ctx context.Context, level string, projectIDOrName interface{}, action rbac.Action) error { - if level == robot.LEVELSYSTEM { +func (rAPI *robotAPI) requireAccess(ctx context.Context, r *robot.Robot, action rbac.Action) error { + if r.Level == robot.LEVELSYSTEM { return rAPI.RequireSystemAccess(ctx, action, rbac.ResourceRobot) - } else if level == robot.LEVELPROJECT { - return rAPI.RequireProjectAccess(ctx, projectIDOrName, action, rbac.ResourceRobot) + } else if r.Level == robot.LEVELPROJECT { + 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) } // more validation -func (rAPI *robotAPI) validate(ctx context.Context, d int64, level string, permissions []*models.RobotPermission) error { +func (rAPI *robotAPI) validate(d int64, level string, permissions []*models.RobotPermission) error { if !isValidDuration(d) { return errors.New(nil).WithMessage("bad request error duration input: %d, duration must be either -1(Never) or a positive integer", d).WithCode(errors.BadRequestCode) } @@ -316,7 +355,7 @@ func (rAPI *robotAPI) validate(ctx context.Context, d int64, level string, permi return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode) } - provider := rbac.GetPermissionProvider(ctx) + provider := rbac.GetPermissionProvider() // to validate the access scope for _, perm := range permissions { if perm.Kind == robot.LEVELSYSTEM { @@ -345,7 +384,7 @@ func (rAPI *robotAPI) updateV2Robot(ctx context.Context, params operation.Update if params.Robot.Duration == nil { params.Robot.Duration = &r.Duration } - if err := rAPI.validate(ctx, *params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { + if err := rAPI.validate(*params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil { return err } if r.Level != robot.LEVELSYSTEM { @@ -357,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") } } - 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 } if params.Robot.Level != r.Level || params.Robot.Name != r.Name { @@ -381,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{ WithPermission: true, }); err != nil { @@ -415,3 +491,39 @@ func containsAccess(policies []*types.Policy, item *models.Access) bool { } 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 +} diff --git a/src/server/v2.0/handler/robot_test.go b/src/server/v2.0/handler/robot_test.go index e3cfe076ee4..9cd64de3e1c 100644 --- a/src/server/v2.0/handler/robot_test.go +++ b/src/server/v2.0/handler/robot_test.go @@ -1,10 +1,15 @@ package handler import ( - "github.com/goharbor/harbor/src/common/rbac" - "github.com/goharbor/harbor/src/server/v2.0/models" "math" "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) { @@ -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) + }) + } +}