mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(pam): PAM Platform V1
This commit is contained in:
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@@ -28,6 +28,9 @@ import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
|
||||
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||
import { TPamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
|
||||
import { TPamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
||||
import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { TPitServiceFactory } from "@app/ee/services/pit/pit-service";
|
||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types";
|
||||
@@ -314,6 +317,9 @@ declare module "fastify" {
|
||||
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
|
||||
notification: TNotificationServiceFactory;
|
||||
offlineUsageReport: TOfflineUsageReportServiceFactory;
|
||||
pamFolder: TPamFolderServiceFactory;
|
||||
pamResource: TPamResourceServiceFactory;
|
||||
pamSession: TPamSessionServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -530,6 +530,10 @@ import {
|
||||
TMicrosoftTeamsIntegrationsInsert,
|
||||
TMicrosoftTeamsIntegrationsUpdate
|
||||
} from "@app/db/schemas/microsoft-teams-integrations";
|
||||
import { TPamAccounts, TPamAccountsInsert, TPamAccountsUpdate } from "@app/db/schemas/pam-accounts";
|
||||
import { TPamFolders, TPamFoldersInsert, TPamFoldersUpdate } from "@app/db/schemas/pam-folders";
|
||||
import { TPamResources, TPamResourcesInsert, TPamResourcesUpdate } from "@app/db/schemas/pam-resources";
|
||||
import { TPamSessions, TPamSessionsInsert, TPamSessionsUpdate } from "@app/db/schemas/pam-sessions";
|
||||
import {
|
||||
TProjectMicrosoftTeamsConfigs,
|
||||
TProjectMicrosoftTeamsConfigsInsert,
|
||||
@@ -1308,5 +1312,9 @@ declare module "knex/types/tables" {
|
||||
TKeyValueStoreInsert,
|
||||
TKeyValueStoreUpdate
|
||||
>;
|
||||
[TableName.PamFolder]: KnexOriginal.CompositeTableType<TPamFolders, TPamFoldersInsert, TPamFoldersUpdate>;
|
||||
[TableName.PamResource]: KnexOriginal.CompositeTableType<TPamResources, TPamResourcesInsert, TPamResourcesUpdate>;
|
||||
[TableName.PamAccount]: KnexOriginal.CompositeTableType<TPamAccounts, TPamAccountsInsert, TPamAccountsUpdate>;
|
||||
[TableName.PamSession]: KnexOriginal.CompositeTableType<TPamSessions, TPamSessionsInsert, TPamSessionsUpdate>;
|
||||
}
|
||||
}
|
||||
|
||||
125
backend/src/db/migrations/20250917052037_pam.ts
Normal file
125
backend/src/db/migrations/20250917052037_pam.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// PAM Folders
|
||||
if (!(await knex.schema.hasTable(TableName.PamFolder))) {
|
||||
await knex.schema.createTable(TableName.PamFolder, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.index("projectId");
|
||||
|
||||
t.uuid("parentId").nullable();
|
||||
t.foreign("parentId").references("id").inTable(TableName.PamFolder).onDelete("CASCADE");
|
||||
t.index("parentId");
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.index("name");
|
||||
t.text("description").nullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
// PAM Resources
|
||||
if (!(await knex.schema.hasTable(TableName.PamResource))) {
|
||||
await knex.schema.createTable(TableName.PamResource, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.index("projectId");
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.index("name");
|
||||
t.string("gatewayId").notNullable();
|
||||
t.index("gatewayId");
|
||||
t.string("resourceType").notNullable();
|
||||
t.index("resourceType");
|
||||
t.binary("encryptedConnectionDetails").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
// PAM Accounts
|
||||
if (!(await knex.schema.hasTable(TableName.PamAccount))) {
|
||||
await knex.schema.createTable(TableName.PamAccount, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.index("projectId");
|
||||
|
||||
t.uuid("folderId").nullable();
|
||||
t.foreign("folderId").references("id").inTable(TableName.PamFolder).onDelete("CASCADE");
|
||||
t.index("folderId");
|
||||
|
||||
t.uuid("resourceId").notNullable();
|
||||
t.foreign("resourceId").references("id").inTable(TableName.PamResource).onDelete("CASCADE");
|
||||
t.index("resourceId");
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.index("name");
|
||||
t.text("description").nullable();
|
||||
|
||||
t.binary("encryptedCredentials").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
// PAM Sessions
|
||||
if (!(await knex.schema.hasTable(TableName.PamSession))) {
|
||||
await knex.schema.createTable(TableName.PamSession, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.index("projectId");
|
||||
|
||||
t.uuid("accountId").nullable();
|
||||
t.foreign("accountId").references("id").inTable(TableName.PamAccount).onDelete("SET NULL");
|
||||
t.index("accountId");
|
||||
|
||||
// To be used in the event of an account deletion
|
||||
t.string("resourceType").notNullable();
|
||||
t.string("resourceName").notNullable();
|
||||
t.string("accountName").notNullable();
|
||||
|
||||
t.uuid("userId").nullable();
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
t.index("userId");
|
||||
|
||||
// To be used in the event of user deletion
|
||||
t.string("actorName").notNullable();
|
||||
t.string("actorEmail").notNullable();
|
||||
|
||||
t.string("actorIp").notNullable();
|
||||
t.string("actorUserAgent").notNullable();
|
||||
|
||||
t.string("status").notNullable();
|
||||
t.index("status");
|
||||
|
||||
t.binary("encryptedLogsBlob").nullable();
|
||||
|
||||
t.datetime("expiresAt").nullable(); // null means unlimited duration / no expiry
|
||||
|
||||
t.datetime("startedAt").nullable(); // Not when the row is created, but when the end-to-end connection between user and resource is established
|
||||
t.datetime("endedAt").nullable();
|
||||
t.index(["startedAt", "endedAt"]);
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.PamSession);
|
||||
await knex.schema.dropTableIfExists(TableName.PamAccount);
|
||||
await knex.schema.dropTableIfExists(TableName.PamResource);
|
||||
await knex.schema.dropTableIfExists(TableName.PamFolder);
|
||||
}
|
||||
@@ -83,6 +83,10 @@ export * from "./org-memberships";
|
||||
export * from "./org-relay-config";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
export * from "./pam-accounts";
|
||||
export * from "./pam-folders";
|
||||
export * from "./pam-resources";
|
||||
export * from "./pam-sessions";
|
||||
export * from "./pki-alerts";
|
||||
export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
|
||||
@@ -287,7 +287,8 @@ export enum ProjectType {
|
||||
CertificateManager = "cert-manager",
|
||||
KMS = "kms",
|
||||
SSH = "ssh",
|
||||
SecretScanning = "secret-scanning"
|
||||
SecretScanning = "secret-scanning",
|
||||
PAM = "pam"
|
||||
}
|
||||
|
||||
export enum ActionProjectType {
|
||||
@@ -296,6 +297,7 @@ export enum ActionProjectType {
|
||||
KMS = ProjectType.KMS,
|
||||
SSH = ProjectType.SSH,
|
||||
SecretScanning = ProjectType.SecretScanning,
|
||||
PAM = ProjectType.PAM,
|
||||
// project operations that happen on all types
|
||||
Any = "any"
|
||||
}
|
||||
|
||||
26
backend/src/db/schemas/pam-accounts.ts
Normal file
26
backend/src/db/schemas/pam-accounts.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const PamAccountsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
folderId: z.string().uuid().nullable().optional(),
|
||||
resourceId: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
encryptedCredentials: zodBuffer,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TPamAccounts = z.infer<typeof PamAccountsSchema>;
|
||||
export type TPamAccountsInsert = Omit<z.input<typeof PamAccountsSchema>, TImmutableDBKeys>;
|
||||
export type TPamAccountsUpdate = Partial<Omit<z.input<typeof PamAccountsSchema>, TImmutableDBKeys>>;
|
||||
22
backend/src/db/schemas/pam-folders.ts
Normal file
22
backend/src/db/schemas/pam-folders.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const PamFoldersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
parentId: z.string().uuid().nullable().optional(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TPamFolders = z.infer<typeof PamFoldersSchema>;
|
||||
export type TPamFoldersInsert = Omit<z.input<typeof PamFoldersSchema>, TImmutableDBKeys>;
|
||||
export type TPamFoldersUpdate = Partial<Omit<z.input<typeof PamFoldersSchema>, TImmutableDBKeys>>;
|
||||
25
backend/src/db/schemas/pam-resources.ts
Normal file
25
backend/src/db/schemas/pam-resources.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const PamResourcesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
gatewayId: z.string(),
|
||||
resourceType: z.string(),
|
||||
encryptedConnectionDetails: zodBuffer,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TPamResources = z.infer<typeof PamResourcesSchema>;
|
||||
export type TPamResourcesInsert = Omit<z.input<typeof PamResourcesSchema>, TImmutableDBKeys>;
|
||||
export type TPamResourcesUpdate = Partial<Omit<z.input<typeof PamResourcesSchema>, TImmutableDBKeys>>;
|
||||
35
backend/src/db/schemas/pam-sessions.ts
Normal file
35
backend/src/db/schemas/pam-sessions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const PamSessionsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
accountId: z.string().uuid().nullable().optional(),
|
||||
resourceType: z.string(),
|
||||
resourceName: z.string(),
|
||||
accountName: z.string(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
actorName: z.string(),
|
||||
actorEmail: z.string(),
|
||||
actorIp: z.string(),
|
||||
actorUserAgent: z.string(),
|
||||
status: z.string(),
|
||||
encryptedLogsBlob: zodBuffer.nullable().optional(),
|
||||
expiresAt: z.date().nullable().optional(),
|
||||
startedAt: z.date().nullable().optional(),
|
||||
endedAt: z.date().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TPamSessions = z.infer<typeof PamSessionsSchema>;
|
||||
export type TPamSessionsInsert = Omit<z.input<typeof PamSessionsSchema>, TImmutableDBKeys>;
|
||||
export type TPamSessionsUpdate = Partial<Omit<z.input<typeof PamSessionsSchema>, TImmutableDBKeys>>;
|
||||
@@ -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" }
|
||||
);
|
||||
};
|
||||
|
||||
131
backend/src/ee/routes/v1/pam-account-router.ts
Normal file
131
backend/src/ee/routes/v1/pam-account-router.ts
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
149
backend/src/ee/routes/v1/pam-folder-router.ts
Normal file
149
backend/src/ee/routes/v1/pam-folder-router.ts
Normal file
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
26
backend/src/ee/routes/v1/pam-resource-routers/index.ts
Normal file
26
backend/src/ee/routes/v1/pam-resource-routers/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
221
backend/src/ee/routes/v1/pam-session-router.ts
Normal file
221
backend/src/ee/routes/v1/pam-session-router.ts
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -500,7 +500,26 @@ export enum EventType {
|
||||
|
||||
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
|
||||
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
|
||||
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value"
|
||||
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value",
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -3687,6 +3706,156 @@ interface OrgRoleDeleteEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionStartEvent {
|
||||
type: EventType.PAM_SESSION_START;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionLogsUpdateEvent {
|
||||
type: EventType.PAM_SESSION_LOGS_UPDATE;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionEndEvent {
|
||||
type: EventType.PAM_SESSION_END;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionGetEvent {
|
||||
type: EventType.PAM_SESSION_GET;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionListEvent {
|
||||
type: EventType.PAM_SESSION_LIST;
|
||||
metadata: {
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamFolderCreateEvent {
|
||||
type: EventType.PAM_FOLDER_CREATE;
|
||||
metadata: {
|
||||
parentId?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamFolderUpdateEvent {
|
||||
type: EventType.PAM_FOLDER_UPDATE;
|
||||
metadata: {
|
||||
folderId: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamFolderDeleteEvent {
|
||||
type: EventType.PAM_FOLDER_DELETE;
|
||||
metadata: {
|
||||
folderId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamAccountListEvent {
|
||||
type: EventType.PAM_ACCOUNT_LIST;
|
||||
metadata: {
|
||||
accountCount: number;
|
||||
folderCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamAccountAccessEvent {
|
||||
type: EventType.PAM_ACCOUNT_ACCESS;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
duration?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamAccountCreateEvent {
|
||||
type: EventType.PAM_ACCOUNT_CREATE;
|
||||
metadata: {
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
folderId?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamAccountUpdateEvent {
|
||||
type: EventType.PAM_ACCOUNT_UPDATE;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamAccountDeleteEvent {
|
||||
type: EventType.PAM_ACCOUNT_DELETE;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamResourceListEvent {
|
||||
type: EventType.PAM_RESOURCE_LIST;
|
||||
metadata: {
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamResourceGetEvent {
|
||||
type: EventType.PAM_RESOURCE_GET;
|
||||
metadata: {
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamResourceCreateEvent {
|
||||
type: EventType.PAM_RESOURCE_CREATE;
|
||||
metadata: {
|
||||
resourceType: string;
|
||||
gatewayId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamResourceUpdateEvent {
|
||||
type: EventType.PAM_RESOURCE_UPDATE;
|
||||
metadata: {
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
gatewayId?: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamResourceDeleteEvent {
|
||||
type: EventType.PAM_RESOURCE_DELETE;
|
||||
metadata: {
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -4020,4 +4189,22 @@ export type Event =
|
||||
| ProjectRoleDeleteEvent
|
||||
| OrgRoleCreateEvent
|
||||
| OrgRoleUpdateEvent
|
||||
| OrgRoleDeleteEvent;
|
||||
| OrgRoleDeleteEvent
|
||||
| PamSessionStartEvent
|
||||
| PamSessionLogsUpdateEvent
|
||||
| PamSessionEndEvent
|
||||
| PamSessionGetEvent
|
||||
| PamSessionListEvent
|
||||
| PamFolderCreateEvent
|
||||
| PamFolderUpdateEvent
|
||||
| PamFolderDeleteEvent
|
||||
| PamAccountListEvent
|
||||
| PamAccountAccessEvent
|
||||
| PamAccountCreateEvent
|
||||
| PamAccountUpdateEvent
|
||||
| PamAccountDeleteEvent
|
||||
| PamResourceListEvent
|
||||
| PamResourceGetEvent
|
||||
| PamResourceCreateEvent
|
||||
| PamResourceUpdateEvent
|
||||
| PamResourceDeleteEvent;
|
||||
|
||||
@@ -268,11 +268,13 @@ export const gatewayV2ServiceFactory = ({
|
||||
const getPlatformConnectionDetailsByGatewayId = async ({
|
||||
gatewayId,
|
||||
targetHost,
|
||||
targetPort
|
||||
targetPort,
|
||||
actorMetadata
|
||||
}: {
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
actorMetadata?: { sessionId?: string; resourceType?: string };
|
||||
}) => {
|
||||
const gateway = await gatewayV2DAL.findById(gatewayId);
|
||||
if (!gateway) {
|
||||
@@ -359,7 +361,9 @@ export const gatewayV2ServiceFactory = ({
|
||||
const actorExtension = new x509.Extension(
|
||||
GATEWAY_ACTOR_OID,
|
||||
false,
|
||||
Buffer.from(JSON.stringify({ type: ActorType.PLATFORM }))
|
||||
Buffer.from(
|
||||
JSON.stringify(actorMetadata ? { type: ActorType.PLATFORM, ...actorMetadata } : { type: ActorType.PLATFORM })
|
||||
)
|
||||
);
|
||||
|
||||
const clientCert = await x509.X509CertificateGenerator.create({
|
||||
|
||||
@@ -58,7 +58,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
enforceMfa: false,
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false,
|
||||
gateway: true,
|
||||
sshHostGroups: false,
|
||||
secretScanning: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
@@ -66,7 +66,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
enterpriseAppConnections: false,
|
||||
fips: false,
|
||||
eventSubscriptions: false,
|
||||
machineIdentityAuthTemplates: false
|
||||
machineIdentityAuthTemplates: false,
|
||||
pam: true
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (
|
||||
|
||||
@@ -80,6 +80,7 @@ export type TFeatureSet = {
|
||||
machineIdentityAuthTemplates: false;
|
||||
fips: false;
|
||||
eventSubscriptions: false;
|
||||
pam: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
||||
9
backend/src/ee/services/pam-folder/pam-folder-dal.ts
Normal file
9
backend/src/ee/services/pam-folder/pam-folder-dal.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TPamFolderDALFactory = ReturnType<typeof pamFolderDALFactory>;
|
||||
export const pamFolderDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.PamFolder);
|
||||
return { ...orm };
|
||||
};
|
||||
33
backend/src/ee/services/pam-folder/pam-folder-fns.ts
Normal file
33
backend/src/ee/services/pam-folder/pam-folder-fns.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { TPamFolderDALFactory } from "./pam-folder-dal";
|
||||
|
||||
type GetFullFolderPath = {
|
||||
pamFolderDAL: Pick<TPamFolderDALFactory, "find">;
|
||||
folderId?: string | null;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const getFullPamFolderPath = async ({
|
||||
pamFolderDAL,
|
||||
folderId,
|
||||
projectId
|
||||
}: GetFullFolderPath): Promise<string> => {
|
||||
if (!folderId) return "/";
|
||||
|
||||
const folders = await pamFolderDAL.find({ projectId });
|
||||
const folderMap = new Map(folders.map((folder) => [folder.id, folder]));
|
||||
|
||||
if (!folderMap.has(folderId)) return "";
|
||||
|
||||
const path: string[] = [];
|
||||
let currentFolderId: string | null | undefined = folderId;
|
||||
|
||||
while (currentFolderId) {
|
||||
const folder = folderMap.get(currentFolderId);
|
||||
if (!folder) break;
|
||||
|
||||
path.unshift(folder.name);
|
||||
currentFolderId = folder.parentId;
|
||||
}
|
||||
|
||||
return `/${path.join("/")}`;
|
||||
};
|
||||
151
backend/src/ee/services/pam-folder/pam-folder-service.ts
Normal file
151
backend/src/ee/services/pam-folder/pam-folder-service.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TPamFolders } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPamFolderDALFactory } from "./pam-folder-dal";
|
||||
import { TCreateFolderDTO, TUpdateFolderDTO } from "./pam-folder-types";
|
||||
|
||||
type TPamFolderServiceFactoryDep = {
|
||||
pamFolderDAL: TPamFolderDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TPamFolderServiceFactory = ReturnType<typeof pamFolderServiceFactory>;
|
||||
|
||||
export const pamFolderServiceFactory = ({
|
||||
pamFolderDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TPamFolderServiceFactoryDep) => {
|
||||
const createFolder = async ({ name, description, parentId, projectId }: TCreateFolderDTO, actor: OrgServiceActor) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PamFolders);
|
||||
|
||||
if (parentId) {
|
||||
if (!(await pamFolderDAL.findOne({ id: parentId, projectId }))) {
|
||||
throw new NotFoundError({
|
||||
message: `Parent folder '${parentId}' not found for project '${projectId}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const existingFolder = await pamFolderDAL.findOne({
|
||||
name,
|
||||
parentId: parentId || null,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (existingFolder) {
|
||||
throw new BadRequestError({
|
||||
message: `Folder with name '${name}' already exists for this parent`
|
||||
});
|
||||
}
|
||||
|
||||
const folder = await pamFolderDAL.create({
|
||||
name,
|
||||
description: description ?? null,
|
||||
parentId: parentId || null,
|
||||
projectId
|
||||
});
|
||||
|
||||
return folder;
|
||||
};
|
||||
|
||||
const updateFolder = async ({ id, name, description }: TUpdateFolderDTO, actor: OrgServiceActor) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const folder = await pamFolderDAL.findById(id);
|
||||
if (!folder) throw new NotFoundError({ message: `Folder with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: folder.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PamFolders);
|
||||
|
||||
const updateDoc: Partial<TPamFolders> = {};
|
||||
|
||||
if (name !== undefined) {
|
||||
updateDoc.name = name;
|
||||
}
|
||||
|
||||
if (description !== undefined) {
|
||||
updateDoc.description = description;
|
||||
}
|
||||
|
||||
if (name && name !== folder.name) {
|
||||
const existingFolder = await pamFolderDAL.findOne({
|
||||
name,
|
||||
parentId: folder.parentId || null,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
|
||||
if (existingFolder) {
|
||||
throw new BadRequestError({
|
||||
message: `Folder with name '${name}' already exists for this parent`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return folder;
|
||||
}
|
||||
|
||||
const updatedFolder = await pamFolderDAL.updateById(id, updateDoc);
|
||||
|
||||
return updatedFolder;
|
||||
};
|
||||
|
||||
const deleteFolder = async (id: string, actor: OrgServiceActor) => {
|
||||
const folder = await pamFolderDAL.findById(id);
|
||||
if (!folder) throw new NotFoundError({ message: `Folder with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: folder.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamFolders);
|
||||
|
||||
const deletedFolder = await pamFolderDAL.deleteById(id);
|
||||
|
||||
return deletedFolder;
|
||||
};
|
||||
|
||||
return { createFolder, updateFolder, deleteFolder };
|
||||
};
|
||||
13
backend/src/ee/services/pam-folder/pam-folder-types.ts
Normal file
13
backend/src/ee/services/pam-folder/pam-folder-types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// DTOs
|
||||
export interface TCreateFolderDTO {
|
||||
projectId: string;
|
||||
parentId?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface TUpdateFolderDTO {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
43
backend/src/ee/services/pam-resource/pam-account-dal.ts
Normal file
43
backend/src/ee/services/pam-resource/pam-account-dal.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TPamAccounts } from "@app/db/schemas";
|
||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TPamAccountDALFactory = ReturnType<typeof pamAccountDALFactory>;
|
||||
|
||||
type PamAccountFindFilter = Parameters<typeof buildFindFilter<TPamAccounts>>[0];
|
||||
|
||||
export const pamAccountDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.PamAccount);
|
||||
|
||||
const findWithResourceDetails = async (filter: PamAccountFindFilter, tx?: Knex) => {
|
||||
const query = (tx || db.replicaNode())(TableName.PamAccount)
|
||||
.leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`)
|
||||
.select(selectAllTableCols(TableName.PamAccount))
|
||||
.select(
|
||||
// resource
|
||||
db.ref("name").withSchema(TableName.PamResource).as("resourceName"),
|
||||
db.ref("resourceType").withSchema(TableName.PamResource)
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PamAccount, filter)));
|
||||
}
|
||||
|
||||
const accounts = await query;
|
||||
|
||||
return accounts.map(({ resourceId, resourceName, resourceType, ...account }) => ({
|
||||
...account,
|
||||
resourceId,
|
||||
resource: {
|
||||
id: resourceId,
|
||||
name: resourceName,
|
||||
resourceType
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
return { ...orm, findWithResourceDetails };
|
||||
};
|
||||
9
backend/src/ee/services/pam-resource/pam-resource-dal.ts
Normal file
9
backend/src/ee/services/pam-resource/pam-resource-dal.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TPamResourceDALFactory = ReturnType<typeof pamResourceDALFactory>;
|
||||
export const pamResourceDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.PamResource);
|
||||
return { ...orm };
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum PamResource {
|
||||
Postgres = "postgres"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types";
|
||||
import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
||||
|
||||
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
||||
|
||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation
|
||||
};
|
||||
126
backend/src/ee/services/pam-resource/pam-resource-fns.ts
Normal file
126
backend/src/ee/services/pam-resource/pam-resource-fns.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { TPamResources } from "@app/db/schemas";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TPamAccountCredentials, TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||
|
||||
export const listResourceOptions = () => {
|
||||
return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
// Resource
|
||||
export const encryptResourceConnectionDetails = async ({
|
||||
orgId,
|
||||
connectionDetails,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
connectionDetails: TPamResourceConnectionDetails;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedConnectionDetailsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(connectionDetails))
|
||||
});
|
||||
|
||||
return encryptedConnectionDetailsBlob;
|
||||
};
|
||||
|
||||
export const decryptResourceConnectionDetails = async ({
|
||||
orgId,
|
||||
encryptedConnectionDetails,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
encryptedConnectionDetails: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedConnectionDetails
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamResourceConnectionDetails;
|
||||
};
|
||||
|
||||
export const decryptResource = async (
|
||||
resource: TPamResources,
|
||||
orgId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
return {
|
||||
...resource,
|
||||
connectionDetails: await decryptResourceConnectionDetails({
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
orgId,
|
||||
kmsService
|
||||
})
|
||||
} as TPamResource;
|
||||
};
|
||||
|
||||
// Account
|
||||
export const encryptAccountCredentials = async ({
|
||||
orgId,
|
||||
credentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
credentials: TPamAccountCredentials;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(credentials))
|
||||
});
|
||||
|
||||
return encryptedCredentialsBlob;
|
||||
};
|
||||
|
||||
export const decryptAccountCredentials = async ({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
encryptedCredentials: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedCredentials
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
||||
};
|
||||
|
||||
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
|
||||
account: T,
|
||||
orgId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<T & { credentials: TPamAccountCredentials }> => {
|
||||
return {
|
||||
...account,
|
||||
credentials: await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
})
|
||||
} as T & { credentials: TPamAccountCredentials };
|
||||
};
|
||||
45
backend/src/ee/services/pam-resource/pam-resource-schemas.ts
Normal file
45
backend/src/ee/services/pam-resource/pam-resource-schemas.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamAccountsSchema, PamResourcesSchema } from "@app/db/schemas";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
// Resources
|
||||
export const BasePamResoureSchema = PamResourcesSchema.omit({
|
||||
encryptedConnectionDetails: true,
|
||||
resourceType: true
|
||||
});
|
||||
|
||||
export const BaseCreatePamResourceSchema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
gatewayId: z.string().uuid(),
|
||||
name: slugSchema({ field: "name" })
|
||||
});
|
||||
|
||||
export const BaseUpdatePamResourceSchema = z.object({
|
||||
gatewayId: z.string().uuid().optional(),
|
||||
name: slugSchema({ field: "name" }).optional()
|
||||
});
|
||||
|
||||
// Accounts
|
||||
export const BasePamAccountSchema = PamAccountsSchema.omit({
|
||||
encryptedCredentials: true
|
||||
});
|
||||
|
||||
export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
|
||||
resource: PamResourcesSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
resourceType: true
|
||||
})
|
||||
});
|
||||
|
||||
export const BaseCreatePamAccountSchema = z.object({
|
||||
folderId: z.string().uuid().optional(),
|
||||
name: slugSchema({ field: "name" }),
|
||||
description: z.string().max(512).optional()
|
||||
});
|
||||
|
||||
export const BaseUpdatePamAccountSchema = z.object({
|
||||
name: slugSchema({ field: "name" }).optional(),
|
||||
description: z.string().max(512).optional()
|
||||
});
|
||||
671
backend/src/ee/services/pam-resource/pam-resource-service.ts
Normal file
671
backend/src/ee/services/pam-resource/pam-resource-service.ts
Normal file
@@ -0,0 +1,671 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionPamAccountActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal";
|
||||
import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns";
|
||||
import { TPamSessionDALFactory } from "../pam-session/pam-session-dal";
|
||||
import { PamSessionStatus } from "../pam-session/pam-session-enums";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPamAccountDALFactory } from "./pam-account-dal";
|
||||
import { TPamResourceDALFactory } from "./pam-resource-dal";
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
|
||||
import {
|
||||
decryptAccount,
|
||||
decryptAccountCredentials,
|
||||
decryptResource,
|
||||
decryptResourceConnectionDetails,
|
||||
encryptAccountCredentials,
|
||||
encryptResourceConnectionDetails,
|
||||
listResourceOptions
|
||||
} from "./pam-resource-fns";
|
||||
import {
|
||||
TAccessAccountDTO,
|
||||
TCreateAccountDTO,
|
||||
TCreateResourceDTO,
|
||||
TPamAccountCredentials,
|
||||
TUpdateAccountDTO,
|
||||
TUpdateResourceDTO
|
||||
} from "./pam-resource-types";
|
||||
|
||||
type TPamResourceServiceFactoryDep = {
|
||||
pamResourceDAL: TPamResourceDALFactory;
|
||||
pamSessionDAL: TPamSessionDALFactory;
|
||||
pamAccountDAL: TPamAccountDALFactory;
|
||||
pamFolderDAL: TPamFolderDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
};
|
||||
|
||||
export type TPamResourceServiceFactory = ReturnType<typeof pamResourceServiceFactory>;
|
||||
|
||||
export const pamResourceServiceFactory = ({
|
||||
pamResourceDAL,
|
||||
pamSessionDAL,
|
||||
pamAccountDAL,
|
||||
pamFolderDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
gatewayV2Service
|
||||
}: TPamResourceServiceFactoryDep) => {
|
||||
const getById = async (id: string, resourceType: PamResource, actor: OrgServiceActor) => {
|
||||
const resource = await pamResourceDAL.findById(id);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: resource.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PamResources);
|
||||
|
||||
if (resource.resourceType !== resourceType) {
|
||||
throw new BadRequestError({
|
||||
message: `Resource with ID '${id}' is not of type '${resourceType}'`
|
||||
});
|
||||
}
|
||||
|
||||
return decryptResource(resource, actor.orgId, kmsService);
|
||||
};
|
||||
|
||||
const create = async (
|
||||
{ resourceType, connectionDetails, gatewayId, name, projectId }: TCreateResourceDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PamResources);
|
||||
|
||||
const factory = PAM_RESOURCE_FACTORY_MAP[resourceType](
|
||||
resourceType,
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
gatewayV2Service
|
||||
);
|
||||
const validatedConnectionDetails = await factory.validateConnection();
|
||||
|
||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||
connectionDetails: validatedConnectionDetails,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const resource = await pamResourceDAL.create({
|
||||
resourceType,
|
||||
encryptedConnectionDetails,
|
||||
gatewayId,
|
||||
name,
|
||||
projectId
|
||||
});
|
||||
|
||||
return decryptResource(resource, actor.orgId, kmsService);
|
||||
};
|
||||
|
||||
const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const resource = await pamResourceDAL.findById(resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: resource.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PamResources);
|
||||
|
||||
const updateDoc: Partial<TPamResources> = {};
|
||||
|
||||
if (name !== undefined) {
|
||||
updateDoc.name = name;
|
||||
}
|
||||
|
||||
if (connectionDetails !== undefined) {
|
||||
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
);
|
||||
const validatedConnectionDetails = await factory.validateConnection();
|
||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||
connectionDetails: validatedConnectionDetails,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
|
||||
}
|
||||
|
||||
// If nothing was updated, return the fetched resource
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return decryptResource(resource, actor.orgId, kmsService);
|
||||
}
|
||||
|
||||
const updatedResource = await pamResourceDAL.updateById(resourceId, updateDoc);
|
||||
|
||||
return decryptResource(updatedResource, actor.orgId, kmsService);
|
||||
};
|
||||
|
||||
const deleteById = async (id: string, actor: OrgServiceActor) => {
|
||||
const resource = await pamResourceDAL.findById(id);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: resource.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamResources);
|
||||
|
||||
const deletedResource = await pamResourceDAL.deleteById(id);
|
||||
|
||||
return decryptResource(deletedResource, actor.orgId, kmsService);
|
||||
};
|
||||
|
||||
const list = async (projectId: string, actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PamResources);
|
||||
|
||||
const resources = await pamResourceDAL.find({ projectId });
|
||||
|
||||
return {
|
||||
resources: await Promise.all(resources.map((resource) => decryptResource(resource, actor.orgId, kmsService)))
|
||||
};
|
||||
};
|
||||
|
||||
// Accounts
|
||||
const createAccount = async (
|
||||
{ credentials, resourceId, name, description, folderId }: TCreateAccountDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const resource = await pamResourceDAL.findById(resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: resource.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId,
|
||||
projectId: resource.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Create,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
|
||||
const connectionDetails = await decryptResourceConnectionDetails({
|
||||
orgId: actor.orgId,
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
);
|
||||
const validatedCredentials = await factory.validateAccountCredentials(credentials);
|
||||
|
||||
const encryptedCredentials = await encryptAccountCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const account = await pamAccountDAL.create({
|
||||
projectId: resource.projectId,
|
||||
resourceId: resource.id,
|
||||
encryptedCredentials,
|
||||
name,
|
||||
description,
|
||||
folderId
|
||||
});
|
||||
|
||||
return {
|
||||
...(await decryptAccount(account, actor.orgId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
const updateAccountById = async (
|
||||
{ accountId, credentials, description, name }: TUpdateAccountDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findById(accountId);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Edit,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
|
||||
const updateDoc: Partial<TPamAccounts> = {};
|
||||
|
||||
if (name !== undefined) {
|
||||
updateDoc.name = name;
|
||||
}
|
||||
|
||||
if (description !== undefined) {
|
||||
updateDoc.description = description;
|
||||
}
|
||||
|
||||
if (credentials !== undefined) {
|
||||
const connectionDetails = await decryptResourceConnectionDetails({
|
||||
orgId: actor.orgId,
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
);
|
||||
|
||||
// Logic to prevent overwriting unedited censored values
|
||||
const finalCredentials = { ...credentials };
|
||||
if (credentials.password === "******") {
|
||||
const decryptedCredentials = await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
finalCredentials.password = decryptedCredentials.password;
|
||||
}
|
||||
|
||||
const validatedCredentials = await factory.validateAccountCredentials(finalCredentials);
|
||||
const encryptedCredentials = await encryptAccountCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
updateDoc.encryptedCredentials = encryptedCredentials;
|
||||
}
|
||||
|
||||
// If nothing was updated, return the fetched account
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return decryptAccount(account, actor.orgId, kmsService);
|
||||
}
|
||||
|
||||
const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc);
|
||||
|
||||
return {
|
||||
...(await decryptAccount(updatedAccount, actor.orgId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
const deleteAccountById = async (id: string, actor: OrgServiceActor) => {
|
||||
const account = await pamAccountDAL.findById(id);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${id}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Delete,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
|
||||
const deletedAccount = await pamAccountDAL.deleteById(id);
|
||||
|
||||
return {
|
||||
...(await decryptAccount(deletedAccount, actor.orgId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
const listAccounts = async (projectId: string, actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId });
|
||||
|
||||
const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders);
|
||||
|
||||
const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : [];
|
||||
|
||||
const decryptedAndPermittedAccounts: Array<
|
||||
TPamAccounts & {
|
||||
resource: Pick<TPamResources, "id" | "name" | "resourceType">;
|
||||
credentials: TPamAccountCredentials;
|
||||
}
|
||||
> = [];
|
||||
|
||||
for await (const account of accountsWithResourceDetails) {
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
// Check permission for each individual account
|
||||
if (
|
||||
permission.can(
|
||||
ProjectPermissionPamAccountActions.Read,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: account.resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
)
|
||||
) {
|
||||
// Decrypt the account only if the user has permission to read it
|
||||
const decryptedAccount = await decryptAccount(account, actor.orgId, kmsService);
|
||||
decryptedAndPermittedAccounts.push({
|
||||
...decryptedAccount,
|
||||
resource: {
|
||||
id: account.resource.id,
|
||||
name: account.resource.name,
|
||||
resourceType: account.resource.resourceType
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accounts: decryptedAndPermittedAccounts,
|
||||
folders
|
||||
};
|
||||
};
|
||||
|
||||
const accessAccount = async (
|
||||
{ accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findById(accountId);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.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
|
||||
})
|
||||
);
|
||||
|
||||
const session = await pamSessionDAL.create({
|
||||
accountName: account.name,
|
||||
actorEmail,
|
||||
actorIp,
|
||||
actorName,
|
||||
actorUserAgent,
|
||||
projectId: account.projectId,
|
||||
resourceName: resource.name,
|
||||
resourceType: resource.resourceType,
|
||||
status: PamSessionStatus.Starting,
|
||||
accountId: account.id,
|
||||
userId: actor.id,
|
||||
expiresAt: duration ? new Date(Date.now() + duration) : null
|
||||
});
|
||||
|
||||
const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, actor.orgId, kmsService);
|
||||
|
||||
const gatewayConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId,
|
||||
targetHost: connectionDetails.host,
|
||||
targetPort: connectionDetails.port,
|
||||
actorMetadata: {
|
||||
sessionId: session.id,
|
||||
resourceType: resource.resourceType
|
||||
}
|
||||
});
|
||||
|
||||
if (!gatewayConnectionDetails) {
|
||||
throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` });
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
resourceType,
|
||||
relayCertificate: gatewayConnectionDetails.relay.clientCertificate,
|
||||
gatewayCertificate: gatewayConnectionDetails.gateway.clientCertificate,
|
||||
relayHost: gatewayConnectionDetails.relayHost,
|
||||
projectId: account.projectId
|
||||
};
|
||||
};
|
||||
|
||||
const getSessionCredentials = async (sessionId: string, actor: OrgServiceActor) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
// To be hit by gateways only
|
||||
if (actor.type !== ActorType.IDENTITY) {
|
||||
throw new ForbiddenRequestError({ message: "Only gateways can perform this action" });
|
||||
}
|
||||
|
||||
const session = await pamSessionDAL.findById(sessionId);
|
||||
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
|
||||
|
||||
const project = await projectDAL.findById(session.projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
project.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
if (!session.accountId) throw new NotFoundError({ message: "Session is missing accountId column" });
|
||||
|
||||
// Verify that the session has not ended
|
||||
if (session.endedAt || (session.expiresAt && session.expiresAt < new Date())) {
|
||||
throw new BadRequestError({ message: "Session has ended or expired" });
|
||||
}
|
||||
|
||||
// Verify that the session has not already had credentials fetched
|
||||
if (session.status !== PamSessionStatus.Starting) {
|
||||
throw new BadRequestError({ message: "Session has already been started" });
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findById(session.accountId);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${session.accountId}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const decryptedAccount = await decryptAccount(account, actor.orgId, kmsService);
|
||||
|
||||
const decryptedResource = await decryptResource(resource, actor.orgId, kmsService);
|
||||
|
||||
// Mark session as started
|
||||
await pamSessionDAL.updateById(sessionId, {
|
||||
status: PamSessionStatus.Active,
|
||||
startedAt: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
credentials: {
|
||||
...decryptedResource.connectionDetails,
|
||||
...decryptedAccount.credentials
|
||||
},
|
||||
projectId: project.id
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getById,
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
list,
|
||||
listResourceOptions,
|
||||
createAccount,
|
||||
updateAccountById,
|
||||
deleteAccountById,
|
||||
listAccounts,
|
||||
accessAccount,
|
||||
getSessionCredentials
|
||||
};
|
||||
};
|
||||
58
backend/src/ee/services/pam-resource/pam-resource-types.ts
Normal file
58
backend/src/ee/services/pam-resource/pam-resource-types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import {
|
||||
TPostgresAccount,
|
||||
TPostgresAccountCredentials,
|
||||
TPostgresResource,
|
||||
TPostgresResourceConnectionDetails
|
||||
} from "./postgres/postgres-resource-types";
|
||||
|
||||
// Resource types
|
||||
export type TPamResource = TPostgresResource;
|
||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
||||
|
||||
// Account types
|
||||
export type TPamAccount = TPostgresAccount;
|
||||
export type TPamAccountCredentials = TPostgresAccountCredentials;
|
||||
|
||||
// Resource DTOs
|
||||
export type TCreateResourceDTO = Pick<
|
||||
TPamResource,
|
||||
"name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId"
|
||||
>;
|
||||
|
||||
export type TUpdateResourceDTO = Partial<Omit<TCreateResourceDTO, "resourceType" | "projectId">> & {
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
// Account DTOs
|
||||
export type TCreateAccountDTO = Pick<TPamAccount, "name" | "description" | "credentials" | "folderId" | "resourceId">;
|
||||
|
||||
export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "resourceId">> & {
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
export type TAccessAccountDTO = {
|
||||
accountId: string;
|
||||
actorEmail: string;
|
||||
actorIp: string;
|
||||
actorName: string;
|
||||
actorUserAgent: string;
|
||||
duration?: number | null;
|
||||
};
|
||||
|
||||
// Resource factory
|
||||
export type TPamResourceFactoryValidateConnection<T extends TPamResourceConnectionDetails> = () => Promise<T>;
|
||||
export type TPamResourceFactoryValidateAccountCredentials<C extends TPamAccountCredentials> = (
|
||||
credentials: C
|
||||
) => Promise<C>;
|
||||
|
||||
export type TPamResourceFactory<T extends TPamResourceConnectionDetails, C extends TPamAccountCredentials> = (
|
||||
resourceType: PamResource,
|
||||
connectionDetails: T,
|
||||
gatewayId: string,
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
|
||||
) => {
|
||||
validateConnection: TPamResourceFactoryValidateConnection<T>;
|
||||
validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<C>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { PostgresResourceListItemSchema } from "./postgres-resource-schemas";
|
||||
|
||||
export const getPostgresResourceListItem = () => {
|
||||
return {
|
||||
name: PostgresResourceListItemSchema.shape.name.value,
|
||||
resource: PostgresResourceListItemSchema.shape.resource.value
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResoureSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import {
|
||||
BaseSqlAccountCredentialsSchema,
|
||||
BaseSqlResourceConnectionDetailsSchema
|
||||
} from "../shared/sql/sql-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export const PostgresResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema;
|
||||
|
||||
const BasePostgresResourceSchema = BasePamResoureSchema.extend({ resourceType: z.literal(PamResource.Postgres) });
|
||||
|
||||
export const PostgresResourceSchema = BasePostgresResourceSchema.extend({
|
||||
connectionDetails: PostgresResourceConnectionDetailsSchema
|
||||
});
|
||||
|
||||
export const PostgresResourceListItemSchema = z.object({
|
||||
name: z.literal("PostgreSQL"),
|
||||
resource: z.literal(PamResource.Postgres)
|
||||
});
|
||||
|
||||
export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
connectionDetails: PostgresResourceConnectionDetailsSchema
|
||||
});
|
||||
|
||||
export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
connectionDetails: PostgresResourceConnectionDetailsSchema.optional()
|
||||
});
|
||||
|
||||
// Accounts
|
||||
export const PostgresAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
|
||||
|
||||
export const PostgresAccountSchema = BasePamAccountSchema.extend({
|
||||
credentials: PostgresAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const CreatePostgresAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||
credentials: PostgresAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdatePostgresAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||
credentials: PostgresAccountCredentialsSchema.optional()
|
||||
});
|
||||
|
||||
export const SanitizedPostgresAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||
credentials: PostgresAccountCredentialsSchema.pick({
|
||||
username: true
|
||||
})
|
||||
});
|
||||
|
||||
// Sessions
|
||||
export const PostgresSessionCredentialsSchema = PostgresResourceConnectionDetailsSchema.and(
|
||||
PostgresAccountCredentialsSchema
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
PostgresAccountCredentialsSchema,
|
||||
PostgresAccountSchema,
|
||||
PostgresResourceConnectionDetailsSchema,
|
||||
PostgresResourceSchema
|
||||
} from "./postgres-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export type TPostgresResource = z.infer<typeof PostgresResourceSchema>;
|
||||
export type TPostgresResourceConnectionDetails = z.infer<typeof PostgresResourceConnectionDetailsSchema>;
|
||||
|
||||
// Accounts
|
||||
export type TPostgresAccount = z.infer<typeof PostgresAccountSchema>;
|
||||
export type TPostgresAccountCredentials = z.infer<typeof PostgresAccountCredentialsSchema>;
|
||||
@@ -0,0 +1,173 @@
|
||||
import knex, { Knex } from "knex";
|
||||
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { GatewayProxyProtocol } from "@app/lib/gateway";
|
||||
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||
|
||||
import { PamResource } from "../../pam-resource-enums";
|
||||
import { TPamResourceFactory, TPamResourceFactoryValidateAccountCredentials } from "../../pam-resource-types";
|
||||
import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "./sql-resource-types";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
|
||||
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
||||
|
||||
const SQL_CONNECTION_CLIENT_MAP = {
|
||||
[PamResource.Postgres]: "pg"
|
||||
};
|
||||
|
||||
const getConnectionConfig = (
|
||||
resourceType: PamResource,
|
||||
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails
|
||||
) => {
|
||||
switch (resourceType) {
|
||||
case PamResource.Postgres: {
|
||||
return {
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate,
|
||||
servername: host
|
||||
}
|
||||
: false
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Unhandled SQL Resource Connection Config: ${resourceType as PamResource}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const executeWithGateway = async <T>(
|
||||
config: {
|
||||
connectionDetails: TSqlResourceConnectionDetails;
|
||||
resourceType: PamResource;
|
||||
gatewayId: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
},
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||
operation: (client: Knex) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const { connectionDetails, resourceType, gatewayId, username, password } = config;
|
||||
|
||||
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
||||
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId,
|
||||
targetHost,
|
||||
targetPort: connectionDetails.port
|
||||
});
|
||||
|
||||
if (!platformConnectionDetails) {
|
||||
throw new BadRequestError({ message: "Unable to connect to gateway, no platform connection details found" });
|
||||
}
|
||||
|
||||
return withGatewayV2Proxy(
|
||||
async (proxyPort) => {
|
||||
const client = knex({
|
||||
client: SQL_CONNECTION_CLIENT_MAP[resourceType],
|
||||
connection: {
|
||||
database: connectionDetails.database,
|
||||
port: proxyPort,
|
||||
host: "localhost",
|
||||
user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback
|
||||
password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
...getConnectionConfig(resourceType, connectionDetails)
|
||||
}
|
||||
});
|
||||
try {
|
||||
return await operation(client);
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
relayHost: platformConnectionDetails.relayHost,
|
||||
gateway: platformConnectionDetails.gateway,
|
||||
relay: platformConnectionDetails.relay
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetails, TSqlAccountCredentials> = (
|
||||
resourceType,
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
gatewayV2Service
|
||||
) => {
|
||||
const validateConnection = async () => {
|
||||
try {
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
||||
await client.raw("Select 1");
|
||||
});
|
||||
return connectionDetails;
|
||||
} catch (error) {
|
||||
// Hacky way to know if we successfully hit the database
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) {
|
||||
return connectionDetails;
|
||||
}
|
||||
|
||||
if (error.message === "Connection terminated unexpectedly") {
|
||||
throw new BadRequestError({
|
||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<TSqlAccountCredentials> = async (
|
||||
credentials
|
||||
) => {
|
||||
try {
|
||||
await executeWithGateway(
|
||||
{
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
resourceType,
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
},
|
||||
gatewayV2Service,
|
||||
async (client) => {
|
||||
await client.raw("Select 1");
|
||||
}
|
||||
);
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
||||
throw new BadRequestError({
|
||||
message: "Account credentials invalid: Username or password incorrect"
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message === "Connection terminated unexpectedly") {
|
||||
throw new BadRequestError({
|
||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate account credentials for ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
validateConnection,
|
||||
validateAccountCredentials
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Resources
|
||||
export const BaseSqlResourceConnectionDetailsSchema = z.object({
|
||||
host: z.string().trim().min(1).max(255),
|
||||
port: z.coerce.number(),
|
||||
database: z.string().trim().min(1).max(255),
|
||||
sslEnabled: z.boolean(),
|
||||
sslRejectUnauthorized: z.boolean(),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
});
|
||||
|
||||
// Accounts
|
||||
export const BaseSqlAccountCredentialsSchema = z.object({
|
||||
username: z.string().trim().min(1),
|
||||
password: z.string().trim().min(1)
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import {
|
||||
TPostgresAccountCredentials,
|
||||
TPostgresResourceConnectionDetails
|
||||
} from "../../postgres/postgres-resource-types";
|
||||
|
||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
||||
export type TSqlAccountCredentials = TPostgresAccountCredentials;
|
||||
9
backend/src/ee/services/pam-session/pam-session-dal.ts
Normal file
9
backend/src/ee/services/pam-session/pam-session-dal.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TPamSessionDALFactory = ReturnType<typeof pamSessionDALFactory>;
|
||||
export const pamSessionDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.PamSession);
|
||||
return { ...orm };
|
||||
};
|
||||
6
backend/src/ee/services/pam-session/pam-session-enums.ts
Normal file
6
backend/src/ee/services/pam-session/pam-session-enums.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum PamSessionStatus {
|
||||
Starting = "starting", // Starting, user connecting to resource
|
||||
Active = "active", // Active, user is connected to resource
|
||||
Ended = "ended", // Ended by user
|
||||
Terminated = "terminated" // Terminated by an admin
|
||||
}
|
||||
43
backend/src/ee/services/pam-session/pam-session-fns.ts
Normal file
43
backend/src/ee/services/pam-session/pam-session-fns.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TPamSessions } from "@app/db/schemas";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TPamSanitizedSession, TPamSessionCommandLog } from "./pam-session.types";
|
||||
|
||||
export const decryptSessionCommandLogs = async ({
|
||||
orgId,
|
||||
encryptedLogs,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
encryptedLogs: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedLogs
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamSessionCommandLog;
|
||||
};
|
||||
|
||||
export const decryptSession = async (
|
||||
session: TPamSessions,
|
||||
orgId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
return {
|
||||
...session,
|
||||
commandLogs: session.encryptedLogsBlob
|
||||
? await decryptSessionCommandLogs({
|
||||
orgId,
|
||||
encryptedLogs: session.encryptedLogsBlob,
|
||||
kmsService
|
||||
})
|
||||
: []
|
||||
} as TPamSanitizedSession;
|
||||
};
|
||||
15
backend/src/ee/services/pam-session/pam-session-schemas.ts
Normal file
15
backend/src/ee/services/pam-session/pam-session-schemas.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamSessionsSchema } from "@app/db/schemas";
|
||||
|
||||
export const PamSessionCommandLogSchema = z.object({
|
||||
input: z.string(),
|
||||
output: z.string(),
|
||||
timestamp: z.coerce.date()
|
||||
});
|
||||
|
||||
export const SanitizedSessionSchema = PamSessionsSchema.omit({
|
||||
encryptedLogsBlob: true
|
||||
}).extend({
|
||||
commandLogs: PamSessionCommandLogSchema.array()
|
||||
});
|
||||
168
backend/src/ee/services/pam-session/pam-session-service.ts
Normal file
168
backend/src/ee/services/pam-session/pam-session-service.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { ProjectPermissionPamSessionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TUpdateSessionLogsDTO } from "./pam-session.types";
|
||||
import { TPamSessionDALFactory } from "./pam-session-dal";
|
||||
import { PamSessionStatus } from "./pam-session-enums";
|
||||
import { decryptSession } from "./pam-session-fns";
|
||||
|
||||
type TPamSessionServiceFactoryDep = {
|
||||
pamSessionDAL: TPamSessionDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TPamSessionServiceFactory = ReturnType<typeof pamSessionServiceFactory>;
|
||||
|
||||
export const pamSessionServiceFactory = ({
|
||||
pamSessionDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService
|
||||
}: TPamSessionServiceFactoryDep) => {
|
||||
const getById = async (sessionId: string, actor: OrgServiceActor) => {
|
||||
const session = await pamSessionDAL.findById(sessionId);
|
||||
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: session.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamSessionActions.Read,
|
||||
ProjectPermissionSub.PamSessions
|
||||
);
|
||||
|
||||
return {
|
||||
session: await decryptSession(session, actor.orgId, kmsService)
|
||||
};
|
||||
};
|
||||
|
||||
const list = async (projectId: string, actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamSessionActions.Read,
|
||||
ProjectPermissionSub.PamSessions
|
||||
);
|
||||
|
||||
const sessions = await pamSessionDAL.find({ projectId });
|
||||
|
||||
return {
|
||||
sessions: await Promise.all(sessions.map((session) => decryptSession(session, actor.orgId, kmsService)))
|
||||
};
|
||||
};
|
||||
|
||||
const updateLogsById = async ({ sessionId, logs }: TUpdateSessionLogsDTO, actor: OrgServiceActor) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
// To be hit by gateways only
|
||||
if (actor.type !== ActorType.IDENTITY) {
|
||||
throw new ForbiddenRequestError({ message: "Only gateways can perform this action" });
|
||||
}
|
||||
|
||||
const session = await pamSessionDAL.findById(sessionId);
|
||||
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
|
||||
|
||||
const project = await projectDAL.findById(session.projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
project.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: project.orgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(logs))
|
||||
});
|
||||
|
||||
const updatedSession = await pamSessionDAL.updateById(sessionId, {
|
||||
encryptedLogsBlob: cipherTextBlob
|
||||
});
|
||||
|
||||
return { session: updatedSession, projectId: project.id };
|
||||
};
|
||||
|
||||
const endSessionById = async (sessionId: string, actor: OrgServiceActor) => {
|
||||
// To be hit by gateways only
|
||||
if (actor.type !== ActorType.IDENTITY) {
|
||||
throw new ForbiddenRequestError({ message: "Only gateways can perform this action" });
|
||||
}
|
||||
|
||||
const session = await pamSessionDAL.findById(sessionId);
|
||||
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
|
||||
|
||||
const project = await projectDAL.findById(session.projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
project.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
if (session.status !== PamSessionStatus.Active) {
|
||||
throw new BadRequestError({ message: "Cannot end sessions that are not active" });
|
||||
}
|
||||
|
||||
const updatedSession = await pamSessionDAL.updateById(sessionId, {
|
||||
endedAt: new Date(),
|
||||
status: PamSessionStatus.Ended
|
||||
});
|
||||
|
||||
return { session: updatedSession, projectId: project.id };
|
||||
};
|
||||
|
||||
return { getById, list, updateLogsById, endSessionById };
|
||||
};
|
||||
12
backend/src/ee/services/pam-session/pam-session.types.ts
Normal file
12
backend/src/ee/services/pam-session/pam-session.types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "./pam-session-schemas";
|
||||
|
||||
export type TPamSessionCommandLog = z.infer<typeof PamSessionCommandLogSchema>;
|
||||
export type TPamSanitizedSession = z.infer<typeof SanitizedSessionSchema>;
|
||||
|
||||
// DTOs
|
||||
export type TUpdateSessionLogsDTO = {
|
||||
sessionId: string;
|
||||
logs: TPamSessionCommandLog[];
|
||||
};
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPamAccountActions,
|
||||
ProjectPermissionPamSessionActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiSyncActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
@@ -49,7 +51,9 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
ProjectPermissionSub.SshHostGroups,
|
||||
ProjectPermissionSub.PamFolders,
|
||||
ProjectPermissionSub.PamResources
|
||||
].forEach((el) => {
|
||||
can(
|
||||
[
|
||||
@@ -290,6 +294,19 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.AppConnections
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionPamAccountActions.Access,
|
||||
ProjectPermissionPamAccountActions.Read,
|
||||
ProjectPermissionPamAccountActions.Create,
|
||||
ProjectPermissionPamAccountActions.Edit,
|
||||
ProjectPermissionPamAccountActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.PamAccounts
|
||||
);
|
||||
|
||||
can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -518,6 +535,15 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(ProjectPermissionAppConnectionActions.Connect, ProjectPermissionSub.AppConnections);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PamFolders);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PamResources);
|
||||
|
||||
can(
|
||||
[ProjectPermissionPamAccountActions.Access, ProjectPermissionPamAccountActions.Read],
|
||||
ProjectPermissionSub.PamAccounts
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -579,6 +605,12 @@ const buildViewerPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretEvents
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PamFolders);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PamResources);
|
||||
|
||||
can([ProjectPermissionPamAccountActions.Read], ProjectPermissionSub.PamAccounts);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
||||
@@ -186,6 +186,19 @@ export enum ProjectPermissionAuditLogsActions {
|
||||
Read = "read"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionPamAccountActions {
|
||||
Access = "access",
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionPamSessionActions {
|
||||
Read = "read"
|
||||
// Terminate = "terminate"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSub {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
@@ -228,7 +241,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 = {
|
||||
@@ -300,6 +317,12 @@ export type AppConnectionSubjectFields = {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type PamAccountSubjectFields = {
|
||||
resourceName: string;
|
||||
accountName: string;
|
||||
accountPath: string;
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionSecretActions,
|
||||
@@ -404,7 +427,14 @@ 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];
|
||||
|
||||
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
@@ -427,6 +457,27 @@ const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
})
|
||||
.partial()
|
||||
]);
|
||||
const PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||
const PAM_ACCOUNT_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
z.string().refine((val) => val.startsWith("/"), SECRET_PATH_MISSING_SLASH_ERR_MSG),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ].refine(
|
||||
(val) => val.startsWith("/"),
|
||||
PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG
|
||||
),
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ].refine(
|
||||
(val) => val.startsWith("/"),
|
||||
PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG
|
||||
),
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN].refine(
|
||||
(val) => val.every((el) => el.startsWith("/")),
|
||||
PAM_ACCOUNT_PATH_MISSING_SLASH_ERR_MSG
|
||||
),
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]);
|
||||
// akhilmhdh: don't modify this for v2
|
||||
// if you want to update create a new schema
|
||||
const SecretConditionV1Schema = z
|
||||
@@ -650,6 +701,34 @@ const AppConnectionConditionSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const PamAccountConditionSchema = z
|
||||
.object({
|
||||
resourceName: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
accountName: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
accountPath: PAM_ACCOUNT_PATH_PERMISSION_OPERATOR_SCHEMA
|
||||
})
|
||||
.partial();
|
||||
|
||||
const GeneralPermissionSchema = [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||
@@ -840,6 +919,34 @@ const GeneralPermissionSchema = [
|
||||
conditions: AppConnectionConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PamFolders).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PamResources).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PamAccounts).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamAccountActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: PamAccountConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PamSessions).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@ import { licenseDALFactory } from "@app/ee/services/license/license-dal";
|
||||
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||
import { pamFolderDALFactory } from "@app/ee/services/pam-folder/pam-folder-dal";
|
||||
import { pamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
|
||||
import { pamAccountDALFactory } from "@app/ee/services/pam-resource/pam-account-dal";
|
||||
import { pamResourceDALFactory } from "@app/ee/services/pam-resource/pam-resource-dal";
|
||||
import { pamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
||||
import { pamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal";
|
||||
import { pamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service";
|
||||
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { pitServiceFactory } from "@app/ee/services/pit/pit-service";
|
||||
@@ -2099,6 +2106,37 @@ export const registerRoutes = async (
|
||||
appConnectionDAL
|
||||
});
|
||||
|
||||
const pamFolderDAL = pamFolderDALFactory(db);
|
||||
const pamResourceDAL = pamResourceDALFactory(db);
|
||||
const pamAccountDAL = pamAccountDALFactory(db);
|
||||
const pamSessionDAL = pamSessionDALFactory(db);
|
||||
|
||||
const pamFolderService = pamFolderServiceFactory({
|
||||
pamFolderDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const pamResourceService = pamResourceServiceFactory({
|
||||
pamResourceDAL,
|
||||
pamSessionDAL,
|
||||
pamAccountDAL,
|
||||
pamFolderDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
gatewayV2Service
|
||||
});
|
||||
|
||||
const pamSessionService = pamSessionServiceFactory({
|
||||
pamSessionDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// setup the communication with license key server
|
||||
await licenseService.init();
|
||||
|
||||
@@ -2236,7 +2274,10 @@ export const registerRoutes = async (
|
||||
reminder: reminderService,
|
||||
bus: eventBusService,
|
||||
sse: sseService,
|
||||
notification: notificationService
|
||||
notification: notificationService,
|
||||
pamFolder: pamFolderService,
|
||||
pamResource: pamResourceService,
|
||||
pamSession: pamSessionService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
@@ -213,6 +213,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
return false;
|
||||
case ProjectType.SSH:
|
||||
return false;
|
||||
case ProjectType.PAM:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user