mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: secret v2 architecture for secret rotation
This commit is contained in:
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -239,6 +239,9 @@ import {
|
||||
TSecretRotationOutputs,
|
||||
TSecretRotationOutputsInsert,
|
||||
TSecretRotationOutputsUpdate,
|
||||
TSecretRotationOutputV2,
|
||||
TSecretRotationOutputV2Insert,
|
||||
TSecretRotationOutputV2Update,
|
||||
TSecretRotations,
|
||||
TSecretRotationsInsert,
|
||||
TSecretRotationsUpdate,
|
||||
@@ -718,6 +721,11 @@ declare module "knex/types/tables" {
|
||||
TSecretApprovalRequestSecretTagsV2Insert,
|
||||
TSecretApprovalRequestSecretTagsV2Update
|
||||
>;
|
||||
[TableName.SecretRotationOutputV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretRotationOutputV2,
|
||||
TSecretRotationOutputV2Insert,
|
||||
TSecretRotationOutputV2Update
|
||||
>;
|
||||
// KMS service
|
||||
[TableName.KmsServerRootConfig]: KnexOriginal.CompositeTableType<
|
||||
TKmsRootConfig,
|
||||
|
||||
@@ -128,7 +128,18 @@ export async function up(knex: Knex): Promise<void> {
|
||||
if (!hasEncryptedAccess) t.binary("encryptedAccess");
|
||||
if (!hasEncryptedAccessId) t.binary("encryptedAccessId");
|
||||
if (!hasEncryptedRefresh) t.binary("encryptedRefresh");
|
||||
if (!hasEncryptedAwsIamAssumRole) t.binary("encryptedAwsIamAssumRole");
|
||||
if (!hasEncryptedAwsIamAssumRole) t.binary("encryptedAwsAssumeIamRoleArn");
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretRotationOutputV2))) {
|
||||
await knex.schema.createTable(TableName.SecretRotationOutputV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("key").notNullable();
|
||||
t.uuid("secretId").notNullable();
|
||||
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
|
||||
t.uuid("rotationId").notNullable();
|
||||
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,13 +152,15 @@ export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretV2JnTag);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretReferenceV2);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretRotationOutputV2);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretVersionV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretVersionV2Tag);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretVersionV2);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretV2);
|
||||
|
||||
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
|
||||
const hasEncryptedAccess = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccess");
|
||||
const hasEncryptedAccessId = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccessId");
|
||||
@@ -160,7 +173,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
if (hasEncryptedAccess) t.dropColumn("encryptedAccess");
|
||||
if (hasEncryptedAccessId) t.dropColumn("encryptedAccessId");
|
||||
if (hasEncryptedRefresh) t.dropColumn("encryptedRefresh");
|
||||
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsIamAssumRole");
|
||||
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,3 +103,4 @@ export * from "./user-encryption-keys";
|
||||
export * from "./user-group-membership";
|
||||
export * from "./users";
|
||||
export * from "./webhooks";
|
||||
export * from "./secret-rotation-output-v2";
|
||||
|
||||
@@ -35,10 +35,11 @@ export const IntegrationAuthsSchema = z.object({
|
||||
awsAssumeIamRoleArnCipherText: z.string().nullable().optional(),
|
||||
awsAssumeIamRoleArnIV: z.string().nullable().optional(),
|
||||
awsAssumeIamRoleArnTag: z.string().nullable().optional(),
|
||||
encryptedAwsIamAssumRole: zodBuffer.nullable().optional(),
|
||||
encryptedAccess: zodBuffer.nullable().optional(),
|
||||
encryptedAccessId: zodBuffer.nullable().optional(),
|
||||
encryptedRefresh: zodBuffer.nullable().optional(),
|
||||
encryptedAwsIamAssumRole: zodBuffer.nullable().optional()
|
||||
encryptedAwsAssumeIamRoleArn: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;
|
||||
|
||||
@@ -101,6 +101,7 @@ export enum TableName {
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
SecretVersionTag = "secret_version_tag_junction",
|
||||
SecretVersionV2Tag = "secret_version_v2_tag_junction",
|
||||
SecretRotationOutputV2 = "secret_rotation_output_v2",
|
||||
// KMS Service
|
||||
KmsServerRootConfig = "kms_root_config",
|
||||
KmsKey = "kms_keys",
|
||||
|
||||
21
backend/src/db/schemas/secret-rotation-output-v2.ts
Normal file
21
backend/src/db/schemas/secret-rotation-output-v2.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretRotationOutputV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
key: z.string(),
|
||||
secretId: z.string().uuid(),
|
||||
rotationId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TSecretRotationOutputV2 = z.infer<typeof SecretRotationOutputV2Schema>;
|
||||
export type TSecretRotationOutputV2Insert = Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretRotationOutputV2Update = Partial<
|
||||
Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas";
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -112,18 +112,10 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
outputs: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
secret: SecretsSchema.pick({
|
||||
id: true,
|
||||
version: true,
|
||||
secretKeyIV: true,
|
||||
secretKeyTag: true,
|
||||
secretKeyCiphertext: true,
|
||||
secretValueIV: true,
|
||||
secretValueTag: true,
|
||||
secretValueCiphertext: true,
|
||||
secretCommentIV: true,
|
||||
secretCommentTag: true,
|
||||
secretCommentCiphertext: true
|
||||
secret: z.object({
|
||||
secretKey: z.string(),
|
||||
id: z.string(),
|
||||
version: z.number()
|
||||
})
|
||||
})
|
||||
.array()
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -869,7 +869,9 @@ export const registerRoutes = async (
|
||||
projectDAL,
|
||||
licenseService,
|
||||
secretDAL,
|
||||
folderDAL
|
||||
folderDAL,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL
|
||||
});
|
||||
|
||||
const integrationService = integrationServiceFactory({
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
TGetSecretRotationList,
|
||||
TGetSecretRotationListDTO,
|
||||
TGetSecretRotationProviders,
|
||||
TSecretRotation,
|
||||
TSecretRotationProviderList
|
||||
@@ -19,7 +14,7 @@ export const secretRotationKeys = {
|
||||
{ workspaceId },
|
||||
"secret-rotation-providers"
|
||||
],
|
||||
list: ({ workspaceId }: Omit<TGetSecretRotationList, "decryptFileKey">) =>
|
||||
list: ({ workspaceId }: Omit<TGetSecretRotationListDTO, "decryptFileKey">) =>
|
||||
[{ workspaceId }, "secret-rotations"] as const
|
||||
};
|
||||
|
||||
@@ -53,7 +48,7 @@ export const useGetSecretRotationProviders = ({
|
||||
|
||||
const fetchSecretRotations = async ({
|
||||
workspaceId
|
||||
}: Omit<TGetSecretRotationList, "decryptFileKey">) => {
|
||||
}: Omit<TGetSecretRotationListDTO, "decryptFileKey">) => {
|
||||
const { data } = await apiRequest.get<{ secretRotations: TSecretRotation[] }>(
|
||||
"/api/v1/secret-rotations",
|
||||
{ params: { workspaceId } }
|
||||
@@ -63,14 +58,13 @@ const fetchSecretRotations = async ({
|
||||
|
||||
export const useGetSecretRotations = ({
|
||||
workspaceId,
|
||||
decryptFileKey,
|
||||
options = {}
|
||||
}: TGetSecretRotationList & {
|
||||
}: TGetSecretRotationListDTO & {
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretRotation[],
|
||||
unknown,
|
||||
TSecretRotation<{ key: string }>[],
|
||||
TSecretRotation[],
|
||||
ReturnType<typeof secretRotationKeys.list>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
@@ -80,31 +74,5 @@ export const useGetSecretRotations = ({
|
||||
...options,
|
||||
queryKey: secretRotationKeys.list({ workspaceId }),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||
queryFn: async () => fetchSecretRotations({ workspaceId }),
|
||||
select: useCallback(
|
||||
(data: TSecretRotation[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const decryptKey = decryptAssymmetric({
|
||||
ciphertext: decryptFileKey.encryptedKey,
|
||||
nonce: decryptFileKey.nonce,
|
||||
publicKey: decryptFileKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
return data.map((el) => ({
|
||||
...el,
|
||||
outputs: el.outputs.map(({ key, secret }) => ({
|
||||
key,
|
||||
secret: {
|
||||
key: decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key: decryptKey
|
||||
})
|
||||
}
|
||||
}))
|
||||
}));
|
||||
},
|
||||
[decryptFileKey]
|
||||
)
|
||||
queryFn: async () => fetchSecretRotations({ workspaceId })
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { WorkspaceEnv } from "../workspace/types";
|
||||
|
||||
export enum TProviderFunctionTypes {
|
||||
@@ -74,7 +72,7 @@ export type TDbProviderTemplate = {
|
||||
outputs: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TSecretRotation<T extends unknown = EncryptedSecret> = {
|
||||
export type TSecretRotation = {
|
||||
id: string;
|
||||
interval: number;
|
||||
provider: string;
|
||||
@@ -85,7 +83,11 @@ export type TSecretRotation<T extends unknown = EncryptedSecret> = {
|
||||
secretPath: string;
|
||||
outputs: Array<{
|
||||
key: string;
|
||||
secret: T;
|
||||
secret: {
|
||||
version: number;
|
||||
id: string;
|
||||
secretKey: string;
|
||||
};
|
||||
}>;
|
||||
status?: "success" | "failed";
|
||||
lastRotatedAt?: string;
|
||||
@@ -103,9 +105,8 @@ export type TGetSecretRotationProviders = {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type TGetSecretRotationList = {
|
||||
export type TGetSecretRotationListDTO = {
|
||||
workspaceId: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export type TCreateSecretRotationDTO = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
faArrowsSpin,
|
||||
@@ -16,12 +15,9 @@ import { formatDistance } from "date-fns";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
Table,
|
||||
@@ -48,22 +44,17 @@ import {
|
||||
useDeleteSecretRotation,
|
||||
useGetSecretRotationProviders,
|
||||
useGetSecretRotations,
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceBot,
|
||||
useRestartSecretRotation,
|
||||
useUpdateBotActiveStatus
|
||||
useRestartSecretRotation
|
||||
} from "@app/hooks/api";
|
||||
import { TSecretRotationProviderTemplate } from "@app/hooks/api/types";
|
||||
|
||||
import { CreateRotationForm } from "./components/CreateRotationForm";
|
||||
import { generateBotKey } from "./SecretRotationPage.utils";
|
||||
|
||||
export const SecretRotationPage = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"createRotation",
|
||||
"activeBot",
|
||||
@@ -77,13 +68,10 @@ export const SecretRotationPage = withProjectPermission(
|
||||
);
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data: userWsKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
const { data: secretRotationProviders, isLoading: isRotationProviderLoading } =
|
||||
useGetSecretRotationProviders({ workspaceId });
|
||||
const { data: secretRotations, isLoading: isRotationLoading } = useGetSecretRotations({
|
||||
workspaceId,
|
||||
decryptFileKey: userWsKey!
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -97,11 +85,6 @@ export const SecretRotationPage = withProjectPermission(
|
||||
isLoading: isRestartingRotation
|
||||
} = useRestartSecretRotation();
|
||||
|
||||
const { data: bot } = useGetWorkspaceBot(workspaceId);
|
||||
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
||||
|
||||
const isBotActive = Boolean(bot?.isActive);
|
||||
|
||||
const handleDeleteRotation = async () => {
|
||||
const { id } = popUp.deleteRotation.data as { id: string };
|
||||
try {
|
||||
@@ -142,29 +125,6 @@ export const SecretRotationPage = withProjectPermission(
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserAcceptBotCondition = async () => {
|
||||
const provider = popUp.activeBot?.data as TSecretRotationProviderTemplate;
|
||||
try {
|
||||
if (bot?.id) {
|
||||
const botKey = generateBotKey(bot.publicKey, userWsKey!);
|
||||
await updateBotActiveStatus({
|
||||
isActive: true,
|
||||
botId: bot.id,
|
||||
workspaceId,
|
||||
botKey
|
||||
});
|
||||
}
|
||||
handlePopUpOpen("createRotation", provider);
|
||||
handlePopUpClose("activeBot");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create bot"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateRotation = async (provider: TSecretRotationProviderTemplate) => {
|
||||
if (subscription && !subscription?.secretRotation) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
@@ -174,11 +134,7 @@ export const SecretRotationPage = withProjectPermission(
|
||||
createNotification({ type: "error", text: "Access permission denied!!" });
|
||||
return;
|
||||
}
|
||||
if (isBotActive) {
|
||||
handlePopUpOpen("createRotation", provider);
|
||||
} else {
|
||||
handlePopUpOpen("activeBot", provider);
|
||||
}
|
||||
handlePopUpOpen("createRotation", provider);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -391,30 +347,6 @@ export const SecretRotationPage = withProjectPermission(
|
||||
onToggle={(isOpen) => handlePopUpToggle("createRotation", isOpen)}
|
||||
provider={(popUp.createRotation.data as TSecretRotationProviderTemplate) || {}}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp.activeBot?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title={t("integrations.grant-access-to-secrets") as string}
|
||||
footerContent={
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button onClick={() => handleUserAcceptBotCondition()}>
|
||||
{t("integrations.grant-access-button") as string}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handlePopUpClose("activeBot")}
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{t("integrations.why-infisical-needs-access")}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRotation.isOpen}
|
||||
title="Are you sure want to delete this rotation?"
|
||||
|
||||
Reference in New Issue
Block a user