feat(admin-panel): signing keys management tab with usage tracking (#20586)

## Summary
- Adds a new admin-only **Security** tab to the Admin Panel (alongside
General/Apps/AI/Config/Health) containing a **Signing Keys** section.
The tab is intentionally introduced now so the upcoming **Encryption
rotation** work can land as a sibling section.
- Lists every JWT signing key with key id, `createdAt`, `revokedAt`,
current/active/revoked status, and a **7-day verification count** read
from Redis. A trailing row aggregates **legacy HS256** verifications so
it is clear when the deprecated path is still in use.
- Lets an admin **revoke** a public key. Revoking the current key drops
`isCurrent`, sets `revokedAt`, nulls the encrypted `privateKey` and
clears the in-process cached current key; the existing lazy path in
`JwtKeyManagerService.getCurrentSigningKey()` then mints a fresh current
key on the next sign.

## Backend
- `SigningKeyVerifyCounterService` — bucketed Redis counter under the
existing `EngineMetrics` namespace. 1-day UTC-aligned buckets, 8-day TTL
refreshed on every increment, batched read via `mget`. Failures are
swallowed and logged at `warn` so a Redis hiccup cannot break auth.
- `JwtWrapperService.verifyJwtToken` records verifies **after success**
for both ES256 (`kid` as identifier) and HS256 (the literal `legacy`
identifier).
- `JwtKeyManagerService.listSigningKeys()` and `revokeSigningKey(id)`:
list ordered by `isCurrent DESC, createdAt DESC`; revoke is idempotent,
validates the UUID, invalidates the public-key cache, and resets the
cached current-key promise.
- `AdminPanelResolver.getSigningKeys` (query) and `revokeSigningKey`
(mutation) are both decorated with `@UseGuards(AdminPanelGuard)` so they
are admin-only, like the 35 existing admin-only methods on this
resolver. `privateKey` is never returned over GraphQL.

## Frontend
- New `SECURITY` tab id wired into `SettingsAdminContent` and
`SettingsAdminTabContent` (gated by `canAccessFullAdminPanel`).
- `SettingsAdminSecurity` / `SettingsAdminSigningKeysTable` strictly
reuse existing admin-panel components: `Section`, `H2Title`,
`Table`/`TableRow`/`TableCell`/`TableHeader` from `@/ui/layout/table`,
`Tag`/`Button` from `twenty-ui`, and `ConfirmationModal` mirroring the
queue retry/delete modals. Only one minimal styled helper for the
monospaced UUID rendering.
- `useRevokeSigningKey` uses `useApolloAdminClient`, refetches
`GetSigningKeys`, shows success/error snackbars (same pattern as
`useRetryJobs`/`useDeleteJobs`).

<img width="1293" height="881" alt="image"
src="https://github.com/user-attachments/assets/7cf98664-950b-4451-af85-27781a8e9a9c"
/>
This commit is contained in:
Charles Bochet
2026-05-15 12:49:18 +02:00
committed by GitHub
parent 218799636f
commit 75b9b2fe5d
19 changed files with 1015 additions and 5 deletions
@@ -379,6 +379,7 @@ export type Mutation = {
removeAiProvider: Scalars['Boolean'];
removeModelFromProvider: Scalars['Boolean'];
retryJobs: RetryJobsResponse;
revokeSigningKey: SigningKeyDto;
setAdminAiModelEnabled: Scalars['Boolean'];
setAdminAiModelRecommended: Scalars['Boolean'];
setAdminAiModelsEnabled: Scalars['Boolean'];
@@ -436,6 +437,11 @@ export type MutationRetryJobsArgs = {
};
export type MutationRevokeSigningKeyArgs = {
id: Scalars['UUID'];
};
export type MutationSetAdminAiModelEnabledArgs = {
enabled: Scalars['Boolean'];
modelId: Scalars['String'];
@@ -505,6 +511,7 @@ export type Query = {
getModelsDevSuggestions: Array<ModelsDevModelSuggestion>;
getQueueJobs: QueueJobsResponse;
getQueueMetrics: QueueMetricsData;
getSigningKeys: SigningKeysAdminPanelDto;
getSystemHealthStatus: SystemHealth;
getUpgradeStatus: Array<WorkspaceUpgradeStatus>;
userLookupAdminPanel: UserLookup;
@@ -661,6 +668,23 @@ export type RetryJobsResponse = {
retriedCount: Scalars['Int'];
};
export type SigningKeyDto = {
__typename?: 'SigningKeyDTO';
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
isCurrent: Scalars['Boolean'];
publicKey: Scalars['String'];
revokedAt?: Maybe<Scalars['DateTime']>;
verifyCountInWindow: Scalars['Int'];
};
export type SigningKeysAdminPanelDto = {
__typename?: 'SigningKeysAdminPanelDTO';
legacyVerifyCountInWindow: Scalars['Int'];
signingKeys: Array<SigningKeyDto>;
verifyWindowDays: Scalars['Int'];
};
export enum SubscriptionInterval {
Month = 'Month',
Year = 'Year'
@@ -1074,6 +1098,18 @@ export type GetMaintenanceModeQueryVariables = Exact<{ [key: string]: never; }>;
export type GetMaintenanceModeQuery = { __typename?: 'Query', getMaintenanceMode?: { __typename?: 'MaintenanceMode', startAt: string, endAt: string, link?: string | null } | null };
export type RevokeSigningKeyMutationVariables = Exact<{
id: Scalars['UUID'];
}>;
export type RevokeSigningKeyMutation = { __typename?: 'Mutation', revokeSigningKey: { __typename?: 'SigningKeyDTO', id: string, isCurrent: boolean, createdAt: string, revokedAt?: string | null, verifyCountInWindow: number } };
export type GetSigningKeysQueryVariables = Exact<{ [key: string]: never; }>;
export type GetSigningKeysQuery = { __typename?: 'Query', getSigningKeys: { __typename?: 'SigningKeysAdminPanelDTO', legacyVerifyCountInWindow: number, verifyWindowDays: number, signingKeys: Array<{ __typename?: 'SigningKeyDTO', id: string, publicKey: string, isCurrent: boolean, createdAt: string, revokedAt?: string | null, verifyCountInWindow: number }> } };
export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, logoUrl?: string | null, oAuthClientId: string, oAuthRedirectUris: Array<string>, oAuthScopes: Array<string>, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string };
export const UserInfoFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<UserInfoFragmentFragment, unknown>;
@@ -1119,4 +1155,6 @@ export const GetQueueMetricsDocument = {"kind":"Document","definitions":[{"kind"
export const GetSystemHealthStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSystemHealthStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getSystemHealthStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<GetSystemHealthStatusQuery, GetSystemHealthStatusQueryVariables>;
export const ClearMaintenanceModeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ClearMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"clearMaintenanceMode"}}]}}]} as unknown as DocumentNode<ClearMaintenanceModeMutation, ClearMaintenanceModeMutationVariables>;
export const SetMaintenanceModeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetMaintenanceMode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"startAt"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"endAt"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"link"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setMaintenanceMode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"startAt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"startAt"}}},{"kind":"Argument","name":{"kind":"Name","value":"endAt"},"value":{"kind":"Variable","name":{"kind":"Name","value":"endAt"}}},{"kind":"Argument","name":{"kind":"Name","value":"link"},"value":{"kind":"Variable","name":{"kind":"Name","value":"link"}}}]}]}}]} as unknown as DocumentNode<SetMaintenanceModeMutation, SetMaintenanceModeMutationVariables>;
export const GetMaintenanceModeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startAt"}},{"kind":"Field","name":{"kind":"Name","value":"endAt"}},{"kind":"Field","name":{"kind":"Name","value":"link"}}]}}]}}]} as unknown as DocumentNode<GetMaintenanceModeQuery, GetMaintenanceModeQueryVariables>;
export const GetMaintenanceModeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getMaintenanceMode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startAt"}},{"kind":"Field","name":{"kind":"Name","value":"endAt"}},{"kind":"Field","name":{"kind":"Name","value":"link"}}]}}]}}]} as unknown as DocumentNode<GetMaintenanceModeQuery, GetMaintenanceModeQueryVariables>;
export const RevokeSigningKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeSigningKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"revokeSigningKey"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isCurrent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"revokedAt"}},{"kind":"Field","name":{"kind":"Name","value":"verifyCountInWindow"}}]}}]}}]} as unknown as DocumentNode<RevokeSigningKeyMutation, RevokeSigningKeyMutationVariables>;
export const GetSigningKeysDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSigningKeys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getSigningKeys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"signingKeys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"publicKey"}},{"kind":"Field","name":{"kind":"Name","value":"isCurrent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"revokedAt"}},{"kind":"Field","name":{"kind":"Name","value":"verifyCountInWindow"}}]}},{"kind":"Field","name":{"kind":"Name","value":"legacyVerifyCountInWindow"}},{"kind":"Field","name":{"kind":"Name","value":"verifyWindowDays"}}]}}]}}]} as unknown as DocumentNode<GetSigningKeysQuery, GetSigningKeysQueryVariables>;