Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/common/security/robot/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})
}
}
Expand All @@ -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)
}

Expand Down
45 changes: 45 additions & 0 deletions src/server/v2.0/handler/robot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
155 changes: 155 additions & 0 deletions src/server/v2.0/handler/robot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
111 changes: 110 additions & 1 deletion tests/apitests/python/test_robot_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading