Merge pull request #3440 from Infisical/feat/azureClientSecretsRotation

Feat/azure client secrets rotation
This commit is contained in:
Maidul Islam
2025-04-30 19:45:02 -04:00
committed by GitHub
86 changed files with 1852 additions and 26 deletions

View File

@@ -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
});

View File

@@ -2,6 +2,7 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotat
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router";
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -15,6 +16,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter
};

View File

@@ -3,6 +3,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 { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
@@ -16,8 +17,9 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema
AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {

View File

@@ -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"
}
}
};

View File

@@ -0,0 +1,202 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
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 { BadRequestError } from "@app/lib/errors";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets";
const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
type AzureErrorResponse = { error: { message: string } };
const sleep = async () =>
new Promise((resolve) => {
setTimeout(resolve, 1000);
});
export const azureClientSecretRotationFactory: TRotationFactory<
TAzureClientSecretRotationWithConnection,
TAzureClientSecretRotationGeneratedCredentials
> = (secretRotation, appConnectionDAL, kmsService) => {
const {
connection,
parameters: { objectId, clientId: clientIdParam },
secretsMapping
} = 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/${objectId}/addPassword`;
const now = new Date();
const formattedDate = `${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(
2,
"0"
)}-${now.getFullYear()}`;
const endDateTime = new Date();
endDateTime.setFullYear(now.getFullYear() + 5);
try {
const { data } = await request.post<AzureAddPasswordResponse>(
endpoint,
{
passwordCredential: {
displayName: `Infisical Rotated Secret (${formattedDate})`,
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,
keyId: data.keyId,
clientId: clientIdParam
};
} catch (error: unknown) {
if (error instanceof AxiosError) {
let message;
if (
error.response?.data &&
typeof error.response.data === "object" &&
"error" in error.response.data &&
typeof (error.response.data as AzureErrorResponse).error.message === "string"
) {
message = (error.response.data as AzureErrorResponse).error.message;
}
throw new BadRequestError({
message: `Failed to add client secret to Azure app ${objectId}: ${
message || error.message || "Unknown error"
}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
};
/**
* Revokes a client secret from the Azure app using its keyId.
*/
const revokeCredential = async (keyId: string) => {
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
try {
await request.post(
endpoint,
{ keyId },
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
} catch (error: unknown) {
if (error instanceof AxiosError) {
let message;
if (
error.response?.data &&
typeof error.response.data === "object" &&
"error" in error.response.data &&
typeof (error.response.data as AzureErrorResponse).error.message === "string"
) {
message = (error.response.data as AzureErrorResponse).error.message;
}
throw new BadRequestError({
message: `Failed to remove client secret with keyId ${keyId} from app ${objectId}: ${
message || error.message || "Unknown error"
}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
};
/**
* 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();
for (const { keyId } of credentials) {
await revokeCredential(keyId);
await sleep();
}
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?.keyId) {
await revokeCredential(oldCredentials.keyId);
}
return callback(newCredentials);
};
/**
* Maps the generated credentials into the secret payload format.
*/
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAzureClientSecretRotationGeneratedCredentials> = ({
clientSecret
}) => [
{ key: secretsMapping.clientSecret, value: clientSecret },
{ key: secretsMapping.clientId, value: clientIdParam }
];
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -0,0 +1,74 @@
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(),
keyId: z.string()
})
.array()
.min(1)
.max(2);
const AzureClientSecretRotationParametersSchema = z.object({
objectId: z
.string()
.trim()
.min(1, "Object ID Required")
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.objectId),
appName: z.string().trim().describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appName).optional(),
clientId: z
.string()
.trim()
.min(1, "Client ID Required")
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.clientId)
});
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
});

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -2,8 +2,9 @@ export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret"
AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password"
}
export enum SecretRotationStatus {

View File

@@ -5,6 +5,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret";
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
@@ -21,8 +22,9 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {

View File

@@ -5,14 +5,16 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
[SecretRotation.LdapPassword]: "LDAP Password"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.LdapPassword]: AppConnection.LDAP
};

View File

@@ -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 { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
@@ -102,7 +103,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>;
@@ -117,8 +118,9 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({
@@ -447,7 +449,8 @@ export const secretRotationV2ServiceFactory = ({
{
parameters: payload.parameters,
secretsMapping,
connection
connection,
rotationInterval: payload.rotationInterval
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService

View File

@@ -19,6 +19,13 @@ import {
TAwsIamUserSecretRotationListItem,
TAwsIamUserSecretRotationWithConnection
} from "./aws-iam-user-secret";
import {
TAzureClientSecretRotation,
TAzureClientSecretRotationGeneratedCredentials,
TAzureClientSecretRotationInput,
TAzureClientSecretRotationListItem,
TAzureClientSecretRotationWithConnection
} from "./azure-client-secret";
import {
TLdapPasswordRotation,
TLdapPasswordRotationGeneratedCredentials,
@@ -45,6 +52,7 @@ export type TSecretRotationV2 =
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TAzureClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation;
@@ -52,12 +60,14 @@ export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection
| TAzureClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials
| TAzureClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
@@ -65,6 +75,7 @@ export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput
| TAzureClientSecretRotationInput
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput;
@@ -72,6 +83,7 @@ export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem
| TAzureClientSecretRotationListItem
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem;
@@ -197,7 +209,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>;

View File

@@ -1,6 +1,7 @@
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 { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
@@ -11,6 +12,7 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema,
AzureClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]);

View File

@@ -1875,6 +1875,10 @@ export const AppConnections = {
TEAMCITY: {
instanceUrl: "The TeamCity instance URL to connect with.",
accessToken: "The access token to use to connect with TeamCity."
},
AZURE_CLIENT_SECRETS: {
code: "The OAuth code to use to connect with Azure Client Secrets.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
}
}
};
@@ -2083,6 +2087,11 @@ export const SecretRotations = {
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
},
AZURE_CLIENT_SECRET: {
objectId: "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.",
clientId: "The client ID of the Azure Application to rotate the client secret for."
},
LDAP_PASSWORD: {
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
},
@@ -2113,6 +2122,10 @@ export const SecretRotations = {
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."
},
LDAP_PASSWORD: {
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
password: "The name of the secret that the rotated password will be mapped to."

View File

@@ -10,6 +10,10 @@ import {
AzureAppConfigurationConnectionListItemSchema,
SanitizedAzureAppConfigurationConnectionSchema
} from "@app/services/app-connection/azure-app-configuration";
import {
AzureClientSecretsConnectionListItemSchema,
SanitizedAzureClientSecretsConnectionSchema
} from "@app/services/app-connection/azure-client-secrets";
import {
AzureKeyVaultConnectionListItemSchema,
SanitizedAzureKeyVaultConnectionSchema
@@ -63,8 +67,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options,
...SanitizedAzureClientSecretsConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options
]);
@@ -82,8 +87,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema,
AzureClientSecretsConnectionListItemSchema,
WindmillConnectionListItemSchema,
LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema
]);

View File

@@ -0,0 +1,49 @@
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";
export const registerAzureClientSecretsConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.AzureClientSecrets,
server,
sanitizedResponseSchema: SanitizedAzureClientSecretsConnectionSchema,
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 };
}
});
};

View File

@@ -3,6 +3,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
@@ -26,6 +27,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.GCP]: registerGcpConnectionRouter,
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,

View File

@@ -5,6 +5,7 @@ export enum AppConnection {
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
AzureClientSecrets = "azure-client-secrets",
Humanitec = "humanitec",
TerraformCloud = "terraform-cloud",
Vercel = "vercel",

View File

@@ -23,6 +23,11 @@ import {
getAzureAppConfigurationConnectionListItem,
validateAzureAppConfigurationConnectionCredentials
} from "./azure-app-configuration";
import {
AzureClientSecretsConnectionMethod,
getAzureClientSecretsConnectionListItem,
validateAzureClientSecretsConnectionCredentials
} from "./azure-client-secrets";
import {
AzureKeyVaultConnectionMethod,
getAzureKeyVaultConnectionListItem,
@@ -76,6 +81,7 @@ export const listAppConnectionOptions = () => {
getPostgresConnectionListItem(),
getMsSqlConnectionListItem(),
getCamundaConnectionListItem(),
getAzureClientSecretsConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem(),
getLdapConnectionListItem(),
@@ -136,6 +142,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureAppConfiguration]:
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureClientSecrets]:
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -157,6 +165,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "GitHub App";
case AzureKeyVaultConnectionMethod.OAuth:
case AzureAppConfigurationConnectionMethod.OAuth:
case AzureClientSecretsConnectionMethod.OAuth:
case GitHubConnectionMethod.OAuth:
return "OAuth";
case AwsConnectionMethod.AccessKey:
@@ -226,6 +235,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future

View File

@@ -6,6 +6,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.GCP]: "GCP",
[AppConnection.AzureKeyVault]: "Azure Key Vault",
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
[AppConnection.Databricks]: "Databricks",
[AppConnection.Humanitec]: "Humanitec",
[AppConnection.TerraformCloud]: "Terraform Cloud",

View File

@@ -32,6 +32,8 @@ import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
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";
@@ -76,6 +78,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
@@ -454,8 +457,9 @@ export const appConnectionServiceFactory = ({
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById),
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById)
};
};

View File

@@ -21,6 +21,12 @@ import {
TAzureAppConfigurationConnectionInput,
TValidateAzureAppConfigurationConnectionCredentialsSchema
} from "./azure-app-configuration";
import {
TAzureClientSecretsConnection,
TAzureClientSecretsConnectionConfig,
TAzureClientSecretsConnectionInput,
TValidateAzureClientSecretsConnectionCredentialsSchema
} from "./azure-client-secrets";
import {
TAzureKeyVaultConnection,
TAzureKeyVaultConnectionConfig,
@@ -107,6 +113,7 @@ export type TAppConnection = { id: string } & (
| TPostgresConnection
| TMsSqlConnection
| TCamundaConnection
| TAzureClientSecretsConnection
| TWindmillConnection
| TAuth0Connection
| TLdapConnection
@@ -130,6 +137,7 @@ export type TAppConnectionInput = { id: string } & (
| TPostgresConnectionInput
| TMsSqlConnectionInput
| TCamundaConnectionInput
| TAzureClientSecretsConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput
| TLdapConnectionInput
@@ -153,12 +161,13 @@ export type TAppConnectionConfig =
| TGcpConnectionConfig
| TAzureKeyVaultConnectionConfig
| TAzureAppConfigurationConnectionConfig
| TAzureClientSecretsConnectionConfig
| TDatabricksConnectionConfig
| THumanitecConnectionConfig
| TTerraformCloudConnectionConfig
| TVercelConnectionConfig
| TSqlConnectionConfig
| TCamundaConnectionConfig
| TVercelConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig
| TLdapConnectionConfig
@@ -170,13 +179,14 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateGcpConnectionCredentialsSchema
| TValidateAzureKeyVaultConnectionCredentialsSchema
| TValidateAzureAppConfigurationConnectionCredentialsSchema
| TValidateAzureClientSecretsConnectionCredentialsSchema
| TValidateDatabricksConnectionCredentialsSchema
| TValidateHumanitecConnectionCredentialsSchema
| TValidatePostgresConnectionCredentialsSchema
| TValidateMsSqlConnectionCredentialsSchema
| TValidateCamundaConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema

View File

@@ -0,0 +1,3 @@
export enum AzureClientSecretsConnectionMethod {
OAuth = "oauth"
}

View File

@@ -0,0 +1,169 @@
import { AxiosError, AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
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,
TAzureClientSecretsConnectionCredentials
} from "./azure-client-secrets-connection-types";
export const getAzureClientSecretsConnectionListItem = () => {
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
return {
name: "Azure Client Secrets" as const,
app: AppConnection.AzureClientSecrets as const,
methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
};
};
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 { refreshToken } = credentials;
const currentTime = Date.now();
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: currentTime + 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;
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
if (!SITE_URL) {
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
}
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
let tokenError: AxiosError | null = null;
try {
tokenResp = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
new URLSearchParams({
grant_type: "authorization_code",
code: inputCredentials.code,
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`
})
);
} catch (e: unknown) {
if (e instanceof AxiosError) {
tokenError = e;
} else {
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
}
}
if (tokenError) {
if (tokenError instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to get access token: ${
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
}`
});
} else {
throw new InternalServerError({
message: "Failed to get access token"
});
}
}
if (!tokenResp) {
throw new InternalServerError({
message: `Failed to get access token: Token was empty with no error`
});
}
switch (method) {
case AzureClientSecretsConnectionMethod.OAuth:
return {
tenantId: inputCredentials.tenantId,
accessToken: tokenResp.data.access_token,
refreshToken: tokenResp.data.refresh_token,
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
};
default:
throw new InternalServerError({
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
});
}
};

View File

@@ -0,0 +1,80 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
export const AzureClientSecretsConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.code),
tenantId: z
.string()
.trim()
.min(1, "Tenant ID required")
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
});
export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object({
tenantId: z.string(),
accessToken: z.string(),
refreshToken: z.string(),
expiresAt: z.number()
});
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(AzureClientSecretsConnectionMethod.OAuth)
.describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method),
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
)
})
]);
export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecretsConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets)
);
export const UpdateAzureClientSecretsConnectionSchema = z
.object({
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
const BaseAzureClientSecretsConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.AzureClientSecrets)
});
export const AzureClientSecretsConnectionSchema = z.intersection(
BaseAzureClientSecretsConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
})
])
);
export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion("method", [
BaseAzureClientSecretsConnectionSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true
})
})
]);
export const AzureClientSecretsConnectionListItemSchema = z.object({
name: z.literal("Azure Client Secrets"),
app: z.literal(AppConnection.AzureClientSecrets),
methods: z.nativeEnum(AzureClientSecretsConnectionMethod).array(),
oauthClientId: z.string().optional()
});

View File

@@ -0,0 +1,65 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
AzureClientSecretsConnectionSchema,
CreateAzureClientSecretsConnectionSchema,
ValidateAzureClientSecretsConnectionCredentialsSchema
} from "./azure-client-secrets-connection-schemas";
export type TAzureClientSecretsConnection = z.infer<typeof AzureClientSecretsConnectionSchema>;
export type TAzureClientSecretsConnectionInput = z.infer<typeof CreateAzureClientSecretsConnectionSchema> & {
app: AppConnection.AzureClientSecrets;
};
export type TValidateAzureClientSecretsConnectionCredentialsSchema =
typeof ValidateAzureClientSecretsConnectionCredentialsSchema;
export type TAzureClientSecretsConnectionConfig = DiscriminativePick<
TAzureClientSecretsConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TAzureClientSecretsConnectionCredentials = z.infer<
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
>;
export interface ExchangeCodeAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
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;
}

View File

@@ -0,0 +1,68 @@
import { request } from "@app/lib/config/request";
import { OrgServiceActor } from "@app/lib/types";
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`;
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
};
};

View File

@@ -0,0 +1,4 @@
export * from "./azure-client-secrets-connection-enums";
export * from "./azure-client-secrets-connection-fns";
export * from "./azure-client-secrets-connection-schemas";
export * from "./azure-client-secrets-connection-types";

View File

@@ -38,8 +38,12 @@ export const getAzureConnectionAccessToken = async (
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
}
if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) {
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` });
if (
appConnection.app !== AppConnection.AzureKeyVault &&
appConnection.app !== AppConnection.AzureAppConfiguration &&
appConnection.app !== AppConnection.AzureClientSecrets
) {
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not a valid Azure connection` });
}
const credentials = (await decryptAppConnectionCredentials({