Add permissioning to SSH, add publicKey return for SSH CA, polish

This commit is contained in:
Tuan Dang
2024-12-03 17:38:23 -08:00
parent 4fc8c509ac
commit 07d9398aad
25 changed files with 397 additions and 106 deletions
@@ -1,3 +1,6 @@
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types";
export { OrgPermissionActions, OrgPermissionSubjects } from "./types";
export {
OrgPermissionActions,
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects} from "./types";
@@ -7,6 +7,15 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgPermissionSshCertificateTemplateActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
SignSshKey = "sign-ssh-key",
IssueSshCredentials = "issue-ssh-credentials"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
@@ -51,6 +60,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateAuthorities]
| [OrgPermissionActions, OrgPermissionSubjects.SshCertificateTemplates];
| [OrgPermissionSshCertificateTemplateActions, OrgPermissionSubjects.SshCertificateTemplates];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
+1
View File
@@ -4,6 +4,7 @@ export type { TOrgPermission } from "./OrgPermissionContext";
export {
OrgPermissionActions,
OrgPermissionProvider,
OrgPermissionSshCertificateTemplateActions,
OrgPermissionSubjects,
useOrgPermission
} from "./OrgPermissionContext";
@@ -504,7 +504,7 @@ export const useListOrgSshCas = ({ orgId }: { orgId: string }) => {
queryFn: async () => {
const {
data: { cas }
} = await apiRequest.get<{ cas: TSshCertificateAuthority[] }>(
} = await apiRequest.get<{ cas: Omit<TSshCertificateAuthority, "publicKey">[] }>(
`/api/v1/organization/${orgId}/ssh-cas`
);
return cas;
+2
View File
@@ -9,6 +9,7 @@ export type TSshCertificateAuthority = {
keyAlgorithm: CertKeyAlgorithm;
createdAt: string;
updatedAt: string;
publicKey: string;
};
export type TCreateSshCaDTO = {
@@ -18,6 +19,7 @@ export type TCreateSshCaDTO = {
export type TUpdateSshCaDTO = {
caId: string;
friendlyName?: string;
status?: SshCaStatus;
};
@@ -13,6 +13,17 @@ const generalPermissionSchema = z
})
.optional();
const sshCertificateTemplatePermissionSchmea = z
.object({
read: z.boolean().optional(),
edit: z.boolean().optional(),
delete: z.boolean().optional(),
create: z.boolean().optional(),
"sign-ssh-key": z.boolean().optional(),
"issue-ssh-credentials": z.boolean().optional()
})
.optional();
const adminConsolePermissionSchmea = z
.object({
"access-all-projects": z.boolean().optional()
@@ -49,7 +60,9 @@ export const formSchema = z.object({
identity: generalPermissionSchema,
"organization-admin-console": adminConsolePermissionSchmea,
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
[OrgPermissionSubjects.SshCertificateAuthorities]: generalPermissionSchema,
[OrgPermissionSubjects.SshCertificateTemplates]: sshCertificateTemplatePermissionSchmea
})
.optional()
});
@@ -70,12 +70,6 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
const onFormSubmit = async ({ name, description, slug }: FormData) => {
try {
console.log("onFormSubmit args: ", {
name,
description,
slug
});
if (!orgId) return;
if (role) {
@@ -51,6 +51,15 @@ const PROJECT_TEMPLATES_PERMISSIONS = [
{ action: "delete", label: "Remove" }
] as const;
const SSH_CERTIFICATE_TEMPLATES_PERMISSIONS = [
{ action: "read", label: "Read" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Modify" },
{ action: "delete", label: "Remove" },
{ action: "sign-ssh-key", label: "Sign SSH Key" },
{ action: "issue-ssh-credentials", label: "Issue SSH Credentials" }
] as const;
const getPermissionList = (option: string) => {
switch (option) {
case "secret-scanning":
@@ -63,6 +72,8 @@ const getPermissionList = (option: string) => {
return MEMBERS_PERMISSIONS;
case OrgPermissionSubjects.ProjectTemplates:
return PROJECT_TEMPLATES_PERMISSIONS;
case OrgPermissionSubjects.SshCertificateTemplates:
return SSH_CERTIFICATE_TEMPLATES_PERMISSIONS;
default:
return PERMISSIONS;
}
@@ -97,7 +108,7 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const totalActions = getPermissionList(formName).length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
@@ -69,7 +69,15 @@ const SIMPLE_PERMISSION_OPTIONS = [
title: "External KMS",
formName: OrgPermissionSubjects.Kms
},
{ title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates }
{ title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates },
{
title: "SSH Certificate Authorities",
formName: OrgPermissionSubjects.SshCertificateAuthorities
},
{
title: "SSH Certificate Templates",
formName: OrgPermissionSubjects.SshCertificateTemplates
}
] as const;
type Props = {
@@ -26,6 +26,7 @@ import { withPermission } from "@app/hoc";
import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { SshCaModal } from "../SshPage/components/SshCaModal";
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
export const SshCaPage = withPermission(
@@ -123,6 +124,7 @@ export const SshCaPage = withPermission(
</div>
</div>
)}
<SshCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSshCa.isOpen}
title="Are you sure want to remove the SSH CA from the project?"
@@ -1,5 +1,6 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faCopy, faDownload,faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
@@ -19,13 +20,21 @@ export const SshCaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const [downloadText, isDownloading, setDownloadText] = useTimedReset<string>({
initialState: "Save public key"
});
const { data: ca } = useGetSshCaById(caId);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return ca ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">SSH CA Details</h3>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.SshCertificateAuthorities}
@@ -82,10 +91,31 @@ export const SshCaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
</div>
<div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Public Key</p>
<div className="group flex align-top">
<p className="flex-1 text-sm text-mineshaft-300">{ca.publicKey.substring(0, 20)}...</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={downloadText}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
setDownloadText("Saved");
downloadTxtFile("ssh_ca.pub", ca.publicKey);
}}
>
<FontAwesomeIcon icon={isDownloading ? faCheck : faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
) : (
@@ -185,7 +185,7 @@ export const SshCertificateTemplateModal = ({ popUp, handlePopUpToggle, sshCaId
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="My SSH Certificate Template" />
<Input {...field} placeholder="administrator" />
</FormControl>
)}
/>
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionSshCertificateTemplateActions,OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteSshCertTemplate } from "@app/hooks/api";
@@ -50,7 +50,7 @@ export const SshCertificateTemplatesSection = ({ caId }: Props) => {
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
<OrgPermissionCan
I={OrgPermissionActions.Create}
I={OrgPermissionSshCertificateTemplateActions.Create}
a={OrgPermissionSubjects.SshCertificateTemplates}
>
{(isAllowed) => (
@@ -19,7 +19,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionSshCertificateTemplateActions,OrgPermissionSubjects } from "@app/context";
import { useGetSshCaCertTemplates } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -66,18 +66,23 @@ export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("sshCertificateTemplate", {
id: certificateTemplate.id
})
}
icon={<FontAwesomeIcon icon={faFileAlt} size="sm" className="mr-1" />}
>
Edit Template
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
I={OrgPermissionSshCertificateTemplateActions.Edit}
a={OrgPermissionSubjects.SshCertificateTemplates}
>
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("sshCertificateTemplate", {
id: certificateTemplate.id
})
}
icon={<FontAwesomeIcon icon={faFileAlt} size="sm" className="mr-1" />}
>
Edit Template
</DropdownMenuItem>
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionSshCertificateTemplateActions.Delete}
a={OrgPermissionSubjects.SshCertificateTemplates}
>
{(isAllowed) => (
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -55,15 +56,28 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
}
});
useEffect(() => {
if (ca) {
reset({
friendlyName: ca.friendlyName,
keyAlgorithm: ca.keyAlgorithm
});
} else {
reset({
friendlyName: "",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
});
}
}, [ca]);
const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => {
try {
if (ca) {
// update
await updateMutateAsync({
caId: ca.id
caId: ca.id,
friendlyName
});
} else {
// create
await createMutateAsync({
friendlyName,
keyAlgorithm
@@ -112,7 +126,7 @@ export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="My SSH CA" isDisabled={Boolean(ca)} />
<Input {...field} placeholder="My SSH CA" />
</FormControl>
)}
/>