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
+23
View File
@@ -23,6 +23,11 @@ import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOidcRouter } from "./oidc-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerPamAccountRouter } from "./pam-account-router";
import { registerPamFolderRouter } from "./pam-folder-router";
import { PAM_RESOURCE_REGISTER_ROUTER_MAP } from "./pam-resource-routers";
import { registerPamResourceRouter } from "./pam-resource-routers/pam-resource-router";
import { registerPamSessionRouter } from "./pam-session-router";
import { registerPITRouter } from "./pit-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
@@ -166,4 +171,22 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
},
{ prefix: "/kmip" }
);
await server.register(registerPamFolderRouter, { prefix: "/pam/folders" });
await server.register(registerPamAccountRouter, { prefix: "/pam/accounts" });
await server.register(registerPamSessionRouter, { prefix: "/pam/sessions" });
await server.register(
async (pamResourceRouter) => {
await pamResourceRouter.register(registerPamResourceRouter);
// Provider-specific endpoints
await Promise.all(
Object.entries(PAM_RESOURCE_REGISTER_ROUTER_MAP).map(([provider, router]) =>
pamResourceRouter.register(router, { prefix: `/${provider}` })
)
);
},
{ prefix: "/pam/resources" }
);
};
@@ -0,0 +1,131 @@
import { z } from "zod";
import { PamFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
// Use z.union([...]) when more resources are added
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema;
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List PAM accounts",
querystring: z.object({
projectId: z.string().uuid()
}),
response: {
200: z.object({
accounts: SanitizedAccountSchema.array(),
folders: PamFoldersSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.pamResource.listAccounts(req.query.projectId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.query.projectId,
event: {
type: EventType.PAM_ACCOUNT_LIST,
metadata: {
accountCount: response.accounts.length,
folderCount: response.folders.length
}
}
});
return response;
}
});
server.route({
method: "POST",
url: "/:accountId/access",
config: {
rateLimit: writeLimit
},
schema: {
description: "Access PAM account",
params: z.object({
accountId: z.string().uuid()
}),
body: z.object({
duration: z
.string()
.nullable()
.optional()
.transform((val, ctx) => {
if (val === undefined) return undefined;
if (!val || val === "permanent") return null;
const parsedMs = ms(val);
if (typeof parsedMs !== "number" || parsedMs <= 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid duration format. Must be a positive duration (e.g., '1h', '30m', '2d')."
});
return z.NEVER;
}
return parsedMs;
})
}),
response: {
200: z.object({
sessionId: z.string(),
resourceType: z.nativeEnum(PamResource),
relayCertificate: z.string(),
gatewayCertificate: z.string(),
relayHost: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
if (req.auth.authMode !== AuthMode.JWT) {
throw new BadRequestError({ message: "You can only access PAM accounts using JWT auth tokens." });
}
const response = await server.services.pamResource.accessAccount(
{
accountId: req.params.accountId,
actorEmail: req.auth.user.email ?? "",
actorIp: req.realIp,
actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
actorUserAgent: req.auditLogInfo.userAgent ?? "",
...req.body
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: response.projectId,
event: {
type: EventType.PAM_ACCOUNT_ACCESS,
metadata: {
accountId: req.params.accountId,
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
}
}
});
return response;
}
});
};
@@ -0,0 +1,149 @@
import { z } from "zod";
import { PamFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { isValidFolderName } from "@app/lib/validator";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerPamFolderRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create PAM folder",
body: z.object({
projectId: z.string().uuid(),
parentId: z.string().uuid().nullable().optional(),
name: z
.string()
.trim()
.refine((name) => isValidFolderName(name), {
message: "Folder name can only contain alphanumeric characters, dashes, and underscores."
}),
description: z.string().trim().max(512).nullable().optional()
}),
response: {
200: z.object({
folder: PamFoldersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const folder = await server.services.pamFolder.createFolder(req.body, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.body.projectId,
event: {
type: EventType.PAM_FOLDER_CREATE,
metadata: {
name: req.body.name,
description: req.body.description,
parentId: req.body.parentId
}
}
});
return { folder };
}
});
server.route({
method: "PATCH",
url: "/:folderId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update PAM folder",
params: z.object({
folderId: z.string().uuid()
}),
body: z.object({
name: z
.string()
.trim()
.optional()
.refine((name) => (name ? isValidFolderName(name) : true), {
message: "Folder name can only contain alphanumeric characters, dashes, and underscores."
}),
description: z.string().trim().max(512).nullable().optional()
}),
response: {
200: z.object({
folder: PamFoldersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const folder = await server.services.pamFolder.updateFolder(
{
...req.body,
id: req.params.folderId
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: folder.projectId,
event: {
type: EventType.PAM_FOLDER_UPDATE,
metadata: {
folderId: req.params.folderId,
name: req.body.name,
description: req.body.description
}
}
});
return { folder };
}
});
server.route({
method: "DELETE",
url: "/:folderId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete PAM folder",
params: z.object({
folderId: z.string().uuid()
}),
response: {
200: z.object({
folder: PamFoldersSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const folder = await server.services.pamFolder.deleteFolder(req.params.folderId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: folder.projectId,
event: {
type: EventType.PAM_FOLDER_DELETE,
metadata: {
folderId: req.params.folderId
}
}
});
return { folder };
}
});
};
@@ -0,0 +1,26 @@
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import {
CreatePostgresAccountSchema,
CreatePostgresResourceSchema,
PostgresResourceSchema,
SanitizedPostgresAccountWithResourceSchema,
UpdatePostgresAccountSchema,
UpdatePostgresResourceSchema
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: FastifyZodProvider) => Promise<void>> = {
[PamResource.Postgres]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.Postgres,
resourceResponseSchema: PostgresResourceSchema,
accountResponseSchema: SanitizedPostgresAccountWithResourceSchema,
createResourceSchema: CreatePostgresResourceSchema,
createAccountSchema: CreatePostgresAccountSchema,
updateResourceSchema: UpdatePostgresResourceSchema,
updateAccountSchema: UpdatePostgresAccountSchema
});
}
};
@@ -0,0 +1,351 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { TPamAccount, TPamResource } from "@app/ee/services/pam-resource/pam-resource-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerPamResourceEndpoints = <T extends TPamResource, C extends TPamAccount>({
server,
resourceType,
createResourceSchema,
updateResourceSchema,
createAccountSchema,
updateAccountSchema,
resourceResponseSchema,
accountResponseSchema
}: {
server: FastifyZodProvider;
resourceType: PamResource;
createResourceSchema: z.ZodType<{
projectId: T["projectId"];
connectionDetails: T["connectionDetails"];
gatewayId: T["gatewayId"];
name: T["name"];
}>;
createAccountSchema: z.ZodType<{
credentials: C["credentials"];
folderId?: C["folderId"];
name: C["name"];
description?: C["description"];
}>;
updateResourceSchema: z.ZodType<{
connectionDetails?: T["connectionDetails"];
gatewayId?: T["gatewayId"];
name?: T["name"];
}>;
updateAccountSchema: z.ZodType<{
credentials?: C["credentials"];
name?: C["name"];
description?: C["description"];
}>;
resourceResponseSchema: z.ZodTypeAny;
accountResponseSchema: z.ZodTypeAny;
}) => {
server.route({
method: "GET",
url: "/:resourceId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get PAM resource",
params: z.object({
resourceId: z.string().uuid()
}),
response: {
200: z.object({
resource: resourceResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const resource = await server.services.pamResource.getById(req.params.resourceId, resourceType, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: resource.projectId,
event: {
type: EventType.PAM_RESOURCE_GET,
metadata: {
resourceId: resource.id,
resourceType: resource.resourceType,
name: resource.name
}
}
});
return { resource };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create PAM resource",
body: createResourceSchema,
response: {
200: z.object({
resource: resourceResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const resource = await server.services.pamResource.create(
{
...req.body,
resourceType
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.body.projectId,
event: {
type: EventType.PAM_RESOURCE_CREATE,
metadata: {
resourceType,
gatewayId: req.body.gatewayId,
name: req.body.name
}
}
});
return { resource };
}
});
server.route({
method: "PATCH",
url: "/:resourceId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update PAM resource",
params: z.object({
resourceId: z.string().uuid()
}),
body: updateResourceSchema,
response: {
200: z.object({
resource: resourceResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const resource = await server.services.pamResource.updateById(
{
...req.body,
resourceId: req.params.resourceId
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: resource.projectId,
event: {
type: EventType.PAM_RESOURCE_UPDATE,
metadata: {
resourceId: req.params.resourceId,
resourceType,
gatewayId: req.body.gatewayId,
name: req.body.name
}
}
});
return { resource };
}
});
server.route({
method: "DELETE",
url: "/:resourceId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete PAM resource",
params: z.object({
resourceId: z.string().uuid()
}),
response: {
200: z.object({
resource: resourceResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const resource = await server.services.pamResource.deleteById(req.params.resourceId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: resource.projectId,
event: {
type: EventType.PAM_RESOURCE_DELETE,
metadata: {
resourceId: req.params.resourceId,
resourceType
}
}
});
return { resource };
}
});
// PAM Accounts
server.route({
method: "POST",
url: "/:resourceId/accounts",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create PAM resource account",
params: z.object({
resourceId: z.string().uuid()
}),
body: createAccountSchema,
response: {
200: z.object({
account: accountResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const account = await server.services.pamResource.createAccount(
{
...req.body,
resourceId: req.params.resourceId
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: account.projectId,
event: {
type: EventType.PAM_ACCOUNT_CREATE,
metadata: {
resourceId: req.params.resourceId,
resourceType,
folderId: req.body.folderId,
name: req.body.name,
description: req.body.description
}
}
});
return { account };
}
});
server.route({
method: "PATCH",
url: "/:resourceId/accounts/:accountId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update PAM resource account",
params: z.object({
resourceId: z.string().uuid(),
accountId: z.string().uuid()
}),
body: updateAccountSchema,
response: {
200: z.object({
account: accountResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const account = await server.services.pamResource.updateAccountById(
{
...req.body,
accountId: req.params.accountId
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: account.projectId,
event: {
type: EventType.PAM_ACCOUNT_UPDATE,
metadata: {
accountId: req.params.accountId,
resourceId: req.params.resourceId,
resourceType,
name: req.body.name,
description: req.body.description
}
}
});
return { account };
}
});
server.route({
method: "DELETE",
url: "/:resourceId/accounts/:accountId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete PAM resource account",
params: z.object({
resourceId: z.string().uuid(),
accountId: z.string().uuid()
}),
response: {
200: z.object({
account: accountResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const account = await server.services.pamResource.deleteAccountById(req.params.accountId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: account.projectId,
event: {
type: EventType.PAM_ACCOUNT_DELETE,
metadata: {
accountId: req.params.accountId,
resourceId: req.params.resourceId,
resourceType
}
}
});
return { account };
}
});
};
@@ -0,0 +1,76 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
PostgresResourceListItemSchema,
PostgresResourceSchema
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
// Use z.union([...]) when more resources are added
const ResourceSchema = PostgresResourceSchema;
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/options",
config: {
rateLimit: readLimit
},
schema: {
description: "List PAM resource types",
response: {
200: z.object({
resourceOptions: ResourceOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: () => {
const resourceOptions = server.services.pamResource.listResourceOptions();
return { resourceOptions };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List PAM resources",
querystring: z.object({
projectId: z.string().uuid()
}),
response: {
200: z.object({
resources: ResourceSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.pamResource.list(req.query.projectId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.query.projectId,
event: {
type: EventType.PAM_RESOURCE_LIST,
metadata: {
count: response.resources.length
}
}
});
return response;
}
});
};
@@ -0,0 +1,221 @@
import { z } from "zod";
import { PamSessionsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
// Use z.union([]) once there's multiple
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
// Meant to be hit solely by gateway identities
server.route({
method: "GET",
url: "/:sessionId/credentials",
config: {
rateLimit: readLimit
},
schema: {
description: "Get PAM session credentials and start session",
params: z.object({
sessionId: z.string().uuid()
}),
response: {
200: z.object({
credentials: SessionCredentialsSchema
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { credentials, projectId } = await server.services.pamResource.getSessionCredentials(
req.params.sessionId,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId,
event: {
type: EventType.PAM_SESSION_START,
metadata: {
sessionId: req.params.sessionId
}
}
});
return { credentials };
}
});
// Meant to be hit solely by gateway identities
server.route({
method: "POST",
url: "/:sessionId/logs",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update PAM session logs",
params: z.object({
sessionId: z.string().uuid()
}),
body: z.object({
logs: PamSessionCommandLogSchema.array()
}),
response: {
200: z.object({
session: PamSessionsSchema.omit({
encryptedLogsBlob: true
})
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { session, projectId } = await server.services.pamSession.updateLogsById(
{
sessionId: req.params.sessionId,
logs: req.body.logs
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId,
event: {
type: EventType.PAM_SESSION_LOGS_UPDATE,
metadata: {
sessionId: req.params.sessionId
}
}
});
return { session };
}
});
// Meant to be hit solely by gateway identities
server.route({
method: "POST",
url: "/:sessionId/end",
config: {
rateLimit: writeLimit
},
schema: {
description: "End PAM session",
params: z.object({
sessionId: z.string().uuid()
}),
response: {
200: z.object({
session: PamSessionsSchema.omit({
encryptedLogsBlob: true
})
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { session, projectId } = await server.services.pamSession.endSessionById(
req.params.sessionId,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId,
event: {
type: EventType.PAM_SESSION_END,
metadata: {
sessionId: req.params.sessionId
}
}
});
return { session };
}
});
server.route({
method: "GET",
url: "/:sessionId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get PAM session",
params: z.object({
sessionId: z.string().uuid()
}),
response: {
200: z.object({
session: SanitizedSessionSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.pamSession.getById(req.params.sessionId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: response.session.projectId,
event: {
type: EventType.PAM_SESSION_GET,
metadata: {
sessionId: req.params.sessionId
}
}
});
return response;
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List PAM sessions",
querystring: z.object({
projectId: z.string().uuid()
}),
response: {
200: z.object({
sessions: SanitizedSessionSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.pamSession.list(req.query.projectId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.query.projectId,
event: {
type: EventType.PAM_SESSION_LIST,
metadata: {
count: response.sessions.length
}
}
});
return response;
}
});
};