feat(pam): PAM Platform V1

This commit is contained in:
x032205
2025-09-27 01:57:43 -04:00
parent 270d8237fe
commit 4b1664c30f
141 changed files with 9327 additions and 46 deletions
@@ -0,0 +1,59 @@
import { Helmet } from "react-helmet";
import { useParams } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types";
import { useGetPamSessionById } from "@app/hooks/api/pam";
import { PamSessionDetailsSection } from "./components/PamSessionDetailsSection";
import { PamSessionLogsSection } from "./components/PamSessionLogsSection";
const Page = () => {
const sessionId = useParams({
from: ROUTE_PATHS.Pam.PamSessionByIDPage.id,
select: (el) => el.sessionId
});
const { data: session } = useGetPamSessionById(sessionId);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{session && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title={`${session.accountName} Session`}
description={`View details for this ${session.accountName} session.`}
/>
<div className="flex">
<div className="mr-4 w-96">
<PamSessionDetailsSection session={session} />
</div>
<div className="w-full">
<PamSessionLogsSection session={session} />
</div>
</div>
</div>
)}
</div>
);
};
export const PamSessionByIDPage = () => {
return (
<>
<Helmet>
<title>PAM Session</title>
</Helmet>
<ProjectPermissionCan
I={ProjectPermissionPamSessionActions.Read}
a={ProjectPermissionSub.PamSessions}
passThrough={false}
renderGuardBanner
>
<Page />
</ProjectPermissionCan>
</>
);
};
@@ -0,0 +1,125 @@
import { faBoxOpen, faCheck, faCopy, faUser } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, IconButton, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam";
import { PamSessionStatusBadge } from "../../PamSessionsPage/components/PamSessionStatusBadge";
type Props = {
session: TPamSession;
};
const DetailItem = ({ label, children }: { label: string; children: React.ReactNode }) => (
<div className="mb-4">
<p className="font-semibold">{label}</p>
{children}
</div>
);
export const PamSessionDetailsSection = ({
session: {
id,
accountName,
resourceType,
resourceName,
status,
actorName,
actorEmail,
createdAt,
endedAt,
actorIp,
actorUserAgent,
startedAt,
expiresAt
}
}: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
return (
<div className="max-w-[350px] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Session Details</h3>
</div>
<div className="pt-4 text-sm text-mineshaft-300">
<DetailItem label="Session ID">
<div className="group flex align-top">
<p className="truncate">{id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</DetailItem>
<DetailItem label="Account">
<div className="flex items-center gap-2">
<img
alt={`${details.name} logo`}
src={`/images/integrations/${details.image}`}
className="size-4"
/>
<p>{accountName}</p>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-yellow/20 text-yellow">
<FontAwesomeIcon icon={faBoxOpen} />
{resourceName}
</Badge>
</div>
</DetailItem>
<DetailItem label="Actor">
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faUser} className="-translate-y-px" />
<p>
<strong>{actorName}</strong> ({actorEmail})
</p>
</div>
</DetailItem>
<DetailItem label="Status">
<PamSessionStatusBadge status={status} />
</DetailItem>
<DetailItem label="IP Address">
<p>{actorIp}</p>
</DetailItem>
<DetailItem label="User Agent">
<p className="truncate">{actorUserAgent}</p>
</DetailItem>
<DetailItem label="Created At">
<p>{new Date(createdAt).toLocaleString()}</p>
</DetailItem>
<DetailItem label="Expires At">
<p>{expiresAt ? new Date(expiresAt).toLocaleString() : "Never"}</p>
</DetailItem>
<DetailItem label="Started At">
<p>{startedAt ? new Date(startedAt).toLocaleString() : "Not Started"}</p>
</DetailItem>
<DetailItem label="Ended At">
<p>{endedAt ? new Date(endedAt).toLocaleString() : "Ongoing"}</p>
</DetailItem>
</div>
</div>
);
};
@@ -0,0 +1,37 @@
import { faTerminal } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TPamSession } from "@app/hooks/api/pam";
type Props = {
session: TPamSession;
};
export const PamSessionLogsSection = ({ session }: Props) => {
return (
<div className="flex h-full flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Session Logs</h3>
</div>
<div className="flex grow flex-col gap-4 text-xs">
{session.commandLogs.length > 0 ? (
session.commandLogs.map((log) => (
<div key={log.timestamp} className="flex flex-col">
<div className="flex items-center gap-1.5 text-bunker-400">
<FontAwesomeIcon icon={faTerminal} className="size-3" />
<span>{new Date(log.timestamp).toLocaleString()}</span>
</div>
<div className="whitespace-pre-wrap font-mono">{log.input}</div>
<div className="whitespace-pre-wrap font-mono text-bunker-300">{log.output}</div>
</div>
))
) : (
<div className="flex w-full grow items-center justify-center text-bunker-300">
No session logs
</div>
)}
</div>
</div>
);
};
@@ -0,0 +1,26 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { PamSessionByIDPage } from "./PamSessionByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
)({
component: PamSessionByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Sessions",
link: linkOptions({
to: "/projects/pam/$projectId/sessions",
params
})
},
{
label: "Details"
}
]
};
}
});