feat: secret v2 architecture for secret rotation

This commit is contained in:
=
2024-07-30 23:19:06 +05:30
parent 4a06e3e712
commit acf9a488ac
14 changed files with 206 additions and 182 deletions
@@ -10,6 +10,7 @@ export type TSecretRotationDALFactory = ReturnType<typeof secretRotationDALFacto
export const secretRotationDALFactory = (db: TDbClient) => {
const secretRotationOrm = ormify(db, TableName.SecretRotation);
const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput);
const secretRotationOutputV2Orm = ormify(db, TableName.SecretRotationOutputV2);
const findQuery = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
tx(TableName.SecretRotation)
@@ -31,13 +32,7 @@ export const secretRotationDALFactory = (db: TDbClient) => {
.select(tx.ref("version").withSchema(TableName.Secret).as("secVersion"))
.select(tx.ref("secretKeyIV").withSchema(TableName.Secret))
.select(tx.ref("secretKeyTag").withSchema(TableName.Secret))
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret))
.select(tx.ref("secretValueIV").withSchema(TableName.Secret))
.select(tx.ref("secretValueTag").withSchema(TableName.Secret))
.select(tx.ref("secretValueCiphertext").withSchema(TableName.Secret))
.select(tx.ref("secretCommentIV").withSchema(TableName.Secret))
.select(tx.ref("secretCommentTag").withSchema(TableName.Secret))
.select(tx.ref("secretCommentCiphertext").withSchema(TableName.Secret));
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret));
const find = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
try {
@@ -54,33 +49,65 @@ export const secretRotationDALFactory = (db: TDbClient) => {
{
key: "secId",
label: "outputs" as const,
mapper: ({
secId,
outputKey,
secVersion,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretValueTag,
secretValueIV,
secretValueCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext
}) => ({
mapper: ({ secId, outputKey, secVersion, secretKeyIV, secretKeyTag, secretKeyCiphertext }) => ({
key: outputKey,
secret: {
id: secId,
version: secVersion,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretValueTag,
secretValueIV,
secretValueCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext
secretKeyCiphertext
}
})
}
]
});
} catch (error) {
throw new DatabaseError({ error, name: "SecretRotationFind" });
}
};
const findQuerySecretV2 = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
tx(TableName.SecretRotation)
.where(filter)
.join(TableName.Environment, `${TableName.SecretRotation}.envId`, `${TableName.Environment}.id`)
.leftJoin(
TableName.SecretRotationOutputV2,
`${TableName.SecretRotation}.id`,
`${TableName.SecretRotationOutputV2}.rotationId`
)
.join(TableName.SecretV2, `${TableName.SecretRotationOutputV2}.secretId`, `${TableName.SecretV2}.id`)
.select(selectAllTableCols(TableName.SecretRotation))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(tx.ref("key").withSchema(TableName.SecretRotationOutputV2).as("outputKey"))
.select(tx.ref("id").withSchema(TableName.SecretV2).as("secId"))
.select(tx.ref("version").withSchema(TableName.SecretV2).as("secVersion"))
.select(tx.ref("key").withSchema(TableName.SecretV2).as("secretKey"));
const findSecretV2 = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
try {
const data = await findQuerySecretV2(filter, tx || db.replicaNode());
return sqlNestRelationships({
data,
key: "id",
parentMapper: (el) => ({
...SecretRotationsSchema.parse(el),
projectId: el.projectId,
environment: { id: el.envId, name: el.envName, slug: el.envSlug }
}),
childrenMapper: [
{
key: "secId",
label: "outputs" as const,
mapper: ({ secId, outputKey, secVersion, secretKey }) => ({
key: outputKey,
secret: {
id: secId,
version: secVersion,
secretKey
}
})
}
@@ -114,12 +141,17 @@ export const secretRotationDALFactory = (db: TDbClient) => {
};
const findRotationOutputsByRotationId = async (rotationId: string) => secretRotationOutputOrm.find({ rotationId });
const findRotationOutputsV2ByRotationId = async (rotationId: string) =>
secretRotationOutputV2Orm.find({ rotationId });
return {
...secretRotationOrm,
find,
findSecretV2,
findById,
secretOutputInsertMany: secretRotationOutputOrm.insertMany,
findRotationOutputsByRotationId
secretOutputV2InsertMany: secretRotationOutputV2Orm.insertMany,
findRotationOutputsByRotationId,
findRotationOutputsV2ByRotationId
};
};
@@ -121,7 +121,13 @@ export const secretRotationQueueFactory = ({
try {
if (!rotationProvider || !secretRotation) throw new DisableRotationErrors({ message: "Provider not found" });
const rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(secretRotation.projectId);
let rotationOutputs;
if (shouldUseSecretV2Bridge) {
rotationOutputs = await secretRotationDAL.findRotationOutputsV2ByRotationId(rotationId);
} else {
rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
}
if (!rotationOutputs.length) throw new DisableRotationErrors({ message: "Secrets not found" });
// deep copy
@@ -277,7 +283,6 @@ export const secretRotationQueueFactory = ({
internal: newCredential.internal
});
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables));
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(secretRotation.projectId);
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: secretRotation.projectId
@@ -1,12 +1,15 @@
import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { ProjectVersion } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -22,9 +25,11 @@ type TSecretRotationServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
};
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
@@ -37,7 +42,9 @@ export const secretRotationServiceFactory = ({
licenseService,
projectDAL,
folderDAL,
secretDAL
secretDAL,
projectBotService,
secretV2BridgeDAL
}: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({
actor,
@@ -92,15 +99,25 @@ export const secretRotationServiceFactory = ({
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
const project = await projectDAL.findById(projectId);
const shouldUseBridge = project.version === ProjectVersion.V3;
if (shouldUseBridge) {
const selectedSecrets = await secretV2BridgeDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
} else {
const selectedSecrets = await secretDAL.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
@@ -148,10 +165,18 @@ export const secretRotationServiceFactory = ({
},
tx
);
const outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
let outputSecretMapping;
if (shouldUseBridge) {
outputSecretMapping = await secretRotationDAL.secretOutputV2InsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
} else {
outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx
);
}
return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
});
await secretRotationQueue.addToQueue(secretRotation.id, secretRotation.interval);
@@ -167,8 +192,30 @@ export const secretRotationServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
const doc = await secretRotationDAL.find({ projectId });
return doc;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const docs = await secretRotationDAL.findSecretV2({ projectId });
return docs;
}
if (!botKey) throw new BadRequestError({ message: "bot not found" });
const docs = await secretRotationDAL.find({ projectId });
return docs.map((el) => ({
...el,
outputs: el.outputs.map((output) => ({
...output,
secret: {
id: output.secret.id,
version: output.secret.version,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: output.secret.secretKeyCiphertext,
iv: output.secret.secretKeyIV,
tag: output.secret.secretKeyTag,
key: botKey
})
}
}))
}));
};
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {