feat: support role copy

This commit is contained in:
fatelei 2026-05-09 20:56:02 +08:00
parent 197f5cd03f
commit 7051b574ce
No known key found for this signature in database
GPG Key ID: 2F91DA05646F4EED
4 changed files with 48 additions and 4 deletions

View File

@ -146,6 +146,15 @@ class RBACRoleItemApi(Resource):
return {"result": "success"}
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>/copy")
class RBACRoleCopyApi(Resource):
@login_required
def post(self, role_id):
tenant_id, account_id = _current_ids()
role = svc.RBACService.Roles.copy(tenant_id, account_id, str(role_id))
return _dump(role), 201
# ---------------------------------------------------------------------------
# Access policies (tenant-level permission sets).
# ---------------------------------------------------------------------------

View File

@ -392,6 +392,17 @@ class RBACService:
params={"id": role_id},
)
@staticmethod
def copy(tenant_id: str, account_id: str | None, role_id: str) -> RBACRole:
data = _inner_call(
"POST",
f"{_INNER_PREFIX}/roles/copy",
tenant_id=tenant_id,
account_id=account_id,
params={"id": role_id},
)
return RBACRole.model_validate(data or {})
# ------------------------------------------------------------------
# Access policies (Settings > Access Rules: create/edit permission sets).
# ------------------------------------------------------------------

View File

@ -188,6 +188,20 @@ class TestPaginationMapping:
assert options.reverse is True
class TestRoleCopy:
def test_role_copy_forwards_path_id(self, app):
with (
app.test_request_context("/workspaces/current/rbac/roles/role-1/copy", method="POST"),
_enabled(True),
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.copy") as mock_copy,
patch("controllers.console.workspace.rbac._dump", return_value={}),
):
inspect.unwrap(rbac_mod.RBACRoleCopyApi.post)(rbac_mod.RBACRoleCopyApi(), "role-1")
mock_copy.assert_called_once_with("tenant-1", "acct-1", "role-1")
class TestDumpHelper:
def test_dump_returns_plain_dict(self):
role = rbac_mod.svc.RBACRole(id="role-1", type="workspace", name="Owner")

View File

@ -156,6 +156,16 @@ class TestRoles:
assert call.params == {"id": "role-1"}
assert call.account_id is None
def test_copy_sends_post_with_id_param(self, mock_send: MagicMock):
mock_send.return_value = {"id": "role-1-copy", "type": "workspace", "name": "Owner copy"}
svc.RBACService.Roles.copy("tenant-1", "acct-1", "role-1")
call = _call_args(mock_send)
assert call.method == "POST"
assert call.endpoint == "/rbac/roles/copy"
assert call.params == {"id": "role-1"}
assert call.account_id == "acct-1"
class TestAccessPolicies:
def test_list_filters_by_resource_type(self, mock_send: MagicMock):
@ -261,8 +271,8 @@ class TestWorkspaceAccess:
"resource_type": "app",
"name": "Workspace App Access",
},
"role_ids": None,
"account_ids": None,
"roles": None,
"accounts": None,
}
],
"pagination": None,
@ -270,8 +280,8 @@ class TestWorkspaceAccess:
out = svc.RBACService.WorkspaceAccess.app_matrix("tenant-1")
assert out.items[0].role_ids == []
assert out.items[0].account_ids == []
assert out.items[0].roles == []
assert out.items[0].accounts == []
def test_workspace_app_replace_bindings(self, mock_send: MagicMock):
mock_send.return_value = {"data": []}