misc: developed create kms backup feature

This commit is contained in:
Sheen Capadngan
2024-07-19 01:31:13 +08:00
committed by =
parent 4d032cfbfa
commit 80be054425
5 changed files with 127 additions and 6 deletions

View File

@@ -254,4 +254,36 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "GET",
url: "/:workspaceId/kms/backup",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
secretManager: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const backup = await server.services.project.getProjectKmsBackup({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId
});
// TODO: add audit log
return backup;
}
});
};

View File

@@ -585,6 +585,29 @@ export const kmsServiceFactory = ({
});
};
const getProjectKeyBackup = async (projectId: string) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({
message: "Project not found"
});
}
const secretManagerDataKey = await getProjectSecretManagerKmsDataKey(projectId);
const kmsKeyIdForEncrypt = await getOrgKmsKeyId(project.orgId);
const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt });
const { cipherTextBlob: encryptedSecretManagerDataKey } = await kmsEncryptor({ plainText: secretManagerDataKey });
// format: projectId.kmsFunction.kmsId.Base64(encryptedDataKey)
const secretManagerBackup = `${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKey.toString(
"base64"
)}`;
return {
secretManager: secretManagerBackup
};
};
const startService = async () => {
const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future
@@ -642,6 +665,7 @@ export const kmsServiceFactory = ({
getOrgKmsDataKey,
getProjectSecretManagerKmsDataKey,
getProjectSecretManagerKmsKey,
updateProjectSecretManagerKmsKey
updateProjectSecretManagerKmsKey,
getProjectKeyBackup
};
};

View File

@@ -78,7 +78,7 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
kmsService: Pick<TKmsServiceFactory, "updateProjectSecretManagerKmsKey">;
kmsService: Pick<TKmsServiceFactory, "updateProjectSecretManagerKmsKey" | "getProjectKeyBackup">;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -693,6 +693,34 @@ export const projectServiceFactory = ({
};
};
const getProjectKmsBackup = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.externalKms) {
throw new BadRequestError({
message: "Failed to create KMS backup due to plan restriction. Upgrade to the enterprise plan."
});
}
const kmsBackup = await kmsService.getProjectKeyBackup(projectId);
return kmsBackup;
};
return {
createProject,
deleteProject,
@@ -707,6 +735,7 @@ export const projectServiceFactory = ({
listProjectCertificates,
updateVersionLimit,
updateAuditLogsRetention,
updateProjectKmsKey
updateProjectKmsKey,
getProjectKmsBackup
};
};

View File

@@ -53,3 +53,11 @@ export const useGetActiveProjectKms = (projectId: string) => {
}
});
};
export const fetchProjectKmsBackup = async (projectId: string) => {
const { data } = await apiRequest.get<{
secretManager: string;
}>(`/api/v1/workspace/${projectId}/kms/backup`);
return data;
};

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FileSaver from "file-saver";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -13,6 +14,7 @@ import {
useWorkspace
} from "@app/context";
import { useGetActiveProjectKms, useGetExternalKmsList, useUpdateProjectKms } from "@app/hooks/api";
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
const formSchema = z.object({
kmsKeyId: z.string()
@@ -55,7 +57,7 @@ export const EncryptionTab = () => {
}
}, [kmsKeyId]);
const onFormSubmit = async (data: TForm) => {
const onUpdateProjectKms = async (data: TForm) => {
try {
await updateProjectKms({
secretManagerKmsKeyId: data.kmsKeyId
@@ -70,12 +72,38 @@ export const EncryptionTab = () => {
}
};
const downloadKmsBackup = async () => {
if (!currentWorkspace || !currentOrg) {
return;
}
const { secretManager } = await fetchProjectKmsBackup(currentWorkspace.id);
const [, kmsFunction] = secretManager.split(".");
const file = secretManager;
const blob = new Blob([file], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(
blob,
`kms-backup-${currentOrg.slug}-${currentWorkspace.slug}-${kmsFunction}.infisical.txt`
);
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
onSubmit={handleSubmit(onUpdateProjectKms)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Key Management</h2>
<div className="flex justify-between">
<h2 className="mb-2 flex-1 text-xl font-semibold text-mineshaft-100">Key Management</h2>
<div className="space-x-2">
<Button colorSchema="secondary">Load Backup</Button>
<Button colorSchema="secondary" onClick={downloadKmsBackup}>
Create Backup
</Button>
</div>
</div>
<p className="mb-4 text-gray-400">
Select which Key Management System to use for encrypting your project data
</p>