mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 01:52:22 +00:00
feat(server): added some more validation and feedback for deletion etc
This commit is contained in:
@@ -5,13 +5,14 @@ import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
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 { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||
|
||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find">;
|
||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById">;
|
||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||
};
|
||||
|
||||
@@ -89,44 +90,60 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
|
||||
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 });
|
||||
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];
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
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 Promise.allSettled(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||
await Promise.allSettled(
|
||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
|
||||
}
|
||||
logger.info("Finished dynamic secret job", job.id);
|
||||
} 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 (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 {
|
||||
pruneDynamicSecret,
|
||||
setLeaseRevocation,
|
||||
|
||||
@@ -156,7 +156,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
);
|
||||
|
||||
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, {
|
||||
expireAt,
|
||||
externalEntityId: entityId
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum DynamicSecretLeaseStatus {
|
||||
FailedDeletion = "Failed to delete"
|
||||
}
|
||||
|
||||
export type TCreateDynamicSecretLeaseDTO = {
|
||||
slug: string;
|
||||
path: string;
|
||||
|
||||
@@ -11,9 +11,11 @@ import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import {
|
||||
DynamicSecretStatus,
|
||||
TCreateDynamicSecretDTO,
|
||||
TDeleteDyanmicSecretDTO,
|
||||
TListDyanmicSecretsDTO,
|
||||
TDeleteDynamicSecretDTO,
|
||||
TDetailsDynamicSecretDTO,
|
||||
TListDynamicSecretsDTO,
|
||||
TUpdateDynamicSecretDTO
|
||||
} from "./dynamic-secret-types";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
|
||||
@@ -146,7 +148,9 @@ export const dynamicSecretServiceFactory = ({
|
||||
keyEncoding: encryptedInput.encoding,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
slug: newSlug ?? slug
|
||||
slug: newSlug ?? slug,
|
||||
status: null,
|
||||
statusDetails: null
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
@@ -161,7 +165,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
slug,
|
||||
path,
|
||||
environment
|
||||
}: TDeleteDyanmicSecretDTO) => {
|
||||
}: TDeleteDynamicSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -184,7 +188,9 @@ export const dynamicSecretServiceFactory = ({
|
||||
// if leases exist we should flag it as deleting and then remove leases in background
|
||||
// then delete the main one
|
||||
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);
|
||||
return updatedDynamicSecretCfg;
|
||||
}
|
||||
@@ -193,6 +199,46 @@ export const dynamicSecretServiceFactory = ({
|
||||
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 ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
@@ -201,7 +247,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectId,
|
||||
path,
|
||||
environment
|
||||
}: TListDyanmicSecretsDTO) => {
|
||||
}: TListDynamicSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -225,6 +271,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
create,
|
||||
updateBySlug,
|
||||
deleteBySlug,
|
||||
getDetails,
|
||||
list
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,11 +4,17 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
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>;
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
provider: TProvider;
|
||||
defaultTTL: string;
|
||||
maxTTL?: string;
|
||||
maxTTL?: string | null;
|
||||
path: string;
|
||||
environment: string;
|
||||
slug: string;
|
||||
@@ -18,19 +24,25 @@ export type TUpdateDynamicSecretDTO = {
|
||||
slug: string;
|
||||
newSlug?: string;
|
||||
defaultTTL?: string;
|
||||
maxTTL?: string;
|
||||
maxTTL?: string | null;
|
||||
path: string;
|
||||
environment: string;
|
||||
inputs?: TProvider["inputs"];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeleteDyanmicSecretDTO = {
|
||||
export type TDeleteDynamicSecretDTO = {
|
||||
slug: string;
|
||||
path: string;
|
||||
environment: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TListDyanmicSecretsDTO = {
|
||||
export type TDetailsDynamicSecretDTO = {
|
||||
slug: string;
|
||||
path: string;
|
||||
environment: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TListDynamicSecretsDTO = {
|
||||
path: string;
|
||||
environment: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
Reference in New Issue
Block a user