mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 01:26:20 +00:00
feat(server): added some more validation and feedback for deletion etc
This commit is contained in:
@@ -19,9 +19,9 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
|
t.string("algorithm").notNullable().defaultTo(SecretEncryptionAlgo.AES_256_GCM);
|
||||||
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
|
t.string("keyEncoding").notNullable().defaultTo(SecretKeyEncoding.UTF8);
|
||||||
t.uuid("folderId").notNullable();
|
t.uuid("folderId").notNullable();
|
||||||
t.boolean("isDeleting").defaultTo(false);
|
// for background process communication
|
||||||
// used for flag why delete is failing
|
|
||||||
t.string("status");
|
t.string("status");
|
||||||
|
t.string("statusDetails");
|
||||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||||
t.unique(["slug", "folderId"]);
|
t.unique(["slug", "folderId"]);
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
@@ -36,8 +36,10 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.integer("version").notNullable();
|
t.integer("version").notNullable();
|
||||||
t.string("externalEntityId").notNullable();
|
t.string("externalEntityId").notNullable();
|
||||||
t.string("maxTTL");
|
|
||||||
t.datetime("expireAt").notNullable();
|
t.datetime("expireAt").notNullable();
|
||||||
|
// for background process communication
|
||||||
|
t.string("status");
|
||||||
|
t.string("statusDetails");
|
||||||
t.uuid("dynamicSecretId").notNullable();
|
t.uuid("dynamicSecretId").notNullable();
|
||||||
t.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
|
t.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export const DynamicSecretLeasesSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
version: z.number(),
|
version: z.number(),
|
||||||
externalEntityId: z.string(),
|
externalEntityId: z.string(),
|
||||||
maxTTL: z.string().nullable().optional(),
|
|
||||||
expireAt: z.date(),
|
expireAt: z.date(),
|
||||||
|
status: z.string().nullable().optional(),
|
||||||
|
statusDetails: z.string().nullable().optional(),
|
||||||
dynamicSecretId: z.string().uuid(),
|
dynamicSecretId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export const DynamicSecretsSchema = z.object({
|
|||||||
algorithm: z.string().default("aes-256-gcm"),
|
algorithm: z.string().default("aes-256-gcm"),
|
||||||
keyEncoding: z.string().default("utf8"),
|
keyEncoding: z.string().default("utf8"),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
isDeleting: z.boolean().default(false).nullable().optional(),
|
|
||||||
status: z.string().nullable().optional(),
|
status: z.string().nullable().optional(),
|
||||||
|
statusDetails: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IntegrationAuthsSchema, SecretApprovalPoliciesSchema, UsersSchema } from "@app/db/schemas";
|
import {
|
||||||
|
DynamicSecretsSchema,
|
||||||
|
IntegrationAuthsSchema,
|
||||||
|
SecretApprovalPoliciesSchema,
|
||||||
|
UsersSchema
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
|
||||||
// sometimes the return data must be santizied to avoid leaking important values
|
// sometimes the return data must be santizied to avoid leaking important values
|
||||||
// always prefer pick over omit in zod
|
// always prefer pick over omit in zod
|
||||||
@@ -56,3 +61,11 @@ export const secretRawSchema = z.object({
|
|||||||
secretValue: z.string(),
|
secretValue: z.string(),
|
||||||
secretComment: z.string().optional()
|
secretComment: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||||
|
inputIV: true,
|
||||||
|
inputTag: true,
|
||||||
|
inputCiphertext: true,
|
||||||
|
keyEncoding: true,
|
||||||
|
algorithm: true
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import ms from "ms";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -16,8 +18,15 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
|||||||
ttl: z
|
ttl: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.refine((val) => typeof val === "undefined" || ms(val) > 0, "TTL must be a positive number"),
|
.superRefine((val, ctx) => {
|
||||||
path: z.string().default("/"),
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -49,7 +58,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
path: z.string(),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -80,8 +89,19 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
|||||||
leaseId: z.string()
|
leaseId: z.string()
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
path: z.string(),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -113,7 +133,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
|||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
path: z.string(),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { DynamicSecretsSchema } from "@app/db/schemas";
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { DynamicSecretProviderSchema } from "@app/services/dynamic-secret/providers/models";
|
import { DynamicSecretProviderSchema } from "@app/services/dynamic-secret/providers/models";
|
||||||
|
|
||||||
|
import { SanitizedDynamicSecretSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
@@ -14,18 +17,32 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
provider: DynamicSecretProviderSchema,
|
provider: DynamicSecretProviderSchema,
|
||||||
defaultTTL: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
maxTTL: z
|
maxTTL: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.refine((val) => typeof val === "undefined" || ms(val) > 0, "Max TTL must be a positive number"),
|
.superRefine((val, ctx) => {
|
||||||
path: z.string().default("/"),
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
slug: z.string().toLowerCase()
|
slug: z.string().toLowerCase()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
dynamicSecret: DynamicSecretsSchema
|
dynamicSecret: SanitizedDynamicSecretSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,22 +68,39 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
inputs: z.any().optional(),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
defaultTTL: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.refine((val) => typeof val === "undefined" || ms(val) > 0, "TTL must be a positive number"),
|
|
||||||
maxTTL: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.refine((val) => typeof val === "undefined" || ms(val) > 0, "Max TTL must be a positive number"),
|
|
||||||
path: z.string(),
|
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
newSlug: z.string()
|
data: z.object({
|
||||||
|
inputs: z.any().optional(),
|
||||||
|
defaultTTL: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
newSlug: z.string().optional()
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
dynamicSecret: DynamicSecretsSchema
|
dynamicSecret: SanitizedDynamicSecretSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,7 +112,10 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
slug: req.params.slug,
|
slug: req.params.slug,
|
||||||
...req.body
|
path: req.body.path,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
environment: req.body.environment,
|
||||||
|
...req.body.data
|
||||||
});
|
});
|
||||||
return { dynamicSecret: dynamicSecretCfg };
|
return { dynamicSecret: dynamicSecretCfg };
|
||||||
}
|
}
|
||||||
@@ -93,12 +130,12 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
path: z.string(),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
dynamicSecret: DynamicSecretsSchema
|
dynamicSecret: SanitizedDynamicSecretSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -116,18 +153,52 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:slug",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
environment: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
dynamicSecret: SanitizedDynamicSecretSchema.extend({
|
||||||
|
inputs: z.unknown()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const dynamicSecretCfg = await server.services.dynamicSecret.getDetails({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
slug: req.params.slug,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
return { dynamicSecret: dynamicSecretCfg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/",
|
url: "/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
path: z.string(),
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
environment: z.string()
|
environment: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
dynamicSecrets: DynamicSecretsSchema.array()
|
dynamicSecrets: SanitizedDynamicSecretSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||||
|
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||||
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||||
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||||
|
|
||||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find">;
|
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById">;
|
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,44 +90,60 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
|
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
|
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
|
||||||
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
|
||||||
if (!dynamicSecretCfg.isDeleting) throw new DisableRotationErrors({ message: "Document not deleted" });
|
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
||||||
|
throw new DisableRotationErrors({ message: "Document not deleted" });
|
||||||
|
|
||||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
||||||
if (dynamicSecretLeases.length) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
if (dynamicSecretLeases.length) {
|
||||||
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
|
const decryptedStoredInput = JSON.parse(
|
||||||
|
infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
|
tag: dynamicSecretCfg.inputTag,
|
||||||
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
await Promise.allSettled(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||||
const decryptedStoredInput = JSON.parse(
|
await Promise.allSettled(
|
||||||
infisicalSymmetricDecrypt({
|
dynamicSecretLeases.map(({ externalEntityId }) =>
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
selectedProvider.revoke(decryptedStoredInput, externalEntityId)
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
)
|
||||||
tag: dynamicSecretCfg.inputTag,
|
);
|
||||||
iv: dynamicSecretCfg.inputIV
|
}
|
||||||
})
|
|
||||||
) as object;
|
|
||||||
|
|
||||||
await Promise.allSettled(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
|
||||||
await Promise.allSettled(
|
|
||||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
|
||||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
|
await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
|
||||||
}
|
}
|
||||||
logger.info("Finished dynamic secret job", job.id);
|
logger.info("Finished dynamic secret job", job.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
|
||||||
|
if (job?.name === QueueJobs.DynamicSecretPruning) {
|
||||||
|
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
|
||||||
|
await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
|
||||||
|
status: DynamicSecretStatus.FailedDeletion,
|
||||||
|
statusDetails: (error as Error)?.message?.slice(0, 255)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job?.name === QueueJobs.DynamicSecretRevocation) {
|
||||||
|
const { leaseId } = job.data as { leaseId: string };
|
||||||
|
await dynamicSecretLeaseDAL.updateById(leaseId, {
|
||||||
|
status: DynamicSecretStatus.FailedDeletion,
|
||||||
|
statusDetails: (error as Error)?.message?.slice(0, 255)
|
||||||
|
});
|
||||||
|
}
|
||||||
if (error instanceof DisableRotationErrors) {
|
if (error instanceof DisableRotationErrors) {
|
||||||
if (job.id) {
|
if (job.id) {
|
||||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id);
|
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// propogate to next part
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.listen(QueueName.AuditLogPrune, "failed", (err) => {
|
|
||||||
logger.error(err, `${QueueName.AuditLogPrune}: log pruning failed`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pruneDynamicSecret,
|
pruneDynamicSecret,
|
||||||
setLeaseRevocation,
|
setLeaseRevocation,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(new Date()) - Number(expireAt));
|
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||||
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||||
expireAt,
|
expireAt,
|
||||||
externalEntityId: entityId
|
externalEntityId: entityId
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum DynamicSecretLeaseStatus {
|
||||||
|
FailedDeletion = "Failed to delete"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCreateDynamicSecretLeaseDTO = {
|
export type TCreateDynamicSecretLeaseDTO = {
|
||||||
slug: string;
|
slug: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/
|
|||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||||
import {
|
import {
|
||||||
|
DynamicSecretStatus,
|
||||||
TCreateDynamicSecretDTO,
|
TCreateDynamicSecretDTO,
|
||||||
TDeleteDyanmicSecretDTO,
|
TDeleteDynamicSecretDTO,
|
||||||
TListDyanmicSecretsDTO,
|
TDetailsDynamicSecretDTO,
|
||||||
|
TListDynamicSecretsDTO,
|
||||||
TUpdateDynamicSecretDTO
|
TUpdateDynamicSecretDTO
|
||||||
} from "./dynamic-secret-types";
|
} from "./dynamic-secret-types";
|
||||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||||
@@ -146,7 +148,9 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
keyEncoding: encryptedInput.encoding,
|
keyEncoding: encryptedInput.encoding,
|
||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
slug: newSlug ?? slug
|
slug: newSlug ?? slug,
|
||||||
|
status: null,
|
||||||
|
statusDetails: null
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedDynamicCfg;
|
return updatedDynamicCfg;
|
||||||
@@ -161,7 +165,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
slug,
|
slug,
|
||||||
path,
|
path,
|
||||||
environment
|
environment
|
||||||
}: TDeleteDyanmicSecretDTO) => {
|
}: TDeleteDynamicSecretDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -184,7 +188,9 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
// if leases exist we should flag it as deleting and then remove leases in background
|
// if leases exist we should flag it as deleting and then remove leases in background
|
||||||
// then delete the main one
|
// then delete the main one
|
||||||
if (leases.length) {
|
if (leases.length) {
|
||||||
const updatedDynamicSecretCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { isDeleting: true });
|
const updatedDynamicSecretCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||||
|
status: DynamicSecretStatus.Deleting
|
||||||
|
});
|
||||||
await dynamicSecretQueueService.pruneDynamicSecret(updatedDynamicSecretCfg.id);
|
await dynamicSecretQueueService.pruneDynamicSecret(updatedDynamicSecretCfg.id);
|
||||||
return updatedDynamicSecretCfg;
|
return updatedDynamicSecretCfg;
|
||||||
}
|
}
|
||||||
@@ -193,6 +199,46 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
return deletedDynamicSecretCfg;
|
return deletedDynamicSecretCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDetails = async ({
|
||||||
|
slug,
|
||||||
|
projectId,
|
||||||
|
path,
|
||||||
|
environment,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId,
|
||||||
|
actor
|
||||||
|
}: TDetailsDynamicSecretDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||||
|
);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
|
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ slug, folderId: folder.id });
|
||||||
|
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||||
|
const decryptedStoredInput = JSON.parse(
|
||||||
|
infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
|
tag: dynamicSecretCfg.inputTag,
|
||||||
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
|
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||||
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
|
};
|
||||||
|
|
||||||
const list = async ({
|
const list = async ({
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@@ -201,7 +247,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
path,
|
path,
|
||||||
environment
|
environment
|
||||||
}: TListDyanmicSecretsDTO) => {
|
}: TListDynamicSecretsDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -225,6 +271,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
create,
|
create,
|
||||||
updateBySlug,
|
updateBySlug,
|
||||||
deleteBySlug,
|
deleteBySlug,
|
||||||
|
getDetails,
|
||||||
list
|
list
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import { TProjectPermission } from "@app/lib/types";
|
|||||||
|
|
||||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||||
|
|
||||||
|
// various status for dynamic secret that happens in background
|
||||||
|
export enum DynamicSecretStatus {
|
||||||
|
Deleting = "Revocation in process",
|
||||||
|
FailedDeletion = "Failed to delete"
|
||||||
|
}
|
||||||
|
|
||||||
type TProvider = z.infer<typeof DynamicSecretProviderSchema>;
|
type TProvider = z.infer<typeof DynamicSecretProviderSchema>;
|
||||||
export type TCreateDynamicSecretDTO = {
|
export type TCreateDynamicSecretDTO = {
|
||||||
provider: TProvider;
|
provider: TProvider;
|
||||||
defaultTTL: string;
|
defaultTTL: string;
|
||||||
maxTTL?: string;
|
maxTTL?: string | null;
|
||||||
path: string;
|
path: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -18,19 +24,25 @@ export type TUpdateDynamicSecretDTO = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
newSlug?: string;
|
newSlug?: string;
|
||||||
defaultTTL?: string;
|
defaultTTL?: string;
|
||||||
maxTTL?: string;
|
maxTTL?: string | null;
|
||||||
path: string;
|
path: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
inputs?: TProvider["inputs"];
|
inputs?: TProvider["inputs"];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TDeleteDyanmicSecretDTO = {
|
export type TDeleteDynamicSecretDTO = {
|
||||||
slug: string;
|
slug: string;
|
||||||
path: string;
|
path: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TListDyanmicSecretsDTO = {
|
export type TDetailsDynamicSecretDTO = {
|
||||||
|
slug: string;
|
||||||
|
path: string;
|
||||||
|
environment: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TListDynamicSecretsDTO = {
|
||||||
path: string;
|
path: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|||||||
Reference in New Issue
Block a user