mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
feat(secret-syncs): azure app config & key vault support
This commit is contained in:
+10
@@ -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
|
||||
};
|
||||
+195
@@ -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
|
||||
};
|
||||
};
|
||||
+46
@@ -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)
|
||||
});
|
||||
+19
@@ -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;
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user