From 1184ea1b1199e5f3893ea6438434f048ead08e9c Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 16 Apr 2025 05:04:41 -0300 Subject: [PATCH] Add Azure Client Secrets Rotation --- .../azure-client-secret-rotation-router.ts | 19 +++ .../v2/secret-rotation-v2-routers/index.ts | 4 +- .../secret-rotation-v2-router.ts | 4 +- .../azure-client-secret-rotation-constants.ts | 15 ++ .../azure-client-secret-rotation-fns.ts | 155 ++++++++++++++++++ .../azure-client-secret-rotation-schemas.ts | 68 ++++++++ .../azure-client-secret-rotation-types.ts | 41 +++++ .../azure-client-secret/index.ts | 3 + .../secret-rotation-v2-enums.ts | 3 +- .../secret-rotation-v2-fns.ts | 4 +- .../secret-rotation-v2-maps.ts | 6 +- .../secret-rotation-v2-service.ts | 6 +- .../secret-rotation-v2-types.ts | 27 ++- .../secret-rotation-v2-union-schema.ts | 4 +- backend/src/lib/api-docs/constants.ts | 8 + .../azure-client-secrets-connection-router.ts | 31 ++++ .../app-connection/app-connection-service.ts | 4 +- .../azure-client-secrets-connection-fns.ts | 83 +++++++++- .../azure-client-secrets-connection-types.ts | 35 +++- .../azure-client-secrets-service.ts | 70 ++++++++ .../azure-key-vault-connection-fns.ts | 6 +- ...reClientSecretRotationParametersFields.tsx | 71 ++++++++ .../SecretRotationV2ParametersFields.tsx | 4 +- .../AzureClientSecretRotationReviewFields.tsx | 30 ++++ .../SecretRotationReviewFields.tsx | 4 +- ...ientSecretRotationSecretsMappingFields.tsx | 58 +++++++ .../SecretRotationV2SecretsMappingFields.tsx | 4 +- .../azure-client-secret-rotation-schema.ts | 18 ++ .../forms/schemas/index.ts | 4 +- frontend/src/helpers/secretRotationsV2.ts | 11 +- .../hooks/api/appConnections/azure/index.ts | 1 + .../api/appConnections/azure/queries.tsx | 37 +++++ .../hooks/api/appConnections/azure/types.ts | 4 + .../src/hooks/api/secretRotationsV2/enums.ts | 3 +- .../types/azure-client-secret-rotation.ts | 38 +++++ .../api/secretRotationsV2/types/index.ts | 14 +- 36 files changed, 861 insertions(+), 36 deletions(-) create mode 100644 backend/src/ee/routes/v2/secret-rotation-v2-routers/azure-client-secret-rotation-router.ts create mode 100644 backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-constants.ts create mode 100644 backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-fns.ts create mode 100644 backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-schemas.ts create mode 100644 backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types.ts create mode 100644 backend/src/ee/services/secret-rotation-v2/azure-client-secret/index.ts create mode 100644 backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-service.ts create mode 100644 frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/AzureClientSecretRotationParametersFields.tsx create mode 100644 frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/AzureClientSecretRotationReviewFields.tsx create mode 100644 frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/AzureClientSecretRotationSecretsMappingFields.tsx create mode 100644 frontend/src/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema.ts create mode 100644 frontend/src/hooks/api/appConnections/azure/index.ts create mode 100644 frontend/src/hooks/api/appConnections/azure/queries.tsx create mode 100644 frontend/src/hooks/api/appConnections/azure/types.ts create mode 100644 frontend/src/hooks/api/secretRotationsV2/types/azure-client-secret-rotation.ts diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/azure-client-secret-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/azure-client-secret-rotation-router.ts new file mode 100644 index 000000000..d8ccbc12c --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/azure-client-secret-rotation-router.ts @@ -0,0 +1,19 @@ +import { + AzureClientSecretRotationGeneratedCredentialsSchema, + AzureClientSecretRotationSchema, + CreateAzureClientSecretRotationSchema, + UpdateAzureClientSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/azure-client-secret"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerAzureClientSecretRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.AzureClientSecret, + server, + responseSchema: AzureClientSecretRotationSchema, + createSchema: CreateAzureClientSecretRotationSchema, + updateSchema: UpdateAzureClientSecretRotationSchema, + generatedCredentialsSchema: AzureClientSecretRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 1dacf1bd2..d70b5bd52 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -1,6 +1,7 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router"; +import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; @@ -12,5 +13,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< > = { [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter, - [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter + [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter, + [SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index bfb8b38c0..56896e0bb 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; @@ -13,7 +14,8 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ PostgresCredentialsRotationListItemSchema, MsSqlCredentialsRotationListItemSchema, - Auth0ClientSecretRotationListItemSchema + Auth0ClientSecretRotationListItemSchema, + AzureClientSecretRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-constants.ts new file mode 100644 index 000000000..3e25da403 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "Azure Client Secret", + type: SecretRotation.AzureClientSecret, + connection: AppConnection.AzureClientSecrets, + template: { + secretsMapping: { + clientId: "AZURE_CLIENT_ID", + clientSecret: "AZURE_CLIENT_SECRET" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-fns.ts new file mode 100644 index 000000000..156dc6828 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-fns.ts @@ -0,0 +1,155 @@ +import { + AzureAddPasswordResponse, + TAzureClientSecretRotationGeneratedCredentials, + TAzureClientSecretRotationWithConnection +} from "@app/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types"; +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { request } from "@app/lib/config/request"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets"; + +const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0"; + +export const azureClientSecretRotationFactory: TRotationFactory< + TAzureClientSecretRotationWithConnection, + TAzureClientSecretRotationGeneratedCredentials +> = (secretRotation, appConnectionDAL, kmsService) => { + const { + connection, + parameters: { appId }, + secretsMapping, + rotationInterval + } = secretRotation; + + /** + * Creates a new client secret for the Azure app. + */ + const $rotateClientSecret = async () => { + const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService); + const endpoint = `${GRAPH_API_BASE}/applications/${appId}/addPassword`; + + await blockLocalAndPrivateIpAddresses(endpoint); + + const endDateTime = new Date(); + endDateTime.setDate(endDateTime.getDate() + rotationInterval); + + try { + const { data } = await request.post( + endpoint, + { + passwordCredential: { + displayName: "Infisical Auto-Rotated Secret", + endDateTime: endDateTime.toISOString() + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + + if (!data?.secretText || !data?.keyId) { + throw new Error("Invalid response from Azure: missing secretText or keyId."); + } + + return { + clientSecret: data.secretText, + clientId: data.keyId + }; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to add client secret to Azure app ${appId}: ${message}`); + } + }; + + /** + * Revokes a client secret from the Azure app using its keyId. + */ + const revokeCredential = async (clientId: string) => { + const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService); + const endpoint = `${GRAPH_API_BASE}/applications/${appId}/removePassword`; + + await blockLocalAndPrivateIpAddresses(endpoint); + + try { + await request.post( + endpoint, + { keyId: clientId }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + } + } + ); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to remove client secret with keyId ${clientId} from app ${appId}: ${message}`); + } + }; + + /** + * Issues a new set of credentials. + */ + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotateClientSecret(); + return callback(credentials); + }; + + /** + * Revokes a list of credentials. + */ + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + credentials, + callback + ) => { + if (!credentials?.length) return callback(); + + await Promise.all(credentials.map(({ clientId }) => revokeCredential(clientId))); + return callback(); + }; + + /** + * Rotates credentials by issuing new ones and revoking the old. + */ + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + oldCredentials, + callback + ) => { + const newCredentials = await $rotateClientSecret(); + + if (oldCredentials?.clientId) { + await revokeCredential(oldCredentials.clientId); + } + + return callback(newCredentials); + }; + + /** + * Maps the generated credentials into the secret payload format. + */ + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ({ + clientSecret, + clientId + }) => [ + { key: secretsMapping.clientSecret, value: clientSecret }, + { key: secretsMapping.clientId, value: clientId } + ]; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-schemas.ts new file mode 100644 index 000000000..514ea815d --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-schemas.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const AzureClientSecretRotationGeneratedCredentialsSchema = z + .object({ + clientId: z.string(), + clientSecret: z.string() + }) + .array() + .min(1) + .max(2); + +const AzureClientSecretRotationParametersSchema = z.object({ + appId: z.string().trim().min(1, "Client ID Required").describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appId), + appName: z + .string() + .trim() + .min(1, "App Name Required") + .describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appName) +}); + +const AzureClientSecretRotationSecretsMappingSchema = z.object({ + clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientId), + clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientSecret) +}); + +export const AzureClientSecretRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + clientId: z.string(), + clientSecret: z.string() + }) +}); + +export const AzureClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AzureClientSecret).extend({ + type: z.literal(SecretRotation.AzureClientSecret), + parameters: AzureClientSecretRotationParametersSchema, + secretsMapping: AzureClientSecretRotationSecretsMappingSchema +}); + +export const CreateAzureClientSecretRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.AzureClientSecret +).extend({ + parameters: AzureClientSecretRotationParametersSchema, + secretsMapping: AzureClientSecretRotationSecretsMappingSchema +}); + +export const UpdateAzureClientSecretRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.AzureClientSecret +).extend({ + parameters: AzureClientSecretRotationParametersSchema.optional(), + secretsMapping: AzureClientSecretRotationSecretsMappingSchema.optional() +}); + +export const AzureClientSecretRotationListItemSchema = z.object({ + name: z.literal("Azure Client Secret"), + connection: z.literal(AppConnection.AzureClientSecrets), + type: z.literal(SecretRotation.AzureClientSecret), + template: AzureClientSecretRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types.ts new file mode 100644 index 000000000..91f66a883 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +import { TAzureClientSecretsConnection } from "@app/services/app-connection/azure-client-secrets"; + +import { + AzureClientSecretRotationGeneratedCredentialsSchema, + AzureClientSecretRotationListItemSchema, + AzureClientSecretRotationSchema, + CreateAzureClientSecretRotationSchema +} from "./azure-client-secret-rotation-schemas"; + +export type TAzureClientSecretRotation = z.infer; + +export type TAzureClientSecretRotationInput = z.infer; + +export type TAzureClientSecretRotationListItem = z.infer; + +export type TAzureClientSecretRotationWithConnection = TAzureClientSecretRotation & { + connection: TAzureClientSecretsConnection; +}; + +export type TAzureClientSecretRotationGeneratedCredentials = z.infer< + typeof AzureClientSecretRotationGeneratedCredentialsSchema +>; + +export interface TAzureClientSecretRotationParameters { + appId: string; + keyId?: string; + displayName?: string; +} + +export interface TAzureClientSecretRotationSecretsMapping { + appId: string; + clientSecret: string; + keyId: string; +} + +export interface AzureAddPasswordResponse { + secretText: string; + keyId: string; +} diff --git a/backend/src/ee/services/secret-rotation-v2/azure-client-secret/index.ts b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/index.ts new file mode 100644 index 000000000..8c741bdc6 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/azure-client-secret/index.ts @@ -0,0 +1,3 @@ +export * from "./azure-client-secret-rotation-constants"; +export * from "./azure-client-secret-rotation-schemas"; +export * from "./azure-client-secret-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index d43cacb3a..3a362f50b 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -1,7 +1,8 @@ export enum SecretRotation { PostgresCredentials = "postgres-credentials", MsSqlCredentials = "mssql-credentials", - Auth0ClientSecret = "auth0-client-secret" + Auth0ClientSecret = "auth0-client-secret", + AzureClientSecret = "azure-client-secret" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 603b77cc1..f403796db 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -4,6 +4,7 @@ import { getConfig } from "@app/lib/config/env"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; +import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; @@ -18,7 +19,8 @@ import { const SECRET_ROTATION_LIST_OPTIONS: Record = { [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION, - [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION + [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION, + [SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION }; export const listSecretRotationOptions = () => { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 1050c3419..4e585ce27 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -4,11 +4,13 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials", - [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret" + [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", + [SecretRotation.AzureClientSecret]: "Azure Client Secret" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, - [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, + [SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets }; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index a828acb32..61ea7a1d5 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -14,6 +14,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns"; +import { azureClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-fns"; import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { calculateNextRotationAt, @@ -100,7 +101,7 @@ export type TSecretRotationV2ServiceFactoryDep = { secretQueueService: Pick; snapshotService: Pick; queueService: Pick; - appConnectionDAL: Pick; + appConnectionDAL: Pick; }; export type TSecretRotationV2ServiceFactory = ReturnType; @@ -114,7 +115,8 @@ type TRotationFactoryImplementation = TRotationFactory< const SECRET_ROTATION_FACTORY_MAP: Record = { [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, - [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation + [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation, + [SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation }; export const secretRotationV2ServiceFactory = ({ diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index c52fa5465..03cd82ffb 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -12,6 +12,13 @@ import { TAuth0ClientSecretRotationListItem, TAuth0ClientSecretRotationWithConnection } from "./auth0-client-secret"; +import { + TAzureClientSecretRotation, + TAzureClientSecretRotationGeneratedCredentials, + TAzureClientSecretRotationInput, + TAzureClientSecretRotationListItem, + TAzureClientSecretRotationWithConnection +} from "./azure-client-secret"; import { TMsSqlCredentialsRotation, TMsSqlCredentialsRotationInput, @@ -27,26 +34,34 @@ import { import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { SecretRotation } from "./secret-rotation-v2-enums"; -export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation; +export type TSecretRotationV2 = + | TPostgresCredentialsRotation + | TMsSqlCredentialsRotation + | TAuth0ClientSecretRotation + | TAzureClientSecretRotation; export type TSecretRotationV2WithConnection = | TPostgresCredentialsRotationWithConnection | TMsSqlCredentialsRotationWithConnection - | TAuth0ClientSecretRotationWithConnection; + | TAuth0ClientSecretRotationWithConnection + | TAzureClientSecretRotationWithConnection; export type TSecretRotationV2GeneratedCredentials = | TSqlCredentialsRotationGeneratedCredentials - | TAuth0ClientSecretRotationGeneratedCredentials; + | TAuth0ClientSecretRotationGeneratedCredentials + | TAzureClientSecretRotationGeneratedCredentials; export type TSecretRotationV2Input = | TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput - | TAuth0ClientSecretRotationInput; + | TAuth0ClientSecretRotationInput + | TAzureClientSecretRotationInput; export type TSecretRotationV2ListItem = | TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem - | TAuth0ClientSecretRotationListItem; + | TAuth0ClientSecretRotationListItem + | TAzureClientSecretRotationListItem; export type TSecretRotationV2Raw = NonNullable>>; @@ -170,7 +185,7 @@ export type TRotationFactory< C extends TSecretRotationV2GeneratedCredentials > = ( secretRotation: T, - appConnectionDAL: Pick, + appConnectionDAL: Pick, kmsService: Pick ) => { issueCredentials: TRotationFactoryIssueCredentials; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts index 2db9c0251..08865f57f 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts @@ -1,11 +1,13 @@ import { z } from "zod"; import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; export const SecretRotationV2Schema = z.discriminatedUnion("type", [ PostgresCredentialsRotationSchema, MsSqlCredentialsRotationSchema, - Auth0ClientSecretRotationSchema + Auth0ClientSecretRotationSchema, + AzureClientSecretRotationSchema ]); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 125564ab3..9cd05ce97 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2006,6 +2006,10 @@ export const SecretRotations = { }, AUTH0_CLIENT_SECRET: { clientId: "The client ID of the Auth0 Application to rotate the client secret for." + }, + AZURE_CLIENT_SECRET: { + appId: "The ID of the Azure Application to rotate the client secret for.", + appName: "The name of the Azure Application to rotate the client secret for." } }, SECRETS_MAPPING: { @@ -2016,6 +2020,10 @@ export const SecretRotations = { AUTH0_CLIENT_SECRET: { clientId: "The name of the secret that the client ID will be mapped to.", clientSecret: "The name of the secret that the rotated client secret will be mapped to." + }, + AZURE_CLIENT_SECRET: { + clientId: "The name of the secret that the client ID will be mapped to.", + clientSecret: "The name of the secret that the rotated client secret will be mapped to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-client-secrets-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-client-secrets-connection-router.ts index 3d8eabf31..f699e60f1 100644 --- a/backend/src/server/routes/v1/app-connection-routers/azure-client-secrets-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/azure-client-secrets-connection-router.ts @@ -1,9 +1,14 @@ +import { z } from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { CreateAzureClientSecretsConnectionSchema, SanitizedAzureClientSecretsConnectionSchema, UpdateAzureClientSecretsConnectionSchema } from "@app/services/app-connection/azure-client-secrets"; +import { AuthMode } from "@app/services/auth/auth-type"; import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; @@ -15,4 +20,30 @@ export const registerAzureClientSecretsConnectionRouter = async (server: Fastify createSchema: CreateAzureClientSecretsConnectionSchema, updateSchema: UpdateAzureClientSecretsConnectionSchema }); + + server.route({ + method: "GET", + url: `/:connectionId/clients`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + clients: z.object({ name: z.string(), id: z.string(), appId: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const clients = await server.services.appConnection.azureClientSecrets.listApps(connectionId, req.permission); + + return { clients }; + } + }); }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index f32f9e044..ce745e8ba 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -33,6 +33,7 @@ import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { awsConnectionService } from "./aws/aws-connection-service"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets"; +import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service"; import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateCamundaConnectionCredentialsSchema } from "./camunda"; import { camundaConnectionService } from "./camunda/camunda-connection-service"; @@ -448,6 +449,7 @@ export const appConnectionServiceFactory = ({ terraformCloud: terraformCloudConnectionService(connectAppConnectionById), camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), vercel: vercelConnectionService(connectAppConnectionById), - auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) + auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), + azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) }; }; diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index 3be861b0e..580e42fd7 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -2,15 +2,22 @@ import { AxiosError, AxiosResponse } from "axios"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; -import { BadRequestError, InternalServerError } from "@app/lib/errors"; -import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { + decryptAppConnectionCredentials, + encryptAppConnectionCredentials, + getAppConnectionMethodName +} from "@app/services/app-connection/app-connection-fns"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TAppConnectionDALFactory } from "../app-connection-dal"; import { AppConnection } from "../app-connection-enums"; import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums"; import { ExchangeCodeAzureResponse, - TAzureClientSecretsConnectionConfig + TAzureClientSecretsConnectionConfig, + TAzureClientSecretsConnectionCredentials } from "./azure-client-secrets-connection-types"; export const getAzureClientSecretsConnectionListItem = () => { @@ -24,6 +31,72 @@ export const getAzureClientSecretsConnectionListItem = () => { }; }; +export const getAzureConnectionAccessToken = async ( + connectionId: string, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const appCfg = getConfig(); + if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new BadRequestError({ + message: `Azure environment variables have not been configured` + }); + } + + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AzureClientSecrets) { + throw new BadRequestError({ + message: `Connection with ID '${connectionId}' is not an Azure Client Secrets connection` + }); + } + + const credentials = (await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials + })) as TAzureClientSecretsConnectionCredentials; + + const { expiresAt, refreshToken } = credentials; + + // get new token if expired or less than 5 minutes until expiry + if (Date.now() < expiresAt - 300000) { + return credentials.accessToken; + } + + const { data } = await request.post( + IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"), + new URLSearchParams({ + grant_type: "refresh_token", + scope: `openid offline_access https://graph.microsoft.com/.default`, + client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + refresh_token: refreshToken + }) + ); + + const updatedCredentials = { + ...credentials, + accessToken: data.access_token, + expiresAt: Date.now() + data.expires_in * 1000, + refreshToken: data.refresh_token + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId: appConnection.orgId, + kmsService + }); + + await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials }); + + return data.access_token; +}; + export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => { const { credentials: inputCredentials, method } = config; @@ -44,10 +117,10 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA new URLSearchParams({ grant_type: "authorization_code", code: inputCredentials.code, - scope: `openid offline_access https://azconfig.io/.default`, + scope: `openid offline_access https://graph.microsoft.com/.default`, client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, - redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` + redirect_uri: `${SITE_URL}/organization/app-connections/azure-client-secrets/oauth/callback` }) ); } catch (e: unknown) { diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts index 0005edc41..d7e6e2d50 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts @@ -26,7 +26,11 @@ export type TAzureClientSecretsConnectionConfig = DiscriminativePick< orgId: string; }; -export type ExchangeCodeAzureResponse = { +export type TAzureClientSecretsConnectionCredentials = z.infer< + typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema +>; + +export interface ExchangeCodeAzureResponse { token_type: string; scope: string; expires_in: number; @@ -34,8 +38,29 @@ export type ExchangeCodeAzureResponse = { access_token: string; refresh_token: string; id_token: string; -}; +} + +export interface TAzureRegisteredApp { + id: string; + appId: string; + displayName: string; + description?: string; + createdDateTime: string; + identifierUris?: string[]; + signInAudience?: string; +} + +export interface TAzureListRegisteredAppsResponse { + "@odata.context": string; + "@odata.nextLink"?: string; + value: TAzureRegisteredApp[]; +} + +export interface TAzureClientSecret { + keyId: string; + displayName?: string; + startDateTime: string; + endDateTime: string; + secretText?: string; +} -export type TAzureClientSecretsConnectionCredentials = z.infer< - typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema ->; diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-service.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-service.ts new file mode 100644 index 000000000..e95178973 --- /dev/null +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-service.ts @@ -0,0 +1,70 @@ +import { request } from "@app/lib/config/request"; +import { OrgServiceActor } from "@app/lib/types"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { + TAzureClientSecretsConnection, + TAzureListRegisteredAppsResponse, + TAzureRegisteredApp +} from "./azure-client-secrets-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listAzureRegisteredApps = async ( + appConnection: TAzureClientSecretsConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const accessToken = await getAzureConnectionAccessToken(appConnection.id, appConnectionDAL, kmsService); + + const graphEndpoint = `https://graph.microsoft.com/v1.0/applications`; + await blockLocalAndPrivateIpAddresses(graphEndpoint); + + const apps: TAzureRegisteredApp[] = []; + let nextLink = graphEndpoint; + + while (nextLink) { + // eslint-disable-next-line no-await-in-loop + const { data: appsPage } = await request.get(nextLink, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + }); + + apps.push(...appsPage.value); + nextLink = appsPage["@odata.nextLink"] || ""; + } + + return apps; +}; + +export const azureClientSecretsConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listApps = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.AzureClientSecrets, connectionId, actor); + + const apps = await listAzureRegisteredApps(appConnection, appConnectionDAL, kmsService); + + return apps.map((app) => ({ + id: app.id, + name: app.displayName, + appId: app.appId + })); + }; + + return { + listApps + }; +}; diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts index 8e8a6b2a7..ee90b33ee 100644 --- a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -38,7 +38,11 @@ export const getAzureConnectionAccessToken = async ( throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); } - if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) { + if ( + appConnection.app !== AppConnection.AzureKeyVault && + appConnection.app !== AppConnection.AzureAppConfiguration && + appConnection.app !== AppConnection.AzureClientSecrets + ) { throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` }); } diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/AzureClientSecretRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/AzureClientSecretRotationParametersFields.tsx new file mode 100644 index 000000000..ceee60dec --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/AzureClientSecretRotationParametersFields.tsx @@ -0,0 +1,71 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { useAzureConnectionListClients } from "@app/hooks/api/appConnections/azure"; +import { TAzureClient } from "@app/hooks/api/appConnections/azure/types"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const AzureClientSecretRotationParametersFields = () => { + const { control, watch, setValue } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.AzureClientSecret; + } + >(); + + const connectionId = watch("connection.id"); + + const { data: clients, isPending: isClientsPending } = useAzureConnectionListClients( + connectionId, + { enabled: Boolean(connectionId) } + ); + + return ( + ( + + Ensure that your connection has the{" "} + read_clients permission and the application + exists in the connection's audience. + + } + > +
+ Don't see the application you're looking for?{" "} + +
+ + } + > + client.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("parameters.appName", (option as SingleValue)?.name ?? ""); + }} + options={clients} + placeholder="Select an application..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx index 444510e1e..fc33de2bb 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx @@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2Form } from "../schemas"; import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields"; +import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields"; import { SqlCredentialsRotationParametersFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields, + [SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields }; export const SecretRotationV2ParametersFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/AzureClientSecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/AzureClientSecretRotationReviewFields.tsx new file mode 100644 index 000000000..8c777f7cf --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/AzureClientSecretRotationReviewFields.tsx @@ -0,0 +1,30 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +import { SecretRotationReviewSection } from "./shared"; + +export const AzureClientSecretRotationReviewFields = () => { + const { watch } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.AzureClientSecret; + } + >(); + + const [parameters, { clientId, clientSecret }] = watch(["parameters", "secretsMapping"]); + + return ( + <> + + {parameters.appName} + {parameters.appId} + + + {clientId} + {clientSecret} + + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index 4fb3b6d24..60bdd32fe 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -7,12 +7,14 @@ import { getRotateAtLocal } from "@app/helpers/secretRotationsV2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields"; +import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields"; import { SqlCredentialsRotationReviewFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields, + [SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields }; export const SecretRotationV2ReviewFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/AzureClientSecretRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/AzureClientSecretRotationSecretsMappingFields.tsx new file mode 100644 index 000000000..77a34d083 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/AzureClientSecretRotationSecretsMappingFields.tsx @@ -0,0 +1,58 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2"; + +import { SecretsMappingTable } from "./shared"; + +export const AzureClientSecretRotationSecretsMappingFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.AzureClientSecret; + } + >(); + + const { rotationOption } = useSecretRotationV2Option(SecretRotation.AzureClientSecret); + + const items = [ + { + name: "Client ID", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.clientId" + /> + ) + }, + { + name: "Client Secret", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.clientSecret" + /> + ) + } + ]; + + return ; +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index 58277d593..0c2213557 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2Form } from "../schemas"; import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields"; +import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields"; import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields, + [SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields }; export const SecretRotationV2SecretsMappingFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema.ts new file mode 100644 index 000000000..fbceaad13 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const AzureClientSecretRotationSchema = z + .object({ + type: z.literal(SecretRotation.AzureClientSecret), + parameters: z.object({ + appId: z.string().trim().min(1, "App ID required"), + appName: z.string().trim().min(1, "App Name required") + }), + secretsMapping: z.object({ + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client Secret required") + }) + }) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index 295e199fe..e6b4bad6c 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -1,13 +1,15 @@ import { z } from "zod"; import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/auth0-client-secret-rotation-schema"; +import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema"; import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema"; import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema"; const SecretRotationUnionSchema = z.discriminatedUnion("type", [ PostgresCredentialsRotationSchema, MsSqlCredentialsRotationSchema, - Auth0ClientSecretRotationSchema + Auth0ClientSecretRotationSchema, + AzureClientSecretRotationSchema ]); export const SecretRotationV2FormSchema = SecretRotationUnionSchema; diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index 1a57d37cd..20b5d3878 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -19,20 +19,27 @@ export const SECRET_ROTATION_MAP: Record< name: "Auth0 Client Secret", image: "Auth0.png", size: 35 + }, + [SecretRotation.AzureClientSecret]: { + name: "Azure Client Secret", + image: "Microsoft Azure.png", + size: 35 } }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, - [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, + [SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets }; // if a rotation can potentially have downtime due to rotating a single credential set this to false export const IS_ROTATION_DUAL_CREDENTIALS: Record = { [SecretRotation.PostgresCredentials]: true, [SecretRotation.MsSqlCredentials]: true, - [SecretRotation.Auth0ClientSecret]: false + [SecretRotation.Auth0ClientSecret]: false, + [SecretRotation.AzureClientSecret]: false }; export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { diff --git a/frontend/src/hooks/api/appConnections/azure/index.ts b/frontend/src/hooks/api/appConnections/azure/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/azure/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/frontend/src/hooks/api/appConnections/azure/queries.tsx b/frontend/src/hooks/api/appConnections/azure/queries.tsx new file mode 100644 index 000000000..98d1c2d29 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/azure/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TAzureClient } from "./types"; + +const azureConnectionKeys = { + all: [...appConnectionKeys.all, "azure"] as const, + listClients: (connectionId: string) => + [...azureConnectionKeys.all, "clients", connectionId] as const +}; + +export const useAzureConnectionListClients = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TAzureClient[], + unknown, + TAzureClient[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: azureConnectionKeys.listClients(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ clients: TAzureClient[] }>( + `/api/v1/app-connections/azure-client-secrets/${connectionId}/clients` + ); + + return data.clients; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/azure/types.ts b/frontend/src/hooks/api/appConnections/azure/types.ts new file mode 100644 index 000000000..ce74879f0 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/azure/types.ts @@ -0,0 +1,4 @@ +export type TAzureClient = { + name: string; + id: string; +}; diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index d43cacb3a..3a362f50b 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -1,7 +1,8 @@ export enum SecretRotation { PostgresCredentials = "postgres-credentials", MsSqlCredentials = "mssql-credentials", - Auth0ClientSecret = "auth0-client-secret" + Auth0ClientSecret = "auth0-client-secret", + AzureClientSecret = "azure-client-secret" } export enum SecretRotationStatus { diff --git a/frontend/src/hooks/api/secretRotationsV2/types/azure-client-secret-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/azure-client-secret-rotation.ts new file mode 100644 index 000000000..09af2b85d --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/azure-client-secret-rotation.ts @@ -0,0 +1,38 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TAzureClientSecretRotation = TSecretRotationV2Base & { + type: SecretRotation.AzureClientSecret; + parameters: { + appId: string; + appName: string; + }; + secretsMapping: { + clientId: string; + clientSecret: string; + }; +}; + +export type TAzureClientSecretRotationGeneratedCredentials = { + clientId: string; + clientSecret: string; +}; + +export type TAzureClientSecretRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.AzureClientSecret, + TAzureClientSecretRotationGeneratedCredentials + >; + +export type TAzureClientSecretRotationOption = { + name: string; + type: SecretRotation.AzureClientSecret; + connection: AppConnection.AzureClientSecrets; + template: { + secretsMapping: TAzureClientSecretRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index 96d568d74..4a4d9fc75 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -4,6 +4,11 @@ import { TAuth0ClientSecretRotationGeneratedCredentialsResponse, TAuth0ClientSecretRotationOption } from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation"; +import { + TAzureClientSecretRotation, + TAzureClientSecretRotationGeneratedCredentialsResponse, + TAzureClientSecretRotationOption +} from "@app/hooks/api/secretRotationsV2/types/azure-client-secret-rotation"; import { TMsSqlCredentialsRotation, TMsSqlCredentialsRotationGeneratedCredentialsResponse @@ -20,13 +25,15 @@ export type TSecretRotationV2 = ( | TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation + | TAzureClientSecretRotation ) & { secrets: (SecretV3RawSanitized | null)[]; }; export type TSecretRotationV2Option = | TSqlCredentialsRotationOption - | TAuth0ClientSecretRotationOption; + | TAuth0ClientSecretRotationOption + | TAzureClientSecretRotationOption; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; @@ -35,7 +42,8 @@ export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 }; export type TViewSecretRotationGeneratedCredentialsResponse = | TPostgresCredentialsRotationGeneratedCredentialsResponse | TMsSqlCredentialsRotationGeneratedCredentialsResponse - | TAuth0ClientSecretRotationGeneratedCredentialsResponse; + | TAuth0ClientSecretRotationGeneratedCredentialsResponse + | TAzureClientSecretRotationGeneratedCredentialsResponse; export type TCreateSecretRotationV2DTO = DiscriminativePick< TSecretRotationV2, @@ -82,10 +90,12 @@ export type TSecretRotationOptionMap = { [SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption; [SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption; [SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption; + [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption; }; export type TSecretRotationGeneratedCredentialsResponseMap = { [SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse; [SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse; [SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse; + [SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse; };