diff --git a/api/controllers/console/workspace/rbac.py b/api/controllers/console/workspace/rbac.py index 36184077cf..89a74798b4 100644 --- a/api/controllers/console/workspace/rbac.py +++ b/api/controllers/console/workspace/rbac.py @@ -106,10 +106,22 @@ class _PaginationQuery(BaseModel): return svc.ListOption.model_validate(self.model_dump()) +class _RolesListQuery(_PaginationQuery): + include_owner: int = Field(default=0, ge=0, le=1) + + def _pagination_options() -> svc.ListOption: return _PaginationQuery.model_validate(request.args.to_dict(flat=True)).to_inner_options() +def _filter_out_owner(paginated: svc.Paginated[svc.RBACRole]) -> svc.Paginated[svc.RBACRole]: + filtered = [r for r in paginated.data if r.name != "owner"] + return svc.Paginated[svc.RBACRole]( + data=filtered, + pagination=paginated.pagination, + ) + + def _legacy_workspace_roles(options: svc.ListOption | None = None) -> svc.Paginated[svc.RBACRole]: """Return the built-in legacy workspace roles in the RBAC list shape. @@ -127,6 +139,7 @@ def _legacy_workspace_roles(options: svc.ListOption | None = None) -> svc.Pagina description="", is_builtin=True, permission_keys=list(_LEGACY_ROLE_PERMISSION_KEYS[role_name]), + role_tag="owner" if role_name == "owner" else "", ) for role_name in ("owner", "admin", "editor", "normal", "dataset_operator") ] @@ -207,10 +220,15 @@ class RBACRolesApi(Resource): @login_required def get(self): tenant_id, account_id = _current_ids() - options = _pagination_options() + query = _RolesListQuery.model_validate(request.args.to_dict(flat=True)) + options = query.to_inner_options() if not dify_config.RBAC_ENABLED: - return _dump(_legacy_workspace_roles(options)) - return _dump(svc.RBACService.Roles.list(tenant_id, account_id, options=options)) + result = _legacy_workspace_roles(options) + else: + result = svc.RBACService.Roles.list(tenant_id, account_id, options=options) + if query.include_owner == 0: + result = _filter_out_owner(result) + return _dump(result) @login_required def post(self): diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index 43dc771a2c..3975cffc63 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -69,6 +69,7 @@ class RBACRole(_RBACModel): description: str = "" is_builtin: bool = False permission_keys: list[str] = Field(default_factory=list) + role_tag: str = "" @field_validator("permission_keys", mode="before") @classmethod 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 762ba28506..2a44e2dd5e 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py @@ -120,7 +120,7 @@ class TestPydanticModels: class TestPaginationMapping: def test_roles_get_returns_legacy_compatible_roles_when_rbac_disabled(self, app): with ( - app.test_request_context("/workspaces/current/rbac/roles?page=1&limit=2"), + app.test_request_context("/workspaces/current/rbac/roles?page=1&limit=2&include_owner=1"), patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False), patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list, @@ -147,6 +147,7 @@ class TestPaginationMapping: "dataset.acl.edit", "dataset.acl.use", ], + "role_tag": "owner", }, { "id": "admin", @@ -167,6 +168,7 @@ class TestPaginationMapping: "dataset.acl.edit", "dataset.acl.use", ], + "role_tag": "", }, ] assert response["pagination"] == { @@ -177,9 +179,45 @@ class TestPaginationMapping: } mock_list.assert_not_called() + def test_roles_get_filters_out_owner_when_include_owner_is_zero(self, app): + with ( + app.test_request_context("/workspaces/current/rbac/roles?include_owner=0"), + patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False), + patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), + patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"), + ): + response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + + names = [r["name"] for r in response["data"]] + assert "owner" not in names + + def test_roles_get_keeps_owner_when_include_owner_is_one(self, app): + with ( + app.test_request_context("/workspaces/current/rbac/roles?include_owner=1"), + patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False), + patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), + patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"), + ): + response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + + names = [r["name"] for r in response["data"]] + assert "owner" in names + + def test_roles_get_filters_out_owner_by_default(self, app): + with ( + app.test_request_context("/workspaces/current/rbac/roles"), + patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", False), + patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), + patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"), + ): + response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi()) + + names = [r["name"] for r in response["data"]] + assert "owner" not in names + def test_roles_get_forwards_outer_pagination_params(self, app): with ( - app.test_request_context("/workspaces/current/rbac/roles?page=2&limit=50&reverse=true"), + app.test_request_context("/workspaces/current/rbac/roles?page=2&limit=50&reverse=true&include_owner=1"), patch("controllers.console.workspace.rbac.dify_config.RBAC_ENABLED", True), patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,