feature secret rotations v2

This commit is contained in:
Scott Wilson
2025-03-31 20:34:48 -07:00
parent da094383b8
commit 766c1242fd
275 changed files with 9692 additions and 783 deletions
@@ -25,6 +25,7 @@ import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-app
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { diff, groupBy } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex";
@@ -414,6 +415,8 @@ export const secretV2BridgeServiceFactory = ({
});
if (!sharedSecretToModify)
throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` });
if (sharedSecretToModify.isRotatedSecret && (inputSecret.newSecretName || inputSecret.secretValue))
throw new BadRequestError({ message: "Cannot update rotated secret name or value" });
secretId = sharedSecretToModify.id;
secret = sharedSecretToModify;
}
@@ -624,66 +627,79 @@ export const secretV2BridgeServiceFactory = ({
})
);
const deletedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({
projectId,
folderId,
actorId,
secretDAL,
secretQueueService,
inputSecrets: [
{
type: inputSecret.type as SecretType,
secretKey: inputSecret.secretName
}
],
tx
})
);
try {
const deletedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({
projectId,
folderId,
actorId,
secretDAL,
secretQueueService,
inputSecrets: [
{
type: inputSecret.type as SecretType,
secretKey: inputSecret.secretName
}
],
tx
})
);
if (inputSecret.type === SecretType.Shared) {
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
secretPath,
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
if (inputSecret.type === SecretType.Shared) {
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
secretPath,
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath,
secretName: secretToDelete.key,
secretTags: secretToDelete.tags?.map((el) => el.slug)
}
);
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
secretName: secretToDelete.key,
secretTags: secretToDelete.tags?.map((el) => el.slug)
{
...deletedSecret[0],
value: deletedSecret[0].encryptedValue
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
: "",
comment: deletedSecret[0].encryptedComment
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
: ""
},
secretValueHidden
);
} catch (err) {
// deferred errors aren't return as DatabaseError
const error = err as { code: string; table: string };
if (
error?.code === DatabaseErrorCode.ForeignKeyViolation &&
error?.table === TableName.SecretRotationV2SecretMapping
) {
throw new BadRequestError({ message: "Cannot delete rotated secrets" });
}
);
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...deletedSecret[0],
value: deletedSecret[0].encryptedValue
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
: "",
comment: deletedSecret[0].encryptedComment
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
: ""
},
secretValueHidden
);
throw err;
}
};
// get unique secrets count for multiple envs
@@ -946,6 +962,7 @@ export const secretV2BridgeServiceFactory = ({
projectId
});
// scott: if any of this changes it also needs to be mirrored in secret rotation for getting dashboard secrets
const decryptedSecrets = secrets
.filter((el) => {
const canDescribeSecret = hasSecretReadValueOrDescribePermission(
@@ -1667,6 +1684,13 @@ export const secretV2BridgeServiceFactory = ({
secretTags: el.tags.map((i) => i.slug)
})
);
if (el.isRotatedSecret) {
const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key);
if (input && (input.newSecretName || input.secretValue))
throw new BadRequestError({ message: `Cannot update rotated secret name or value: ${el.key}` });
}
});
// get all tags
@@ -1969,61 +1993,76 @@ export const secretV2BridgeServiceFactory = ({
);
});
const secretsDeleted = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({
secretDAL,
secretQueueService,
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
secretKey,
type: type || SecretType.Shared
})),
projectId,
folderId,
actorId,
tx
})
);
// await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
actor,
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return secretsDeleted.map((el) => {
const secretToDeleteMatch = secretsToDelete.find(
(i) => i.key === el.key && (i.type || SecretType.Shared) === el.type
try {
const secretsDeleted = await secretDAL.transaction(async (tx) =>
fnSecretBulkDelete({
secretDAL,
secretQueueService,
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
secretKey,
type: type || SecretType.Shared
})),
projectId,
folderId,
actorId,
tx
})
);
const secretValueHidden =
!secretToDeleteMatch ||
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
actor,
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return secretsDeleted.map((el) => {
const secretToDeleteMatch = secretsToDelete.find(
(i) => i.key === el.key && (i.type || SecretType.Shared) === el.type
);
const secretValueHidden =
!secretToDeleteMatch ||
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath,
secretName: el.key,
secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
});
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
secretName: el.key,
secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
});
{
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: ""
},
secretValueHidden
);
});
} catch (err) {
// deferred errors aren't return as DatabaseError
const error = err as { code: string; table: string };
if (
error?.code === DatabaseErrorCode.ForeignKeyViolation &&
error?.table === TableName.SecretRotationV2SecretMapping
) {
throw new BadRequestError({ message: "Cannot delete rotated secrets" });
}
return reshapeBridgeSecret(
projectId,
environment,
secretPath,
{
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
},
secretValueHidden
);
});
throw err;
}
};
const getSecretVersions = async ({