diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index ebc23a4bd..1d400ee90 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -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) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-client-secrets-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-client-secrets-connection-router.ts new file mode 100644 index 000000000..3d8eabf31 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-client-secrets-connection-router.ts @@ -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 + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index b89eb0991..0c60d2311 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -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 { 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 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 }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 8c77bfc58..ca30577ee 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -6,6 +6,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [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", diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 143cce3e3..804ead1c4 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -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>>; @@ -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; diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts new file mode 100644 index 000000000..338126c1e --- /dev/null +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts @@ -0,0 +1,3 @@ +export enum AzureClientSecretsConnectionMethod { + OAuth = "oauth" +} diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts new file mode 100644 index 000000000..3be861b0e --- /dev/null +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -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 | null = null; + let tokenError: AxiosError | null = null; + + try { + tokenResp = await request.post( + 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}` + }); + } +}; diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts new file mode 100644 index 000000000..962125b5a --- /dev/null +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts @@ -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() +}); diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts new file mode 100644 index 000000000..0005edc41 --- /dev/null +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts @@ -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; + +export type TAzureClientSecretsConnectionInput = z.infer & { + 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 +>; diff --git a/backend/src/services/app-connection/azure-client-secrets/index.ts b/backend/src/services/app-connection/azure-client-secrets/index.ts new file mode 100644 index 000000000..60177973e --- /dev/null +++ b/backend/src/services/app-connection/azure-client-secrets/index.ts @@ -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"; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 53e2f5468..e4c06f4f4 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -5,6 +5,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AwsConnectionMethod, AzureAppConfigurationConnectionMethod, + AzureClientSecretsConnectionMethod, AzureKeyVaultConnectionMethod, CamundaConnectionMethod, DatabricksConnectionMethod, @@ -30,6 +31,10 @@ export const APP_CONNECTION_MAP: Record { return ; case AppConnection.Camunda: return ; + case AppConnection.AzureClientSecrets: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -143,6 +146,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Camunda: return ; + case AppConnection.AzureClientSecrets: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx new file mode 100644 index 000000000..e1d978eac --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx @@ -0,0 +1,178 @@ +import crypto from "crypto"; + +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { isInfisicalCloud } from "@app/helpers/platform"; +import { + AzureClientSecretsConnectionMethod, + TAzureClientSecretsConnection, + useGetAppConnectionOption +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TAzureClientSecretsConnection; +}; + +const formSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.AzureClientSecrets), + method: z.nativeEnum(AzureClientSecretsConnectionMethod), + tenantId: z.string().trim().optional() +}); + +type FormData = z.infer; + +export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => { + const isUpdate = Boolean(appConnection); + const [isRedirecting, setIsRedirecting] = useState(false); + + const { + option: { oauthClientId }, + isLoading + } = useGetAppConnectionOption(AppConnection.AzureClientSecrets); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection + ? { + ...appConnection, + tenantId: appConnection.credentials.tenantId + } + : { + app: AppConnection.AzureClientSecrets, + method: AzureClientSecretsConnectionMethod.OAuth + } + }); + + const { + handleSubmit, + control, + watch, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + + const onSubmit = (formData: FormData) => { + setIsRedirecting(true); + const state = crypto.randomBytes(16).toString("hex"); + localStorage.setItem("latestCSRFToken", state); + localStorage.setItem( + "azureClientSecretsConnectionFormData", + JSON.stringify({ ...formData, connectionId: appConnection?.id }) + ); + + switch (formData.method) { + case AzureClientSecretsConnectionMethod.OAuth: + window.location.assign( + `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets` + ); + break; + default: + throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`); + } + }; + + let isMissingConfig: boolean; + + switch (selectedMethod) { + case AzureClientSecretsConnectionMethod.OAuth: + isMissingConfig = !oauthClientId; + break; + default: + throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`); + } + + const methodDetails = getAppConnectionMethodDetails(selectedMethod); + + return ( + +
+ {!isUpdate && } + + ( + + + + )} + /> + + ( + + + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx index 8832d611e..ce7a1b489 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx @@ -7,9 +7,11 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { AzureAppConfigurationConnectionMethod, + AzureClientSecretsConnectionMethod, AzureKeyVaultConnectionMethod, GitHubConnectionMethod, TAzureAppConfigurationConnection, + TAzureClientSecretsConnection, TAzureKeyVaultConnection, TGitHubConnection, useCreateAppConnection, @@ -32,18 +34,26 @@ type AzureAppConfigurationFormData = BaseFormData & Pick & Pick; +type AzureClientSecretsFormData = BaseFormData & + Pick & + Pick; + type FormDataMap = { [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub }; [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault }; [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & { app: AppConnection.AzureAppConfiguration; }; + [AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & { + app: AppConnection.AzureClientSecrets; + }; }; const formDataStorageFieldMap: Partial> = { [AppConnection.GitHub]: "githubConnectionFormData", [AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData", - [AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData" + [AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData", + [AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData" }; export const OAuthCallbackPage = () => { @@ -194,6 +204,54 @@ export const OAuthCallbackPage = () => { }; }, []); + const handleAzureClientSecrets = useCallback(async () => { + const formData = getFormData(AppConnection.AzureClientSecrets); + if (formData === null) return null; + + clearState(AppConnection.AzureClientSecrets); + + const { connectionId, name, description, returnUrl } = formData; + + try { + if (connectionId) { + await updateAppConnection.mutateAsync({ + app: AppConnection.AzureClientSecrets, + connectionId, + credentials: { + code: code as string, + tenantId: formData.tenantId + } + }); + } else { + await createAppConnection.mutateAsync({ + app: AppConnection.AzureClientSecrets, + name, + description, + method: AzureClientSecretsConnectionMethod.OAuth, + credentials: { + code: code as string, + tenantId: formData.tenantId + } + }); + } + } catch (err: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} Azure Client Secrets Connection`, + text: err?.message, + type: "error" + }); + navigate({ + to: returnUrl ?? "/organization/app-connections" + }); + } + + return { + connectionId, + returnUrl, + appConnectionName: formData.app + }; + }, []); + const handleGithub = useCallback(async () => { const formData = getFormData(AppConnection.GitHub); if (formData === null) return null; @@ -280,6 +338,8 @@ export const OAuthCallbackPage = () => { data = await handleAzureKeyVault(); } else if (appConnection === AppConnection.AzureAppConfiguration) { data = await handleAzureAppConfiguration(); + } else if (appConnection === AppConnection.AzureClientSecrets) { + data = await handleAzureClientSecrets(); } if (data) {