Merge pull request #4970 from Infisical/chore/pam-access-account-with-path

chore: updates pam access account endpoint to use account path instead of id [PAM-64]
This commit is contained in:
Piyush Gupta
2025-12-05 20:03:33 +05:30
committed by GitHub
6 changed files with 74 additions and 29 deletions
@@ -106,7 +106,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Access PAM account", description: "Access PAM account",
body: z.object({ body: z.object({
accountId: z.string().uuid(), accountPath: z.string().trim(),
projectId: z.string().uuid(),
duration: z duration: z
.string() .string()
.min(1) .min(1)
@@ -151,7 +152,9 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
actorIp: req.realIp, actorIp: req.realIp,
actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(), actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
actorUserAgent: req.auditLogInfo.userAgent ?? "", actorUserAgent: req.auditLogInfo.userAgent ?? "",
...req.body accountPath: req.body.accountPath,
projectId: req.body.projectId,
duration: req.body.duration
}, },
req.permission req.permission
); );
@@ -163,7 +166,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
event: { event: {
type: EventType.PAM_ACCOUNT_ACCESS, type: EventType.PAM_ACCOUNT_ACCESS,
metadata: { metadata: {
accountId: req.body.accountId, accountId: response.account.id,
accountPath: req.body.accountPath,
accountName: response.account.name, accountName: response.account.name,
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
} }
@@ -4076,6 +4076,7 @@ interface PamAccountAccessEvent {
type: EventType.PAM_ACCOUNT_ACCESS; type: EventType.PAM_ACCOUNT_ACCESS;
metadata: { metadata: {
accountId: string; accountId: string;
accountPath: string;
accountName: string; accountName: string;
duration?: string; duration?: string;
}; };
@@ -487,7 +487,7 @@ export const pamAccountServiceFactory = ({
}; };
const access = async ( const access = async (
{ accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO, { accountPath, projectId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId); const orgLicensePlan = await licenseService.getPlan(actor.orgId);
@@ -497,8 +497,36 @@ export const pamAccountServiceFactory = ({
}); });
} }
const account = await pamAccountDAL.findById(accountId); const pathSegments: string[] = accountPath.split("/").filter(Boolean);
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` }); if (pathSegments.length === 0) {
throw new BadRequestError({ message: "Invalid accountPath. Path must contain at least the account name." });
}
const accountName: string = pathSegments[pathSegments.length - 1] ?? "";
const folderPathSegments: string[] = pathSegments.slice(0, -1);
const folderPath: string = folderPathSegments.length > 0 ? `/${folderPathSegments.join("/")}` : "/";
let folderId: string | null = null;
if (folderPath !== "/") {
const folder = await pamFolderDAL.findByPath(projectId, folderPath);
if (!folder) {
throw new NotFoundError({ message: `Folder at path '${folderPath}' not found` });
}
folderId = folder.id;
}
const account = await pamAccountDAL.findOne({
projectId,
folderId,
name: accountName
});
if (!account) {
throw new NotFoundError({
message: `Account with name '${accountName}' not found at path '${accountPath}'`
});
}
const resource = await pamResourceDAL.findById(account.resourceId); const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
@@ -508,22 +536,16 @@ export const pamAccountServiceFactory = ({
actorAuthMethod: actor.authMethod, actorAuthMethod: actor.authMethod,
actorId: actor.id, actorId: actor.id,
actorOrgId: actor.orgId, actorOrgId: actor.orgId,
projectId: account.projectId, projectId,
actionProjectType: ActionProjectType.PAM actionProjectType: ActionProjectType.PAM
}); });
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Access, ProjectPermissionPamAccountActions.Access,
subject(ProjectPermissionSub.PamAccounts, { subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name, resourceName: resource.name,
accountName: account.name, accountName: account.name,
accountPath accountPath: folderPath
}) })
); );
@@ -533,7 +555,7 @@ export const pamAccountServiceFactory = ({
actorIp, actorIp,
actorName, actorName,
actorUserAgent, actorUserAgent,
projectId: account.projectId, projectId,
resourceName: resource.name, resourceName: resource.name,
resourceType: resource.resourceType, resourceType: resource.resourceType,
status: PamSessionStatus.Starting, status: PamSessionStatus.Starting,
@@ -542,11 +564,7 @@ export const pamAccountServiceFactory = ({
expiresAt: new Date(Date.now() + duration) expiresAt: new Date(Date.now() + duration)
}); });
const { connectionDetails, gatewayId, resourceType } = await decryptResource( const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, projectId, kmsService);
resource,
account.projectId,
kmsService
);
const user = await userDAL.findById(actor.id); const user = await userDAL.findById(actor.id);
if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` }); if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` });
@@ -578,20 +596,20 @@ export const pamAccountServiceFactory = ({
const connectionCredentials = (await decryptResourceConnectionDetails({ const connectionCredentials = (await decryptResourceConnectionDetails({
encryptedConnectionDetails: resource.encryptedConnectionDetails, encryptedConnectionDetails: resource.encryptedConnectionDetails,
kmsService, kmsService,
projectId: account.projectId projectId
})) as TSqlResourceConnectionDetails; })) as TSqlResourceConnectionDetails;
const credentials = await decryptAccountCredentials({ const credentials = await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials, encryptedCredentials: account.encryptedCredentials,
kmsService, kmsService,
projectId: account.projectId projectId
}); });
metadata = { metadata = {
username: credentials.username, username: credentials.username,
database: connectionCredentials.database, database: connectionCredentials.database,
accountName: account.name, accountName: account.name,
accountPath accountPath: folderPath
}; };
} }
break; break;
@@ -600,7 +618,7 @@ export const pamAccountServiceFactory = ({
const credentials = await decryptAccountCredentials({ const credentials = await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials, encryptedCredentials: account.encryptedCredentials,
kmsService, kmsService,
projectId: account.projectId projectId
}); });
metadata = { metadata = {
@@ -622,7 +640,7 @@ export const pamAccountServiceFactory = ({
gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey, gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey,
gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain, gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain,
relayHost: gatewayConnectionDetails.relayHost, relayHost: gatewayConnectionDetails.relayHost,
projectId: account.projectId, projectId,
account, account,
metadata metadata
}; };
@@ -14,7 +14,8 @@ export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "re
}; };
export type TAccessAccountDTO = { export type TAccessAccountDTO = {
accountId: string; accountPath: string;
projectId: string;
actorEmail: string; actorEmail: string;
actorIp: string; actorIp: string;
actorName: string; actorName: string;
@@ -10,11 +10,26 @@ import { PamResourceType, TPamAccount } from "@app/hooks/api/pam";
type Props = { type Props = {
account?: TPamAccount; account?: TPamAccount;
accountPath?: string;
isOpen: boolean; isOpen: boolean;
onOpenChange: (isOpen: boolean) => void; onOpenChange: (isOpen: boolean) => void;
projectId: string;
}; };
export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => { export const PamAccessAccountModal = ({
isOpen,
onOpenChange,
account,
projectId,
accountPath
}: Props) => {
let fullAccountPath = account?.name;
if (accountPath) {
let path = accountPath;
if (path.startsWith("/")) path = path.slice(1);
fullAccountPath = `${path}/${account?.name}`;
}
const { protocol, hostname, port } = window.location; const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== "80" && port !== "443" ? `:${port}` : ""; const portSuffix = port && port !== "80" && port !== "443" ? `:${port}` : "";
const siteURL = `${protocol}//${hostname}${portSuffix}`; const siteURL = `${protocol}//${hostname}${portSuffix}`;
@@ -68,9 +83,9 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
switch (account.resource.resourceType) { switch (account.resource.resourceType) {
case PamResourceType.Postgres: case PamResourceType.Postgres:
case PamResourceType.MySQL: case PamResourceType.MySQL:
return `infisical pam db access-account ${account.id} --duration ${cliDuration} --domain ${siteURL}`; return `infisical pam db access-account ${fullAccountPath} --project-id ${projectId} --duration ${cliDuration} --domain ${siteURL}`;
case PamResourceType.SSH: case PamResourceType.SSH:
return `infisical pam ssh access-account ${account.id} --duration ${cliDuration} --domain ${siteURL}`; return `infisical pam ssh access-account ${fullAccountPath} --project-id ${projectId} --duration ${cliDuration} --domain ${siteURL}`;
default: default:
return ""; return "";
} }
@@ -466,6 +466,12 @@ export const PamAccountsTable = ({ projectId }: Props) => {
isOpen={popUp.accessAccount.isOpen} isOpen={popUp.accessAccount.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("accessAccount", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("accessAccount", isOpen)}
account={popUp.accessAccount.data} account={popUp.accessAccount.data}
accountPath={
popUp.accessAccount.data?.folderId
? folderPaths[popUp.accessAccount.data.folderId]
: undefined
}
projectId={projectId}
/> />
<PamDeleteAccountModal <PamDeleteAccountModal
isOpen={popUp.deleteAccount.isOpen} isOpen={popUp.deleteAccount.isOpen}