mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 02:16:34 +00:00
feature secret rotations v2
This commit is contained in:
@@ -35,15 +35,25 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||
const data = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({
|
||||
_id: el.id,
|
||||
...SecretsV2Schema.parse(el),
|
||||
isRotatedSecret: Boolean(el.rotationId),
|
||||
rotationId: el.rotationId
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
@@ -79,6 +89,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
@@ -87,7 +102,8 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
@@ -98,7 +114,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
const data = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({
|
||||
_id: el.id,
|
||||
...SecretsV2Schema.parse(el),
|
||||
rotationId: el.rotationId,
|
||||
isRotatedSecret: Boolean(el.rotationId)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
@@ -332,6 +353,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
|
||||
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.whereIn("folderId", folderIds)
|
||||
.where((bd) => {
|
||||
if (filters?.search) {
|
||||
@@ -414,6 +440,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.where((qb) => {
|
||||
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
|
||||
filters.metadataFilter.forEach((meta) => {
|
||||
@@ -444,6 +475,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
)
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping))
|
||||
.where((bd) => {
|
||||
const slugs = filters?.tagSlugs?.filter(Boolean);
|
||||
if (slugs && slugs.length > 0) {
|
||||
@@ -472,7 +504,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
const data = sqlNestRelationships({
|
||||
data: secs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({
|
||||
_id: el.id,
|
||||
...SecretsV2Schema.parse(el),
|
||||
rotationId: el.rotationId,
|
||||
isRotatedSecret: Boolean(el.rotationId)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
|
||||
@@ -666,6 +666,8 @@ export const reshapeBridgeSecret = (
|
||||
name: string;
|
||||
}[];
|
||||
secretMetadata?: ResourceMetadataDTO;
|
||||
isRotatedSecret?: boolean;
|
||||
rotationId?: string;
|
||||
},
|
||||
secretValueHidden: boolean
|
||||
) => ({
|
||||
@@ -695,7 +697,8 @@ export const reshapeBridgeSecret = (
|
||||
secretMetadata: secret.secretMetadata,
|
||||
createdAt: secret.createdAt,
|
||||
updatedAt: secret.updatedAt,
|
||||
|
||||
isRotatedSecret: secret.isRotatedSecret,
|
||||
rotationId: secret.rotationId,
|
||||
...(secretValueHidden
|
||||
? {
|
||||
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -132,7 +132,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
secrets: {
|
||||
secretKey: string;
|
||||
newSecretName?: string;
|
||||
secretValue: string;
|
||||
secretValue?: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tagIds?: string[];
|
||||
|
||||
Reference in New Issue
Block a user