feat(secret-syncs): azure app config & key vault support

This commit is contained in:
Daniel Hougaard
2025-02-06 04:18:09 +04:00
parent 4e41e84491
commit 5cd6a66989
70 changed files with 2065 additions and 204 deletions

View File

@@ -849,7 +849,8 @@ export const registerRoutes = async (
secretVersionTagDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
resourceMetadataDAL,
appConnectionDAL
});
const secretQueueService = secretQueueFactory({

View File

@@ -8,6 +8,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
import { AzureResources } from "@app/services/app-connection/azure";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
@@ -73,7 +74,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
description: `List the ${appName} Connections the current user has permission to establish connections with.`,
response: {
200: z.object({
appConnections: z.object({ app: z.literal(app), name: z.string(), id: z.string().uuid() }).array()
appConnections: z
.object({
app: z.literal(app),
name: z.string(),
id: z.string().uuid(),
azureResource: z.nativeEnum(AzureResources).optional()
})
.array()
})
}
},

View File

@@ -4,6 +4,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import { AzureConnectionListItemSchema, SanitizedAzureConnectionSchema } from "@app/services/app-connection/azure";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -12,13 +13,15 @@ import { AuthMode } from "@app/services/auth/auth-type";
const SanitizedAppConnectionSchema = z.union([
...SanitizedAwsConnectionSchema.options,
...SanitizedGitHubConnectionSchema.options,
...SanitizedGcpConnectionSchema.options
...SanitizedGcpConnectionSchema.options,
...SanitizedAzureConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AwsConnectionListItemSchema,
GitHubConnectionListItemSchema,
GcpConnectionListItemSchema
GcpConnectionListItemSchema,
AzureConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,20 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateAzureConnectionSchema,
SanitizedAzureConnectionSchema,
UpdateAzureConnectionSchema
} from "@app/services/app-connection/azure";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerAzureConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Azure,
server,
sanitizedResponseSchema: SanitizedAzureConnectionSchema,
createSchema: CreateAzureConnectionSchema,
updateSchema: UpdateAzureConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
};

View File

@@ -1,6 +1,7 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureConnectionRouter } from "./azure-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
@@ -10,5 +11,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
{
[AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter,
[AppConnection.GCP]: registerGcpConnectionRouter
[AppConnection.GCP]: registerGcpConnectionRouter,
[AppConnection.Azure]: registerAzureConnectionRouter
};

View File

@@ -0,0 +1,17 @@
import {
AzureAppConfigurationSyncSchema,
CreateAzureAppConfigurationSyncSchema,
UpdateAzureAppConfigurationSyncSchema
} from "@app/services/secret-sync/azure-app-configuration";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerAzureAppConfigurationSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.AzureAppConfiguration,
server,
responseSchema: AzureAppConfigurationSyncSchema,
createSchema: CreateAzureAppConfigurationSyncSchema,
updateSchema: UpdateAzureAppConfigurationSyncSchema
});

View File

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

View File

@@ -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<SecretSync, (server: Fastif
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
[SecretSync.AWSSecretsManager]: registerAwsSecretsManagerSyncRouter,
[SecretSync.GitHub]: registerGitHubSyncRouter,
[SecretSync.GCPSecretManager]: registerGcpSyncRouter
[SecretSync.GCPSecretManager]: registerGcpSyncRouter,
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter
};

View File

@@ -13,6 +13,11 @@ import {
AwsSecretsManagerSyncListItemSchema,
AwsSecretsManagerSyncSchema
} from "@app/services/secret-sync/aws-secrets-manager";
import {
AzureAppConfigurationSyncListItemSchema,
AzureAppConfigurationSyncSchema
} from "@app/services/secret-sync/azure-app-configuration";
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
@@ -20,14 +25,18 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema,
AwsSecretsManagerSyncSchema,
GitHubSyncSchema,
GcpSyncSchema
GcpSyncSchema,
AzureKeyVaultSyncSchema,
AzureAppConfigurationSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncListItemSchema,
AwsSecretsManagerSyncListItemSchema,
GitHubSyncListItemSchema,
GcpSyncListItemSchema
GcpSyncListItemSchema,
AzureKeyVaultSyncListItemSchema,
AzureAppConfigurationSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -1,7 +1,8 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws",
GCP = "gcp"
GCP = "gcp",
Azure = "azure"
}
export enum AWSRegion {

View File

@@ -20,10 +20,15 @@ import {
} from "@app/services/app-connection/github";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { AzureConnectionMethod, getAzureConnectionListItem, validateAzureConnectionCredentials } from "./azure";
export const listAppConnectionOptions = () => {
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) =>
a.name.localeCompare(b.name)
);
return [
getAwsAppConnectionListItem(),
getGitHubConnectionListItem(),
getGcpAppConnectionListItem(),
getAzureConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
export const encryptAppConnectionCredentials = async ({
@@ -79,6 +84,8 @@ export const validateAppConnectionCredentials = async (
return validateGitHubConnectionCredentials(appConnection);
case AppConnection.GCP:
return validateGcpConnectionCredentials(appConnection);
case AppConnection.Azure:
return validateAzureConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
@@ -89,6 +96,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
switch (method) {
case GitHubConnectionMethod.App:
return "GitHub App";
case AzureConnectionMethod.OAuth:
case GitHubConnectionMethod.OAuth:
return "OAuth";
case AwsConnectionMethod.AccessKey:

View File

@@ -3,5 +3,6 @@ import { AppConnection } from "./app-connection-enums";
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AWS]: "AWS",
[AppConnection.GitHub]: "GitHub",
[AppConnection.GCP]: "GCP"
[AppConnection.GCP]: "GCP",
[AppConnection.Azure]: "Azure"
};

View File

@@ -27,7 +27,9 @@ import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-con
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TAppConnectionDALFactory } from "./app-connection-dal";
import { AzureResources } from "./azure";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service";
@@ -42,7 +44,8 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
[AppConnection.Azure]: ValidateGcpConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -347,7 +350,27 @@ export const appConnectionServiceFactory = ({
)
);
return availableConnections as Omit<TAppConnection, "credentials">[];
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actor.orgId
});
const decryptedConnections = availableConnections.map((connection) => {
const decryptedPlainTextBlob = decryptor({
cipherTextBlob: connection.encryptedCredentials
});
const credentials = JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
return {
...connection,
...(app === AppConnection.Azure && {
azureResource: (credentials as { resource: AzureResources }).resource
})
};
});
return decryptedConnections as Omit<TAppConnection, "credentials">[];
};
return {

View File

@@ -11,11 +11,22 @@ import {
TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
import {
TAzureConnection,
TAzureConnectionConfig,
TAzureConnectionInput,
TValidateAzureConnectionCredentials
} from "./azure";
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection);
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection | TAzureConnection);
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput);
export type TAppConnectionInput = { id: string } & (
| TAwsConnectionInput
| TGitHubConnectionInput
| TGcpConnectionInput
| TAzureConnectionInput
);
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
@@ -26,9 +37,14 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
connectionId: string;
};
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
export type TAppConnectionConfig =
| TAwsConnectionConfig
| TGitHubConnectionConfig
| TGcpConnectionConfig
| TAzureConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
| TValidateGitHubConnectionCredentials
| TValidateGcpConnectionCredentials;
| TValidateGcpConnectionCredentials
| TValidateAzureConnectionCredentials;

View File

@@ -0,0 +1,8 @@
export enum AzureConnectionMethod {
OAuth = "oauth"
}
export enum AzureResources {
KeyVault = "key-vault",
AppConfiguration = "app-configuration"
}

View File

@@ -0,0 +1,182 @@
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 { AzureConnectionMethod, AzureResources } from "./azure-connection-enums";
import { TAzureConnectionConfig, TAzureConnectionCredentials } from "./azure-connection-types";
const resourceScopes: Record<AzureResources, string> = {
[AzureResources.AppConfiguration]: "https://azconfig.io/.default",
[AzureResources.KeyVault]: "https://vault.azure.net/.default"
};
export const getAzureConnectionAccessToken = async (
connectionId: string,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const appCfg = getConfig();
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) {
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
}
if (appConnection.app !== AppConnection.Azure) {
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure connection` });
}
const credentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId,
kmsService,
encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureConnectionCredentials;
const { data } = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
new URLSearchParams({
grant_type: "refresh_token",
scope: `openid offline_access`,
client_id: appCfg.CLIENT_ID_AZURE!,
client_secret: appCfg.CLIENT_SECRET_AZURE!,
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 getAzureConnectionListItem = () => {
const { CLIENT_ID_AZURE } = getConfig();
return {
name: "Azure" as const,
app: AppConnection.Azure as const,
methods: Object.values(AzureConnectionMethod) as [AzureConnectionMethod.OAuth],
oauthClientId: CLIENT_ID_AZURE
};
};
type ExchangeCodeAzureResponse = {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
};
export const validateAzureConnectionCredentials = async (config: TAzureConnectionConfig) => {
const { credentials: inputCredentials, method } = config;
const { CLIENT_ID_AZURE, CLIENT_SECRET_AZURE } = getConfig();
if (!CLIENT_ID_AZURE || !CLIENT_SECRET_AZURE) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
let tokenError: AxiosError | null = null;
try {
const appCfg = getConfig();
if (!appCfg.CLIENT_ID_AZURE || !appCfg.CLIENT_SECRET_AZURE) {
throw new BadRequestError({ message: "Missing client id and client secret" });
}
tokenResp = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
new URLSearchParams({
grant_type: "authorization_code",
code: inputCredentials.code,
scope: `openid offline_access ${resourceScopes[inputCredentials.resource]}`,
client_id: appCfg.CLIENT_ID_AZURE,
client_secret: appCfg.CLIENT_SECRET_AZURE,
redirect_uri: `${appCfg.SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
// TODO(daniel): handle token refreshing
} 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 AzureConnectionMethod.OAuth:
return {
accessToken: tokenResp.data.access_token,
refreshToken: tokenResp.data.refresh_token,
expiresAt: Date.now() + tokenResp.data.expires_in * 1000,
resource: inputCredentials.resource
};
default:
throw new InternalServerError({
message: `Unhandled Azure connection method: ${method as AzureConnectionMethod}`
});
}
};

View File

@@ -0,0 +1,74 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { AzureConnectionMethod, AzureResources } from "./azure-connection-enums";
export const AzureConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required"),
tenantId: z.string().trim().optional(),
resource: z.nativeEnum(AzureResources)
});
export const AzureConnectionOAuthOutputCredentialsSchema = z.object({
tenantId: z.string().optional(),
accessToken: z.string(),
refreshToken: z.string(),
expiresAt: z.number(), // unix timestamp,
resource: z.nativeEnum(AzureResources)
});
export const ValidateAzureConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(AzureConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.Azure).method),
credentials: AzureConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Azure).credentials
)
})
]);
export const CreateAzureConnectionSchema = ValidateAzureConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Azure)
);
export const UpdateAzureConnectionSchema = z
.object({
credentials: AzureConnectionOAuthInputCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Azure).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Azure));
const BaseAzureConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Azure) });
export const AzureConnectionSchema = z.intersection(
BaseAzureConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(AzureConnectionMethod.OAuth),
credentials: AzureConnectionOAuthOutputCredentialsSchema
})
])
);
export const SanitizedAzureConnectionSchema = z.discriminatedUnion("method", [
BaseAzureConnectionSchema.extend({
method: z.literal(AzureConnectionMethod.OAuth),
credentials: AzureConnectionOAuthOutputCredentialsSchema.pick({
resource: true
})
})
]);
export const AzureConnectionListItemSchema = z.object({
name: z.literal("Azure"),
app: z.literal(AppConnection.Azure),
methods: z.nativeEnum(AzureConnectionMethod).array(),
oauthClientId: z.string().optional()
});

View File

@@ -0,0 +1,35 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
AzureConnectionOAuthOutputCredentialsSchema,
AzureConnectionSchema,
CreateAzureConnectionSchema,
ValidateAzureConnectionCredentialsSchema
} from "./azure-connection-schemas";
export type TAzureConnection = z.infer<typeof AzureConnectionSchema>;
export type TAzureConnectionInput = z.infer<typeof CreateAzureConnectionSchema> & {
app: AppConnection.Azure;
};
export type TValidateAzureConnectionCredentials = typeof ValidateAzureConnectionCredentialsSchema;
export type TAzureConnectionConfig = DiscriminativePick<TAzureConnectionInput, "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 TAzureConnectionCredentials = z.infer<typeof AzureConnectionOAuthOutputCredentialsSchema>;

View File

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

View File

@@ -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.Azure,
canImportSecrets: false
};

View File

@@ -0,0 +1,195 @@
/* 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";
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<TAppConnectionDALFactory, "findById" | "update">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
interface AzureAppConfigKeyValue {
key: string;
value: 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 azureAppConfigSecrets = Object.fromEntries(
(
await $getCompleteAzureAppConfigValues(
accessToken,
secretSync.destinationConfig.configurationUrl,
azureAppConfigValuesUrl
)
).map((entry) => [entry.key, entry.value])
);
// add the secrets to azure app config, that are in infisical
for await (const key of Object.keys(secretMap)) {
if (!(key in azureAppConfigSecrets) || secretMap[key]?.value !== azureAppConfigSecrets[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
})
}
);
}
}
// delete the secrets that are in azure app config, but not in infisical
for await (const key of Object.keys(azureAppConfigSecrets)) {
if (!(key in secretMap) || secretMap[key] === null) {
await $deleteAzureSecret(
accessToken,
secretSync.destinationConfig.configurationUrl,
key,
secretSync.destinationConfig.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
};
};

View File

@@ -0,0 +1,46 @@
import { z } from "zod";
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"),
label: z.string().optional()
});
const AzureAppConfigurationSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
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.Azure),
destination: z.literal(SecretSync.AzureAppConfiguration),
canImportSecrets: z.literal(false)
});

View File

@@ -0,0 +1,19 @@
import { z } from "zod";
import { TAzureConnection } from "@app/services/app-connection/azure";
import {
AzureAppConfigurationSyncListItemSchema,
AzureAppConfigurationSyncSchema,
CreateAzureAppConfigurationSyncSchema
} from "./azure-app-configuration-sync-schemas";
export type TAzureAppConfigurationSync = z.infer<typeof AzureAppConfigurationSyncSchema>;
export type TAzureAppConfigurationSyncInput = z.infer<typeof CreateAzureAppConfigurationSyncSchema>;
export type TAzureAppConfigurationSyncListItem = z.infer<typeof AzureAppConfigurationSyncListItemSchema>;
export type TAzureAppConfigurationSyncWithCredentials = TAzureAppConfigurationSync & {
connection: TAzureConnection;
};

View File

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

View File

@@ -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.Azure,
canImportSecrets: false
};

View File

@@ -0,0 +1,246 @@
/* 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";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types";
type TAzureKeyVaultSecretSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
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>(
`${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<string, GetAzureKeyVaultSecret>
);
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 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) {
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;
}
}
}
}
};
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
};
};

View File

@@ -0,0 +1,45 @@
import { z } from "zod";
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().min(1, "Vault base URL required")
});
const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
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.Azure),
destination: z.literal(SecretSync.AzureKeyVault),
canImportSecrets: z.literal(false)
});

View File

@@ -0,0 +1,35 @@
import { z } from "zod";
import { TAzureConnection } from "@app/services/app-connection/azure";
import {
AzureKeyVaultSyncListItemSchema,
AzureKeyVaultSyncSchema,
CreateAzureKeyVaultSyncSchema
} from "./azure-key-vault-sync-schemas";
export type TAzureKeyVaultSync = z.infer<typeof AzureKeyVaultSyncSchema>;
export type TAzureKeyVaultSyncInput = z.infer<typeof CreateAzureKeyVaultSyncSchema>;
export type TAzureKeyVaultSyncListItem = z.infer<typeof AzureKeyVaultSyncListItemSchema>;
export type TAzureKeyVaultSyncWithCredentials = TAzureKeyVaultSync & {
connection: TAzureConnection;
};
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;
}

View File

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

View File

@@ -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 {

View File

@@ -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, TSecretSyncListItem> = {
[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<TAppConnectionDALFactory, "findById" | "update">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// let secretMap = { ...unprocessedSecretMap };
//
@@ -72,9 +86,23 @@ export const listSecretSyncOptions = () => {
// };
export const SecretSyncFns = {
syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
syncSecrets: (
secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap);
const azureKeyVaultSecretSync = azureKeyVaultSecretSyncFactory({
appConnectionDAL,
kmsService
});
const azureAppConfigurationSecretSync = azureAppConfigurationSecretSyncFactory({
appConnectionDAL,
kmsService
});
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
@@ -84,13 +112,25 @@ export const SecretSyncFns = {
return GithubSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSecretSync.syncSecrets(secretSync, secretMap);
case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSecretSync.syncSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
);
}
},
getSecrets: async (secretSync: TSecretSyncWithCredentials): Promise<TSecretMap> => {
getSecrets: async (
secretSync: TSecretSyncWithCredentials,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<TSecretMap> => {
const azureKeyVaultSecretSync = azureKeyVaultSecretSyncFactory({
appConnectionDAL,
kmsService
});
let secretMap: TSecretMap;
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
@@ -105,6 +145,10 @@ export const SecretSyncFns = {
case SecretSync.GCPSecretManager:
secretMap = await GcpSyncFns.getSecrets(secretSync);
break;
case SecretSync.AzureKeyVault:
secretMap = await azureKeyVaultSecretSync.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -114,9 +158,18 @@ export const SecretSyncFns = {
return secretMap;
// return stripAffixes(secretSync, secretMap);
},
removeSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
removeSecrets: (
secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap);
const azureKeyVaultSecretSync = azureKeyVaultSecretSyncFactory({
appConnectionDAL,
kmsService
});
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
@@ -126,6 +179,8 @@ export const SecretSyncFns = {
return GithubSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSecretSync.removeSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -5,12 +5,16 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[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, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS,
[SecretSync.AWSSecretsManager]: AppConnection.AWS,
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCPSecretManager]: AppConnection.GCP
[SecretSync.GCPSecretManager]: AppConnection.GCP,
[SecretSync.AzureKeyVault]: AppConnection.Azure,
[SecretSync.AzureAppConfiguration]: AppConnection.Azure
};

View File

@@ -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<typeof secretSyncQueueFactory>;
type TSecretSyncQueueFactoryDep = {
queueService: Pick<TQueueServiceFactory, "queue" | "start">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
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;

View File

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

View File

@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -8,22 +9,28 @@ import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import {
TAvailableAppConnection,
useListAvailableAppConnections
} from "@app/hooks/api/appConnections";
import { TSecretSyncForm } from "./schemas";
type Props = {
onChange?: VoidFunction;
filterConnections?: (
connections?: TAvailableAppConnection[]
) => TAvailableAppConnection[] | undefined;
};
export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
export const SecretSyncConnectionField = ({ onChange: callback, filterConnections }: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretSyncForm>();
const destination = watch("destination");
const app = SECRET_SYNC_CONNECTION_MAP[destination];
const { data: options, isLoading } = useListAvailableAppConnections(app);
const { data: allConnections, isLoading } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
@@ -32,6 +39,10 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
OrgPermissionSubjects.AppConnections
);
const availableConnections = useMemo(() => {
return filterConnections ? filterConnections(allConnections) : allConnections;
}, [allConnections]);
const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name;
return (
@@ -55,7 +66,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 +76,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
control={control}
name="connection"
/>
{options?.length === 0 && (
{availableConnections?.length === 0 && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (

View File

@@ -0,0 +1,64 @@
import { Controller, useFormContext } from "react-hook-form";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input } from "@app/components/v2";
import { AzureResources } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const AzureAppConfigurationSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration }
>();
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.configurationUrl", "");
}}
filterConnections={(connections) => {
if (!connections) return connections;
return connections.filter(
(connection) =>
connection.app === AppConnection.Azure &&
connection.azureResource === AzureResources.AppConfiguration
);
}}
/>
<Controller
name="destinationConfig.configurationUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault Base URL"
tooltipText="Enter your Azure App Configuration URL. This is the base URL for your Azure App Configuration, e.g. https://resource-name-here.azconfig.io."
>
<Input {...field} placeholder="https://resource-name-here.azconfig.io" />
</FormControl>
)}
/>
<Controller
name="destinationConfig.label"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Label"
isOptional
tooltipText="Enter the label for the secret in Azure App Configuration."
>
<Input {...field} placeholder="infisical-secret" />
</FormControl>
)}
/>
</>
);
};

View File

@@ -0,0 +1,48 @@
import { Controller, useFormContext } from "react-hook-form";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input } from "@app/components/v2";
import { AzureResources } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const AzureKeyVaultSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureKeyVault }
>();
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.vaultBaseUrl", "");
}}
filterConnections={(connections) => {
if (!connections) return connections;
return connections.filter(
(connection) =>
connection.app === AppConnection.Azure &&
connection.azureResource === AzureResources.KeyVault
);
}}
/>
<Controller
name="destinationConfig.vaultBaseUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault Base URL"
tooltipText="Enter your Azure Key Vault URL. This is the base URL for your Azure Key Vault, e.g. https://example.vault.azure.net."
>
<Input {...field} placeholder="https://example.vault.azure.net" />
</FormControl>
)}
/>
</>
);
};

View File

@@ -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 <GitHubSyncFields />;
case SecretSync.GCPSecretManager:
return <GcpSyncFields />;
case SecretSync.AzureKeyVault:
return <AzureKeyVaultSyncFields />;
case SecretSync.AzureAppConfiguration:
return <AzureAppConfigurationSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}

View File

@@ -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 (
<>
<SecretSyncLabel label="Configuration URL">{vaultBaseUrl}</SecretSyncLabel>
<SecretSyncLabel label="Label">{label}</SecretSyncLabel>
</>
);
};

View File

@@ -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<TSecretSyncForm & { destination: SecretSync.AzureKeyVault }>();
const vaultBaseUrl = watch("destinationConfig.vaultBaseUrl");
return <SecretSyncLabel label="Vault URL">{vaultBaseUrl}</SecretSyncLabel>;
};

View File

@@ -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 = <GcpSyncReviewFields />;
break;
case SecretSync.AzureKeyVault:
DestinationFieldsComponent = <AzureKeyVaultSyncReviewFields />;
break;
case SecretSync.AzureAppConfiguration:
DestinationFieldsComponent = <AzureAppConfigurationSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

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

View File

@@ -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().min(1, "Vault Base URL required")
})
});

View File

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

View File

@@ -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"
)
}
},

View File

@@ -15,7 +15,8 @@ export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: st
[AppConnection.GCP]: {
name: "GCP",
image: "Google Cloud Platform.png"
}
},
[AppConnection.Azure]: { name: "Azure", image: "Microsoft Azure.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {

View File

@@ -9,14 +9,21 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
[SecretSync.AWSSecretsManager]: { name: "AWS Secrets Manager", image: "Amazon Web Services.png" },
[SecretSync.GitHub]: { name: "GitHub", image: "GitHub.png" },
[SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" }
[SecretSync.GCPSecretManager]: { name: "GCP Secret Manager", image: "Google Cloud Platform.png" },
[SecretSync.AzureKeyVault]: { name: "Azure Key Vault", image: "Microsoft Azure.png" },
[SecretSync.AzureAppConfiguration]: {
name: "Azure App Configuration",
image: "Microsoft Azure.png"
}
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS,
[SecretSync.AWSSecretsManager]: AppConnection.AWS,
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCPSecretManager]: AppConnection.GCP
[SecretSync.GCPSecretManager]: AppConnection.GCP,
[SecretSync.AzureKeyVault]: AppConnection.Azure,
[SecretSync.AzureAppConfiguration]: AppConnection.Azure
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<

View File

@@ -1,5 +1,6 @@
export enum AppConnection {
AWS = "aws",
GitHub = "github",
GCP = "gcp"
GCP = "gcp",
Azure = "azure"
}

View File

@@ -20,10 +20,16 @@ export type TGcpConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.GCP;
};
export type TAzureConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Azure;
oauthClientId?: string;
};
export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
[AppConnection.GitHub]: TGitHubConnectionOption;
[AppConnection.GCP]: TGcpConnectionOption;
[AppConnection.Azure]: TAzureConnectionOption;
};

View File

@@ -0,0 +1,26 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum AzureConnectionMethod {
OAuth = "oauth"
}
export enum AzureResources {
KeyVault = "key-vault",
AppConfiguration = "app-configuration"
}
export const azureResourcesMap: Record<AzureResources, string> = {
[AzureResources.AppConfiguration]: "App Configuration",
[AzureResources.KeyVault]: "Key Vault"
};
export type TAzureConnection = TRootAppConnection & { app: AppConnection.Azure } & {
method: AzureConnectionMethod.OAuth;
resource: AzureResources;
credentials: {
code: string;
tenantId?: string;
resource: AzureResources;
};
};

View File

@@ -3,15 +3,22 @@ import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-op
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
import { AzureResources, TAzureConnection } from "./azure-connection";
import { TGcpConnection } from "./gcp-connection";
export * from "./aws-connection";
export * from "./azure-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection;
export type TAppConnection = TAwsConnection | TGitHubConnection | TGcpConnection | TAzureConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "app" | "id">;
export type TAvailableAppConnection =
| (Pick<TAppConnection, "name" | "id"> & { app: Exclude<AppConnection, AppConnection.Azure> })
| (Pick<TAppConnection, "name" | "id"> & {
app: AppConnection.Azure;
azureResource?: AzureResources;
});
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
@@ -40,4 +47,5 @@ export type TAppConnectionMap = {
[AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GCP]: TGcpConnection;
[AppConnection.Azure]: TAzureConnection;
};

View File

@@ -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 {

View File

@@ -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.Azure;
name: string;
id: string;
};
};

View File

@@ -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.Azure;
name: string;
id: string;
};
};

View File

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

View File

@@ -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<TGitHubConnection, "name" | "method" | "description"> & {
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 (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader text="Please wait! Authentication in process." />
</div>
);
};

View File

@@ -0,0 +1,240 @@
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 {
AzureConnectionMethod,
AzureResources,
GitHubConnectionMethod,
TGitHubConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type GithubFormData = Pick<TGitHubConnection, "name" | "method" | "description"> & {
returnUrl?: string;
connectionId?: string;
};
type AzureFormData = Pick<TGitHubConnection, "name" | "method" | "description"> & {
returnUrl?: string;
connectionId?: string;
tenantId?: string;
resource: AzureResources;
};
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.Azure]: AzureFormData & { app: AppConnection.Azure };
};
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.Azure]: "azureConnectionFormData"
};
export const OAuthCallbackPage = () => {
const navigate = useNavigate();
const [isReady, setIsReady] = useState(false);
const search = useSearch({
from: ROUTE_PATHS.Organization.AppConnections.OauthCallbackPage.id
});
const appConnection = useParams({
strict: false,
select: (el) => el?.appConnection as AppConnection
});
const updateAppConnection = useUpdateAppConnection();
const createAppConnection = useCreateAppConnection();
const { code, state, installation_id: installationId } = search;
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 = <T extends keyof FormDataMap>(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 handleAzure = useCallback(async () => {
const formData = getFormData(AppConnection.Azure);
if (formData === null) return null;
clearState(AppConnection.Azure);
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.Azure,
connectionId,
credentials: {
code: code as string
}
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.Azure,
name,
description,
method: AzureConnectionMethod.OAuth,
credentials: {
resource: formData.resource,
tenantId: formData.tenantId,
code: code as string
}
});
}
} catch (err: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} Azure Connection`,
text: err?.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/settings?selectedTab=app-connections"
});
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
}, []);
const handleGithub = useCallback(async () => {
const formData = getFormData(AppConnection.GitHub);
if (formData === null) return null;
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.Azure) {
data = await handleAzure();
}
if (data) {
createNotification({
text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName || ""} Connection`,
type: "success"
});
}
await navigate({
to: data?.returnUrl ?? "/organization/settings?selectedTab=app-connections"
});
})();
}, [isReady]);
return (
<div className="flex h-full w-full items-center justify-center">
<ContentLoader text="Please wait! Authentication in process." />
</div>
);
};

View File

@@ -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: "" })]

View File

@@ -10,6 +10,7 @@ import { DiscriminativePick } from "@app/types";
import { AppConnectionHeader } from "../AppConnectionHeader";
import { AwsConnectionForm } from "./AwsConnectionForm";
import { AzureConnectionForm } from "./AzureConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
@@ -53,6 +54,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <GitHubConnectionForm />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.Azure:
return <AzureConnectionForm />;
default:
throw new Error(`Unhandled App ${app}`);
}

View File

@@ -0,0 +1,208 @@
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 {
AzureConnectionMethod,
AzureResources,
TAzureConnection
} from "@app/hooks/api/appConnections/types/azure-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TAzureConnection;
};
const resourceScopes: Record<AzureResources, string> = {
[AzureResources.AppConfiguration]: "https://azconfig.io/.default",
[AzureResources.KeyVault]: "https://vault.azure.net/.default"
};
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Azure),
method: z.nativeEnum(AzureConnectionMethod),
tenantId: z.string().trim().optional(),
resource: z.nativeEnum(AzureResources)
});
type FormData = z.infer<typeof formSchema>;
export const AzureConnectionForm = ({ appConnection }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
const {
option: { oauthClientId },
isLoading
} = useGetAppConnectionOption(AppConnection.Azure);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Azure,
method: AzureConnectionMethod.OAuth,
resource: AzureResources.KeyVault
}
});
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(
"azureConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case AzureConnectionMethod.OAuth:
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=${resourceScopes[formData.resource]}%20openid%20offline_access&state=${state}`
);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
};
let isMissingConfig: boolean;
switch (selectedMethod) {
case AzureConnectionMethod.OAuth:
isMissingConfig = !oauthClientId;
break;
default:
throw new Error(`Unhandled Azure Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="tenantId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="The Azure Active Directory (Entra ID) Tenant ID. This field cannot be changed after creation."
isError={Boolean(error?.message)}
label="Tenant ID"
isOptional
errorText={error?.message}
>
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
</FormControl>
)}
/>
<Controller
name="resource"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="Select the Azure resource you would like the app connection to access."
errorText={error?.message}
isError={Boolean(error?.message)}
label="Resource"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value={AzureResources.KeyVault}>Azure Key Vault</SelectItem>
<SelectItem value={AzureResources.AppConfiguration}>
Azure App Configuration
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Azure].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig
? `Environment variables have not been configured. ${
isInfisicalCloud()
? "Please contact Infisical."
: `See documentation to configure Azure ${methodDetails.name} Connections.`
}`
: error?.message
}
isError={Boolean(error?.message) || isMissingConfig}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(AzureConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to Azure" : "Connect to Azure"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -27,7 +27,8 @@ import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { useToggle } from "@app/hooks";
import { TAppConnection } from "@app/hooks/api/appConnections";
import { azureResourcesMap, TAppConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type Props = {
appConnection: TAppConnection;
@@ -42,7 +43,7 @@ export const AppConnectionRow = ({
onEditCredentials,
onEditDetails
}: Props) => {
const { id, name, method, app, description } = appConnection;
const { id, name, method, app, description, credentials } = appConnection;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -75,7 +76,10 @@ export const AppConnectionRow = ({
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
className="mr-0.5 h-5 w-5"
/>
<span className="hidden lg:inline">{APP_CONNECTION_MAP[app].name}</span>
<span className="hidden lg:inline">
{APP_CONNECTION_MAP[app].name}
{app === AppConnection.Azure && ` ${azureResourcesMap[credentials.resource]}`}
</span>
</div>
</Td>
<Td className="!min-w-[8rem] max-w-0">

View File

@@ -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 <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -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 <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -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 <GitHubSyncDestinationCol secretSync={secretSync} />;
case SecretSync.GCPSecretManager:
return <GcpSyncDestinationCol secretSync={secretSync} />;
case SecretSync.AzureKeyVault:
return <AzureKeyVaultDestinationSyncCol secretSync={secretSync} />;
case SecretSync.AzureAppConfiguration:
return <AzureAppConfigurationDestinationSyncCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

View File

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

View File

@@ -0,0 +1,21 @@
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 (
<>
<SecretSyncLabel label="Configuration URL">{configurationUrl}</SecretSyncLabel>
<SecretSyncLabel label="Label">
{label && label.length > 0 ? label : <span className="opacity-40">Not set</span>}
</SecretSyncLabel>
</>
);
};

View File

@@ -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 <SecretSyncLabel label="Vault URL">{vaultBaseUrl}</SecretSyncLabel>;
};

View File

@@ -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 = <GcpSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.AzureKeyVault:
DestinationComponents = <AzureKeyVaultSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.AzureAppConfiguration:
DestinationComponents = (
<AzureAppConfigurationSyncDestinationSection secretSync={secretSync} />
);
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}

View File

@@ -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 */
ROUTE_MANIFEST_END */

View File

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