diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 0b7f89b6c..74bd5eeb1 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -106,7 +106,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { schema: { description: "Access PAM account", body: z.object({ - accountId: z.string().uuid(), + accountPath: z.string().trim(), + projectId: z.string().uuid(), duration: z .string() .min(1) @@ -151,7 +152,9 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { actorIp: req.realIp, actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(), actorUserAgent: req.auditLogInfo.userAgent ?? "", - ...req.body + accountPath: req.body.accountPath, + projectId: req.body.projectId, + duration: req.body.duration }, req.permission ); @@ -163,7 +166,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { event: { type: EventType.PAM_ACCOUNT_ACCESS, metadata: { - accountId: req.body.accountId, + accountId: response.account.id, + accountPath: req.body.accountPath, accountName: response.account.name, duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined } diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 36b49a37c..504339d18 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -4076,6 +4076,7 @@ interface PamAccountAccessEvent { type: EventType.PAM_ACCOUNT_ACCESS; metadata: { accountId: string; + accountPath: string; accountName: string; duration?: string; }; diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 1eae8df15..019df00ec 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -487,7 +487,7 @@ export const pamAccountServiceFactory = ({ }; const access = async ( - { accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO, + { accountPath, projectId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO, actor: OrgServiceActor ) => { const orgLicensePlan = await licenseService.getPlan(actor.orgId); @@ -497,8 +497,36 @@ export const pamAccountServiceFactory = ({ }); } - const account = await pamAccountDAL.findById(accountId); - if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` }); + const pathSegments: string[] = accountPath.split("/").filter(Boolean); + 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); if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); @@ -508,22 +536,16 @@ export const pamAccountServiceFactory = ({ actorAuthMethod: actor.authMethod, actorId: actor.id, actorOrgId: actor.orgId, - projectId: account.projectId, + projectId, actionProjectType: ActionProjectType.PAM }); - const accountPath = await getFullPamFolderPath({ - pamFolderDAL, - folderId: account.folderId, - projectId: account.projectId - }); - ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionPamAccountActions.Access, subject(ProjectPermissionSub.PamAccounts, { resourceName: resource.name, accountName: account.name, - accountPath + accountPath: folderPath }) ); @@ -533,7 +555,7 @@ export const pamAccountServiceFactory = ({ actorIp, actorName, actorUserAgent, - projectId: account.projectId, + projectId, resourceName: resource.name, resourceType: resource.resourceType, status: PamSessionStatus.Starting, @@ -542,11 +564,7 @@ export const pamAccountServiceFactory = ({ expiresAt: new Date(Date.now() + duration) }); - const { connectionDetails, gatewayId, resourceType } = await decryptResource( - resource, - account.projectId, - kmsService - ); + const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, projectId, kmsService); const user = await userDAL.findById(actor.id); if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` }); @@ -578,20 +596,20 @@ export const pamAccountServiceFactory = ({ const connectionCredentials = (await decryptResourceConnectionDetails({ encryptedConnectionDetails: resource.encryptedConnectionDetails, kmsService, - projectId: account.projectId + projectId })) as TSqlResourceConnectionDetails; const credentials = await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, kmsService, - projectId: account.projectId + projectId }); metadata = { username: credentials.username, database: connectionCredentials.database, accountName: account.name, - accountPath + accountPath: folderPath }; } break; @@ -600,7 +618,7 @@ export const pamAccountServiceFactory = ({ const credentials = await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, kmsService, - projectId: account.projectId + projectId }); metadata = { @@ -622,7 +640,7 @@ export const pamAccountServiceFactory = ({ gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey, gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain, relayHost: gatewayConnectionDetails.relayHost, - projectId: account.projectId, + projectId, account, metadata }; diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index b8498036e..ac799d869 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -14,7 +14,8 @@ export type TUpdateAccountDTO = Partial 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 portSuffix = port && port !== "80" && port !== "443" ? `:${port}` : ""; const siteURL = `${protocol}//${hostname}${portSuffix}`; @@ -68,9 +83,9 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) switch (account.resource.resourceType) { case PamResourceType.Postgres: 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: - 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: return ""; } diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx index d45f89a90..1dd5e7f30 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountsTable.tsx @@ -466,6 +466,12 @@ export const PamAccountsTable = ({ projectId }: Props) => { isOpen={popUp.accessAccount.isOpen} onOpenChange={(isOpen) => handlePopUpToggle("accessAccount", isOpen)} account={popUp.accessAccount.data} + accountPath={ + popUp.accessAccount.data?.folderId + ? folderPaths[popUp.accessAccount.data.folderId] + : undefined + } + projectId={projectId} />