diff --git a/.env.example b/.env.example index 23a0b8be0..e647ee38c 100644 --- a/.env.example +++ b/.env.example @@ -92,20 +92,24 @@ ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT=true # App Connections -# aws assume-role +# aws assume-role connection INF_APP_CONNECTION_AWS_ACCESS_KEY_ID= INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY= -# github oauth +# github oauth connection INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID= INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET= -#github app +#github app connection INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID= INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET= INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY= INF_APP_CONNECTION_GITHUB_APP_SLUG= INF_APP_CONNECTION_GITHUB_APP_ID= -#gcp app +#gcp app connection INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= + +# azure app connection +INF_APP_CONNECTION_AZURE_CLIENT_ID= +INF_APP_CONNECTION_AZURE_CLIENT_SECRET= \ No newline at end of file diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5f6f243b9..32829a1d7 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1739,6 +1739,15 @@ export const SecretSyncs = { OWNER: "The name of the GitHub account owner of the repository.", REPO: "The name of the GitHub repository.", ENV: "The name of the GitHub environment." + }, + AZURE_KEY_VAULT: { + VAULT_BASE_URL: + "The base URL of the Azure Key Vault to sync secrets to. Example: https://example.vault.azure.net/" + }, + AZURE_APP_CONFIGURATION: { + CONFIGURATION_URL: + "The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/", + LABEL: "An optional label to assign to secrets created in Azure App Configuration." } } }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 627ccb053..7f0f31728 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -204,6 +204,10 @@ const envSchema = z // gcp app INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()), + // azure app + INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()), + /* CORS ----------------------------------------------------------------------------- */ CORS_ALLOWED_ORIGINS: zpStr( diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3e5947956..8cebbdebd 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -849,7 +849,8 @@ export const registerRoutes = async ( secretVersionTagDAL, secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, - resourceMetadataDAL + resourceMetadataDAL, + appConnectionDAL }); const secretQueueService = secretQueueFactory({ 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 41a87feb5..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 @@ -73,7 +73,13 @@ 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-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 4551a0fbb..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,6 +1,8 @@ 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 { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -10,5 +12,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.AzureAppConfiguration, + server, + responseSchema: AzureAppConfigurationSyncSchema, + createSchema: CreateAzureAppConfigurationSyncSchema, + updateSchema: UpdateAzureAppConfigurationSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts new file mode 100644 index 000000000..a33c513c8 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/azure-key-vault-sync-router.ts @@ -0,0 +1,17 @@ +import { + AzureKeyVaultSyncSchema, + CreateAzureKeyVaultSyncSchema, + UpdateAzureKeyVaultSyncSchema +} from "@app/services/secret-sync/azure-key-vault"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerAzureKeyVaultSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.AzureKeyVault, + server, + responseSchema: AzureKeyVaultSyncSchema, + createSchema: CreateAzureKeyVaultSyncSchema, + updateSchema: UpdateAzureKeyVaultSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 20573719b..1b9592c7c 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -2,6 +2,8 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router"; import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router"; +import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; +import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; @@ -11,5 +13,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 61787b47c..6a8e2aa97 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -1,7 +1,9 @@ export enum AppConnection { GitHub = "github", AWS = "aws", - GCP = "gcp" + GCP = "gcp", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index d4c9f97ad..645890b3b 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -20,10 +20,25 @@ import { } from "@app/services/app-connection/github"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { + AzureAppConfigurationConnectionMethod, + getAzureAppConfigurationConnectionListItem, + validateAzureAppConfigurationConnectionCredentials +} from "./azure-app-configuration"; +import { + AzureKeyVaultConnectionMethod, + getAzureKeyVaultConnectionListItem, + validateAzureKeyVaultConnectionCredentials +} from "./azure-key-vault"; + export const listAppConnectionOptions = () => { - return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) => - a.name.localeCompare(b.name) - ); + return [ + getAwsAppConnectionListItem(), + getGitHubConnectionListItem(), + getGcpAppConnectionListItem(), + getAzureKeyVaultConnectionListItem(), + getAzureAppConfigurationConnectionListItem() + ].sort((a, b) => a.name.localeCompare(b.name)); }; export const encryptAppConnectionCredentials = async ({ @@ -79,6 +94,10 @@ export const validateAppConnectionCredentials = async ( return validateGitHubConnectionCredentials(appConnection); case AppConnection.GCP: return validateGcpConnectionCredentials(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}`); @@ -89,6 +108,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => switch (method) { case GitHubConnectionMethod.App: return "GitHub App"; + 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 abff5cf3b..78fde3127 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -3,5 +3,7 @@ import { AppConnection } from "./app-connection-enums"; export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AWS]: "AWS", [AppConnection.GitHub]: "GitHub", - [AppConnection.GCP]: "GCP" + [AppConnection.GCP]: "GCP", + [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 8beaec21d..b5397b915 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -28,6 +28,8 @@ import { githubConnectionService } from "@app/services/app-connection/github/git import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TAppConnectionDALFactory } from "./app-connection-dal"; +import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; +import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { gcpConnectionService } from "./gcp/gcp-connection-service"; @@ -42,7 +44,9 @@ export type TAppConnectionServiceFactory = ReturnType = { [AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema, [AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema, - [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema + [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema, + [AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema, + [AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema }; export const appConnectionServiceFactory = ({ diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index dfe2d1c64..95ac8145c 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -11,11 +11,35 @@ import { TValidateGitHubConnectionCredentials } from "@app/services/app-connection/github"; +import { + 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); +export type TAppConnection = { id: string } & ( + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection +); -export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput); +export type TAppConnectionInput = { id: string } & ( + | TAwsConnectionInput + | TGitHubConnectionInput + | TGcpConnectionInput + | TAzureKeyVaultConnectionInput + | TAzureAppConfigurationConnectionInput +); export type TCreateAppConnectionDTO = Pick< TAppConnectionInput, @@ -26,9 +50,16 @@ export type TUpdateAppConnectionDTO = Partial { + 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..183376acf --- /dev/null +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-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 { 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({ + 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-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts new file mode 100644 index 000000000..12b1b3f3b --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -0,0 +1,170 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { + decryptAppConnectionCredentials, + encryptAppConnectionCredentials, + getAppConnectionMethodName +} from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "../app-connection-dal"; +import { AppConnection } from "../app-connection-enums"; +import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; +import { + ExchangeCodeAzureResponse, + TAzureKeyVaultConnectionConfig, + TAzureKeyVaultConnectionCredentials +} from "./azure-key-vault-connection-types"; + +export const getAzureConnectionAccessToken = async ( + connectionId: string, + appConnectionDAL: Pick, + 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); + + if (!appConnection) { + throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) { + throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` }); + } + + const credentials = (await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials + })) 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, + refresh_token: credentials.refreshToken + }) + ); + + const accessExpiresAt = new Date(); + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in); + + const updatedCredentials = { + ...credentials, + accessToken: data.access_token, + expiresAt: accessExpiresAt.getTime(), + refreshToken: data.refresh_token + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId: appConnection.orgId, + kmsService + }); + + await appConnectionDAL.update( + { id: connectionId }, + { + encryptedCredentials + } + ); + + return { + accessToken: data.access_token + }; +}; + +export const getAzureKeyVaultConnectionListItem = () => { + const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + + return { + 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 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(); + + 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://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` + }) + ); + } 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 AzureKeyVaultConnectionMethod.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 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..f3c7c43b8 --- /dev/null +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-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 { 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({ + 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/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 new file mode 100644 index 000000000..07876f088 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +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.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 new file mode 100644 index 000000000..2d2f9d129 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -0,0 +1,214 @@ +/* eslint-disable no-await-in-loop */ +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-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"; + +import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types"; + +type TAzureAppConfigurationSecretSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +interface AzureAppConfigKeyValue { + key: string; + value: string; + label?: string; +} + +export const azureAppConfigurationSecretSyncFactory = ({ + kmsService, + appConnectionDAL +}: TAzureAppConfigurationSecretSyncFactoryDeps) => { + const $getCompleteAzureAppConfigValues = async (accessToken: string, baseURL: string, url: string) => { + let result: AzureAppConfigKeyValue[] = []; + let currentUrl = url; + + while (currentUrl) { + const res = await request.get<{ items: AzureAppConfigKeyValue[]; ["@nextLink"]: string }>(currentUrl, { + baseURL, + headers: { + Authorization: `Bearer ${accessToken}` + }, + // we force IPV4 because docker setup fails with ipv6 + httpsAgent: new https.Agent({ + family: 4 + }) + }); + + result = result.concat(res.data.items); + currentUrl = res.data?.["@nextLink"]; + } + + return result; + }; + + const $deleteAzureSecret = async (accessToken: string, configurationUrl: string, key: string, label?: string) => { + await request.delete(`${configurationUrl}/kv/${key}?api-version=2023-11-01`, { + headers: { + Authorization: `Bearer ${accessToken}` + }, + ...(label && + label.length > 0 && { + params: { + label + } + }), + httpsAgent: new https.Agent({ + family: 4 + }) + }); + }; + + const syncSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => { + if (!secretSync.destinationConfig.configurationUrl.endsWith(".azconfig.io")) { + throw new BadRequestError({ + message: "Invalid Azure App Configuration URL provided." + }); + } + + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigValuesUrlAllSecrets = `/kv?api-version=2023-11-01`; + + const azureAppConfigSecretsLabeled = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrlAllSecrets + ) + ).map((entry) => [ + entry.key, + { + value: entry.value, + label: entry.label + } + ]) + ); + + // add the secrets to azure app config, that are in infisical + for await (const key of Object.keys(secretMap)) { + if (!(key in azureAppConfigSecretsLabeled) || secretMap[key]?.value !== azureAppConfigSecretsLabeled[key]) { + await request.put( + `${secretSync.destinationConfig.configurationUrl}/kv/${key}?api-version=2023-11-01`, + { + value: secretMap[key]?.value, + ...(isAzureKeyVaultReference(secretMap[key]?.value || "") && { + content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" + }) + }, + { + ...(secretSync.destinationConfig.label && { + params: { + label: secretSync.destinationConfig.label + } + }), + + headers: { + Authorization: `Bearer ${accessToken}` + }, + httpsAgent: new https.Agent({ + family: 4 + }) + } + ); + } + } + + for await (const key of Object.keys(azureAppConfigSecrets)) { + const azureSecret = azureAppConfigSecrets[key]; + if ( + !(key in secretMap) || + secretMap[key] === null || + (azureSecret.label && azureSecret.label !== secretSync.destinationConfig.label) || + (!azureSecret.label && secretSync.destinationConfig.label) + ) { + await $deleteAzureSecret(accessToken, secretSync.destinationConfig.configurationUrl, key, azureSecret.label); + } + } + }; + + const removeSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + for await (const infisicalKey of Object.keys(secretMap)) { + if (infisicalKey in azureAppConfigSecrets) { + await $deleteAzureSecret( + accessToken, + secretSync.destinationConfig.configurationUrl, + infisicalKey, + secretSync.destinationConfig.label + ); + } + } + }; + + const getSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService); + + const secretMap: TSecretMap = {}; + + const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${ + secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00" + }`; + + const azureAppConfigSecrets = Object.fromEntries( + ( + await $getCompleteAzureAppConfigValues( + accessToken, + secretSync.destinationConfig.configurationUrl, + azureAppConfigValuesUrl + ) + ).map((entry) => [entry.key, entry.value]) + ); + + Object.keys(azureAppConfigSecrets).forEach((key) => { + secretMap[key] = { + value: azureAppConfigSecrets[key] + }; + }); + + return secretMap; + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; 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 new file mode 100644 index 000000000..b11d67858 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-schemas.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const AzureAppConfigurationSyncDestinationConfigSchema = z.object({ + configurationUrl: z + .string() + .min(1, "App Configuration URL required") + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.CONFIGURATION_URL), + label: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.LABEL) +}); + +const AzureAppConfigurationSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AzureAppConfigurationSyncSchema = BaseSecretSyncSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.AzureAppConfiguration), + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema +}); + +export const CreateAzureAppConfigurationSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema +}); + +export const UpdateAzureAppConfigurationSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AzureAppConfiguration, + AzureAppConfigurationSyncOptionsConfig +).extend({ + destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema.optional() +}); + +export const AzureAppConfigurationSyncListItemSchema = z.object({ + name: z.literal("Azure App Configuration"), + connection: z.literal(AppConnection.AzureAppConfiguration), + destination: z.literal(SecretSync.AzureAppConfiguration), + 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 new file mode 100644 index 000000000..4cfbd5472 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-types.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { TAzureAppConfigurationConnection } from "@app/services/app-connection/azure-app-configuration"; + +import { + AzureAppConfigurationSyncListItemSchema, + AzureAppConfigurationSyncSchema, + CreateAzureAppConfigurationSyncSchema +} from "./azure-app-configuration-sync-schemas"; + +export type TAzureAppConfigurationSync = z.infer; + +export type TAzureAppConfigurationSyncInput = z.infer; + +export type TAzureAppConfigurationSyncListItem = z.infer; + +export type TAzureAppConfigurationSyncWithCredentials = TAzureAppConfigurationSync & { + connection: TAzureAppConfigurationConnection; +}; diff --git a/backend/src/services/secret-sync/azure-app-configuration/index.ts b/backend/src/services/secret-sync/azure-app-configuration/index.ts new file mode 100644 index 000000000..0ed052ff2 --- /dev/null +++ b/backend/src/services/secret-sync/azure-app-configuration/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-app-configuration-sync-constants"; +export * from "./azure-app-configuration-sync-fns"; +export * from "./azure-app-configuration-sync-schemas"; +export * from "./azure-app-configuration-sync-types"; 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 new file mode 100644 index 000000000..9e2f986ce --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +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.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 new file mode 100644 index 000000000..e4254074d --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -0,0 +1,256 @@ +/* eslint-disable no-await-in-loop */ +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-key-vault"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types"; + +type TAzureKeyVaultSecretSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +export const azureKeyVaultSecretSyncFactory = ({ + kmsService, + appConnectionDAL +}: TAzureKeyVaultSecretSyncFactoryDeps) => { + const $getAzureKeyVaultSecrets = async (accessToken: string, vaultBaseUrl: string) => { + const paginateAzureKeyVaultSecrets = async () => { + let result: GetAzureKeyVaultSecret[] = []; + + let currentUrl = `${vaultBaseUrl}/secrets?api-version=7.3`; + + while (currentUrl) { + const res = await request.get<{ value: GetAzureKeyVaultSecret; nextLink: string }>(currentUrl, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + result = result.concat(res.data.value); + currentUrl = res.data.nextLink; + } + + return result; + }; + + const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(); + + const enabledAzureKeyVaultSecrets = getAzureKeyVaultSecrets.filter((secret) => secret.attributes.enabled); + + // disabled keys to skip sending updates to + const disabledAzureKeyVaultSecretKeys = getAzureKeyVaultSecrets + .filter(({ attributes }) => !attributes.enabled) + .map((getAzureKeyVaultSecret) => { + return getAzureKeyVaultSecret.id.substring(getAzureKeyVaultSecret.id.lastIndexOf("/") + 1); + }); + + let lastSlashIndex: number; + const res = ( + await Promise.all( + enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { + if (!lastSlashIndex) { + lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); + } + + const azureKeyVaultSecret = await request.get( + `${getAzureKeyVaultSecret.id}?api-version=7.3`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + return { + ...azureKeyVaultSecret.data, + key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1) + }; + }) + ) + ).reduce( + (obj, secret) => ({ + ...obj, + [secret.key]: secret + }), + {} as Record + ); + + return { + vaultSecrets: res, + disabledAzureKeyVaultSecretKeys + }; + }; + + const syncSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + const setSecrets: { + key: string; + value: string; + }[] = []; + + const deleteSecrets: string[] = []; + + Object.keys(secretMap).forEach((infisicalKey) => { + const hyphenatedKey = infisicalKey.replace(/_/g, "-"); + if (!(hyphenatedKey in vaultSecrets)) { + // case: secret has been created + setSecrets.push({ + key: hyphenatedKey, + value: secretMap[infisicalKey].value + }); + } else if (secretMap[infisicalKey].value !== vaultSecrets[hyphenatedKey].value) { + // case: secret has been updated + setSecrets.push({ + key: hyphenatedKey, + value: secretMap[infisicalKey].value + }); + } + }); + + Object.keys(vaultSecrets).forEach((key) => { + const underscoredKey = key.replace(/-/g, "_"); + if (!(underscoredKey in secretMap)) { + deleteSecrets.push(key); + } + }); + + const setSecretAzureKeyVault = async ({ key, value }: { key: string; value: string }) => { + let isSecretSet = false; + let syncError: Error | null = null; + let maxTries = 6; + if (disabledAzureKeyVaultSecretKeys.includes(key)) return; + + while (!isSecretSet && maxTries > 0) { + // try to set secret + try { + await request.put( + `${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`, + { + value + }, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + isSecretSet = true; + } catch (err) { + syncError = err as Error; + if (err instanceof AxiosError) { + // eslint-disable-next-line + if (err.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { + await request.post( + `${secretSync.destinationConfig.vaultBaseUrl}/deletedsecrets/${key}/recover?api-version=7.3`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + await new Promise((resolve) => { + setTimeout(resolve, 10_000); + }); + } else { + await new Promise((resolve) => { + setTimeout(resolve, 10_000); + }); + maxTries -= 1; + } + } + } + } + + if (!isSecretSet) { + throw new SecretSyncError({ + error: syncError, + secretKey: key + }); + } + }; + + for await (const setSecret of setSecrets) { + const { key, value } = setSecret; + await setSecretAzureKeyVault({ + key, + value + }); + } + + for await (const deleteSecretKey of deleteSecrets.filter( + (secret) => !setSecrets.find((setSecret) => setSecret.key === secret) + )) { + await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } + }; + + const removeSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + for await (const [key] of Object.entries(vaultSecrets)) { + const underscoredKey = key.replace(/-/g, "_"); + + if (underscoredKey in secretMap) { + if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) { + await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + } + } + } + }; + + const getSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials) => { + const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService); + + const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets( + accessToken, + secretSync.destinationConfig.vaultBaseUrl + ); + + const secretMap: TSecretMap = {}; + + Object.keys(vaultSecrets).forEach((key) => { + if (!disabledAzureKeyVaultSecretKeys.includes(key)) { + const underscoredKey = key.replace(/-/g, "_"); + secretMap[underscoredKey] = { + value: vaultSecrets[key].value + }; + } + }); + + return secretMap; + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; 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 new file mode 100644 index 000000000..40b476744 --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-schemas.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const AzureKeyVaultSyncDestinationConfigSchema = z.object({ + vaultBaseUrl: z + .string() + .url("Invalid vault base URL format") + .min(1, "Vault base URL required") + .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.VAULT_BASE_URL) +}); + +const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const AzureKeyVaultSyncSchema = BaseSecretSyncSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.AzureKeyVault), + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema +}); + +export const CreateAzureKeyVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema +}); + +export const UpdateAzureKeyVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.AzureKeyVault, + AzureKeyVaultSyncOptionsConfig +).extend({ + destinationConfig: AzureKeyVaultSyncDestinationConfigSchema.optional() +}); + +export const AzureKeyVaultSyncListItemSchema = z.object({ + name: z.literal("Azure Key Vault"), + connection: z.literal(AppConnection.AzureKeyVault), + destination: z.literal(SecretSync.AzureKeyVault), + 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 new file mode 100644 index 000000000..d8083d640 --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-types.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +import { TAzureKeyVaultConnection } from "@app/services/app-connection/azure-key-vault"; + +import { + AzureKeyVaultSyncListItemSchema, + AzureKeyVaultSyncSchema, + CreateAzureKeyVaultSyncSchema +} from "./azure-key-vault-sync-schemas"; + +export type TAzureKeyVaultSync = z.infer; + +export type TAzureKeyVaultSyncInput = z.infer; + +export type TAzureKeyVaultSyncListItem = z.infer; + +export type TAzureKeyVaultSyncWithCredentials = TAzureKeyVaultSync & { + connection: TAzureKeyVaultConnection; +}; + +export interface GetAzureKeyVaultSecret { + id: string; // secret URI + value: string; + attributes: { + enabled: boolean; + created: number; + updated: number; + recoveryLevel: string; + recoverableDays: number; + }; +} + +export interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { + key: string; +} diff --git a/backend/src/services/secret-sync/azure-key-vault/index.ts b/backend/src/services/secret-sync/azure-key-vault/index.ts new file mode 100644 index 000000000..f2a7e036f --- /dev/null +++ b/backend/src/services/secret-sync/azure-key-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-key-vault-sync-constants"; +export * from "./azure-key-vault-sync-fns"; +export * from "./azure-key-vault-sync-schemas"; +export * from "./azure-key-vault-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 14d5f06c7..0bee11d95 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -2,7 +2,9 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", AWSSecretsManager = "aws-secrets-manager", GitHub = "github", - GCPSecretManager = "gcp-secret-manager" + GCPSecretManager = "gcp-secret-manager", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 184be9507..c39ceed54 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -17,6 +17,13 @@ import { TSecretSyncWithCredentials } from "@app/services/secret-sync/secret-sync-types"; +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { + AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, + azureAppConfigurationSecretSyncFactory +} from "./azure-app-configuration"; +import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSecretSyncFactory } from "./azure-key-vault"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; @@ -24,13 +31,20 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, [SecretSync.AWSSecretsManager]: AWS_SECRETS_MANAGER_SYNC_LIST_OPTION, [SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION, - [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION + [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION, + [SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION, + [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); }; +type TSyncSecretDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + // const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { // let secretMap = { ...unprocessedSecretMap }; // @@ -72,7 +86,11 @@ export const listSecretSyncOptions = () => { // }; export const SecretSyncFns = { - syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { + syncSecrets: ( + secretSync: TSecretSyncWithCredentials, + secretMap: TSecretMap, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { // const affixedSecretMap = addAffixes(secretSync, secretMap); switch (secretSync.destination) { @@ -84,13 +102,26 @@ export const SecretSyncFns = { return GithubSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.GCPSecretManager: return GcpSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.AzureKeyVault: + return azureKeyVaultSecretSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); + case SecretSync.AzureAppConfiguration: + return azureAppConfigurationSecretSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` ); } }, - getSecrets: async (secretSync: TSecretSyncWithCredentials): Promise => { + getSecrets: async ( + secretSync: TSecretSyncWithCredentials, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { let secretMap: TSecretMap; switch (secretSync.destination) { case SecretSync.AWSParameterStore: @@ -105,6 +136,18 @@ export const SecretSyncFns = { case SecretSync.GCPSecretManager: secretMap = await GcpSyncFns.getSecrets(secretSync); break; + case SecretSync.AzureKeyVault: + secretMap = await azureKeyVaultSecretSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; + case SecretSync.AzureAppConfiguration: + secretMap = await azureAppConfigurationSecretSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -114,7 +157,11 @@ export const SecretSyncFns = { return secretMap; // return stripAffixes(secretSync, secretMap); }, - removeSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise => { + removeSecrets: ( + secretSync: TSecretSyncWithCredentials, + secretMap: TSecretMap, + { kmsService, appConnectionDAL }: TSyncSecretDeps + ): Promise => { // const affixedSecretMap = addAffixes(secretSync, secretMap); switch (secretSync.destination) { @@ -126,6 +173,16 @@ export const SecretSyncFns = { return GithubSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.GCPSecretManager: return GcpSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.AzureKeyVault: + return azureKeyVaultSecretSyncFactory({ + appConnectionDAL, + kmsService + }).removeSecrets(secretSync, secretMap); + case SecretSync.AzureAppConfiguration: + 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 8ed4cdb1b..33a87fbef 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -5,12 +5,16 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", [SecretSync.AWSSecretsManager]: "AWS Secrets Manager", [SecretSync.GitHub]: "GitHub", - [SecretSync.GCPSecretManager]: "GCP Secret Manager" + [SecretSync.GCPSecretManager]: "GCP Secret Manager", + [SecretSync.AzureKeyVault]: "Azure Key Vault", + [SecretSync.AzureAppConfiguration]: "Azure App Configuration" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, - [SecretSync.GCPSecretManager]: AppConnection.GCP + [SecretSync.GCPSecretManager]: AppConnection.GCP, + [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, + [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index d2bcdb590..e822a20b7 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -57,11 +57,14 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; + export type TSecretSyncQueueFactory = ReturnType; type TSecretSyncQueueFactoryDep = { queueService: Pick; kmsService: Pick; + appConnectionDAL: Pick; keyStore: Pick; folderDAL: TSecretFolderDALFactory; secretV2BridgeDAL: Pick< @@ -111,6 +114,7 @@ const getRequeueDelay = (failureCount?: number) => { export const secretSyncQueueFactory = ({ queueService, kmsService, + appConnectionDAL, keyStore, folderDAL, secretV2BridgeDAL, @@ -322,7 +326,10 @@ export const secretSyncQueueFactory = ({ "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path." ); - const importedSecrets = await SecretSyncFns.getSecrets(secretSync); + const importedSecrets = await SecretSyncFns.getSecrets(secretSync, { + appConnectionDAL, + kmsService + }); if (!Object.keys(importedSecrets).length) return {}; @@ -434,7 +441,10 @@ export const secretSyncQueueFactory = ({ }); } - await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap); + await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, { + appConnectionDAL, + kmsService + }); isSynced = true; } catch (err) { @@ -672,7 +682,11 @@ export const secretSyncQueueFactory = ({ credentials } } as TSecretSyncWithCredentials, - secretMap + secretMap, + { + appConnectionDAL, + kmsService + } ); isSuccess = true; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 9ce331e8e..bf43ae927 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -23,27 +23,51 @@ import { TAwsParameterStoreSyncListItem, TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store"; +import { + TAzureAppConfigurationSync, + TAzureAppConfigurationSyncInput, + TAzureAppConfigurationSyncListItem, + TAzureAppConfigurationSyncWithCredentials +} from "./azure-app-configuration"; +import { + TAzureKeyVaultSync, + TAzureKeyVaultSyncInput, + TAzureKeyVaultSyncListItem, + TAzureKeyVaultSyncWithCredentials +} from "./azure-key-vault"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; -export type TSecretSync = TAwsParameterStoreSync | TAwsSecretsManagerSync | TGitHubSync | TGcpSync; +export type TSecretSync = + | TAwsParameterStoreSync + | TAwsSecretsManagerSync + | TGitHubSync + | TGcpSync + | TAzureKeyVaultSync + | TAzureAppConfigurationSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials | TAwsSecretsManagerSyncWithCredentials | TGitHubSyncWithCredentials - | TGcpSyncWithCredentials; + | TGcpSyncWithCredentials + | TAzureKeyVaultSyncWithCredentials + | TAzureAppConfigurationSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput | TAwsSecretsManagerSyncInput | TGitHubSyncInput - | TGcpSyncInput; + | TGcpSyncInput + | TAzureKeyVaultSyncInput + | TAzureAppConfigurationSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem | TAwsSecretsManagerSyncListItem | TGitHubSyncListItem - | TGcpSyncListItem; + | TGcpSyncListItem + | TAzureKeyVaultSyncListItem + | TAzureAppConfigurationSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx new file mode 100644 index 000000000..03d5f3537 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/azure-app-configuration/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx new file mode 100644 index 000000000..6c429c369 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/azure-app-configuration" +--- + + + Azure App Configuration Connections must be created through the Infisical UI. + Check out the configuration docs for [Azure App Configuration Connections](/integrations/app-connections/azure-app-configuration) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx new file mode 100644 index 000000000..cc2e4ca38 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx new file mode 100644 index 000000000..c49afe500 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx new file mode 100644 index 000000000..a38365b01 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/azure-app-configuration/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx new file mode 100644 index 000000000..4da96476d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx b/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx new file mode 100644 index 000000000..65d29899e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-app-configuration/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/azure-app-configuration/{connectionId}" +--- + + + Azure App Configuration Connections must be updated through the Infisical UI. + Check out the configuration docs for [Azure App Configuration Connections](/integrations/app-connections/azure-app-configuration) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx new file mode 100644 index 000000000..4b1c758ee --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/azure-key-vault/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx new file mode 100644 index 000000000..d0d9f7e6f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/azure-key-vault" +--- + + + Azure Key Vault Connections must be created through the Infisical UI. + Check out the configuration docs for [Azure Key Vault Connections](/integrations/app-connections/azure-key-vault) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx new file mode 100644 index 000000000..02fbe4a31 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/azure-key-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx new file mode 100644 index 000000000..e5d3e77a1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/azure-key-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx new file mode 100644 index 000000000..55502def8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/azure-key-vault/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx new file mode 100644 index 000000000..76f1f8b88 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx b/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx new file mode 100644 index 000000000..8637b838a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-key-vault/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/azure-key-vault/{connectionId}" +--- + + + Azure Key Vault Connections must be updated through the Infisical UI. + Check out the configuration docs for [Azure Key Vault Connections](/integrations/app-connections/azure-key-vault) for a step-by-step + guide. + diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx new file mode 100644 index 000000000..82456fb6c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx new file mode 100644 index 000000000..23c2ad27b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx new file mode 100644 index 000000000..45418f047 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx new file mode 100644 index 000000000..488a15974 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx new file mode 100644 index 000000000..b44944951 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx new file mode 100644 index 000000000..2ea72fb20 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/azure-app-configuration" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx new file mode 100644 index 000000000..952a852d2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx new file mode 100644 index 000000000..e3d37dd56 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/azure-app-configuration/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx new file mode 100644 index 000000000..b22a302dc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-app-configuration/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/azure-app-configuration/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx new file mode 100644 index 000000000..493e41abd --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx new file mode 100644 index 000000000..7b02b6f68 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx new file mode 100644 index 000000000..5b8ecd8c7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx new file mode 100644 index 000000000..dbb7b6f5c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/azure-key-vault/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx new file mode 100644 index 000000000..08ac487c9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx new file mode 100644 index 000000000..a462739f7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/azure-key-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx new file mode 100644 index 000000000..8882c5e47 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx new file mode 100644 index 000000000..87f6b4f56 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/azure-key-vault/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx new file mode 100644 index 000000000..2d390ab8b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/azure-key-vault/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/azure-key-vault/{syncId}" +--- diff --git a/docs/images/app-connections/azure/app-configuration/create-oauth-method.png b/docs/images/app-connections/azure/app-configuration/create-oauth-method.png new file mode 100644 index 000000000..1c8ed186b Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/create-oauth-method.png differ diff --git a/docs/images/app-connections/azure/app-configuration/oauth-connection.png b/docs/images/app-connections/azure/app-configuration/oauth-connection.png new file mode 100644 index 000000000..906b30899 Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/oauth-connection.png differ diff --git a/docs/images/app-connections/azure/app-configuration/select-connection.png b/docs/images/app-connections/azure/app-configuration/select-connection.png new file mode 100644 index 000000000..ff452fb8b Binary files /dev/null and b/docs/images/app-connections/azure/app-configuration/select-connection.png differ diff --git a/docs/images/app-connections/azure/grant-access.png b/docs/images/app-connections/azure/grant-access.png new file mode 100644 index 000000000..c0545a343 Binary files /dev/null and b/docs/images/app-connections/azure/grant-access.png differ diff --git a/docs/images/app-connections/azure/key-vault/create-oauth-method.png b/docs/images/app-connections/azure/key-vault/create-oauth-method.png new file mode 100644 index 000000000..6d7de342a Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/create-oauth-method.png differ diff --git a/docs/images/app-connections/azure/key-vault/oauth-connection.png b/docs/images/app-connections/azure/key-vault/oauth-connection.png new file mode 100644 index 000000000..5c0239cdc Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/oauth-connection.png differ diff --git a/docs/images/app-connections/azure/key-vault/select-connection.png b/docs/images/app-connections/azure/key-vault/select-connection.png new file mode 100644 index 000000000..e55028588 Binary files /dev/null and b/docs/images/app-connections/azure/key-vault/select-connection.png differ diff --git a/docs/images/app-connections/azure/keyvault-azure-permissions.png b/docs/images/app-connections/azure/keyvault-azure-permissions.png new file mode 100644 index 000000000..0f009cdc1 Binary files /dev/null and b/docs/images/app-connections/azure/keyvault-azure-permissions.png differ diff --git a/docs/images/app-connections/azure/register-callback.png b/docs/images/app-connections/azure/register-callback.png new file mode 100644 index 000000000..b06b5ce6f Binary files /dev/null and b/docs/images/app-connections/azure/register-callback.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png b/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png new file mode 100644 index 000000000..22610ae22 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-destination.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-details.png b/docs/images/secret-syncs/azure-app-configuration/app-config-details.png new file mode 100644 index 000000000..e007bd7f5 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-details.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-options.png b/docs/images/secret-syncs/azure-app-configuration/app-config-options.png new file mode 100644 index 000000000..94b70a547 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-options.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-review.png b/docs/images/secret-syncs/azure-app-configuration/app-config-review.png new file mode 100644 index 000000000..db10d6d41 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-review.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-source.png b/docs/images/secret-syncs/azure-app-configuration/app-config-source.png new file mode 100644 index 000000000..df2e8b22f Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-source.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png b/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png new file mode 100644 index 000000000..1d229319f Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/app-config-synced.png differ diff --git a/docs/images/secret-syncs/azure-app-configuration/select-app-config.png b/docs/images/secret-syncs/azure-app-configuration/select-app-config.png new file mode 100644 index 000000000..42ad36997 Binary files /dev/null and b/docs/images/secret-syncs/azure-app-configuration/select-app-config.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png b/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png new file mode 100644 index 000000000..6380b315b Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/select-key-vault-option.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-destination.png b/docs/images/secret-syncs/azure-key-vault/vault-destination.png new file mode 100644 index 000000000..636c892f4 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-destination.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-details.png b/docs/images/secret-syncs/azure-key-vault/vault-details.png new file mode 100644 index 000000000..fdaa51ec8 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-details.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-options.png b/docs/images/secret-syncs/azure-key-vault/vault-options.png new file mode 100644 index 000000000..f35d1cc3b Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-options.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-review.png b/docs/images/secret-syncs/azure-key-vault/vault-review.png new file mode 100644 index 000000000..51572a4ca Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-review.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-source.png b/docs/images/secret-syncs/azure-key-vault/vault-source.png new file mode 100644 index 000000000..5e5e624f1 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-source.png differ diff --git a/docs/images/secret-syncs/azure-key-vault/vault-synced.png b/docs/images/secret-syncs/azure-key-vault/vault-synced.png new file mode 100644 index 000000000..828aa42a8 Binary files /dev/null and b/docs/images/secret-syncs/azure-key-vault/vault-synced.png differ diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx new file mode 100644 index 000000000..0d481986d --- /dev/null +++ b/docs/integrations/app-connections/azure-app-configuration.mdx @@ -0,0 +1,90 @@ +--- +title: "Azure App Configuration Connection" +description: "Learn how to configure a Azure App Configuration Connection for Infisical." +--- + +Infisical currently only supports one method for connecting to Azure, which is OAuth. + + + Using the Azure App Configuration connection on a self-hosted instance of Infisical requires configuring an application in Azure + and registering your instance with it. + + **Prerequisites:** + + - Set up Azure and have an existing App Configuration instance. + + + + Navigate to Azure Active Directory > App registrations to create a new application. + + + Azure Active Directory is now Microsoft Entra ID. + + ![Azure app config](/images/integrations/azure-app-configuration/config-aad.png) + ![Azure app config](/images/integrations/azure-app-configuration/config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/azure/oauth/callback`. + + The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. + + + ![Azure app config](/images/app-connections/azure/register-callback.png) + + + + For the Azure Connection to work with App Configuration, you need to assign multiple permissions to the application. + + #### Azure App Configuration permissions + + Set the API permissions of the Azure application to include the following Azure App Configuration permissions: `KeyValue.Delete`, `KeyValue.Read`, and `KeyValue.Write`. + ![Azure app config](../../images/integrations/azure-app-configuration/app-api-permissions.png) + + + + + Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. + + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-1.png) + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-2.png) + ![Azure app config](../../images/integrations/azure-app-configuration/config-credentials-3.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. + + - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + + Once added, restart your Infisical instance and use the Azure App Configuration connection. + + + + + +## Setup Azure Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/app-configuration/select-connection.png) + + + You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID. + + Now select the **OAuth** method and click **Connect to Azure**. + + ![Connect via Azure OAUth](/images/app-connections/azure/app-configuration/create-oauth-method.png) + + + + + + You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, + you will redirect you back to Infisical's App Connections page. ![Azure App Configuration + Authorization](/images/app-connections/azure/grant-access.png) + + + Your **Azure App Configuration Connection** is now available for use. ![Assume Role AWS Connection](/images/app-connections/azure/app-configuration/oauth-connection.png) + + diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx new file mode 100644 index 000000000..705401708 --- /dev/null +++ b/docs/integrations/app-connections/azure-key-vault.mdx @@ -0,0 +1,89 @@ +--- +title: "Azure Key Vault Connection" +description: "Learn how to configure a Azure Key Vault Connection for Infisical." +--- + +Infisical currently only supports one method for connecting to Azure, which is OAuth. + + + Using the Azure Key Vault connection on a self-hosted instance of Infisical requires configuring an application in Azure + and registering your instance with it. + + **Prerequisites:** + + - Set up Azure and have an existing Key Vault instance. + + + + Navigate to Azure Active Directory > App registrations to create a new application. + + + Azure Active Directory is now Microsoft Entra ID. + + ![Azure key vault](/images/integrations/azure-app-configuration/config-aad.png) + ![Azure key vault](/images/integrations/azure-app-configuration/config-new-app.png) + + Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/organization/app-connections/azure/oauth/callback`. + + The domain you defined in the Redirect URI should be equivalent to the `SITE_URL` configured in your Infisical instance. + + + ![Azure key vault](/images/app-connections/azure/register-callback.png) + + + + For the Azure Connection to work with Key Vault, you need to assign multiple permissions to the application. + + #### Azure Key Vault permissions + + Set the API permissions of the Azure application to include `user.impersonation` for the Key Vault API. + ![Azure key vault](/images/app-connections/azure/keyvault-azure-permissions.png) + + + + Obtain the **Application (Client) ID** in Overview and generate a **Client Secret** in Certificate & secrets for your Azure application. + + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-1.png) + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-2.png) + ![Azure key vault](../../images/integrations/azure-app-configuration/config-credentials-3.png) + + Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. + + - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + + Once added, restart your Infisical instance and use the Azure Key Vault connection. + + + + + +## Setup Azure Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Azure Connection** option from the connection options modal. ![Select Azure Connection](/images/app-connections/azure/key-vault/select-connection.png) + + + You can optionally authenticate against a specific tenant by providing the Azure Tenant or Directory ID. + + Now select the **OAuth** method and click **Connect to Azure**. + + ![Connect via Azure OAUth](/images/app-connections/azure/key-vault/create-oauth-method.png) + + + + + + You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted, + you will redirect you back to Infisical's App Connections page. ![Azure Key Vault + Authorization](/images/app-connections/azure/grant-access.png) + + + Your **Azure Key Vault Connection** is now available for use. ![Assume Role AWS Connection](/images/app-connections/azure/key-vault/oauth-connection.png) + + diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx new file mode 100644 index 000000000..3b443c672 --- /dev/null +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -0,0 +1,147 @@ +--- +title: "Azure App Configuration Sync" +description: "Learn how to configure an Azure App Configuration Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure App Configuration. + + + The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal + for Infisical to sync secrets to Azure App Configuration: `Read Key-Value`, `Write Key-Value`, `Delete Key-Value`. + + Any role with these permissions would work such as the **App Configuration Data Owner** role. Alternatively, you can use the **App Configuration Data Contributor** role for read/write access. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Azure App Configuration** option. + ![Select Azure App Configuration](/images/secret-syncs/azure-app-configuration/select-app-config.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/azure-app-configuration/app-config-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/azure-app-configuration/app-config-destination.png) + + - **Azure Connection**: The Azure Connection to authenticate with. + - **Configuration URL**: The URL of your Azure App Configuration. + - **Label**: An optional label to attach to all secrets created by Infisical inside your Azure App Configuration. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/azure-app-configuration/app-config-options.png) + + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Azure App Configuration Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/azure-app-configuration/app-config-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Azure App Configuration Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/azure-app-configuration/app-config-review.png) + + 8. If enabled, your Azure App Configuration Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/azure-app-configuration/app-config-synced.png) + + + + To create an **Azure App Configuration Sync**, make an API request to the [Create Azure App Configuration Sync](/api-reference/endpoints/secret-syncs/azure-app-configuration/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/azure-app-configuration \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-azure-app-configuration-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "configurationUrl": "https://my-azure-app-configuration.azconfig.io", + "label": "my-label" + } + }' + ``` + + ### Sample response + + ```json Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-azure-app-configuration-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "azure", + "name": "my-azure-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "azure-app-configuration", + "destinationConfig": { + "configurationUrl": "https://my-azure-app-configuration.azconfig.io", + "label": "my-label" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx new file mode 100644 index 000000000..9b28f841f --- /dev/null +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -0,0 +1,147 @@ +--- +title: "Azure Key Vault Sync" +description: "Learn how to configure a Azure Key Vault Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure Key Vault. + + + The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal + for Infisical to sync secrets to Azure Key Vault: `secrets/list`, `secrets/get`, `secrets/set`, `secrets/recover`. + + Any role with these permissions would work such as the **Key Vault Secrets Officer** role. + + + + Secrets in Infisical that contain an underscore (`_`) will be converted to a hyphen (`-`) when synced to Azure Key Vault. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Azure Key Vault** option. + ![Select Key Vault](/images/secret-syncs/azure-key-vault/select-key-vault-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/azure-key-vault/vault-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/azure-key-vault/vault-destination.png) + + - **Azure Connection**: The Azure Connection to authenticate with. + - **Vault Base URL**: The URL of your Azure Key Vault. +

+ + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/azure-key-vault/vault-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. + - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Azure Key Vault Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/azure-key-vault/vault-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Azure Key Vault Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/azure-key-vault/vault-review.png) + + 8. If enabled, your Azure Key Vault Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/azure-key-vault/vault-synced.png) + + + + To create a **Azure Key Vault Sync**, make an API request to the [Create Key Vault Sync](/api-reference/endpoints/secret-syncs/azure-key-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/azure-key-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-key-vault-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + } + }' + ``` + + ### Sample response + + ```json Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-key-vault-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "azure", + "name": "my-azure-key-vault-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "azure-key-vault", + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 4c6d52efb..d7d7afd86 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -13,7 +13,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) - 2. Select the **GitHub Store** option. + 2. Select the **GitHub** option. ![Select GitHub](/images/secret-syncs/github/select-github-option.png) 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. diff --git a/docs/mint.json b/docs/mint.json index 55498832e..4b55a687a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -393,7 +393,9 @@ "pages": [ "integrations/app-connections/aws", "integrations/app-connections/github", - "integrations/app-connections/gcp" + "integrations/app-connections/gcp", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/azure-app-configuration" ] } ] @@ -408,7 +410,9 @@ "integrations/secret-syncs/aws-parameter-store", "integrations/secret-syncs/aws-secrets-manager", "integrations/secret-syncs/github", - "integrations/secret-syncs/gcp-secret-manager" + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/azure-app-configuration" ] } ] @@ -843,6 +847,30 @@ "api-reference/endpoints/app-connections/gcp/update", "api-reference/endpoints/app-connections/gcp/delete" ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/app-connections/azure-key-vault/list", + "api-reference/endpoints/app-connections/azure-key-vault/available", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-id", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-name", + "api-reference/endpoints/app-connections/azure-key-vault/create", + "api-reference/endpoints/app-connections/azure-key-vault/update", + "api-reference/endpoints/app-connections/azure-key-vault/delete" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/app-connections/azure-app-configuration/list", + "api-reference/endpoints/app-connections/azure-app-configuration/available", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-id", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-name", + "api-reference/endpoints/app-connections/azure-app-configuration/create", + "api-reference/endpoints/app-connections/azure-app-configuration/update", + "api-reference/endpoints/app-connections/azure-app-configuration/delete" + ] } ] }, @@ -905,7 +933,36 @@ "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-key-vault/list", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name", + "api-reference/endpoints/secret-syncs/azure-key-vault/create", + "api-reference/endpoints/secret-syncs/azure-key-vault/update", + "api-reference/endpoints/secret-syncs/azure-key-vault/delete", + "api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-app-configuration/list", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name", + "api-reference/endpoints/secret-syncs/azure-app-configuration/create", + "api-reference/endpoints/secret-syncs/azure-app-configuration/update", + "api-reference/endpoints/secret-syncs/azure-app-configuration/delete", + "api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets" + ] } + ] }, { diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx index 408e4af67..d1a10d7e8 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncConnectionField.tsx @@ -23,7 +23,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { const destination = watch("destination"); const app = SECRET_SYNC_CONNECTION_MAP[destination]; - const { data: options, isLoading } = useListAvailableAppConnections(app); + const { data: availableConnections, isLoading } = useListAvailableAppConnections(app); const connectionName = APP_CONNECTION_MAP[app].name; @@ -55,7 +55,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { if (callback) callback(); }} isLoading={isLoading} - options={options} + options={availableConnections} placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} @@ -65,7 +65,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => { control={control} name="connection" /> - {options?.length === 0 && ( + {availableConnections?.length === 0 && (

{canCreateConnection ? ( diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx new file mode 100644 index 000000000..b4d1f8c42 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureAppConfigurationSyncFields.tsx @@ -0,0 +1,53 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const AzureAppConfigurationSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration } + >(); + + return ( + <> + { + setValue("destinationConfig.configurationUrl", ""); + }} + /> + ( + + + + )} + /> + + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureKeyVaultSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureKeyVaultSyncFields.tsx new file mode 100644 index 000000000..a5316d81e --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/AzureKeyVaultSyncFields.tsx @@ -0,0 +1,47 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const AzureKeyVaultSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AzureKeyVault } + >(); + + return ( + <> + { + setValue("destinationConfig.vaultBaseUrl", ""); + }} + /> + ( + + + + )} + /> + +

+ +

+ Secret keys with underscores (_) will be converted to hyphens (-) when syncing to Azure + Key Vault. +

+
+ + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 262677a6f..23724f7b5 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -5,6 +5,8 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields"; import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields"; +import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields"; +import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; @@ -22,6 +24,10 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.GCPSecretManager: return ; + case SecretSync.AzureKeyVault: + return ; + case SecretSync.AzureAppConfiguration: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureAppConfigurationSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureAppConfigurationSyncReviewFields.tsx new file mode 100644 index 000000000..e318397ec --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureAppConfigurationSyncReviewFields.tsx @@ -0,0 +1,20 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureAppConfigurationSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration } + >(); + const vaultBaseUrl = watch("destinationConfig.configurationUrl"); + const label = watch("destinationConfig.label"); + + return ( + <> + {vaultBaseUrl} + {label} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureKeyVaultSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureKeyVaultSyncReviewFields.tsx new file mode 100644 index 000000000..94a0b985e --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/AzureKeyVaultSyncReviewFields.tsx @@ -0,0 +1,12 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureKeyVaultSyncReviewFields = () => { + const { watch } = useFormContext(); + const vaultBaseUrl = watch("destinationConfig.vaultBaseUrl"); + + return {vaultBaseUrl}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index c343532cb..a772b603b 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -9,6 +9,8 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/hel import { SecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields"; +import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; +import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; @@ -46,6 +48,12 @@ export const SecretSyncReviewFields = () => { case SecretSync.GCPSecretManager: DestinationFieldsComponent = ; break; + case SecretSync.AzureKeyVault: + DestinationFieldsComponent = ; + break; + case SecretSync.AzureAppConfiguration: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts new file mode 100644 index 000000000..d472ddb04 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/azure-app-configuration-sync-destination-schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureAppConfigurationSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.AzureAppConfiguration), + destinationConfig: z.object({ + configurationUrl: z + .string() + .trim() + .min(1, { message: "Azure App Configuration URL is required" }) + .url() + .refine( + (val) => val.endsWith(".azconfig.io"), + "URL should have the following format: https://resource-name-here.azconfig.io" + ), + label: z.string().optional() + }) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts new file mode 100644 index 000000000..11fa6279b --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/azure-key-vault-sync-destination-schema.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const AzureKeyVaultSyncDestinationSchema = z.object({ + destination: z.literal(SecretSync.AzureKeyVault), + destinationConfig: z.object({ + vaultBaseUrl: z.string().url("Invalid vault base URL format").min(1, "Vault base URL required") + }) +}); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 817beafde..28fe232d3 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -6,6 +6,8 @@ import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; import { slugSchema } from "@app/lib/schemas"; import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema"; +import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; +import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; const BaseSecretSyncSchema = z.object({ @@ -35,7 +37,9 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncDestinationSchema, AwsSecretsManagerSyncDestinationSchema, GitHubSyncDestinationSchema, - GcpSyncDestinationSchema + GcpSyncDestinationSchema, + AzureKeyVaultSyncDestinationSchema, + AzureAppConfigurationSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema); diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 42823af4d..6a41e42b8 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -46,9 +46,9 @@ export const ROUTE_PATHS = Object.freeze({ "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId" ), AppConnections: { - GithubOauthCallbackPage: setRoute( - "/organization/app-connections/github/oauth/callback", - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback" + OauthCallbackPage: setRoute( + "/organization/app-connections/$appConnection/oauth/callback", + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" ) } }, diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 1349395b8..ece5fe906 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -4,6 +4,8 @@ import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AwsConnectionMethod, + AzureAppConfigurationConnectionMethod, + AzureKeyVaultConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, TAppConnection @@ -15,6 +17,11 @@ export const APP_CONNECTION_MAP: Record = { [SecretSync.AWSParameterStore]: AppConnection.AWS, [SecretSync.AWSSecretsManager]: AppConnection.AWS, [SecretSync.GitHub]: AppConnection.GitHub, - [SecretSync.GCPSecretManager]: AppConnection.GCP + [SecretSync.GCPSecretManager]: AppConnection.GCP, + [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 ba29a3781..46e853cb4 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -1,5 +1,7 @@ export enum AppConnection { AWS = "aws", GitHub = "github", - GCP = "gcp" + GCP = "gcp", + 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 91fc25cc2..b4aeb29d5 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -20,10 +20,22 @@ export type TGcpConnectionOption = TAppConnectionOptionBase & { app: AppConnection.GCP; }; +export type TAzureKeyVaultConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AzureKeyVault; + oauthClientId?: string; +}; + +export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AzureKeyVault; + oauthClientId?: string; +}; + export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; [AppConnection.GitHub]: TGitHubConnectionOption; [AppConnection.GCP]: TGcpConnectionOption; + [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-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 283a6d1a0..b3005a2df 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -3,15 +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 { 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-app-configuration-connection"; +export * from "./azure-key-vault-connection"; export * from "./gcp-connection"; export * from "./github-connection"; -export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection; +export type TAppConnection = + | TAwsConnection + | TGitHubConnection + | TGcpConnection + | TAzureKeyVaultConnection + | TAzureAppConfigurationConnection; -export type TAvailableAppConnection = Pick; +export type TAvailableAppConnection = Pick; export type TListAppConnections = { appConnections: T[] }; export type TGetAppConnection = { appConnection: T }; @@ -40,4 +49,6 @@ export type TAppConnectionMap = { [AppConnection.AWS]: TAwsConnection; [AppConnection.GitHub]: TGitHubConnection; [AppConnection.GCP]: TGcpConnection; + [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; + [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 0f9c820b5..e141693b5 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -2,7 +2,9 @@ export enum SecretSync { AWSParameterStore = "aws-parameter-store", AWSSecretsManager = "aws-secrets-manager", GitHub = "github", - GCPSecretManager = "gcp-secret-manager" + GCPSecretManager = "gcp-secret-manager", + AzureKeyVault = "azure-key-vault", + AzureAppConfiguration = "azure-app-configuration" } export enum SecretSyncStatus { 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 new file mode 100644 index 000000000..535d2cb7c --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/azure-app-configuration-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TAzureAppConfigurationSync = TRootSecretSync & { + destination: SecretSync.AzureAppConfiguration; + destinationConfig: { + configurationUrl: string; + label?: string; + }; + connection: { + 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 new file mode 100644 index 000000000..7cdf57cb3 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/azure-key-vault-sync.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TAzureKeyVaultSync = TRootSecretSync & { + destination: SecretSync.AzureKeyVault; + destinationConfig: { + vaultBaseUrl: string; + }; + connection: { + app: AppConnection.AzureKeyVault; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index f7e718a0b..51f60810c 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -4,6 +4,8 @@ import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync"; import { DiscriminativePick } from "@app/types"; import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; +import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync"; +import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TGcpSync } from "./gcp-sync"; export type TSecretSyncOption = { @@ -12,7 +14,13 @@ export type TSecretSyncOption = { canImportSecrets: boolean; }; -export type TSecretSync = TAwsParameterStoreSync | TAwsSecretsManagerSync | TGitHubSync | TGcpSync; +export type TSecretSync = + | TAwsParameterStoreSync + | TAwsSecretsManagerSync + | TGitHubSync + | TGcpSync + | TAzureKeyVaultSync + | TAzureAppConfigurationSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/organization/AppConnections/GithubOauthCallbackPage/GithubOauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/GithubOauthCallbackPage/GithubOauthCallbackPage.tsx deleted file mode 100644 index a71bb1744..000000000 --- a/frontend/src/pages/organization/AppConnections/GithubOauthCallbackPage/GithubOauthCallbackPage.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useEffect } from "react"; -import { useNavigate, useSearch } from "@tanstack/react-router"; - -import { createNotification } from "@app/components/notifications"; -import { ContentLoader } from "@app/components/v2"; -import { ROUTE_PATHS } from "@app/const/routes"; -import { - GitHubConnectionMethod, - TGitHubConnection, - useCreateAppConnection, - useUpdateAppConnection -} from "@app/hooks/api/appConnections"; -import { AppConnection } from "@app/hooks/api/appConnections/enums"; - -type FormData = Pick & { - returnUrl?: string; - connectionId?: string; -}; - -export const GitHubOAuthCallbackPage = () => { - const navigate = useNavigate(); - const search = useSearch({ - from: ROUTE_PATHS.Organization.AppConnections.GithubOauthCallbackPage.id - }); - const updateAppConnection = useUpdateAppConnection(); - const createAppConnection = useCreateAppConnection(); - - const { code, state, installation_id: installationId } = search; - - useEffect(() => { - (async () => { - let formData: FormData; - - try { - formData = JSON.parse(localStorage.getItem("githubConnectionFormData") ?? "{}") as FormData; - } catch { - createNotification({ - type: "error", - text: "Invalid form state, redirecting..." - }); - navigate({ to: "/" }); - return; - } - - // validate state - if (state !== localStorage.getItem("latestCSRFToken")) { - return; - } - - localStorage.removeItem("githubConnectionFormData"); - localStorage.removeItem("latestCSRFToken"); - - const { connectionId, name, description, returnUrl } = formData; - - try { - if (connectionId) { - await updateAppConnection.mutateAsync({ - app: AppConnection.GitHub, - ...(installationId - ? { - connectionId, - credentials: { - code: code as string, - installationId: installationId as string - } - } - : { - connectionId, - credentials: { - code: code as string - } - }) - }); - } else { - await createAppConnection.mutateAsync({ - app: AppConnection.GitHub, - name, - description, - ...(installationId - ? { - method: GitHubConnectionMethod.App, - credentials: { - code: code as string, - installationId: installationId as string - } - } - : { - method: GitHubConnectionMethod.OAuth, - credentials: { - code: code as string - } - }) - }); - } - } catch (e: any) { - createNotification({ - title: `Failed to ${connectionId ? "update" : "add"} GitHub Connection`, - text: e.message, - type: "error" - }); - navigate({ - to: returnUrl ?? "/organization/settings?selectedTab=app-connections" - }); - return; - } - - createNotification({ - text: `Successfully ${connectionId ? "updated" : "added"} GitHub Connection`, - type: "success" - }); - - navigate({ - to: returnUrl ?? "/organization/settings?selectedTab=app-connections" - }); - })(); - }, []); - - return ( -
- -
- ); -}; diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx new file mode 100644 index 000000000..e327a78c7 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/OauthCallbackPage.tsx @@ -0,0 +1,308 @@ +import { useCallback, useEffect, useState } from "react"; +import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; + +import { createNotification } from "@app/components/notifications"; +import { ContentLoader } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; +import { + AzureAppConfigurationConnectionMethod, + AzureKeyVaultConnectionMethod, + GitHubConnectionMethod, + TAzureAppConfigurationConnection, + TAzureKeyVaultConnection, + TGitHubConnection, + useCreateAppConnection, + useUpdateAppConnection +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +type BaseFormData = { + returnUrl?: string; + connectionId?: string; +}; + +type GithubFormData = BaseFormData & Pick; + +type AzureKeyVaultFormData = BaseFormData & + Pick & + Pick; + +type AzureAppConfigurationFormData = BaseFormData & + Pick & + Pick; + +type FormDataMap = { + [AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub }; + [AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault }; + [AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & { + app: AppConnection.AzureAppConfiguration; + }; +}; + +const formDataStorageFieldMap: Partial> = { + [AppConnection.GitHub]: "githubConnectionFormData", + [AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData", + [AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData" +}; + +export const OAuthCallbackPage = () => { + const navigate = useNavigate(); + const [isReady, setIsReady] = useState(false); + + const search = useSearch({ + from: ROUTE_PATHS.Organization.AppConnections.OauthCallbackPage.id + }); + + const rawAppConnection = useParams({ + strict: false, + select: (el) => el?.appConnection as AppConnection + }); + + const updateAppConnection = useUpdateAppConnection(); + const createAppConnection = useCreateAppConnection(); + + 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")) { + throw new Error("Invalid CSRF token"); + } + + const dataFieldName = formDataStorageFieldMap[app]; + + localStorage.removeItem(dataFieldName!); + localStorage.removeItem("latestCSRFToken"); + }; + + const getFormData = (app: T): FormDataMap[T] | null => { + const dataFieldName = formDataStorageFieldMap[app]; + + try { + const rawData = JSON.parse(localStorage.getItem(dataFieldName!) ?? "{}"); + + return { + ...rawData, + app + } as FormDataMap[T]; + } catch { + createNotification({ + type: "error", + text: `Invalid ${app || ""} form state, redirecting...` + }); + navigate({ to: "/" }); + return null; + } + }; + + const handleAzureKeyVault = useCallback(async () => { + const formData = getFormData(AppConnection.AzureKeyVault); + if (formData === null) return null; + + clearState(AppConnection.AzureKeyVault); + + const { connectionId, name, description, returnUrl } = formData; + + try { + if (connectionId) { + await updateAppConnection.mutateAsync({ + app: AppConnection.AzureKeyVault, + connectionId, + credentials: { + code: code as string, + tenantId: formData.tenantId + } + }); + } else { + await createAppConnection.mutateAsync({ + app: AppConnection.AzureKeyVault, + name, + description, + method: AzureKeyVaultConnectionMethod.OAuth, + credentials: { + tenantId: formData.tenantId, + code: code as string + } + }); + } + } catch (err: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} Azure Key Vault Connection`, + text: err?.message, + type: "error" + }); + navigate({ + to: returnUrl ?? "/organization/settings?selectedTab=app-connections" + }); + } + + return { + connectionId, + returnUrl, + appConnectionName: formData.app + }; + }, []); + + 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, + tenantId: formData.tenantId + } + }); + } else { + await createAppConnection.mutateAsync({ + app: AppConnection.AzureAppConfiguration, + name, + description, + method: AzureAppConfigurationConnectionMethod.OAuth, + credentials: { + code: code as string, + tenantId: formData.tenantId + } + }); + } + } catch (err: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} Azure App Configuration 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; + + clearState(AppConnection.GitHub); + + const { connectionId, name, description, returnUrl } = formData; + + try { + if (connectionId) { + await updateAppConnection.mutateAsync({ + app: AppConnection.GitHub, + ...(installationId + ? { + connectionId, + credentials: { + code: code as string, + installationId: installationId as string + } + } + : { + connectionId, + credentials: { + code: code as string + } + }) + }); + } else { + await createAppConnection.mutateAsync({ + app: AppConnection.GitHub, + name, + description, + ...(installationId + ? { + method: GitHubConnectionMethod.App, + credentials: { + code: code as string, + installationId: installationId as string + } + } + : { + method: GitHubConnectionMethod.OAuth, + credentials: { + code: code as string + } + }) + }); + } + } catch (e: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} GitHub Connection`, + text: e.message, + type: "error" + }); + navigate({ + to: returnUrl ?? "/organization/settings?selectedTab=app-connections" + }); + } + + return { + connectionId, + returnUrl, + appConnectionName: formData.app + }; + }, []); + + // Ensure that the localstorage is ready for use, to avoid the form data being malformed + useEffect(() => { + if (!isReady) { + setIsReady(!!localStorage.length); + } + }, [localStorage.length]); + + useEffect(() => { + if (!isReady) return; + + (async () => { + let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null = + null; + + if (appConnection === AppConnection.GitHub) { + data = await handleGithub(); + } else if (appConnection === AppConnection.AzureKeyVault) { + data = await handleAzureKeyVault(); + } else if (appConnection === AppConnection.AzureAppConfiguration) { + data = await handleAzureAppConfiguration(); + } + + if (data) { + createNotification({ + text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName ? APP_CONNECTION_MAP[data.appConnectionName as AppConnection].name : ""} Connection`, + type: "success" + }); + } else { + createNotification({ + text: "Failed to add connection", + type: "error" + }); + } + + await navigate({ + to: data?.returnUrl ?? "/organization/settings?selectedTab=app-connections" + }); + })(); + }, [isReady]); + + return ( +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/GithubOauthCallbackPage/route.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/route.tsx similarity index 81% rename from frontend/src/pages/organization/AppConnections/GithubOauthCallbackPage/route.tsx rename to frontend/src/pages/organization/AppConnections/OauthCallbackPage/route.tsx index a69c27aee..4a4dfa848 100644 --- a/frontend/src/pages/organization/AppConnections/GithubOauthCallbackPage/route.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/route.tsx @@ -2,7 +2,7 @@ import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; -import { GitHubOAuthCallbackPage } from "./GithubOauthCallbackPage"; +import { OAuthCallbackPage } from "./OauthCallbackPage"; const GitHubOAuthCallbackPageQueryParamsSchema = z.object({ code: z.coerce.string().catch(""), @@ -11,9 +11,9 @@ const GitHubOAuthCallbackPageQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback" + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" )({ - component: GitHubOAuthCallbackPage, + component: OAuthCallbackPage, validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema), search: { middlewares: [stripSearchParams({ state: "", installation_id: "" })] 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 99a18a6fc..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,6 +10,8 @@ import { DiscriminativePick } from "@app/types"; import { AppConnectionHeader } from "../AppConnectionHeader"; import { AwsConnectionForm } from "./AwsConnectionForm"; +import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm"; +import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; @@ -53,6 +55,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.GCP: return ; + case AppConnection.AzureKeyVault: + return ; + case AppConnection.AzureAppConfiguration: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -92,6 +98,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.GCP: 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..75449fed3 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureAppConfigurationConnectionForm.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 { + 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, + tenantId: appConnection.credentials.tenantId + } + : { + 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/AzureKeyVaultConnectionForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureKeyVaultConnectionForm.tsx new file mode 100644 index 000000000..58f9739ec --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AzureKeyVaultConnectionForm.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 { useGetAppConnectionOption } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + AzureKeyVaultConnectionMethod, + TAzureKeyVaultConnection +} from "@app/hooks/api/appConnections/types/azure-key-vault-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TAzureKeyVaultConnection; +}; + +const formSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.AzureKeyVault), + method: z.nativeEnum(AzureKeyVaultConnectionMethod), + tenantId: z.string().trim().optional() +}); + +type FormData = z.infer; + +export const AzureKeyVaultConnectionForm = ({ appConnection }: Props) => { + const isUpdate = Boolean(appConnection); + const [isRedirecting, setIsRedirecting] = useState(false); + + const { + option: { oauthClientId }, + isLoading + } = useGetAppConnectionOption(AppConnection.AzureKeyVault); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection + ? { + ...appConnection, + tenantId: appConnection.credentials.tenantId + } + : { + app: AppConnection.AzureKeyVault, + method: AzureKeyVaultConnectionMethod.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( + "azureKeyVaultConnectionFormData", + JSON.stringify({ ...formData, connectionId: appConnection?.id }) + ); + + switch (formData.method) { + 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=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault` + ); + break; + default: + throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`); + } + }; + + let isMissingConfig: boolean; + + switch (selectedMethod) { + case AzureKeyVaultConnectionMethod.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/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureAppConfigurationDestinationSyncCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureAppConfigurationDestinationSyncCol.tsx new file mode 100644 index 000000000..5a80a5b35 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureAppConfigurationDestinationSyncCol.tsx @@ -0,0 +1,14 @@ +import { TAzureAppConfigurationSync } from "@app/hooks/api/secretSyncs/types/azure-app-configuration-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TAzureAppConfigurationSync; +}; + +export const AzureAppConfigurationDestinationSyncCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureKeyVaultDestinationSyncCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureKeyVaultDestinationSyncCol.tsx new file mode 100644 index 000000000..149861d33 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/AzureKeyVaultDestinationSyncCol.tsx @@ -0,0 +1,14 @@ +import { TAzureKeyVaultSync } from "@app/hooks/api/secretSyncs/types/azure-key-vault-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TAzureKeyVaultSync; +}; + +export const AzureKeyVaultDestinationSyncCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 376258bfb..70e8acf4b 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -2,6 +2,8 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol"; import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol"; +import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol"; +import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; @@ -19,6 +21,11 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.GCPSecretManager: return ; + case SecretSync.AzureKeyVault: + return ; + case SecretSync.AzureAppConfiguration: + return ; + default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 647770ee8..7b98c183b 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -47,6 +47,15 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectId; secondaryText = "Global"; break; + case SecretSync.AzureKeyVault: + primaryText = destinationConfig.vaultBaseUrl; + break; + case SecretSync.AzureAppConfiguration: + primaryText = destinationConfig.configurationUrl; + if (destinationConfig.label) { + secondaryText = `Label - ${destinationConfig.label}`; + } + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx new file mode 100644 index 000000000..7db01f7fe --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureAppConfigurationSyncDestinationSection.tsx @@ -0,0 +1,19 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TAzureAppConfigurationSync } from "@app/hooks/api/secretSyncs/types/azure-app-configuration-sync"; + +type Props = { + secretSync: TAzureAppConfigurationSync; +}; + +export const AzureAppConfigurationSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { configurationUrl, label } + } = secretSync; + + return ( + <> + {configurationUrl} + {label} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureKeyVaultSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureKeyVaultSyncDestinationSection.tsx new file mode 100644 index 000000000..4a30e6e08 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AzureKeyVaultSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TAzureKeyVaultSync } from "@app/hooks/api/secretSyncs/types/azure-key-vault-sync"; + +type Props = { + secretSync: TAzureKeyVaultSync; +}; + +export const AzureKeyVaultSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { vaultBaseUrl } + } = secretSync; + + return {vaultBaseUrl}; +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 686f3498c..473ca8ecf 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -13,6 +13,8 @@ import { AwsParameterStoreSyncDestinationSection } from "@app/pages/secret-manag import { AwsSecretsManagerSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/AwsSecretsManagerSyncDestinationSection"; import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/GitHubSyncDestinationSection"; +import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection"; +import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; type Props = { @@ -39,6 +41,15 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.GCPSecretManager: DestinationComponents = ; break; + case SecretSync.AzureKeyVault: + DestinationComponents = ; + break; + case SecretSync.AzureAppConfiguration: + DestinationComponents = ( + + ); + break; + default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index cc89058c3..ca3a45155 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -101,7 +101,7 @@ import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/ import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route' import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route' import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route' -import { Route as organizationAppConnectionsGithubOauthCallbackPageRouteImport } from './pages/organization/AppConnections/GithubOauthCallbackPage/route' +import { Route as organizationAppConnectionsOauthCallbackPageRouteImport } from './pages/organization/AppConnections/OauthCallbackPage/route' import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route' import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route' import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route' @@ -916,10 +916,10 @@ const secretManagerIntegrationsDetailsByIDPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute, } as any) -const organizationAppConnectionsGithubOauthCallbackPageRouteRoute = - organizationAppConnectionsGithubOauthCallbackPageRouteImport.update({ - id: '/app-connections/github/oauth/callback', - path: '/app-connections/github/oauth/callback', +const organizationAppConnectionsOauthCallbackPageRouteRoute = + organizationAppConnectionsOauthCallbackPageRouteImport.update({ + id: '/app-connections/$appConnection/oauth/callback', + path: '/app-connections/$appConnection/oauth/callback', getParentRoute: () => AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) @@ -2139,11 +2139,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport parentRoute: typeof certManagerLayoutImport } - '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback' - path: '/app-connections/github/oauth/callback' - fullPath: '/organization/app-connections/github/oauth/callback' - preLoaderRoute: typeof organizationAppConnectionsGithubOauthCallbackPageRouteImport + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' + path: '/app-connections/$appConnection/oauth/callback' + fullPath: '/organization/app-connections/$appConnection/oauth/callback' + preLoaderRoute: typeof organizationAppConnectionsOauthCallbackPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': { @@ -2851,7 +2851,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationRoleByIDPageRouteRoute: typeof organizationRoleByIDPageRouteRoute organizationSecretManagerOverviewPageRouteRoute: typeof organizationSecretManagerOverviewPageRouteRoute organizationSshOverviewPageRouteRoute: typeof organizationSshOverviewPageRouteRoute - organizationAppConnectionsGithubOauthCallbackPageRouteRoute: typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute + organizationAppConnectionsOauthCallbackPageRouteRoute: typeof organizationAppConnectionsOauthCallbackPageRouteRoute } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren = @@ -2882,8 +2882,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationSecretManagerOverviewPageRouteRoute, organizationSshOverviewPageRouteRoute: organizationSshOverviewPageRouteRoute, - organizationAppConnectionsGithubOauthCallbackPageRouteRoute: - organizationAppConnectionsGithubOauthCallbackPageRouteRoute, + organizationAppConnectionsOauthCallbackPageRouteRoute: + organizationAppConnectionsOauthCallbackPageRouteRoute, } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren = @@ -3576,7 +3576,7 @@ export interface FileRoutesByFullPath { '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute - '/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute + '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute @@ -3742,7 +3742,7 @@ export interface FileRoutesByTo { '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute - '/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute + '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute @@ -3923,7 +3923,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute @@ -4096,7 +4096,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/access-management' | '/secret-manager/$projectId/integrations/' | '/cert-manager/$projectId/ca/$caId' - | '/organization/app-connections/github/oauth/callback' + | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' | '/secret-manager/$projectId/secrets/$envSlug' @@ -4261,7 +4261,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/access-management' | '/secret-manager/$projectId/integrations' | '/cert-manager/$projectId/ca/$caId' - | '/organization/app-connections/github/oauth/callback' + | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' | '/secret-manager/$projectId/secrets/$envSlug' @@ -4440,7 +4440,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId' - | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback' + | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug' @@ -4767,7 +4767,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId", "/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview", "/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview", - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback" + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" ] }, "/_authenticate/_inject-org-details/admin/_admin-layout": { @@ -5117,8 +5117,8 @@ export const routeTree = rootRoute "filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/github/oauth/callback": { - "filePath": "organization/AppConnections/GithubOauthCallbackPage/route.tsx", + "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback": { + "filePath": "organization/AppConnections/OauthCallbackPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId": { @@ -5491,4 +5491,4 @@ export const routeTree = rootRoute } } } -ROUTE_MANIFEST_END */ \ No newline at end of file +ROUTE_MANIFEST_END */ diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 5a7f3645b..8cdd4087c 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -24,9 +24,10 @@ const organizationRoutes = route("/organization", [ route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"), route("/roles/$roleId", "organization/RoleByIDPage/route.tsx"), route("/identities/$identityId", "organization/IdentityDetailsByIDPage/route.tsx"), + route( - "/app-connections/github/oauth/callback", - "organization/AppConnections/GithubOauthCallbackPage/route.tsx" + "/app-connections/$appConnection/oauth/callback", + "organization/AppConnections/OauthCallbackPage/route.tsx" ) ]);