Add Azure Client Secrets App Connection

This commit is contained in:
carlosmonastyrski
2025-04-15 23:39:06 -03:00
parent 0f7e8585dc
commit a889f92528
21 changed files with 557 additions and 10 deletions
@@ -8,6 +8,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
@@ -51,7 +55,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedVercelConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options
...SanitizedCamundaConnectionSchema.options,
...SanitizedAzureClientSecretsConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -66,7 +71,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
VercelConnectionListItemSchema,
PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema
CamundaConnectionListItemSchema,
AzureClientSecretsConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateAzureClientSecretsConnectionSchema,
SanitizedAzureClientSecretsConnectionSchema,
UpdateAzureClientSecretsConnectionSchema
} from "@app/services/app-connection/azure-client-secrets";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerAzureClientSecretsConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.AzureClientSecrets,
server,
sanitizedResponseSchema: SanitizedAzureClientSecretsConnectionSchema,
createSchema: CreateAzureClientSecretsConnectionSchema,
updateSchema: UpdateAzureClientSecretsConnectionSchema
});
};
@@ -2,6 +2,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
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";
@@ -28,5 +29,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Vercel]: registerVercelConnectionRouter,
[AppConnection.Postgres]: registerPostgresConnectionRouter,
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter
[AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter
};
@@ -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",
@@ -22,6 +22,11 @@ import {
getAzureAppConfigurationConnectionListItem,
validateAzureAppConfigurationConnectionCredentials
} from "./azure-app-configuration";
import {
AzureClientSecretsConnectionMethod,
getAzureClientSecretsConnectionListItem,
validateAzureClientSecretsConnectionCredentials
} from "./azure-client-secrets";
import {
AzureKeyVaultConnectionMethod,
getAzureKeyVaultConnectionListItem,
@@ -63,7 +68,8 @@ export const listAppConnectionOptions = () => {
getVercelConnectionListItem(),
getPostgresConnectionListItem(),
getMsSqlConnectionListItem(),
getCamundaConnectionListItem()
getCamundaConnectionListItem(),
getAzureClientSecretsConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -122,7 +128,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnect
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureClientSecrets]:
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator
};
export const validateAppConnectionCredentials = async (
@@ -135,6 +143,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:
@@ -196,5 +205,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported
};
@@ -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",
@@ -30,6 +30,7 @@ import {
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service";
@@ -68,7 +69,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -15,6 +15,12 @@ import {
TAzureAppConfigurationConnectionInput,
TValidateAzureAppConfigurationConnectionCredentialsSchema
} from "./azure-app-configuration";
import {
TAzureClientSecretsConnection,
TAzureClientSecretsConnectionConfig,
TAzureClientSecretsConnectionInput,
TValidateAzureClientSecretsConnectionCredentialsSchema
} from "./azure-client-secrets";
import {
TAzureKeyVaultConnection,
TAzureKeyVaultConnectionConfig,
@@ -83,6 +89,7 @@ export type TAppConnection = { id: string } & (
| TPostgresConnection
| TMsSqlConnection
| TCamundaConnection
| TAzureClientSecretsConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -102,6 +109,7 @@ export type TAppConnectionInput = { id: string } & (
| TPostgresConnectionInput
| TMsSqlConnectionInput
| TCamundaConnectionInput
| TAzureClientSecretsConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -126,7 +134,8 @@ export type TAppConnectionConfig =
| TTerraformCloudConnectionConfig
| TVercelConnectionConfig
| TSqlConnectionConfig
| TCamundaConnectionConfig;
| TCamundaConnectionConfig
| TAzureClientSecretsConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -140,7 +149,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateMsSqlConnectionCredentialsSchema
| TValidateCamundaConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema;
| TValidateVercelConnectionCredentialsSchema
| TValidateAzureClientSecretsConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -0,0 +1,3 @@
export enum AzureClientSecretsConnectionMethod {
OAuth = "oauth"
}
@@ -0,0 +1,96 @@
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 { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
import {
ExchangeCodeAzureResponse,
TAzureClientSecretsConnectionConfig
} 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 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 (!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://azconfig.io/.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}`
});
}
};
@@ -0,0 +1,76 @@
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"),
tenantId: z.string().trim().optional()
});
export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object({
tenantId: z.string().optional(),
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()
});
@@ -0,0 +1,41 @@
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 ExchangeCodeAzureResponse = {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
};
export type TAzureClientSecretsConnectionCredentials = z.infer<
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
>;
@@ -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";