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

View File

@@ -80,6 +80,10 @@ const PROJECT_TYPE_MENU_ITEMS = [
{
label: "Secret Scanning",
value: ProjectType.SecretScanning
},
{
label: "PAM",
value: ProjectType.PAM
}
];
@@ -193,12 +197,12 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
errorText={error?.message}
className="flex-1"
>
<div className="mt-2 grid grid-cols-5 gap-4">
<div className="mt-2 grid grid-cols-3 gap-3">
{PROJECT_TYPE_MENU_ITEMS.map((el) => (
<div
key={el.value}
className={twMerge(
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 px-2 py-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
field.value === el.value && "border-primary-400 bg-mineshaft-600 opacity-100"
)}
onClick={() => field.onChange(el.value)}

View File

@@ -8,9 +8,24 @@ export const HighlightText = ({
highlightClassName?: string;
}) => {
if (!text) return null;
const renderTextWithNewlines = (input: string, baseKeyPrefix: string = ""): React.ReactNode[] => {
if (!input) return [];
const lines = input.split("\n");
return lines.flatMap((line, index) => {
const nodes: React.ReactNode[] = [line];
if (index < lines.length - 1) {
nodes.push(<br key={`${baseKeyPrefix}-br-${line}`} />);
}
return nodes;
});
};
const searchTerm = highlight.toLowerCase().trim();
if (!searchTerm) return <span>{text}</span>;
if (!searchTerm) {
return <span>{renderTextWithNewlines(text, "full-text")}</span>;
}
const parts: React.ReactNode[] = [];
let lastIndex = 0;
@@ -20,12 +35,17 @@ export const HighlightText = ({
text.replace(regex, (match: string, offset: number) => {
if (offset > lastIndex) {
parts.push(<span key={`pre-${lastIndex}`}>{text.substring(lastIndex, offset)}</span>);
const preMatchText = text.substring(lastIndex, offset);
parts.push(
<span key={`pre-${lastIndex}`}>
{renderTextWithNewlines(preMatchText, `pre-${lastIndex}`)}
</span>
);
}
parts.push(
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
{match}
{renderTextWithNewlines(match, `match-${offset}`)}
</span>
);
@@ -35,7 +55,12 @@ export const HighlightText = ({
});
if (lastIndex < text.length) {
parts.push(<span key={`post-${lastIndex}`}>{text.substring(lastIndex)}</span>);
const postMatchText = text.substring(lastIndex);
parts.push(
<span key={`post-${lastIndex}`}>
{renderTextWithNewlines(postMatchText, `post-${lastIndex}`)}
</span>
);
}
return parts;

View File

@@ -350,6 +350,24 @@ export const ROUTE_PATHS = Object.freeze({
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings"
)
},
Pam: {
AccountsPage: setRoute(
"/projects/pam/$projectId/accounts",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts"
),
ResourcesPage: setRoute(
"/projects/pam/$projectId/resources",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources"
),
SessionsPage: setRoute(
"/projects/pam/$projectId/sessions",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/"
),
PamSessionByIDPage: setRoute(
"/projects/pam/$projectId/sessions/$sessionId",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
)
},
Public: {
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
ViewSecretRequestByIDPage: setRoute(

View File

@@ -187,6 +187,19 @@ export enum ProjectPermissionCommitsActions {
PerformRollback = "perform-rollback"
}
export enum ProjectPermissionPamAccountActions {
Access = "access",
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionPamSessionActions {
Read = "read"
// Terminate = "terminate"
}
export type IdentityManagementSubjectFields = {
identityId: string;
};
@@ -208,7 +221,8 @@ export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.SecretEvents
| ProjectPermissionSub.AppConnections;
| ProjectPermissionSub.AppConnections
| ProjectPermissionSub.PamAccounts;
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to",
@@ -289,7 +303,11 @@ export enum ProjectPermissionSub {
SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs",
SecretEvents = "secret-events",
AppConnections = "app-connections"
AppConnections = "app-connections",
PamFolders = "pam-folders",
PamResources = "pam-resources",
PamAccounts = "pam-accounts",
PamSessions = "pam-sessions"
}
export type SecretSubjectFields = {
@@ -350,6 +368,12 @@ export type PkiTemplateSubjectFields = {
// (dangtony98): consider adding [commonName] as a subject field in the future
};
export type PamAccountSubjectFields = {
resourceName: string;
accountName: string;
accountPath: string;
};
export type ProjectPermissionSet =
| [
ProjectPermissionSecretActions,
@@ -475,6 +499,16 @@ export type ProjectPermissionSet =
| ProjectPermissionSub.AppConnections
| (ForcedSubject<ProjectPermissionSub.AppConnections> & AppConnectionSubjectFields)
)
];
]
| [ProjectPermissionActions, ProjectPermissionSub.PamFolders]
| [ProjectPermissionActions, ProjectPermissionSub.PamResources]
| [
ProjectPermissionPamAccountActions,
(
| ProjectPermissionSub.PamAccounts
| (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
)
]
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@@ -82,6 +82,8 @@ export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]
return "/projects/cert-management/$projectId/subscribers" as const;
case ProjectType.SecretScanning:
return `/projects/${type}/$projectId/data-sources` as const;
case ProjectType.PAM:
return `/projects/${type}/$projectId/accounts` as const;
default:
return `/projects/${type}/$projectId/overview` as const;
}
@@ -93,7 +95,8 @@ export const getProjectTitle = (type: ProjectType) => {
[ProjectType.KMS]: "Key Management",
[ProjectType.CertificateManager]: "Cert Management",
[ProjectType.SSH]: "SSH",
[ProjectType.SecretScanning]: "Secret Scanning"
[ProjectType.SecretScanning]: "Secret Scanning",
[ProjectType.PAM]: "PAM"
};
return titleConvert[type];
};
@@ -104,7 +107,8 @@ export const getProjectLottieIcon = (type: ProjectType) => {
[ProjectType.KMS]: "unlock",
[ProjectType.CertificateManager]: "note",
[ProjectType.SSH]: "terminal",
[ProjectType.SecretScanning]: "secret-scan"
[ProjectType.SecretScanning]: "secret-scan",
[ProjectType.PAM]: "groups"
};
return titleConvert[type];
};

View File

@@ -1,3 +1,4 @@
import { ProjectType } from "../projects/types";
import { EventType, UserAgentType } from "./enums";
export const secretEvents: EventType[] = [
@@ -246,7 +247,26 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_ORG_ROLE]: "Create Org Role",
[EventType.UPDATE_ORG_ROLE]: "Update Org Role",
[EventType.DELETE_ORG_ROLE]: "Delete Org Role"
[EventType.DELETE_ORG_ROLE]: "Delete Org Role",
[EventType.PAM_SESSION_START]: "PAM Session Start",
[EventType.PAM_SESSION_LOGS_UPDATE]: "PAM Session Logs Update",
[EventType.PAM_SESSION_END]: "PAM Session End",
[EventType.PAM_SESSION_GET]: "PAM Session Get",
[EventType.PAM_SESSION_LIST]: "PAM Session List",
[EventType.PAM_FOLDER_CREATE]: "PAM Folder Create",
[EventType.PAM_FOLDER_UPDATE]: "PAM Folder Update",
[EventType.PAM_FOLDER_DELETE]: "PAM Folder Delete",
[EventType.PAM_ACCOUNT_LIST]: "PAM Account List",
[EventType.PAM_ACCOUNT_ACCESS]: "PAM Account Access",
[EventType.PAM_ACCOUNT_CREATE]: "PAM Account Create",
[EventType.PAM_ACCOUNT_UPDATE]: "PAM Account Update",
[EventType.PAM_ACCOUNT_DELETE]: "PAM Account Delete",
[EventType.PAM_RESOURCE_LIST]: "PAM Resource List",
[EventType.PAM_RESOURCE_GET]: "PAM Resource Get",
[EventType.PAM_RESOURCE_CREATE]: "PAM Resource Create",
[EventType.PAM_RESOURCE_UPDATE]: "PAM Resource Update",
[EventType.PAM_RESOURCE_DELETE]: "PAM Resource Delete"
};
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
@@ -258,3 +278,35 @@ export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
[UserAgentType.PYTHON_SDK]: "InfisicalPythonSDK",
[UserAgentType.OTHER]: "Other"
};
const sharedProjectEvents = [
EventType.ADD_PROJECT_MEMBER,
EventType.REMOVE_PROJECT_MEMBER,
EventType.CREATE_PROJECT_ROLE,
EventType.UPDATE_PROJECT_ROLE,
EventType.DELETE_PROJECT_ROLE
];
export const projectToEventsMap: Partial<Record<ProjectType, EventType[]>> = {
[ProjectType.PAM]: [
...sharedProjectEvents,
EventType.PAM_SESSION_START,
EventType.PAM_SESSION_LOGS_UPDATE,
EventType.PAM_SESSION_END,
EventType.PAM_SESSION_GET,
EventType.PAM_SESSION_LIST,
EventType.PAM_FOLDER_CREATE,
EventType.PAM_FOLDER_UPDATE,
EventType.PAM_FOLDER_DELETE,
EventType.PAM_ACCOUNT_LIST,
EventType.PAM_ACCOUNT_ACCESS,
EventType.PAM_ACCOUNT_CREATE,
EventType.PAM_ACCOUNT_UPDATE,
EventType.PAM_ACCOUNT_DELETE,
EventType.PAM_RESOURCE_LIST,
EventType.PAM_RESOURCE_GET,
EventType.PAM_RESOURCE_CREATE,
EventType.PAM_RESOURCE_UPDATE,
EventType.PAM_RESOURCE_DELETE
]
};

View File

@@ -240,5 +240,24 @@ export enum EventType {
CREATE_ORG_ROLE = "create-org-role",
UPDATE_ORG_ROLE = "update-org-role",
DELETE_ORG_ROLE = "delete-org-role"
DELETE_ORG_ROLE = "delete-org-role",
PAM_SESSION_START = "pam-session-start",
PAM_SESSION_LOGS_UPDATE = "pam-session-logs-update",
PAM_SESSION_END = "pam-session-end",
PAM_SESSION_GET = "pam-session-get",
PAM_SESSION_LIST = "pam-session-list",
PAM_FOLDER_CREATE = "pam-folder-create",
PAM_FOLDER_UPDATE = "pam-folder-update",
PAM_FOLDER_DELETE = "pam-folder-delete",
PAM_ACCOUNT_LIST = "pam-account-list",
PAM_ACCOUNT_ACCESS = "pam-account-access",
PAM_ACCOUNT_CREATE = "pam-account-create",
PAM_ACCOUNT_UPDATE = "pam-account-update",
PAM_ACCOUNT_DELETE = "pam-account-delete",
PAM_RESOURCE_LIST = "pam-resource-list",
PAM_RESOURCE_GET = "pam-resource-get",
PAM_RESOURCE_CREATE = "pam-resource-create",
PAM_RESOURCE_UPDATE = "pam-resource-update",
PAM_RESOURCE_DELETE = "pam-resource-delete"
}

View File

@@ -0,0 +1,10 @@
export enum PamResourceType {
Postgres = "postgres"
}
export enum PamSessionStatus {
Starting = "starting",
Active = "active",
Ended = "ended",
Terminated = "terminated"
}

View File

@@ -0,0 +1,5 @@
export * from "./enums";
export * from "./maps";
export * from "./mutations";
export * from "./queries";
export * from "./types";

View File

@@ -0,0 +1,8 @@
import { PamResourceType } from "./enums";
export const PAM_RESOURCE_TYPE_MAP: Record<
PamResourceType,
{ name: string; image: string; size?: number }
> = {
[PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" }
};

View File

@@ -0,0 +1,174 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { pamKeys } from "./queries";
import {
TCreatePamAccountDTO,
TCreatePamFolderDTO,
TCreatePamResourceDTO,
TDeletePamAccountDTO,
TDeletePamFolderDTO,
TDeletePamResourceDTO,
TPamAccount,
TPamFolder,
TPamResource,
TUpdatePamAccountDTO,
TUpdatePamFolderDTO,
TUpdatePamResourceDTO
} from "./types";
// Resources
export const useCreatePamResource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceType, ...params }: TCreatePamResourceDTO) => {
const { data } = await apiRequest.post<{ resource: TPamResource }>(
`/api/v1/pam/resources/${resourceType}`,
params
);
return data.resource;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) });
}
});
};
export const useUpdatePamResource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceId, resourceType, ...params }: TUpdatePamResourceDTO) => {
const { data } = await apiRequest.patch<{ resource: TPamResource }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}`,
params
);
return data.resource;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) });
}
});
};
export const useDeletePamResource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceId, resourceType }: TDeletePamResourceDTO) => {
const { data } = await apiRequest.delete<{ resource: TPamResource }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}`
);
return data.resource;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listResources(projectId) });
}
});
};
// Accounts
export const useCreatePamAccount = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceId, resourceType, ...params }: TCreatePamAccountDTO) => {
const { data } = await apiRequest.post<{ account: TPamAccount }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts`,
params
);
return data.account;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
}
});
};
export const useUpdatePamAccount = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
resourceId,
resourceType,
accountId,
...params
}: TUpdatePamAccountDTO) => {
const { data } = await apiRequest.patch<{ account: TPamAccount }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`,
params
);
return data.account;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
}
});
};
export const useDeletePamAccount = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceId, resourceType, accountId }: TDeletePamAccountDTO) => {
const { data } = await apiRequest.delete<{ account: TPamAccount }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`
);
return data.account;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
}
});
};
// Folders
export const useCreatePamFolder = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (params: TCreatePamFolderDTO) => {
const { data } = await apiRequest.post<{ folder: TPamFolder }>("/api/v1/pam/folders", params);
return data.folder;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
}
});
};
export const useUpdatePamFolder = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ folderId, ...params }: TUpdatePamFolderDTO) => {
const { data } = await apiRequest.patch<{ folder: TPamFolder }>(
`/api/v1/pam/folders/${folderId}`,
params
);
return data.folder;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
}
});
};
export const useDeletePamFolder = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ folderId }: TDeletePamFolderDTO) => {
const { data } = await apiRequest.delete<{ folder: TPamFolder }>(
`/api/v1/pam/folders/${folderId}`
);
return data.folder;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: pamKeys.listAccounts(projectId) });
}
});
};

View File

@@ -0,0 +1,138 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TPamResourceOption } from "./types/resource-options";
import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types";
export const pamKeys = {
all: ["pam"] as const,
resource: () => [...pamKeys.all, "resource"] as const,
account: () => [...pamKeys.all, "account"] as const,
session: () => [...pamKeys.all, "session"] as const,
listResourceOptions: () => [...pamKeys.resource(), "options"] as const,
listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId],
listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId],
getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId],
listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId]
};
// Resources
export const useListPamResourceOptions = (
options?: Omit<
UseQueryOptions<
TPamResourceOption[],
unknown,
TPamResourceOption[],
ReturnType<typeof pamKeys.listResourceOptions>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: pamKeys.listResourceOptions(),
queryFn: async () => {
const { data } = await apiRequest.get<{ resourceOptions: TPamResourceOption[] }>(
"/api/v1/pam/resources/options"
);
return data.resourceOptions;
},
...options
});
};
export const useListPamResources = (
projectId: string,
options?: Omit<
UseQueryOptions<
TPamResource[],
unknown,
TPamResource[],
ReturnType<typeof pamKeys.listResources>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: pamKeys.listResources(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<{ resources: TPamResource[] }>(
"/api/v1/pam/resources",
{ params: { projectId } }
);
return data.resources;
},
...options
});
};
// Accounts
export const useListPamAccounts = (
projectId: string,
options?: Omit<
UseQueryOptions<
{ accounts: TPamAccount[]; folders: TPamFolder[] },
unknown,
{ accounts: TPamAccount[]; folders: TPamFolder[] },
ReturnType<typeof pamKeys.listAccounts>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: pamKeys.listAccounts(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<{ accounts: TPamAccount[]; folders: TPamFolder[] }>(
"/api/v1/pam/accounts",
{ params: { projectId } }
);
return data;
},
...options
});
};
// Sessions
export const useGetPamSessionById = (
sessionId: string,
options?: Omit<
UseQueryOptions<TPamSession, unknown, TPamSession, ReturnType<typeof pamKeys.getSession>>,
"queryKey" | "queryFn" | "enabled"
>
) => {
return useQuery({
queryKey: pamKeys.getSession(sessionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ session: TPamSession }>(
`/api/v1/pam/sessions/${sessionId}`
);
return data.session;
},
enabled: !!sessionId,
...options
});
};
export const useListPamSessions = (
projectId: string,
options?: Omit<
UseQueryOptions<TPamSession[], unknown, TPamSession[], ReturnType<typeof pamKeys.listSessions>>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: pamKeys.listSessions(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<{ sessions: TPamSession[] }>("/api/v1/pam/sessions", {
params: { projectId }
});
return data.sessions;
},
...options
});
};

View File

@@ -0,0 +1,17 @@
import { PamResourceType } from "../enums";
export interface TBasePamAccount {
id: string;
projectId: string;
folderId?: string | null;
resourceId: string;
resource: {
id: string;
name: string;
resourceType: PamResourceType;
};
name: string;
description?: string | null;
createdAt: string;
updatedAt: string;
}

View File

@@ -0,0 +1,8 @@
export interface TBasePamResource {
id: string;
projectId: string;
name: string;
gatewayId: string;
createdAt: string;
updatedAt: string;
}

View File

@@ -0,0 +1,97 @@
import { PamResourceType, PamSessionStatus } from "../enums";
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
export * from "./postgres-resource";
export type TPamResource = TPostgresResource;
export type TPamAccount = TPostgresAccount;
export type TPamFolder = {
id: string;
projectId: string;
parentId?: string | null;
name: string;
description?: string | null;
createdAt: string;
updatedAt: string;
};
export type TPamSession = {
id: string;
projectId: string;
accountId?: string | null;
resourceType: PamResourceType;
resourceName: string;
accountName: string;
userId?: string | null;
actorName: string;
actorEmail: string;
actorIp: string;
actorUserAgent: string;
status: PamSessionStatus;
expiresAt?: string | null;
startedAt?: string | null;
endedAt?: string | null;
createdAt: string;
updatedAt: string;
commandLogs: {
input: string;
output: string;
timestamp: string;
}[];
};
// Resource DTOs
export type TCreatePamResourceDTO = Pick<
TPamResource,
"name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId"
>;
export type TUpdatePamResourceDTO = Partial<
Pick<TPamResource, "name" | "connectionDetails" | "gatewayId">
> & {
resourceId: string;
resourceType: PamResourceType;
};
export type TDeletePamResourceDTO = {
resourceId: string;
resourceType: PamResourceType;
};
// Account DTOs
export type TCreatePamAccountDTO = Pick<
TPamAccount,
"name" | "description" | "credentials" | "projectId" | "resourceId" | "folderId"
> & {
resourceType: PamResourceType;
};
export type TUpdatePamAccountDTO = Partial<
Pick<TPamAccount, "name" | "description" | "credentials">
> & {
accountId: string;
resourceId: string;
resourceType: PamResourceType;
};
export type TDeletePamAccountDTO = {
accountId: string;
resourceId: string;
resourceType: PamResourceType;
};
// Folder DTOs
export type TCreatePamFolderDTO = Pick<
TPamFolder,
"name" | "description" | "parentId" | "projectId"
>;
export type TUpdatePamFolderDTO = Partial<Pick<TPamFolder, "name" | "description">> & {
folderId: string;
};
export type TDeletePamFolderDTO = {
folderId: string;
};

View File

@@ -0,0 +1,14 @@
import { PamResourceType } from "../enums";
import { TBaseSqlConnectionDetails, TBaseSqlCredentials } from "./shared/sql-resource";
import { TBasePamAccount } from "./base-account";
import { TBasePamResource } from "./base-resource";
// Resources
export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & {
connectionDetails: TBaseSqlConnectionDetails;
};
// Accounts
export type TPostgresAccount = TBasePamAccount & {
credentials: TBaseSqlCredentials;
};

View File

@@ -0,0 +1,11 @@
import { PamResourceType } from "../enums";
export type TPamResourceOptionBase = {
name: string;
};
export type TPostgresResourceOption = TPamResourceOptionBase & {
resource: PamResourceType.Postgres;
};
export type TPamResourceOption = TPostgresResourceOption;

View File

@@ -0,0 +1,12 @@
export type TBaseSqlConnectionDetails = {
host: string;
port: number;
database: string;
sslEnabled: boolean;
sslRejectUnauthorized: boolean;
};
export type TBaseSqlCredentials = {
username: string;
password: string;
};

View File

@@ -13,7 +13,8 @@ export enum ProjectType {
CertificateManager = "cert-manager",
KMS = "kms",
SSH = "ssh",
SecretScanning = "secret-scanning"
SecretScanning = "secret-scanning",
PAM = "pam"
}
export enum ProjectUserMembershipTemporaryMode {

View File

@@ -58,4 +58,5 @@ export type SubscriptionPlan = {
cardDeclined?: boolean;
cardDeclinedReason?: string;
machineIdentityAuthTemplates: boolean;
pam: boolean;
};

View File

@@ -0,0 +1,195 @@
import {
faBook,
faBoxOpen,
faCog,
faDisplay,
faHome,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { useProject, useProjectPermission, useSubscription } from "@app/context";
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
import { useEffect } from "react";
import { usePopUp } from "@app/hooks";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
export const PamLayout = () => {
const { currentProject } = useProject();
const { subscription } = useSubscription();
const { assumedPrivilegeDetails } = useProjectPermission();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
useEffect(() => {
if (subscription && !subscription.pam) {
handlePopUpOpen("upgradePlan");
}
}, [subscription]);
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="groups" />
PAM
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/pam/$projectId/accounts"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUser} />
</div>
Accounts
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/resources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBoxOpen} />
</div>
Resources
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDisplay} />
</div>
Sessions
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/pam/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Access Management
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link>
</Menu>
</div>
</nav>
</motion.div>
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
</div>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use PAM if you switch to a paid Infisical plan."
/>
</>
);
};

View File

@@ -0,0 +1 @@
export { PamLayout } from "./PamLayout";

View File

@@ -164,7 +164,10 @@ export const PkiSyncRow = ({
</div>
</Td>
{subscriberId ? (
<PkiSyncTableCell primaryText={pkiSync.subscriber?.name || subscriberId} secondaryText="PKI Subscriber" />
<PkiSyncTableCell
primaryText={pkiSync.subscriber?.name || subscriberId}
secondaryText="PKI Subscriber"
/>
) : (
<Td>
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">

View File

@@ -24,12 +24,13 @@ import { useOrganization } from "@app/context";
import { useGetUserProjects } from "@app/hooks/api";
import {
eventToNameMap,
projectToEventsMap,
secretEvents,
userAgentTypeToNameMap
} from "@app/hooks/api/auditLogs/constants";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { UserAgentType } from "@app/hooks/api/auth/types";
import { Project } from "@app/hooks/api/projects/types";
import { Project, ProjectType } from "@app/hooks/api/projects/types";
import { LogFilterItem } from "./LogFilterItem";
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
@@ -94,10 +95,20 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
const selectedProject = project ?? watch("project");
const currentSelectedEventTypes = selectedEventTypes ?? [];
const hasSecretEventFilter = currentSelectedEventTypes.some((eventType) =>
secretEvents.includes(eventType)
);
const showSecretsSection =
selectedEventTypes?.some(
(eventType) => secretEvents.includes(eventType) && eventType !== EventType.GET_SECRETS
) || selectedEventTypes?.length === 0;
project?.type !== ProjectType.PAM &&
(hasSecretEventFilter || currentSelectedEventTypes.length === 0);
const filteredEventTypes = useMemo(() => {
const projectEvents = project?.type ? projectToEventsMap[project.type] : undefined;
if (!projectEvents) return eventTypes;
return eventTypes.filter((v) => projectEvents.includes(v.value as EventType));
}, [project]);
const availableEnvironments = useMemo(() => {
if (!selectedProject) return [];
@@ -166,7 +177,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<DropdownMenuTrigger asChild>
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{selectedEventTypes?.length === 1
? eventTypes.find(
? filteredEventTypes.find(
(eventType) => eventType.value === selectedEventTypes[0]
)?.label
: selectedEventTypes?.length === 0
@@ -181,8 +192,8 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
>
<div className="max-h-80 overflow-y-auto">
{eventTypes && eventTypes.length > 0 ? (
eventTypes.map((eventType) => {
{filteredEventTypes.length > 0 ? (
filteredEventTypes.map((eventType) => {
const isSelected = selectedEventTypes?.includes(
eventType.value as EventType
);
@@ -190,7 +201,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
return (
<DropdownMenuItem
onSelect={(event) =>
eventTypes.length > 1 && event.preventDefault()
filteredEventTypes.length > 1 && event.preventDefault()
}
onClick={() => {
if (

View File

@@ -63,6 +63,10 @@ const PROJECT_TYPE_MENU_ITEMS = [
{
label: "Secret Scanning",
value: ProjectType.SecretScanning
},
{
label: "PAM",
value: ProjectType.PAM
}
];
@@ -131,12 +135,12 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
errorText={error?.message}
className="flex-1"
>
<div className="mt-2 grid grid-cols-5 gap-4">
<div className="mt-2 grid grid-cols-3 gap-3">
{PROJECT_TYPE_MENU_ITEMS.map((el) => (
<div
key={el.value}
className={twMerge(
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
"flex cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 px-2 py-4 opacity-75 transition-all hover:border-primary-400 hover:bg-mineshaft-600",
field.value === el.value && "border-primary-400 bg-mineshaft-600 opacity-100"
)}
onClick={() => field.onChange(el.value)}

View File

@@ -0,0 +1,34 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
import { PamAccountsSection } from "./components/PamAccountsSection";
export const PamAccountsPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "PAM" })}</title>
</Helmet>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionPamAccountActions.Read}
a={ProjectPermissionSub.PamAccounts}
>
<div className="h-full bg-bunker-800">
<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">
<PageHeader title="Accounts" description="View, access, and manage accounts." />
<PamAccountsSection />
</div>
</div>
</div>
</ProjectPermissionCan>
</>
);
};

View File

@@ -0,0 +1,42 @@
import { Button } from "@app/components/v2";
export enum AccountView {
Flat = "flat",
Nested = "nested"
}
type Props = {
value: AccountView;
onChange: (value: AccountView) => void;
};
export const AccountViewToggle = ({ value, onChange }: Props) => {
return (
<div className="flex gap-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Button
variant="outline_bg"
onClick={() => {
onChange(AccountView.Flat);
}}
size="xs"
className={`${
value === AccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
>
Hide Folders
</Button>
<Button
variant="outline_bg"
onClick={() => {
onChange(AccountView.Nested);
}}
size="xs"
className={`${
value === AccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
>
Show Folders
</Button>
</div>
);
};

View File

@@ -0,0 +1,89 @@
import { useMemo, useState } from "react";
import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
import { createNotification } from "@app/components/notifications";
import { Button, FormLabel, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { TPamAccount } from "@app/hooks/api/pam";
type Props = {
account?: TPamAccount;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
const [duration, setDuration] = useState("4h");
const isDurationValid = useMemo(() => ms(duration || "1s") > 0, [duration]);
const command = useMemo(
() =>
account
? `infisical pam access ${account.id}${duration ? ` --duration ${duration}` : ""}`
: "",
[account, duration]
);
if (!account) return null;
const copyCommand = () => {
navigator.clipboard.writeText(command);
createNotification({
text: "Command copied to clipboard",
type: "info"
});
onOpenChange(false);
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Access Account"
subTitle={`Access ${account.name} using a CLI command.`}
>
<FormLabel
label="Duration"
tooltipText="The maximum duration of your session. Ex: 1h, 3w, 30d"
/>
<Input
value={duration}
onChange={(e) => setDuration(e.target.value)}
placeholder="permanent"
isError={!isDurationValid}
/>
<FormLabel label="CLI Command" className="mt-4" />
<Input value={command} isDisabled />
<a
href="https://infisical.com/docs/cli/overview"
target="_blank"
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
rel="noreferrer"
>
<span>Install the Infisical CLI</span>
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
</a>
<div className="mt-6 flex items-center">
<Button
isDisabled={!isDurationValid}
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={copyCommand}
>
Copy Command
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,49 @@
import { Controller, useFormContext } from "react-hook-form";
import { z } from "zod";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { slugSchema } from "@app/lib/schemas";
export const genericAccountFieldsSchema = z.object({
name: slugSchema({ min: 1, max: 64, field: "Name" }),
description: z.string().max(512).nullable().optional()
});
export const GenericAccountFields = () => {
const {
formState: { errors },
control
} = useFormContext<{ name: string; description: string }>();
return (
<>
<Controller
name="name"
control={control}
render={({ field }) => (
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="my-account" {...field} />
</FormControl>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<FormControl
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Description"
>
<TextArea {...field} />
</FormControl>
)}
/>
</>
);
};

View File

@@ -0,0 +1,147 @@
import { createNotification } from "@app/components/notifications";
import {
PamResourceType,
TPamAccount,
useCreatePamAccount,
useUpdatePamAccount
} from "@app/hooks/api/pam";
import { DiscriminativePick } from "@app/types";
import { PamAccountHeader } from "../PamAccountHeader";
import { PostgresAccountForm } from "./PostgresAccountForm";
type FormProps = {
onComplete: (account: TPamAccount) => void;
};
type CreateFormProps = FormProps & {
projectId: string;
resourceId: string;
resourceType: PamResourceType;
folderId?: string;
};
type UpdateFormProps = FormProps & {
account: TPamAccount;
};
const CreateForm = ({
onComplete,
projectId,
resourceId,
resourceType,
folderId
}: CreateFormProps) => {
const createPamAccount = useCreatePamAccount();
console.log({ folderId });
const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
) => {
try {
const account = await createPamAccount.mutateAsync({
...formData,
folderId,
resourceId,
resourceType,
projectId
});
createNotification({
text: "Successfully created account",
type: "success"
});
onComplete(account);
} catch (err: any) {
console.error(err);
createNotification({
title: "Failed to create account",
text: err.message,
type: "error"
});
}
};
switch (resourceType) {
case PamResourceType.Postgres:
return <PostgresAccountForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${resourceType}`);
}
};
const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
const updatePamAccount = useUpdatePamAccount();
const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
) => {
try {
const updatedAccount = await updatePamAccount.mutateAsync({
accountId: account.id,
resourceId: account.resourceId,
resourceType: account.resource.resourceType,
...formData
});
createNotification({
text: "Successfully updated account",
type: "success"
});
onComplete(updatedAccount);
} catch (err: any) {
console.error(err);
createNotification({
title: "Failed to update account",
text: err.message,
type: "error"
});
}
};
switch (account.resource.resourceType) {
case PamResourceType.Postgres:
return <PostgresAccountForm account={account} onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
}
};
type Props = {
onBack?: () => void;
projectId: string;
} & FormProps &
(
| {
account: TPamAccount;
resourceId?: undefined;
resourceName?: undefined;
resourceType?: undefined;
folderId?: undefined;
}
| {
account?: undefined;
resourceId: string;
resourceName: string;
resourceType: PamResourceType;
folderId?: string;
}
);
export const PamAccountForm = ({ onBack, projectId, ...props }: Props) => {
const { account, resourceName, resourceType } = props;
return (
<div>
<PamAccountHeader
resourceName={account ? account.resource.name : resourceName}
resourceType={account ? account.resource.resourceType : resourceType}
onBack={onBack}
/>
{account ? (
<UpdateForm {...props} account={account} />
) : (
<CreateForm {...props} projectId={projectId} />
)}
</div>
);
};

View File

@@ -0,0 +1,73 @@
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { TPostgresAccount } from "@app/hooks/api/pam";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
type Props = {
account?: TPostgresAccount;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericAccountFieldsSchema.extend({
credentials: BaseSqlAccountSchema
});
type FormData = z.infer<typeof formSchema>;
export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
const isUpdate = Boolean(account);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: account
? {
...account,
credentials: {
...account.credentials,
password: "******"
}
}
: undefined
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Account" : "Create Account"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,55 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2";
export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control } = useFormContext();
return (
<div className="flex gap-2">
<Controller
name="credentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
>
<Input
{...field}
type="password"
onFocus={(e) => {
if (isUpdate && field.value === "******") {
field.onChange("");
}
e.target.type = "text";
}}
onBlur={(e) => {
if (isUpdate && field.value === "") {
field.onChange("******");
}
e.target.type = "password";
}}
/>
</FormControl>
)}
/>
</div>
);
};

View File

@@ -0,0 +1,6 @@
import { z } from "zod";
export const BaseSqlAccountSchema = z.object({
username: z.string().trim().min(1, "Username required"),
password: z.string().trim().min(1, "Password required")
});

View File

@@ -0,0 +1,34 @@
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
type Props = {
resourceName: string;
resourceType: PamResourceType;
onBack?: () => void;
};
export const PamAccountHeader = ({ resourceName, resourceType, onBack }: Props) => {
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
return (
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
<img
alt={`${details.name} logo`}
src={`/images/integrations/${details.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">{resourceName}</div>
<p className="text-sm leading-4 text-mineshaft-400">{details.name} resource</p>
</div>
{onBack && (
<button
type="button"
className="ml-auto mt-1 text-xs text-mineshaft-400 underline underline-offset-2 hover:text-mineshaft-300"
onClick={onBack}
>
Select another resource
</button>
)}
</div>
);
};

View File

@@ -0,0 +1,159 @@
import { useCallback } from "react";
import {
faBoxOpen,
faCheck,
faCopy,
faEdit,
faEllipsisV,
faRightToBracket,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { PAM_RESOURCE_TYPE_MAP, TPamAccount } from "@app/hooks/api/pam";
type Props = {
account: TPamAccount;
onAccess: (resource: TPamAccount) => void;
onUpdate: (resource: TPamAccount) => void;
onDelete: (resource: TPamAccount) => void;
search: string;
};
export const PamAccountRow = ({ account, search, onAccess, onUpdate, onDelete }: Props) => {
const { id, name } = account;
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[account.resource.resourceType];
const [isIdCopied, setIsIdCopied] = useToggle(false);
const handleCopyId = useCallback(
(idToCopy: string) => {
setIsIdCopied.on();
navigator.clipboard.writeText(idToCopy);
createNotification({
text: "Account ID copied to clipboard",
type: "info"
});
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
},
[isIdCopied]
);
return (
<Tr className={twMerge("group h-10")} key={`account-${id}`}>
<Td>
<div className="flex items-center gap-4">
<div className="relative">
<img alt={resourceTypeName} src={`/images/integrations/${image}`} className="size-5" />
</div>
<div className="flex items-center gap-3">
<span>
<HighlightText text={name} highlight={search} />
</span>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-bunker-300/20 text-bunker-300">
<FontAwesomeIcon icon={faBoxOpen} />
<span>
<HighlightText text={account.resource.name} highlight={search} />
</span>
</Badge>
</div>
</div>
</Td>
<Td>
<div className="flex items-center gap-2">
<ProjectPermissionCan
I={ProjectPermissionPamAccountActions.Access}
a={ProjectPermissionSub.PamAccounts}
>
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faRightToBracket} />}
onClick={() => onAccess(account)}
size="xs"
>
Access
</Button>
</ProjectPermissionCan>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
handleCopyId(id);
}}
>
Copy Account ID
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionPamAccountActions.Edit}
a={ProjectPermissionSub.PamAccounts}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onUpdate(account)}
>
Edit Account
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPamAccountActions.Delete}
a={ProjectPermissionSub.PamAccounts}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(account)}
>
Delete Account
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</div>
</Td>
</Tr>
);
};

View File

@@ -0,0 +1,23 @@
import { ContentLoader } from "@app/components/v2";
import { useProject } from "@app/context";
import { useListPamAccounts } from "@app/hooks/api/pam";
import { PamAccountsTable } from "./PamAccountsTable";
export const PamAccountsSection = () => {
const { currentProject } = useProject();
const { data, isPending } = useListPamAccounts(currentProject.id, {
refetchInterval: 30000
});
if (isPending) return <ContentLoader />;
return (
<PamAccountsTable
projectId={currentProject.id}
accounts={data?.accounts || []}
folders={data?.folders || []}
/>
);
};

View File

@@ -0,0 +1,501 @@
import { useMemo, useState } from "react";
import { faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import {
faAngleDown,
faArrowDown,
faArrowUp,
faCheckCircle,
faFilter,
faFolderPlus,
faMagnifyingGlass,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
ProjectPermissionPamAccountActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { PAM_RESOURCE_TYPE_MAP, TPamAccount, TPamFolder } from "@app/hooks/api/pam";
import { AccountView, AccountViewToggle } from "./AccountViewToggle";
import { PamAccessAccountModal } from "./PamAccessAccountModal";
import { PamAccountRow } from "./PamAccountRow";
import { PamAddAccountModal } from "./PamAddAccountModal";
import { PamAddFolderModal } from "./PamAddFolderModal";
import { PamDeleteAccountModal } from "./PamDeleteAccountModal";
import { PamDeleteFolderModal } from "./PamDeleteFolderModal";
import { PamFolderRow } from "./PamFolderRow";
import { PamUpdateAccountModal } from "./PamUpdateAccountModal";
import { PamUpdateFolderModal } from "./PamUpdateFolderModal";
import { useSubscription } from "@app/context";
enum OrderBy {
Name = "name"
}
type Filters = {
resource: string[];
};
type Props = {
accounts: TPamAccount[];
folders: TPamFolder[];
projectId: string;
};
export const PamAccountsTable = ({ accounts, folders, projectId }: Props) => {
const { subscription } = useSubscription();
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.AccountsPage.path });
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"misc",
"addFolder",
"updateFolder",
"deleteFolder",
"addAccount",
"accessAccount",
"updateAccount",
"deleteAccount"
] as const);
const {
search: initSearch,
accountPath,
accountView: initAccountView
} = useSearch({
from: ROUTE_PATHS.Pam.AccountsPage.id
});
const [accountView, setAccountView] = useState<AccountView>(initAccountView ?? AccountView.Flat);
const [filters, setFilters] = useState<Filters>({
resource: []
});
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy
} = usePagination<OrderBy>(OrderBy.Name, { initPerPage: 20, initSearch });
const { foldersByParentId, pathMap } = useMemo(() => {
const foldersById: Record<string, TPamFolder> = {};
const tempFoldersByParentId: Record<string, TPamFolder[]> = { null: [] };
const tempPathMap: Record<string, string> = { "/": "null" };
folders.forEach((folder) => {
foldersById[folder.id] = folder;
if (!tempFoldersByParentId[folder.parentId || "null"]) {
tempFoldersByParentId[folder.parentId || "null"] = [];
}
tempFoldersByParentId[folder.parentId || "null"].push(folder);
});
const buildPaths = (parentId: string | null, currentPath: string) => {
(tempFoldersByParentId[parentId || "null"] || []).forEach((folder) => {
const newPath = `${currentPath}${folder.name}/`;
tempPathMap[newPath] = folder.id;
buildPaths(folder.id, newPath);
});
};
buildPaths(null, "/");
return { foldersByParentId: tempFoldersByParentId, pathMap: tempPathMap };
}, [folders]);
const effectiveFolderIdForFiltering = useMemo(() => {
if (accountView === AccountView.Flat) {
return null;
}
const folderId = pathMap[accountPath];
return folderId === "null" ? null : folderId || null;
}, [accountView, accountPath, pathMap]);
const foldersToRender = useMemo(() => {
if (accountView === AccountView.Flat) {
return [];
}
return (foldersByParentId[effectiveFolderIdForFiltering || "null"] || []).filter((folder) =>
folder.name.toLowerCase().includes(search.trim().toLowerCase())
);
}, [accountView, effectiveFolderIdForFiltering, foldersByParentId, search]);
const accountsToProcess = useMemo(() => {
if (accountView === AccountView.Flat) {
return accounts;
}
return accounts.filter(
(acc) => (acc.folderId || "null") === (effectiveFolderIdForFiltering || "null")
);
}, [accountView, accounts, effectiveFolderIdForFiltering]);
const filteredAccounts = useMemo(
() =>
accountsToProcess
.filter((account) => {
const {
name,
description,
resource: { name: resourceName, id: resourceId }
} = account;
if (filters.resource.length && !filters.resource.includes(resourceId)) {
return false;
}
const searchValue = search.trim().toLowerCase();
return (
name.toLowerCase().includes(searchValue) ||
resourceName.toLowerCase().includes(searchValue) ||
(description || "").toLowerCase().includes(searchValue)
);
})
.sort((a, b) => {
const [accOne, accTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case OrderBy.Name:
default:
return accOne.name.toLowerCase().localeCompare(accTwo.name.toLowerCase());
}
}),
[accountsToProcess, orderDirection, search, orderBy, filters]
);
useResetPageHelper({
totalCount: filteredAccounts.length,
offset,
setPage
});
const currentPageData = useMemo(
() => filteredAccounts.slice(offset, perPage * page),
[filteredAccounts, offset, perPage, page]
);
const handleSort = (column: OrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: OrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.resource.length);
const handleFolderClick = (folder: TPamFolder) => {
if (accountView === AccountView.Flat) {
return;
}
const newPath = `${accountPath}${folder.name}/`;
navigate({ search: (prev) => ({ ...prev, accountPath: newPath }) });
};
const isContentEmpty = !filteredAccounts.length && !foldersToRender.length;
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
const uniqueResources = useMemo(() => {
const resourceMap = new Map<string, TPamAccount["resource"]>();
accounts.forEach((account) => {
resourceMap.set(account.resource.id, account.resource);
});
return Array.from(resourceMap.values());
}, [accounts]);
return (
<div>
<div className="mt-4 flex gap-2">
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
{(isAllowed) =>
isAllowed && (
<AccountViewToggle
value={accountView}
onChange={(e) => {
setAccountView(e);
navigate({
search: (prev) => ({
...prev,
accountView: e === AccountView.Flat ? undefined : e,
accountPath: e === AccountView.Flat ? "/" : prev.accountPath
})
});
}}
/>
)
}
</ProjectPermissionCan>
<Input
value={search}
onChange={(e) => {
const newSearch = e.target.value;
setSearch(newSearch);
navigate({
search: (prev) => ({ ...prev, search: newSearch || undefined }),
replace: true
});
}}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search accounts..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter accounts"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 min-w-10 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Resource</DropdownMenuLabel>
{uniqueResources.length ? (
uniqueResources.map((resource) => {
const { name, image } = PAM_RESOURCE_TYPE_MAP[resource.resourceType];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
const newResources = filters.resource.includes(resource.id)
? filters.resource.filter((a) => a !== resource.id)
: [...filters.resource, resource.id];
setFilters((prev) => ({
...prev,
resource: newResources
}));
}}
key={resource.id}
icon={
filters.resource.includes(resource.id) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${name} resource`}
src={`/images/integrations/${image}`}
className="h-4 w-4"
/>
<span>{resource.name}</span>
</div>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No Account Resources</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionPamAccountActions.Create}
a={ProjectPermissionSub.PamAccounts}
>
{(isAllowedToCreateAccounts) => (
<div className="flex">
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addAccount")}
isDisabled={!isAllowedToCreateAccounts || !subscription.pam}
className={`h-10 transition-colors ${accountView === AccountView.Flat ? "" : "rounded-r-none"}`}
>
Add Account
</Button>
{accountView !== AccountView.Flat && (
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
>
<DropdownMenuTrigger asChild>
<IconButton
variant="outline_bg"
ariaLabel="add-folder-or-import"
className="rounded-l-none bg-mineshaft-600 p-3"
>
<FontAwesomeIcon icon={faAngleDown} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={5}>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.PamFolders}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} className="pr-2" />}
onClick={() => {
handlePopUpOpen("addFolder");
handlePopUpClose("misc");
}}
isDisabled={!isAllowed}
variant="outline_bg"
className="h-10 text-left"
isFullWidth
>
Add Folder
</Button>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
)}
</ProjectPermissionCan>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>
<div className="flex items-center">
Accounts
<IconButton
variant="plain"
className={getClassName(OrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(OrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{accountView !== AccountView.Flat &&
foldersToRender.map((folder) => (
<PamFolderRow
key={folder.id}
folder={folder}
search={search}
onClick={() => handleFolderClick(folder)}
onUpdate={(e) => handlePopUpOpen("updateFolder", e)}
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
/>
))}
{currentPageData.map((account) => (
<PamAccountRow
key={account.id}
account={account}
search={search}
onAccess={(e) => {
handlePopUpOpen("accessAccount", e);
}}
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
/>
))}
</TBody>
</Table>
{Boolean(filteredAccounts.length) && (
<Pagination
count={filteredAccounts.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{isContentEmpty && (
<EmptyState
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
icon={isSearchEmpty ? faSearch : faCircleXmark}
/>
)}
</TableContainer>
<PamDeleteFolderModal
isOpen={popUp.deleteFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
folder={popUp.deleteFolder.data}
/>
<PamUpdateFolderModal
isOpen={popUp.updateFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("updateFolder", isOpen)}
folder={popUp.updateFolder.data}
/>
<PamAddFolderModal
isOpen={popUp.addFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
projectId={projectId}
currentFolderId={effectiveFolderIdForFiltering}
/>
<PamAccessAccountModal
isOpen={popUp.accessAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("accessAccount", isOpen)}
account={popUp.accessAccount.data}
/>
<PamDeleteAccountModal
isOpen={popUp.deleteAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}
account={popUp.deleteAccount.data}
/>
<PamUpdateAccountModal
isOpen={popUp.updateAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("updateAccount", isOpen)}
account={popUp.updateAccount.data}
/>
<PamAddAccountModal
isOpen={popUp.addAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addAccount", isOpen)}
projectId={projectId}
currentFolderId={effectiveFolderIdForFiltering}
/>
</div>
);
};

View File

@@ -0,0 +1,73 @@
import { useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { PamResourceType, TPamAccount } from "@app/hooks/api/pam";
import { PamAccountForm } from "./PamAccountForm/PamAccountForm";
import { ResourceSelect } from "./ResourceSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
projectId: string;
onComplete?: (account: TPamAccount) => void;
currentFolderId: string | null;
};
type ContentProps = {
onComplete: (account: TPamAccount) => void;
projectId: string;
currentFolderId: string | null;
};
const Content = ({ onComplete, projectId, currentFolderId }: ContentProps) => {
const [selectedResource, setSelectedResource] = useState<{
id: string;
name: string;
resourceType: PamResourceType;
} | null>(null);
if (selectedResource) {
return (
<PamAccountForm
onComplete={onComplete}
onBack={() => setSelectedResource(null)}
resourceId={selectedResource.id}
resourceName={selectedResource.name}
resourceType={selectedResource.resourceType}
projectId={projectId}
folderId={currentFolderId ?? undefined}
/>
);
}
return <ResourceSelect projectId={projectId} onSubmit={(e) => setSelectedResource(e.resource)} />;
};
export const PamAddAccountModal = ({
isOpen,
onOpenChange,
projectId,
onComplete,
currentFolderId
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Add Account"
subTitle="Select a resource to add an account under."
bodyClassName="overflow-visible"
>
<Content
projectId={projectId}
onComplete={(account) => {
if (onComplete) onComplete(account);
onOpenChange(false);
}}
currentFolderId={currentFolderId}
/>
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,48 @@
import { createNotification } from "@app/components/notifications";
import { Modal, ModalContent } from "@app/components/v2";
import { TPamFolder, useCreatePamFolder } from "@app/hooks/api/pam";
import { PamFolderForm } from "./PamFolderForm";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
projectId: string;
currentFolderId: string | null;
};
export const PamAddFolderModal = ({ isOpen, onOpenChange, projectId, currentFolderId }: Props) => {
const createPamFolder = useCreatePamFolder();
console.log({ currentFolderId });
const onSubmit = async (formData: Pick<TPamFolder, "name" | "description">) => {
try {
await createPamFolder.mutateAsync({
...formData,
parentId: currentFolderId,
projectId
});
createNotification({
text: "Successfully created folder",
type: "success"
});
onOpenChange(false);
} catch (err: any) {
console.error(err);
createNotification({
title: "Failed to create folder",
text: err.message,
type: "error"
});
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent className="max-w-2xl" title="Create Folder">
<PamFolderForm onSubmit={onSubmit} />
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,55 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { TPamAccount, useDeletePamAccount } from "@app/hooks/api/pam";
type Props = {
account?: TPamAccount;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
const deletePamAccount = useDeletePamAccount();
if (!account) return null;
const {
id: accountId,
name,
resourceId,
resource: { resourceType }
} = account;
const handleDelete = async () => {
try {
await deletePamAccount.mutateAsync({
accountId,
resourceId,
resourceType
});
createNotification({
text: "Successfully deleted account",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete account",
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDelete}
/>
);
};

View File

@@ -0,0 +1,48 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { TPamFolder, useDeletePamFolder } from "@app/hooks/api/pam";
type Props = {
folder?: TPamFolder;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const PamDeleteFolderModal = ({ isOpen, onOpenChange, folder }: Props) => {
const deletePamFolder = useDeletePamFolder();
if (!folder) return null;
const { id: folderId, name } = folder;
const handleDelete = async () => {
try {
await deletePamFolder.mutateAsync({
folderId
});
createNotification({
text: "Successfully deleted folder",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete folder",
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDelete}
/>
);
};

View File

@@ -0,0 +1,95 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, TextArea } from "@app/components/v2";
import { TPamFolder } from "@app/hooks/api/pam";
import { slugSchema } from "@app/lib/schemas";
type Props = {
folder?: TPamFolder;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = z.object({
name: slugSchema({ min: 1, max: 64, field: "Name" }),
description: z.string().max(512).optional()
});
type FormData = z.infer<typeof formSchema>;
export const PamFolderForm = ({ folder, onSubmit }: Props) => {
const isUpdate = Boolean(folder);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: folder
? {
name: folder.name,
description: folder.description || ""
}
: undefined
});
const {
control,
handleSubmit,
formState: { isSubmitting, isDirty, errors }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<Controller
name="name"
control={control}
render={({ field }) => (
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="my-folder" {...field} />
</FormControl>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<FormControl
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Description"
isOptional
>
<TextArea {...field} />
</FormControl>
)}
/>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Folder" : "Create Folder"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,99 @@
import { faEdit, faEllipsisV, faFolder, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { TPamFolder } from "@app/hooks/api/pam";
type Props = {
folder: TPamFolder;
onUpdate: (folder: TPamFolder) => void;
onDelete: (folder: TPamFolder) => void;
onClick: () => void;
search: string;
};
export const PamFolderRow = ({ folder, onClick, onDelete, onUpdate, search }: Props) => {
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={onClick}
>
<Td>
<div className="flex items-center gap-4">
<div className="flex w-5 justify-center">
<FontAwesomeIcon icon={faFolder} className="size-4 text-yellow-700" />
</div>
<span>
<HighlightText text={folder.name} highlight={search} />
</span>
</div>
</Td>
<Td>
<div className="flex h-[22px] justify-end">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="hidden w-6 group-hover:flex data-[state=open]:!flex"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PamFolders}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={(e) => {
e.stopPropagation();
onUpdate(folder);
}}
>
Edit Folder
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.PamFolders}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={(e) => {
e.stopPropagation();
onDelete(folder);
}}
>
Delete Folder
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</div>
</Td>
</Tr>
);
};

View File

@@ -0,0 +1,47 @@
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge } from "@app/components/v2";
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
export const PamResourceOption = ({
isSelected,
children,
...props
}: OptionProps<{ id: string; name: string; resourceType: PamResourceType }>) => {
const isCreateOption = props.data.id === "_create";
const { name, image } = PAM_RESOURCE_TYPE_MAP[props.data.resourceType];
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
{isCreateOption ? (
<div className="flex items-center gap-x-1 text-mineshaft-400">
<FontAwesomeIcon icon={faPlus} size="sm" />
<span className="mr-auto">Create New Resource</span>
</div>
) : (
<>
<p className="truncate">{children}</p>
<div className="ml-2 mr-auto">
<Badge className="flex h-5 items-center gap-1 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<img
alt={`${name} logo`}
src={`/images/integrations/${image}`}
className="size-3"
/>
{name}
</Badge>
</div>
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</>
)}
</div>
</components.Option>
);
};

View File

@@ -0,0 +1,26 @@
import { Modal, ModalContent } from "@app/components/v2";
import { TPamAccount } from "@app/hooks/api/pam";
import { PamAccountForm } from "./PamAccountForm/PamAccountForm";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
account?: TPamAccount;
};
export const PamUpdateAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
if (!account) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent className="max-w-2xl" title="Edit Account" subTitle="Update account details.">
<PamAccountForm
onComplete={() => onOpenChange(false)}
account={account}
projectId={account.projectId}
/>
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,46 @@
import { createNotification } from "@app/components/notifications";
import { Modal, ModalContent } from "@app/components/v2";
import { TPamFolder, useUpdatePamFolder } from "@app/hooks/api/pam";
import { PamFolderForm } from "./PamFolderForm";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
folder?: TPamFolder;
};
export const PamUpdateFolderModal = ({ isOpen, onOpenChange, folder }: Props) => {
const updatePamFolder = useUpdatePamFolder();
if (!folder) return null;
const onSubmit = async (formData: Pick<TPamFolder, "name" | "description">) => {
try {
await updatePamFolder.mutateAsync({
...formData,
folderId: folder.id
});
createNotification({
text: "Successfully updated folder",
type: "success"
});
onOpenChange(false);
} catch (err: any) {
console.error(err);
createNotification({
title: "Failed to updated folder",
text: err.message,
type: "error"
});
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent className="max-w-2xl" title="Edit Account" subTitle="Update account details.">
<PamFolderForm onSubmit={onSubmit} folder={folder} />
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,121 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { SingleValue } from "react-select";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FilterableSelect, FormControl, ModalClose, Spinner } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { usePopUp } from "@app/hooks";
import { PamResourceType, useListPamResources } from "@app/hooks/api/pam";
import { PamAddResourceModal } from "../../PamResourcesPage/components/PamAddResourceModal";
import { PamResourceOption } from "./PamResourceOption";
type Props = {
onSubmit: (data: FormData) => void;
projectId: string;
};
const formSchema = z.object({
resource: z.object({
id: z.string(),
name: z.string(),
resourceType: z.nativeEnum(PamResourceType)
})
});
type FormData = z.infer<typeof formSchema>;
export const ResourceSelect = ({ onSubmit, projectId }: Props) => {
const { permission } = useProjectPermission();
const { isPending, data: resources } = useListPamResources(projectId);
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addResource"] as const);
const form = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const { handleSubmit, control, setValue } = form;
const canCreateResource = permission.can(
ProjectPermissionActions.Create,
ProjectPermissionSub.PamResources
);
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<>
<FormProvider {...form}>
<form
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Resource">
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string }>)?.id === "_create") {
handlePopUpOpen("addResource");
onChange(null);
return;
}
onChange(newValue);
}}
isLoading={isPending}
options={[
...(canCreateResource
? [
{
id: "_create",
name: "Create Resource",
// This is just to make typescript happy. Does not actually do anything
resourceType: PamResourceType.Postgres
}
]
: []),
...(resources ?? [])
]}
placeholder="Select resource..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: PamResourceOption }}
/>
</FormControl>
)}
control={control}
name="resource"
/>
<div className="mt-6 flex items-center">
<Button className="mr-4" size="sm" type="submit" colorSchema="secondary">
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
<PamAddResourceModal
isOpen={popUp.addResource.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addResource", isOpen)}
projectId={projectId}
onComplete={(resource) => setValue("resource", resource)}
/>
</>
);
};

View File

@@ -0,0 +1,49 @@
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { AccountView } from "./components/AccountViewToggle";
import { PamAccountsPage } from "./PamAccountsPage";
const PamAccountsPageQueryParamsSchema = z.object({
search: z.string().optional(),
accountView: z.nativeEnum(AccountView).optional(),
accountPath: z.string().catch("/")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts"
)({
validateSearch: zodValidator(PamAccountsPageQueryParamsSchema),
search: {
middlewares: [stripSearchParams({ accountPath: "/" })]
},
beforeLoad: ({ context, params, search }) => {
const accountPathSegments = search.accountPath.split("/").filter(Boolean);
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Accounts",
link: linkOptions({
to: "/projects/pam/$projectId/accounts",
params: () => params as never,
search: (prev) => ({ ...prev, accountPath: "/" })
})
},
...accountPathSegments.map((segment, index) => {
const newPath = `/${accountPathSegments.slice(0, index + 1).join("/")}/`;
return {
label: segment,
link: linkOptions({
to: "/projects/pam/$projectId/accounts",
params: () => params as never,
search: (prev) => ({ ...prev, accountPath: newPath })
})
};
})
]
};
},
component: PamAccountsPage
});

View File

@@ -0,0 +1,37 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamAccountActions } from "@app/context/ProjectPermissionContext/types";
import { PamResourcesSection } from "./components/PamResourcesSection";
export const PamResourcesPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "PAM" })}</title>
</Helmet>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionPamAccountActions.Read}
a={ProjectPermissionSub.PamResources}
>
<div className="h-full bg-bunker-800">
<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">
<PageHeader
title="Resources"
description="Manage resources such as servers, databases, and more."
/>
<PamResourcesSection />
</div>
</div>
</div>
</ProjectPermissionCan>
</>
);
};

View File

@@ -0,0 +1,52 @@
import { useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { PamResourceType, TPamResource } from "@app/hooks/api/pam";
import { PamResourceForm } from "./PamResourceForm";
import { ResourceTypeSelect } from "./ResourceTypeSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
projectId: string;
onComplete?: (resource: TPamResource) => void;
};
type ContentProps = {
onComplete: (resource: TPamResource) => void;
projectId: string;
};
const Content = ({ onComplete, projectId }: ContentProps) => {
const [selectedResourceType, setSelectedResourceType] = useState<PamResourceType | null>(null);
if (selectedResourceType) {
return (
<PamResourceForm
onComplete={onComplete}
onBack={() => setSelectedResourceType(null)}
resourceType={selectedResourceType}
projectId={projectId}
/>
);
}
return <ResourceTypeSelect onSelect={setSelectedResourceType} />;
};
export const PamAddResourceModal = ({ isOpen, onOpenChange, projectId, onComplete }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent className="max-w-2xl" title="Add Resource" subTitle="Select a resource to add.">
<Content
projectId={projectId}
onComplete={(resource) => {
if (onComplete) onComplete(resource);
onOpenChange(false);
}}
/>
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,49 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { PAM_RESOURCE_TYPE_MAP, TPamResource, useDeletePamResource } from "@app/hooks/api/pam";
type Props = {
resource?: TPamResource;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const PamDeleteResourceModal = ({ isOpen, onOpenChange, resource }: Props) => {
const deletePamResource = useDeletePamResource();
if (!resource) return null;
const { id: resourceId, name, resourceType } = resource;
const handleDelete = async () => {
try {
await deletePamResource.mutateAsync({
resourceId,
resourceType
});
createNotification({
text: `Successfully removed ${PAM_RESOURCE_TYPE_MAP[resourceType].name} resource`,
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to remove ${PAM_RESOURCE_TYPE_MAP[resourceType].name} resource`,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDelete}
/>
);
};

View File

@@ -0,0 +1,63 @@
import { Controller, useFormContext } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import { FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { gatewaysQueryKeys } from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
export const genericResourceFieldsSchema = z.object({
name: slugSchema({ min: 1, max: 64, field: "Name" }),
gatewayId: z.string().min(1)
});
export const GenericResourceFields = () => {
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const {
formState: { errors },
control
} = useFormContext<{ name: string; gatewayId: string }>();
return (
<>
<Controller
name="name"
control={control}
render={({ field }) => (
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="my-resource" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="gatewayId"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message} label="Gateway">
<Select
value={value || (null as unknown as string)}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Select a Gateway..."
position="popper"
>
{(gateways || []).map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</>
);
};

View File

@@ -0,0 +1,118 @@
import { createNotification } from "@app/components/notifications";
import {
PAM_RESOURCE_TYPE_MAP,
PamResourceType,
TPamResource,
useCreatePamResource,
useUpdatePamResource
} from "@app/hooks/api/pam";
import { DiscriminativePick } from "@app/types";
import { PamResourceHeader } from "../PamResourceHeader";
import { PostgresResourceForm } from "./PostgresResourceForm";
type FormProps = {
onComplete: (resource: TPamResource) => void;
} & ({ resource: TPamResource } | { resourceType: PamResourceType });
type CreateFormProps = FormProps & {
resourceType: PamResourceType;
projectId: string;
};
type UpdateFormProps = FormProps & {
resource: TPamResource;
};
const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) => {
const createPamResource = useCreatePamResource();
const { name: resourceName } = PAM_RESOURCE_TYPE_MAP[resourceType];
const onSubmit = async (
formData: DiscriminativePick<
TPamResource,
"name" | "resourceType" | "connectionDetails" | "gatewayId"
>
) => {
try {
const resource = await createPamResource.mutateAsync({
...formData,
projectId
});
createNotification({
text: `Successfully created ${resourceName} resource`,
type: "success"
});
onComplete(resource);
} catch (err: any) {
console.error(err);
createNotification({
title: `Failed to create ${resourceName} resource`,
text: err.message,
type: "error"
});
}
};
switch (resourceType) {
case PamResourceType.Postgres:
return <PostgresResourceForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${resourceType}`);
}
};
const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => {
const updatePamResource = useUpdatePamResource();
const { name: resourceName } = PAM_RESOURCE_TYPE_MAP[resource.resourceType];
const onSubmit = async (
formData: DiscriminativePick<TPamResource, "name" | "resourceType" | "connectionDetails">
) => {
try {
const updatedResource = await updatePamResource.mutateAsync({
resourceId: resource.id,
...formData
});
createNotification({
text: `Successfully updated ${resourceName} resource`,
type: "success"
});
onComplete(updatedResource);
} catch (err: any) {
console.error(err);
createNotification({
title: `Failed to update ${resourceName} resource`,
text: err.message,
type: "error"
});
}
};
switch (resource.resourceType) {
case PamResourceType.Postgres:
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${resource.resourceType}`);
}
};
type Props = { onBack?: () => void; projectId: string } & Pick<FormProps, "onComplete"> &
(
| { resourceType: PamResourceType; resource?: undefined }
| { resourceType?: undefined; resource: TPamResource }
);
export const PamResourceForm = ({ onBack, projectId, ...props }: Props) => {
const { resource, resourceType } = props;
return (
<div>
<PamResourceHeader resourceType={resourceType || resource.resourceType} onBack={onBack} />
{resource ? (
<UpdateForm {...props} resource={resource} />
) : (
<CreateForm {...props} resourceType={resourceType} projectId={projectId} />
)}
</div>
);
};

View File

@@ -0,0 +1,82 @@
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam";
import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
import { SqlResourceFields } from "./shared/SqlResourceFields";
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
type Props = {
resource?: TPostgresResource;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericResourceFieldsSchema.extend({
resourceType: z.literal(PamResourceType.Postgres),
connectionDetails: BaseSqlResourceSchema
});
type FormData = z.infer<typeof formSchema>;
export const PostgresResourceForm = ({ resource, onSubmit }: Props) => {
const isUpdate = Boolean(resource);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: resource ?? {
resourceType: PamResourceType.Postgres,
connectionDetails: {
host: "",
port: 5432,
database: "default",
sslEnabled: true,
sslRejectUnauthorized: true,
sslCertificate: undefined
}
}
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
setSelectedTabIndex(0);
handleSubmit(onSubmit)(e);
}}
>
<GenericResourceFields />
<SqlResourceFields
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
/>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Details" : "Create Resource"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./GenericResourceFields";
export * from "./PamResourceForm";

View File

@@ -0,0 +1,159 @@
import { Dispatch, SetStateAction } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
import { FormControl, Input, Switch, TextArea, Tooltip } from "@app/components/v2";
type Props = {
selectedTabIndex: number;
setSelectedTabIndex: Dispatch<SetStateAction<number>>;
};
export const SqlResourceFields = ({ setSelectedTabIndex, selectedTabIndex }: Props) => {
const { control, watch } = useFormContext();
const sslEnabled = watch("connectionDetails.sslEnabled");
return (
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected ? "border-b-2 border-mineshaft-300 text-mineshaft-200" : "text-bunker-300"
}`
}
>
Configuration
</Tab>
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected ? "border-b-2 border-mineshaft-300 text-mineshaft-200" : "text-bunker-300"
}`
}
>
SSL ({sslEnabled ? "Enabled" : "Disabled"})
</Tab>
</Tab.List>
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
<Tab.Panel>
<div className="mt-[0.675rem] flex items-start gap-2">
<Controller
name="connectionDetails.host"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Host"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="connectionDetails.database"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Database Name"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="connectionDetails.port"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="w-28"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Port"
>
<Input type="number" {...field} />
</FormControl>
)}
/>
</div>
</Tab.Panel>
<Tab.Panel>
<Controller
name="connectionDetails.sslEnabled"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Enable SSL
</Switch>
</FormControl>
)}
/>
<Controller
name="connectionDetails.sslCertificate"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className={sslEnabled ? "" : "opacity-50"}
label="SSL Certificate"
isOptional
>
<TextArea className="h-[3.5rem] !resize-none" {...field} isDisabled={!sslEnabled} />
</FormControl>
)}
/>
<Controller
name="connectionDetails.sslRejectUnauthorized"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className={sslEnabled ? "" : "opacity-50"}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-reject-unauthorized"
thumbClassName="bg-mineshaft-800"
isChecked={sslEnabled ? value : false}
onCheckedChange={onChange}
isDisabled={!sslEnabled}
>
<p className="w-[9.5rem]">
Reject Unauthorized
<Tooltip
className="max-w-md"
content={
<p>
If enabled, Infisical will only connect to the server if it has a valid,
trusted SSL certificate.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
};

View File

@@ -0,0 +1,14 @@
import { z } from "zod";
export const BaseSqlResourceSchema = z.object({
host: z.string().trim().min(1, "Host required"),
port: z.coerce.number().default(5432),
database: z.string().trim().min(1, "Database required").default("default"),
sslEnabled: z.boolean().default(true),
sslRejectUnauthorized: z.boolean().default(true),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
});

View File

@@ -0,0 +1,33 @@
import { PAM_RESOURCE_TYPE_MAP, PamResourceType } from "@app/hooks/api/pam";
type Props = {
resourceType: PamResourceType;
onBack?: () => void;
};
export const PamResourceHeader = ({ resourceType, onBack }: Props) => {
const details = PAM_RESOURCE_TYPE_MAP[resourceType];
return (
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
<img
alt={`${details.name} logo`}
src={`/images/integrations/${details.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">{details.name}</div>
<p className="text-sm leading-4 text-mineshaft-400">External resource</p>
</div>
{onBack && (
<button
type="button"
className="ml-auto mt-1 text-xs text-mineshaft-400 underline underline-offset-2 hover:text-mineshaft-300"
onClick={onBack}
>
Select another resource type
</button>
)}
</div>
);
};

View File

@@ -0,0 +1,98 @@
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { PAM_RESOURCE_TYPE_MAP, TPamResource } from "@app/hooks/api/pam";
type Props = {
resource: TPamResource;
onUpdate: (resource: TPamResource) => void;
onDelete: (resource: TPamResource) => void;
search: string;
};
export const PamResourceRow = ({ resource, onUpdate, onDelete, search }: Props) => {
const { name, resourceType } = resource;
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
return (
<Tr className={twMerge("group h-10")}>
<Td>
<div className="flex items-center gap-4">
<div className="relative">
<img alt={resourceTypeName} src={`/images/integrations/${image}`} className="size-5" />
</div>
<span>
<HighlightText text={name} highlight={search} />
</span>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<span>
<HighlightText text={resourceTypeName} highlight={search} />
</span>
</Badge>
</div>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PamResources}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onUpdate(resource)}
>
Edit Resource
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.PamResources}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(resource)}
>
Delete Resource
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
};

View File

@@ -0,0 +1,17 @@
import { ContentLoader } from "@app/components/v2";
import { useProject } from "@app/context";
import { useListPamResources } from "@app/hooks/api/pam";
import { PamResourcesTable } from "./PamResourcesTable";
export const PamResourcesSection = () => {
const { currentProject } = useProject();
const { data: resources = [], isPending } = useListPamResources(currentProject.id, {
refetchInterval: 30000
});
if (isPending) return <ContentLoader />;
return <PamResourcesTable resources={resources} projectId={currentProject.id} />;
};

View File

@@ -0,0 +1,326 @@
import { useMemo, useState } from "react";
import { faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TBody,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { PAM_RESOURCE_TYPE_MAP, PamResourceType, TPamResource } from "@app/hooks/api/pam";
import { PamAddResourceModal } from "./PamAddResourceModal";
import { PamDeleteResourceModal } from "./PamDeleteResourceModal";
import { PamResourceRow } from "./PamResourceRow";
import { PamUpdateResourceModal } from "./PamUpdateResourceModal";
enum OrderBy {
Name = "name"
}
type Filters = {
resourceType: PamResourceType[];
};
type Props = {
projectId: string;
resources: TPamResource[];
};
export const PamResourcesTable = ({ projectId, resources }: Props) => {
const { subscription } = useSubscription();
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.ResourcesPage.path });
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"updateResource",
"addResource",
"deleteResource"
] as const);
const { search: initSearch } = useSearch({
from: ROUTE_PATHS.Pam.ResourcesPage.id
});
const [filters, setFilters] = useState<Filters>({
resourceType: []
});
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy
} = usePagination<OrderBy>(OrderBy.Name, { initPerPage: 20, initSearch });
const filteredResources = useMemo(
() =>
resources
.filter((resource) => {
const { name, resourceType } = resource;
if (filters.resourceType.length && !filters.resourceType.includes(resourceType)) {
return false;
}
const searchValue = search.trim().toLowerCase();
const { name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
return (
name.toLowerCase().includes(searchValue) ||
resourceTypeName.toLowerCase().includes(searchValue)
);
})
.sort((a, b) => {
const [one, two] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case OrderBy.Name:
default:
return one.name.toLowerCase().localeCompare(two.name.toLowerCase());
}
}),
[resources, orderDirection, search, orderBy, filters]
);
useResetPageHelper({
totalCount: filteredResources.length,
offset,
setPage
});
const currentPageData = useMemo(
() => filteredResources.slice(offset, perPage * page),
[filteredResources, offset, perPage, page]
);
const handleSort = (column: OrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: OrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.resourceType.length);
const isContentEmpty = !filteredResources.length;
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
return (
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => {
const newSearch = e.target.value;
setSearch(newSearch);
navigate({
search: (prev) => ({ ...prev, search: newSearch || undefined }),
replace: true
});
}}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search resources..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter resources"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Resource Type</DropdownMenuLabel>
{resources.length ? (
[...new Set(resources.map(({ resourceType }) => resourceType))].map((type) => {
const { name, image } = PAM_RESOURCE_TYPE_MAP[type];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
resourceType: prev.resourceType.includes(type)
? prev.resourceType.filter((a) => a !== type)
: [...prev.resourceType, type]
}));
}}
key={type}
icon={
filters.resourceType.includes(type) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${name} resource type`}
src={`/images/integrations/${image}`}
className="h-4 w-4"
/>
<span>{name}</span>
</div>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No Resources</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isGatewayAllowed) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.PamResources}
>
{(isAllowed) => (
<Tooltip
isDisabled={isGatewayAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addResource")}
isDisabled={
!isAllowed || !isGatewayAllowed || !subscription.gateway || !subscription.pam
}
>
Add Resource
</Button>
</Tooltip>
)}
</ProjectPermissionCan>
)}
</OrgPermissionCan>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>
<div className="flex items-center">
Resource
<IconButton
variant="plain"
className={getClassName(OrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(OrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{currentPageData.map((resource) => (
<PamResourceRow
key={resource.id}
resource={resource}
onUpdate={(e) => handlePopUpOpen("updateResource", e)}
onDelete={(e) => handlePopUpOpen("deleteResource", e)}
search={search.trim().toLowerCase()}
/>
))}
</TBody>
</Table>
{Boolean(filteredResources.length) && (
<Pagination
count={filteredResources.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{isContentEmpty && (
<EmptyState
title={isSearchEmpty ? "No resources match search" : "No resources"}
icon={isSearchEmpty ? faSearch : faCircleXmark}
/>
)}
</TableContainer>
<PamDeleteResourceModal
isOpen={popUp.deleteResource.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteResource", isOpen)}
resource={popUp.deleteResource.data}
/>
<PamUpdateResourceModal
isOpen={popUp.updateResource.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("updateResource", isOpen)}
resource={popUp.updateResource.data}
/>
<PamAddResourceModal
isOpen={popUp.addResource.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addResource", isOpen)}
projectId={projectId}
/>
</div>
);
};

View File

@@ -0,0 +1,32 @@
import { Modal, ModalContent } from "@app/components/v2";
import { PAM_RESOURCE_TYPE_MAP, TPamResource } from "@app/hooks/api/pam";
import { PamResourceForm } from "./PamResourceForm";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
resource?: TPamResource;
};
export const PamUpdateResourceModal = ({ isOpen, onOpenChange, resource }: Props) => {
if (!resource) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Edit Resource"
subTitle={`Update details for this ${
PAM_RESOURCE_TYPE_MAP[resource.resourceType].name
} resource.`}
>
<PamResourceForm
onComplete={() => onOpenChange(false)}
resource={resource}
projectId={resource.projectId}
/>
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,141 @@
import { useMemo } from "react";
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
import { usePagination, useResetPageHelper } from "@app/hooks";
import {
PAM_RESOURCE_TYPE_MAP,
PamResourceType,
useListPamResourceOptions
} from "@app/hooks/api/pam";
type Props = {
onSelect: (resource: PamResourceType) => void;
};
export const ResourceTypeSelect = ({ onSelect }: Props) => {
const { isPending, data: resourceOptions } = useListPamResourceOptions();
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
initPerPage: 16
});
const filteredOptions = useMemo(
() =>
resourceOptions?.filter(
({ name, resource }) =>
name.toLowerCase().includes(search.trim().toLowerCase()) ||
resource.toLowerCase().includes(search.trim().toLowerCase())
) ?? [],
[resourceOptions, search]
);
useResetPageHelper({
totalCount: filteredOptions.length,
offset,
setPage
});
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search options..."
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
/>
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
const { image, name, size = 50 } = PAM_RESOURCE_TYPE_MAP[option.resource];
return (
<button
type="button"
onClick={() => onSelect(option.resource)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<div className="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
{!filteredOptions?.length && (
<EmptyState
className="col-span-full mt-40"
title="No resources match search"
icon={faSearch}
/>
)}
</div>
{Boolean(filteredOptions.length) && (
<Pagination
startAdornment={
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more resources.</p>
<p>
{"If you don't see the resource you're looking for,"}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
<span className="text-xs">Don&#39;t see the resource you&#39;re looking for?</span>
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
</div>
</Tooltip>
}
count={filteredOptions.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
perPageList={[16]}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,26 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { PamResourcesPage } from "./PamResourcesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources"
)({
validateSearch: zodValidator(
z.object({
search: z.string().optional()
})
),
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Resources"
}
]
};
},
component: PamResourcesPage
});

View File

@@ -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>
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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"
}
]
};
}
});

View File

@@ -0,0 +1,37 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types";
import { PamSessionSection } from "./components/PamSessionSection";
export const PamSessionPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "PAM" })}</title>
</Helmet>
<ProjectPermissionCan
renderGuardBanner
I={ProjectPermissionPamSessionActions.Read}
a={ProjectPermissionSub.PamSessions}
>
<div className="h-full bg-bunker-800">
<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">
<PageHeader
title="Sessions"
description="Filter and search through account sessions."
/>
<PamSessionSection />
</div>
</div>
</div>
</ProjectPermissionCan>
</>
);
};

View File

@@ -0,0 +1,198 @@
import { useState } from "react";
import {
faBoxOpen,
faChevronDown,
faChevronUp,
faEdit,
faEllipsisV,
faTerminal
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useRouter } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { PAM_RESOURCE_TYPE_MAP, TPamSession } from "@app/hooks/api/pam";
import { PamSessionStatusBadge } from "./PamSessionStatusBadge";
type Props = {
session: TPamSession;
search: string;
filteredCommandLogs: TPamSession["commandLogs"];
};
export const PamSessionRow = ({ session, search, filteredCommandLogs }: Props) => {
const router = useRouter();
const [showAllLogs, setShowAllLogs] = useState(false);
const {
id,
accountName,
resourceType,
resourceName,
projectId,
status,
actorName,
actorEmail,
createdAt,
endedAt
} = session;
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
const LOGS_TO_SHOW = 5;
const logsToShow = showAllLogs ? filteredCommandLogs : filteredCommandLogs.slice(0, LOGS_TO_SHOW);
return (
<>
<Tr
className={twMerge("group h-10 cursor-pointer hover:bg-bunker-400/20")}
onClick={() => router.history.push(`/projects/pam/${projectId}/sessions/${id}`)}
>
<Td>
<div className="flex items-center gap-4">
<div className="relative">
<img
alt={resourceTypeName}
src={`/images/integrations/${image}`}
className="size-6"
/>
</div>
<div className="flex items-center gap-2">
<div className="flex flex-col">
<span>
<HighlightText text={accountName} highlight={search} />
</span>
<div className="flex items-center gap-1 text-xs text-bunker-300">
<FontAwesomeIcon icon={faBoxOpen} className="size-3" />
<span>
<HighlightText text={resourceName} highlight={search} />
</span>
</div>
</div>
</div>
</div>
</Td>
<Td>
<div className="flex flex-col">
<span>
<HighlightText text={actorName} highlight={search} />
</span>
<span className="text-xs text-bunker-300">
<HighlightText text={actorEmail} highlight={search} />
</span>
</div>
</Td>
<Td>
<div className="flex flex-col">
<span>{new Date(createdAt).toLocaleTimeString()}</span>
<span className="text-xs text-bunker-300">
{new Date(createdAt).toLocaleDateString()}
</span>
</div>
</Td>
<Td>
{endedAt ? (
<div className="flex flex-col">
<span>{new Date(endedAt).toLocaleTimeString()}</span>
<span className="text-xs text-bunker-300">
{new Date(endedAt).toLocaleDateString()}
</span>
</div>
) : (
<span className="text-bunker-400">Ongoing</span>
)}
</Td>
<Td>
<div className="flex items-center justify-end gap-2">
<PamSessionStatusBadge status={status} />
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.PamResources}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() =>
router.history.push(`/projects/pam/${projectId}/sessions/${id}`)
}
>
View Session
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</div>
</Td>
</Tr>
{filteredCommandLogs.length > 0 && (
<Tr>
<Td colSpan={5} className="py-3 text-xs">
{logsToShow.map((log) => (
<div key={`${id}-log-${log.timestamp}`} className="mb-4 flex flex-col last:mb-0">
<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="font-mono">
<HighlightText text={log.input} highlight={search} />
</div>
<div className="font-mono text-bunker-300">
<HighlightText text={log.output} highlight={search} />
</div>
</div>
))}
{filteredCommandLogs.length > LOGS_TO_SHOW && (
<div className="mt-2">
<Button
variant="link"
size="xs"
leftIcon={<FontAwesomeIcon icon={showAllLogs ? faChevronUp : faChevronDown} />}
onClick={() => setShowAllLogs(!showAllLogs)}
className="p-0 text-mineshaft-300 hover:text-primary"
>
{showAllLogs
? "Show less"
: `Show ${filteredCommandLogs.length - LOGS_TO_SHOW} more log${filteredCommandLogs.length - LOGS_TO_SHOW === 1 ? "" : "s"}`}
</Button>
</div>
)}
</Td>
</Tr>
)}
</>
);
};

View File

@@ -0,0 +1,17 @@
import { ContentLoader } from "@app/components/v2";
import { useProject } from "@app/context";
import { useListPamSessions } from "@app/hooks/api/pam";
import { PamSessionsTable } from "./PamSessionsTable";
export const PamSessionSection = () => {
const { currentProject } = useProject();
const { data: sessions = [], isPending } = useListPamSessions(currentProject.id, {
refetchInterval: 30000
});
if (isPending) return <ContentLoader />;
return <PamSessionsTable sessions={sessions} />;
};

View File

@@ -0,0 +1,63 @@
import {
faBan,
faCircle,
faGavel,
faHourglass,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v2";
import { PamSessionStatus } from "@app/hooks/api/pam";
interface StatusConfig {
bgColor: string;
textColor: string;
icon: IconDefinition;
iconClassName?: string;
}
const PAM_SESSION_STATUS_CONFIG: Record<PamSessionStatus, StatusConfig> = {
[PamSessionStatus.Active]: {
bgColor: "bg-green/20",
textColor: "text-green",
icon: faCircle,
iconClassName: "size-2 animate-pulse"
},
[PamSessionStatus.Terminated]: {
bgColor: "bg-red/20",
textColor: "text-red",
icon: faGavel
},
[PamSessionStatus.Starting]: {
bgColor: "bg-yellow/20",
textColor: "text-yellow",
icon: faHourglass,
iconClassName: "animate-spin"
},
[PamSessionStatus.Ended]: {
bgColor: "bg-bunker-300/20",
textColor: "text-bunker-300",
icon: faBan
}
};
export const PamSessionStatusBadge = ({ status }: { status: PamSessionStatus }) => {
const config = PAM_SESSION_STATUS_CONFIG[status];
const displayName = status[0].toUpperCase() + status.slice(1);
return (
<Badge
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap",
config.bgColor,
config.textColor
)}
>
<FontAwesomeIcon icon={config.icon} className={config.iconClassName} />
{displayName}
</Badge>
);
};

View File

@@ -0,0 +1,411 @@
import { useEffect, useMemo, useState } from "react";
import { faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import {
PAM_RESOURCE_TYPE_MAP,
PamResourceType,
PamSessionStatus,
TPamSession
} from "@app/hooks/api/pam";
import { PamSessionRow } from "./PamSessionRow";
enum OrderBy {
Account = "account",
Actor = "actor",
CreatedAt = "createdAt",
EndedAt = "endedAt"
}
type Filters = {
resourceType: PamResourceType[];
status: PamSessionStatus[];
};
type Props = {
sessions: TPamSession[];
};
export const PamSessionsTable = ({ sessions }: Props) => {
const navigate = useNavigate({ from: ROUTE_PATHS.Pam.SessionsPage.path });
const { search: initSearch } = useSearch({
from: ROUTE_PATHS.Pam.SessionsPage.id
});
const [filters, setFilters] = useState<Filters>({
resourceType: [],
status: []
});
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy
} = usePagination<OrderBy>(OrderBy.CreatedAt, {
initPerPage: 20,
initSearch
});
useEffect(() => {
setOrderDirection(OrderByDirection.DESC);
}, []);
const sessionsWithMatches = useMemo(() => {
const searchValue = search.trim().toLowerCase();
if (!searchValue) {
return sessions.map((session) => ({
session,
isMatch: true,
filteredLogs: []
}));
}
return sessions.map((session) => {
const {
resourceType,
accountName,
actorEmail,
actorIp,
actorName,
actorUserAgent,
id,
resourceName,
userId,
commandLogs
} = session;
const { name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[resourceType];
const isMetaMatch =
resourceTypeName.toLowerCase().includes(searchValue) ||
accountName.toLowerCase().includes(searchValue) ||
actorEmail.toLowerCase().includes(searchValue) ||
actorIp.toLowerCase().includes(searchValue) ||
actorName.toLowerCase().includes(searchValue) ||
actorUserAgent.toLowerCase().includes(searchValue) ||
id.toLowerCase().includes(searchValue) ||
(userId ?? "").toLowerCase().includes(searchValue) ||
resourceName.toLowerCase().includes(searchValue);
const filteredLogs =
searchValue.length >= 2
? commandLogs.filter(
(log) =>
log.input.toLowerCase().includes(searchValue) ||
log.output.toLowerCase().includes(searchValue)
)
: [];
return {
session,
isMatch: isMetaMatch || filteredLogs.length > 0,
filteredLogs
};
});
}, [sessions, search]);
const filteredSessions = useMemo(
() =>
sessionsWithMatches
.filter((item) => {
if (!item.isMatch) return false;
const { resourceType, status } = item.session;
if (
(filters.resourceType.length && !filters.resourceType.includes(resourceType)) ||
(filters.status.length && !filters.status.includes(status))
) {
return false;
}
return true;
})
.sort((a, b) => {
const [one, two] =
orderDirection === OrderByDirection.ASC
? [a.session, b.session]
: [b.session, a.session];
switch (orderBy) {
case OrderBy.Account:
return one.accountName.toLowerCase().localeCompare(two.accountName.toLowerCase());
case OrderBy.Actor:
return one.actorName.toLowerCase().localeCompare(two.actorName.toLowerCase());
case OrderBy.EndedAt: {
const dateOne = one.endedAt || one.createdAt;
const dateTwo = two.endedAt || two.createdAt;
return new Date(dateOne).getTime() - new Date(dateTwo).getTime();
}
case OrderBy.CreatedAt:
default: {
const dateOne = one.createdAt;
const dateTwo = two.createdAt;
return new Date(dateOne).getTime() - new Date(dateTwo).getTime();
}
}
}),
[sessionsWithMatches, orderDirection, orderBy, filters]
);
useResetPageHelper({
totalCount: filteredSessions.length,
offset,
setPage
});
const currentPageData = useMemo(
() => filteredSessions.slice(offset, perPage * page),
[filteredSessions, offset, perPage, page]
);
const handleSort = (column: OrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: OrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: OrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.resourceType.length || filters.status.length);
const isContentEmpty = !filteredSessions.length;
const isSearchEmpty = isContentEmpty && (Boolean(search) || isTableFiltered);
return (
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => {
const newSearch = e.target.value;
setSearch(newSearch);
navigate({
search: (prev) => ({ ...prev, search: newSearch || undefined }),
replace: true
});
}}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search sessions and logs..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter sessions"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Session Status</DropdownMenuLabel>
{sessions.length ? (
[...new Set(sessions.map(({ status }) => status))].map((status) => {
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
status: prev.status.includes(status)
? prev.status.filter((a) => a !== status)
: [...prev.status, status]
}));
}}
key={status}
icon={
filters.status.includes(status) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
{status[0].toUpperCase() + status.slice(1)}
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No Sessions</DropdownMenuItem>
)}
<DropdownMenuLabel>Resource Type</DropdownMenuLabel>
{sessions.length ? (
[...new Set(sessions.map(({ resourceType }) => resourceType))].map((type) => {
const { name, image } = PAM_RESOURCE_TYPE_MAP[type];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
resourceType: prev.resourceType.includes(type)
? prev.resourceType.filter((a) => a !== type)
: [...prev.resourceType, type]
}));
}}
key={type}
icon={
filters.resourceType.includes(type) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${name} resource type`}
src={`/images/integrations/${image}`}
className="h-4 w-4"
/>
<span>{name}</span>
</div>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No Sessions</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>
<div className="flex items-center">
Account
<IconButton
variant="plain"
className={getClassName(OrderBy.Account)}
ariaLabel="sort"
onClick={() => handleSort(OrderBy.Account)}
>
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Account)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Actor
<IconButton
variant="plain"
className={getClassName(OrderBy.Actor)}
ariaLabel="sort"
onClick={() => handleSort(OrderBy.Actor)}
>
<FontAwesomeIcon icon={getColSortIcon(OrderBy.Actor)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Created At
<IconButton
variant="plain"
className={getClassName(OrderBy.CreatedAt)}
ariaLabel="sort"
onClick={() => handleSort(OrderBy.CreatedAt)}
>
<FontAwesomeIcon icon={getColSortIcon(OrderBy.CreatedAt)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Ended At
<IconButton
variant="plain"
className={getClassName(OrderBy.EndedAt)}
ariaLabel="sort"
onClick={() => handleSort(OrderBy.EndedAt)}
>
<FontAwesomeIcon icon={getColSortIcon(OrderBy.EndedAt)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{currentPageData.map(({ session, filteredLogs }) => (
<PamSessionRow
key={session.id}
session={session}
search={search.trim().toLowerCase()}
filteredCommandLogs={filteredLogs}
/>
))}
</TBody>
</Table>
{Boolean(filteredSessions.length) && (
<Pagination
count={filteredSessions.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{isContentEmpty && (
<EmptyState
title={isSearchEmpty ? "No sessions match search" : "No sessions"}
icon={isSearchEmpty ? faSearch : faCircleXmark}
/>
)}
</TableContainer>
</div>
);
};

View File

@@ -0,0 +1,26 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { PamSessionPage } from "./PamSessionsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/"
)({
validateSearch: zodValidator(
z.object({
search: z.string().optional()
})
),
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Sessions"
}
]
};
},
component: PamSessionPage
});

View File

@@ -0,0 +1,28 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
export const SettingsPage = () => {
const { t } = useTranslation();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl">
<PageHeader title="Settings" description="Configure your PAM project." />
<Tabs defaultValue="tab-project-general">
<TabList>
<Tab value="tab-project-general">General</Tab>
</TabList>
<TabPanel value="tab-project-general">
<ProjectGeneralTab />
</TabPanel>
</Tabs>
</div>
</div>
);
};

View File

@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings"
)({
component: SettingsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Settings"
}
]
};
}
});

View File

@@ -0,0 +1,37 @@
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from "@app/components/v2";
import { projectKeys } from "@app/hooks/api";
import { fetchProjectById } from "@app/hooks/api/projects/queries";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { PamLayout } from "@app/layouts/PamLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
)({
component: PamLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: projectKeys.getProjectById(params.projectId),
queryFn: () => fetchProjectById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
projectId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId })
});
return {
project,
breadcrumbs: [
{
type: BreadcrumbTypes.Component,
component: ProjectSelect
}
]
};
}
});

View File

@@ -0,0 +1,32 @@
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { ProjectAccessControlTabs } from "@app/types/project";
import { AccessControlPage } from "./AccessControlPage";
const AccessControlPageQuerySchema = z.object({
selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member),
requesterEmail: z.string().catch("")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema),
search: {
middlewares: [stripSearchParams({ requesterEmail: "" })]
},
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control"
}
]
};
}
});

View File

@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { AuditLogsPage } from "./AuditLogsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs"
)({
component: AuditLogsPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Audit Logs"
}
]
};
}
});

View File

@@ -0,0 +1,33 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { ProjectAccessControlTabs } from "@app/types/project";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-management/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Groups
}
})
},
{
label: "Group"
}
]
};
}
});

View File

@@ -0,0 +1,33 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { ProjectAccessControlTabs } from "@app/types/project";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-management/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Identities
}
})
},
{
label: "Machine Identity"
}
]
};
}
});

View File

@@ -0,0 +1,33 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { ProjectAccessControlTabs } from "@app/types/project";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-scanning/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Member
}
})
},
{
label: "User"
}
]
};
}
});

View File

@@ -0,0 +1,23 @@
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { ConditionsFields } from "./ConditionsFields";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const PamAccountPermissionConditions = ({ position = 0, isDisabled }: Props) => {
return (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.PamAccounts}
position={position}
selectOptions={[
{ value: "resourceName", label: "Resource Name" },
{ value: "accountName", label: "Account Name" },
{ value: "accountPath", label: "Account Path" }
]}
/>
);
};

View File

@@ -20,6 +20,8 @@ import {
ProjectPermissionIdentityActions,
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionPamAccountActions,
ProjectPermissionPamSessionActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions,
@@ -221,6 +223,18 @@ const SecretEventsPolicyActionSchema = z.object({
[ProjectPermissionSecretEventActions.SubscribeImportMutations]: z.boolean().optional()
});
const PamAccountPolicyActionSchema = z.object({
[ProjectPermissionPamAccountActions.Access]: z.boolean().optional(),
[ProjectPermissionPamAccountActions.Create]: z.boolean().optional(),
[ProjectPermissionPamAccountActions.Read]: z.boolean().optional(),
[ProjectPermissionPamAccountActions.Edit]: z.boolean().optional(),
[ProjectPermissionPamAccountActions.Delete]: z.boolean().optional()
});
const PamSessionPolicyActionSchema = z.object({
[ProjectPermissionPamSessionActions.Read]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(),
create: z.boolean().optional()
@@ -406,7 +420,16 @@ export const projectRoleFormSchema = z.object({
conditions: ConditionSchema
})
.array()
.default([])
.default([]),
[ProjectPermissionSub.PamFolders]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PamResources]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PamAccounts]: PamAccountPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.PamSessions]: PamSessionPolicyActionSchema.array().default([])
})
.partial()
.optional()
@@ -427,7 +450,8 @@ type TConditionalFields =
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.PkiSyncs
| ProjectPermissionSub.SecretEvents
| ProjectPermissionSub.AppConnections;
| ProjectPermissionSub.AppConnections
| ProjectPermissionSub.PamAccounts;
export const isConditionalSubjects = (
subject: ProjectPermissionSub
@@ -444,7 +468,8 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.SecretSyncs ||
subject === ProjectPermissionSub.PkiSyncs ||
subject === ProjectPermissionSub.SecretEvents ||
subject === ProjectPermissionSub.AppConnections;
subject === ProjectPermissionSub.AppConnections ||
subject === ProjectPermissionSub.PamAccounts;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -553,7 +578,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SecretSyncs,
ProjectPermissionSub.PkiSyncs,
ProjectPermissionSub.SecretEvents,
ProjectPermissionSub.AppConnections
ProjectPermissionSub.AppConnections,
ProjectPermissionSub.PamFolders,
ProjectPermissionSub.PamResources
].includes(subject)
) {
// from above statement we are sure it won't be undefined
@@ -1112,6 +1139,40 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
inverted
});
}
if (subject === ProjectPermissionSub.PamAccounts) {
if (!formVal[subject]) formVal[subject] = [];
formVal[subject].push({
[ProjectPermissionPamAccountActions.Access]: action.includes(
ProjectPermissionPamAccountActions.Access
),
[ProjectPermissionPamAccountActions.Create]: action.includes(
ProjectPermissionPamAccountActions.Create
),
[ProjectPermissionPamAccountActions.Delete]: action.includes(
ProjectPermissionPamAccountActions.Delete
),
[ProjectPermissionPamAccountActions.Edit]: action.includes(
ProjectPermissionPamAccountActions.Edit
),
[ProjectPermissionPamAccountActions.Read]: action.includes(
ProjectPermissionPamAccountActions.Read
),
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
if (subject === ProjectPermissionSub.PamSessions) {
const canRead = action.includes(ProjectPermissionPamSessionActions.Read);
if (!formVal[subject]) formVal[subject] = [{}];
// Map actions to the keys defined in ApprovalPolicyActionSchema
if (canRead) formVal[subject]![0][ProjectPermissionPamAccountActions.Read] = true;
}
});
return formVal;
@@ -1731,6 +1792,38 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
value: ProjectPermissionAppConnectionActions.Connect
}
]
},
[ProjectPermissionSub.PamFolders]: {
title: "Folders",
actions: [
{ label: "Read", value: ProjectPermissionActions.Read },
{ label: "Create", value: ProjectPermissionActions.Create },
{ label: "Modify", value: ProjectPermissionActions.Edit },
{ label: "Remove", value: ProjectPermissionActions.Delete }
]
},
[ProjectPermissionSub.PamResources]: {
title: "Resources",
actions: [
{ label: "Read", value: ProjectPermissionActions.Read },
{ label: "Create", value: ProjectPermissionActions.Create },
{ label: "Modify", value: ProjectPermissionActions.Edit },
{ label: "Remove", value: ProjectPermissionActions.Delete }
]
},
[ProjectPermissionSub.PamAccounts]: {
title: "Accounts",
actions: [
{ label: "Access", value: ProjectPermissionPamAccountActions.Access },
{ label: "Read", value: ProjectPermissionPamAccountActions.Read },
{ label: "Create", value: ProjectPermissionPamAccountActions.Create },
{ label: "Modify", value: ProjectPermissionPamAccountActions.Edit },
{ label: "Remove", value: ProjectPermissionPamAccountActions.Delete }
]
},
[ProjectPermissionSub.PamSessions]: {
title: "Sessions",
actions: [{ label: "Read", value: ProjectPermissionPamSessionActions.Read }]
}
};
@@ -1793,6 +1886,13 @@ const SecretScanningSubject = (enabled = false) => ({
[ProjectPermissionSub.SecretScanningConfigs]: enabled
});
const PamPermissionSubjects = (enabled = false) => ({
[ProjectPermissionSub.PamFolders]: enabled,
[ProjectPermissionSub.PamResources]: enabled,
[ProjectPermissionSub.PamAccounts]: enabled,
[ProjectPermissionSub.PamSessions]: enabled
});
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
export const ProjectTypePermissionSubjects: Record<
ProjectType,
@@ -1805,6 +1905,7 @@ export const ProjectTypePermissionSubjects: Record<
...CertificateManagerPermissionSubjects(),
...SshPermissionSubjects(),
...SecretScanningSubject(),
...PamPermissionSubjects(),
[ProjectPermissionSub.AppConnections]: true
},
[ProjectType.KMS]: {
@@ -1814,6 +1915,7 @@ export const ProjectTypePermissionSubjects: Record<
...CertificateManagerPermissionSubjects(),
...SshPermissionSubjects(),
...SecretScanningSubject(),
...PamPermissionSubjects(),
[ProjectPermissionSub.AppConnections]: false
},
[ProjectType.CertificateManager]: {
@@ -1823,6 +1925,7 @@ export const ProjectTypePermissionSubjects: Record<
...SecretsManagerPermissionSubjects(),
...SshPermissionSubjects(),
...SecretScanningSubject(),
...PamPermissionSubjects(),
[ProjectPermissionSub.AppConnections]: true
},
[ProjectType.SSH]: {
@@ -1832,6 +1935,7 @@ export const ProjectTypePermissionSubjects: Record<
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects(),
...SecretScanningSubject(),
...PamPermissionSubjects(),
[ProjectPermissionSub.AppConnections]: false
},
[ProjectType.SecretScanning]: {
@@ -1841,7 +1945,18 @@ export const ProjectTypePermissionSubjects: Record<
...CertificateManagerPermissionSubjects(),
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects(),
...PamPermissionSubjects(),
[ProjectPermissionSub.AppConnections]: true
},
[ProjectType.PAM]: {
...SharedPermissionSubjects,
...SecretScanningSubject(),
...SshPermissionSubjects(),
...CertificateManagerPermissionSubjects(),
...KmsPermissionSubjects(),
...SecretsManagerPermissionSubjects(),
...PamPermissionSubjects(true),
[ProjectPermissionSub.AppConnections]: false
}
};
@@ -2245,5 +2360,65 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
actions: Object.values(ProjectPermissionActions)
}
])
],
[ProjectType.PAM]: [
{
id: "pam-viewer",
name: "PAM Viewing Policies",
description: "Grants read access to PAM accounts and resources",
permissions: [
{
subject: ProjectPermissionSub.PamFolders,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.PamResources,
actions: [ProjectPermissionActions.Read]
},
{
subject: ProjectPermissionSub.PamAccounts,
actions: [ProjectPermissionPamAccountActions.Read]
}
]
},
{
id: "pam-accessor",
name: "PAM Accessing Policies",
description: "Grants the right to access all PAM accounts",
permissions: [
{
subject: ProjectPermissionSub.PamAccounts,
actions: [
ProjectPermissionPamAccountActions.Access,
ProjectPermissionPamAccountActions.Read
]
}
]
},
{
id: "pam-editor",
name: "PAM Editing Policies",
description: "Grants read and edit access to PAM accounts and resources",
permissions: [
{
subject: ProjectPermissionSub.PamFolders,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.PamResources,
actions: Object.values(ProjectPermissionActions)
},
{
subject: ProjectPermissionSub.PamAccounts,
actions: [
ProjectPermissionPamAccountActions.Read,
ProjectPermissionPamAccountActions.Edit,
ProjectPermissionPamAccountActions.Create,
ProjectPermissionPamAccountActions.Delete
]
}
]
},
projectManagerTemplate()
]
};

View File

@@ -21,6 +21,7 @@ import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionCond
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
import { PamAccountPermissionConditions } from "./PamAccountPermissionConditions";
import { PermissionEmptyState } from "./PermissionEmptyState";
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
import { PkiSyncPermissionConditions } from "./PkiSyncPermissionConditions";
@@ -87,6 +88,10 @@ export const renderConditionalComponents = (
return <AppConnectionPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.PamAccounts) {
return <PamAccountPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
}

View File

@@ -0,0 +1,33 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { ProjectAccessControlTabs } from "@app/types/project";
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug"
)({
component: RoleDetailsBySlugPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-management/$projectId/access-management",
params: {
projectId: params.projectId
},
search: {
selectedTab: ProjectAccessControlTabs.Roles
}
})
},
{
label: "Roles"
}
]
};
}
});

View File

@@ -72,7 +72,10 @@ import {
useMoveSecrets,
useUpdateSecretBatch
} from "@app/hooks/api";
import { dashboardKeys, fetchDashboardProjectSecretsByKeys } from "@app/hooks/api/dashboard/queries";
import {
dashboardKeys,
fetchDashboardProjectSecretsByKeys
} from "@app/hooks/api/dashboard/queries";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { PendingAction } from "@app/hooks/api/secretFolders/types";

View File

@@ -67,6 +67,7 @@ import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from
import { Route as sshLayoutImport } from './pages/ssh/layout'
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
import { Route as secretManagerLayoutImport } from './pages/secret-manager/layout'
import { Route as pamLayoutImport } from './pages/pam/layout'
import { Route as kmsLayoutImport } from './pages/kms/layout'
import { Route as certManagerLayoutImport } from './pages/cert-manager/layout'
import { Route as secretManagerIntegrationsRouteVercelOauthRedirectImport } from './pages/secret-manager/integrations/route-vercel-oauth-redirect'
@@ -87,6 +88,8 @@ import { Route as projectAccessControlPageRouteSecretScanningImport } from './pa
import { Route as projectAuditLogsPageRouteSecretManagerImport } from './pages/project/AuditLogsPage/route-secret-manager'
import { Route as projectAppConnectionsPageRouteSecretManagerImport } from './pages/project/AppConnectionsPage/route-secret-manager'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
import { Route as projectAuditLogsPageRoutePamImport } from './pages/project/AuditLogsPage/route-pam'
import { Route as projectAccessControlPageRoutePamImport } from './pages/project/AccessControlPage/route-pam'
import { Route as projectAuditLogsPageRouteKmsImport } from './pages/project/AuditLogsPage/route-kms'
import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms'
import { Route as projectAuditLogsPageRouteCertManagerImport } from './pages/project/AuditLogsPage/route-cert-manager'
@@ -103,6 +106,9 @@ import { Route as secretManagerSecretRotationPageRouteImport } from './pages/sec
import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route'
import { Route as secretManagerSecretApprovalsPageRouteImport } from './pages/secret-manager/SecretApprovalsPage/route'
import { Route as secretManagerIPAllowlistPageRouteImport } from './pages/secret-manager/IPAllowlistPage/route'
import { Route as pamSettingsPageRouteImport } from './pages/pam/SettingsPage/route'
import { Route as pamPamResourcesPageRouteImport } from './pages/pam/PamResourcesPage/route'
import { Route as pamPamAccountsPageRouteImport } from './pages/pam/PamAccountsPage/route'
import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/route'
import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route'
import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route'
@@ -123,6 +129,10 @@ import { Route as projectRoleDetailsBySlugPageRouteSecretManagerImport } from '.
import { Route as projectMemberDetailsByIDPageRouteSecretManagerImport } from './pages/project/MemberDetailsByIDPage/route-secret-manager'
import { Route as projectIdentityDetailsByIDPageRouteSecretManagerImport } from './pages/project/IdentityDetailsByIDPage/route-secret-manager'
import { Route as projectGroupDetailsByIDPageRouteSecretManagerImport } from './pages/project/GroupDetailsByIDPage/route-secret-manager'
import { Route as projectRoleDetailsBySlugPageRoutePamImport } from './pages/project/RoleDetailsBySlugPage/route-pam'
import { Route as projectMemberDetailsByIDPageRoutePamImport } from './pages/project/MemberDetailsByIDPage/route-pam'
import { Route as projectIdentityDetailsByIDPageRoutePamImport } from './pages/project/IdentityDetailsByIDPage/route-pam'
import { Route as projectGroupDetailsByIDPageRoutePamImport } from './pages/project/GroupDetailsByIDPage/route-pam'
import { Route as projectRoleDetailsBySlugPageRouteKmsImport } from './pages/project/RoleDetailsBySlugPage/route-kms'
import { Route as projectMemberDetailsByIDPageRouteKmsImport } from './pages/project/MemberDetailsByIDPage/route-kms'
import { Route as projectIdentityDetailsByIDPageRouteKmsImport } from './pages/project/IdentityDetailsByIDPage/route-kms'
@@ -137,11 +147,13 @@ import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/
import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route'
import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route'
import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route'
import { Route as pamPamSessionsByIDPageRouteImport } from './pages/pam/PamSessionsByIDPage/route'
import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSubscriberDetailsByIDPage/route'
import { Route as certManagerPkiSyncDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSyncDetailsByIDPage/route'
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
import { Route as secretScanningSecretScanningDataSourcesPageRouteImport } from './pages/secret-scanning/SecretScanningDataSourcesPage/route'
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route'
import { Route as pamPamSessionsPageRouteImport } from './pages/pam/PamSessionsPage/route'
import { Route as certManagerPkiSubscribersPageRouteImport } from './pages/cert-manager/PkiSubscribersPage/route'
import { Route as certManagerIntegrationsListPageRouteImport } from './pages/cert-manager/IntegrationsListPage/route'
import { Route as certManagerPkiTemplateListPageRouteImport } from './pages/cert-manager/PkiTemplateListPage/route'
@@ -277,6 +289,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdImpo
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId',
@@ -293,6 +309,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecr
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations',
)()
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions',
)()
const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers',
@@ -692,6 +712,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRout
} as any,
)
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport.update({
id: '/projects/pam/$projectId',
path: '/projects/pam/$projectId',
getParentRoute: () => organizationLayoutRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport.update({
id: '/projects/kms/$projectId',
@@ -805,6 +832,12 @@ const secretManagerLayoutRoute = secretManagerLayoutImport.update({
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRoute,
} as any)
const pamLayoutRoute = pamLayoutImport.update({
id: '/_pam-layout',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute,
} as any)
const kmsLayoutRoute = kmsLayoutImport.update({
id: '/_kms-layout',
getParentRoute: () =>
@@ -973,6 +1006,29 @@ const projectAccessControlPageRouteSecretManagerRoute =
getParentRoute: () => secretManagerLayoutRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport.update(
{
id: '/sessions',
path: '/sessions',
getParentRoute: () => pamLayoutRoute,
} as any,
)
const projectAuditLogsPageRoutePamRoute =
projectAuditLogsPageRoutePamImport.update({
id: '/audit-logs',
path: '/audit-logs',
getParentRoute: () => pamLayoutRoute,
} as any)
const projectAccessControlPageRoutePamRoute =
projectAccessControlPageRoutePamImport.update({
id: '/access-management',
path: '/access-management',
getParentRoute: () => pamLayoutRoute,
} as any)
const projectAuditLogsPageRouteKmsRoute =
projectAuditLogsPageRouteKmsImport.update({
id: '/audit-logs',
@@ -1108,6 +1164,24 @@ const secretManagerIPAllowlistPageRouteRoute =
getParentRoute: () => secretManagerLayoutRoute,
} as any)
const pamSettingsPageRouteRoute = pamSettingsPageRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => pamLayoutRoute,
} as any)
const pamPamResourcesPageRouteRoute = pamPamResourcesPageRouteImport.update({
id: '/resources',
path: '/resources',
getParentRoute: () => pamLayoutRoute,
} as any)
const pamPamAccountsPageRouteRoute = pamPamAccountsPageRouteImport.update({
id: '/accounts',
path: '/accounts',
getParentRoute: () => pamLayoutRoute,
} as any)
const kmsSettingsPageRouteRoute = kmsSettingsPageRouteImport.update({
id: '/settings',
path: '/settings',
@@ -1246,6 +1320,34 @@ const projectGroupDetailsByIDPageRouteSecretManagerRoute =
getParentRoute: () => secretManagerLayoutRoute,
} as any)
const projectRoleDetailsBySlugPageRoutePamRoute =
projectRoleDetailsBySlugPageRoutePamImport.update({
id: '/roles/$roleSlug',
path: '/roles/$roleSlug',
getParentRoute: () => pamLayoutRoute,
} as any)
const projectMemberDetailsByIDPageRoutePamRoute =
projectMemberDetailsByIDPageRoutePamImport.update({
id: '/members/$membershipId',
path: '/members/$membershipId',
getParentRoute: () => pamLayoutRoute,
} as any)
const projectIdentityDetailsByIDPageRoutePamRoute =
projectIdentityDetailsByIDPageRoutePamImport.update({
id: '/identities/$identityId',
path: '/identities/$identityId',
getParentRoute: () => pamLayoutRoute,
} as any)
const projectGroupDetailsByIDPageRoutePamRoute =
projectGroupDetailsByIDPageRoutePamImport.update({
id: '/groups/$groupId',
path: '/groups/$groupId',
getParentRoute: () => pamLayoutRoute,
} as any)
const projectRoleDetailsBySlugPageRouteKmsRoute =
projectRoleDetailsBySlugPageRouteKmsImport.update({
id: '/roles/$roleSlug',
@@ -1345,6 +1447,14 @@ const secretManagerIntegrationsDetailsByIDPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute,
} as any)
const pamPamSessionsByIDPageRouteRoute =
pamPamSessionsByIDPageRouteImport.update({
id: '/$sessionId',
path: '/$sessionId',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute,
} as any)
const certManagerPkiSubscriberDetailsByIDPageRouteRoute =
certManagerPkiSubscriberDetailsByIDPageRouteImport.update({
id: '/$subscriberName',
@@ -1384,6 +1494,13 @@ const secretManagerIntegrationsListPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute,
} as any)
const pamPamSessionsPageRouteRoute = pamPamSessionsPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute,
} as any)
const certManagerPkiSubscribersPageRouteRoute =
certManagerPkiSubscribersPageRouteImport.update({
id: '/',
@@ -2512,6 +2629,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport
parentRoute: typeof organizationLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId'
path: '/projects/pam/$projectId'
fullPath: '/projects/pam/$projectId'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport
parentRoute: typeof organizationLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId'
path: '/projects/secret-management/$projectId'
@@ -2624,6 +2748,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof kmsLayoutImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout'
path: ''
fullPath: '/projects/pam/$projectId'
preLoaderRoute: typeof pamLayoutImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout'
path: ''
@@ -2701,6 +2832,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof kmsSettingsPageRouteImport
parentRoute: typeof kmsLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts'
path: '/accounts'
fullPath: '/projects/pam/$projectId/accounts'
preLoaderRoute: typeof pamPamAccountsPageRouteImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources'
path: '/resources'
fullPath: '/projects/pam/$projectId/resources'
preLoaderRoute: typeof pamPamResourcesPageRouteImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings'
path: '/settings'
fullPath: '/projects/pam/$projectId/settings'
preLoaderRoute: typeof pamSettingsPageRouteImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist'
path: '/allowlist'
@@ -2834,6 +2986,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAuditLogsPageRouteKmsImport
parentRoute: typeof kmsLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management'
path: '/access-management'
fullPath: '/projects/pam/$projectId/access-management'
preLoaderRoute: typeof projectAccessControlPageRoutePamImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs'
path: '/audit-logs'
fullPath: '/projects/pam/$projectId/audit-logs'
preLoaderRoute: typeof projectAuditLogsPageRoutePamImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions'
path: '/sessions'
fullPath: '/projects/pam/$projectId/sessions'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management'
path: '/access-management'
@@ -2925,6 +3098,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof certManagerPkiSubscribersPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/'
path: '/'
fullPath: '/projects/pam/$projectId/sessions/'
preLoaderRoute: typeof pamPamSessionsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/'
path: '/'
@@ -2960,6 +3140,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId'
path: '/$sessionId'
fullPath: '/projects/pam/$projectId/sessions/$sessionId'
preLoaderRoute: typeof pamPamSessionsByIDPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId'
path: '/$integrationId'
@@ -3058,6 +3245,34 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteKmsImport
parentRoute: typeof kmsLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId'
path: '/groups/$groupId'
fullPath: '/projects/pam/$projectId/groups/$groupId'
preLoaderRoute: typeof projectGroupDetailsByIDPageRoutePamImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId'
path: '/identities/$identityId'
fullPath: '/projects/pam/$projectId/identities/$identityId'
preLoaderRoute: typeof projectIdentityDetailsByIDPageRoutePamImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId'
path: '/members/$membershipId'
fullPath: '/projects/pam/$projectId/members/$membershipId'
preLoaderRoute: typeof projectMemberDetailsByIDPageRoutePamImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug'
path: '/roles/$roleSlug'
fullPath: '/projects/pam/$projectId/roles/$roleSlug'
preLoaderRoute: typeof projectRoleDetailsBySlugPageRoutePamImport
parentRoute: typeof pamLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId'
path: '/groups/$groupId'
@@ -4044,6 +4259,71 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren {
pamPamSessionsPageRouteRoute: typeof pamPamSessionsPageRouteRoute
pamPamSessionsByIDPageRouteRoute: typeof pamPamSessionsByIDPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren =
{
pamPamSessionsPageRouteRoute: pamPamSessionsPageRouteRoute,
pamPamSessionsByIDPageRouteRoute: pamPamSessionsByIDPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteChildren,
)
interface pamLayoutRouteChildren {
pamPamAccountsPageRouteRoute: typeof pamPamAccountsPageRouteRoute
pamPamResourcesPageRouteRoute: typeof pamPamResourcesPageRouteRoute
pamSettingsPageRouteRoute: typeof pamSettingsPageRouteRoute
projectAccessControlPageRoutePamRoute: typeof projectAccessControlPageRoutePamRoute
projectAuditLogsPageRoutePamRoute: typeof projectAuditLogsPageRoutePamRoute
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren
projectGroupDetailsByIDPageRoutePamRoute: typeof projectGroupDetailsByIDPageRoutePamRoute
projectIdentityDetailsByIDPageRoutePamRoute: typeof projectIdentityDetailsByIDPageRoutePamRoute
projectMemberDetailsByIDPageRoutePamRoute: typeof projectMemberDetailsByIDPageRoutePamRoute
projectRoleDetailsBySlugPageRoutePamRoute: typeof projectRoleDetailsBySlugPageRoutePamRoute
}
const pamLayoutRouteChildren: pamLayoutRouteChildren = {
pamPamAccountsPageRouteRoute: pamPamAccountsPageRouteRoute,
pamPamResourcesPageRouteRoute: pamPamResourcesPageRouteRoute,
pamSettingsPageRouteRoute: pamSettingsPageRouteRoute,
projectAccessControlPageRoutePamRoute: projectAccessControlPageRoutePamRoute,
projectAuditLogsPageRoutePamRoute: projectAuditLogsPageRoutePamRoute,
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren,
projectGroupDetailsByIDPageRoutePamRoute:
projectGroupDetailsByIDPageRoutePamRoute,
projectIdentityDetailsByIDPageRoutePamRoute:
projectIdentityDetailsByIDPageRoutePamRoute,
projectMemberDetailsByIDPageRoutePamRoute:
projectMemberDetailsByIDPageRoutePamRoute,
projectRoleDetailsBySlugPageRoutePamRoute:
projectRoleDetailsBySlugPageRoutePamRoute,
}
const pamLayoutRouteWithChildren = pamLayoutRoute._addFileChildren(
pamLayoutRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren {
pamLayoutRoute: typeof pamLayoutRouteWithChildren
}
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren =
{
pamLayoutRoute: pamLayoutRouteWithChildren,
}
const AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteChildren {
secretManagerIntegrationsListPageRouteRoute: typeof secretManagerIntegrationsListPageRouteRoute
secretManagerIntegrationsDetailsByIDPageRouteRoute: typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
@@ -4520,6 +4800,7 @@ interface organizationLayoutRouteChildren {
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsSshProjectIdRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSshProjectIdRouteWithChildren
@@ -4536,6 +4817,8 @@ const organizationLayoutRouteChildren: organizationLayoutRouteChildren = {
AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRoute:
@@ -4781,6 +5064,7 @@ export interface FileRoutesByFullPath {
'/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute
'/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren
'/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/projects/pam/$projectId': typeof pamLayoutRouteWithChildren
'/projects/secret-management/$projectId': typeof secretManagerLayoutRouteWithChildren
'/projects/secret-scanning/$projectId': typeof secretScanningLayoutRouteWithChildren
'/projects/ssh/$projectId': typeof sshLayoutRouteWithChildren
@@ -4803,6 +5087,9 @@ export interface FileRoutesByFullPath {
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
'/projects/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
'/projects/pam/$projectId/accounts': typeof pamPamAccountsPageRouteRoute
'/projects/pam/$projectId/resources': typeof pamPamResourcesPageRouteRoute
'/projects/pam/$projectId/settings': typeof pamSettingsPageRouteRoute
'/projects/secret-management/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/projects/secret-management/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/projects/secret-management/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
@@ -4822,6 +5109,9 @@ export interface FileRoutesByFullPath {
'/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/projects/pam/$projectId/access-management': typeof projectAccessControlPageRoutePamRoute
'/projects/pam/$projectId/audit-logs': typeof projectAuditLogsPageRoutePamRoute
'/projects/pam/$projectId/sessions': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
@@ -4835,11 +5125,13 @@ export interface FileRoutesByFullPath {
'/projects/cert-management/$projectId/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
'/projects/cert-management/$projectId/integrations/': typeof certManagerIntegrationsListPageRouteRoute
'/projects/cert-management/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
'/projects/pam/$projectId/sessions/': typeof pamPamSessionsPageRouteRoute
'/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
'/projects/secret-scanning/$projectId/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
'/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
'/projects/pam/$projectId/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
'/projects/secret-management/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
@@ -4854,6 +5146,10 @@ export interface FileRoutesByFullPath {
'/projects/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute
'/projects/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute
'/projects/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute
'/projects/pam/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRoutePamRoute
'/projects/pam/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRoutePamRoute
'/projects/pam/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRoutePamRoute
'/projects/pam/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRoutePamRoute
'/projects/secret-management/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
@@ -5000,6 +5296,7 @@ export interface FileRoutesByTo {
'/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute
'/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren
'/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/projects/pam/$projectId': typeof pamLayoutRouteWithChildren
'/projects/secret-management/$projectId': typeof secretManagerLayoutRouteWithChildren
'/projects/secret-scanning/$projectId': typeof secretScanningLayoutRouteWithChildren
'/projects/ssh/$projectId': typeof sshLayoutRouteWithChildren
@@ -5022,6 +5319,9 @@ export interface FileRoutesByTo {
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
'/projects/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute
'/projects/pam/$projectId/accounts': typeof pamPamAccountsPageRouteRoute
'/projects/pam/$projectId/resources': typeof pamPamResourcesPageRouteRoute
'/projects/pam/$projectId/settings': typeof pamSettingsPageRouteRoute
'/projects/secret-management/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/projects/secret-management/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/projects/secret-management/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
@@ -5038,6 +5338,8 @@ export interface FileRoutesByTo {
'/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute
'/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/projects/pam/$projectId/access-management': typeof projectAccessControlPageRoutePamRoute
'/projects/pam/$projectId/audit-logs': typeof projectAuditLogsPageRoutePamRoute
'/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
@@ -5049,11 +5351,13 @@ export interface FileRoutesByTo {
'/projects/cert-management/$projectId/certificate-templates': typeof certManagerPkiTemplateListPageRouteRoute
'/projects/cert-management/$projectId/integrations': typeof certManagerIntegrationsListPageRouteRoute
'/projects/cert-management/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute
'/projects/pam/$projectId/sessions': typeof pamPamSessionsPageRouteRoute
'/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
'/projects/secret-scanning/$projectId/data-sources': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
'/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
'/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
'/projects/pam/$projectId/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
'/projects/secret-management/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
'/projects/secret-management/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
@@ -5068,6 +5372,10 @@ export interface FileRoutesByTo {
'/projects/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute
'/projects/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute
'/projects/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute
'/projects/pam/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRoutePamRoute
'/projects/pam/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRoutePamRoute
'/projects/pam/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRoutePamRoute
'/projects/pam/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRoutePamRoute
'/projects/secret-management/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
'/projects/secret-management/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
@@ -5226,6 +5534,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': typeof adminResourceOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSshProjectIdRouteWithChildren
@@ -5242,6 +5551,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsRouteVercelOauthRedirectRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout': typeof certManagerLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout': typeof kmsLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout': typeof pamLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout': typeof secretManagerLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout': typeof secretScanningLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren
@@ -5253,6 +5563,9 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings': typeof kmsSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts': typeof pamPamAccountsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources': typeof pamPamResourcesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings': typeof pamSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute
@@ -5272,6 +5585,9 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management': typeof projectAccessControlPageRoutePamRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs': typeof projectAuditLogsPageRoutePamRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsPamProjectIdPamLayoutSessionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections': typeof projectAppConnectionsPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute
@@ -5285,11 +5601,13 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/': typeof certManagerIntegrationsListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/': typeof pamPamSessionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
@@ -5304,6 +5622,10 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRoutePamRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRoutePamRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRoutePamRoute
'/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRoutePamRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
@@ -5460,6 +5782,7 @@ export interface FileRouteTypes {
| '/admin/resources/overview'
| '/projects/cert-management/$projectId'
| '/projects/kms/$projectId'
| '/projects/pam/$projectId'
| '/projects/secret-management/$projectId'
| '/projects/secret-scanning/$projectId'
| '/projects/ssh/$projectId'
@@ -5482,6 +5805,9 @@ export interface FileRouteTypes {
| '/projects/kms/$projectId/kmip'
| '/projects/kms/$projectId/overview'
| '/projects/kms/$projectId/settings'
| '/projects/pam/$projectId/accounts'
| '/projects/pam/$projectId/resources'
| '/projects/pam/$projectId/settings'
| '/projects/secret-management/$projectId/allowlist'
| '/projects/secret-management/$projectId/approval'
| '/projects/secret-management/$projectId/overview'
@@ -5501,6 +5827,9 @@ export interface FileRouteTypes {
| '/projects/cert-management/$projectId/subscribers'
| '/projects/kms/$projectId/access-management'
| '/projects/kms/$projectId/audit-logs'
| '/projects/pam/$projectId/access-management'
| '/projects/pam/$projectId/audit-logs'
| '/projects/pam/$projectId/sessions'
| '/projects/secret-management/$projectId/access-management'
| '/projects/secret-management/$projectId/app-connections'
| '/projects/secret-management/$projectId/audit-logs'
@@ -5514,11 +5843,13 @@ export interface FileRouteTypes {
| '/projects/cert-management/$projectId/certificate-templates/'
| '/projects/cert-management/$projectId/integrations/'
| '/projects/cert-management/$projectId/subscribers/'
| '/projects/pam/$projectId/sessions/'
| '/projects/secret-management/$projectId/integrations/'
| '/projects/secret-scanning/$projectId/data-sources/'
| '/projects/cert-management/$projectId/ca/$caName'
| '/projects/cert-management/$projectId/integrations/$syncId'
| '/projects/cert-management/$projectId/subscribers/$subscriberName'
| '/projects/pam/$projectId/sessions/$sessionId'
| '/projects/secret-management/$projectId/integrations/$integrationId'
| '/projects/secret-management/$projectId/integrations/select-integration-auth'
| '/projects/secret-management/$projectId/secrets/$envSlug'
@@ -5533,6 +5864,10 @@ export interface FileRouteTypes {
| '/projects/kms/$projectId/identities/$identityId'
| '/projects/kms/$projectId/members/$membershipId'
| '/projects/kms/$projectId/roles/$roleSlug'
| '/projects/pam/$projectId/groups/$groupId'
| '/projects/pam/$projectId/identities/$identityId'
| '/projects/pam/$projectId/members/$membershipId'
| '/projects/pam/$projectId/roles/$roleSlug'
| '/projects/secret-management/$projectId/groups/$groupId'
| '/projects/secret-management/$projectId/identities/$identityId'
| '/projects/secret-management/$projectId/members/$membershipId'
@@ -5678,6 +6013,7 @@ export interface FileRouteTypes {
| '/admin/resources/overview'
| '/projects/cert-management/$projectId'
| '/projects/kms/$projectId'
| '/projects/pam/$projectId'
| '/projects/secret-management/$projectId'
| '/projects/secret-scanning/$projectId'
| '/projects/ssh/$projectId'
@@ -5700,6 +6036,9 @@ export interface FileRouteTypes {
| '/projects/kms/$projectId/kmip'
| '/projects/kms/$projectId/overview'
| '/projects/kms/$projectId/settings'
| '/projects/pam/$projectId/accounts'
| '/projects/pam/$projectId/resources'
| '/projects/pam/$projectId/settings'
| '/projects/secret-management/$projectId/allowlist'
| '/projects/secret-management/$projectId/approval'
| '/projects/secret-management/$projectId/overview'
@@ -5716,6 +6055,8 @@ export interface FileRouteTypes {
| '/projects/cert-management/$projectId/audit-logs'
| '/projects/kms/$projectId/access-management'
| '/projects/kms/$projectId/audit-logs'
| '/projects/pam/$projectId/access-management'
| '/projects/pam/$projectId/audit-logs'
| '/projects/secret-management/$projectId/access-management'
| '/projects/secret-management/$projectId/app-connections'
| '/projects/secret-management/$projectId/audit-logs'
@@ -5727,11 +6068,13 @@ export interface FileRouteTypes {
| '/projects/cert-management/$projectId/certificate-templates'
| '/projects/cert-management/$projectId/integrations'
| '/projects/cert-management/$projectId/subscribers'
| '/projects/pam/$projectId/sessions'
| '/projects/secret-management/$projectId/integrations'
| '/projects/secret-scanning/$projectId/data-sources'
| '/projects/cert-management/$projectId/ca/$caName'
| '/projects/cert-management/$projectId/integrations/$syncId'
| '/projects/cert-management/$projectId/subscribers/$subscriberName'
| '/projects/pam/$projectId/sessions/$sessionId'
| '/projects/secret-management/$projectId/integrations/$integrationId'
| '/projects/secret-management/$projectId/integrations/select-integration-auth'
| '/projects/secret-management/$projectId/secrets/$envSlug'
@@ -5746,6 +6089,10 @@ export interface FileRouteTypes {
| '/projects/kms/$projectId/identities/$identityId'
| '/projects/kms/$projectId/members/$membershipId'
| '/projects/kms/$projectId/roles/$roleSlug'
| '/projects/pam/$projectId/groups/$groupId'
| '/projects/pam/$projectId/identities/$identityId'
| '/projects/pam/$projectId/members/$membershipId'
| '/projects/pam/$projectId/roles/$roleSlug'
| '/projects/secret-management/$projectId/groups/$groupId'
| '/projects/secret-management/$projectId/identities/$identityId'
| '/projects/secret-management/$projectId/members/$membershipId'
@@ -5902,6 +6249,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId'
@@ -5918,6 +6266,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/integrations/vercel/oauth2/callback'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout'
| '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout'
@@ -5929,6 +6278,9 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview'
@@ -5948,6 +6300,9 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs'
@@ -5961,11 +6316,13 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId'
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug'
@@ -5980,6 +6337,10 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId'
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId'
| '/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/identities/$identityId'
| '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/members/$membershipId'
@@ -6260,6 +6621,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId",
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId",
"/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId"
@@ -6474,6 +6836,13 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
]
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
"children": [
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
]
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@@ -6576,6 +6945,22 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug"
]
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout": {
"filePath": "pam/layout.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId",
"children": [
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug"
]
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout": {
"filePath": "secret-manager/layout.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId",
@@ -6663,6 +7048,18 @@ export const routeTree = rootRoute
"filePath": "kms/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts": {
"filePath": "pam/PamAccountsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources": {
"filePath": "pam/PamResourcesPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings": {
"filePath": "pam/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/allowlist": {
"filePath": "secret-manager/IPAllowlistPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
@@ -6750,6 +7147,22 @@ export const routeTree = rootRoute
"filePath": "project/AuditLogsPage/route-kms.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/access-management": {
"filePath": "project/AccessControlPage/route-pam.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/audit-logs": {
"filePath": "project/AuditLogsPage/route-pam.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout",
"children": [
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
]
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management": {
"filePath": "project/AccessControlPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"
@@ -6886,6 +7299,10 @@ export const routeTree = rootRoute
"filePath": "cert-manager/PkiSubscribersPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/": {
"filePath": "pam/PamSessionsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/": {
"filePath": "secret-manager/IntegrationsListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations"
@@ -6906,6 +7323,10 @@ export const routeTree = rootRoute
"filePath": "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId": {
"filePath": "pam/PamSessionsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId": {
"filePath": "secret-manager/IntegrationsDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations"
@@ -6962,6 +7383,22 @@ export const routeTree = rootRoute
"filePath": "project/RoleDetailsBySlugPage/route-kms.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/groups/$groupId": {
"filePath": "project/GroupDetailsByIDPage/route-pam.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId": {
"filePath": "project/IdentityDetailsByIDPage/route-pam.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId": {
"filePath": "project/MemberDetailsByIDPage/route-pam.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug": {
"filePath": "project/RoleDetailsBySlugPage/route-pam.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId": {
"filePath": "project/GroupDetailsByIDPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout"

View File

@@ -375,6 +375,26 @@ const secretScanningRoutes = route("/projects/secret-scanning/$projectId", [
])
]);
const pamRoutes = route("/projects/pam/$projectId", [
layout("pam-layout", "pam/layout.tsx", [
route("/accounts", "pam/PamAccountsPage/route.tsx"),
route("/sessions", [
index("pam/PamSessionsPage/route.tsx"),
route("/$sessionId", "pam/PamSessionsByIDPage/route.tsx")
]),
route("/resources", "pam/PamResourcesPage/route.tsx"),
route("/audit-logs", "project/AuditLogsPage/route-pam.tsx"),
route("/settings", "pam/SettingsPage/route.tsx"),
// Access Management
route("/access-management", "project/AccessControlPage/route-pam.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-pam.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-pam.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-pam.tsx"),
route("/groups/$groupId", "project/GroupDetailsByIDPage/route-pam.tsx")
])
]);
export const routes = rootRoute("root.tsx", [
index("index.tsx"),
route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"),
@@ -420,7 +440,8 @@ export const routes = rootRoute("root.tsx", [
certManagerRoutes,
kmsRoutes,
sshRoutes,
secretScanningRoutes
secretScanningRoutes,
pamRoutes
])
])
])