From 62c3d4219678b13a394638c6bca0ddc292187ff3 Mon Sep 17 00:00:00 2001 From: Ludvig Szklarz Anderson Date: Wed, 15 Jul 2026 16:22:15 +0200 Subject: [PATCH] feat(store): add canManageTokens capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a `tokenManagement` permission area and a `canManageTokens` user capability, mirroring the existing `canManageUsers`. It is derived from the `ManageTokens` permission set, and — since the personal access token endpoints currently also live in the `Basic` set — from `Basic` as well, so it resolves to true for all built-in roles and any custom role granted `Basic` today, while already covering `ManageTokens`-only roles (e.g. the CI built-in role) for when tokens are split out of `Basic`. Ticket: MEN-8386 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Ludvig Szklarz Anderson --- packages/store/src/commonSelectors.ts | 3 +++ packages/testing/src/mockData.ts | 5 ++++- packages/utils/src/constants/users.ts | 23 ++++++++++++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/store/src/commonSelectors.ts b/packages/store/src/commonSelectors.ts index f27109fed..980de635c 100644 --- a/packages/store/src/commonSelectors.ts +++ b/packages/store/src/commonSelectors.ts @@ -67,6 +67,8 @@ export const getUserCapabilities = createSelector([getUserRoles, getIsServicePro const canReadUsers = uiPermissions.userManagement.includes(uiPermissionsById.read.value); const canManageUsers = uiPermissions.userManagement.includes(uiPermissionsById.manage.value); + const canManageTokens = uiPermissions.tokenManagement?.includes(uiPermissionsById.manage.value) ?? false; + const canReadDevices = hasPermission(uiPermissions.groups, uiPermissionsById.read.value); const canWriteDevices = Object.values(uiPermissions.groups).some( groupPermissions => groupPermissions.includes(uiPermissionsById.read.value) && groupPermissions.length > 1 @@ -85,6 +87,7 @@ export const getUserCapabilities = createSelector([getUserRoles, getIsServicePro canDeploy, canManageDevices, canManageReleases, + canManageTokens, canManageUsers, canReadDeployments, canReadDevices, diff --git a/packages/testing/src/mockData.ts b/packages/testing/src/mockData.ts index 9e0d85255..669a0023f 100644 --- a/packages/testing/src/mockData.ts +++ b/packages/testing/src/mockData.ts @@ -160,6 +160,7 @@ export const adminUserCapabilities = { canDeploy: true, canManageDevices: true, canManageReleases: true, + canManageTokens: true, canManageUsers: true, canReadDeployments: true, canReadDevices: true, @@ -871,7 +872,8 @@ export const rbacRoles = [ permission_sets_with_scope: [ { ...defaultPermissionSets.ReadReleases }, { ...defaultPermissionSets.ManageReleases }, - { ...defaultPermissionSets.UploadArtifacts } + { ...defaultPermissionSets.UploadArtifacts }, + { ...defaultPermissionSets.ManageTokens } ] }, { @@ -967,6 +969,7 @@ const expectedParsedRoles = { [ALL_RELEASES]: [uiPermissionsById.manage.value, uiPermissionsById.upload.value, uiPermissionsById.read.value] }, tenantManagement: [uiPermissionsById.read.value, uiPermissionsById.manage.value], + tokenManagement: [uiPermissionsById.manage.value], userManagement: [uiPermissionsById.read.value, uiPermissionsById.manage.value] } } diff --git a/packages/utils/src/constants/users.ts b/packages/utils/src/constants/users.ts index 3e6827efd..c19a9b353 100644 --- a/packages/utils/src/constants/users.ts +++ b/packages/utils/src/constants/users.ts @@ -38,6 +38,7 @@ export type UiPermissionsDefinition = Readonly<{ groups: Record; releases: Record; tenantManagement: string[]; + tokenManagement: string[]; userManagement: string[]; }>; @@ -56,6 +57,7 @@ export const emptyUiPermissions: UiPermissionsDefinition = Object.freeze({ groups: Object.freeze({}), releases: Object.freeze({}), tenantManagement: [], + tokenManagement: [], userManagement: [] }); @@ -117,6 +119,7 @@ const permissionSetIds = { ManageDevices: 'ManageDevices', ManageReleases: 'ManageReleases', ManageTenants: 'ManageTenants', + ManageTokens: 'ManageTokens', ManageUsers: 'ManageUsers', ReadAuditLogs: 'ReadAuditLogs', ReadDevices: 'ReadDevices', @@ -163,6 +166,7 @@ export const uiPermissionsById = { groups: permissionSetIds.ManageDevices, releases: permissionSetIds.ManageReleases, tenantManagement: permissionSetIds.ManageTenants, + tokenManagement: permissionSetIds.ManageTokens, userManagement: permissionSetIds.ManageUsers }, title: 'Manage', @@ -305,6 +309,7 @@ export const rolesById: Record = Object.freeze({ deployments: uiPermissionsByArea.deployments.uiPermissions.map(permissionMapper), groups: { [ALL_DEVICES]: uiPermissionsByArea.groups.uiPermissions.map(permissionMapper) }, releases: { [ALL_RELEASES]: uiPermissionsByArea.releases.uiPermissions.map(permissionMapper) }, + tokenManagement: [uiPermissionsById.manage.value], userManagement: uiPermissionsByArea.userManagement.uiPermissions.map(permissionMapper) } }, @@ -319,6 +324,7 @@ export const rolesById: Record = Object.freeze({ deployments: [uiPermissionsById.read.value], groups: { [ALL_DEVICES]: [uiPermissionsById.read.value] }, releases: { [ALL_RELEASES]: [uiPermissionsById.read.value] }, + tokenManagement: [uiPermissionsById.manage.value], userManagement: [uiPermissionsById.read.value] } }, @@ -330,7 +336,8 @@ export const rolesById: Record = Object.freeze({ permissions: [], uiPermissions: { ...emptyUiPermissions, - releases: { [ALL_RELEASES]: uiPermissionsByArea.releases.uiPermissions.map(permissionMapper) } + releases: { [ALL_RELEASES]: uiPermissionsByArea.releases.uiPermissions.map(permissionMapper) }, + tokenManagement: [uiPermissionsById.manage.value] } }, [staticRolesByName.deploymentsManager]: { @@ -342,7 +349,8 @@ export const rolesById: Record = Object.freeze({ ...emptyUiPermissions, deployments: uiPermissionsByArea.deployments.uiPermissions.map(permissionMapper), groups: { [ALL_DEVICES]: [uiPermissionsById.deploy.value, uiPermissionsById.read.value] }, - releases: { [ALL_RELEASES]: [uiPermissionsById.read.value] } + releases: { [ALL_RELEASES]: [uiPermissionsById.read.value] }, + tokenManagement: [uiPermissionsById.manage.value] } }, [staticRolesByName.terminalAccess]: { @@ -352,7 +360,8 @@ export const rolesById: Record = Object.freeze({ permissions: [], uiPermissions: { ...emptyUiPermissions, - groups: { [ALL_DEVICES]: [uiPermissionsById.connect.value] } + groups: { [ALL_DEVICES]: [uiPermissionsById.connect.value] }, + tokenManagement: [uiPermissionsById.manage.value] } } }); @@ -388,6 +397,8 @@ export const defaultPermissionSets: Record