Compare commits
38 Commits
create-pro
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
c68138ac21 | ||
|
d4f0301104 | ||
|
253c46f21d | ||
|
d8e39aed16 | ||
|
72ee468208 | ||
|
18238b46a7 | ||
|
d0ffae2c10 | ||
|
7ce11cde95 | ||
|
af32948a05 | ||
|
25753fc995 | ||
|
cd71848800 | ||
|
4afc7a1981 | ||
|
11ca76ccca | ||
|
418aca8af0 | ||
|
7365f60835 | ||
|
929822514e | ||
|
616ccb97f2 | ||
|
7917a767e6 | ||
|
ccff675e0d | ||
|
ad905b2ff7 | ||
|
2ada753527 | ||
|
c031736701 | ||
|
91a1c34637 | ||
|
eadb1a63fa | ||
|
f70a1e3db6 | ||
|
fc6ab94a06 | ||
|
4feb3314e7 | ||
|
83e59ae160 | ||
|
d2a4f265de | ||
|
4af872e504 | ||
|
716b88fa49 | ||
|
716f061c01 | ||
|
5af939992c | ||
|
aec4ee905e | ||
|
464e32b0e9 | ||
|
bfd8b64871 | ||
|
185cc4efba | ||
|
7150b9314d |
@@ -145,7 +145,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
|||||||
&& cd openssl-3.1.2 \
|
&& cd openssl-3.1.2 \
|
||||||
&& ./Configure enable-fips \
|
&& ./Configure enable-fips \
|
||||||
&& make \
|
&& make \
|
||||||
&& make install_fips
|
&& make install_fips \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /openssl-build \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
# Install Infisical CLI
|
# Install Infisical CLI
|
||||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||||
@@ -186,12 +190,11 @@ ENV NODE_ENV production
|
|||||||
ENV STANDALONE_BUILD true
|
ENV STANDALONE_BUILD true
|
||||||
ENV STANDALONE_MODE true
|
ENV STANDALONE_MODE true
|
||||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
ENV NODE_OPTIONS="--max-old-space-size=8192 --force-fips"
|
||||||
|
|
||||||
# FIPS mode of operation:
|
# FIPS mode of operation:
|
||||||
ENV OPENSSL_CONF=/backend/nodejs.fips.cnf
|
ENV OPENSSL_CONF=/backend/nodejs.fips.cnf
|
||||||
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
|
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
|
||||||
ENV NODE_OPTIONS=--force-fips
|
|
||||||
ENV FIPS_ENABLED=true
|
ENV FIPS_ENABLED=true
|
||||||
|
|
||||||
|
|
||||||
|
@@ -59,7 +59,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
|||||||
&& cd openssl-3.1.2 \
|
&& cd openssl-3.1.2 \
|
||||||
&& ./Configure enable-fips \
|
&& ./Configure enable-fips \
|
||||||
&& make \
|
&& make \
|
||||||
&& make install_fips
|
&& make install_fips \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /openssl-build \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
# ? App setup
|
# ? App setup
|
||||||
|
|
||||||
|
@@ -0,0 +1,111 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { chunkArray } from "@app/lib/fn";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { TReminders, TRemindersInsert } from "../schemas/reminders";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
logger.info("Initializing secret reminders migration");
|
||||||
|
const hasReminderTable = await knex.schema.hasTable(TableName.Reminder);
|
||||||
|
|
||||||
|
if (hasReminderTable) {
|
||||||
|
const secretsWithLatestVersions = await knex(TableName.SecretV2)
|
||||||
|
.whereNotNull(`${TableName.SecretV2}.reminderRepeatDays`)
|
||||||
|
.whereRaw(`"${TableName.SecretV2}"."reminderRepeatDays" > 0`)
|
||||||
|
.innerJoin(TableName.SecretVersionV2, (qb) => {
|
||||||
|
void qb
|
||||||
|
.on(`${TableName.SecretVersionV2}.secretId`, "=", `${TableName.SecretV2}.id`)
|
||||||
|
.andOn(`${TableName.SecretVersionV2}.reminderRepeatDays`, "=", `${TableName.SecretV2}.reminderRepeatDays`);
|
||||||
|
})
|
||||||
|
.whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => {
|
||||||
|
void qb
|
||||||
|
.select(["v2.secretId", knex.raw("MIN(v2.version) as version")])
|
||||||
|
.from(`${TableName.SecretVersionV2} as v2`)
|
||||||
|
.innerJoin(`${TableName.SecretV2} as s2`, "v2.secretId", "s2.id")
|
||||||
|
.whereRaw(`v2."reminderRepeatDays" = s2."reminderRepeatDays"`)
|
||||||
|
.whereNotNull("v2.reminderRepeatDays")
|
||||||
|
.whereRaw(`v2."reminderRepeatDays" > 0`)
|
||||||
|
.groupBy("v2.secretId");
|
||||||
|
})
|
||||||
|
// Add LEFT JOIN with Reminder table to check for existing reminders
|
||||||
|
.leftJoin(TableName.Reminder, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
|
// Only include secrets that don't already have reminders
|
||||||
|
.whereNull(`${TableName.Reminder}.secretId`)
|
||||||
|
.select(
|
||||||
|
knex.ref("id").withSchema(TableName.SecretV2).as("secretId"),
|
||||||
|
knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"),
|
||||||
|
knex.ref("reminderNote").withSchema(TableName.SecretV2).as("reminderNote"),
|
||||||
|
knex.ref("createdAt").withSchema(TableName.SecretVersionV2).as("createdAt")
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Found ${secretsWithLatestVersions.length} reminders to migrate`);
|
||||||
|
|
||||||
|
const reminderInserts: TRemindersInsert[] = [];
|
||||||
|
if (secretsWithLatestVersions.length > 0) {
|
||||||
|
secretsWithLatestVersions.forEach((secret) => {
|
||||||
|
if (!secret.reminderRepeatDays) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const createdAt = new Date(secret.createdAt);
|
||||||
|
let nextReminderDate = new Date(createdAt);
|
||||||
|
nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays);
|
||||||
|
|
||||||
|
// If the next reminder date is in the past, calculate the proper next occurrence
|
||||||
|
if (nextReminderDate < now) {
|
||||||
|
const daysSinceCreation = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
const daysIntoCurrentCycle = daysSinceCreation % secret.reminderRepeatDays;
|
||||||
|
const daysUntilNextReminder = secret.reminderRepeatDays - daysIntoCurrentCycle;
|
||||||
|
|
||||||
|
nextReminderDate = new Date(now);
|
||||||
|
nextReminderDate.setDate(now.getDate() + daysUntilNextReminder);
|
||||||
|
}
|
||||||
|
|
||||||
|
reminderInserts.push({
|
||||||
|
secretId: secret.secretId,
|
||||||
|
message: secret.reminderNote,
|
||||||
|
repeatDays: secret.reminderRepeatDays,
|
||||||
|
nextReminderDate
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const commitBatches = chunkArray(reminderInserts, 2000);
|
||||||
|
for (const commitBatch of commitBatches) {
|
||||||
|
const insertedReminders = (await knex
|
||||||
|
.batchInsert(TableName.Reminder, commitBatch)
|
||||||
|
.returning("*")) as TReminders[];
|
||||||
|
|
||||||
|
const insertedReminderSecretIds = insertedReminders.map((reminder) => reminder.secretId).filter(Boolean);
|
||||||
|
|
||||||
|
const recipients = await knex(TableName.SecretReminderRecipients)
|
||||||
|
.whereRaw(`??.?? IN (${insertedReminderSecretIds.map(() => "?").join(",")})`, [
|
||||||
|
TableName.SecretReminderRecipients,
|
||||||
|
"secretId",
|
||||||
|
...insertedReminderSecretIds
|
||||||
|
])
|
||||||
|
.select(
|
||||||
|
knex.ref("userId").withSchema(TableName.SecretReminderRecipients).as("userId"),
|
||||||
|
knex.ref("secretId").withSchema(TableName.SecretReminderRecipients).as("secretId")
|
||||||
|
);
|
||||||
|
const reminderRecipients = recipients.map((recipient) => ({
|
||||||
|
reminderId: insertedReminders.find((reminder) => reminder.secretId === recipient.secretId)?.id,
|
||||||
|
userId: recipient.userId
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filteredRecipients = reminderRecipients.filter((recipient) => Boolean(recipient.reminderId));
|
||||||
|
await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients);
|
||||||
|
}
|
||||||
|
logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Secret reminders migration completed");
|
||||||
|
} else {
|
||||||
|
logger.warn("Reminder table does not exist, skipping migration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {
|
||||||
|
logger.info("Rollback not implemented for secret reminders fix migration");
|
||||||
|
}
|
@@ -53,7 +53,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory
|
|||||||
|
|
||||||
let envCfg = Object.freeze(parsedEnv.data);
|
let envCfg = Object.freeze(parsedEnv.data);
|
||||||
|
|
||||||
const fipsEnabled = await crypto.initialize(superAdminDAL);
|
const fipsEnabled = await crypto.initialize(superAdminDAL, envCfg);
|
||||||
|
|
||||||
// Fix for 128-bit entropy encryption key expansion issue:
|
// Fix for 128-bit entropy encryption key expansion issue:
|
||||||
// In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY.
|
// In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY.
|
||||||
|
@@ -2245,7 +2245,9 @@ export const AppConnections = {
|
|||||||
},
|
},
|
||||||
AZURE_CLIENT_SECRETS: {
|
AZURE_CLIENT_SECRETS: {
|
||||||
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
||||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
|
||||||
|
clientId: "The Client ID to use to connect with Azure Client Secrets.",
|
||||||
|
clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
|
||||||
},
|
},
|
||||||
AZURE_DEVOPS: {
|
AZURE_DEVOPS: {
|
||||||
code: "The OAuth code to use to connect with Azure DevOps.",
|
code: "The OAuth code to use to connect with Azure DevOps.",
|
||||||
@@ -2373,6 +2375,10 @@ export const SecretSyncs = {
|
|||||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||||
tags: "Optional tags to add to secrets synced by Infisical.",
|
tags: "Optional tags to add to secrets synced by Infisical.",
|
||||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
||||||
|
},
|
||||||
|
RENDER: {
|
||||||
|
autoRedeployServices:
|
||||||
|
"Whether Infisical should automatically redeploy the configured Render service upon secret changes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DESTINATION_CONFIG: {
|
DESTINATION_CONFIG: {
|
||||||
|
@@ -14,7 +14,7 @@ import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal
|
|||||||
import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service";
|
import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
import { isBase64 } from "../../base64";
|
import { isBase64 } from "../../base64";
|
||||||
import { getConfig } from "../../config/env";
|
import { getConfig, TEnvConfig } from "../../config/env";
|
||||||
import { CryptographyError } from "../../errors";
|
import { CryptographyError } from "../../errors";
|
||||||
import { logger } from "../../logger";
|
import { logger } from "../../logger";
|
||||||
import { asymmetricFipsValidated } from "./asymmetric-fips";
|
import { asymmetricFipsValidated } from "./asymmetric-fips";
|
||||||
@@ -106,12 +106,12 @@ const cryptographyFactory = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const $setFipsModeEnabled = (enabled: boolean) => {
|
const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
|
||||||
// If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key.
|
// If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key.
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
crypto.setFips(true);
|
crypto.setFips(true);
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = envCfg || getConfig();
|
||||||
|
|
||||||
if (appCfg.ENCRYPTION_KEY) {
|
if (appCfg.ENCRYPTION_KEY) {
|
||||||
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
|
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
|
||||||
@@ -141,14 +141,14 @@ const cryptographyFactory = () => {
|
|||||||
$isInitialized = true;
|
$isInitialized = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialize = async (superAdminDAL: TSuperAdminDALFactory) => {
|
const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick<TEnvConfig, "ENCRYPTION_KEY">) => {
|
||||||
if ($isInitialized) {
|
if ($isInitialized) {
|
||||||
return isFipsModeEnabled();
|
return isFipsModeEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.FIPS_ENABLED !== "true") {
|
if (process.env.FIPS_ENABLED !== "true") {
|
||||||
logger.info("Cryptography module initialized in normal operation mode.");
|
logger.info("Cryptography module initialized in normal operation mode.");
|
||||||
$setFipsModeEnabled(false);
|
$setFipsModeEnabled(false, envCfg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +158,11 @@ const cryptographyFactory = () => {
|
|||||||
if (serverCfg) {
|
if (serverCfg) {
|
||||||
if (serverCfg.fipsEnabled) {
|
if (serverCfg.fipsEnabled) {
|
||||||
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
|
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
|
||||||
$setFipsModeEnabled(true);
|
$setFipsModeEnabled(true, envCfg);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
|
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
|
||||||
$setFipsModeEnabled(false);
|
$setFipsModeEnabled(false, envCfg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ const cryptographyFactory = () => {
|
|||||||
// TODO(daniel): check if it's an enterprise deployment
|
// TODO(daniel): check if it's an enterprise deployment
|
||||||
|
|
||||||
// if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true.
|
// if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true.
|
||||||
$setFipsModeEnabled(true);
|
$setFipsModeEnabled(true, envCfg);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import fastifyMultipart from "@fastify/multipart";
|
import fastifyMultipart from "@fastify/multipart";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { VaultMappingType } from "@app/services/external-migration/external-migration-types";
|
||||||
|
|
||||||
const MB25_IN_BYTES = 26214400;
|
const MB25_IN_BYTES = 26214400;
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
bodyLimit: MB25_IN_BYTES,
|
bodyLimit: MB25_IN_BYTES,
|
||||||
url: "/env-key",
|
url: "/env-key",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
@@ -52,4 +54,30 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/vault",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
vaultAccessToken: z.string(),
|
||||||
|
vaultNamespace: z.string().trim().optional(),
|
||||||
|
vaultUrl: z.string(),
|
||||||
|
mappingType: z.nativeEnum(VaultMappingType)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.migration.importVaultData({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -11,5 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
||||||
await server.register(registerExternalMigrationRouter, { prefix: "/migrate" });
|
await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" });
|
||||||
};
|
};
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
export enum AzureClientSecretsConnectionMethod {
|
export enum AzureClientSecretsConnectionMethod {
|
||||||
OAuth = "oauth"
|
OAuth = "oauth",
|
||||||
|
ClientSecret = "client-secret"
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
import { AxiosError, AxiosResponse } from "axios";
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -16,6 +17,7 @@ import { AppConnection } from "../app-connection-enums";
|
|||||||
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
||||||
import {
|
import {
|
||||||
ExchangeCodeAzureResponse,
|
ExchangeCodeAzureResponse,
|
||||||
|
TAzureClientSecretsConnectionClientSecretCredentials,
|
||||||
TAzureClientSecretsConnectionConfig,
|
TAzureClientSecretsConnectionConfig,
|
||||||
TAzureClientSecretsConnectionCredentials
|
TAzureClientSecretsConnectionCredentials
|
||||||
} from "./azure-client-secrets-connection-types";
|
} from "./azure-client-secrets-connection-types";
|
||||||
@@ -26,7 +28,10 @@ export const getAzureClientSecretsConnectionListItem = () => {
|
|||||||
return {
|
return {
|
||||||
name: "Azure Client Secrets" as const,
|
name: "Azure Client Secrets" as const,
|
||||||
app: AppConnection.AzureClientSecrets as const,
|
app: AppConnection.AzureClientSecrets as const,
|
||||||
methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
|
methods: Object.values(AzureClientSecretsConnectionMethod) as [
|
||||||
|
AzureClientSecretsConnectionMethod.OAuth,
|
||||||
|
AzureClientSecretsConnectionMethod.ClientSecret
|
||||||
|
],
|
||||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
) => {
|
) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Azure environment variables have not been configured`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
if (!appConnection) {
|
if (!appConnection) {
|
||||||
@@ -63,34 +62,81 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
|
|
||||||
const { refreshToken } = credentials;
|
const { refreshToken } = credentials;
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Azure OAuth environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 https://graph.microsoft.com/.default`,
|
||||||
|
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
refresh_token: refreshToken
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
const updatedCredentials = {
|
||||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
...credentials,
|
||||||
new URLSearchParams({
|
accessToken: data.access_token,
|
||||||
grant_type: "refresh_token",
|
expiresAt: currentTime + data.expires_in * 1000,
|
||||||
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
refreshToken: data.refresh_token
|
||||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
};
|
||||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
|
||||||
refresh_token: refreshToken
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedCredentials = {
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
...credentials,
|
credentials: updatedCredentials,
|
||||||
accessToken: data.access_token,
|
orgId: appConnection.orgId,
|
||||||
expiresAt: currentTime + data.expires_in * 1000,
|
kmsService
|
||||||
refreshToken: data.refresh_token
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||||
credentials: updatedCredentials,
|
|
||||||
orgId: appConnection.orgId,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
return data.access_token;
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
const accessTokenCredentials = (await decryptAppConnectionCredentials({
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials
|
||||||
|
})) as TAzureClientSecretsConnectionClientSecretCredentials;
|
||||||
|
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = accessTokenCredentials;
|
||||||
|
if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
return data.access_token;
|
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
scope: `https://graph.microsoft.com/.default`,
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedClientCredentials = {
|
||||||
|
...accessTokenCredentials,
|
||||||
|
accessToken: clientData.access_token,
|
||||||
|
expiresAt: currentTime + clientData.expires_in * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: updatedClientCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
|
||||||
|
|
||||||
|
return clientData.access_token;
|
||||||
|
default:
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Unhandled Azure connection method: ${appConnection.method as AzureClientSecretsConnectionMethod}`
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
||||||
@@ -98,69 +144,103 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
|
|||||||
|
|
||||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||||
|
|
||||||
if (!SITE_URL) {
|
|
||||||
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
|
||||||
let tokenError: AxiosError | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
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 https://graph.microsoft.com/.default`,
|
|
||||||
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
|
||||||
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
|
||||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if (e instanceof AxiosError) {
|
|
||||||
tokenError = e;
|
|
||||||
} else {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Unable to validate connection: verify credentials`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenError) {
|
|
||||||
if (tokenError instanceof AxiosError) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Failed to get access token: ${
|
|
||||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
|
||||||
}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: "Failed to get access token"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenResp) {
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: `Failed to get access token: Token was empty with no error`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
if (!SITE_URL) {
|
||||||
|
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||||
|
let tokenError: AxiosError | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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 https://graph.microsoft.com/.default`,
|
||||||
|
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof AxiosError) {
|
||||||
|
tokenError = e;
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection: verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenError) {
|
||||||
|
if (tokenError instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to get access token: ${
|
||||||
|
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get access token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenResp) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Failed to get access token: Token was empty with no error`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tenantId: inputCredentials.tenantId,
|
tenantId: inputCredentials.tenantId,
|
||||||
accessToken: tokenResp.data.access_token,
|
accessToken: tokenResp.data.access_token,
|
||||||
refreshToken: tokenResp.data.refresh_token,
|
refreshToken: tokenResp.data.refresh_token,
|
||||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
const { tenantId, clientId, clientSecret } = inputCredentials;
|
||||||
|
try {
|
||||||
|
const { data: clientData } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
scope: `https://graph.microsoft.com/.default`,
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenantId,
|
||||||
|
accessToken: clientData.access_token,
|
||||||
|
expiresAt: Date.now() + clientData.expires_in * 1000,
|
||||||
|
clientId,
|
||||||
|
clientSecret
|
||||||
|
};
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to get access token: ${
|
||||||
|
(e?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get access token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new InternalServerError({
|
throw new InternalServerError({
|
||||||
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
||||||
|
@@ -26,6 +26,36 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
|
|||||||
expiresAt: z.number()
|
expiresAt: z.number()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z.object({
|
||||||
|
clientId: z
|
||||||
|
.string()
|
||||||
|
.uuid()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client ID required")
|
||||||
|
.max(50, "Client ID must be at most 50 characters long")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId),
|
||||||
|
clientSecret: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client Secret required")
|
||||||
|
.max(50, "Client Secret must be at most 50 characters long")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret),
|
||||||
|
tenantId: z
|
||||||
|
.string()
|
||||||
|
.uuid()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Tenant ID required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
accessToken: z.string(),
|
||||||
|
expiresAt: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
z.object({
|
z.object({
|
||||||
method: z
|
method: z
|
||||||
@@ -34,6 +64,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin
|
|||||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
||||||
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(AzureClientSecretsConnectionMethod.ClientSecret)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method),
|
||||||
|
credentials: AzureClientSecretsConnectionClientSecretInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
||||||
|
)
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -43,9 +81,13 @@ export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecre
|
|||||||
|
|
||||||
export const UpdateAzureClientSecretsConnectionSchema = z
|
export const UpdateAzureClientSecretsConnectionSchema = z
|
||||||
.object({
|
.object({
|
||||||
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
|
credentials: z
|
||||||
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
|
.union([
|
||||||
)
|
AzureClientSecretsConnectionOAuthInputCredentialsSchema,
|
||||||
|
AzureClientSecretsConnectionClientSecretInputCredentialsSchema
|
||||||
|
])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
||||||
|
|
||||||
@@ -59,6 +101,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
|
|||||||
z.object({
|
z.object({
|
||||||
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
||||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||||
|
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -69,6 +115,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
|
|||||||
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
||||||
tenantId: true
|
tenantId: true
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
BaseAzureClientSecretsConnectionSchema.extend({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||||
|
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
|
||||||
|
clientId: true,
|
||||||
|
tenantId: true
|
||||||
|
})
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
|
|||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import {
|
import {
|
||||||
|
AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
|
||||||
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
||||||
AzureClientSecretsConnectionSchema,
|
AzureClientSecretsConnectionSchema,
|
||||||
CreateAzureClientSecretsConnectionSchema,
|
CreateAzureClientSecretsConnectionSchema,
|
||||||
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
|
|||||||
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
|
||||||
|
typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export interface ExchangeCodeAzureResponse {
|
export interface ExchangeCodeAzureResponse {
|
||||||
token_type: string;
|
token_type: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
|
@@ -1,32 +1,26 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
|
||||||
import sjcl from "sjcl";
|
import sjcl from "sjcl";
|
||||||
import tweetnacl from "tweetnacl";
|
import tweetnacl from "tweetnacl";
|
||||||
import tweetnaclUtil from "tweetnacl-util";
|
import tweetnaclUtil from "tweetnacl-util";
|
||||||
|
|
||||||
import { SecretType, TSecretFolders } from "@app/db/schemas";
|
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { chunkArray } from "@app/lib/fn";
|
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|
||||||
|
|
||||||
import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
import { TFolderCommitServiceFactory } from "../../folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { TProjectDALFactory } from "../../project/project-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectServiceFactory } from "../../project/project-service";
|
||||||
import { TProjectServiceFactory } from "../project/project-service";
|
import { TProjectEnvDALFactory } from "../../project-env/project-env-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvServiceFactory } from "../../project-env/project-env-service";
|
||||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
import { TResourceMetadataDALFactory } from "../../resource-metadata/resource-metadata-dal";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TSecretFolderDALFactory } from "../../secret-folder/secret-folder-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderVersionDALFactory } from "../../secret-folder/secret-folder-version-dal";
|
||||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
import { TSecretTagDALFactory } from "../../secret-tag/secret-tag-dal";
|
||||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
import { TSecretV2BridgeDALFactory } from "../../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import type { TSecretV2BridgeServiceFactory } from "../../secret-v2-bridge/secret-v2-bridge-service";
|
||||||
import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
import { TSecretVersionV2DALFactory } from "../../secret-v2-bridge/secret-version-dal";
|
||||||
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
|
import { TSecretVersionV2TagDALFactory } from "../../secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "../external-migration-types";
|
||||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
|
||||||
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
|
|
||||||
|
|
||||||
export type TImportDataIntoInfisicalDTO = {
|
export type TImportDataIntoInfisicalDTO = {
|
||||||
projectDAL: Pick<TProjectDALFactory, "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "transaction">;
|
||||||
@@ -499,326 +493,3 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
|||||||
|
|
||||||
return infisicalImportData;
|
return infisicalImportData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const importDataIntoInfisicalFn = async ({
|
|
||||||
projectService,
|
|
||||||
projectEnvDAL,
|
|
||||||
projectDAL,
|
|
||||||
secretDAL,
|
|
||||||
kmsService,
|
|
||||||
secretVersionDAL,
|
|
||||||
secretTagDAL,
|
|
||||||
secretVersionTagDAL,
|
|
||||||
folderDAL,
|
|
||||||
resourceMetadataDAL,
|
|
||||||
folderVersionDAL,
|
|
||||||
folderCommitService,
|
|
||||||
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
|
||||||
}: TImportDataIntoInfisicalDTO) => {
|
|
||||||
// Import data to infisical
|
|
||||||
if (!data || !data.projects) {
|
|
||||||
throw new BadRequestError({ message: "No projects found in data" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalToNewProjectId = new Map<string, string>();
|
|
||||||
const originalToNewEnvironmentId = new Map<
|
|
||||||
string,
|
|
||||||
{ envId: string; envSlug: string; rootFolderId: string; projectId: string }
|
|
||||||
>();
|
|
||||||
const originalToNewFolderId = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
folderId: string;
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
const projectsNotImported: string[] = [];
|
|
||||||
|
|
||||||
await projectDAL.transaction(async (tx) => {
|
|
||||||
for await (const project of data.projects) {
|
|
||||||
const newProject = await projectService
|
|
||||||
.createProject({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorOrgId,
|
|
||||||
actorAuthMethod,
|
|
||||||
workspaceName: project.name,
|
|
||||||
createDefaultEnvs: false,
|
|
||||||
tx
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error(e, `Failed to import to project [name:${project.name}]`);
|
|
||||||
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` });
|
|
||||||
});
|
|
||||||
originalToNewProjectId.set(project.id, newProject.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import environments
|
|
||||||
if (data.environments) {
|
|
||||||
for await (const environment of data.environments) {
|
|
||||||
const projectId = originalToNewProjectId.get(environment.projectId);
|
|
||||||
const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`);
|
|
||||||
|
|
||||||
if (!projectId) {
|
|
||||||
projectsNotImported.push(environment.projectId);
|
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx);
|
|
||||||
|
|
||||||
if (existingEnv) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Environment with slug '${slug}' already exist`,
|
|
||||||
name: "CreateEnvironment"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
|
||||||
const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
|
|
||||||
const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
|
||||||
|
|
||||||
originalToNewEnvironmentId.set(environment.id, {
|
|
||||||
envSlug: doc.slug,
|
|
||||||
envId: doc.id,
|
|
||||||
rootFolderId: folder.id,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.folders) {
|
|
||||||
for await (const folder of data.folders) {
|
|
||||||
const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string);
|
|
||||||
|
|
||||||
if (!parentEnv) {
|
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFolder = await folderDAL.create(
|
|
||||||
{
|
|
||||||
name: folder.name,
|
|
||||||
envId: parentEnv.envId,
|
|
||||||
parentId: parentEnv.rootFolderId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const newFolderVersion = await folderVersionDAL.create(
|
|
||||||
{
|
|
||||||
name: newFolder.name,
|
|
||||||
envId: newFolder.envId,
|
|
||||||
version: newFolder.version,
|
|
||||||
folderId: newFolder.id
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
await folderCommitService.createCommit(
|
|
||||||
{
|
|
||||||
actor: {
|
|
||||||
type: actor,
|
|
||||||
metadata: {
|
|
||||||
id: actorId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
message: "Changed by external migration",
|
|
||||||
folderId: parentEnv.rootFolderId,
|
|
||||||
changes: [
|
|
||||||
{
|
|
||||||
type: CommitType.ADD,
|
|
||||||
folderVersionId: newFolderVersion.id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
originalToNewFolderId.set(folder.id, {
|
|
||||||
folderId: newFolder.id,
|
|
||||||
projectId: parentEnv.projectId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Useful for debugging:
|
|
||||||
// console.log("data.secrets", data.secrets);
|
|
||||||
// console.log("data.folders", data.folders);
|
|
||||||
// console.log("data.environment", data.environments);
|
|
||||||
|
|
||||||
if (data.secrets && data.secrets.length > 0) {
|
|
||||||
const mappedToEnvironmentId = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
secretKey: string;
|
|
||||||
secretValue: string;
|
|
||||||
folderId?: string;
|
|
||||||
isFromBlock?: boolean;
|
|
||||||
}[]
|
|
||||||
>();
|
|
||||||
|
|
||||||
for (const secret of data.secrets) {
|
|
||||||
const targetId = secret.folderId || secret.environmentId;
|
|
||||||
|
|
||||||
// Skip if we can't find either an environment or folder mapping for this secret
|
|
||||||
if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
|
|
||||||
logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret");
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mappedToEnvironmentId.has(targetId)) {
|
|
||||||
mappedToEnvironmentId.set(targetId, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const alreadyHasSecret = mappedToEnvironmentId
|
|
||||||
.get(targetId)!
|
|
||||||
.find((el) => el.secretKey === secret.name && el.folderId === secret.folderId);
|
|
||||||
|
|
||||||
if (alreadyHasSecret && alreadyHasSecret.isFromBlock) {
|
|
||||||
// remove the existing secret if any
|
|
||||||
mappedToEnvironmentId
|
|
||||||
.get(targetId)!
|
|
||||||
.splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1);
|
|
||||||
}
|
|
||||||
mappedToEnvironmentId.get(targetId)!.push({
|
|
||||||
secretKey: secret.name,
|
|
||||||
secretValue: secret.value || "",
|
|
||||||
folderId: secret.folderId,
|
|
||||||
isFromBlock: secret.appBlockOrderIndex !== undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// for each of the mappedEnvironmentId
|
|
||||||
for await (const [targetId, secrets] of mappedToEnvironmentId) {
|
|
||||||
logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId);
|
|
||||||
|
|
||||||
let selectedFolder: TSecretFolders | undefined;
|
|
||||||
let selectedProjectId: string | undefined;
|
|
||||||
|
|
||||||
// Case 1: Secret belongs to a folder / branch / branch of a block
|
|
||||||
const foundFolder = originalToNewFolderId.get(targetId);
|
|
||||||
if (foundFolder) {
|
|
||||||
logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder");
|
|
||||||
selectedFolder = await folderDAL.findById(foundFolder.folderId, tx);
|
|
||||||
selectedProjectId = foundFolder.projectId;
|
|
||||||
} else {
|
|
||||||
logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment");
|
|
||||||
const environment = data.environments.find((env) => env.id === targetId);
|
|
||||||
if (!environment) {
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
targetId
|
|
||||||
},
|
|
||||||
"[importDataIntoInfisicalFn]: Could not find environment for secret"
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectId = originalToNewProjectId.get(environment.projectId)!;
|
|
||||||
|
|
||||||
if (!projectId) {
|
|
||||||
throw new BadRequestError({ message: `Failed to import secret, project not found` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = originalToNewEnvironmentId.get(targetId);
|
|
||||||
if (!env) {
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
targetId
|
|
||||||
},
|
|
||||||
"[importDataIntoInfisicalFn]: Could not find environment for secret"
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx);
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`,
|
|
||||||
name: "Create secret"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedFolder = folder;
|
|
||||||
selectedProjectId = projectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedFolder) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: `Folder not found for the given environment slug & secret path`,
|
|
||||||
name: "CreateSecret"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedProjectId) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: `Project not found for the given environment slug & secret path`,
|
|
||||||
name: "CreateSecret"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
|
|
||||||
{
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId: selectedProjectId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretBatches = chunkArray(secrets, 2500);
|
|
||||||
for await (const secretBatch of secretBatches) {
|
|
||||||
const secretsByKeys = await secretDAL.findBySecretKeys(
|
|
||||||
selectedFolder.id,
|
|
||||||
secretBatch.map((el) => ({
|
|
||||||
key: el.secretKey,
|
|
||||||
type: SecretType.Shared
|
|
||||||
})),
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
if (secretsByKeys.length) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await fnSecretBulkInsert({
|
|
||||||
inputSecrets: secretBatch.map((el) => {
|
|
||||||
const references = getAllSecretReferences(el.secretValue).nestedReferences;
|
|
||||||
|
|
||||||
return {
|
|
||||||
version: 1,
|
|
||||||
encryptedValue: el.secretValue
|
|
||||||
? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
|
|
||||||
: undefined,
|
|
||||||
key: el.secretKey,
|
|
||||||
references,
|
|
||||||
type: SecretType.Shared
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
folderId: selectedFolder.id,
|
|
||||||
orgId: actorOrgId,
|
|
||||||
resourceMetadataDAL,
|
|
||||||
secretDAL,
|
|
||||||
secretVersionDAL,
|
|
||||||
secretTagDAL,
|
|
||||||
secretVersionTagDAL,
|
|
||||||
folderCommitService,
|
|
||||||
actor: {
|
|
||||||
type: actor,
|
|
||||||
actorId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { projectsNotImported };
|
|
||||||
};
|
|
@@ -0,0 +1,352 @@
|
|||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
|
import { SecretType, TSecretFolders } from "@app/db/schemas";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { chunkArray } from "@app/lib/fn";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { CommitType } from "@app/services/folder-commit/folder-commit-service";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
import { fnSecretBulkInsert, getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
|
|
||||||
|
import { TImportDataIntoInfisicalDTO } from "./envkey";
|
||||||
|
|
||||||
|
export const importDataIntoInfisicalFn = async ({
|
||||||
|
projectService,
|
||||||
|
projectEnvDAL,
|
||||||
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
kmsService,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
folderDAL,
|
||||||
|
resourceMetadataDAL,
|
||||||
|
folderVersionDAL,
|
||||||
|
folderCommitService,
|
||||||
|
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
||||||
|
}: TImportDataIntoInfisicalDTO) => {
|
||||||
|
// Import data to infisical
|
||||||
|
if (!data || !data.projects) {
|
||||||
|
throw new BadRequestError({ message: "No projects found in data" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalToNewProjectId = new Map<string, string>();
|
||||||
|
const originalToNewEnvironmentId = new Map<
|
||||||
|
string,
|
||||||
|
{ envId: string; envSlug: string; rootFolderId?: string; projectId: string }
|
||||||
|
>();
|
||||||
|
const originalToNewFolderId = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
envId: string;
|
||||||
|
envSlug: string;
|
||||||
|
folderId: string;
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
const projectsNotImported: string[] = [];
|
||||||
|
|
||||||
|
await projectDAL.transaction(async (tx) => {
|
||||||
|
for await (const project of data.projects) {
|
||||||
|
const newProject = await projectService
|
||||||
|
.createProject({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
workspaceName: project.name,
|
||||||
|
createDefaultEnvs: false,
|
||||||
|
tx
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(e, `Failed to import to project [name:${project.name}]`);
|
||||||
|
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` });
|
||||||
|
});
|
||||||
|
originalToNewProjectId.set(project.id, newProject.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import environments
|
||||||
|
if (data.environments) {
|
||||||
|
for await (const environment of data.environments) {
|
||||||
|
const projectId = originalToNewProjectId.get(environment.projectId);
|
||||||
|
const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`);
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
projectsNotImported.push(environment.projectId);
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx);
|
||||||
|
|
||||||
|
if (existingEnv) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Environment with slug '${slug}' already exist`,
|
||||||
|
name: "CreateEnvironment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
||||||
|
const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
|
||||||
|
const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
||||||
|
|
||||||
|
originalToNewEnvironmentId.set(environment.id, {
|
||||||
|
envSlug: doc.slug,
|
||||||
|
envId: doc.id,
|
||||||
|
rootFolderId: folder.id,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.folders) {
|
||||||
|
for await (const folder of data.folders) {
|
||||||
|
const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string);
|
||||||
|
const parentFolder = originalToNewFolderId.get(folder.parentFolderId as string);
|
||||||
|
|
||||||
|
let newFolder: TSecretFolders;
|
||||||
|
|
||||||
|
if (parentEnv?.rootFolderId) {
|
||||||
|
newFolder = await folderDAL.create(
|
||||||
|
{
|
||||||
|
name: folder.name,
|
||||||
|
envId: parentEnv.envId,
|
||||||
|
parentId: parentEnv.rootFolderId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} else if (parentFolder) {
|
||||||
|
newFolder = await folderDAL.create(
|
||||||
|
{
|
||||||
|
name: folder.name,
|
||||||
|
envId: parentFolder.envId,
|
||||||
|
parentId: parentFolder.folderId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.info({ folder }, "No parent environment found for folder");
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFolderVersion = await folderVersionDAL.create(
|
||||||
|
{
|
||||||
|
name: newFolder.name,
|
||||||
|
envId: newFolder.envId,
|
||||||
|
version: newFolder.version,
|
||||||
|
folderId: newFolder.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Changed by external migration",
|
||||||
|
folderId: parentEnv?.rootFolderId || parentFolder?.folderId || "",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.ADD,
|
||||||
|
folderVersionId: newFolderVersion.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
originalToNewFolderId.set(folder.id, {
|
||||||
|
folderId: newFolder.id,
|
||||||
|
envId: parentEnv?.envId || parentFolder?.envId || "",
|
||||||
|
envSlug: parentEnv?.envSlug || parentFolder?.envSlug || "",
|
||||||
|
projectId: parentEnv?.projectId || parentFolder?.projectId || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Useful for debugging:
|
||||||
|
// console.log("data.secrets", data.secrets);
|
||||||
|
// console.log("data.folders", data.folders);
|
||||||
|
// console.log("data.environment", data.environments);
|
||||||
|
|
||||||
|
if (data.secrets && data.secrets.length > 0) {
|
||||||
|
const mappedToEnvironmentId = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
secretKey: string;
|
||||||
|
secretValue: string;
|
||||||
|
folderId?: string;
|
||||||
|
isFromBlock?: boolean;
|
||||||
|
}[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const secret of data.secrets) {
|
||||||
|
const targetId = secret.folderId || secret.environmentId;
|
||||||
|
|
||||||
|
// Skip if we can't find either an environment or folder mapping for this secret
|
||||||
|
if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
|
||||||
|
logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret");
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mappedToEnvironmentId.has(targetId)) {
|
||||||
|
mappedToEnvironmentId.set(targetId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyHasSecret = mappedToEnvironmentId
|
||||||
|
.get(targetId)!
|
||||||
|
.find((el) => el.secretKey === secret.name && el.folderId === secret.folderId);
|
||||||
|
|
||||||
|
if (alreadyHasSecret && alreadyHasSecret.isFromBlock) {
|
||||||
|
// remove the existing secret if any
|
||||||
|
mappedToEnvironmentId
|
||||||
|
.get(targetId)!
|
||||||
|
.splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1);
|
||||||
|
}
|
||||||
|
mappedToEnvironmentId.get(targetId)!.push({
|
||||||
|
secretKey: secret.name,
|
||||||
|
secretValue: secret.value || "",
|
||||||
|
folderId: secret.folderId,
|
||||||
|
isFromBlock: secret.appBlockOrderIndex !== undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each of the mappedEnvironmentId
|
||||||
|
for await (const [targetId, secrets] of mappedToEnvironmentId) {
|
||||||
|
logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId);
|
||||||
|
|
||||||
|
let selectedFolder: TSecretFolders | undefined;
|
||||||
|
let selectedProjectId: string | undefined;
|
||||||
|
|
||||||
|
// Case 1: Secret belongs to a folder / branch / branch of a block
|
||||||
|
const foundFolder = originalToNewFolderId.get(targetId);
|
||||||
|
if (foundFolder) {
|
||||||
|
logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder");
|
||||||
|
selectedFolder = await folderDAL.findById(foundFolder.folderId, tx);
|
||||||
|
selectedProjectId = foundFolder.projectId;
|
||||||
|
} else {
|
||||||
|
logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment");
|
||||||
|
const environment = data.environments.find((env) => env.id === targetId);
|
||||||
|
if (!environment) {
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
targetId
|
||||||
|
},
|
||||||
|
"[importDataIntoInfisicalFn]: Could not find environment for secret"
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectId = originalToNewProjectId.get(environment.projectId)!;
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
throw new BadRequestError({ message: `Failed to import secret, project not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = originalToNewEnvironmentId.get(targetId);
|
||||||
|
if (!env) {
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
targetId
|
||||||
|
},
|
||||||
|
"[importDataIntoInfisicalFn]: Could not find environment for secret"
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx);
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`,
|
||||||
|
name: "Create secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFolder = folder;
|
||||||
|
selectedProjectId = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedFolder) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder not found for the given environment slug & secret path`,
|
||||||
|
name: "CreateSecret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedProjectId) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Project not found for the given environment slug & secret path`,
|
||||||
|
name: "CreateSecret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
|
||||||
|
{
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId: selectedProjectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const secretBatches = chunkArray(secrets, 2500);
|
||||||
|
for await (const secretBatch of secretBatches) {
|
||||||
|
const secretsByKeys = await secretDAL.findBySecretKeys(
|
||||||
|
selectedFolder.id,
|
||||||
|
secretBatch.map((el) => ({
|
||||||
|
key: el.secretKey,
|
||||||
|
type: SecretType.Shared
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (secretsByKeys.length) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await fnSecretBulkInsert({
|
||||||
|
inputSecrets: secretBatch.map((el) => {
|
||||||
|
const references = getAllSecretReferences(el.secretValue).nestedReferences;
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
encryptedValue: el.secretValue
|
||||||
|
? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
|
||||||
|
: undefined,
|
||||||
|
key: el.secretKey,
|
||||||
|
references,
|
||||||
|
type: SecretType.Shared
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
folderId: selectedFolder.id,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
resourceMetadataDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
folderCommitService,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { projectsNotImported };
|
||||||
|
};
|
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./envkey";
|
||||||
|
export * from "./import";
|
||||||
|
export * from "./vault";
|
@@ -0,0 +1,341 @@
|
|||||||
|
import axios, { AxiosInstance } from "axios";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
|
||||||
|
import { InfisicalImportData, VaultMappingType } from "../external-migration-types";
|
||||||
|
|
||||||
|
type VaultData = {
|
||||||
|
namespace: string;
|
||||||
|
mount: string;
|
||||||
|
path: string;
|
||||||
|
secretData: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const vaultFactory = () => {
|
||||||
|
const getMounts = async (request: AxiosInstance) => {
|
||||||
|
const response = await request
|
||||||
|
.get<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
accessor: string;
|
||||||
|
options: {
|
||||||
|
version?: string;
|
||||||
|
} | null;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>("/v1/sys/mounts")
|
||||||
|
.catch((err) => {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
logger.error(err.response?.data, "External migration: Failed to get Vault mounts");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaths = async (
|
||||||
|
request: AxiosInstance,
|
||||||
|
{ mountPath, secretPath = "" }: { mountPath: string; secretPath?: string }
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// For KV v2: /v1/{mount}/metadata/{path}?list=true
|
||||||
|
const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
|
||||||
|
const response = await request.get<{
|
||||||
|
data: {
|
||||||
|
keys: string[];
|
||||||
|
};
|
||||||
|
}>(`/v1/${path}?list=true`);
|
||||||
|
|
||||||
|
return response.data.data.keys;
|
||||||
|
} catch (err) {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
logger.error(err.response?.data, "External migration: Failed to get Vault paths");
|
||||||
|
if (err.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecrets = async (
|
||||||
|
request: AxiosInstance,
|
||||||
|
{ mountPath, secretPath }: { mountPath: string; secretPath: string }
|
||||||
|
) => {
|
||||||
|
// For KV v2: /v1/{mount}/data/{path}
|
||||||
|
const response = await request
|
||||||
|
.get<{
|
||||||
|
data: {
|
||||||
|
data: Record<string, string>; // KV v2 has nested data structure
|
||||||
|
metadata: {
|
||||||
|
created_time: string;
|
||||||
|
deletion_time: string;
|
||||||
|
destroyed: boolean;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>(`/v1/${mountPath}/data/${secretPath}`)
|
||||||
|
.catch((err) => {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
logger.error(err.response?.data, "External migration: Failed to get Vault secret");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1)
|
||||||
|
// const isKvV2Mount = (mountInfo: { type: string; options?: { version?: string } | null }) => {
|
||||||
|
// return mountInfo.type === "kv" && mountInfo.options?.version === "2";
|
||||||
|
// };
|
||||||
|
|
||||||
|
const recursivelyGetAllPaths = async (
|
||||||
|
request: AxiosInstance,
|
||||||
|
mountPath: string,
|
||||||
|
currentPath: string = ""
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const paths = await getPaths(request, { mountPath, secretPath: currentPath });
|
||||||
|
|
||||||
|
if (paths === null || paths.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSecrets: string[] = [];
|
||||||
|
|
||||||
|
for await (const path of paths) {
|
||||||
|
const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
||||||
|
const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath;
|
||||||
|
|
||||||
|
if (path.endsWith("/")) {
|
||||||
|
// it's a folder so we recurse into it
|
||||||
|
const subSecrets = await recursivelyGetAllPaths(request, mountPath, fullItemPath);
|
||||||
|
allSecrets.push(...subSecrets);
|
||||||
|
} else {
|
||||||
|
// it's a secret so we add it to our results
|
||||||
|
allSecrets.push(`${mountPath}/${fullItemPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function collectVaultData({
|
||||||
|
baseUrl,
|
||||||
|
namespace,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
baseUrl: string;
|
||||||
|
namespace?: string;
|
||||||
|
accessToken: string;
|
||||||
|
}): Promise<VaultData[]> {
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(namespace ? { "X-Vault-Namespace": namespace } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const allData: VaultData[] = [];
|
||||||
|
|
||||||
|
// Get all mounts in this namespace
|
||||||
|
const mounts = await getMounts(request);
|
||||||
|
|
||||||
|
for (const mount of Object.keys(mounts)) {
|
||||||
|
if (!mount.endsWith("/")) {
|
||||||
|
delete mounts[mount];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const [mountPath, mountInfo] of Object.entries(mounts)) {
|
||||||
|
// skip non-KV mounts
|
||||||
|
if (!mountInfo.type.startsWith("kv")) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all paths in this mount
|
||||||
|
const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`);
|
||||||
|
|
||||||
|
const cleanMountPath = mountPath.replace(/\/$/, "");
|
||||||
|
|
||||||
|
for await (const secretPath of paths) {
|
||||||
|
// get the actual secret data
|
||||||
|
const secretData = await getSecrets(request, {
|
||||||
|
mountPath: cleanMountPath,
|
||||||
|
secretPath: secretPath.replace(`${cleanMountPath}/`, "")
|
||||||
|
});
|
||||||
|
|
||||||
|
allData.push({
|
||||||
|
namespace: namespace || "",
|
||||||
|
mount: mountPath.replace(/\/$/, ""),
|
||||||
|
path: secretPath.replace(`${cleanMountPath}/`, ""),
|
||||||
|
secretData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
collectVaultData,
|
||||||
|
getMounts,
|
||||||
|
getPaths,
|
||||||
|
getSecrets,
|
||||||
|
recursivelyGetAllPaths
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformToInfisicalFormatNamespaceToProjects = (
|
||||||
|
vaultData: VaultData[],
|
||||||
|
mappingType: VaultMappingType
|
||||||
|
): InfisicalImportData => {
|
||||||
|
const projects: Array<{ name: string; id: string }> = [];
|
||||||
|
const environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }> = [];
|
||||||
|
const folders: Array<{ id: string; name: string; environmentId: string; parentFolderId?: string }> = [];
|
||||||
|
const secrets: Array<{ id: string; name: string; environmentId: string; value: string; folderId?: string }> = [];
|
||||||
|
|
||||||
|
// track created entities to avoid duplicates
|
||||||
|
const projectMap = new Map<string, string>(); // namespace -> projectId
|
||||||
|
const environmentMap = new Map<string, string>(); // namespace:mount -> environmentId
|
||||||
|
const folderMap = new Map<string, string>(); // namespace:mount:folderPath -> folderId
|
||||||
|
|
||||||
|
let environmentId: string = "";
|
||||||
|
for (const data of vaultData) {
|
||||||
|
const { namespace, mount, path, secretData } = data;
|
||||||
|
|
||||||
|
if (mappingType === VaultMappingType.Namespace) {
|
||||||
|
// create project (namespace)
|
||||||
|
if (!projectMap.has(namespace)) {
|
||||||
|
const projectId = uuidv4();
|
||||||
|
projectMap.set(namespace, projectId);
|
||||||
|
projects.push({
|
||||||
|
name: namespace,
|
||||||
|
id: projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const projectId = projectMap.get(namespace)!;
|
||||||
|
|
||||||
|
// create environment (mount)
|
||||||
|
const envKey = `${namespace}:${mount}`;
|
||||||
|
if (!environmentMap.has(envKey)) {
|
||||||
|
environmentId = uuidv4();
|
||||||
|
environmentMap.set(envKey, environmentId);
|
||||||
|
environments.push({
|
||||||
|
name: mount,
|
||||||
|
id: environmentId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
environmentId = environmentMap.get(envKey)!;
|
||||||
|
} else if (mappingType === VaultMappingType.KeyVault) {
|
||||||
|
if (!projectMap.has(mount)) {
|
||||||
|
const projectId = uuidv4();
|
||||||
|
projectMap.set(mount, projectId);
|
||||||
|
projects.push({
|
||||||
|
name: mount,
|
||||||
|
id: projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const projectId = projectMap.get(mount)!;
|
||||||
|
|
||||||
|
// create single "Production" environment per project, because we have no good way of determining environments from vault
|
||||||
|
if (!environmentMap.has(mount)) {
|
||||||
|
environmentId = uuidv4();
|
||||||
|
environmentMap.set(mount, environmentId);
|
||||||
|
environments.push({
|
||||||
|
name: "Production",
|
||||||
|
id: environmentId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
environmentId = environmentMap.get(mount)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create folder structure
|
||||||
|
let currentFolderId: string | undefined;
|
||||||
|
let currentPath = "";
|
||||||
|
|
||||||
|
if (path.includes("/")) {
|
||||||
|
const pathParts = path.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
const folderParts = pathParts;
|
||||||
|
|
||||||
|
// create nested folder structure for the entire path
|
||||||
|
for (const folderName of folderParts) {
|
||||||
|
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
|
||||||
|
const folderKey = `${namespace}:${mount}:${currentPath}`;
|
||||||
|
|
||||||
|
if (!folderMap.has(folderKey)) {
|
||||||
|
const folderId = uuidv4();
|
||||||
|
folderMap.set(folderKey, folderId);
|
||||||
|
folders.push({
|
||||||
|
id: folderId,
|
||||||
|
name: folderName,
|
||||||
|
environmentId,
|
||||||
|
parentFolderId: currentFolderId || environmentId
|
||||||
|
});
|
||||||
|
currentFolderId = folderId;
|
||||||
|
} else {
|
||||||
|
currentFolderId = folderMap.get(folderKey)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(secretData)) {
|
||||||
|
secrets.push({
|
||||||
|
id: uuidv4(),
|
||||||
|
name: key,
|
||||||
|
environmentId,
|
||||||
|
value: String(value),
|
||||||
|
folderId: currentFolderId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
environments,
|
||||||
|
folders,
|
||||||
|
secrets
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importVaultDataFn = async ({
|
||||||
|
vaultAccessToken,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultUrl,
|
||||||
|
mappingType
|
||||||
|
}: {
|
||||||
|
vaultAccessToken: string;
|
||||||
|
vaultNamespace?: string;
|
||||||
|
vaultUrl: string;
|
||||||
|
mappingType: VaultMappingType;
|
||||||
|
}) => {
|
||||||
|
await blockLocalAndPrivateIpAddresses(vaultUrl);
|
||||||
|
|
||||||
|
if (mappingType === VaultMappingType.Namespace && !vaultNamespace) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Vault namespace is required when project mapping type is set to namespace."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultApi = vaultFactory();
|
||||||
|
|
||||||
|
const vaultData = await vaultApi.collectVaultData({
|
||||||
|
accessToken: vaultAccessToken,
|
||||||
|
baseUrl: vaultUrl,
|
||||||
|
namespace: vaultNamespace
|
||||||
|
});
|
||||||
|
|
||||||
|
const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType);
|
||||||
|
|
||||||
|
return infisicalData;
|
||||||
|
};
|
@@ -19,7 +19,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
|
|||||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { importDataIntoInfisicalFn } from "./external-migration-fns";
|
import { importDataIntoInfisicalFn } from "./external-migration-fns";
|
||||||
import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types";
|
import { ExternalPlatforms, ImportType, TImportInfisicalDataCreate } from "./external-migration-types";
|
||||||
|
|
||||||
export type TExternalMigrationQueueFactoryDep = {
|
export type TExternalMigrationQueueFactoryDep = {
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
@@ -67,6 +67,7 @@ export const externalMigrationQueueFactory = ({
|
|||||||
const startImport = async (dto: {
|
const startImport = async (dto: {
|
||||||
actorEmail: string;
|
actorEmail: string;
|
||||||
data: {
|
data: {
|
||||||
|
importType: ImportType;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
|
@@ -4,9 +4,9 @@ import { crypto } from "@app/lib/crypto/cryptography";
|
|||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
||||||
import { TExternalMigrationQueueFactory } from "./external-migration-queue";
|
import { TExternalMigrationQueueFactory } from "./external-migration-queue";
|
||||||
import { TImportEnvKeyDataCreate } from "./external-migration-types";
|
import { ImportType, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types";
|
||||||
|
|
||||||
type TExternalMigrationServiceFactoryDep = {
|
type TExternalMigrationServiceFactoryDep = {
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
@@ -28,7 +28,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TImportEnvKeyDataCreate) => {
|
}: TImportEnvKeyDataDTO) => {
|
||||||
if (crypto.isFipsModeEnabled()) {
|
if (crypto.isFipsModeEnabled()) {
|
||||||
throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." });
|
throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." });
|
||||||
}
|
}
|
||||||
@@ -60,11 +60,65 @@ export const externalMigrationServiceFactory = ({
|
|||||||
|
|
||||||
await externalMigrationQueue.startImport({
|
await externalMigrationQueue.startImport({
|
||||||
actorEmail: user.email!,
|
actorEmail: user.email!,
|
||||||
data: encrypted
|
data: {
|
||||||
|
importType: ImportType.EnvKey,
|
||||||
|
...encrypted
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const importVaultData = async ({
|
||||||
|
vaultAccessToken,
|
||||||
|
vaultNamespace,
|
||||||
|
mappingType,
|
||||||
|
vaultUrl,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TImportVaultDataDTO) => {
|
||||||
|
const { membership } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (membership.role !== OrgMembershipRole.Admin) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Only admins can import data" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await userDAL.findById(actorId);
|
||||||
|
|
||||||
|
const vaultData = await importVaultDataFn({
|
||||||
|
vaultAccessToken,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultUrl,
|
||||||
|
mappingType
|
||||||
|
});
|
||||||
|
|
||||||
|
const stringifiedJson = JSON.stringify({
|
||||||
|
data: vaultData,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||||
|
|
||||||
|
await externalMigrationQueue.startImport({
|
||||||
|
actorEmail: user.email!,
|
||||||
|
data: {
|
||||||
|
importType: ImportType.Vault,
|
||||||
|
...encrypted
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
importEnvKeyData
|
importEnvKeyData,
|
||||||
|
importVaultData
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,17 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export enum ImportType {
|
||||||
|
EnvKey = "envkey",
|
||||||
|
Vault = "vault"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VaultMappingType {
|
||||||
|
Namespace = "namespace",
|
||||||
|
KeyVault = "key-vault"
|
||||||
|
}
|
||||||
|
|
||||||
export type InfisicalImportData = {
|
export type InfisicalImportData = {
|
||||||
projects: Array<{ name: string; id: string }>;
|
projects: Array<{ name: string; id: string }>;
|
||||||
environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }>;
|
environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }>;
|
||||||
@@ -14,14 +26,17 @@ export type InfisicalImportData = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TImportEnvKeyDataCreate = {
|
export type TImportEnvKeyDataDTO = {
|
||||||
decryptionKey: string;
|
decryptionKey: string;
|
||||||
encryptedJson: { nonce: string; data: string };
|
encryptedJson: { nonce: string; data: string };
|
||||||
actor: ActorType;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
actorId: string;
|
|
||||||
actorOrgId: string;
|
export type TImportVaultDataDTO = {
|
||||||
actorAuthMethod: ActorAuthMethod;
|
vaultAccessToken: string;
|
||||||
};
|
vaultNamespace?: string;
|
||||||
|
mappingType: VaultMappingType;
|
||||||
|
vaultUrl: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TImportInfisicalDataCreate = {
|
export type TImportInfisicalDataCreate = {
|
||||||
data: InfisicalImportData;
|
data: InfisicalImportData;
|
||||||
|
@@ -11,7 +11,7 @@ import { TReminderServiceFactory } from "./reminder-types";
|
|||||||
type TDailyReminderQueueServiceFactoryDep = {
|
type TDailyReminderQueueServiceFactoryDep = {
|
||||||
reminderService: TReminderServiceFactory;
|
reminderService: TReminderServiceFactory;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "transaction" | "findSecretsWithReminderRecipients">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "transaction" | "findSecretsWithReminderRecipientsOld">;
|
||||||
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export const dailyReminderQueueServiceFactory = ({
|
|||||||
|
|
||||||
// Find existing secrets with pagination
|
// Find existing secrets with pagination
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const secrets = await secretDAL.findSecretsWithReminderRecipients(batchIds, REMINDER_PRUNE_BATCH_SIZE);
|
const secrets = await secretDAL.findSecretsWithReminderRecipientsOld(batchIds, REMINDER_PRUNE_BATCH_SIZE);
|
||||||
const secretsWithReminder = secrets.filter((secret) => secret.reminderRepeatDays);
|
const secretsWithReminder = secrets.filter((secret) => secret.reminderRepeatDays);
|
||||||
|
|
||||||
const foundSecretIds = new Set(secretsWithReminder.map((secret) => secret.id));
|
const foundSecretIds = new Set(secretsWithReminder.map((secret) => secret.id));
|
||||||
@@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({
|
|||||||
{ pattern: "0 */1 * * *", utc: true },
|
{ pattern: "0 */1 * * *", utc: true },
|
||||||
QueueName.SecretReminderMigration // just a job id
|
QueueName.SecretReminderMigration // just a job id
|
||||||
);
|
);
|
||||||
|
|
||||||
await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, {
|
|
||||||
delay: 5000,
|
|
||||||
jobId: QueueName.SecretReminderMigration,
|
|
||||||
repeat: { pattern: "0 */1 * * *", utc: true }
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {
|
queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {
|
||||||
|
@@ -308,12 +308,11 @@ export const reminderServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const newReminders = await reminderDAL.insertMany(
|
const newReminders = await reminderDAL.insertMany(
|
||||||
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, projectId }) => ({
|
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({
|
||||||
secretId,
|
secretId,
|
||||||
message,
|
message,
|
||||||
repeatDays,
|
repeatDays,
|
||||||
nextReminderDate,
|
nextReminderDate
|
||||||
projectId
|
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -8,7 +8,26 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
|||||||
|
|
||||||
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
||||||
|
|
||||||
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials) => {
|
const MAX_RETRIES = 5;
|
||||||
|
|
||||||
|
const retrySleep = async () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0): Promise<T> => {
|
||||||
|
try {
|
||||||
|
return await requestFn();
|
||||||
|
} catch (error) {
|
||||||
|
if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) {
|
||||||
|
await retrySleep();
|
||||||
|
return await makeRequestWithRetry(requestFn, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
|
||||||
const {
|
const {
|
||||||
destinationConfig,
|
destinationConfig,
|
||||||
connection: {
|
connection: {
|
||||||
@@ -22,20 +41,23 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
|
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
|
||||||
const { data } = await request.get<
|
|
||||||
{
|
const { data } = await makeRequestWithRetry(() =>
|
||||||
envVar: {
|
request.get<
|
||||||
key: string;
|
{
|
||||||
value: string;
|
envVar: {
|
||||||
};
|
key: string;
|
||||||
cursor: string;
|
value: string;
|
||||||
}[]
|
};
|
||||||
>(url, {
|
cursor: string;
|
||||||
headers: {
|
}[]
|
||||||
Authorization: `Bearer ${apiKey}`,
|
>(url, {
|
||||||
Accept: "application/json"
|
headers: {
|
||||||
}
|
Authorization: `Bearer ${apiKey}`,
|
||||||
});
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const secrets = data.map((item) => ({
|
const secrets = data.map((item) => ({
|
||||||
key: item.envVar.key,
|
key: item.envVar.key,
|
||||||
@@ -44,13 +66,20 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
|
|||||||
|
|
||||||
allSecrets.push(...secrets);
|
allSecrets.push(...secrets);
|
||||||
|
|
||||||
cursor = data[data.length - 1]?.cursor;
|
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||||
|
cursor = data[data.length - 1].cursor;
|
||||||
|
} else {
|
||||||
|
cursor = undefined;
|
||||||
|
}
|
||||||
} while (cursor);
|
} while (cursor);
|
||||||
|
|
||||||
return allSecrets;
|
return allSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap, key: string) => {
|
const batchUpdateEnvironmentSecrets = async (
|
||||||
|
secretSync: TRenderSyncWithCredentials,
|
||||||
|
envVars: Array<{ key: string; value: string }>
|
||||||
|
): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
destinationConfig,
|
destinationConfig,
|
||||||
connection: {
|
connection: {
|
||||||
@@ -58,22 +87,17 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr
|
|||||||
}
|
}
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
|
|
||||||
await request.put(
|
await makeRequestWithRetry(() =>
|
||||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`,
|
request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
|
||||||
{
|
|
||||||
key,
|
|
||||||
value: secretMap[key].value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
Accept: "application/json"
|
Accept: "application/json"
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick<TRenderSecret, "key">) => {
|
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||||
const {
|
const {
|
||||||
destinationConfig,
|
destinationConfig,
|
||||||
connection: {
|
connection: {
|
||||||
@@ -81,70 +105,81 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s
|
|||||||
}
|
}
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
|
|
||||||
try {
|
await makeRequestWithRetry(() =>
|
||||||
await request.delete(
|
request.post(
|
||||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
|
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
|
||||||
|
{},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
Accept: "application/json"
|
Accept: "application/json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
} catch (error) {
|
);
|
||||||
if (isAxiosError(error) && error.response?.status === 404) {
|
|
||||||
// If the secret does not exist, we can ignore this error
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sleep = async () =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const RenderSyncFns = {
|
export const RenderSyncFns = {
|
||||||
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
|
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
||||||
for await (const key of Object.keys(secretMap)) {
|
|
||||||
// If value is empty skip it as render does not allow empty variables
|
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
||||||
if (secretMap[key].value === "") {
|
|
||||||
// eslint-disable-next-line no-continue
|
for (const renderSecret of renderSecrets) {
|
||||||
continue;
|
const shouldKeep =
|
||||||
|
secretMap[renderSecret.key] ||
|
||||||
|
(secretSync.syncOptions.disableSecretDeletion &&
|
||||||
|
!matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema));
|
||||||
|
|
||||||
|
if (shouldKeep && !secretMap[renderSecret.key]) {
|
||||||
|
finalEnvVars.push({
|
||||||
|
key: renderSecret.key,
|
||||||
|
value: renderSecret.value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await putEnvironmentSecret(secretSync, secretMap, key);
|
|
||||||
await sleep();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
for (const [key, secret] of Object.entries(secretMap)) {
|
||||||
|
// Skip empty values as render does not allow empty variables
|
||||||
for await (const renderSecret of renderSecrets) {
|
if (secret.value === "") {
|
||||||
if (!matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
|
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!secretMap[renderSecret.key]) {
|
|
||||||
await deleteEnvironmentSecret(secretSync, renderSecret);
|
|
||||||
await sleep();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalEnvVars.push({
|
||||||
|
key,
|
||||||
|
value: secret.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||||
|
|
||||||
|
if (secretSync.syncOptions.autoRedeployServices) {
|
||||||
|
await redeployService(secretSync);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => {
|
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => {
|
||||||
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
||||||
return Object.fromEntries(renderSecrets.map((secret) => [secret.key, { value: secret.value ?? "" }]));
|
return Object.fromEntries(renderSecrets.map((secret) => [secret.key, { value: secret.value ?? "" }]));
|
||||||
},
|
},
|
||||||
|
|
||||||
removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
|
removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const encryptedSecrets = await getRenderEnvironmentSecrets(secretSync);
|
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
||||||
|
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
||||||
|
|
||||||
for await (const encryptedSecret of encryptedSecrets) {
|
for (const renderSecret of renderSecrets) {
|
||||||
if (encryptedSecret.key in secretMap) {
|
if (!(renderSecret.key in secretMap)) {
|
||||||
await deleteEnvironmentSecret(secretSync, encryptedSecret);
|
finalEnvVars.push({
|
||||||
await sleep();
|
key: renderSecret.key,
|
||||||
|
value: renderSecret.value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||||
|
|
||||||
|
if (secretSync.syncOptions.autoRedeployServices) {
|
||||||
|
await redeployService(secretSync);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -20,23 +20,33 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const RenderSyncOptionsSchema = z.object({
|
||||||
|
autoRedeployServices: z.boolean().optional().describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.RENDER.autoRedeployServices)
|
||||||
|
});
|
||||||
|
|
||||||
const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({
|
export const RenderSyncSchema = BaseSecretSyncSchema(
|
||||||
|
SecretSync.Render,
|
||||||
|
RenderSyncOptionsConfig,
|
||||||
|
RenderSyncOptionsSchema
|
||||||
|
).extend({
|
||||||
destination: z.literal(SecretSync.Render),
|
destination: z.literal(SecretSync.Render),
|
||||||
destinationConfig: RenderSyncDestinationConfigSchema
|
destinationConfig: RenderSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
SecretSync.Render,
|
SecretSync.Render,
|
||||||
RenderSyncOptionsConfig
|
RenderSyncOptionsConfig,
|
||||||
|
RenderSyncOptionsSchema
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: RenderSyncDestinationConfigSchema
|
destinationConfig: RenderSyncDestinationConfigSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
SecretSync.Render,
|
SecretSync.Render,
|
||||||
RenderSyncOptionsConfig
|
RenderSyncOptionsConfig,
|
||||||
|
RenderSyncOptionsSchema
|
||||||
).extend({
|
).extend({
|
||||||
destinationConfig: RenderSyncDestinationConfigSchema.optional()
|
destinationConfig: RenderSyncDestinationConfigSchema.optional()
|
||||||
});
|
});
|
||||||
|
@@ -875,6 +875,48 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findSecretsWithReminderRecipientsOld = async (ids: string[], limit: number, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
// Create a subquery to get limited secret IDs
|
||||||
|
const limitedSecretIds = (tx || db)(TableName.SecretV2)
|
||||||
|
.whereIn(`${TableName.SecretV2}.id`, ids)
|
||||||
|
.limit(limit)
|
||||||
|
.select("id");
|
||||||
|
|
||||||
|
// Join with all recipients for the limited secrets
|
||||||
|
const docs = await (tx || db)(TableName.SecretV2)
|
||||||
|
.whereIn(`${TableName.SecretV2}.id`, limitedSecretIds)
|
||||||
|
.leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretReminderRecipients,
|
||||||
|
`${TableName.SecretV2}.id`,
|
||||||
|
`${TableName.SecretReminderRecipients}.secretId`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.SecretV2))
|
||||||
|
.select(db.ref("userId").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientUserId"));
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({
|
||||||
|
_id: el.id,
|
||||||
|
...SecretsV2Schema.parse(el)
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "reminderRecipientUserId",
|
||||||
|
label: "recipients" as const,
|
||||||
|
mapper: ({ reminderRecipientUserId }) => reminderRecipientUserId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "findSecretsWithReminderRecipientsOld" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
update,
|
update,
|
||||||
@@ -893,6 +935,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
findOne,
|
findOne,
|
||||||
find,
|
find,
|
||||||
invalidateSecretCacheByProjectId,
|
invalidateSecretCacheByProjectId,
|
||||||
findSecretsWithReminderRecipients
|
findSecretsWithReminderRecipients,
|
||||||
|
findSecretsWithReminderRecipientsOld
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -198,6 +198,14 @@
|
|||||||
"documentation/platform/workflow-integrations/microsoft-teams-integration"
|
"documentation/platform/workflow-integrations/microsoft-teams-integration"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "External Migrations",
|
||||||
|
"pages": [
|
||||||
|
"documentation/platform/external-migrations/overview",
|
||||||
|
"documentation/platform/external-migrations/envkey",
|
||||||
|
"documentation/platform/external-migrations/vault"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Admin Consoles",
|
"group": "Admin Consoles",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Migrating from EnvKey to Infisical"
|
|
||||||
sidebarTitle: "Migration"
|
|
||||||
description: "Learn how to migrate from EnvKey to Infisical in the easiest way possible."
|
|
||||||
---
|
|
||||||
|
|
||||||
## What is Infisical?
|
|
||||||
|
|
||||||
[Infisical](https://infisical.com) is an open-source all-in-one secret management platform that helps developers manage secrets (e.g., API-keys, DB access tokens, [certificates](https://infisical.com/docs/documentation/platform/pki/overview)) across their infrastructure. In addition, Infisical provides [secret sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality, ability to [prevent secret leaks](https://infisical.com/docs/cli/scanning-overview), and more.
|
|
||||||
|
|
||||||
Infisical is used by 10,000+ organizations across all industries including First American Financial Corporation, Delivery Hero, and [Hugging Face](https://infisical.com/customers/hugging-face).
|
|
||||||
|
|
||||||
## Migrating from EnvKey
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step>
|
|
||||||
Open the EnvKey dashboard and go to My Org.
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
<Step>
|
|
||||||
Go to Import/Export on the top right corner, Click on Export Org and save the exported file.
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
<Step>
|
|
||||||
Click on copy to copy the encryption key and save it.
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
<Step>
|
|
||||||
Open the Infisical dashboard and go to Organization Settings > Import.
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
<Step>
|
|
||||||
Upload the exported file from EnvKey, paste the encryption key and click Import.
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
|
|
||||||
## Talk to our team
|
|
||||||
|
|
||||||
To make the migration process even more seamless, you can [schedule a meeting with our team](https://infisical.cal.com/vlad/migration-from-envkey-to-infisical) to learn more about how Infisical compares to EnvKey and discuss unique needs of your organization. You are also welcome to email us at [support@infisical.com](mailto:support@infisical.com) to ask any questions or get any technical help.
|
|
44
docs/documentation/platform/external-migrations/envkey.mdx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
title: "Migrating from EnvKey to Infisical"
|
||||||
|
sidebarTitle: "EnvKey"
|
||||||
|
description: "Learn how to migrate secrets from EnvKey to Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrating from EnvKey
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open the EnvKey dashboard and go to My Org">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Export your EnvKey organization">
|
||||||
|
Go to Import/Export on the top right corner, Click on Export Org and save the exported file.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Obtain EnvKey encryption key">
|
||||||
|
Click on copy to copy the encryption key and save it.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Navigate to Infisical external migrations">
|
||||||
|
Open the Infisical dashboard and go to Organization Settings > External Migrations.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Select the EnvKey platform">
|
||||||
|
Select the EnvKey platform and click on Next.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Upload the exported file from EnvKey">
|
||||||
|
Upload the exported file from EnvKey, paste the encryption key and click Import data.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
It may take several minutes to complete the migration. You will receive an email when the migration is complete, or if there were any errors during the migration process.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
|
||||||
|
## Talk to our team
|
||||||
|
|
||||||
|
To make the migration process even more seamless, you can [schedule a meeting with our team](https://infisical.cal.com/vlad/migration-from-envkey-to-infisical) to learn more about how Infisical compares to EnvKey and discuss unique needs of your organization. You are also welcome to email us at [support@infisical.com](mailto:support@infisical.com) to ask any questions or get any technical help.
|
16
docs/documentation/platform/external-migrations/overview.mdx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "External Migrations"
|
||||||
|
sidebarTitle: "Overview"
|
||||||
|
description: "Learn how to migrate secrets from third-party secrets management platforms to Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Infisical supports migrating secrets from third-party secrets management platforms to Infisical. This is useful if you're looking to easily switch to Infisical and wish to move over your existing secrets from a different platform.
|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
|
||||||
|
- [EnvKey](./envkey)
|
||||||
|
- [Vault](./vault)
|
||||||
|
|
||||||
|
We're always looking to add more migration paths for other providers. If we're missing a platform, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues).
|
127
docs/documentation/platform/external-migrations/vault.mdx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: "Migrating from Vault to Infisical"
|
||||||
|
sidebarTitle: "Vault"
|
||||||
|
description: "Learn how to migrate secrets from Vault to Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrating from Vault
|
||||||
|
|
||||||
|
Migrating from Vault Self-Hosted or Dedicated Vault is a straight forward process with our inbuilt migration option. In order to migrate from Vault, you'll need to provide Infisical an access token to your Vault instance.
|
||||||
|
|
||||||
|
Currently the Vault migration only supports migrating secrets from the KV v2 secrets engine. If you're using a different secrets engine, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues).
|
||||||
|
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- A Vault instance with the KV v2 secrets engine enabled.
|
||||||
|
- An access token to your Vault instance.
|
||||||
|
|
||||||
|
|
||||||
|
### Project Mapping
|
||||||
|
|
||||||
|
When migrating from Vault, you'll need to choose how you want to map your Vault resources to Infisical projects.
|
||||||
|
|
||||||
|
There are two options for project mapping:
|
||||||
|
|
||||||
|
- `Namespace`: This will map your selected Vault namespace to a single Infisical project. When you select this option, each KV secret engine within the namespace will be mapped to a single Infisical project. Each KV secret engine will be mapped to a Infisical environment within the project. This means if you have 3 KV secret engines, you'll have 3 environments inside the same project, where the name of the environments correspond to the name of the KV secret engines.
|
||||||
|
- `Key Vault`: This will map all the KV secret engines within your Vault instance to a Infisical project. Each KV engine will be created as a Infisical project. This means if you have 3 KV secret engines, you'll have 3 Infisical projects. For each of the created projects, a single default environment will be created called `Production`, which will contain all your secrets from the corresponding KV secret engine.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create a Vault policy">
|
||||||
|
In order to migrate from Vault, you'll need to create a Vault policy that allows Infisical to read the secrets and metadata from the KV v2 secrets engines within your Vault instance.
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Allow listing secret engines/mounts
|
||||||
|
path "sys/mounts" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# For KV v2 engines - access to both data and metadata
|
||||||
|
path "*/data/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "*/metadata/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# If using Vault Enterprise - allow listing namespaces
|
||||||
|
path "sys/namespaces" {
|
||||||
|
capabilities = ["list", "read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cross-namespace access (Enterprise only)
|
||||||
|
path "+/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "+/sys/mounts" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Save this policy with the name `infisical-migration`.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Generate an access token">
|
||||||
|
You can use the Vault CLI to easily generate an access token for the new `infisical-migration` policy that you created in the previous step.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vault token create --policy="infisical-migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
After generating the token, you should see the following output:
|
||||||
|
|
||||||
|
```t
|
||||||
|
$ vault token create --policy="infisical-migration"
|
||||||
|
|
||||||
|
Key Value
|
||||||
|
--- -----
|
||||||
|
token <your-access-token>
|
||||||
|
token_accessor p6kJDiBSzYYdabJUIpGCsCBm
|
||||||
|
token_duration 768h
|
||||||
|
token_renewable true
|
||||||
|
token_policies ["default" "infisical-migration"]
|
||||||
|
identity_policies []
|
||||||
|
policies ["default" "infisical-migration"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the `token` field and save it for later, as you'll need this when configuring the migration to Infisical.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Navigate to Infisical external migrations">
|
||||||
|
Open the Infisical dashboard and go to Organization Settings > External Migrations.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Select the Vault platform">
|
||||||
|
Select the Vault platform and click on Next.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Configure the Vault migration">
|
||||||
|
Enter the Vault access token that you generated in the previous step and click Import data.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- `Vault URL`: The URL of your Vault instance.
|
||||||
|
- `Vault Namespace`: The namespace of your Vault instance. This is optional, and can be left blank if you're not using namespaces for your Vault instance.
|
||||||
|
- `Vault Access Token`: The access token that you generated in the previous step.
|
||||||
|
|
||||||
|
- `Project Mapping`: Choose how you want to map your Vault resources to Infisical projects. You can review the mapping options in the [Project Mapping](#project-mapping) section.
|
||||||
|
|
||||||
|
Click on Import data to start the migration.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
It may take several minutes to complete the migration. You will receive an email when the migration is complete, or if there were any errors during the migration process.
|
||||||
|
</Note>
|
After Width: | Height: | Size: 452 KiB |
Before Width: | Height: | Size: 896 KiB |
Before Width: | Height: | Size: 609 KiB |
Before Width: | Height: | Size: 403 KiB After Width: | Height: | Size: 403 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 413 KiB After Width: | Height: | Size: 413 KiB |
After Width: | Height: | Size: 193 KiB |
After Width: | Height: | Size: 135 KiB |
After Width: | Height: | Size: 139 KiB |
After Width: | Height: | Size: 136 KiB |
After Width: | Height: | Size: 161 KiB |
@@ -72,6 +72,38 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="Client Secret Authentication">
|
||||||
|
Ensure your Azure application has the required permissions that Infisical needs for the Azure Client Secrets connection to work.
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- An active Azure setup.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Assign API permissions to the application">
|
||||||
|
For the Azure Client Secrets connection to work, assign the following permissions to your Azure application:
|
||||||
|
|
||||||
|
#### Required API Permissions
|
||||||
|
|
||||||
|
**Microsoft Graph**
|
||||||
|
- `Application.ReadWrite.All`
|
||||||
|
- `Application.ReadWrite.OwnedBy`
|
||||||
|
- `Application.ReadWrite.All` (Delegated)
|
||||||
|
- `Directory.ReadWrite.All` (Delegated)
|
||||||
|
- `User.Read` (Delegated)
|
||||||
|
|
||||||
|
**Azure App Configuration**
|
||||||
|
- `KeyValue.Delete` (Delegated)
|
||||||
|
- `KeyValue.Read` (Delegated)
|
||||||
|
- `KeyValue.Write` (Delegated)
|
||||||
|
|
||||||
|
**Access Key Vault**
|
||||||
|
- `user_impersonation` (Delegated)
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
## Setup Azure Connection in Infisical
|
## Setup Azure Connection in Infisical
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
@@ -82,21 +114,31 @@ Infisical currently only supports one method for connecting to Azure, which is O
|
|||||||
<Step title="Add Connection">
|
<Step title="Add Connection">
|
||||||
Select the **Azure Connection** option from the connection options modal. 
|
Select the **Azure Connection** option from the connection options modal. 
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Authorize Connection">
|
<Step title="Create Connection">
|
||||||
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
|
<Tabs>
|
||||||
|
<Tab title="OAuth">
|
||||||
|
<Step title="Authorize Connection">
|
||||||
|
Fill in the **Tenant ID** field with the Directory (Tenant) ID you obtained in the previous step.
|
||||||
|
|
||||||
Now select the **OAuth** method and click **Connect to Azure**.
|
Now select the **OAuth** method and click **Connect to Azure**.
|
||||||
|
|
||||||

|

|
||||||
|
</Step>
|
||||||
|
<Step title="Grant Access">
|
||||||
|
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
||||||
|
you will be redirected back to Infisical's App Connections page. 
|
||||||
|
</Step>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Client Secret">
|
||||||
|
<Step title="Create Connection">
|
||||||
|
Fill in the **Tenant ID**, **Client ID** and **Client Secret** fields with the Directory (Tenant) ID, Application (Client) ID and Client Secret you obtained in the previous step.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
</Step>
|
</Tab>
|
||||||
<Step title="Grant Access">
|
</Tabs>
|
||||||
You will then be redirected to Azure to grant Infisical access to your Azure account. Once granted,
|
</Step>
|
||||||
you will be redirected back to Infisical's App Connections page. 
|
|
||||||
</Step>
|
|
||||||
<Step title="Connection Created">
|
<Step title="Connection Created">
|
||||||
Your **Azure Client Secrets Connection** is now available for use. 
|
Your **Azure Client Secrets Connection** is now available for use. 
|
||||||
</Step>
|
</Step>
|
||||||
|
@@ -9,7 +9,7 @@ import {
|
|||||||
useRenderConnectionListServices
|
useRenderConnectionListServices
|
||||||
} from "@app/hooks/api/appConnections/render";
|
} from "@app/hooks/api/appConnections/render";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync";
|
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||||
|
|
||||||
import { TSecretSyncForm } from "../schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { FormControl, Switch, Tooltip } from "@app/components/v2";
|
||||||
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
import { TSecretSyncForm } from "../schemas";
|
||||||
|
|
||||||
|
export const RenderSyncOptionsFields = () => {
|
||||||
|
const { control } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name="syncOptions.autoRedeployServices"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl className="mt-4" isError={Boolean(error?.message)} errorText={error?.message}>
|
||||||
|
<Switch
|
||||||
|
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||||
|
id="auto-redeploy-services"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
isChecked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
>
|
||||||
|
Auto Redeploy Services On Sync
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content={
|
||||||
|
<p>If enabled, services will be automatically redeployed upon secret changes.</p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||||
|
</Tooltip>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
|||||||
import { TSecretSyncForm } from "../schemas";
|
import { TSecretSyncForm } from "../schemas";
|
||||||
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
||||||
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
|
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
|
||||||
|
import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hideInitialSync?: boolean;
|
hideInitialSync?: boolean;
|
||||||
@@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
|
AdditionalSyncOptionsFieldsComponent = <AwsSecretsManagerSyncOptionsFields />;
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.Render:
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsFields />;
|
||||||
|
break;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
@@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
|||||||
case SecretSync.OnePass:
|
case SecretSync.OnePass:
|
||||||
case SecretSync.OCIVault:
|
case SecretSync.OCIVault:
|
||||||
case SecretSync.Heroku:
|
case SecretSync.Heroku:
|
||||||
case SecretSync.Render:
|
|
||||||
case SecretSync.Flyio:
|
case SecretSync.Flyio:
|
||||||
case SecretSync.GitLab:
|
case SecretSync.GitLab:
|
||||||
case SecretSync.CloudflarePages:
|
case SecretSync.CloudflarePages:
|
||||||
|
@@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form";
|
|||||||
|
|
||||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||||
|
import { Badge } from "@app/components/v2";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
|
export const RenderSyncOptionsReviewFields = () => {
|
||||||
|
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||||
|
|
||||||
|
const [{ autoRedeployServices }] = watch(["syncOptions"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{autoRedeployServices ? (
|
||||||
|
<GenericFieldLabel label="Auto Redeploy Services">
|
||||||
|
<Badge variant="success">Enabled</Badge>
|
||||||
|
</GenericFieldLabel>
|
||||||
|
) : (
|
||||||
|
<GenericFieldLabel label="Auto Redeploy Services">
|
||||||
|
<Badge variant="danger">Disabled</Badge>
|
||||||
|
</GenericFieldLabel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const RenderSyncReviewFields = () => {
|
export const RenderSyncReviewFields = () => {
|
||||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Render }>();
|
||||||
const serviceName = watch("destinationConfig.serviceName");
|
const serviceName = watch("destinationConfig.serviceName");
|
||||||
|
@@ -35,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
|
|||||||
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
|
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
|
||||||
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
|
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
|
||||||
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
|
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
|
||||||
import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
|
import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields";
|
||||||
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
|
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
|
||||||
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
|
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
|
||||||
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
|
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
|
||||||
@@ -121,6 +121,7 @@ export const SecretSyncReviewFields = () => {
|
|||||||
break;
|
break;
|
||||||
case SecretSync.Render:
|
case SecretSync.Render:
|
||||||
DestinationFieldsComponent = <RenderSyncReviewFields />;
|
DestinationFieldsComponent = <RenderSyncReviewFields />;
|
||||||
|
AdditionalSyncOptionsFieldsComponent = <RenderSyncOptionsReviewFields />;
|
||||||
break;
|
break;
|
||||||
case SecretSync.Flyio:
|
case SecretSync.Flyio:
|
||||||
DestinationFieldsComponent = <FlyioSyncReviewFields />;
|
DestinationFieldsComponent = <FlyioSyncReviewFields />;
|
||||||
|
@@ -2,9 +2,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync";
|
import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||||
|
|
||||||
export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
|
||||||
|
z.object({
|
||||||
|
autoRedeployServices: z.boolean().optional()
|
||||||
|
})
|
||||||
|
).merge(
|
||||||
z.object({
|
z.object({
|
||||||
destination: z.literal(SecretSync.Render),
|
destination: z.literal(SecretSync.Render),
|
||||||
destinationConfig: z.discriminatedUnion("scope", [
|
destinationConfig: z.discriminatedUnion("scope", [
|
||||||
|
@@ -171,7 +171,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
case RenderConnectionMethod.ApiKey:
|
case RenderConnectionMethod.ApiKey:
|
||||||
case ChecklyConnectionMethod.ApiKey:
|
case ChecklyConnectionMethod.ApiKey:
|
||||||
return { name: "API Key", icon: faKey };
|
return { name: "API Key", icon: faKey };
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
return { name: "Client Secret", icon: faKey };
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
}
|
}
|
||||||
|
@@ -4,9 +4,9 @@ import {
|
|||||||
SecretSyncImportBehavior,
|
SecretSyncImportBehavior,
|
||||||
SecretSyncInitialSyncBehavior
|
SecretSyncInitialSyncBehavior
|
||||||
} from "@app/hooks/api/secretSyncs";
|
} from "@app/hooks/api/secretSyncs";
|
||||||
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/render-sync";
|
|
||||||
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
|
||||||
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
|
||||||
|
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||||
|
|
||||||
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }> = {
|
||||||
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
|
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
|
||||||
|
@@ -2,15 +2,26 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
|||||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||||
|
|
||||||
export enum AzureClientSecretsConnectionMethod {
|
export enum AzureClientSecretsConnectionMethod {
|
||||||
OAuth = "oauth"
|
OAuth = "oauth",
|
||||||
|
ClientSecret = "client-secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
export type TAzureClientSecretsConnection = TRootAppConnection & {
|
||||||
app: AppConnection.AzureClientSecrets;
|
app: AppConnection.AzureClientSecrets;
|
||||||
} & {
|
} & (
|
||||||
method: AzureClientSecretsConnectionMethod.OAuth;
|
| {
|
||||||
credentials: {
|
method: AzureClientSecretsConnectionMethod.OAuth;
|
||||||
code: string;
|
credentials: {
|
||||||
tenantId: string;
|
code: string;
|
||||||
};
|
tenantId: string;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: AzureClientSecretsConnectionMethod.ClientSecret;
|
||||||
|
credentials: {
|
||||||
|
clientSecret: string;
|
||||||
|
clientId: string;
|
||||||
|
tenantId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@@ -86,6 +86,7 @@ export const useCreateIdentity = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
|
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -110,6 +111,7 @@ export const useUpdateIdentity = () => {
|
|||||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -130,6 +132,7 @@ export const useDeleteIdentity = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
|
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentitiesRoot });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -25,7 +25,9 @@ import {
|
|||||||
|
|
||||||
export const identitiesKeys = {
|
export const identitiesKeys = {
|
||||||
getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const,
|
getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const,
|
||||||
searchIdentities: (dto: TSearchIdentitiesDTO) => ["identity", "search", dto] as const,
|
searchIdentitiesRoot: ["identity", "search"] as const,
|
||||||
|
searchIdentities: (dto: TSearchIdentitiesDTO) =>
|
||||||
|
[...identitiesKeys.searchIdentitiesRoot, dto] as const,
|
||||||
getIdentityUniversalAuth: (identityId: string) =>
|
getIdentityUniversalAuth: (identityId: string) =>
|
||||||
[{ identityId }, "identity-universal-auth"] as const,
|
[{ identityId }, "identity-universal-auth"] as const,
|
||||||
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
|
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
|
||||||
|
1
frontend/src/hooks/api/migration/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./mutations";
|
@@ -15,7 +15,7 @@ export const useImportEnvKey = () => {
|
|||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiRequest.post("/api/v3/migrate/env-key/", formData, {
|
const response = await apiRequest.post("/api/v3/external-migration/env-key/", formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
},
|
},
|
||||||
@@ -39,3 +39,26 @@ export const useImportEnvKey = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useImportVault = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
vaultAccessToken,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultUrl,
|
||||||
|
mappingType
|
||||||
|
}: {
|
||||||
|
vaultAccessToken: string;
|
||||||
|
vaultNamespace?: string;
|
||||||
|
vaultUrl: string;
|
||||||
|
mappingType: string;
|
||||||
|
}) => {
|
||||||
|
await apiRequest.post("/api/v3/external-migration/vault/", {
|
||||||
|
vaultAccessToken,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultUrl,
|
||||||
|
mappingType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
|
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
|
||||||
import { DiscriminativePick } from "@app/types";
|
import { DiscriminativePick } from "@app/types";
|
||||||
|
|
||||||
import { TRenderSync } from "../render-sync";
|
|
||||||
import { TOnePassSync } from "./1password-sync";
|
import { TOnePassSync } from "./1password-sync";
|
||||||
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
|
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
|
||||||
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
|
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
|
||||||
@@ -24,6 +23,7 @@ import { THerokuSync } from "./heroku-sync";
|
|||||||
import { THumanitecSync } from "./humanitec-sync";
|
import { THumanitecSync } from "./humanitec-sync";
|
||||||
import { TOCIVaultSync } from "./oci-vault-sync";
|
import { TOCIVaultSync } from "./oci-vault-sync";
|
||||||
import { TRailwaySync } from "./railway-sync";
|
import { TRailwaySync } from "./railway-sync";
|
||||||
|
import { TRenderSync } from "./render-sync";
|
||||||
import { TSupabaseSync } from "./supabase";
|
import { TSupabaseSync } from "./supabase";
|
||||||
import { TTeamCitySync } from "./teamcity-sync";
|
import { TTeamCitySync } from "./teamcity-sync";
|
||||||
import { TTerraformCloudSync } from "./terraform-cloud-sync";
|
import { TTerraformCloudSync } from "./terraform-cloud-sync";
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||||
|
|
||||||
export type TRenderSync = TRootSecretSync & {
|
export type TRenderSync = TRootSecretSync & {
|
||||||
destination: SecretSync.Render;
|
destination: SecretSync.Render;
|
||||||
@@ -16,6 +16,10 @@ export type TRenderSync = TRootSecretSync & {
|
|||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
syncOptions: RootSyncOptions & {
|
||||||
|
autoRedeployServices?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum RenderSyncScope {
|
export enum RenderSyncScope {
|
71
frontend/src/hooks/useResizableColWidth.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
minWidth: number;
|
||||||
|
maxWidth: number;
|
||||||
|
initialWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => {
|
||||||
|
const [colWidth, setColWidth] = useState(initialWidth);
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const startX = useRef(0);
|
||||||
|
const startWidth = useRef(0);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsResizing(true);
|
||||||
|
startX.current = e.clientX;
|
||||||
|
startWidth.current = colWidth;
|
||||||
|
},
|
||||||
|
[colWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - startX.current;
|
||||||
|
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + deltaX));
|
||||||
|
|
||||||
|
setColWidth(newWidth);
|
||||||
|
},
|
||||||
|
[isResizing]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isResizing) {
|
||||||
|
document.addEventListener(
|
||||||
|
"mousemove",
|
||||||
|
// @ts-expect-error native discrepancy
|
||||||
|
handleMouseMove
|
||||||
|
);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
document.body.style.cursor = "ew-resize";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(
|
||||||
|
"mousemove",
|
||||||
|
// @ts-expect-error native discrepancy
|
||||||
|
handleMouseMove
|
||||||
|
);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
};
|
||||||
|
}, [isResizing, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
colWidth,
|
||||||
|
handleMouseDown,
|
||||||
|
isResizing
|
||||||
|
};
|
||||||
|
};
|
@@ -114,7 +114,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
|||||||
case AppConnection.Camunda:
|
case AppConnection.Camunda:
|
||||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm />;
|
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureDevOps:
|
case AppConnection.AzureDevOps:
|
||||||
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
@@ -222,7 +222,7 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
case AppConnection.Camunda:
|
case AppConnection.Camunda:
|
||||||
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.AzureClientSecrets:
|
case AppConnection.AzureClientSecrets:
|
||||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.AzureDevOps:
|
case AppConnection.AzureDevOps:
|
||||||
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||||
case AppConnection.Windmill:
|
case AppConnection.Windmill:
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-case-declarations */
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -20,19 +21,83 @@ import {
|
|||||||
GenericAppConnectionsFields
|
GenericAppConnectionsFields
|
||||||
} from "./GenericAppConnectionFields";
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
|
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appConnection?: TAzureClientSecretsConnection;
|
appConnection?: TAzureClientSecretsConnection;
|
||||||
|
onSubmit: (formData: ClientSecretForm) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
const baseSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
app: z.literal(AppConnection.AzureClientSecrets),
|
app: z.literal(AppConnection.AzureClientSecrets),
|
||||||
method: z.nativeEnum(AzureClientSecretsConnectionMethod),
|
method: z.nativeEnum(AzureClientSecretsConnectionMethod)
|
||||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oauthSchema = baseSchema.extend({
|
||||||
|
tenantId: z.string().trim().min(1, "Tenant ID is required"),
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.OAuth)
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientSecretSchema = baseSchema.extend({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
|
||||||
|
credentials: z.object({
|
||||||
|
clientSecret: z.string().trim().min(1, "Client Secret is required"),
|
||||||
|
clientId: z.string().trim().min(1, "Client ID is required"),
|
||||||
|
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partial<FormData> => {
|
||||||
|
if (!appConnection) {
|
||||||
|
return {
|
||||||
|
app: AppConnection.AzureClientSecrets,
|
||||||
|
method: AzureClientSecretsConnectionMethod.OAuth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
name: appConnection.name,
|
||||||
|
description: appConnection.description,
|
||||||
|
app: appConnection.app,
|
||||||
|
method: appConnection.method
|
||||||
|
};
|
||||||
|
const { credentials } = appConnection;
|
||||||
|
|
||||||
|
switch (appConnection.method) {
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
if ("tenantId" in credentials) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
method: AzureClientSecretsConnectionMethod.OAuth,
|
||||||
|
tenantId: credentials.tenantId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
if ("clientSecret" in credentials && "clientId" in credentials) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
method: AzureClientSecretsConnectionMethod.ClientSecret,
|
||||||
|
credentials: {
|
||||||
|
clientSecret: credentials.clientSecret,
|
||||||
|
clientId: credentials.clientId,
|
||||||
|
tenantId: credentials.tenantId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||||
const isUpdate = Boolean(appConnection);
|
const isUpdate = Boolean(appConnection);
|
||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
|
|
||||||
@@ -43,70 +108,51 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: appConnection
|
defaultValues: getDefaultValues(appConnection)
|
||||||
? {
|
|
||||||
...appConnection,
|
|
||||||
tenantId: appConnection.credentials.tenantId
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
app: AppConnection.AzureClientSecrets,
|
|
||||||
method: AzureClientSecretsConnectionMethod.OAuth
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const selectedMethod = watch("method");
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
const onSubmit = (formData: FormData) => {
|
const onSubmitHandler = (formData: FormData) => {
|
||||||
setIsRedirecting(true);
|
|
||||||
const state = crypto.randomBytes(16).toString("hex");
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
localStorage.setItem("latestCSRFToken", state);
|
|
||||||
localStorage.setItem(
|
|
||||||
"azureClientSecretsConnectionFormData",
|
|
||||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (formData.method) {
|
switch (formData.method) {
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
setIsRedirecting(true);
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
localStorage.setItem(
|
||||||
|
"azureClientSecretsConnectionFormData",
|
||||||
|
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||||
|
);
|
||||||
window.location.assign(
|
window.location.assign(
|
||||||
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||||
|
onSubmit(formData);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMissingConfig = !oauthClientId;
|
const isMissingConfig =
|
||||||
|
selectedMethod === AzureClientSecretsConnectionMethod.OAuth && !oauthClientId;
|
||||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmitHandler)}>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="tenantId"
|
|
||||||
control={control}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
tooltipText="The Directory (tenant) ID."
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
label="Tenant ID"
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -146,6 +192,61 @@ export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="tenantId"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="The Directory (tenant) ID."
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Tenant ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="00000000-0000-0000-0000-000000000000"
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e.target.value);
|
||||||
|
setValue("credentials.tenantId", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Access Token-specific fields */}
|
||||||
|
{selectedMethod === AzureClientSecretsConnectionMethod.ClientSecret && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="credentials.clientId"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Client ID"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="credentials.clientSecret"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Client Secret"
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" placeholder="Enter your Client Secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-8 flex items-center">
|
<div className="mt-8 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
@@ -9,7 +9,7 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|||||||
|
|
||||||
import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal";
|
import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal";
|
||||||
|
|
||||||
export const ImportTab = () => {
|
export const ExternalMigrationsTab = () => {
|
||||||
const { membership } = useOrgPermission();
|
const { membership } = useOrgPermission();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
|
||||||
@@ -30,7 +30,7 @@ export const ImportTab = () => {
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="https://infisical.com/docs/documentation/guides/migrating-from-envkey"
|
href="https://infisical.com/docs/documentation/platform/external-migrations/overview"
|
||||||
>
|
>
|
||||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
@@ -45,25 +45,21 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await importEnvKey({
|
||||||
await importEnvKey({
|
file: data.file,
|
||||||
file: data.file,
|
decryptionKey: data.encryptionKey
|
||||||
decryptionKey: data.encryptionKey
|
});
|
||||||
});
|
createNotification({
|
||||||
createNotification({
|
title: "Import started",
|
||||||
title: "Import started",
|
text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.",
|
||||||
text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.",
|
type: "info"
|
||||||
type: "info"
|
});
|
||||||
});
|
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
if (fileUploadRef.current) {
|
if (fileUploadRef.current) {
|
||||||
fileUploadRef.current.value = "";
|
fileUploadRef.current.value = "";
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faKey, faVault } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
|
||||||
import { EnvKeyPlatformModal } from "./EnvKeyPlatformModal";
|
import { EnvKeyPlatformModal } from "./EnvKeyPlatformModal";
|
||||||
|
import { VaultPlatformModal } from "./VaultPlatformModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -22,6 +23,11 @@ const PLATFORM_LIST = [
|
|||||||
icon: faKey,
|
icon: faKey,
|
||||||
platform: "env-key",
|
platform: "env-key",
|
||||||
title: "Env Key"
|
title: "Env Key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faVault,
|
||||||
|
platform: "vault",
|
||||||
|
title: "Vault"
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -82,18 +88,32 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{wizardStep === WizardSteps.PlatformInputs &&
|
{wizardStep === WizardSteps.PlatformInputs && (
|
||||||
selectedPlatform?.platform === "env-key" && (
|
<>
|
||||||
<motion.div
|
{selectedPlatform?.platform === "env-key" && (
|
||||||
key="env-key-step"
|
<motion.div
|
||||||
transition={{ duration: 0.1 }}
|
key="env-key-step"
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
transition={{ duration: 0.1 }}
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
exit={{ opacity: 0, translateX: -30 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
>
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
<EnvKeyPlatformModal onClose={() => handleFormReset(false)} />
|
>
|
||||||
</motion.div>
|
<EnvKeyPlatformModal onClose={() => handleFormReset(false)} />
|
||||||
)}
|
</motion.div>
|
||||||
|
)}
|
||||||
|
{selectedPlatform?.platform === "vault" && (
|
||||||
|
<motion.div
|
||||||
|
key="vault-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<VaultPlatformModal onClose={() => handleFormReset(false)} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
@@ -0,0 +1,222 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, Input, Tooltip } from "@app/components/v2";
|
||||||
|
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||||
|
import { useImportVault } from "@app/hooks/api/migration/mutations";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum VaultMappingType {
|
||||||
|
KeyVault = "key-vault",
|
||||||
|
Namespace = "namespace"
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAPPING_TYPE_MENU_ITEMS = [
|
||||||
|
{
|
||||||
|
value: VaultMappingType.KeyVault,
|
||||||
|
label: "Key Vaults",
|
||||||
|
tooltip: (
|
||||||
|
<div>
|
||||||
|
When using key vaults for mapping, each key vault within Vault will be created in Infisical
|
||||||
|
as a project. Each secret path inside the key vault, will be created as an environment
|
||||||
|
inside the corresponding project. When using Key Vaults as the project mapping type, a
|
||||||
|
default environment called "Production" will be created for each project, which
|
||||||
|
will contain the secrets from the key vault.
|
||||||
|
<div className="mt-4 flex flex-col gap-1 text-sm">
|
||||||
|
<div>Key Vault → Project</div>
|
||||||
|
<div>Default Environment (Production)</div>
|
||||||
|
<div>Secret Path → Secret Folder</div>
|
||||||
|
<div>Secret data → Secrets</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VaultMappingType.Namespace,
|
||||||
|
label: "Namespaces",
|
||||||
|
tooltip: (
|
||||||
|
<div>
|
||||||
|
When using namespaces for mapping, each namespace within Vault will be created in Infisical
|
||||||
|
as a project. Each key vault (KV) inside the namespace, will be created as an environment
|
||||||
|
inside the corresponding project.
|
||||||
|
<div className="mt-4 flex flex-col gap-1 text-sm">
|
||||||
|
<div>Namespace → Project</div>
|
||||||
|
<div>Key Vault → Project Environment</div>
|
||||||
|
<div>Secret Path → Secret Folder</div>
|
||||||
|
<div>Secret data → Secrets</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const VaultPlatformModal = ({ onClose }: Props) => {
|
||||||
|
const formSchema = z.object({
|
||||||
|
vaultUrl: z.string().min(1),
|
||||||
|
vaultNamespace: z.string().trim().optional(),
|
||||||
|
vaultAccessToken: z.string().min(1),
|
||||||
|
mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault)
|
||||||
|
});
|
||||||
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const { mutateAsync: importVault } = useImportVault();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isLoading, isDirty, isSubmitting, isValid, errors }
|
||||||
|
} = useForm<TFormData>({
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
isSubmitting,
|
||||||
|
isLoading,
|
||||||
|
isValid,
|
||||||
|
errors
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: TFormData) => {
|
||||||
|
await importVault({
|
||||||
|
vaultAccessToken: data.vaultAccessToken,
|
||||||
|
vaultNamespace: data.vaultNamespace,
|
||||||
|
vaultUrl: data.vaultUrl,
|
||||||
|
mappingType: data.mappingType
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
title: "Import started",
|
||||||
|
text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<NoticeBannerV2 title="Vault KV Secret Engine Import" className="mb-4">
|
||||||
|
<p className="text-sm">
|
||||||
|
The Vault migration currently supports importing static secrets from Vault
|
||||||
|
Dedicated/Self-Hosted.
|
||||||
|
<div className="mt-2 text-xs opacity-80">
|
||||||
|
Currently only KV Secret Engine V2 is supported for Vault migrations.
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</NoticeBannerV2>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultUrl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault URL"
|
||||||
|
isRequired
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultNamespace"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault Namespace"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input type="text" placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="vaultAccessToken"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Vault Access Token"
|
||||||
|
isRequired
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input type="password" placeholder="" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="mappingType"
|
||||||
|
defaultValue={VaultMappingType.KeyVault}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Mapping"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
errorText={error?.message}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<div className="mt-2 grid h-full w-full grid-cols-2 gap-4">
|
||||||
|
{MAPPING_TYPE_MENU_ITEMS.map((el) => (
|
||||||
|
<div
|
||||||
|
key={el.value}
|
||||||
|
className={twMerge(
|
||||||
|
"flex w-full cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all",
|
||||||
|
field.value === el.value
|
||||||
|
? "border-primary-700 border-opacity-70 bg-mineshaft-600 opacity-100"
|
||||||
|
: "hover:border-primary-700 hover:bg-mineshaft-600"
|
||||||
|
)}
|
||||||
|
onClick={() => field.onChange(el.value)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
field.onChange(el.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="text-center text-sm">{el.label}</div>
|
||||||
|
{el.tooltip && (
|
||||||
|
<div className="text-center text-sm">
|
||||||
|
<Tooltip content={el.tooltip} className="max-w-96">
|
||||||
|
<FontAwesomeIcon className="opacity-60" icon={faQuestionCircle} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-6 flex items-center space-x-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={!isDirty || isSubmitting || isLoading || !isValid}
|
||||||
|
>
|
||||||
|
Import data
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1 @@
|
|||||||
|
export { ExternalMigrationsTab } from "./ExternalMigrationsTab";
|
@@ -1 +0,0 @@
|
|||||||
export { ImportTab } from "./ImportTab";
|
|
@@ -5,7 +5,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
|||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
|
||||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||||
import { ImportTab } from "../ImportTab";
|
import { ExternalMigrationsTab } from "../ExternalMigrationsTab";
|
||||||
import { KmipTab } from "../KmipTab/OrgKmipTab";
|
import { KmipTab } from "../KmipTab/OrgKmipTab";
|
||||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||||
@@ -39,7 +39,11 @@ export const OrgTabGroup = () => {
|
|||||||
component: OrgWorkflowIntegrationTab
|
component: OrgWorkflowIntegrationTab
|
||||||
},
|
},
|
||||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
|
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
|
||||||
{ name: "Import", key: "tab-import", component: ImportTab },
|
{
|
||||||
|
name: "External Migrations",
|
||||||
|
key: "tab-external-migrations",
|
||||||
|
component: ExternalMigrationsTab
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Project Templates",
|
name: "Project Templates",
|
||||||
key: "project-templates",
|
key: "project-templates",
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync";
|
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||||
|
|
||||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
@@ -53,6 +53,7 @@ import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
|||||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||||
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||||
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
||||||
|
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
||||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||||
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
|
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
|
||||||
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
|
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
|
||||||
@@ -104,6 +105,8 @@ const Page = () => {
|
|||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { mutateAsync: createCommit } = useCreateCommit();
|
const { mutateAsync: createCommit } = useCreateCommit();
|
||||||
|
|
||||||
|
const tableRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const { isBatchMode, pendingChanges } = useBatchMode();
|
const { isBatchMode, pendingChanges } = useBatchMode();
|
||||||
const { loadPendingChanges, setExistingKeys } = useBatchModeActions();
|
const { loadPendingChanges, setExistingKeys } = useBatchModeActions();
|
||||||
@@ -483,6 +486,14 @@ const Page = () => {
|
|||||||
handlePopUpClose("snapshots");
|
handlePopUpClose("snapshots");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({
|
||||||
|
initialWidth: 320,
|
||||||
|
minWidth: 100,
|
||||||
|
maxWidth: tableRef.current
|
||||||
|
? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely
|
||||||
|
: 800
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// restore filters for path if set
|
// restore filters for path if set
|
||||||
const restore = filterHistory.get(secretPath);
|
const restore = filterHistory.get(secretPath);
|
||||||
@@ -787,6 +798,7 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
ref={tableRef}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300",
|
"thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300",
|
||||||
isNotEmpty && "rounded-b-none"
|
isNotEmpty && "rounded-b-none"
|
||||||
@@ -820,20 +832,34 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div
|
<div className="relative">
|
||||||
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4"
|
<div
|
||||||
role="button"
|
tabIndex={-1}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onClick={handleSortToggle}
|
className={`absolute -right-[0.05rem] z-40 h-full w-0.5 cursor-ew-resize hover:bg-blue-400/20 ${
|
||||||
onKeyDown={(evt) => {
|
isResizing ? "bg-blue-400/75" : "bg-transparent"
|
||||||
if (evt.key === "Enter") handleSortToggle();
|
}`}
|
||||||
}}
|
onMouseDown={handleMouseDown}
|
||||||
>
|
|
||||||
Key
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
|
|
||||||
className="ml-2"
|
|
||||||
/>
|
/>
|
||||||
|
<div className="pointer-events-none absolute -right-[0.04rem] top-2 z-30">
|
||||||
|
<div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex flex-shrink-0 items-center border-r border-mineshaft-600 py-2 pl-4"
|
||||||
|
style={{ width: colWidth }}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleSortToggle}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") handleSortToggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Key
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow px-4 py-2">Value</div>
|
<div className="flex-grow px-4 py-2">Value</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -927,6 +953,7 @@ const Page = () => {
|
|||||||
)}
|
)}
|
||||||
{canReadSecret && Boolean(mergedSecrets?.length) && (
|
{canReadSecret && Boolean(mergedSecrets?.length) && (
|
||||||
<SecretListView
|
<SecretListView
|
||||||
|
colWidth={colWidth}
|
||||||
secrets={mergedSecrets}
|
secrets={mergedSecrets}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
|
@@ -235,7 +235,7 @@ export const FolderListView = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
<div className="flex w-16 items-center justify-between border-l border-mineshaft-600 px-3 py-3">
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="edit-folder"
|
ariaLabel="edit-folder"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
@@ -90,6 +90,7 @@ type Props = {
|
|||||||
}[];
|
}[];
|
||||||
isPending?: boolean;
|
isPending?: boolean;
|
||||||
pendingAction?: PendingAction;
|
pendingAction?: PendingAction;
|
||||||
|
colWidth: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretItem = memo(
|
export const SecretItem = memo(
|
||||||
@@ -108,7 +109,8 @@ export const SecretItem = memo(
|
|||||||
handleSecretShare,
|
handleSecretShare,
|
||||||
importedBy,
|
importedBy,
|
||||||
isPending,
|
isPending,
|
||||||
pendingAction
|
pendingAction,
|
||||||
|
colWidth
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
"editSecret"
|
"editSecret"
|
||||||
@@ -383,7 +385,10 @@ export const SecretItem = memo(
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
<div
|
||||||
|
className="flex h-11 flex-shrink-0 items-center px-4 py-2"
|
||||||
|
style={{ width: colWidth }}
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="key"
|
name="key"
|
||||||
control={control}
|
control={control}
|
||||||
|
@@ -51,6 +51,7 @@ type Props = {
|
|||||||
isImported: boolean;
|
isImported: boolean;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
|
colWidth: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretListView = ({
|
export const SecretListView = ({
|
||||||
@@ -62,7 +63,8 @@ export const SecretListView = ({
|
|||||||
isVisible,
|
isVisible,
|
||||||
isProtectedBranch = false,
|
isProtectedBranch = false,
|
||||||
usedBySecretSyncs,
|
usedBySecretSyncs,
|
||||||
importedBy
|
importedBy,
|
||||||
|
colWidth
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -554,6 +556,7 @@ export const SecretListView = ({
|
|||||||
))}
|
))}
|
||||||
{secrets.map((secret) => (
|
{secrets.map((secret) => (
|
||||||
<SecretItem
|
<SecretItem
|
||||||
|
colWidth={colWidth}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
tags={wsTags}
|
tags={wsTags}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||||
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render";
|
||||||
import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync";
|
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secretSync: TRenderSync;
|
secretSync: TRenderSync;
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||||
|
import { Badge } from "@app/components/v2";
|
||||||
|
import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretSync: TRenderSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RenderSyncOptionsSection = ({ secretSync }: Props) => {
|
||||||
|
const {
|
||||||
|
syncOptions: { autoRedeployServices }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericFieldLabel label="Auto Redeploy Services">
|
||||||
|
<Badge variant={autoRedeployServices ? "success" : "danger"}>
|
||||||
|
{autoRedeployServices ? "Enabled" : "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</GenericFieldLabel>
|
||||||
|
);
|
||||||
|
};
|
@@ -13,6 +13,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
|
|||||||
|
|
||||||
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
|
import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection";
|
||||||
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
|
import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection";
|
||||||
|
import { RenderSyncOptionsSection } from "./RenderSyncOptionsSection";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secretSync: TSecretSync;
|
secretSync: TSecretSync;
|
||||||
@@ -40,6 +41,9 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
|||||||
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
|
<AwsSecretsManagerSyncOptionsSection secretSync={secretSync} />
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.Render:
|
||||||
|
AdditionalSyncOptionsComponent = <RenderSyncOptionsSection secretSync={secretSync} />;
|
||||||
|
break;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
@@ -56,7 +60,6 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
|||||||
case SecretSync.OCIVault:
|
case SecretSync.OCIVault:
|
||||||
case SecretSync.OnePass:
|
case SecretSync.OnePass:
|
||||||
case SecretSync.Heroku:
|
case SecretSync.Heroku:
|
||||||
case SecretSync.Render:
|
|
||||||
case SecretSync.Flyio:
|
case SecretSync.Flyio:
|
||||||
case SecretSync.GitLab:
|
case SecretSync.GitLab:
|
||||||
case SecretSync.CloudflarePages:
|
case SecretSync.CloudflarePages:
|
||||||
|