Skip to content

Commit

Permalink
feat: redis-based external imports
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielHougaard committed Oct 11, 2024
1 parent 09d2815 commit 162005d
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 110 deletions.
57 changes: 39 additions & 18 deletions backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { ForbiddenError, subject } from "@casl/ability";

import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import {
TableName,
TSecretSnapshotFolders,
TSecretSnapshotSecretsV2,
TSecretTagJunctionInsert,
TSecretV2TagJunctionInsert
} from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { chunkArray, groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
Expand Down Expand Up @@ -240,22 +246,37 @@ export const secretSnapshotServiceFactory = ({
},
tx
);
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
secretVersions.map(({ id }) => ({
secretVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);
const snapshotFolders = await snapshotFolderDAL.insertMany(
folderVersions.map(({ id }) => ({
folderVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);

const chunkedSnapshotSecrets = chunkArray(secretVersions, 2500);
const chunkedSnapshotFolders = chunkArray(folderVersions, 2500);
const snapshotSecrets: TSecretSnapshotSecretsV2[] = [];
const snapshotFolders: TSecretSnapshotFolders[] = [];

for await (const chunk of chunkedSnapshotSecrets) {
const result = await snapshotSecretV2BridgeDAL.insertMany(
chunk.map(({ id }) => ({
secretVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);

snapshotSecrets.push(...result);
}

for await (const chunk of chunkedSnapshotFolders) {
const result = await snapshotFolderDAL.insertMany(
chunk.map(({ id }) => ({
folderVersionId: id,
envId: folder.environment.envId,
snapshotId: newSnapshot.id
})),
tx
);

snapshotFolders.push(...result);
}

return { ...newSnapshot, secrets: snapshotSecrets, folder: snapshotFolders };
});
Expand Down
11 changes: 11 additions & 0 deletions backend/src/lib/fn/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,14 @@ export const objectify = <T, Key extends string | number | symbol, Value = T>(
{} 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;
};
21 changes: 18 additions & 3 deletions backend/src/queue/queue-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
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 {
TScanFullRepoEventPayload,
Expand Down Expand Up @@ -32,7 +32,8 @@ export enum QueueName {
SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update"
AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
}

export enum QueueJobs {
Expand All @@ -56,7 +57,8 @@ export enum QueueJobs {
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration",
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 = {
Expand Down Expand Up @@ -166,6 +168,19 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration;
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>;
Expand Down
16 changes: 12 additions & 4 deletions backend/src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-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 { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
Expand Down Expand Up @@ -1202,12 +1203,19 @@ export const registerRoutes = async (
permissionService
});

const migrationService = externalMigrationServiceFactory({
projectService,
const externalMigrationQueue = externalMigrationQueueFactory({
orgService,
projectEnvService,
permissionService,
secretService
projectService,
smtpService,
queueService,
secretV2BridgeService
});

const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService
});

await superAdminService.initServerCfg();
Expand Down
99 changes: 59 additions & 40 deletions backend/src/services/external-migration/external-migration-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@ import sjcl from "sjcl";
import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util";

import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { chunkArray } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";

import { TOrgServiceFactory } from "../org/org-service";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service";
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";

export type TImportDataIntoInfisicalDTO = {
projectService: TProjectServiceFactory;
orgService: TOrgServiceFactory;
projectEnvService: TProjectEnvServiceFactory;
secretService: TSecretServiceFactory;
projectService: Pick<TProjectServiceFactory, "createProject">;
orgService: Pick<TOrgServiceFactory, "inviteUserToOrganization">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;

input: TImportInfisicalDataCreate;
};
Expand All @@ -46,13 +47,13 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;

const infisicalImportData: InfisicalImportData = {
projects: new Map<string, { name: string; id: string }>(),
environments: new Map<string, { name: string; id: string; projectId: string }>(),
secrets: new Map<string, { name: string; id: string; projectId: string; environmentId: string; value: string }>()
projects: [],
environments: [],
secrets: []
};

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
Expand All @@ -63,7 +64,7 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica

// environments
for (const env of parsedJson.baseEnvironments) {
infisicalImportData.environments?.set(env.id, {
infisicalImportData.environments.push({
id: env.id,
name: envTemplates.get(env.environmentRoleId)!,
projectId: env.envParentId
Expand All @@ -75,9 +76,8 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
if (!env.includes("|")) {
const envData = parsedJson.envs[env];
for (const secret of Object.keys(envData.variables)) {
const id = randomUUID();
infisicalImportData.secrets?.set(id, {
id,
infisicalImportData.secrets.push({
id: randomUUID(),
name: secret,
environmentId: env,
value: envData.variables[secret].val
Expand All @@ -93,7 +93,7 @@ export const importDataIntoInfisicalFn = async ({
projectService,
orgService,
projectEnvService,
secretService,
secretV2BridgeService,
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => {
// Import data to infisical
Expand All @@ -104,7 +104,7 @@ export const importDataIntoInfisicalFn = async ({
const originalToNewProjectId = new Map<string, string>();
const originalToNewEnvironmentId = new Map<string, string>();

for await (const [id, project] of data.projects) {
for await (const project of data.projects) {
const newProject = await projectService
.createProject({
actor,
Expand All @@ -115,7 +115,7 @@ export const importDataIntoInfisicalFn = async ({
createDefaultEnvs: false
})
.catch(() => {
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}` });
});

originalToNewProjectId.set(project.id, newProject.id);
Expand All @@ -141,7 +141,7 @@ export const importDataIntoInfisicalFn = async ({

// Import environments
if (data.environments) {
for await (const [id, environment] of data.environments) {
for await (const environment of data.environments) {
try {
const newEnvironment = await projectEnvService.createEnvironment({
actor,
Expand All @@ -154,12 +154,12 @@ export const importDataIntoInfisicalFn = async ({
});

if (!newEnvironment) {
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`);
logger.error(`Failed to import environment: [name:${environment.name}]`);
throw new BadRequestError({
message: `Failed to import environment: [name:${environment.name}] [id:${id}]`
message: `Failed to import environment: [name:${environment.name}]`
});
}
originalToNewEnvironmentId.set(id, newEnvironment.slug);
originalToNewEnvironmentId.set(environment.id, newEnvironment.slug);
} catch (error) {
throw new BadRequestError({
message: `Failed to import environment: ${environment.name}]`,
Expand All @@ -169,28 +169,47 @@ export const importDataIntoInfisicalFn = async ({
}
}

// Import secrets
if (data.secrets) {
for await (const [id, secret] of data.secrets) {
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId;
if (!dataProjectId) {
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` });
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, []);
}
const projectId = originalToNewProjectId.get(dataProjectId);
const newSecret = await secretService.createSecretRaw({
actorId,
actor,
actorOrgId,
environment: originalToNewEnvironmentId.get(secret.environmentId)!,
actorAuthMethod,
projectId: projectId!,
secretPath: "/",
secretName: secret.name,
type: SecretType.Shared,
mappedToEnvironmentId.get(secret.environmentId)!.push({
secretKey: secret.name,
secretValue: secret.value || ""
});
if (!newSecret) {
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });
}

// for each of the mappedEnvironmentId
for await (const [envId, secrets] of mappedToEnvironmentId) {
const environment = data.environments.find((env) => env.id === envId);
const projectId = environment?.projectId;

if (!projectId) {
throw new BadRequestError({ message: `Failed to import secret, project not found` });
}

const secretBatches = chunkArray(secrets, 2500);

for await (const secretBatch of secretBatches) {
await secretV2BridgeService.createManySecret({
actorId,
actor,
actorOrgId,
environment: originalToNewEnvironmentId.get(envId)!,
actorAuthMethod,
projectId: originalToNewProjectId.get(projectId)!,
secretPath: "/",
secrets: secretBatch
});
}
}
}
Expand Down
Loading

0 comments on commit 162005d

Please sign in to comment.