diff --git a/api/controllers/console/workspace/rbac.py b/api/controllers/console/workspace/rbac.py index d1157f36c0..939fa25d0e 100644 --- a/api/controllers/console/workspace/rbac.py +++ b/api/controllers/console/workspace/rbac.py @@ -146,6 +146,15 @@ class RBACRoleItemApi(Resource): return {"result": "success"} +@console_ns.route("/workspaces/current/rbac/roles//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). # --------------------------------------------------------------------------- diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index d366a2205e..4399048d94 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -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). # ------------------------------------------------------------------ diff --git a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py index 40aee8e690..9cba20ba2b 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py @@ -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") diff --git a/api/tests/unit_tests/services/enterprise/test_rbac_service.py b/api/tests/unit_tests/services/enterprise/test_rbac_service.py index 677dcc1f79..90839a32d0 100644 --- a/api/tests/unit_tests/services/enterprise/test_rbac_service.py +++ b/api/tests/unit_tests/services/enterprise/test_rbac_service.py @@ -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": []}