diff --git a/src/common/const.go b/src/common/const.go index 67aff0a18793..a8166cea3a2d 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 d8ce12603147..7594efb997cb 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 34979bbc5c72..9a794b864654 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 21b17afdf64b..0132eba5cd41 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 32d6a0c8a079..375483cdd84c 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 3b087315fb82..04213eb4c3c5 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/lib/config/metadata/metadatalist.go b/src/lib/config/metadata/metadatalist.go index bfa1b59632af..aab4919fd89f 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 0f28dff95d27..4012097c9e3c 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 530f299df9c4..192e88a27586 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 191bce7a3af6..119c29fce230 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 e3cfe076ee4b..9cd64de3e1cc 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) + }) + } +}