mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
made move operation transactional
This commit is contained in:
@@ -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([
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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">;
|
||||||
|
|||||||
Reference in New Issue
Block a user