Update permission logic

This commit is contained in:
Tuan Dang
2025-04-08 18:45:16 -07:00
parent 728c3f56a7
commit 20ebfcefaa
11 changed files with 109 additions and 63 deletions

View File

@@ -68,7 +68,6 @@ export enum ProjectPermissionGroupActions {
}
export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
@@ -629,7 +628,6 @@ const buildAdminPermissionRules = () => {
can(
[
ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete,

View File

@@ -337,7 +337,7 @@ export const sshHostServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission({
await permissionService.getProjectPermission({
actor,
actorId,
projectId: host.projectId,
@@ -346,13 +346,6 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.Read,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
return host;
};

View File

@@ -1068,8 +1068,7 @@ export const projectServiceFactory = ({
actor,
projectId
}: TListProjectSshHostsDTO) => {
console.log("listProjectSshHosts: ", actor, actorId, actorAuthMethod, actorOrgId, projectId);
const { permission } = await permissionService.getProjectPermission({
await permissionService.getProjectPermission({
actor,
actorId,
projectId,
@@ -1078,10 +1077,7 @@ export const projectServiceFactory = ({
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId);
return hosts;
};

View File

@@ -76,7 +76,6 @@ export enum ProjectPermissionGroupActions {
}
export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",

View File

@@ -1,2 +1,2 @@
export { useCreateSshHost, useDeleteSshHost, useUpdateSshHost } from "./mutations";
export { useGetSshHostById } from "./queries";
export { fetchSshHostUserCaPublicKey, useGetSshHostById } from "./queries";

View File

@@ -5,7 +5,8 @@ import { apiRequest } from "@app/config/request";
import { TSshHost } from "./types";
export const sshHostKeys = {
getSshHostById: (sshHostId: string) => [{ sshHostId }, "ssh-host"]
getSshHostById: (sshHostId: string) => [{ sshHostId }, "ssh-host"],
getSshHostUserCaPublicKey: (sshHostId: string) => [{ sshHostId }, "ssh-host-user-ca-public-key"]
};
export const useGetSshHostById = (sshHostId: string) => {
@@ -18,3 +19,10 @@ export const useGetSshHostById = (sshHostId: string) => {
enabled: Boolean(sshHostId)
});
};
export const fetchSshHostUserCaPublicKey = async (sshHostId: string): Promise<string> => {
const { data } = await apiRequest.get<string>(
`/api/v1/ssh/hosts/${sshHostId}/user-ca-public-key`
);
return data;
};

View File

@@ -133,7 +133,7 @@ export const ProjectLayout = () => {
</MenuItem>
)}
</Link>
<Link
{/* <Link
to={`/${ProjectType.SSH}/$projectId/certificates` as const}
params={{
projectId: currentWorkspace.id
@@ -144,8 +144,8 @@ export const ProjectLayout = () => {
Certificates
</MenuItem>
)}
</Link>
<Link
</Link> */}
{/* <Link
to={`/${ProjectType.SSH}/$projectId/cas` as const}
params={{
projectId: currentWorkspace.id
@@ -160,7 +160,7 @@ export const ProjectLayout = () => {
Certificate Authorities
</MenuItem>
)}
</Link>
</Link> */}
</>
)}
{isSecretManager && (

View File

@@ -100,7 +100,6 @@ const GroupPolicyActionSchema = z.object({
});
const SshHostPolicyActionSchema = z.object({
[ProjectPermissionSshHostActions.Read]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Create]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Edit]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Delete]: z.boolean().optional(),
@@ -559,28 +558,27 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
}
if (subject === ProjectPermissionSub.SshHosts) {
const canRead = action.includes(ProjectPermissionSshHostActions.Read);
const canEdit = action.includes(ProjectPermissionSshHostActions.Edit);
const canDelete = action.includes(ProjectPermissionSshHostActions.Delete);
const canCreate = action.includes(ProjectPermissionSshHostActions.Create);
const canIssueUserCert = action.includes(ProjectPermissionSshHostActions.IssueUserCert);
const canIssueHostCert = action.includes(ProjectPermissionSshHostActions.IssueHostCert);
if (!formVal[subject]) formVal[subject] = [];
if (!formVal[subject]) formVal[subject] = [{ conditions: [] }];
if (canRead) formVal[subject]![0][ProjectPermissionSshHostActions.Read] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionSshHostActions.Edit] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionSshHostActions.Delete] = true;
if (canCreate) formVal[subject]![0][ProjectPermissionSshHostActions.Create] = true;
if (canIssueUserCert)
formVal[subject]![0][ProjectPermissionSshHostActions.IssueUserCert] = true;
if (canIssueHostCert)
formVal[subject]![0][ProjectPermissionSshHostActions.IssueHostCert] = true;
formVal[subject]![0].conditions = conditions
? convertCaslConditionToFormOperator(conditions)
: [];
formVal[subject]![0].inverted = inverted;
formVal[subject]!.push({
[ProjectPermissionSshHostActions.Edit]: action.includes(
ProjectPermissionSshHostActions.Edit
),
[ProjectPermissionSshHostActions.Delete]: action.includes(
ProjectPermissionSshHostActions.Delete
),
[ProjectPermissionSshHostActions.Create]: action.includes(
ProjectPermissionSshHostActions.Create
),
[ProjectPermissionSshHostActions.IssueUserCert]: action.includes(
ProjectPermissionSshHostActions.IssueUserCert
),
[ProjectPermissionSshHostActions.IssueHostCert]: action.includes(
ProjectPermissionSshHostActions.IssueHostCert
),
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
}
});
@@ -910,11 +908,10 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.SshHosts]: {
title: "SSH Hosts",
actions: [
{ label: "Read", value: ProjectPermissionSshHostActions.Read },
{ label: "Create", value: ProjectPermissionSshHostActions.Create },
{ label: "Modify", value: ProjectPermissionSshHostActions.Edit },
{ label: "Remove", value: ProjectPermissionSshHostActions.Delete },
{ label: "Connect", value: ProjectPermissionSshHostActions.IssueUserCert },
{ label: "Issue User Certificate", value: ProjectPermissionSshHostActions.IssueUserCert },
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
]
},

View File

@@ -57,12 +57,12 @@ export const SshCertificatesTable = () => {
</Td>
<Td>
{certificate.notBefore
? format(new Date(certificate.notBefore), "yyyy-MM-dd")
? format(new Date(certificate.notBefore), "yyyy-MM-dd | HH:mm:ss")
: "-"}
</Td>
<Td>
{certificate.notAfter
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
? format(new Date(certificate.notAfter), "yyyy-MM-dd | HH:mm:ss")
: "-"}
</Td>
</Tr>

View File

@@ -43,17 +43,19 @@ export const SshHostsSection = () => {
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Hosts</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.SshHosts}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("sshHost")}
isDisabled={!isAllowed}
>
Add Host
</Button>
)}
{(isAllowed) =>
isAllowed && (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("sshHost")}
isDisabled={!isAllowed}
>
Add Host
</Button>
)
}
</ProjectPermissionCan>
</div>
<SshHostsTable handlePopUpOpen={handlePopUpOpen} />

View File

@@ -1,7 +1,15 @@
import { faEllipsis, faPencil, faServer, faTrash } from "@fortawesome/free-solid-svg-icons";
import {
faDownload,
faEllipsis,
faPencil,
faServer,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
@@ -20,7 +28,7 @@ import {
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceSshHosts } from "@app/hooks/api";
import { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -34,6 +42,24 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListWorkspaceSshHosts(currentWorkspace?.id || "");
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
const handleDownloadUserCaKey = async (sshHostId: string) => {
try {
const publicKey = await fetchSshHostUserCaPublicKey(sshHostId);
downloadTxtFile("infisical_user_ca.pub", publicKey);
} catch (err) {
console.error("Failed to download User CA public key", err);
createNotification({
type: "error",
text: "Failed to download User CA public key"
});
}
};
return (
<div>
<TableContainer>
@@ -41,6 +67,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
<THead>
<Tr>
<Th>Hostname</Th>
<Th>Login User - Authorized Principals Mapping</Th>
<Th />
</Tr>
</THead>
@@ -52,11 +79,28 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
data.map((host) => {
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
// className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
className="h-10"
key={`ssh-host-${host.id}`}
>
<Td>{host.hostname}</Td>
<Td className="flex justify-end">
<Td>
{host.loginMappings.length === 0 ? (
<span className="italic text-mineshaft-400">None</span>
) : (
host.loginMappings.map(({ loginUser, allowedPrincipals }) => (
<div key={`${host.id}-${loginUser}`} className="mb-2">
<div className="text-mineshaft-200">{loginUser}</div>
{allowedPrincipals.map((principal) => (
<div key={principal} className="ml-4">
└─ {principal}
</div>
))}
</div>
))
)}
</Td>
<Td className="text-right align-middle">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -66,6 +110,15 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={async (e) => {
e.stopPropagation();
handleDownloadUserCaKey(host.id);
}}
icon={<FontAwesomeIcon icon={faDownload} />}
>
Download User CA Public Key
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshHosts}