From 980c139de716c33f3d14bab725c562116bd5faf7 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Tue, 21 Apr 2026 12:11:36 +0500 Subject: [PATCH 1/5] Add allow_managers_manage_secrets server setting --- src/dstack/_internal/core/models/projects.py | 1 + .../_internal/server/routers/secrets.py | 16 +- .../_internal/server/services/permissions.py | 6 + .../_internal/server/services/projects.py | 9 + .../_internal/server/services/secrets.py | 21 +++ .../_internal/server/routers/test_secrets.py | 160 +++++++++++++++++- 6 files changed, 202 insertions(+), 11 deletions(-) diff --git a/src/dstack/_internal/core/models/projects.py b/src/dstack/_internal/core/models/projects.py index deb9ce3790..1b54c6f236 100644 --- a/src/dstack/_internal/core/models/projects.py +++ b/src/dstack/_internal/core/models/projects.py @@ -10,6 +10,7 @@ class MemberPermissions(CoreModel): can_manage_ssh_fleets: bool + can_manage_secrets: bool class Member(CoreModel): diff --git a/src/dstack/_internal/server/routers/secrets.py b/src/dstack/_internal/server/routers/secrets.py index 30cbdc60c7..cd4f38cd23 100644 --- a/src/dstack/_internal/server/routers/secrets.py +++ b/src/dstack/_internal/server/routers/secrets.py @@ -12,7 +12,7 @@ DeleteSecretsRequest, GetSecretRequest, ) -from dstack._internal.server.security.permissions import ProjectAdmin +from dstack._internal.server.security.permissions import ProjectManager from dstack._internal.server.services import secrets as secrets_services from dstack._internal.server.utils.routers import CustomORJSONResponse @@ -25,13 +25,14 @@ @router.post("/list", response_model=List[Secret]) async def list_secrets( session: AsyncSession = Depends(get_session), - user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()), + user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectManager()), ): - _, project = user_project + user, project = user_project return CustomORJSONResponse( await secrets_services.list_secrets( session=session, project=project, + user=user, ) ) @@ -40,13 +41,14 @@ async def list_secrets( async def get_secret( body: GetSecretRequest, session: AsyncSession = Depends(get_session), - user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()), + user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectManager()), ): - _, project = user_project + user, project = user_project secret = await secrets_services.get_secret( session=session, project=project, name=body.name, + user=user, ) if secret is None: raise ResourceNotExistsError() @@ -57,7 +59,7 @@ async def get_secret( async def create_or_update_secret( body: CreateOrUpdateSecretRequest, session: AsyncSession = Depends(get_session), - user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()), + user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectManager()), ): user, project = user_project return CustomORJSONResponse( @@ -75,7 +77,7 @@ async def create_or_update_secret( async def delete_secrets( body: DeleteSecretsRequest, session: AsyncSession = Depends(get_session), - user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()), + user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectManager()), ): user, project = user_project await secrets_services.delete_secrets( diff --git a/src/dstack/_internal/server/services/permissions.py b/src/dstack/_internal/server/services/permissions.py index 213c229d64..b3758e725c 100644 --- a/src/dstack/_internal/server/services/permissions.py +++ b/src/dstack/_internal/server/services/permissions.py @@ -23,6 +23,12 @@ class DefaultPermissions(CoreModel): ) ), ] = True + allow_managers_manage_secrets: Annotated[ + bool, + Field( + description=("This flag controls whether project managers can manage project secrets") + ), + ] = False _default_permissions = DefaultPermissions() diff --git a/src/dstack/_internal/server/services/projects.py b/src/dstack/_internal/server/services/projects.py index 90a19f0f06..15438efbcc 100644 --- a/src/dstack/_internal/server/services/projects.py +++ b/src/dstack/_internal/server/services/projects.py @@ -716,8 +716,17 @@ def get_member_permissions(member_model: MemberModel) -> MemberPermissions: and member_model.project_role != ProjectRole.ADMIN ): can_manage_ssh_fleets = False + can_manage_secrets = ( + user_model.global_role == GlobalRole.ADMIN + or member_model.project_role == ProjectRole.ADMIN + or ( + member_model.project_role == ProjectRole.MANAGER + and default_permissions.allow_managers_manage_secrets + ) + ) return MemberPermissions( can_manage_ssh_fleets=can_manage_ssh_fleets, + can_manage_secrets=can_manage_secrets, ) diff --git a/src/dstack/_internal/server/services/secrets.py b/src/dstack/_internal/server/services/secrets.py index ed12576256..fd7e484eaa 100644 --- a/src/dstack/_internal/server/services/secrets.py +++ b/src/dstack/_internal/server/services/secrets.py @@ -9,15 +9,18 @@ from sqlalchemy.ext.asyncio import AsyncSession from dstack._internal.core.errors import ( + ForbiddenError, ResourceExistsError, ResourceNotExistsError, ServerClientError, ) from dstack._internal.core.models.secrets import Secret +from dstack._internal.core.models.users import GlobalRole from dstack._internal.server.db import get_db from dstack._internal.server.models import DecryptedString, ProjectModel, SecretModel, UserModel from dstack._internal.server.services import events from dstack._internal.server.services.locking import get_locker +from dstack._internal.server.services.projects import get_member, get_member_permissions _SECRET_NAME_REGEX = "^[A-Za-z0-9-_]{1,200}$" _SECRET_VALUE_MAX_LENGTH = 5000 @@ -26,7 +29,9 @@ async def list_secrets( session: AsyncSession, project: ProjectModel, + user: UserModel, ) -> List[Secret]: + _check_can_manage_secrets(user=user, project=project) secret_models = await list_project_secret_models(session=session, project=project) return [secret_model_to_secret(s, include_value=False) for s in secret_models] @@ -43,7 +48,9 @@ async def get_secret( session: AsyncSession, project: ProjectModel, name: str, + user: UserModel, ) -> Optional[Secret]: + _check_can_manage_secrets(user=user, project=project) secret_model = await get_project_secret_model_by_name( session=session, project=project, @@ -61,6 +68,7 @@ async def create_or_update_secret( value: str, user: UserModel, ) -> Secret: + _check_can_manage_secrets(user=user, project=project) _validate_secret(name=name, value=value) try: secret_model = await create_secret( @@ -87,6 +95,7 @@ async def delete_secrets( names: List[str], user: UserModel, ): + _check_can_manage_secrets(user=user, project=project) async with get_project_secret_models_by_name_for_update( session=session, project=project, names=names ) as secret_models: @@ -243,3 +252,15 @@ def _validate_secret_name(name: str): def _validate_secret_value(value: str): if len(value) > _SECRET_VALUE_MAX_LENGTH: raise ServerClientError(f"Secret value length must not exceed {_SECRET_VALUE_MAX_LENGTH}") + + +def _check_can_manage_secrets(user: UserModel, project: ProjectModel): + if user.global_role == GlobalRole.ADMIN: + return + member = get_member(user=user, project=project) + if member is None: + raise ForbiddenError() + permissions = get_member_permissions(member) + if permissions.can_manage_secrets: + return + raise ForbiddenError() diff --git a/src/tests/_internal/server/routers/test_secrets.py b/src/tests/_internal/server/routers/test_secrets.py index 302591b881..2c617a5103 100644 --- a/src/tests/_internal/server/routers/test_secrets.py +++ b/src/tests/_internal/server/routers/test_secrets.py @@ -5,11 +5,13 @@ from dstack._internal.core.models.users import GlobalRole, ProjectRole from dstack._internal.server.models import SecretModel +from dstack._internal.server.services.permissions import DefaultPermissions from dstack._internal.server.services.projects import add_project_member from dstack._internal.server.testing.common import ( create_project, create_secret, create_user, + default_permissions_context, get_auth_headers, list_events, ) @@ -18,7 +20,7 @@ class TestListSecrets: @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) - async def test_returns_403_if_not_admin( + async def test_returns_403_if_not_authorized( self, test_db, session: AsyncSession, client: AsyncClient ): user = await create_user(session=session, global_role=GlobalRole.USER) @@ -33,6 +35,43 @@ async def test_returns_403_if_not_admin( ) assert response.status_code == 403 + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_returns_403_for_manager_by_default( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + response = await client.post( + f"/api/project/{project.name}/secrets/list", + headers=get_auth_headers(manager.token), + json={}, + ) + assert response.status_code == 403 + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_manager_can_list_when_allowed( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + with default_permissions_context(DefaultPermissions(allow_managers_manage_secrets=True)): + response = await client.post( + f"/api/project/{project.name}/secrets/list", + headers=get_auth_headers(manager.token), + json={}, + ) + assert response.status_code == 200 + @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) async def test_lists_secrets(self, test_db, session: AsyncSession, client: AsyncClient): @@ -70,7 +109,7 @@ async def test_lists_secrets(self, test_db, session: AsyncSession, client: Async class TestGetSecret: @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) - async def test_returns_403_if_not_admin( + async def test_returns_403_if_not_authorized( self, test_db, session: AsyncSession, client: AsyncClient ): user = await create_user(session=session, global_role=GlobalRole.USER) @@ -85,6 +124,44 @@ async def test_returns_403_if_not_admin( ) assert response.status_code == 403 + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_returns_403_for_manager_by_default( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + response = await client.post( + f"/api/project/{project.name}/secrets/get", + headers=get_auth_headers(manager.token), + json={"name": "my_secret"}, + ) + assert response.status_code == 403 + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_manager_can_get_when_allowed( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + await create_secret(session=session, project=project, name="secret1", value="123456") + with default_permissions_context(DefaultPermissions(allow_managers_manage_secrets=True)): + response = await client.post( + f"/api/project/{project.name}/secrets/get", + headers=get_auth_headers(manager.token), + json={"name": "secret1"}, + ) + assert response.status_code == 200 + @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) async def test_returns_secret_with_value( @@ -114,7 +191,7 @@ async def test_returns_secret_with_value( class TestCreateOrUpdateSecret: @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) - async def test_returns_403_if_not_admin( + async def test_returns_403_if_not_authorized( self, test_db, session: AsyncSession, client: AsyncClient ): user = await create_user(session=session, global_role=GlobalRole.USER) @@ -129,6 +206,43 @@ async def test_returns_403_if_not_admin( ) assert response.status_code == 403 + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_returns_403_for_manager_by_default( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + response = await client.post( + f"/api/project/{project.name}/secrets/create_or_update", + headers=get_auth_headers(manager.token), + json={"name": "my_secret", "value": "123456"}, + ) + assert response.status_code == 403 + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_manager_can_create_when_allowed( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + with default_permissions_context(DefaultPermissions(allow_managers_manage_secrets=True)): + response = await client.post( + f"/api/project/{project.name}/secrets/create_or_update", + headers=get_auth_headers(manager.token), + json={"name": "secret1", "value": "123456"}, + ) + assert response.status_code == 200 + @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) async def test_creates_secret(self, test_db, session: AsyncSession, client: AsyncClient): @@ -230,7 +344,7 @@ async def test_rejects_bad_names_values( class TestDeleteSecrets: @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) - async def test_returns_403_if_not_admin( + async def test_returns_403_if_not_authorized( self, test_db, session: AsyncSession, client: AsyncClient ): user = await create_user(session=session, global_role=GlobalRole.USER) @@ -245,6 +359,44 @@ async def test_returns_403_if_not_admin( ) assert response.status_code == 403 + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_returns_403_for_manager_by_default( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + response = await client.post( + f"/api/project/{project.name}/secrets/delete", + headers=get_auth_headers(manager.token), + json={"secrets_names": ["my_secret"]}, + ) + assert response.status_code == 403 + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_manager_can_delete_when_allowed( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + manager = await create_user(session=session, name="manager", global_role=GlobalRole.USER) + await add_project_member( + session=session, project=project, user=manager, project_role=ProjectRole.MANAGER + ) + await create_secret(session=session, project=project, name="secret1", value="123456") + with default_permissions_context(DefaultPermissions(allow_managers_manage_secrets=True)): + response = await client.post( + f"/api/project/{project.name}/secrets/delete", + headers=get_auth_headers(manager.token), + json={"secrets_names": ["secret1"]}, + ) + assert response.status_code == 200 + @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) async def test_deletes_secrets(self, test_db, session: AsyncSession, client: AsyncClient): From 40a79ec1ca1a1f07bbfb13f8d3aa0fcb9d25c466 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Tue, 21 Apr 2026 12:27:44 +0500 Subject: [PATCH 2/5] Fix new clients with old servers --- src/dstack/_internal/core/models/projects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dstack/_internal/core/models/projects.py b/src/dstack/_internal/core/models/projects.py index 1b54c6f236..9cb765c683 100644 --- a/src/dstack/_internal/core/models/projects.py +++ b/src/dstack/_internal/core/models/projects.py @@ -10,7 +10,9 @@ class MemberPermissions(CoreModel): can_manage_ssh_fleets: bool - can_manage_secrets: bool + can_manage_secrets: bool = False + """Default is for client-side compatibility with older servers. + Always explicitly set on the server.""" class Member(CoreModel): From 9c895b606826ba54578a3fabd0664037e5bd5b1b Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Tue, 21 Apr 2026 14:32:18 +0500 Subject: [PATCH 3/5] Update frontend --- frontend/src/pages/Project/Secrets/index.tsx | 13 +++++-------- frontend/src/pages/Project/utils.ts | 5 +++++ frontend/src/types/project.d.ts | 6 ++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/Project/Secrets/index.tsx b/frontend/src/pages/Project/Secrets/index.tsx index 09b8dbe7d0..febc739682 100644 --- a/frontend/src/pages/Project/Secrets/index.tsx +++ b/frontend/src/pages/Project/Secrets/index.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Button, ButtonWithConfirmation, Header, ListEmptyMessage, Modal, Pagination, SpaceBetween, Table } from 'components'; -import { useAppSelector, useCollection, useNotifications, usePermissionGuard } from 'hooks'; +import { useAppSelector, useCollection, useNotifications } from 'hooks'; import { getServerError } from 'libs'; import { useDeleteSecretsMutation, @@ -11,11 +11,11 @@ import { useLazyGetSecretQuery, useUpdateSecretMutation, } from 'services/secrets'; -import { GlobalUserRole, ProjectUserRole } from 'types'; +import { GlobalUserRole } from 'types'; import { selectUserData } from 'App/slice'; -import { getProjectRoleByUserName } from '../utils'; +import { getMemberCanManageSecrets } from '../utils'; import { SecretForm } from './Form'; import { IProps, TFormValues } from './types'; @@ -30,11 +30,8 @@ export const ProjectSecrets: React.FC = ({ project, loading }) => { const projectName = project?.project_name ?? ''; const [pushNotification] = useNotifications(); - const [hasPermissionForSecretsManaging] = usePermissionGuard({ - allowedProjectRoles: [ProjectUserRole.ADMIN], - allowedGlobalRoles: [GlobalUserRole.ADMIN], - projectRole: project ? (getProjectRoleByUserName(project, userName) ?? undefined) : undefined, - }); + const hasPermissionForSecretsManaging = + userData?.global_role === GlobalUserRole.ADMIN || (project ? getMemberCanManageSecrets(project, userName) : false); const { data, isLoading, isFetching } = useGetAllSecretsQuery( { project_name: projectName }, diff --git a/frontend/src/pages/Project/utils.ts b/frontend/src/pages/Project/utils.ts index 759e7da6fa..9b7879d7f6 100644 --- a/frontend/src/pages/Project/utils.ts +++ b/frontend/src/pages/Project/utils.ts @@ -4,3 +4,8 @@ export const getProjectRoleByUserName = ( ): TProjectRole | null => { return project.members.find((m) => m.user.username === userName)?.project_role ?? null; }; + +export const getMemberCanManageSecrets = (project: IProject, userName: IProjectMember['user']['username']): boolean => { + const member = project.members.find((m) => m.user.username === userName); + return member?.permissions?.can_manage_secrets ?? false; +}; diff --git a/frontend/src/types/project.d.ts b/frontend/src/types/project.d.ts index 915bc4d902..fa8177b219 100644 --- a/frontend/src/types/project.d.ts +++ b/frontend/src/types/project.d.ts @@ -33,9 +33,15 @@ declare interface IProject { templates_repo?: string | null; } +declare interface IProjectMemberPermissions { + can_manage_ssh_fleets: boolean; + can_manage_secrets: boolean; +} + declare interface IProjectMember { project_role: TProjectRole; user: IUser | { username: string }; + permissions?: IProjectMemberPermissions; } declare type TSetProjectMembersParams = { From 3c22cfe24e40c49187518c8a5aff96ed2729d73a Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Tue, 21 Apr 2026 14:57:18 +0500 Subject: [PATCH 4/5] Fix tests --- src/tests/_internal/server/routers/test_projects.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tests/_internal/server/routers/test_projects.py b/src/tests/_internal/server/routers/test_projects.py index b398073bd8..dfb5bfdb6a 100644 --- a/src/tests/_internal/server/routers/test_projects.py +++ b/src/tests/_internal/server/routers/test_projects.py @@ -976,6 +976,7 @@ async def test_creates_project(self, test_db, session: AsyncSession, client: Asy "project_role": ProjectRole.ADMIN, "permissions": { "can_manage_ssh_fleets": True, + "can_manage_secrets": True, }, } ], @@ -1439,6 +1440,7 @@ async def test_returns_project(self, test_db, session: AsyncSession, client: Asy "project_role": ProjectRole.ADMIN, "permissions": { "can_manage_ssh_fleets": True, + "can_manage_secrets": True, }, } ], @@ -1669,6 +1671,7 @@ async def test_sets_project_members(self, test_db, session: AsyncSession, client "project_role": ProjectRole.ADMIN, "permissions": { "can_manage_ssh_fleets": True, + "can_manage_secrets": True, }, }, { @@ -1687,6 +1690,7 @@ async def test_sets_project_members(self, test_db, session: AsyncSession, client "project_role": ProjectRole.ADMIN, "permissions": { "can_manage_ssh_fleets": True, + "can_manage_secrets": True, }, }, { @@ -1705,6 +1709,7 @@ async def test_sets_project_members(self, test_db, session: AsyncSession, client "project_role": ProjectRole.USER, "permissions": { "can_manage_ssh_fleets": True, + "can_manage_secrets": False, }, }, ] @@ -1762,6 +1767,7 @@ async def test_sets_project_members_by_email( "project_role": ProjectRole.ADMIN, "permissions": { "can_manage_ssh_fleets": True, + "can_manage_secrets": True, }, }, ] From f14b1efaf242a9e75e9ba9e5c240328f885a1776 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Tue, 21 Apr 2026 15:05:59 +0500 Subject: [PATCH 5/5] Fix tests --- src/tests/_internal/server/routers/test_projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/_internal/server/routers/test_projects.py b/src/tests/_internal/server/routers/test_projects.py index dfb5bfdb6a..3b8b4aab06 100644 --- a/src/tests/_internal/server/routers/test_projects.py +++ b/src/tests/_internal/server/routers/test_projects.py @@ -1709,7 +1709,7 @@ async def test_sets_project_members(self, test_db, session: AsyncSession, client "project_role": ProjectRole.USER, "permissions": { "can_manage_ssh_fleets": True, - "can_manage_secrets": False, + "can_manage_secrets": True, }, }, ]