Merge pull request #2573 from Infisical/daniel/envkey-import-bug

feat: Process Envkey import in queue
This commit is contained in:
Maidul Islam
2024-10-13 22:48:59 -07:00
committed by GitHub
20 changed files with 488 additions and 190 deletions
@@ -240,7 +240,8 @@ export const secretSnapshotServiceFactory = ({
}, },
tx tx
); );
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
const snapshotSecrets = await snapshotSecretV2BridgeDAL.batchInsert(
secretVersions.map(({ id }) => ({ secretVersions.map(({ id }) => ({
secretVersionId: id, secretVersionId: id,
envId: folder.environment.envId, envId: folder.environment.envId,
@@ -248,7 +249,8 @@ export const secretSnapshotServiceFactory = ({
})), })),
tx tx
); );
const snapshotFolders = await snapshotFolderDAL.insertMany(
const snapshotFolders = await snapshotFolderDAL.batchInsert(
folderVersions.map(({ id }) => ({ folderVersions.map(({ id }) => ({
folderVersionId: id, folderVersionId: id,
envId: folder.environment.envId, envId: folder.environment.envId,
+11
View File
@@ -70,3 +70,14 @@ export const objectify = <T, Key extends string | number | symbol, Value = T>(
{} as Record<Key, Value> {} as Record<Key, Value>
); );
}; };
/**
* Chunks an array into smaller arrays of the given size.
*/
export const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
};
+18 -3
View File
@@ -1,7 +1,7 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq"; import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis"; import Redis from "ioredis";
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { import {
TScanFullRepoEventPayload, TScanFullRepoEventPayload,
@@ -32,7 +32,8 @@ export enum QueueName {
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update" AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
} }
export enum QueueJobs { export enum QueueJobs {
@@ -56,7 +57,8 @@ export enum QueueJobs {
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update", IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update" ServiceTokenStatusUpdate = "service-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -166,6 +168,19 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration; name: QueueJobs.ProjectV3Migration;
payload: { projectId: string }; payload: { projectId: string };
}; };
[QueueName.ImportSecretsFromExternalSource]: {
name: QueueJobs.ImportSecretsFromExternalSource;
payload: {
actorEmail: string;
data: {
iv: string;
tag: string;
ciphertext: string;
algorithm: SecretEncryptionAlgo;
encoding: SecretKeyEncoding;
};
};
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
+20 -5
View File
@@ -97,6 +97,7 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
@@ -1202,12 +1203,26 @@ export const registerRoutes = async (
permissionService permissionService
}); });
const migrationService = externalMigrationServiceFactory({ const externalMigrationQueue = externalMigrationQueueFactory({
projectService,
orgService,
projectEnvService, projectEnvService,
permissionService, projectDAL,
secretService projectService,
smtpService,
kmsService,
projectEnvDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderDAL,
secretDAL: secretV2BridgeDAL,
queueService,
secretV2BridgeService
});
const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -4,22 +4,41 @@ import sjcl from "sjcl";
import tweetnacl from "tweetnacl"; import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util"; import tweetnaclUtil from "tweetnacl-util";
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas"; import { SecretType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } 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 { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgServiceFactory } from "../org/org-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
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 { TProjectEnvServiceFactory } from "../project-env/project-env-service"; import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { fnSecretBulkInsert, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types"; import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
export type TImportDataIntoInfisicalDTO = { export type TImportDataIntoInfisicalDTO = {
projectService: TProjectServiceFactory; projectDAL: Pick<TProjectDALFactory, "transaction">;
orgService: TOrgServiceFactory; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
projectEnvService: TProjectEnvServiceFactory; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretService: TSecretServiceFactory;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
input: TImportInfisicalDataCreate; input: TImportInfisicalDataCreate;
}; };
@@ -46,13 +65,13 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON; const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
const infisicalImportData: InfisicalImportData = { const infisicalImportData: InfisicalImportData = {
projects: new Map<string, { name: string; id: string }>(), projects: [],
environments: new Map<string, { name: string; id: string; projectId: string }>(), environments: [],
secrets: new Map<string, { name: string; id: string; projectId: string; environmentId: string; value: string }>() secrets: []
}; };
parsedJson.apps.forEach((app: { name: string; id: string }) => { parsedJson.apps.forEach((app: { name: string; id: string }) => {
infisicalImportData.projects.set(app.id, { name: app.name, id: app.id }); infisicalImportData.projects.push({ name: app.name, id: app.id });
}); });
// string to string map for env templates // string to string map for env templates
@@ -63,7 +82,7 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
// environments // environments
for (const env of parsedJson.baseEnvironments) { for (const env of parsedJson.baseEnvironments) {
infisicalImportData.environments?.set(env.id, { infisicalImportData.environments.push({
id: env.id, id: env.id,
name: envTemplates.get(env.environmentRoleId)!, name: envTemplates.get(env.environmentRoleId)!,
projectId: env.envParentId projectId: env.envParentId
@@ -75,9 +94,8 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
if (!env.includes("|")) { if (!env.includes("|")) {
const envData = parsedJson.envs[env]; const envData = parsedJson.envs[env];
for (const secret of Object.keys(envData.variables)) { for (const secret of Object.keys(envData.variables)) {
const id = randomUUID(); infisicalImportData.secrets.push({
infisicalImportData.secrets?.set(id, { id: randomUUID(),
id,
name: secret, name: secret,
environmentId: env, environmentId: env,
value: envData.variables[secret].val value: envData.variables[secret].val
@@ -91,9 +109,14 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
export const importDataIntoInfisicalFn = async ({ export const importDataIntoInfisicalFn = async ({
projectService, projectService,
orgService, projectEnvDAL,
projectEnvService, projectDAL,
secretService, secretDAL,
kmsService,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
input: { data, actor, actorId, actorOrgId, actorAuthMethod } input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => { }: TImportDataIntoInfisicalDTO) => {
// Import data to infisical // Import data to infisical
@@ -104,94 +127,132 @@ export const importDataIntoInfisicalFn = async ({
const originalToNewProjectId = new Map<string, string>(); const originalToNewProjectId = new Map<string, string>();
const originalToNewEnvironmentId = new Map<string, string>(); const originalToNewEnvironmentId = new Map<string, string>();
for await (const [id, project] of data.projects) { await projectDAL.transaction(async (tx) => {
const newProject = await projectService for await (const project of data.projects) {
.createProject({ const newProject = await projectService
actor, .createProject({
actorId,
actorOrgId,
actorAuthMethod,
workspaceName: project.name,
createDefaultEnvs: false
})
.catch(() => {
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
});
originalToNewProjectId.set(project.id, newProject.id);
}
// Invite user importing projects
const invites = await orgService.inviteUserToOrganization({
actorAuthMethod,
actorId,
actorOrgId,
actor,
inviteeEmails: [],
orgId: actorOrgId,
organizationRoleSlug: OrgMembershipRole.NoAccess,
projects: Array.from(originalToNewProjectId.values()).map((project) => ({
id: project,
projectRoleSlug: [ProjectMembershipRole.Member]
}))
});
if (!invites) {
throw new BadRequestError({ message: `Failed to invite user to projects: [userId:${actorId}]` });
}
// Import environments
if (data.environments) {
for await (const [id, environment] of data.environments) {
try {
const newEnvironment = await projectEnvService.createEnvironment({
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
name: environment.name, workspaceName: project.name,
projectId: originalToNewProjectId.get(environment.projectId)!, createDefaultEnvs: false,
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`) 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}]` });
}); });
if (!newEnvironment) { originalToNewProjectId.set(project.id, newProject.id);
logger.error(`Failed to import environment: [name:${environment.name}] [id:${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)}`);
const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx);
if (existingEnv) {
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to import environment: [name:${environment.name}] [id:${id}]` message: `Environment with slug '${slug}' already exist`,
name: "CreateEnvironment"
}); });
} }
originalToNewEnvironmentId.set(id, newEnvironment.slug);
} catch (error) { const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
throw new BadRequestError({ const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
message: `Failed to import environment: ${environment.name}]`, await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
name: "EnvKeyMigrationImportEnvironment"
originalToNewEnvironmentId.set(environment.id, doc.slug);
}
}
if (data.secrets && data.secrets.length > 0) {
const mappedToEnvironmentId = new Map<
string,
{
secretKey: string;
secretValue: string;
}[]
>();
for (const secret of data.secrets) {
if (!mappedToEnvironmentId.has(secret.environmentId)) {
mappedToEnvironmentId.set(secret.environmentId, []);
}
mappedToEnvironmentId.get(secret.environmentId)!.push({
secretKey: secret.name,
secretValue: secret.value || ""
}); });
} }
}
}
// Import secrets // for each of the mappedEnvironmentId
if (data.secrets) { for await (const [envId, secrets] of mappedToEnvironmentId) {
for await (const [id, secret] of data.secrets) { const environment = data.environments.find((env) => env.id === envId);
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId; const projectId = originalToNewProjectId.get(environment?.projectId as string)!;
if (!dataProjectId) {
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` }); if (!projectId) {
} throw new BadRequestError({ message: `Failed to import secret, project not found` });
const projectId = originalToNewProjectId.get(dataProjectId); }
const newSecret = await secretService.createSecretRaw({
actorId, const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
actor, {
actorOrgId, type: KmsDataKey.SecretManager,
environment: originalToNewEnvironmentId.get(secret.environmentId)!, projectId
actorAuthMethod, },
projectId: projectId!, tx
secretPath: "/", );
secretName: secret.name,
type: SecretType.Shared, const envSlug = originalToNewEnvironmentId.get(envId)!;
secretValue: secret.value || "" const folder = await folderDAL.findBySecretPath(projectId, envSlug, "/", tx);
}); if (!folder)
if (!newSecret) { throw new NotFoundError({
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` }); message: `Folder not found for the given environment slug (${envSlug}) & secret path (/)`,
name: "Create secret"
});
const secretsByKeys = await secretDAL.findBySecretKeys(
folder.id,
secrets.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(",")}`
});
}
const secretBatches = chunkArray(secrets, 2500);
for await (const secretBatch of secretBatches) {
await fnSecretBulkInsert({
inputSecrets: secretBatch.map((el) => {
const references = getAllNestedSecretReferences(el.secretValue);
return {
version: 1,
encryptedValue: el.secretValue
? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
: undefined,
key: el.secretKey,
references,
type: SecretType.Shared
};
}),
folderId: folder.id,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
});
}
} }
} }
} });
}; };
@@ -0,0 +1,141 @@
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { importDataIntoInfisicalFn } from "./external-migration-fns";
import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types";
export type TExternalMigrationQueueFactoryDep = {
smtpService: TSmtpService;
queueService: TQueueServiceFactory;
projectDAL: Pick<TProjectDALFactory, "transaction">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
};
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
export const externalMigrationQueueFactory = ({
queueService,
projectService,
smtpService,
projectDAL,
projectEnvService,
secretV2BridgeService,
kmsService,
projectEnvDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
}: TExternalMigrationQueueFactoryDep) => {
const startImport = async (dto: {
actorEmail: string;
data: {
iv: string;
tag: string;
ciphertext: string;
algorithm: SecretEncryptionAlgo;
encoding: SecretKeyEncoding;
};
}) => {
await queueService.queue(
QueueName.ImportSecretsFromExternalSource,
QueueJobs.ImportSecretsFromExternalSource,
dto,
{
removeOnComplete: true,
removeOnFail: true
}
);
};
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
try {
const { data, actorEmail } = job.data;
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import started",
substitutions: {
provider: ExternalPlatforms.EnvKey
},
template: SmtpTemplates.ExternalImportStarted
});
const decrypted = infisicalSymmetricDecrypt({
ciphertext: data.ciphertext,
iv: data.iv,
keyEncoding: data.encoding,
tag: data.tag
});
const decryptedJson = JSON.parse(decrypted) as TImportInfisicalDataCreate;
await importDataIntoInfisicalFn({
input: decryptedJson,
projectDAL,
projectEnvDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
kmsService,
projectService,
projectEnvService,
secretV2BridgeService
});
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import successful",
substitutions: {
provider: ExternalPlatforms.EnvKey
},
template: SmtpTemplates.ExternalImportSuccessful
});
} catch (err) {
await smtpService.sendMail({
recipients: [job.data.actorEmail],
subjectLine: "Infisical import failed",
substitutions: {
provider: ExternalPlatforms.EnvKey,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
error: (err as any)?.message || "Unknown error"
},
template: SmtpTemplates.ExternalImportFailed
});
logger.error(err, "Failed to import data from external source");
}
});
return {
startImport
};
};
@@ -1,30 +1,25 @@
import { OrgMembershipRole } from "@app/db/schemas"; import { OrgMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { ForbiddenRequestError } from "@app/lib/errors"; import { ForbiddenRequestError } from "@app/lib/errors";
import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectServiceFactory } from "../project/project-service"; import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; import { TExternalMigrationQueueFactory } from "./external-migration-queue";
import { TSecretServiceFactory } from "../secret/secret-service";
import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TImportEnvKeyDataCreate } from "./external-migration-types"; import { TImportEnvKeyDataCreate } from "./external-migration-types";
type TExternalMigrationServiceFactoryDep = { type TExternalMigrationServiceFactoryDep = {
projectService: TProjectServiceFactory;
orgService: TOrgServiceFactory;
projectEnvService: TProjectEnvServiceFactory;
secretService: TSecretServiceFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
externalMigrationQueue: TExternalMigrationQueueFactory;
userDAL: Pick<TUserDALFactory, "findById">;
}; };
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>; export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
export const externalMigrationServiceFactory = ({ export const externalMigrationServiceFactory = ({
projectService,
orgService,
projectEnvService,
permissionService, permissionService,
secretService externalMigrationQueue,
userDAL
}: TExternalMigrationServiceFactoryDep) => { }: TExternalMigrationServiceFactoryDep) => {
const importEnvKeyData = async ({ const importEnvKeyData = async ({
decryptionKey, decryptionKey,
@@ -41,21 +36,28 @@ export const externalMigrationServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (membership.role !== OrgMembershipRole.Admin) { if (membership.role !== OrgMembershipRole.Admin) {
throw new ForbiddenRequestError({ message: "Only admins can import data" }); throw new ForbiddenRequestError({ message: "Only admins can import data" });
} }
const user = await userDAL.findById(actorId);
const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson); const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
const envKeyData = await parseEnvKeyDataFn(json); const envKeyData = await parseEnvKeyDataFn(json);
const response = await importDataIntoInfisicalFn({
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod }, const stringifiedJson = JSON.stringify({
projectService, data: envKeyData,
orgService, actor,
projectEnvService, actorId,
secretService actorOrgId,
actorAuthMethod
});
const encrypted = infisicalSymmetricEncypt(stringifiedJson);
await externalMigrationQueue.startImport({
actorEmail: user.email!,
data: encrypted
}); });
return response;
}; };
return { return {
@@ -1,26 +1,9 @@
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type InfisicalImportData = { export type InfisicalImportData = {
projects: Map<string, { name: string; id: string }>; projects: Array<{ name: string; id: string }>;
environments: Array<{ name: string; id: string; projectId: string }>;
environments?: Map< secrets: Array<{ name: string; id: string; environmentId: string; value: string }>;
string,
{
name: string;
id: string;
projectId: string;
}
>;
secrets?: Map<
string,
{
name: string;
id: string;
environmentId: string;
value?: string;
}
>;
}; };
export type TImportEnvKeyDataCreate = { export type TImportEnvKeyDataCreate = {
@@ -104,3 +87,7 @@ export type TEnvKeyExportJSON = {
} }
>; >;
}; };
export enum ExternalPlatforms {
EnvKey = "EnvKey"
}
+37 -30
View File
@@ -160,8 +160,8 @@ export const kmsServiceFactory = ({
* In mean time the rest of the request will wait until creation is finished followed by getting the created on * In mean time the rest of the request will wait until creation is finished followed by getting the created on
* In real time this would be milliseconds * In real time this would be milliseconds
*/ */
const getOrgKmsKeyId = async (orgId: string) => { const getOrgKmsKeyId = async (orgId: string, trx?: Knex) => {
let org = await orgDAL.findById(orgId); let org = await orgDAL.findById(orgId, trx);
if (!org) { if (!org) {
throw new NotFoundError({ message: "Org not found" }); throw new NotFoundError({ message: "Org not found" });
@@ -180,9 +180,9 @@ export const kmsServiceFactory = ({
waitingCb: () => logger.info("KMS. Waiting for org key to be created") waitingCb: () => logger.info("KMS. Waiting for org key to be created")
}); });
org = await orgDAL.findById(orgId); org = await orgDAL.findById(orgId, trx);
} else { } else {
const keyId = await orgDAL.transaction(async (tx) => { const keyId = await (trx || orgDAL).transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx); org = await orgDAL.findById(orgId, tx);
if (org.kmsDefaultKeyId) { if (org.kmsDefaultKeyId) {
return org.kmsDefaultKeyId; return org.kmsDefaultKeyId;
@@ -240,11 +240,12 @@ export const kmsServiceFactory = ({
const decryptWithKmsKey = async ({ const decryptWithKmsKey = async ({
kmsId, kmsId,
depth = 0 depth = 0,
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number }) => { tx
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number; tx?: Knex }) => {
if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" }); if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" });
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) { if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found" }); throw new NotFoundError({ message: "KMS ID not found" });
} }
@@ -261,7 +262,8 @@ export const kmsServiceFactory = ({
// we put a limit of depth to avoid too many cycles // we put a limit of depth to avoid too many cycles
const orgKmsDecryptor = await decryptWithKmsKey({ const orgKmsDecryptor = await decryptWithKmsKey({
kmsId: kmsDoc.orgKms.id, kmsId: kmsDoc.orgKms.id,
depth: depth + 1 depth: depth + 1,
tx
}); });
const orgKmsDataKey = await orgKmsDecryptor({ const orgKmsDataKey = await orgKmsDecryptor({
@@ -375,9 +377,9 @@ export const kmsServiceFactory = ({
}; };
}; };
const $getOrgKmsDataKey = async (orgId: string) => { const $getOrgKmsDataKey = async (orgId: string, trx?: Knex) => {
const kmsKeyId = await getOrgKmsKeyId(orgId); const kmsKeyId = await getOrgKmsKeyId(orgId, trx);
let org = await orgDAL.findById(orgId); let org = await orgDAL.findById(orgId, trx);
if (!org) { if (!org) {
throw new NotFoundError({ message: "Org not found" }); throw new NotFoundError({ message: "Org not found" });
@@ -396,9 +398,9 @@ export const kmsServiceFactory = ({
waitingCb: () => logger.info("KMS. Waiting for org data key to be created") waitingCb: () => logger.info("KMS. Waiting for org data key to be created")
}); });
org = await orgDAL.findById(orgId); org = await orgDAL.findById(orgId, trx);
} else { } else {
const orgDataKey = await orgDAL.transaction(async (tx) => { const orgDataKey = await (trx || orgDAL).transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx); org = await orgDAL.findById(orgId, tx);
if (org.kmsEncryptedDataKey) { if (org.kmsEncryptedDataKey) {
return; return;
@@ -455,8 +457,8 @@ export const kmsServiceFactory = ({
}); });
}; };
const getProjectSecretManagerKmsKeyId = async (projectId: string) => { const getProjectSecretManagerKmsKeyId = async (projectId: string, trx?: Knex) => {
let project = await projectDAL.findById(projectId); let project = await projectDAL.findById(projectId, trx);
if (!project) { if (!project) {
throw new NotFoundError({ message: "Project not found" }); throw new NotFoundError({ message: "Project not found" });
} }
@@ -477,7 +479,7 @@ export const kmsServiceFactory = ({
project = await projectDAL.findById(projectId); project = await projectDAL.findById(projectId);
} else { } else {
const kmsKeyId = await projectDAL.transaction(async (tx) => { const kmsKeyId = await (trx || projectDAL).transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx); project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerKeyId) { if (project.kmsSecretManagerKeyId) {
return project.kmsSecretManagerKeyId; return project.kmsSecretManagerKeyId;
@@ -520,9 +522,9 @@ export const kmsServiceFactory = ({
return project.kmsSecretManagerKeyId; return project.kmsSecretManagerKeyId;
}; };
const $getProjectSecretManagerKmsDataKey = async (projectId: string) => { const $getProjectSecretManagerKmsDataKey = async (projectId: string, trx?: Knex) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId); const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId, trx);
let project = await projectDAL.findById(projectId); let project = await projectDAL.findById(projectId, trx);
if (!project.kmsSecretManagerEncryptedDataKey) { if (!project.kmsSecretManagerEncryptedDataKey) {
const lock = await keyStore const lock = await keyStore
@@ -538,18 +540,21 @@ export const kmsServiceFactory = ({
delay: 500 delay: 500
}); });
project = await projectDAL.findById(projectId); project = await projectDAL.findById(projectId, trx);
} else { } else {
const projectDataKey = await projectDAL.transaction(async (tx) => { const projectDataKey = await (trx || projectDAL).transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx); project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerEncryptedDataKey) { if (project.kmsSecretManagerEncryptedDataKey) {
return; return;
} }
const dataKey = randomSecureBytes(); const dataKey = randomSecureBytes();
const kmsEncryptor = await encryptWithKmsKey({ const kmsEncryptor = await encryptWithKmsKey(
kmsId: kmsKeyId {
}); kmsId: kmsKeyId
},
tx
);
const { cipherTextBlob } = await kmsEncryptor({ const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey plainText: dataKey
@@ -585,7 +590,8 @@ export const kmsServiceFactory = ({
} }
const kmsDecryptor = await decryptWithKmsKey({ const kmsDecryptor = await decryptWithKmsKey({
kmsId: kmsKeyId kmsId: kmsKeyId,
tx: trx
}); });
return kmsDecryptor({ return kmsDecryptor({
@@ -593,13 +599,13 @@ export const kmsServiceFactory = ({
}); });
}; };
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => { const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
switch (dto.type) { switch (dto.type) {
case KmsDataKey.SecretManager: { case KmsDataKey.SecretManager: {
return $getProjectSecretManagerKmsDataKey(dto.projectId); return $getProjectSecretManagerKmsDataKey(dto.projectId, trx);
} }
default: { default: {
return $getOrgKmsDataKey(dto.orgId); return $getOrgKmsDataKey(dto.orgId, trx);
} }
} }
}; };
@@ -607,8 +613,9 @@ export const kmsServiceFactory = ({
// by keeping the decrypted data key in inner scope // by keeping the decrypted data key in inner scope
// none of the entities outside can interact directly or expose the data key // none of the entities outside can interact directly or expose the data key
// NOTICE: If changing here update migrations/utils/kms // NOTICE: If changing here update migrations/utils/kms
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => { const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
const dataKey = await $getDataKey(encryptionContext); const dataKey = await $getDataKey(encryptionContext, trx);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return { return {
+1 -6
View File
@@ -26,18 +26,13 @@ export type TDeleteOrgMembershipDTO = {
}; };
export type TInviteUserToOrgDTO = { export type TInviteUserToOrgDTO = {
actorId: string;
actor: ActorType;
orgId: string;
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
inviteeEmails: string[]; inviteeEmails: string[];
organizationRoleSlug: string; organizationRoleSlug: string;
projects?: { projects?: {
id: string; id: string;
projectRoleSlug?: string[]; projectRoleSlug?: string[];
}[]; }[];
}; } & TOrgPermission;
export type TVerifyUserToOrgDTO = { export type TVerifyUserToOrgDTO = {
email: string; email: string;
@@ -147,6 +147,7 @@ export const projectServiceFactory = ({
workspaceName, workspaceName,
slug: projectSlug, slug: projectSlug,
kmsKeyId, kmsKeyId,
tx: trx,
createDefaultEnvs = true createDefaultEnvs = true
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -169,7 +170,7 @@ export const projectServiceFactory = ({
}); });
} }
const results = await projectDAL.transaction(async (tx) => { const results = await (trx || projectDAL).transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(organization.id, tx); const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) { if (kmsKeyId) {
@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TProjectKeys } from "@app/db/schemas"; import { TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
@@ -30,6 +32,7 @@ export type TCreateProjectDTO = {
slug?: string; slug?: string;
kmsKeyId?: string; kmsKeyId?: string;
createDefaultEnvs?: boolean; createDefaultEnvs?: boolean;
tx?: Knex;
}; };
export type TDeleteProjectBySlugDTO = { export type TDeleteProjectBySlugDTO = {
@@ -82,7 +82,10 @@ export const fnSecretBulkInsert = async ({
}) })
); );
const newSecrets = await secretDAL.insertMany(sanitizedInputSecrets.map((el) => ({ ...el, folderId }))); const newSecrets = await secretDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
tx
);
const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key); const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key);
const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) => const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) =>
secretTags.map((tag) => ({ secretTags.map((tag) => ({
+1 -1
View File
@@ -85,7 +85,7 @@ type TSecretQueueFactoryDep = {
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
userDAL: Pick<TUserDALFactory, "findById">; userDAL: Pick<TUserDALFactory, "findById">;
secretVersionTagDAL: TSecretVersionTagDALFactory; secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: TKmsServiceFactory;
secretV2BridgeDAL: TSecretV2BridgeDALFactory; secretV2BridgeDAL: TSecretV2BridgeDALFactory;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
+4 -1
View File
@@ -34,7 +34,10 @@ export enum SmtpTemplates {
WorkspaceInvite = "workspaceInvitation.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars", ScimUserProvisioned = "scimUserProvisioned.handlebars",
PkiExpirationAlert = "pkiExpirationAlert.handlebars", PkiExpirationAlert = "pkiExpirationAlert.handlebars",
IntegrationSyncFailed = "integrationSyncFailed.handlebars" IntegrationSyncFailed = "integrationSyncFailed.handlebars",
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
ExternalImportFailed = "externalImportFailed.handlebars",
ExternalImportStarted = "externalImportStarted.handlebars"
} }
export enum SmtpHost { export enum SmtpHost {
@@ -0,0 +1,21 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import failed</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical has failed</h2>
<p>An import from
{{provider}}
to Infisical has failed due to unforeseen circumstances. Please re-try your import, and if the issue persists, you
can contact the Infisical team at [email protected].
</p>
<p>Error: {{error}}</p>
</body>
</html>
@@ -0,0 +1,17 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import in progress</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical is in progress</h2>
<p>An import from
{{provider}}
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
has finished or if it fails.</p>
</body>
</html>
@@ -0,0 +1,14 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Import successful</title>
</head>
<body>
<h2>An import from {{provider}} to Infisical was successful</h2>
<p>An import from {{provider}} was successful. Your data is now available in Infisical.</p>
</body>
</html>
@@ -18,11 +18,10 @@ export const useImportEnvKey = () => {
}; };
decryptionKey: string; decryptionKey: string;
}) => { }) => {
const { data } = await apiRequest.post("/api/v3/migrate/env-key/", { await apiRequest.post("/api/v3/migrate/env-key/", {
encryptedJson, encryptedJson,
decryptionKey decryptionKey
}); });
return data;
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
@@ -54,8 +54,9 @@ export const EnvKeyPlatformModal = ({ onClose }: Props) => {
decryptionKey: data.encryptionKey decryptionKey: data.encryptionKey
}); });
createNotification({ createNotification({
text: "Data imported successfully.", title: "Import started",
type: "success" 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(); onClose();