mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add Azure Client Secrets Rotation
This commit is contained in:
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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<AzureAddPasswordResponse>(
|
||||
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<TAzureClientSecretRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revokes a list of credentials.
|
||||
*/
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TAzureClientSecretRotationGeneratedCredentials> = 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<TAzureClientSecretRotationGeneratedCredentials> = 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<TAzureClientSecretRotationGeneratedCredentials> = ({
|
||||
clientSecret,
|
||||
clientId
|
||||
}) => [
|
||||
{ key: secretsMapping.clientSecret, value: clientSecret },
|
||||
{ key: secretsMapping.clientId, value: clientId }
|
||||
];
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
@@ -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<typeof AzureClientSecretRotationSchema>;
|
||||
|
||||
export type TAzureClientSecretRotationInput = z.infer<typeof CreateAzureClientSecretRotationSchema>;
|
||||
|
||||
export type TAzureClientSecretRotationListItem = z.infer<typeof AzureClientSecretRotationListItemSchema>;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, TSecretRotationV2ListItem> = {
|
||||
[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 = () => {
|
||||
|
||||
@@ -4,11 +4,13 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[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, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets
|
||||
};
|
||||
|
||||
@@ -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<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
};
|
||||
|
||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||
@@ -114,7 +115,8 @@ type TRotationFactoryImplementation = TRotationFactory<
|
||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
||||
[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 = ({
|
||||
|
||||
@@ -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<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||
|
||||
@@ -170,7 +185,7 @@ export type TRotationFactory<
|
||||
C extends TSecretRotationV2GeneratedCredentials
|
||||
> = (
|
||||
secretRotation: T,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
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<ExchangeCodeAzureResponse>(
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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<TAzureClientSecretsConnection>;
|
||||
|
||||
const listAzureRegisteredApps = async (
|
||||
appConnection: TAzureClientSecretsConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
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<TAzureListRegisteredAppsResponse>(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<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -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` });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Controller
|
||||
name="parameters.appId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Application"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Ensure that your connection has the{" "}
|
||||
<span className="font-semibold">read_clients</span> permission and the application
|
||||
exists in the connection's audience.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the application you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isClientsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={clients?.find((client) => client.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TAzureClient>)?.id ?? null);
|
||||
setValue("parameters.appName", (option as SingleValue<TAzureClient>)?.name ?? "");
|
||||
}}
|
||||
options={clients}
|
||||
placeholder="Select an application..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ParametersFields = () => {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<SecretRotationReviewSection label="Parameters">
|
||||
<GenericFieldLabel label="App Name">{parameters.appName}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="App ID">{parameters.appId}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
<SecretRotationReviewSection label="Secrets Mapping">
|
||||
<GenericFieldLabel label="Client ID">{clientId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Client Secret">{clientSecret}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ReviewFields = () => {
|
||||
|
||||
@@ -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: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.clientId}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.clientId"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "Client Secret",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.clientSecret}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.clientSecret"
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return <SecretsMappingTable items={items} />;
|
||||
};
|
||||
@@ -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, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2SecretsMappingFields = () => {
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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, AppConnection> = {
|
||||
[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, boolean> = {
|
||||
[SecretRotation.PostgresCredentials]: true,
|
||||
[SecretRotation.MsSqlCredentials]: true,
|
||||
[SecretRotation.Auth0ClientSecret]: false
|
||||
[SecretRotation.Auth0ClientSecret]: false,
|
||||
[SecretRotation.AzureClientSecret]: false
|
||||
};
|
||||
|
||||
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
|
||||
|
||||
1
frontend/src/hooks/api/appConnections/azure/index.ts
Normal file
1
frontend/src/hooks/api/appConnections/azure/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./queries";
|
||||
37
frontend/src/hooks/api/appConnections/azure/queries.tsx
Normal file
37
frontend/src/hooks/api/appConnections/azure/queries.tsx
Normal file
@@ -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<typeof azureConnectionKeys.listClients>
|
||||
>,
|
||||
"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
|
||||
});
|
||||
};
|
||||
4
frontend/src/hooks/api/appConnections/azure/types.ts
Normal file
4
frontend/src/hooks/api/appConnections/azure/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type TAzureClient = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"];
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user