made move operation transactional

This commit is contained in:
Sheen Capadngan
2024-07-09 16:03:50 +08:00
parent a06dee66f8
commit 05bf2e4696
3 changed files with 118 additions and 110 deletions

View File

@@ -1338,12 +1338,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
destinationEnvironment: z.string().trim(), destinationEnvironment: z.string().trim(),
destinationSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), destinationSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
secrets: z secretIds: z.string().array()
.object({
id: z.string()
})
.array()
.min(1)
}), }),
response: { response: {
200: z.union([ 200: z.union([

View File

@@ -1703,7 +1703,7 @@ export const secretServiceFactory = ({
sourceSecretPath, sourceSecretPath,
destinationEnvironment, destinationEnvironment,
destinationSecretPath, destinationSecretPath,
secrets, secretIds,
projectSlug, projectSlug,
actor, actor,
actorId, actorId,
@@ -1767,11 +1767,11 @@ export const secretServiceFactory = ({
const sourceSecrets = await secretDAL.find({ const sourceSecrets = await secretDAL.find({
type: SecretType.Shared, type: SecretType.Shared,
$in: { $in: {
id: secrets.map((secret) => secret.id) id: secretIds
} }
}); });
if (sourceSecrets.length !== secrets.length) { if (sourceSecrets.length !== secretIds.length) {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid secrets" message: "Invalid secrets"
}); });
@@ -1793,69 +1793,79 @@ export const secretServiceFactory = ({
}) })
})); }));
let isSourceFolderUpdated = false;
let isDestinationFolderUpdated = false;
// Moving secrets is a two-step process. // Moving secrets is a two-step process.
// First step is to create/update the secret in the destination: await secretDAL.transaction(async (tx) => {
const destinationSecretsFromDB = await secretDAL.find({ // First step is to create/update the secret in the destination:
folderId: destinationFolder.id const destinationSecretsFromDB = await secretDAL.find(
}); {
folderId: destinationFolder.id
},
{ tx }
);
const decryptedDestinationSecrets = destinationSecretsFromDB.map((secret) => { const decryptedDestinationSecrets = destinationSecretsFromDB.map((secret) => {
return { return {
...secret, ...secret,
secretKey: decryptSymmetric128BitHexKeyUTF8({ secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
tag: secret.secretKeyTag, tag: secret.secretKeyTag,
key: botKey key: botKey
}), }),
secretValue: decryptSymmetric128BitHexKeyUTF8({ secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext, ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV, iv: secret.secretValueIV,
tag: secret.secretValueTag, tag: secret.secretValueTag,
key: botKey key: botKey
}) })
}; };
});
const destinationSecretsGroupedByBlindIndex = groupBy(
decryptedDestinationSecrets.filter(({ secretBlindIndex }) => Boolean(secretBlindIndex)),
(i) => i.secretBlindIndex as string
);
const locallyCreatedSecrets = decryptedSourceSecrets
.filter(({ secretBlindIndex }) => !destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0])
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
const locallyUpdatedSecrets = decryptedSourceSecrets
.filter(
({ secretBlindIndex, secretKey, secretValue }) =>
destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0] &&
// if key or value changed
(destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretKey !== secretKey ||
destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretValue !== secretValue)
)
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
const isEmpty = locallyCreatedSecrets.length + locallyUpdatedSecrets.length === 0;
if (isEmpty) {
throw new BadRequestError({
message: "No changes were detected between the source and destination."
}); });
}
const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( const destinationSecretsGroupedByBlindIndex = groupBy(
project.id, decryptedDestinationSecrets.filter(({ secretBlindIndex }) => Boolean(secretBlindIndex)),
destinationFolder.environment.slug, (i) => i.secretBlindIndex as string
destinationFolder.path );
);
if (destinationFolderPolicy && actor === ActorType.USER) { const locallyCreatedSecrets = decryptedSourceSecrets
// if secret approval policy exists for destination, we create the secret approval request .filter(({ secretBlindIndex }) => !destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0])
const localSecretsIds = decryptedDestinationSecrets.map(({ id }) => id); .map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(destinationFolder.id, localSecretsIds);
const locallyUpdatedSecrets = decryptedSourceSecrets
.filter(
({ secretBlindIndex, secretKey, secretValue }) =>
destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0] &&
// if key or value changed
(destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretKey !== secretKey ||
destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretValue !== secretValue)
)
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
const isEmpty = locallyCreatedSecrets.length + locallyUpdatedSecrets.length === 0;
if (isEmpty) {
throw new BadRequestError({
message: "No changes were detected between the source and destination."
});
}
const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
project.id,
destinationFolder.environment.slug,
destinationFolder.path
);
if (destinationFolderPolicy && actor === ActorType.USER) {
// if secret approval policy exists for destination, we create the secret approval request
const localSecretsIds = decryptedDestinationSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
destinationFolder.id,
localSecretsIds,
tx
);
await secretApprovalRequestDAL.transaction(async (tx) => {
const approvalRequestDoc = await secretApprovalRequestDAL.create( const approvalRequestDoc = await secretApprovalRequestDAL.create(
{ {
folderId: destinationFolder.id, folderId: destinationFolder.id,
@@ -1895,12 +1905,9 @@ export const secretServiceFactory = ({
: {}) : {})
}; };
}); });
const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(commits, tx); await secretApprovalRequestSecretDAL.insertMany(commits, tx);
return { ...approvalRequestDoc, commits: approvalCommits }; } else {
}); // apply changes directly
} else {
// apply changes directly
await secretDAL.transaction(async (tx) => {
if (locallyCreatedSecrets.length) { if (locallyCreatedSecrets.length) {
await fnSecretBulkInsert({ await fnSecretBulkInsert({
folderId: destinationFolder.id, folderId: destinationFolder.id,
@@ -1967,33 +1974,23 @@ export const secretServiceFactory = ({
}); });
} }
await snapshotService.performSnapshot(destinationFolder.id); isDestinationFolderUpdated = true;
await secretQueueService.syncSecrets({ }
projectId: project.id,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environment.slug,
actorId,
actor
});
});
}
// Next step is to delete the secrets from the source folder: // Next step is to delete the secrets from the source folder:
const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string); const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string);
const locallyDeletedSecrets = decryptedSourceSecrets.map((el) => ({ ...el, operation: SecretOperations.Delete })); const locallyDeletedSecrets = decryptedSourceSecrets.map((el) => ({ ...el, operation: SecretOperations.Delete }));
const sourceFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( const sourceFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
project.id, project.id,
sourceFolder.environment.slug, sourceFolder.environment.slug,
sourceFolder.path sourceFolder.path
); );
if (sourceFolderPolicy && actor === ActorType.USER) { if (sourceFolderPolicy && actor === ActorType.USER) {
// if secret approval policy exists for source, we create the secret approval request // if secret approval policy exists for source, we create the secret approval request
const localSecretsIds = decryptedSourceSecrets.map(({ id }) => id); const localSecretsIds = decryptedSourceSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(sourceFolder.id, localSecretsIds); const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(sourceFolder.id, localSecretsIds, tx);
await secretApprovalRequestDAL.transaction(async (tx) => {
const approvalRequestDoc = await secretApprovalRequestDAL.create( const approvalRequestDoc = await secretApprovalRequestDAL.create(
{ {
folderId: sourceFolder.id, folderId: sourceFolder.id,
@@ -2031,18 +2028,36 @@ export const secretServiceFactory = ({
secretVersion: latestSecretVersions[localSecret.id].id secretVersion: latestSecretVersions[localSecret.id].id
}; };
}); });
const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(commits, tx);
return { ...approvalRequestDoc, commits: approvalCommits };
});
} else {
// if no secret approval policy is present, we delete directly.
await secretDAL.delete({
$in: {
id: locallyDeletedSecrets.map(({ id }) => id)
},
folderId: sourceFolder.id
});
await secretApprovalRequestSecretDAL.insertMany(commits, tx);
} else {
// if no secret approval policy is present, we delete directly.
await secretDAL.delete(
{
$in: {
id: locallyDeletedSecrets.map(({ id }) => id)
},
folderId: sourceFolder.id
},
tx
);
isSourceFolderUpdated = true;
}
});
if (isDestinationFolderUpdated) {
await snapshotService.performSnapshot(destinationFolder.id);
await secretQueueService.syncSecrets({
projectId: project.id,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environment.slug,
actorId,
actor
});
}
if (isSourceFolderUpdated) {
await snapshotService.performSnapshot(sourceFolder.id); await snapshotService.performSnapshot(sourceFolder.id);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
projectId: project.id, projectId: project.id,

View File

@@ -404,7 +404,5 @@ export type TMoveSecretsDTO = {
sourceSecretPath: string; sourceSecretPath: string;
destinationEnvironment: string; destinationEnvironment: string;
destinationSecretPath: string; destinationSecretPath: string;
secrets: { secretIds: string[];
id: string;
}[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;