misc: added ability for users to select KMS during project creation

This commit is contained in:
Sheen Capadngan
2024-07-30 23:03:54 +05:30
committed by =
parent a57d1f1c9a
commit 6b6fd9735c
11 changed files with 197 additions and 57 deletions
+2 -1
View File
@@ -627,7 +627,8 @@ export const registerRoutes = async (
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore, keyStore,
kmsService kmsService,
kmsDAL
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -161,7 +161,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid slug" message: "Slug must be a valid slug"
}) })
.optional() .optional()
.describe(PROJECTS.CREATE.slug) .describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -177,7 +178,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName, workspaceName: req.body.projectName,
slug: req.body.slug slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId
}); });
await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
+1 -1
View File
@@ -40,7 +40,7 @@ type TKmsServiceFactoryDep = {
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>; export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
const INTERNAL_KMS_KEY_ID = "internal"; export const INTERNAL_KMS_KEY_ID = "internal";
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
@@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TKmsKeyDALFactory } from "../kms/kms-key-dal";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
@@ -83,6 +84,7 @@ type TProjectServiceFactoryDep = {
TKmsServiceFactory, TKmsServiceFactory,
"updateProjectSecretManagerKmsKey" | "getProjectKeyBackup" | "loadProjectKeyBackup" "updateProjectSecretManagerKmsKey" | "getProjectKeyBackup" | "loadProjectKeyBackup"
>; >;
kmsDAL: Pick<TKmsKeyDALFactory, "findByIdWithAssociatedKms">;
}; };
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>; export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -108,7 +110,8 @@ export const projectServiceFactory = ({
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
keyStore, keyStore,
kmsService kmsService,
kmsDAL
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * Create workspace. Make user the admin
@@ -119,7 +122,8 @@ export const projectServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
workspaceName, workspaceName,
slug: projectSlug slug: projectSlug,
kmsKeyId
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -147,16 +151,34 @@ export const projectServiceFactory = ({
const results = await projectDAL.transaction(async (tx) => { const results = await projectDAL.transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(organization.id, tx); const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) {
const kms = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId, tx);
if (!kms.id) {
throw new NotFoundError({
message: "KMS not found"
});
}
if (kms.orgId !== organization.id) {
throw new BadRequestError({
message: "KMS does not belong in the organization"
});
}
}
const project = await projectDAL.create( const project = await projectDAL.create(
{ {
name: workspaceName, name: workspaceName,
orgId: organization.id, orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
version: ProjectVersion.V2, version: ProjectVersion.V2,
pitVersionLimit: 10 pitVersionLimit: 10,
kmsSecretManagerKeyId: kmsKeyId
}, },
tx tx
); );
// set ghost user as admin of project // set ghost user as admin of project
const projectMembership = await projectMembershipDAL.create( const projectMembership = await projectMembershipDAL.create(
{ {
@@ -27,6 +27,7 @@ export type TCreateProjectDTO = {
actorOrgId?: string; actorOrgId?: string;
workspaceName: string; workspaceName: string;
slug?: string; slug?: string;
kmsKeyId?: string;
}; };
export type TDeleteProjectBySlugDTO = { export type TDeleteProjectBySlugDTO = {
+2
View File
@@ -29,3 +29,5 @@ export type KmsListEntry = {
export enum ExternalKmsProvider { export enum ExternalKmsProvider {
AWS = "aws" AWS = "aws"
} }
export const INTERNAL_KMS_KEY_ID = "internal";
+6 -4
View File
@@ -226,18 +226,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
}); });
export const createWorkspace = ({ export const createWorkspace = ({
projectName projectName,
kmsKeyId
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => { }: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", { projectName }); return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId });
}; };
export const useCreateWorkspace = () => { export const useCreateWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({ return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ projectName }) => mutationFn: async ({ projectName, kmsKeyId }) =>
createWorkspace({ createWorkspace({
projectName projectName,
kmsKeyId
}), }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
@@ -48,6 +48,7 @@ export type TGetUpgradeProjectStatusDTO = {
// mutation dto // mutation dto
export type CreateWorkspaceDTO = { export type CreateWorkspaceDTO = {
projectName: string; projectName: string;
kmsKeyId?: string;
}; };
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
+77 -22
View File
@@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage"; import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
import { import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button, Button,
Checkbox, Checkbox,
DropdownMenu, DropdownMenu,
@@ -66,11 +70,13 @@ import {
useAddUserToWsNonE2EE, useAddUserToWsNonE2EE,
useCreateWorkspace, useCreateWorkspace,
useGetAccessRequestsCount, useGetAccessRequestsCount,
useGetExternalKmsList,
useGetOrgTrialUrl, useGetOrgTrialUrl,
useGetSecretApprovalRequestCount, useGetSecretApprovalRequestCount,
useLogoutUser, useLogoutUser,
useSelectOrganization useSelectOrganization
} from "@app/hooks/api"; } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { Workspace } from "@app/hooks/api/types"; import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
@@ -113,7 +119,8 @@ const formSchema = yup.object({
.label("Project Name") .label("Project Name")
.trim() .trim()
.max(64, "Too long, maximum length is 64 characters"), .max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members") addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID")
}); });
type TAddProjectFormData = yup.InferType<typeof formSchema>; type TAddProjectFormData = yup.InferType<typeof formSchema>;
@@ -147,6 +154,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const pendingRequestsCount = useMemo(() => { const pendingRequestsCount = useMemo(() => {
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
@@ -172,7 +180,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
reset, reset,
handleSubmit handleSubmit
} = useForm<TAddProjectFormData>({ } = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema) resolver: yupResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID
}
}); });
const { t } = useTranslation(); const { t } = useTranslation();
@@ -245,7 +256,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
putUserInOrg(); putUserInOrg();
}, [router.query.id]); }, [router.query.id]);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
// type check // type check
if (!currentOrg) return; if (!currentOrg) return;
if (!user) return; if (!user) return;
@@ -255,7 +266,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
project: { id: newProjectId } project: { id: newProjectId }
} }
} = await createWs.mutateAsync({ } = await createWs.mutateAsync({
projectName: name projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
}); });
if (addMembers) { if (addMembers) {
@@ -889,24 +901,67 @@ export const AppLayout = ({ children }: LayoutProps) => {
)} )}
/> />
</div> </div>
<div className="mt-7 flex items-center"> <div className="mt-14 flex">
<Button <Accordion type="single" collapsible className="w-full">
isDisabled={isSubmitting} <AccordionItem
isLoading={isSubmitting} value="advance-settings"
key="layout-create-project-submit" className="data-[state=open]:border-none"
className="mr-4" >
type="submit" <AccordionTrigger className="h-fit flex-none pl-1 text-sm">
> <div className="order-1 ml-3">Advanced Settings</div>
Create Project </AccordionTrigger>
</Button> <AccordionContent>
<Button <Controller
key="layout-cancel-create-project" render={({ field: { onChange, ...field }, fieldState: { error } }) => (
onClick={() => handlePopUpClose("addNewWs")} <FormControl
variant="plain" errorText={error?.message}
colorSchema="secondary" isError={Boolean(error)}
> label="KMS"
Cancel >
</Button> <Select
{...field}
onValueChange={(e) => {
onChange(e);
}}
className="mb-12 mr-4 w-full bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex items-end justify-end">
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
colorSchema="secondary"
variant="plain"
className="py-2"
>
Cancel
</Button>
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="ml-4"
type="submit"
>
Create Project
</Button>
</div>
</div> </div>
</form> </form>
</ModalContent> </ModalContent>
+77 -22
View File
@@ -36,6 +36,10 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button, Button,
Checkbox, Checkbox,
FormControl, FormControl,
@@ -43,6 +47,8 @@ import {
Input, Input,
Modal, Modal,
ModalContent, ModalContent,
Select,
SelectItem,
Skeleton, Skeleton,
UpgradePlanModal UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
@@ -59,8 +65,10 @@ import {
fetchOrgUsers, fetchOrgUsers,
useAddUserToWsNonE2EE, useAddUserToWsNonE2EE,
useCreateWorkspace, useCreateWorkspace,
useGetExternalKmsList,
useRegisterUserAction useRegisterUserAction
} from "@app/hooks/api"; } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types"; import { Workspace } from "@app/hooks/api/types";
@@ -473,7 +481,8 @@ const formSchema = yup.object({
.label("Project Name") .label("Project Name")
.trim() .trim()
.max(64, "Too long, maximum length is 64 characters"), .max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members") addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID")
}); });
type TAddProjectFormData = yup.InferType<typeof formSchema>; type TAddProjectFormData = yup.InferType<typeof formSchema>;
@@ -506,7 +515,10 @@ const OrganizationPage = withPermission(
reset, reset,
handleSubmit handleSubmit
} = useForm<TAddProjectFormData>({ } = useForm<TAddProjectFormData>({
resolver: yupResolver(formSchema) resolver: yupResolver(formSchema),
defaultValues: {
kmsKeyId: INTERNAL_KMS_KEY_ID
}
}); });
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
@@ -521,7 +533,9 @@ const OrganizationPage = withPermission(
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
); );
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
// type check // type check
if (!currentOrg) return; if (!currentOrg) return;
if (!user) return; if (!user) return;
@@ -531,7 +545,8 @@ const OrganizationPage = withPermission(
project: { id: newProjectId } project: { id: newProjectId }
} }
} = await createWs.mutateAsync({ } = await createWs.mutateAsync({
projectName: name projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
}); });
if (addMembers) { if (addMembers) {
@@ -1064,24 +1079,64 @@ const OrganizationPage = withPermission(
)} )}
/> />
</div> </div>
<div className="mt-7 flex items-center"> <div className="mt-14 flex">
<Button <Accordion type="single" collapsible className="w-full">
isDisabled={isSubmitting} <AccordionItem value="advance-settings" className="data-[state=open]:border-none">
isLoading={isSubmitting} <AccordionTrigger className="h-fit flex-none pl-1 text-sm">
key="layout-create-project-submit" <div className="order-1 ml-3">Advanced Settings</div>
className="mr-4" </AccordionTrigger>
type="submit" <AccordionContent>
> <Controller
Create Project render={({ field: { onChange, ...field }, fieldState: { error } }) => (
</Button> <FormControl
<Button errorText={error?.message}
key="layout-cancel-create-project" isError={Boolean(error)}
onClick={() => handlePopUpClose("addNewWs")} label="KMS"
variant="plain" >
colorSchema="secondary" <Select
> {...field}
Cancel onValueChange={(e) => {
</Button> onChange(e);
}}
className="mb-12 mr-4 w-full bg-mineshaft-600"
>
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug}
</SelectItem>
))}
</Select>
</FormControl>
)}
control={control}
name="kmsKeyId"
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex items-end justify-end">
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose("addNewWs")}
colorSchema="secondary"
variant="plain"
className="py-2"
>
Cancel
</Button>
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="ml-4"
type="submit"
>
Create Project
</Button>
</div>
</div> </div>
</form> </form>
</ModalContent> </ModalContent>
@@ -31,6 +31,7 @@ import {
useUpdateProjectKms useUpdateProjectKms
} from "@app/hooks/api"; } from "@app/hooks/api";
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries"; import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { Organization, Workspace } from "@app/hooks/api/types"; import { Organization, Workspace } from "@app/hooks/api/types";
const formSchema = z.object({ const formSchema = z.object({
@@ -39,8 +40,6 @@ const formSchema = z.object({
type TForm = z.infer<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
const INTERNAL_KMS_KEY_ID = "internal";
const BackupConfirmationModal = ({ const BackupConfirmationModal = ({
isOpen, isOpen,
onOpenChange, onOpenChange,