diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts index a1cc6ff5c..e23d52004 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-endpoints.ts @@ -8,7 +8,6 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types"; -import { AzureResources } from "@app/services/app-connection/azure"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerAppConnectionEndpoints = ({ @@ -78,8 +77,7 @@ export const registerAppConnectionEndpoints = { diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts new file mode 100644 index 000000000..3f3ca7a1a --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-app-configuration-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAzureAppConfigurationConnectionSchema, + SanitizedAzureAppConfigurationConnectionSchema, + UpdateAzureAppConfigurationConnectionSchema +} from "@app/services/app-connection/azure-app-configuration"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAzureAppConfigurationConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AzureAppConfiguration, + server, + sanitizedResponseSchema: SanitizedAzureAppConfigurationConnectionSchema, + createSchema: CreateAzureAppConfigurationConnectionSchema, + updateSchema: UpdateAzureAppConfigurationConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-connection-router.ts deleted file mode 100644 index 20ef199a4..000000000 --- a/backend/src/server/routes/v1/app-connection-routers/azure-connection-router.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AppConnection } from "@app/services/app-connection/app-connection-enums"; -import { - CreateAzureConnectionSchema, - SanitizedAzureConnectionSchema, - UpdateAzureConnectionSchema -} from "@app/services/app-connection/azure"; - -import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; - -export const registerAzureConnectionRouter = async (server: FastifyZodProvider) => { - registerAppConnectionEndpoints({ - app: AppConnection.Azure, - server, - sanitizedResponseSchema: SanitizedAzureConnectionSchema, - createSchema: CreateAzureConnectionSchema, - updateSchema: UpdateAzureConnectionSchema - }); -}; diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts new file mode 100644 index 000000000..7097ed98b --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-key-vault-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAzureKeyVaultConnectionSchema, + SanitizedAzureKeyVaultConnectionSchema, + UpdateAzureKeyVaultConnectionSchema +} from "@app/services/app-connection/azure-key-vault"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAzureKeyVaultConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AzureKeyVault, + server, + sanitizedResponseSchema: SanitizedAzureKeyVaultConnectionSchema, + createSchema: CreateAzureKeyVaultConnectionSchema, + updateSchema: UpdateAzureKeyVaultConnectionSchema + }); +}; 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 46cbca36c..91eadc942 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,7 +1,8 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { registerAwsConnectionRouter } from "./aws-connection-router"; -import { registerAzureConnectionRouter } from "./azure-connection-router"; +import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; +import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -12,5 +13,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { return [ getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem(), - getAzureConnectionListItem() + getAzureKeyVaultConnectionListItem(), + getAzureAppConfigurationConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -84,8 +94,10 @@ export const validateAppConnectionCredentials = async ( return validateGitHubConnectionCredentials(appConnection); case AppConnection.GCP: return validateGcpConnectionCredentials(appConnection); - case AppConnection.Azure: - return validateAzureConnectionCredentials(appConnection); + case AppConnection.AzureKeyVault: + return validateAzureKeyVaultConnectionCredentials(appConnection); + case AppConnection.AzureAppConfiguration: + return validateAzureAppConfigurationConnectionCredentials(appConnection); default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection ${app}`); @@ -96,7 +108,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => switch (method) { case GitHubConnectionMethod.App: return "GitHub App"; - case AzureConnectionMethod.OAuth: + case AzureKeyVaultConnectionMethod.OAuth: + case AzureAppConfigurationConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth: return "OAuth"; case AwsConnectionMethod.AccessKey: diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index de1a78244..78fde3127 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -4,5 +4,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AWS]: "AWS", [AppConnection.GitHub]: "GitHub", [AppConnection.GCP]: "GCP", - [AppConnection.Azure]: "Azure" + [AppConnection.AzureKeyVault]: "Azure Key Vault", + [AppConnection.AzureAppConfiguration]: "Azure App Configuration" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index fbcae3f42..5504bdf18 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -27,9 +27,8 @@ import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-con import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "../kms/kms-types"; import { TAppConnectionDALFactory } from "./app-connection-dal"; -import { AzureResources } from "./azure"; +import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { gcpConnectionService } from "./gcp/gcp-connection-service"; @@ -45,7 +44,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { - const decryptedPlainTextBlob = decryptor({ - cipherTextBlob: connection.encryptedCredentials - }); - - const credentials = JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; - - return { - ...connection, - ...(app === AppConnection.Azure && { - azureResource: (credentials as { resource: AzureResources }).resource - }) - }; - }); - - return decryptedConnections as Omit[]; + return availableConnections as Omit[]; }; return { diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index d6e168467..95ac8145c 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -12,20 +12,33 @@ import { } from "@app/services/app-connection/github"; import { - TAzureConnection, - TAzureConnectionConfig, - TAzureConnectionInput, - TValidateAzureConnectionCredentials -} from "./azure"; + TAzureAppConfigurationConnection, + TAzureAppConfigurationConnectionConfig, + TAzureAppConfigurationConnectionInput, + TValidateAzureAppConfigurationConnectionCredentials +} from "./azure-app-configuration"; +import { + TAzureKeyVaultConnection, + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionInput, + TValidateAzureKeyVaultConnectionCredentials +} from "./azure-key-vault"; import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp"; -export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection | TAzureConnection); +export type TAppConnection = { id: string } & ( + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection +); export type TAppConnectionInput = { id: string } & ( | TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput - | TAzureConnectionInput + | TAzureKeyVaultConnectionInput + | TAzureAppConfigurationConnectionInput ); export type TCreateAppConnectionDTO = Pick< @@ -41,10 +54,12 @@ export type TAppConnectionConfig = | TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig - | TAzureConnectionConfig; + | TAzureKeyVaultConnectionConfig + | TAzureAppConfigurationConnectionConfig; export type TValidateAppConnectionCredentials = | TValidateAwsConnectionCredentials | TValidateGitHubConnectionCredentials | TValidateGcpConnectionCredentials - | TValidateAzureConnectionCredentials; + | TValidateAzureKeyVaultConnectionCredentials + | TValidateAzureAppConfigurationConnectionCredentials; diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts new file mode 100644 index 000000000..450cb9255 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-enums.ts @@ -0,0 +1,3 @@ +export enum AzureAppConfigurationConnectionMethod { + OAuth = "oauth" +} diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts new file mode 100644 index 000000000..9ccfc72b6 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts @@ -0,0 +1,98 @@ +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 { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums"; +import { + ExchangeCodeAzureResponse, + TAzureAppConfigurationConnectionConfig +} from "./azure-app-configuration-connection-types"; + +export const getAzureAppConfigurationConnectionListItem = () => { + const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + + return { + name: "Azure App Configuration" as const, + app: AppConnection.AzureAppConfiguration as const, + methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth], + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + }; +}; + +export const validateAzureAppConfigurationConnectionCredentials = async ( + config: TAzureAppConfigurationConnectionConfig +) => { + 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 AzureAppConfigurationConnectionMethod.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 AzureAppConfigurationConnectionMethod}` + }); + } +}; diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts new file mode 100644 index 000000000..e71451164 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-schemas.ts @@ -0,0 +1,77 @@ +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 { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums"; + +export const AzureAppConfigurationConnectionOAuthInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required"), + tenantId: z.string().trim().optional() +}); + +export const AzureAppConfigurationConnectionOAuthOutputCredentialsSchema = z.object({ + tenantId: z.string().optional(), + accessToken: z.string(), + refreshToken: z.string(), + expiresAt: z.number() +}); + +export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(AzureAppConfigurationConnectionMethod.OAuth) + .describe(AppConnections.CREATE(AppConnection.AzureAppConfiguration).method), + credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials + ) + }) +]); + +export const CreateAzureAppConfigurationConnectionSchema = ValidateAzureAppConfigurationConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration) +); + +export const UpdateAzureAppConfigurationConnectionSchema = z + .object({ + credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration)); + +const BaseAzureAppConfigurationConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.AzureAppConfiguration) +}); + +export const AzureAppConfigurationConnectionSchema = z.intersection( + BaseAzureAppConfigurationConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AzureAppConfigurationConnectionMethod.OAuth), + credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUnion("method", [ + BaseAzureAppConfigurationConnectionSchema.extend({ + method: z.literal(AzureAppConfigurationConnectionMethod.OAuth), + credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({ + resource: true, + tenantId: true + }) + }) +]); + +export const AzureAppConfigurationConnectionListItemSchema = z.object({ + name: z.literal("Azure App Configuration"), + app: z.literal(AppConnection.AzureAppConfiguration), + methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(), + oauthClientId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts new file mode 100644 index 000000000..db59a1558 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-types.ts @@ -0,0 +1,41 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + AzureAppConfigurationConnectionOAuthOutputCredentialsSchema, + AzureAppConfigurationConnectionSchema, + CreateAzureAppConfigurationConnectionSchema, + ValidateAzureAppConfigurationConnectionCredentialsSchema +} from "./azure-app-configuration-connection-schemas"; + +export type TAzureAppConfigurationConnection = z.infer; + +export type TAzureAppConfigurationConnectionInput = z.infer & { + app: AppConnection.AzureAppConfiguration; +}; + +export type TValidateAzureAppConfigurationConnectionCredentials = + typeof ValidateAzureAppConfigurationConnectionCredentialsSchema; + +export type TAzureAppConfigurationConnectionConfig = DiscriminativePick< + TAzureAppConfigurationConnectionInput, + "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 TAzureAppConfigurationConnectionCredentials = z.infer< + typeof AzureAppConfigurationConnectionOAuthOutputCredentialsSchema +>; diff --git a/backend/src/services/app-connection/azure-app-configuration/index.ts b/backend/src/services/app-connection/azure-app-configuration/index.ts new file mode 100644 index 000000000..5fbe876f5 --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-app-configuration-connection-enums"; +export * from "./azure-app-configuration-connection-fns"; +export * from "./azure-app-configuration-connection-schemas"; +export * from "./azure-app-configuration-connection-types"; diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts new file mode 100644 index 000000000..895e88298 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-enums.ts @@ -0,0 +1,3 @@ +export enum AzureKeyVaultConnectionMethod { + OAuth = "oauth" +} diff --git a/backend/src/services/app-connection/azure/azure-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts similarity index 78% rename from backend/src/services/app-connection/azure/azure-connection-fns.ts rename to backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts index 22e984190..12b1b3f3b 100644 --- a/backend/src/services/app-connection/azure/azure-connection-fns.ts +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -13,17 +13,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TAppConnectionDALFactory } from "../app-connection-dal"; import { AppConnection } from "../app-connection-enums"; -import { AzureConnectionMethod, AzureResources } from "./azure-connection-enums"; +import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; import { ExchangeCodeAzureResponse, - TAzureConnectionConfig, - TAzureConnectionCredentials -} from "./azure-connection-types"; - -const resourceScopes: Record = { - [AzureResources.AppConfiguration]: "https://azconfig.io/.default", - [AzureResources.KeyVault]: "https://vault.azure.net/.default" -}; + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionCredentials +} from "./azure-key-vault-connection-types"; export const getAzureConnectionAccessToken = async ( connectionId: string, @@ -31,6 +26,11 @@ export const getAzureConnectionAccessToken = async ( kmsService: Pick ) => { const appCfg = getConfig(); + if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + throw new BadRequestError({ + message: `Azure environment variables have not been configured` + }); + } const appConnection = await appConnectionDAL.findById(connectionId); @@ -38,23 +38,23 @@ export const getAzureConnectionAccessToken = async ( throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); } - if (appConnection.app !== AppConnection.Azure) { - throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure connection` }); + if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) { + throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` }); } const credentials = (await decryptAppConnectionCredentials({ orgId: appConnection.orgId, kmsService, encryptedCredentials: appConnection.encryptedCredentials - })) as TAzureConnectionCredentials; + })) as TAzureKeyVaultConnectionCredentials; const { data } = await request.post( IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"), new URLSearchParams({ grant_type: "refresh_token", scope: `openid offline_access`, - client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID!, - client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET!, + client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, + client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, refresh_token: credentials.refreshToken }) ); @@ -87,18 +87,18 @@ export const getAzureConnectionAccessToken = async ( }; }; -export const getAzureConnectionListItem = () => { +export const getAzureKeyVaultConnectionListItem = () => { const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); return { - name: "Azure" as const, - app: AppConnection.Azure as const, - methods: Object.values(AzureConnectionMethod) as [AzureConnectionMethod.OAuth], + name: "Azure Key Vault" as const, + app: AppConnection.AzureKeyVault as const, + methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth], oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID }; }; -export const validateAzureConnectionCredentials = async (config: TAzureConnectionConfig) => { +export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => { const { credentials: inputCredentials, method } = config; const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); @@ -118,7 +118,7 @@ export const validateAzureConnectionCredentials = async (config: TAzureConnectio new URLSearchParams({ grant_type: "authorization_code", code: inputCredentials.code, - scope: `openid offline_access ${resourceScopes[inputCredentials.resource]}`, + scope: `openid offline_access https://vault.azure.net/.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` @@ -155,16 +155,16 @@ export const validateAzureConnectionCredentials = async (config: TAzureConnectio } switch (method) { - case AzureConnectionMethod.OAuth: + case AzureKeyVaultConnectionMethod.OAuth: return { + tenantId: inputCredentials.tenantId, accessToken: tokenResp.data.access_token, refreshToken: tokenResp.data.refresh_token, - expiresAt: Date.now() + tokenResp.data.expires_in * 1000, - resource: inputCredentials.resource + expiresAt: Date.now() + tokenResp.data.expires_in * 1000 }; default: throw new InternalServerError({ - message: `Unhandled Azure connection method: ${method as AzureConnectionMethod}` + message: `Unhandled Azure connection method: ${method as AzureKeyVaultConnectionMethod}` }); } }; diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts new file mode 100644 index 000000000..33777445c --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-schemas.ts @@ -0,0 +1,77 @@ +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 { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; + +export const AzureKeyVaultConnectionOAuthInputCredentialsSchema = z.object({ + code: z.string().trim().min(1, "OAuth code required"), + tenantId: z.string().trim().optional() +}); + +export const AzureKeyVaultConnectionOAuthOutputCredentialsSchema = z.object({ + tenantId: z.string().optional(), + accessToken: z.string(), + refreshToken: z.string(), + expiresAt: z.number() +}); + +export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(AzureKeyVaultConnectionMethod.OAuth) + .describe(AppConnections.CREATE(AppConnection.AzureKeyVault).method), + credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureKeyVault).credentials + ) + }) +]); + +export const CreateAzureKeyVaultConnectionSchema = ValidateAzureKeyVaultConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AzureKeyVault) +); + +export const UpdateAzureKeyVaultConnectionSchema = z + .object({ + credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureKeyVault)); + +const BaseAzureKeyVaultConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.AzureKeyVault) +}); + +export const AzureKeyVaultConnectionSchema = z.intersection( + BaseAzureKeyVaultConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(AzureKeyVaultConnectionMethod.OAuth), + credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema + }) + ]) +); + +export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("method", [ + BaseAzureKeyVaultConnectionSchema.extend({ + method: z.literal(AzureKeyVaultConnectionMethod.OAuth), + credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({ + resource: true, + tenantId: true + }) + }) +]); + +export const AzureKeyVaultConnectionListItemSchema = z.object({ + name: z.literal("Azure Key Vault"), + app: z.literal(AppConnection.AzureKeyVault), + methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(), + oauthClientId: z.string().optional() +}); diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts new file mode 100644 index 000000000..d784ca100 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-types.ts @@ -0,0 +1,38 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + AzureKeyVaultConnectionOAuthOutputCredentialsSchema, + AzureKeyVaultConnectionSchema, + CreateAzureKeyVaultConnectionSchema, + ValidateAzureKeyVaultConnectionCredentialsSchema +} from "./azure-key-vault-connection-schemas"; + +export type TAzureKeyVaultConnection = z.infer; + +export type TAzureKeyVaultConnectionInput = z.infer & { + app: AppConnection.AzureKeyVault; +}; + +export type TValidateAzureKeyVaultConnectionCredentials = typeof ValidateAzureKeyVaultConnectionCredentialsSchema; + +export type TAzureKeyVaultConnectionConfig = DiscriminativePick< + TAzureKeyVaultConnectionInput, + "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 TAzureKeyVaultConnectionCredentials = z.infer; diff --git a/backend/src/services/app-connection/azure-key-vault/index.ts b/backend/src/services/app-connection/azure-key-vault/index.ts new file mode 100644 index 000000000..b80b07c17 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-key-vault-connection-enums"; +export * from "./azure-key-vault-connection-fns"; +export * from "./azure-key-vault-connection-schemas"; +export * from "./azure-key-vault-connection-types"; diff --git a/backend/src/services/app-connection/azure/azure-connection-enums.ts b/backend/src/services/app-connection/azure/azure-connection-enums.ts deleted file mode 100644 index 8d3963db7..000000000 --- a/backend/src/services/app-connection/azure/azure-connection-enums.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum AzureConnectionMethod { - OAuth = "oauth" -} - -export enum AzureResources { - KeyVault = "key-vault", - AppConfiguration = "app-configuration" -} diff --git a/backend/src/services/app-connection/azure/azure-connection-schemas.ts b/backend/src/services/app-connection/azure/azure-connection-schemas.ts deleted file mode 100644 index 4c1e62d74..000000000 --- a/backend/src/services/app-connection/azure/azure-connection-schemas.ts +++ /dev/null @@ -1,74 +0,0 @@ -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 { AzureConnectionMethod, AzureResources } from "./azure-connection-enums"; - -export const AzureConnectionOAuthInputCredentialsSchema = z.object({ - code: z.string().trim().min(1, "OAuth code required"), - tenantId: z.string().trim().optional(), - resource: z.nativeEnum(AzureResources) -}); - -export const AzureConnectionOAuthOutputCredentialsSchema = z.object({ - tenantId: z.string().optional(), - accessToken: z.string(), - refreshToken: z.string(), - expiresAt: z.number(), // unix timestamp, - resource: z.nativeEnum(AzureResources) -}); - -export const ValidateAzureConnectionCredentialsSchema = z.discriminatedUnion("method", [ - z.object({ - method: z.literal(AzureConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.Azure).method), - credentials: AzureConnectionOAuthInputCredentialsSchema.describe( - AppConnections.CREATE(AppConnection.Azure).credentials - ) - }) -]); - -export const CreateAzureConnectionSchema = ValidateAzureConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.Azure) -); - -export const UpdateAzureConnectionSchema = z - .object({ - credentials: AzureConnectionOAuthInputCredentialsSchema.optional().describe( - AppConnections.UPDATE(AppConnection.Azure).credentials - ) - }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Azure)); - -const BaseAzureConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Azure) }); - -export const AzureConnectionSchema = z.intersection( - BaseAzureConnectionSchema, - z.discriminatedUnion("method", [ - z.object({ - method: z.literal(AzureConnectionMethod.OAuth), - credentials: AzureConnectionOAuthOutputCredentialsSchema - }) - ]) -); - -export const SanitizedAzureConnectionSchema = z.discriminatedUnion("method", [ - BaseAzureConnectionSchema.extend({ - method: z.literal(AzureConnectionMethod.OAuth), - credentials: AzureConnectionOAuthOutputCredentialsSchema.pick({ - resource: true - }) - }) -]); - -export const AzureConnectionListItemSchema = z.object({ - name: z.literal("Azure"), - app: z.literal(AppConnection.Azure), - methods: z.nativeEnum(AzureConnectionMethod).array(), - oauthClientId: z.string().optional() -}); diff --git a/backend/src/services/app-connection/azure/azure-connection-types.ts b/backend/src/services/app-connection/azure/azure-connection-types.ts deleted file mode 100644 index 167b327b6..000000000 --- a/backend/src/services/app-connection/azure/azure-connection-types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import z from "zod"; - -import { DiscriminativePick } from "@app/lib/types"; - -import { AppConnection } from "../app-connection-enums"; -import { - AzureConnectionOAuthOutputCredentialsSchema, - AzureConnectionSchema, - CreateAzureConnectionSchema, - ValidateAzureConnectionCredentialsSchema -} from "./azure-connection-schemas"; - -export type TAzureConnection = z.infer; - -export type TAzureConnectionInput = z.infer & { - app: AppConnection.Azure; -}; - -export type TValidateAzureConnectionCredentials = typeof ValidateAzureConnectionCredentialsSchema; - -export type TAzureConnectionConfig = DiscriminativePick & { - 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 TAzureConnectionCredentials = z.infer; diff --git a/backend/src/services/app-connection/azure/index.ts b/backend/src/services/app-connection/azure/index.ts deleted file mode 100644 index f3ae01dd0..000000000 --- a/backend/src/services/app-connection/azure/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./azure-connection-enums"; -export * from "./azure-connection-fns"; -export * from "./azure-connection-schemas"; -export * from "./azure-connection-types"; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts index da4e335ae..07876f088 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts @@ -5,6 +5,6 @@ import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types export const AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION: TSecretSyncListItem = { name: "Azure App Configuration", destination: SecretSync.AzureAppConfiguration, - connection: AppConnection.Azure, - canImportSecrets: false + connection: AppConnection.AzureAppConfiguration, + canImportSecrets: true }; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts index 848f11710..9cdf771d0 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -4,7 +4,7 @@ import https from "https"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; -import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts index a0b34f2dc..30a9a9ae3 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts @@ -40,7 +40,7 @@ export const UpdateAzureAppConfigurationSyncSchema = GenericUpdateSecretSyncFiel export const AzureAppConfigurationSyncListItemSchema = z.object({ name: z.literal("Azure App Configuration"), - connection: z.literal(AppConnection.Azure), + connection: z.literal(AppConnection.AzureAppConfiguration), destination: z.literal(SecretSync.AzureAppConfiguration), - canImportSecrets: z.literal(false) + canImportSecrets: z.literal(true) }); diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts index a2d001bd4..4cfbd5472 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { TAzureConnection } from "@app/services/app-connection/azure"; +import { TAzureAppConfigurationConnection } from "@app/services/app-connection/azure-app-configuration"; import { AzureAppConfigurationSyncListItemSchema, @@ -15,5 +15,5 @@ export type TAzureAppConfigurationSyncInput = z.infer; export type TAzureAppConfigurationSyncWithCredentials = TAzureAppConfigurationSync & { - connection: TAzureConnection; + connection: TAzureAppConfigurationConnection; }; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts index 0b9f863da..9e2f986ce 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts @@ -5,6 +5,6 @@ import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types export const AZURE_KEY_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = { name: "Azure Key Vault", destination: SecretSync.AzureKeyVault, - connection: AppConnection.Azure, - canImportSecrets: false + connection: AppConnection.AzureKeyVault, + canImportSecrets: true }; diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts index 1cb4f39f0..ea3e251ad 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -3,7 +3,7 @@ import { AxiosError } from "axios"; import { request } from "@app/lib/config/request"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; -import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure"; +import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; @@ -172,6 +172,10 @@ export const azureKeyVaultSecretSyncFactory = ({ } } } + + if (!isSecretSet) { + throw new Error(`Failed to set secret ${key}`); + } }; for await (const setSecret of setSecrets) { diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts index d991ca6a2..cad87b83d 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts @@ -10,7 +10,7 @@ import { import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; const AzureKeyVaultSyncDestinationConfigSchema = z.object({ - vaultBaseUrl: z.string().min(1, "Vault base URL required") + vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required") }); const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; @@ -39,7 +39,7 @@ export const UpdateAzureKeyVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema export const AzureKeyVaultSyncListItemSchema = z.object({ name: z.literal("Azure Key Vault"), - connection: z.literal(AppConnection.Azure), + connection: z.literal(AppConnection.AzureKeyVault), destination: z.literal(SecretSync.AzureKeyVault), - canImportSecrets: z.literal(false) + canImportSecrets: z.literal(true) }); diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts index 930792aff..d8083d640 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { TAzureConnection } from "@app/services/app-connection/azure"; +import { TAzureKeyVaultConnection } from "@app/services/app-connection/azure-key-vault"; import { AzureKeyVaultSyncListItemSchema, @@ -15,7 +15,7 @@ export type TAzureKeyVaultSyncInput = z.infer; export type TAzureKeyVaultSyncWithCredentials = TAzureKeyVaultSync & { - connection: TAzureConnection; + connection: TAzureKeyVaultConnection; }; export interface GetAzureKeyVaultSecret { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 22bfdbd6c..c39ceed54 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -93,16 +93,6 @@ export const SecretSyncFns = { ): Promise => { // const affixedSecretMap = addAffixes(secretSync, secretMap); - const azureKeyVaultSecretSync = azureKeyVaultSecretSyncFactory({ - appConnectionDAL, - kmsService - }); - - const azureAppConfigurationSecretSync = azureAppConfigurationSecretSyncFactory({ - appConnectionDAL, - kmsService - }); - switch (secretSync.destination) { case SecretSync.AWSParameterStore: return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); @@ -113,9 +103,15 @@ export const SecretSyncFns = { case SecretSync.GCPSecretManager: return GcpSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.AzureKeyVault: - return azureKeyVaultSecretSync.syncSecrets(secretSync, secretMap); + return azureKeyVaultSecretSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); case SecretSync.AzureAppConfiguration: - return azureAppConfigurationSecretSync.syncSecrets(secretSync, secretMap); + return azureAppConfigurationSecretSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -126,16 +122,6 @@ export const SecretSyncFns = { secretSync: TSecretSyncWithCredentials, { kmsService, appConnectionDAL }: TSyncSecretDeps ): Promise => { - const azureKeyVaultSecretSync = azureKeyVaultSecretSyncFactory({ - appConnectionDAL, - kmsService - }); - - const azureAppConfigurationSecretSync = azureAppConfigurationSecretSyncFactory({ - appConnectionDAL, - kmsService - }); - let secretMap: TSecretMap; switch (secretSync.destination) { case SecretSync.AWSParameterStore: @@ -151,10 +137,16 @@ export const SecretSyncFns = { secretMap = await GcpSyncFns.getSecrets(secretSync); break; case SecretSync.AzureKeyVault: - secretMap = await azureKeyVaultSecretSync.getSecrets(secretSync); + secretMap = await azureKeyVaultSecretSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); break; case SecretSync.AzureAppConfiguration: - secretMap = await azureAppConfigurationSecretSync.getSecrets(secretSync); + secretMap = await azureAppConfigurationSecretSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); break; default: throw new Error( @@ -172,16 +164,6 @@ export const SecretSyncFns = { ): Promise => { // const affixedSecretMap = addAffixes(secretSync, secretMap); - const azureKeyVaultSecretSync = azureKeyVaultSecretSyncFactory({ - appConnectionDAL, - kmsService - }); - - const azureAppConfigurationSecretSync = azureAppConfigurationSecretSyncFactory({ - appConnectionDAL, - kmsService - }); - switch (secretSync.destination) { case SecretSync.AWSParameterStore: return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); @@ -192,9 +174,15 @@ export const SecretSyncFns = { case SecretSync.GCPSecretManager: return GcpSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.AzureKeyVault: - return azureKeyVaultSecretSync.removeSecrets(secretSync, secretMap); + return azureKeyVaultSecretSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); case SecretSync.AzureAppConfiguration: - return azureAppConfigurationSecretSync.removeSecrets(secretSync, secretMap); + return azureAppConfigurationSecretSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 507f11baa..33a87fbef 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -15,6 +15,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, [SecretSync.GCPSecretManager]: AppConnection.GCP, - [SecretSync.AzureKeyVault]: AppConnection.Azure, - [SecretSync.AzureAppConfiguration]: AppConnection.Azure + [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx index 6301fd21a..5c9df0890 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx @@ -18,9 +18,6 @@ import { TSecretSyncForm } from "./schemas"; type Props = { onChange?: VoidFunction; - filterConnections?: ( - connections?: TAvailableAppConnection[] - ) => TAvailableAppConnection[] | undefined; }; export const SecretSyncConnectionField = ({ onChange: callback, filterConnections }: Props) => { @@ -30,7 +27,7 @@ export const SecretSyncConnectionField = ({ onChange: callback, filterConnection const destination = watch("destination"); const app = SECRET_SYNC_CONNECTION_MAP[destination]; - const { data: allConnections, isLoading } = useListAvailableAppConnections(app); + const { data: availableConnections, isLoading } = useListAvailableAppConnections(app); const connectionName = APP_CONNECTION_MAP[app].name; @@ -39,10 +36,6 @@ export const SecretSyncConnectionField = ({ onChange: callback, filterConnection OrgPermissionSubjects.AppConnections ); - const availableConnections = useMemo(() => { - return filterConnections ? filterConnections(allConnections) : allConnections; - }, [allConnections]); - const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name; return ( diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx index 46c481463..b4d1f8c42 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx @@ -2,8 +2,6 @@ import { Controller, useFormContext } from "react-hook-form"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; import { FormControl, Input } from "@app/components/v2"; -import { AzureResources } from "@app/hooks/api/appConnections"; -import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; @@ -19,15 +17,6 @@ export const AzureAppConfigurationSyncFields = () => { onChange={() => { setValue("destinationConfig.configurationUrl", ""); }} - filterConnections={(connections) => { - if (!connections) return connections; - - return connections.filter( - (connection) => - connection.app === AppConnection.Azure && - connection.azureResource === AzureResources.AppConfiguration - ); - }} /> { onChange={() => { setValue("destinationConfig.vaultBaseUrl", ""); }} - filterConnections={(connections) => { - if (!connections) return connections; - - return connections.filter( - (connection) => - connection.app === AppConnection.Azure && - connection.azureResource === AzureResources.KeyVault - ); - }} /> { switch (method) { case GitHubConnectionMethod.App: return { name: "GitHub App", icon: faGithub }; + case AzureKeyVaultConnectionMethod.OAuth: + case AzureAppConfigurationConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth: return { name: "OAuth", icon: faPassport }; case AwsConnectionMethod.AccessKey: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index c064b091d..104a45ef4 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -22,8 +22,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, [SecretSync.GCPSecretManager]: AppConnection.GCP, - [SecretSync.AzureKeyVault]: AppConnection.Azure, - [SecretSync.AzureAppConfiguration]: AppConnection.Azure + [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 3f941fc94..46e853cb4 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -2,5 +2,6 @@ export enum AppConnection { AWS = "aws", GitHub = "github", GCP = "gcp", - Azure = "azure" + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index a7a17639c..b4aeb29d5 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -20,8 +20,13 @@ export type TGcpConnectionOption = TAppConnectionOptionBase & { app: AppConnection.GCP; }; -export type TAzureConnectionOption = TAppConnectionOptionBase & { - app: AppConnection.Azure; +export type TAzureKeyVaultConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AzureKeyVault; + oauthClientId?: string; +}; + +export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AzureKeyVault; oauthClientId?: string; }; @@ -31,5 +36,6 @@ export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; [AppConnection.GitHub]: TGitHubConnectionOption; [AppConnection.GCP]: TGcpConnectionOption; - [AppConnection.Azure]: TAzureConnectionOption; + [AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption; + [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/azure-app-configuration-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-app-configuration-connection.ts new file mode 100644 index 000000000..b56003cad --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/azure-app-configuration-connection.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum AzureAppConfigurationConnectionMethod { + OAuth = "oauth" +} + +export type TAzureAppConfigurationConnection = TRootAppConnection & { + app: AppConnection.AzureAppConfiguration; +} & { + method: AzureAppConfigurationConnectionMethod.OAuth; + credentials: { + code: string; + tenantId?: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/azure-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-connection.ts deleted file mode 100644 index d8d194b6a..000000000 --- a/frontend/src/hooks/api/appConnections/types/azure-connection.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AppConnection } from "@app/hooks/api/appConnections/enums"; -import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; - -export enum AzureConnectionMethod { - OAuth = "oauth" -} - -export enum AzureResources { - KeyVault = "key-vault", - AppConfiguration = "app-configuration" -} - -export const azureResourcesMap: Record = { - [AzureResources.AppConfiguration]: "App Configuration", - [AzureResources.KeyVault]: "Key Vault" -}; - -export type TAzureConnection = TRootAppConnection & { app: AppConnection.Azure } & { - method: AzureConnectionMethod.OAuth; - resource: AzureResources; - credentials: { - code: string; - tenantId?: string; - resource: AzureResources; - }; -}; diff --git a/frontend/src/hooks/api/appConnections/types/azure-key-vault-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-key-vault-connection.ts new file mode 100644 index 000000000..68888531a --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/azure-key-vault-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum AzureKeyVaultConnectionMethod { + OAuth = "oauth" +} + +export type TAzureKeyVaultConnection = TRootAppConnection & { app: AppConnection.AzureKeyVault } & { + method: AzureKeyVaultConnectionMethod.OAuth; + credentials: { + code: string; + tenantId?: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 68afe72b4..b3005a2df 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -3,22 +3,24 @@ import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-op import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection"; import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection"; -import { AzureResources, TAzureConnection } from "./azure-connection"; +import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection"; +import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; import { TGcpConnection } from "./gcp-connection"; export * from "./aws-connection"; -export * from "./azure-connection"; +export * from "./azure-app-configuration-connection"; +export * from "./azure-key-vault-connection"; export * from "./gcp-connection"; export * from "./github-connection"; -export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection | TAzureConnection; +export type TAppConnection = + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection; -export type TAvailableAppConnection = - | (Pick & { app: Exclude }) - | (Pick & { - app: AppConnection.Azure; - azureResource?: AzureResources; - }); +export type TAvailableAppConnection = Pick; export type TListAppConnections = { appConnections: T[] }; export type TGetAppConnection = { appConnection: T }; @@ -47,5 +49,6 @@ export type TAppConnectionMap = { [AppConnection.AWS]: TAwsConnection; [AppConnection.GitHub]: TGitHubConnection; [AppConnection.GCP]: TGcpConnection; - [AppConnection.Azure]: TAzureConnection; + [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; + [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts b/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts index 2e894ba25..535d2cb7c 100644 --- a/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts @@ -9,7 +9,7 @@ export type TAzureAppConfigurationSync = TRootSecretSync & { label?: string; }; connection: { - app: AppConnection.Azure; + app: AppConnection.AzureAppConfiguration; name: string; id: string; }; diff --git a/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts b/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts index 4cd907a03..7cdf57cb3 100644 --- a/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts @@ -8,7 +8,7 @@ export type TAzureKeyVaultSync = TRootSecretSync & { vaultBaseUrl: string; }; connection: { - app: AppConnection.Azure; + app: AppConnection.AzureKeyVault; name: string; id: string; }; diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx index 8b861d192..5210e31e5 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx @@ -5,9 +5,10 @@ import { createNotification } from "@app/components/notifications"; import { ContentLoader } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { - AzureConnectionMethod, - AzureResources, + AzureAppConfigurationConnectionMethod, + AzureKeyVaultConnectionMethod, GitHubConnectionMethod, + TAzureKeyVaultConnection, TGitHubConnection, useCreateAppConnection, useUpdateAppConnection @@ -22,18 +23,20 @@ type GithubFormData = Pick type AzureFormData = Pick & { returnUrl?: string; connectionId?: string; - tenantId?: string; - resource: AzureResources; -}; +} & Pick; type FormDataMap = { [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub }; - [AppConnection.Azure]: AzureFormData & { app: AppConnection.Azure }; + [AppConnection.AzureKeyVault]: AzureFormData & { app: AppConnection.AzureKeyVault }; + [AppConnection.AzureAppConfiguration]: AzureFormData & { + app: AppConnection.AzureAppConfiguration; + }; }; const formDataStorageFieldMap: Partial> = { [AppConnection.GitHub]: "githubConnectionFormData", - [AppConnection.Azure]: "azureConnectionFormData" + [AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData", + [AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData" }; export const OAuthCallbackPage = () => { @@ -44,7 +47,7 @@ export const OAuthCallbackPage = () => { from: ROUTE_PATHS.Organization.AppConnections.OauthCallbackPage.id }); - const appConnection = useParams({ + const rawAppConnection = useParams({ strict: false, select: (el) => el?.appConnection as AppConnection }); @@ -52,7 +55,10 @@ export const OAuthCallbackPage = () => { const updateAppConnection = useUpdateAppConnection(); const createAppConnection = useCreateAppConnection(); - const { code, state, installation_id: installationId } = search; + const { code, state: rawState, installation_id: installationId } = search; + + const state = rawState.includes("<:>") ? rawState.split("<:>")[0] : rawState; + const appConnection = rawState.includes("<:>") ? rawState.split("<:>")[1] : rawAppConnection; const clearState = (app: AppConnection) => { if (state !== localStorage.getItem("latestCSRFToken")) { @@ -85,18 +91,18 @@ export const OAuthCallbackPage = () => { } }; - const handleAzure = useCallback(async () => { - const formData = getFormData(AppConnection.Azure); + const handleAzureKeyVault = useCallback(async () => { + const formData = getFormData(AppConnection.AzureKeyVault); if (formData === null) return null; - clearState(AppConnection.Azure); + clearState(AppConnection.AzureKeyVault); const { connectionId, name, description, returnUrl } = formData; try { if (connectionId) { await updateAppConnection.mutateAsync({ - app: AppConnection.Azure, + app: AppConnection.AzureKeyVault, connectionId, credentials: { code: code as string @@ -104,12 +110,11 @@ export const OAuthCallbackPage = () => { }); } else { await createAppConnection.mutateAsync({ - app: AppConnection.Azure, + app: AppConnection.AzureKeyVault, name, description, - method: AzureConnectionMethod.OAuth, + method: AzureKeyVaultConnectionMethod.OAuth, credentials: { - resource: formData.resource, tenantId: formData.tenantId, code: code as string } @@ -133,6 +138,52 @@ export const OAuthCallbackPage = () => { }; }, []); + const handleAzureAppConfiguration = useCallback(async () => { + const formData = getFormData(AppConnection.AzureAppConfiguration); + if (formData === null) return null; + + clearState(AppConnection.AzureAppConfiguration); + + const { connectionId, name, description, returnUrl } = formData; + + try { + if (connectionId) { + await updateAppConnection.mutateAsync({ + app: AppConnection.AzureAppConfiguration, + connectionId, + credentials: { + code: code as string + } + }); + } else { + await createAppConnection.mutateAsync({ + app: AppConnection.AzureAppConfiguration, + name, + description, + method: AzureAppConfigurationConnectionMethod.OAuth, + credentials: { + code: code as string + } + }); + } + } catch (err: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} Azure Connection`, + text: err?.message, + type: "error" + }); + navigate({ + to: returnUrl ?? "/organization/settings?selectedTab=app-connections" + }); + } + + return { + connectionId, + returnUrl, + appConnectionName: formData.app + }; + }, []); + const handleGithub = useCallback(async () => { const formData = getFormData(AppConnection.GitHub); if (formData === null) return null; @@ -215,8 +266,10 @@ export const OAuthCallbackPage = () => { if (appConnection === AppConnection.GitHub) { data = await handleGithub(); - } else if (appConnection === AppConnection.Azure) { - data = await handleAzure(); + } else if (appConnection === AppConnection.AzureKeyVault) { + data = await handleAzureKeyVault(); + } else if (appConnection === AppConnection.AzureAppConfiguration) { + data = await handleAzureAppConfiguration(); } if (data) { @@ -224,6 +277,11 @@ export const OAuthCallbackPage = () => { text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName || ""} Connection`, type: "success" }); + } else { + createNotification({ + text: "Failed to add connection", + type: "error" + }); } await navigate({ diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx index 2c1e8b8b0..29fef31ec 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx @@ -10,7 +10,8 @@ import { DiscriminativePick } from "@app/types"; import { AppConnectionHeader } from "../AppConnectionHeader"; import { AwsConnectionForm } from "./AwsConnectionForm"; -import { AzureConnectionForm } from "./AzureConnectionForm"; +import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm"; +import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; @@ -54,8 +55,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.GCP: return ; - case AppConnection.Azure: - return ; + case AppConnection.AzureKeyVault: + return ; + case AppConnection.AzureAppConfiguration: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -95,8 +98,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.GCP: return ; - case AppConnection.Azure: - return ; + case AppConnection.AzureKeyVault: + return ; + case AppConnection.AzureAppConfiguration: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx new file mode 100644 index 000000000..84bb3910c --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureAppConfigurationConnectionForm.tsx @@ -0,0 +1,177 @@ +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 { + AzureAppConfigurationConnectionMethod, + TAzureAppConfigurationConnection, + useGetAppConnectionOption +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TAzureAppConfigurationConnection; +}; + +const formSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.AzureAppConfiguration), + method: z.nativeEnum(AzureAppConfigurationConnectionMethod), + tenantId: z.string().trim().optional() +}); + +type FormData = z.infer; + +export const AzureAppConfigurationConnectionForm = ({ appConnection }: Props) => { + const isUpdate = Boolean(appConnection); + const [isRedirecting, setIsRedirecting] = useState(false); + + const { + option: { oauthClientId }, + isLoading + } = useGetAppConnectionOption(AppConnection.AzureAppConfiguration); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection + ? { + ...appConnection + } + : { + app: AppConnection.AzureAppConfiguration, + method: AzureAppConfigurationConnectionMethod.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( + "azureAppConfigurationConnectionFormData", + JSON.stringify({ ...formData, connectionId: appConnection?.id }) + ); + + switch (formData.method) { + case AzureAppConfigurationConnectionMethod.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-app-configuration` + ); + break; + default: + throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`); + } + }; + + let isMissingConfig: boolean; + + switch (selectedMethod) { + case AzureAppConfigurationConnectionMethod.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/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx similarity index 69% rename from frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureConnectionForm.tsx rename to frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx index 4d1664829..92f1eb821 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureConnectionForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx @@ -11,10 +11,9 @@ import { isInfisicalCloud } from "@app/helpers/platform"; import { useGetAppConnectionOption } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { - AzureConnectionMethod, - AzureResources, - TAzureConnection -} from "@app/hooks/api/appConnections/types/azure-connection"; + AzureKeyVaultConnectionMethod, + TAzureKeyVaultConnection +} from "@app/hooks/api/appConnections/types/azure-key-vault-connection"; import { genericAppConnectionFieldsSchema, @@ -22,43 +21,35 @@ import { } from "./GenericAppConnectionFields"; type Props = { - appConnection?: TAzureConnection; -}; - -const resourceScopes: Record = { - [AzureResources.AppConfiguration]: "https://azconfig.io/.default", - [AzureResources.KeyVault]: "https://vault.azure.net/.default" + appConnection?: TAzureKeyVaultConnection; }; const formSchema = genericAppConnectionFieldsSchema.extend({ - app: z.literal(AppConnection.Azure), - method: z.nativeEnum(AzureConnectionMethod), - tenantId: z.string().trim().optional(), - resource: z.nativeEnum(AzureResources) + app: z.literal(AppConnection.AzureKeyVault), + method: z.nativeEnum(AzureKeyVaultConnectionMethod), + tenantId: z.string().trim().optional() }); type FormData = z.infer; -export const AzureConnectionForm = ({ appConnection }: Props) => { +export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => { const isUpdate = Boolean(appConnection); const [isRedirecting, setIsRedirecting] = useState(false); const { option: { oauthClientId }, isLoading - } = useGetAppConnectionOption(AppConnection.Azure); + } = useGetAppConnectionOption(AppConnection.AzureKeyVault); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: appConnection ? { - ...appConnection, - resource: appConnection?.credentials.resource + ...appConnection } : { - app: AppConnection.Azure, - method: AzureConnectionMethod.OAuth, - resource: AzureResources.KeyVault + app: AppConnection.AzureKeyVault, + method: AzureKeyVaultConnectionMethod.OAuth } }); @@ -76,14 +67,14 @@ export const AzureConnectionForm = ({ appConnection }: Props) => { const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); localStorage.setItem( - "azureConnectionFormData", + "azureKeyVaultConnectionFormData", JSON.stringify({ ...formData, connectionId: appConnection?.id }) ); switch (formData.method) { - case AzureConnectionMethod.OAuth: + case AzureKeyVaultConnectionMethod.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=${resourceScopes[formData.resource]}%20openid%20offline_access&state=${state}` + `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://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault` ); break; default: @@ -94,7 +85,7 @@ export const AzureConnectionForm = ({ appConnection }: Props) => { let isMissingConfig: boolean; switch (selectedMethod) { - case AzureConnectionMethod.OAuth: + case AzureKeyVaultConnectionMethod.OAuth: isMissingConfig = !oauthClientId; break; default: @@ -113,7 +104,7 @@ export const AzureConnectionForm = ({ appConnection }: Props) => { control={control} render={({ field, fieldState: { error } }) => ( { )} /> - ( - - - - )} - /> - ( { position="popper" dropdownContainerClassName="max-w-none" > - {Object.values(AzureConnectionMethod).map((method) => { + {Object.values(AzureKeyVaultConnectionMethod).map((method) => { return ( {getAppConnectionMethodDetails(method).name} diff --git a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionRow.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionRow.tsx index a63f1b59d..a733bae8e 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionRow.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionRow.tsx @@ -27,8 +27,7 @@ import { OrgPermissionSubjects } from "@app/context"; import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; import { useToggle } from "@app/hooks"; -import { azureResourcesMap, TAppConnection } from "@app/hooks/api/appConnections"; -import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TAppConnection } from "@app/hooks/api/appConnections"; type Props = { appConnection: TAppConnection; @@ -43,7 +42,7 @@ export const AppConnectionRow = ({ onEditCredentials, onEditDetails }: Props) => { - const { id, name, method, app, description, credentials } = appConnection; + const { id, name, method, app, description } = appConnection; const [isIdCopied, setIsIdCopied] = useToggle(false); @@ -76,10 +75,7 @@ export const AppConnectionRow = ({ src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`} className="mr-0.5 h-5 w-5" /> - - {APP_CONNECTION_MAP[app].name} - {app === AppConnection.Azure && ` ${azureResourcesMap[credentials.resource]}`} - + {APP_CONNECTION_MAP[app].name} diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx index c2535e114..7db01f7fe 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx @@ -13,9 +13,7 @@ export const AzureAppConfigurationSyncDestinationSection = ({ secretSync }: Prop return ( <> {configurationUrl} - - {label && label.length > 0 ? label : Not set} - + {label} ); };