diff --git a/src/common/security/robot/context.go b/src/common/security/robot/context.go index 5c175046d94..5d8e7dd56f6 100644 --- a/src/common/security/robot/context.go +++ b/src/common/security/robot/context.go @@ -90,10 +90,11 @@ func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource var accesses []*types.Policy for _, p := range s.robot.Permissions { for _, a := range p.Access { + policyResource := getPolicyResource(p, a) accesses = append(accesses, &types.Policy{ Action: a.Action, Effect: a.Effect, - Resource: types.Resource(getPolicyResource(p, a)), + Resource: types.Resource(policyResource), }) } } @@ -113,13 +114,14 @@ func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource evaluators = evaluators.Add(system.NewEvaluator(s.GetUsername(), sysPolicies)) } if len(proPolicies) != 0 { - evaluators = evaluators.Add(rbac_project.NewEvaluator(s.ctl, rbac_project.NewBuilderForPolicies(s.GetUsername(), proPolicies))) + evaluators = evaluators.Add(rbac_project.NewEvaluator(s.ctl, rbac_project.NewBuilderForPolicies(s.GetUsername(), proPolicies, filterRobotPolicies))) } s.evaluator = evaluators } else { s.evaluator = rbac_project.NewEvaluator(s.ctl, rbac_project.NewBuilderForPolicies(s.GetUsername(), accesses, filterRobotPolicies)) } }) + return s.evaluator != nil && s.evaluator.HasPermission(ctx, resource, action) } diff --git a/src/server/v2.0/handler/robot.go b/src/server/v2.0/handler/robot.go index 221a0725779..f198752d627 100644 --- a/src/server/v2.0/handler/robot.go +++ b/src/server/v2.0/handler/robot.go @@ -112,6 +112,28 @@ func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRo if err != nil { return rAPI.SendError(ctx, err) } + + // If no creator robots found in the specific project, check for system robots with wildcard permissions + if len(creatorRobots) == 0 && r.Level == robot.LEVELPROJECT { + systemCreatorRobots, err := rAPI.robotCtl.List(ctx, q.New(q.KeyWords{ + "name": strings.TrimPrefix(sc.GetUsername(), config.RobotPrefix(ctx)), + "project_id": 0, // System robots have project_id = 0 + }), &robot.Option{ + WithPermission: true, + }) + if err != nil { + return rAPI.SendError(ctx, err) + } + + // Check if any system robot has wildcard project permissions for robot creation + for _, sysRobot := range systemCreatorRobots { + if rAPI.hasWildcardRobotPermission(sysRobot, rbac.ActionCreate) { + creatorRobots = systemCreatorRobots + break + } + } + } + if len(creatorRobots) == 0 { return rAPI.SendError(ctx, errors.DeniedError(nil)) } @@ -315,6 +337,15 @@ func (rAPI *robotAPI) RefreshSec(ctx context.Context, params operation.RefreshSe } func (rAPI *robotAPI) requireAccess(ctx context.Context, r *robot.Robot, action rbac.Action) error { + sc, _ := rAPI.GetSecurityContext(ctx) + + // Special case: system robots with wildcard project permissions + if robotSc, ok := sc.(*robotSc.SecurityContext); ok && robotSc.User().Level == robot.LEVELSYSTEM { + if r.Level == robot.LEVELPROJECT && rAPI.hasWildcardRobotPermission(robotSc.User(), action) { + return nil // Allow system robots with wildcard permissions + } + } + if r.Level == robot.LEVELSYSTEM { return rAPI.RequireSystemAccess(ctx, action, rbac.ResourceRobot) } else if r.Level == robot.LEVELPROJECT { @@ -496,3 +527,17 @@ func isValidPermissionScope(creating []*models.RobotPermission, creator []*robot } return true } + +// hasWildcardRobotPermission checks if a robot has wildcard project permissions for the specified action on robot resources +func (rAPI *robotAPI) hasWildcardRobotPermission(robot *robot.Robot, action rbac.Action) bool { + for _, perm := range robot.Permissions { + if perm.Kind == "project" && perm.Namespace == "*" { + for _, access := range perm.Access { + if access.Resource == "robot" && access.Action == action { + return true + } + } + } + } + return false +} diff --git a/src/server/v2.0/handler/robot_test.go b/src/server/v2.0/handler/robot_test.go index fdcf326e473..235e95c0e56 100644 --- a/src/server/v2.0/handler/robot_test.go +++ b/src/server/v2.0/handler/robot_test.go @@ -480,3 +480,158 @@ func TestValidPermissionScope(t *testing.T) { }) } } + +func TestHasWildcardRobotPermission(t *testing.T) { + rAPI := &robotAPI{} + + tests := []struct { + name string + robot *robot.Robot + action rbac.Action + expected bool + }{ + { + name: "Robot with wildcard project permissions for robot:create", + robot: &robot.Robot{ + Permissions: []*robot.Permission{ + { + Kind: "project", + Namespace: "*", + Access: []*types.Policy{ + {Resource: "robot", Action: "create", Effect: "allow"}, + }, + }, + }, + }, + action: rbac.ActionCreate, + expected: true, + }, + { + name: "Robot with wildcard project permissions for robot:delete", + robot: &robot.Robot{ + Permissions: []*robot.Permission{ + { + Kind: "project", + Namespace: "*", + Access: []*types.Policy{ + {Resource: "robot", Action: "delete", Effect: "allow"}, + }, + }, + }, + }, + action: rbac.ActionDelete, + expected: true, + }, + { + name: "Robot with wildcard project permissions but wrong resource", + robot: &robot.Robot{ + Permissions: []*robot.Permission{ + { + Kind: "project", + Namespace: "*", + Access: []*types.Policy{ + {Resource: "repository", Action: "create", Effect: "allow"}, + }, + }, + }, + }, + action: rbac.ActionCreate, + expected: false, + }, + { + name: "Robot with wildcard project permissions but wrong action", + robot: &robot.Robot{ + Permissions: []*robot.Permission{ + { + Kind: "project", + Namespace: "*", + Access: []*types.Policy{ + {Resource: "robot", Action: "read", Effect: "allow"}, + }, + }, + }, + }, + action: rbac.ActionCreate, + expected: false, + }, + { + name: "Robot with specific project permissions (not wildcard)", + robot: &robot.Robot{ + Permissions: []*robot.Permission{ + { + Kind: "project", + Namespace: "library", + Access: []*types.Policy{ + {Resource: "robot", Action: "create", Effect: "allow"}, + }, + }, + }, + }, + action: rbac.ActionCreate, + expected: false, + }, + { + name: "Robot with system level permissions", + robot: &robot.Robot{ + Permissions: []*robot.Permission{ + { + Kind: "system", + Namespace: "/", + Access: []*types.Policy{ + {Resource: "robot", Action: "create", Effect: "allow"}, + }, + }, + }, + }, + action: rbac.ActionCreate, + expected: false, + }, + { + name: "Robot with multiple permissions including wildcard robot:create", + robot: &robot.Robot{ + Permissions: []*robot.Permission{ + { + Kind: "system", + Namespace: "/", + Access: []*types.Policy{ + {Resource: "user", Action: "create", Effect: "allow"}, + }, + }, + { + Kind: "project", + Namespace: "*", + Access: []*types.Policy{ + {Resource: "repository", Action: "pull", Effect: "allow"}, + {Resource: "robot", Action: "create", Effect: "allow"}, + }, + }, + }, + }, + action: rbac.ActionCreate, + expected: true, + }, + { + name: "Robot with no permissions", + robot: &robot.Robot{ + Permissions: []*robot.Permission{}, + }, + action: rbac.ActionCreate, + expected: false, + }, + { + name: "Robot is nil", + robot: &robot.Robot{ + Permissions: nil, + }, + action: rbac.ActionCreate, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := rAPI.hasWildcardRobotPermission(tt.robot, tt.action) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/tests/apitests/python/test_robot_account.py b/tests/apitests/python/test_robot_account.py index 6d7db141eb5..26c5ed2f0e1 100644 --- a/tests/apitests/python/test_robot_account.py +++ b/tests/apitests/python/test_robot_account.py @@ -3,7 +3,7 @@ import sys import unittest -from testutils import ADMIN_CLIENT, TEARDOWN, harbor_server, harbor_url, suppress_urllib3_warning +from testutils import ADMIN_CLIENT, TEARDOWN, harbor_server, suppress_urllib3_warning from testutils import created_user, created_project from library.user import User from library.project import Project @@ -341,6 +341,115 @@ def test_02_SystemlevelRobotAccount(self): project_access_list.append(dict(project_name = projects[i].name, project_id = projects[i].project_id, check_list = all_true_access_list)) self.verify_repository_pushable(project_access_list, SYSTEM_RA_CLIENT_COVER_ALL) + def test_03_SystemRobotCreatesProjectRobot(self): + """ + Test case: Verify system-level robot account can create project-level robot accounts + + Test steps: + 1. Create a test project using admin credentials + 2. Create a system-level robot account with robot creation permissions for the project + 3. Use the system robot credentials to create a project-level robot account + 4. Verify the project robot was created successfully + 5. Clean up: Delete created robots and project + """ + + # Step 1: Create a test project using admin credentials + project_id, project_name = self.project.create_project( + metadata={"public": "false"}, **ADMIN_CLIENT + ) + print("Created project: {} (ID: {})".format(project_name, project_id)) + + # Step 2: Create system-level robot with robot creation permissions + # Define permissions: robot resource with create action at system level + # Also include repository:pull since the robot library forces pull permissions + robot_access = v2_swagger_client.Access(resource="robot", action="create") + repository_access = v2_swagger_client.Access( + resource="repository", action="pull" + ) + robot_permission = v2_swagger_client.RobotPermission( + kind="project", namespace="*", access=[robot_access, repository_access] + ) + + system_robot_id, system_robot = self.robot.create_system_robot( + permission_list=[robot_permission], + duration=300, # 5 minutes + robot_name="test-system-robot-creator", + robot_desc="System robot for testing project robot creation", + ) + print("Created system robot: {}".format(system_robot.name)) + + # System robot client configuration + SYSTEM_ROBOT_CLIENT = dict( + endpoint=TestRobotAccount.url, + username=system_robot.name, + password=system_robot.secret, + ) + + # Step 3: Use system robot to create a project-level robot + try: + project_robot_id, project_robot = self.robot.create_project_robot( + project_name=project_name, + duration=300, # 5 minutes + robot_name="test-project-robot-by-system", + robot_desc="Project robot created by system robot", + has_pull_right=False, + has_push_right=False, + **SYSTEM_ROBOT_CLIENT + ) + print( + "SUCCESS: System robot created project robot: {}".format( + project_robot.name + ) + ) + + # Step 4: Verify the project robot was created and has correct properties + retrieved_robot = self.robot.get_robot_account_by_id( + project_robot_id, **ADMIN_CLIENT + ) + _assert_status_code( + "project", + retrieved_robot.level, + "Expected level 'project', got '{}'".format(retrieved_robot.level), + ) + _assert_status_code( + 1, + len(retrieved_robot.permissions), + "Expected 1 permission, got {}".format( + len(retrieved_robot.permissions) + ), + ) + _assert_status_code( + project_name, + retrieved_robot.permissions[0].namespace, + "Expected namespace '{}', got '{}'".format( + project_name, retrieved_robot.permissions[0].namespace + ), + ) + + print("SUCCESS: Project robot verification passed") + + except Exception as e: + print( + "FAILED: System robot could not create project robot: {}".format(str(e)) + ) + raise e + + finally: + # Step 5: Clean up + try: + if "project_robot_id" in locals(): + self.robot.delete_robot_account(project_robot_id, **ADMIN_CLIENT) + print("Cleaned up project robot: {}".format(project_robot_id)) + + self.robot.delete_robot_account(system_robot_id, **ADMIN_CLIENT) + print("Cleaned up system robot: {}".format(system_robot_id)) + + self.project.delete_project(project_id, **ADMIN_CLIENT) + print("Cleaned up project: {}".format(project_name)) + + except Exception as cleanup_error: + print("Warning: Cleanup error: {}".format(cleanup_error)) + if __name__ == '__main__': suite = unittest.TestSuite(unittest.makeSuite(TestRobotAccount)) result = unittest.TextTestRunner(sys.stdout, verbosity=2, failfast=True).run(suite)