Preliminary scaffolding for CA structures

This commit is contained in:
Tuan Dang
2024-05-22 21:33:06 -07:00
parent 303edadb1e
commit 26ea949a4e
41 changed files with 1656 additions and 8 deletions
+6
View File
@@ -0,0 +1,6 @@
import { CertificateAuthorityType } from "./enums";
export const caTypeToNameMap: { [K in CertificateAuthorityType]: string } = {
[CertificateAuthorityType.ROOT]: "Root",
[CertificateAuthorityType.INTERMEDIATE]: "Intermediate"
};
+4
View File
@@ -0,0 +1,4 @@
export enum CertificateAuthorityType {
ROOT = "root",
INTERMEDIATE = "intermediate"
}
+2
View File
@@ -0,0 +1,2 @@
export { CertificateAuthorityType } from "./enums";
export { useCreateCa } from "./mutations";
+23
View File
@@ -0,0 +1,23 @@
import {
useMutation
// useQueryClient
} from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCertificateAuthority, TCreateCaDTO } from "./types";
export const useCreateCa = () => {
// const queryClient = useQueryClient();
return useMutation<TCertificateAuthority, {}, TCreateCaDTO>({
mutationFn: async (body) => {
const {
data: { identity }
} = await apiRequest.post("/api/v1/ca/", body);
return identity;
},
onSuccess: () => {
// queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
}
});
};
+23
View File
@@ -0,0 +1,23 @@
import { CertificateAuthorityType } from "./enums";
export type TCertificateAuthority = {
id: string;
parentCaId?: string;
projectId: string;
type: CertificateAuthorityType;
dn: string;
commonName: string;
createdAt: string;
updatedAt: string;
};
export type TCreateCaDTO = {
projectSlug: string;
type: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
commonName: string;
};
+1
View File
@@ -5,6 +5,7 @@ export * from "./auditLogs";
export * from "./auditLogStreams";
export * from "./auth";
export * from "./bots";
export * from "./ca";
export * from "./dynamicSecret";
export * from "./dynamicSecretLease";
export * from "./groups";
+2 -2
View File
@@ -21,6 +21,7 @@ export {
useGetWorkspaceIntegrations,
useGetWorkspaceSecrets,
useGetWorkspaceUsers,
useListWorkspaceCas,
useListWorkspaceGroups,
useNameWorkspaceSecrets,
useRenameWorkspace,
@@ -28,5 +29,4 @@ export {
useUpdateIdentityWorkspaceRole,
useUpdateUserWorkspaceRole,
useUpdateWsEnvironment,
useUpgradeProject
} from "./queries";
useUpgradeProject} from "./queries";
+18 -1
View File
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCertificateAuthority } from "../ca/types";
import { TGroupMembership } from "../groups/types";
import { IdentityMembership } from "../identities/types";
import { IntegrationAuth } from "../integrationAuth/types";
@@ -39,7 +40,8 @@ export const workspaceKeys = {
getWorkspaceIdentityMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-identity-memberships"] as const,
getWorkspaceGroupMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-groups"] as const
[{ workspaceId }, "workspace-groups"] as const,
getWorkspaceCas: (workspaceId: string) => [{ workspaceId }, "workspace-cas"] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
@@ -469,3 +471,18 @@ export const useListWorkspaceGroups = (projectSlug: string) => {
enabled: true
});
};
export const useListWorkspaceCas = (projectSlug: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceCas(projectSlug),
queryFn: async () => {
const {
data: { cas }
} = await apiRequest.get<{ cas: TCertificateAuthority[] }>(
`/api/v2/workspace/${projectSlug}/cas`
);
return cas;
},
enabled: Boolean(projectSlug)
});
};
@@ -519,6 +519,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?.id}/certificates`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?.id}/certificates`
}
icon="system-outline-90-lock-closed"
>
Certificates
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?.id}/members`} passHref>
<a>
<MenuItem
@@ -0,0 +1,24 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { CertificatesPage } from "@app/views/Project/CertificatesPage";
const Certificates = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<div>Test CA page</div>
<CertificatesPage />
</div>
);
};
export default Certificates;
Certificates.requireAuth = true;
@@ -0,0 +1,44 @@
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { CaTab } from "./components";
// import { CaTable } from "./components";
// TODO: fix permission
/**
* TODO 1: CA section
* TODO 2: Certificates section
*/
enum TabSections {
Ca = "certificate-authorities",
Certificates = "certificates"
}
export const CertificatesPage = withProjectPermission(
() => {
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Certificates</p>
<Tabs defaultValue={TabSections.Ca}>
<TabList>
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
<Tab value={TabSections.Certificates}>Certificates</Tab>
</TabList>
<TabPanel value={TabSections.Ca}>
<CaTab />
</TabPanel>
<TabPanel value={TabSections.Certificates}>
<div>Certs</div>
{/* <OrgGroupsTab /> */}
</TabPanel>
</Tabs>
</div>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
);
@@ -0,0 +1,17 @@
import { motion } from "framer-motion";
import { CaSection } from "./components";
export const CaTab = () => {
return (
<motion.div
key="panel-certificate-authorities"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<CaSection />
</motion.div>
);
};
@@ -0,0 +1,346 @@
// import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// import { yupResolver } from "@hookform/resolvers/yup";
// import * as yup from "yup";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { CertificateAuthorityType, useCreateCa } from "@app/hooks/api/ca";
// import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
type: z.enum(["root", "intermediate"]), // move to ref enum of hooks/api
organization: z.string(),
ou: z.string(),
country: z.string(),
province: z.string(),
locality: z.string(),
commonName: z.string()
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["ca"]>;
// handlePopUpOpen: (
// popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
// data: {
// identityId: string;
// name: string;
// authMethod?: IdentityAuthMethod;
// }
// ) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
};
const caTypes = [
{ label: "Root", value: CertificateAuthorityType.ROOT },
{ label: "intermediate", value: CertificateAuthorityType.INTERMEDIATE }
];
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
console.log("CaModal currentWorkspace: ", currentWorkspace);
const { mutateAsync: createMutateAsync } = useCreateCa();
// const { data: roles } = useGetOrgRoles(orgId);
// const { mutateAsync: createMutateAsync } = useCreateIdentity();
// const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
// const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
type: CertificateAuthorityType.ROOT,
organization: "",
ou: "",
country: "",
province: "",
locality: "",
commonName: ""
}
});
// useEffect(() => {
// const identity = popUp?.identity?.data as {
// identityId: string;
// name: string;
// role: string;
// customRole: {
// name: string;
// slug: string;
// };
// };
// if (!roles?.length) return;
// if (identity) {
// reset({
// name: identity.name,
// role: identity?.customRole?.slug ?? identity.role
// });
// } else {
// reset({
// name: "",
// role: roles[0].slug
// });
// }
// }, [popUp?.identity?.data, roles]);
const onFormSubmit = async ({
type,
commonName,
organization,
ou,
country,
locality,
province
}: FormData) => {
try {
console.log("onFormSubmit args: ", {
commonName,
organization,
ou,
country,
locality,
province
});
if (!currentWorkspace?.slug) return;
await createMutateAsync({
projectSlug: currentWorkspace.slug,
type,
commonName,
organization,
ou,
country,
province,
locality
});
// const identity = popUp?.identity?.data as {
// identityId: string;
// name: string;
// role: string;
// };
// if (identity) {
// // update
// await updateMutateAsync({
// identityId: identity.identityId,
// name,
// role: role || undefined,
// organizationId: orgId
// });
// handlePopUpToggle("identity", false);
// } else {
// // create
// const {
// id: createdId,
// name: createdName,
// authMethod
// } = await createMutateAsync({
// name,
// role: role || undefined,
// organizationId: orgId
// });
// await addMutateAsync({
// organizationId: orgId,
// identityId: createdId,
// clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
// accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
// accessTokenTTL: 2592000,
// accessTokenMaxTTL: 2592000,
// accessTokenNumUsesLimit: 0
// });
// handlePopUpToggle("identity", false);
// handlePopUpOpen("identityAuthMethod", {
// identityId: createdId,
// name: createdName,
// authMethod
// });
// }
createNotification({
text: `Successfully ${popUp?.ca?.data ? "updated" : "created"} CA`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ?? `Failed to ${popUp?.ca?.data ? "update" : "create"} CA`;
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.ca?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("ca", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.ca?.data ? "Update" : "Create"} CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="type"
defaultValue={CertificateAuthorityType.ROOT}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
// isDisabled={!!identityAuthMethodData?.authMethod}
>
{caTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="organization"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization (O)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Acme Corp" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="ou"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization Unit (OU)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Engineering" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="country"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Country Name (C)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="United States (US)" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="province"
render={({ field, fieldState: { error } }) => (
<FormControl
label="State or Province Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="California" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="locality"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Locality Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="San Francisco" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name (CN)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Example CA" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.ca?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("ca", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,141 @@
// import { useState } from "react";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { createNotification } from "@app/components/notifications";
// import { OrgPermissionCan } from "@app/components/permissions";
import {
Button
// DeleteActionModal,
// EmailServiceSetupModal,
// UpgradePlanModal
} from "@app/components/v2";
// import {
// OrgPermissionActions,
// OrgPermissionSubjects,
// useOrganization,
// useSubscription
// } from "@app/context";
// import { useDeleteOrgMembership } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "./CaModal";
import { CaTable } from "./CaTable";
// import { AddOrgMemberModal } from "./AddOrgMemberModal";
// import { OrgMembersTable } from "./OrgMembersTable";
export const CaSection = () => {
// const { subscription } = useSubscription();
// const { currentOrg } = useOrganization();
// const orgId = currentOrg?.id ?? "";
// const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
const {
popUp,
handlePopUpOpen,
// handlePopUpClose,
handlePopUpToggle
} = usePopUp(["ca"] as const);
// const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
// const isMoreUsersNotAllowed = subscription?.memberLimit
// ? subscription.membersUsed >= subscription.memberLimit
// : false;
// const handleAddMemberModal = () => {
// if (currentOrg?.authEnforced) {
// createNotification({
// text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
// type: "error"
// });
// return;
// }
// if (isMoreUsersNotAllowed) {
// handlePopUpOpen("upgradePlan", {
// description: "You can add more members if you upgrade your Infisical plan."
// });
// } else {
// handlePopUpOpen("addMember");
// }
// };
// const onRemoveMemberSubmit = async (orgMembershipId: string) => {
// try {
// await deleteMutateAsync({
// orgId,
// membershipId: orgMembershipId
// });
// createNotification({
// text: "Successfully removed user from org",
// type: "success"
// });
// } catch (err) {
// console.error(err);
// createNotification({
// text: "Failed to remove user from the organization",
// type: "error"
// });
// }
// handlePopUpClose("removeMember");
// };
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
{/* <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => ( */}
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("ca")}
// isDisabled={!isAllowed}
>
Create CA
</Button>
{/* )} */}
{/* </OrgPermissionCan> */}
</div>
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaTable />
{/* <OrgMembersTable
handlePopUpOpen={handlePopUpOpen}
setCompleteInviteLink={setCompleteInviteLink}
/>
<AddOrgMemberModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
completeInviteLink={completeInviteLink}
setCompleteInviteLink={setCompleteInviteLink}
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveMemberSubmit(
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
/> */}
</div>
);
};
@@ -0,0 +1,54 @@
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useListWorkspaceCas } from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
export const CaTable = () => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCas(currentWorkspace?.slug ?? "");
return (
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Subject</Th>
<Th>Status</Th>
<Th>Type</Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map((ca) => {
return (
<Tr className="h-10" key={`ca-${ca.id}`}>
<Td>{ca.dn}</Td>
<Td>Pending</Td>
<Td>{caTypeToNameMap[ca.type]}</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && data?.length === 0 && (
<EmptyState title="No groups have been added to this project" icon={faCertificate} />
)}
</TableContainer>
</div>
);
};
@@ -0,0 +1 @@
export { CaSection } from "./CaSection";
@@ -0,0 +1 @@
export { CaTab } from "./CaTab";
@@ -0,0 +1 @@
export { CaTab } from "./CaTab";
@@ -0,0 +1 @@
export { CertificatesPage } from "./CertificatesPage";