mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 16:52:38 +00:00
Merge pull request #4590 from Infisical/ENG-3723
feat(pam): PAM Platform V1
This commit is contained in:
@@ -23,6 +23,12 @@ import { registerLdapRouter } from "./ldap-router";
|
||||
import { registerLicenseRouter } from "./license-router";
|
||||
import { registerOidcRouter } from "./oidc-router";
|
||||
import { registerOrgRoleRouter } from "./org-role-router";
|
||||
import { PAM_ACCOUNT_REGISTER_ROUTER_MAP } from "./pam-account-routers";
|
||||
import { registerPamAccountRouter } from "./pam-account-routers/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 +172,40 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ prefix: "/kmip" }
|
||||
);
|
||||
|
||||
await server.register(
|
||||
async (pamRouter) => {
|
||||
await pamRouter.register(registerPamFolderRouter, { prefix: "/folders" });
|
||||
await pamRouter.register(registerPamSessionRouter, { prefix: "/sessions" });
|
||||
|
||||
await pamRouter.register(
|
||||
async (pamAccountRouter) => {
|
||||
await pamAccountRouter.register(registerPamAccountRouter);
|
||||
|
||||
// Provider-specific endpoints
|
||||
await Promise.all(
|
||||
Object.entries(PAM_ACCOUNT_REGISTER_ROUTER_MAP).map(([provider, router]) =>
|
||||
pamAccountRouter.register(router, { prefix: `/${provider}` })
|
||||
)
|
||||
);
|
||||
},
|
||||
{ prefix: "/accounts" }
|
||||
);
|
||||
|
||||
await pamRouter.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: "/resources" }
|
||||
);
|
||||
},
|
||||
{ prefix: "/pam" }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import {
|
||||
CreatePostgresAccountSchema,
|
||||
SanitizedPostgresAccountWithResourceSchema,
|
||||
UpdatePostgresAccountSchema
|
||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
|
||||
import { registerPamResourceEndpoints } from "./pam-account-endpoints";
|
||||
|
||||
export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: FastifyZodProvider) => Promise<void>> = {
|
||||
[PamResource.Postgres]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.Postgres,
|
||||
accountResponseSchema: SanitizedPostgresAccountWithResourceSchema,
|
||||
createAccountSchema: CreatePostgresAccountSchema,
|
||||
updateAccountSchema: UpdatePostgresAccountSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
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 } from "@app/ee/services/pam-resource/pam-resource-types";
|
||||
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 registerPamResourceEndpoints = <C extends TPamAccount>({
|
||||
server,
|
||||
resourceType,
|
||||
createAccountSchema,
|
||||
updateAccountSchema,
|
||||
accountResponseSchema
|
||||
}: {
|
||||
server: FastifyZodProvider;
|
||||
resourceType: PamResource;
|
||||
createAccountSchema: z.ZodType<{
|
||||
credentials: C["credentials"];
|
||||
resourceId: C["resourceId"];
|
||||
folderId?: C["folderId"];
|
||||
name: C["name"];
|
||||
description?: C["description"];
|
||||
}>;
|
||||
updateAccountSchema: z.ZodType<{
|
||||
credentials?: C["credentials"];
|
||||
name?: C["name"];
|
||||
description?: C["description"];
|
||||
}>;
|
||||
accountResponseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create PAM account",
|
||||
body: createAccountSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
account: accountResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const account = await server.services.pamAccount.create(req.body, 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.body.resourceId,
|
||||
resourceType,
|
||||
folderId: req.body.folderId,
|
||||
name: req.body.name,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { account };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:accountId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update PAM account",
|
||||
params: z.object({
|
||||
accountId: z.string().uuid()
|
||||
}),
|
||||
body: updateAccountSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
account: accountResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const account = await server.services.pamAccount.updateById(
|
||||
{
|
||||
...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: account.resourceId,
|
||||
resourceType,
|
||||
name: req.body.name,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { account };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:accountId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete PAM account",
|
||||
params: z.object({
|
||||
accountId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
account: accountResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const account = await server.services.pamAccount.deleteById(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,
|
||||
accountName: account.name,
|
||||
resourceId: account.resourceId,
|
||||
resourceType
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { account };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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.pamAccount.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_ACCOUNT_LIST,
|
||||
metadata: {
|
||||
accountCount: response.accounts.length,
|
||||
folderCount: response.folders.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/access",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Access PAM account",
|
||||
body: z.object({
|
||||
accountId: z.string().uuid(),
|
||||
duration: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((val, ctx) => {
|
||||
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),
|
||||
relayClientCertificate: z.string(),
|
||||
relayClientPrivateKey: z.string(),
|
||||
relayServerCertificateChain: z.string(),
|
||||
gatewayClientCertificate: z.string(),
|
||||
gatewayClientPrivateKey: z.string(),
|
||||
gatewayServerCertificateChain: z.string(),
|
||||
relayHost: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
// To prevent type errors when accessing req.auth
|
||||
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.pamAccount.access(
|
||||
{
|
||||
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.body.accountId,
|
||||
accountName: response.account.name,
|
||||
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
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: {
|
||||
folderName: folder.name,
|
||||
folderId: req.params.folderId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { folder };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import {
|
||||
CreatePostgresResourceSchema,
|
||||
PostgresResourceSchema,
|
||||
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,
|
||||
createResourceSchema: CreatePostgresResourceSchema,
|
||||
updateResourceSchema: UpdatePostgresResourceSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
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 { 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>({
|
||||
server,
|
||||
resourceType,
|
||||
createResourceSchema,
|
||||
updateResourceSchema,
|
||||
resourceResponseSchema
|
||||
}: {
|
||||
server: FastifyZodProvider;
|
||||
resourceType: PamResource;
|
||||
createResourceSchema: z.ZodType<{
|
||||
projectId: T["projectId"];
|
||||
connectionDetails: T["connectionDetails"];
|
||||
gatewayId: T["gatewayId"];
|
||||
name: T["name"];
|
||||
}>;
|
||||
updateResourceSchema: z.ZodType<{
|
||||
connectionDetails?: T["connectionDetails"];
|
||||
gatewayId?: T["gatewayId"];
|
||||
name?: T["name"];
|
||||
}>;
|
||||
resourceResponseSchema: 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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
PostgresResourceListItemSchema,
|
||||
PostgresResourceSchema
|
||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// Use z.union([...]) when more resources are added
|
||||
const ResourceSchema = PostgresResourceSchema;
|
||||
|
||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
|
||||
|
||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/options",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List PAM resource types",
|
||||
response: {
|
||||
200: z.object({
|
||||
resourceOptions: ResourceOptionsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: () => {
|
||||
const resourceOptions = server.services.pamResource.listResourceOptions();
|
||||
|
||||
return { resourceOptions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List PAM resources",
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
resources: ResourceSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const response = await server.services.pamResource.list(req.query.projectId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.PAM_RESOURCE_LIST,
|
||||
metadata: {
|
||||
count: response.resources.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
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, account } = await server.services.pamAccount.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,
|
||||
accountName: account.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
accountName: session.accountName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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.JWT, 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,
|
||||
accountName: session.accountName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import z from "zod";
|
||||
|
||||
import { GatewaysV2Schema } from "@app/db/schemas";
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -130,4 +131,25 @@ export const registerGatewayV2Router = async (server: FastifyZodProvider) => {
|
||||
return gateway;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/pam-session-key",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: zodBuffer
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const pamSessionKey = await server.services.gatewayV2.getPamSessionKey({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
|
||||
return pamSessionKey;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,162 @@ interface OrgRoleDeleteEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionStartEvent {
|
||||
type: EventType.PAM_SESSION_START;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
accountName: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionLogsUpdateEvent {
|
||||
type: EventType.PAM_SESSION_LOGS_UPDATE;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
accountName: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamSessionEndEvent {
|
||||
type: EventType.PAM_SESSION_END;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
accountName: 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;
|
||||
folderName: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamAccountListEvent {
|
||||
type: EventType.PAM_ACCOUNT_LIST;
|
||||
metadata: {
|
||||
accountCount: number;
|
||||
folderCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface PamAccountAccessEvent {
|
||||
type: EventType.PAM_ACCOUNT_ACCESS;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
accountName: 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: {
|
||||
accountName: string;
|
||||
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 +4195,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;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export const GATEWAY_ROUTING_INFO_OID = "1.3.6.1.4.1.12345.100.1";
|
||||
export const GATEWAY_ACTOR_OID = "1.3.6.1.4.1.12345.100.2";
|
||||
export const PAM_INFO_OID = "1.3.6.1.4.1.12345.100.3";
|
||||
|
||||
@@ -22,11 +22,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { TRelayDALFactory } from "../relay/relay-dal";
|
||||
import { TRelayServiceFactory } from "../relay/relay-service";
|
||||
import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID } from "./gateway-v2-constants";
|
||||
import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID, PAM_INFO_OID } from "./gateway-v2-constants";
|
||||
import { TGatewayV2DALFactory } from "./gateway-v2-dal";
|
||||
import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal";
|
||||
|
||||
@@ -414,6 +415,176 @@ export const gatewayV2ServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getPAMConnectionDetails = async ({
|
||||
gatewayId,
|
||||
sessionId,
|
||||
duration,
|
||||
resourceType,
|
||||
host,
|
||||
port,
|
||||
actorMetadata
|
||||
}: {
|
||||
gatewayId: string;
|
||||
sessionId: string;
|
||||
resourceType: PamResource;
|
||||
duration?: number;
|
||||
host: string;
|
||||
port: number;
|
||||
actorMetadata: { id: string; type: ActorType; name: string };
|
||||
}) => {
|
||||
const gateway = await gatewayV2DAL.findById(gatewayId);
|
||||
if (!gateway) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orgGatewayConfig = await orgGatewayConfigV2DAL.findOne({ orgId: gateway.orgId });
|
||||
if (!orgGatewayConfig) {
|
||||
throw new NotFoundError({ message: `Gateway Config for org ${gateway.orgId} not found.` });
|
||||
}
|
||||
|
||||
if (!gateway.relayId) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway is not associated with a relay"
|
||||
});
|
||||
}
|
||||
|
||||
const orgLicensePlan = await licenseService.getPlan(orgGatewayConfig.orgId);
|
||||
if (!orgLicensePlan.gateway) {
|
||||
throw new BadRequestError({
|
||||
message: "Please upgrade your instance to Infisical's Enterprise plan to use gateways."
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
});
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
|
||||
const rootGatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayClientCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayServerCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayServerCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayClientCaPrivateKey = orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaPrivateKey
|
||||
});
|
||||
|
||||
const gatewayClientCaSkObj = crypto.nativeCrypto.createPrivateKey({
|
||||
key: gatewayClientCaPrivateKey,
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
const importedGatewayClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
gatewayClientCaSkObj.export({ format: "der", type: "pkcs8" }),
|
||||
alg,
|
||||
true,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const clientCertIssuedAt = new Date();
|
||||
const clientCertExpiration = new Date(new Date().getTime() + (duration ?? 5 * 60 * 1000));
|
||||
const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCertSerialNumber = createSerialNumber();
|
||||
|
||||
const routingInfo = {
|
||||
targetHost: host,
|
||||
targetPort: port
|
||||
};
|
||||
|
||||
const routingExtension = new x509.Extension(
|
||||
GATEWAY_ROUTING_INFO_OID,
|
||||
false,
|
||||
Buffer.from(JSON.stringify(routingInfo))
|
||||
);
|
||||
|
||||
const pamInfoExtension = new x509.Extension(
|
||||
PAM_INFO_OID,
|
||||
false,
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
sessionId,
|
||||
resourceType
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const actorExtension = new x509.Extension(
|
||||
GATEWAY_ACTOR_OID,
|
||||
false,
|
||||
Buffer.from(JSON.stringify({ type: actorMetadata.type, id: actorMetadata.id, name: actorMetadata.name }))
|
||||
);
|
||||
|
||||
const clientCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientCertSerialNumber,
|
||||
subject: `O=${orgGatewayConfig.orgId},OU=gateway-client,CN=${actorMetadata.type}:${gatewayId}`,
|
||||
issuer: gatewayClientCaCert.subject,
|
||||
notAfter: clientCertExpiration,
|
||||
notBefore: clientCertIssuedAt,
|
||||
signingKey: importedGatewayClientCaPrivateKey,
|
||||
publicKey: clientKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(gatewayClientCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true),
|
||||
routingExtension,
|
||||
actorExtension,
|
||||
pamInfoExtension
|
||||
]
|
||||
});
|
||||
|
||||
const gatewayClientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
|
||||
|
||||
const relayCredentials = await relayService.getCredentialsForClient({
|
||||
relayId: gateway.relayId,
|
||||
orgId: gateway.orgId,
|
||||
orgName: gateway.orgName,
|
||||
gatewayId,
|
||||
gatewayName: gateway.name,
|
||||
duration
|
||||
});
|
||||
|
||||
return {
|
||||
relayHost: relayCredentials.relayHost,
|
||||
gateway: {
|
||||
clientCertificate: clientCert.toString("pem"),
|
||||
clientPrivateKey: gatewayClientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(),
|
||||
serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert])
|
||||
},
|
||||
relay: {
|
||||
clientCertificate: relayCredentials.clientCertificate,
|
||||
clientPrivateKey: relayCredentials.clientPrivateKey,
|
||||
serverCertificateChain: relayCredentials.serverCertificateChain
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const registerGateway = async ({
|
||||
orgId,
|
||||
actorId,
|
||||
@@ -645,14 +816,75 @@ export const gatewayV2ServiceFactory = ({
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
return gatewayV2DAL.deleteById(gateway.id);
|
||||
try {
|
||||
return await gatewayV2DAL.deleteById(gateway.id);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof DatabaseError &&
|
||||
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete gateway because it is attached to active resources"
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
return gatewayV2DAL.transaction(async (tx) => {
|
||||
const gateway = await gatewayV2DAL.findOne(
|
||||
{
|
||||
identityId: orgPermission.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!gateway) {
|
||||
throw new NotFoundError({ message: "Gateway not found" });
|
||||
}
|
||||
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
if (gateway.encryptedPamSessionKey) {
|
||||
return decryptor({ cipherTextBlob: gateway.encryptedPamSessionKey });
|
||||
}
|
||||
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.GatewayPamSessionKey(gateway.id)]);
|
||||
|
||||
const newPamSessionKey = crypto.randomBytes(32);
|
||||
const { cipherTextBlob: encryptedPamSessionKey } = encryptor({ plainText: newPamSessionKey });
|
||||
|
||||
await gatewayV2DAL.updateById(gateway.id, { encryptedPamSessionKey }, tx);
|
||||
|
||||
return newPamSessionKey;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
listGateways,
|
||||
registerGateway,
|
||||
getPlatformConnectionDetailsByGatewayId,
|
||||
getPAMConnectionDetails,
|
||||
deleteGatewayById,
|
||||
heartbeat
|
||||
heartbeat,
|
||||
getPamSessionKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -66,7 +66,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
enterpriseAppConnections: false,
|
||||
fips: false,
|
||||
eventSubscriptions: false,
|
||||
machineIdentityAuthTemplates: false
|
||||
machineIdentityAuthTemplates: false,
|
||||
pam: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (
|
||||
|
||||
@@ -80,6 +80,7 @@ export type TFeatureSet = {
|
||||
machineIdentityAuthTemplates: false;
|
||||
fips: false;
|
||||
eventSubscriptions: false;
|
||||
pam: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
|
||||
|
||||
export const encryptAccountCredentials = async ({
|
||||
projectId,
|
||||
credentials,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
credentials: TPamAccountCredentials;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(credentials))
|
||||
});
|
||||
|
||||
return encryptedCredentialsBlob;
|
||||
};
|
||||
|
||||
export const decryptAccountCredentials = async ({
|
||||
projectId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
encryptedCredentials: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedCredentials
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
||||
};
|
||||
|
||||
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
|
||||
account: T,
|
||||
projectId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<T & { credentials: TPamAccountCredentials }> => {
|
||||
return {
|
||||
...account,
|
||||
credentials: await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
projectId,
|
||||
kmsService
|
||||
})
|
||||
} as T & { credentials: TPamAccountCredentials };
|
||||
};
|
||||
@@ -0,0 +1,527 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas";
|
||||
import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory";
|
||||
import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionPamAccountActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, 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 { TUserDALFactory } from "@app/services/user/user-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 { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal";
|
||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
|
||||
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 { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns";
|
||||
import { TAccessAccountDTO, TCreateAccountDTO, TUpdateAccountDTO } from "./pam-account-types";
|
||||
|
||||
type TPamAccountServiceFactoryDep = {
|
||||
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,
|
||||
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
|
||||
>;
|
||||
userDAL: TUserDALFactory;
|
||||
};
|
||||
|
||||
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
|
||||
|
||||
export const pamAccountServiceFactory = ({
|
||||
pamResourceDAL,
|
||||
pamSessionDAL,
|
||||
pamAccountDAL,
|
||||
pamFolderDAL,
|
||||
projectDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
gatewayV2Service
|
||||
}: TPamAccountServiceFactoryDep) => {
|
||||
const create = 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({
|
||||
projectId: resource.projectId,
|
||||
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,
|
||||
projectId: resource.projectId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
try {
|
||||
const account = await pamAccountDAL.create({
|
||||
projectId: resource.projectId,
|
||||
resourceId: resource.id,
|
||||
encryptedCredentials,
|
||||
name,
|
||||
description,
|
||||
folderId
|
||||
});
|
||||
|
||||
return {
|
||||
...(await decryptAccount(account, resource.projectId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `Account with name '${name}' already exists for this path`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const updateById = 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({
|
||||
projectId: account.projectId,
|
||||
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,
|
||||
projectId: account.projectId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
finalCredentials.password = decryptedCredentials.password;
|
||||
}
|
||||
|
||||
const validatedCredentials = await factory.validateAccountCredentials(finalCredentials);
|
||||
const encryptedCredentials = await encryptAccountCredentials({
|
||||
credentials: validatedCredentials,
|
||||
projectId: account.projectId,
|
||||
kmsService
|
||||
});
|
||||
updateDoc.encryptedCredentials = encryptedCredentials;
|
||||
}
|
||||
|
||||
// If nothing was updated, return the fetched account
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return decryptAccount(account, account.projectId, kmsService);
|
||||
}
|
||||
|
||||
const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc);
|
||||
|
||||
return {
|
||||
...(await decryptAccount(updatedAccount, account.projectId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
const deleteById = 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, account.projectId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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, account.projectId, kmsService);
|
||||
decryptedAndPermittedAccounts.push({
|
||||
...decryptedAccount,
|
||||
resource: {
|
||||
id: account.resource.id,
|
||||
name: account.resource.name,
|
||||
resourceType: account.resource.resourceType
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accounts: decryptedAndPermittedAccounts,
|
||||
folders
|
||||
};
|
||||
};
|
||||
|
||||
const access = 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: new Date(Date.now() + duration)
|
||||
});
|
||||
|
||||
const { connectionDetails, gatewayId, resourceType } = await decryptResource(
|
||||
resource,
|
||||
account.projectId,
|
||||
kmsService
|
||||
);
|
||||
|
||||
const user = await userDAL.findById(actor.id);
|
||||
if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` });
|
||||
|
||||
const gatewayConnectionDetails = await gatewayV2Service.getPAMConnectionDetails({
|
||||
gatewayId,
|
||||
duration,
|
||||
sessionId: session.id,
|
||||
resourceType: resource.resourceType as PamResource,
|
||||
host: connectionDetails.host,
|
||||
port: connectionDetails.port,
|
||||
actorMetadata: {
|
||||
id: actor.id,
|
||||
type: actor.type,
|
||||
name: user.email ?? ""
|
||||
}
|
||||
});
|
||||
|
||||
if (!gatewayConnectionDetails) {
|
||||
throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` });
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
resourceType,
|
||||
relayClientCertificate: gatewayConnectionDetails.relay.clientCertificate,
|
||||
relayClientPrivateKey: gatewayConnectionDetails.relay.clientPrivateKey,
|
||||
relayServerCertificateChain: gatewayConnectionDetails.relay.serverCertificateChain,
|
||||
gatewayClientCertificate: gatewayConnectionDetails.gateway.clientCertificate,
|
||||
gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey,
|
||||
gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain,
|
||||
relayHost: gatewayConnectionDetails.relayHost,
|
||||
projectId: account.projectId,
|
||||
account
|
||||
};
|
||||
};
|
||||
|
||||
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` });
|
||||
|
||||
if (resource.gatewayIdentityId !== actor.id) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Identity does not have access to fetch the PAM session credentials"
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedAccount = await decryptAccount(account, session.projectId, kmsService);
|
||||
|
||||
const decryptedResource = await decryptResource(resource, session.projectId, kmsService);
|
||||
|
||||
// Mark session as started
|
||||
await pamSessionDAL.updateById(sessionId, {
|
||||
status: PamSessionStatus.Active,
|
||||
startedAt: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
credentials: {
|
||||
...decryptedResource.connectionDetails,
|
||||
...decryptedAccount.credentials
|
||||
},
|
||||
projectId: project.id,
|
||||
account
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
list,
|
||||
access,
|
||||
getSessionCredentials
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TPamAccount } from "../pam-resource/pam-resource-types";
|
||||
|
||||
// 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;
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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("/")}`;
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
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 { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, 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}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const folder = await pamFolderDAL.create({
|
||||
name,
|
||||
description: description ?? null,
|
||||
parentId: parentId || null,
|
||||
projectId
|
||||
});
|
||||
|
||||
return folder;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `Folder with name '${name}' already exists for this path`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
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 (Object.keys(updateDoc).length === 0) {
|
||||
return folder;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedFolder = await pamFolderDAL.updateById(id, updateDoc);
|
||||
|
||||
return updatedFolder;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `Folder with name '${name}' already exists for this path`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
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 };
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TPamResourceDALFactory = ReturnType<typeof pamResourceDALFactory>;
|
||||
export const pamResourceDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.PamResource);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
const doc = await (tx || db.replicaNode())(TableName.PamResource)
|
||||
.join(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`)
|
||||
.select(selectAllTableCols(TableName.PamResource))
|
||||
.select(db.ref("name").withSchema(TableName.GatewayV2).as("gatewayName"))
|
||||
.select(db.ref("identityId").withSchema(TableName.GatewayV2).as("gatewayIdentityId"))
|
||||
.where(`${TableName.PamResource}.id`, id)
|
||||
.first();
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
return { ...orm, findById };
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { TPamResources } from "@app/db/schemas";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { 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 ({
|
||||
projectId,
|
||||
connectionDetails,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
connectionDetails: TPamResourceConnectionDetails;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedConnectionDetailsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(connectionDetails))
|
||||
});
|
||||
|
||||
return encryptedConnectionDetailsBlob;
|
||||
};
|
||||
|
||||
export const decryptResourceConnectionDetails = async ({
|
||||
projectId,
|
||||
encryptedConnectionDetails,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
encryptedConnectionDetails: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedConnectionDetails
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamResourceConnectionDetails;
|
||||
};
|
||||
|
||||
export const decryptResource = async (
|
||||
resource: TPamResources,
|
||||
projectId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
return {
|
||||
...resource,
|
||||
connectionDetails: await decryptResourceConnectionDetails({
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
projectId,
|
||||
kmsService
|
||||
})
|
||||
} as TPamResource;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamAccountsSchema, PamResourcesSchema } from "@app/db/schemas";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
// Resources
|
||||
export const BasePamResourceSchema = 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({
|
||||
resourceId: z.string().uuid(),
|
||||
folderId: z.string().uuid().optional(),
|
||||
name: slugSchema({ field: "name" }),
|
||||
description: z.string().max(512).nullable().optional()
|
||||
});
|
||||
|
||||
export const BaseUpdatePamAccountSchema = z.object({
|
||||
name: slugSchema({ field: "name" }).optional(),
|
||||
description: z.string().max(512).nullable().optional()
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TPamResources } 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 { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPamResourceDALFactory } from "./pam-resource-dal";
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
|
||||
import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns";
|
||||
import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types";
|
||||
|
||||
type TPamResourceServiceFactoryDep = {
|
||||
pamResourceDAL: TPamResourceDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
gatewayV2Service: Pick<
|
||||
TGatewayV2ServiceFactory,
|
||||
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
|
||||
>;
|
||||
};
|
||||
|
||||
export type TPamResourceServiceFactory = ReturnType<typeof pamResourceServiceFactory>;
|
||||
|
||||
export const pamResourceServiceFactory = ({
|
||||
pamResourceDAL,
|
||||
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, resource.projectId, 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,
|
||||
projectId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const resource = await pamResourceDAL.create({
|
||||
resourceType,
|
||||
encryptedConnectionDetails,
|
||||
gatewayId,
|
||||
name,
|
||||
projectId
|
||||
});
|
||||
|
||||
return decryptResource(resource, projectId, 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,
|
||||
projectId: resource.projectId,
|
||||
kmsService
|
||||
});
|
||||
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
|
||||
}
|
||||
|
||||
// If nothing was updated, return the fetched resource
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return decryptResource(resource, resource.projectId, kmsService);
|
||||
}
|
||||
|
||||
const updatedResource = await pamResourceDAL.updateById(resourceId, updateDoc);
|
||||
|
||||
return decryptResource(updatedResource, resource.projectId, 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);
|
||||
|
||||
try {
|
||||
const deletedResource = await pamResourceDAL.deleteById(id);
|
||||
return await decryptResource(deletedResource, resource.projectId, kmsService);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof DatabaseError &&
|
||||
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete resource because it is attached to active PAM accounts"
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
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, projectId, kmsService)))
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getById,
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
list,
|
||||
listResourceOptions
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
};
|
||||
|
||||
// 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,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import {
|
||||
BaseSqlAccountCredentialsSchema,
|
||||
BaseSqlResourceConnectionDetailsSchema
|
||||
} from "../shared/sql/sql-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export const PostgresResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema;
|
||||
|
||||
const BasePostgresResourceSchema = BasePamResourceSchema.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;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TPamSessionDALFactory = ReturnType<typeof pamSessionDALFactory>;
|
||||
export const pamSessionDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.PamSession);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
const session = await (tx || db.replicaNode())(TableName.PamSession)
|
||||
.leftJoin(TableName.PamAccount, `${TableName.PamSession}.accountId`, `${TableName.PamAccount}.id`)
|
||||
.leftJoin(TableName.PamResource, `${TableName.PamAccount}.resourceId`, `${TableName.PamResource}.id`)
|
||||
.leftJoin(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`)
|
||||
.select(selectAllTableCols(TableName.PamSession))
|
||||
.select(db.ref("name").withSchema(TableName.GatewayV2).as("gatewayName"))
|
||||
.select(db.ref("identityId").withSchema(TableName.GatewayV2).as("gatewayIdentityId"))
|
||||
.where(`${TableName.PamSession}.id`, id)
|
||||
.first();
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
return { ...orm, findById };
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ({
|
||||
projectId,
|
||||
encryptedLogs,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
encryptedLogs: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedLogs
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamSessionCommandLog;
|
||||
};
|
||||
|
||||
export const decryptSession = async (
|
||||
session: TPamSessions,
|
||||
projectId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
return {
|
||||
...session,
|
||||
commandLogs: session.encryptedLogsBlob
|
||||
? await decryptSessionCommandLogs({
|
||||
projectId,
|
||||
encryptedLogs: session.encryptedLogsBlob,
|
||||
kmsService
|
||||
})
|
||||
: []
|
||||
} as TPamSanitizedSession;
|
||||
};
|
||||
@@ -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()
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
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, session.projectId, 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, projectId, 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` });
|
||||
|
||||
if (session.encryptedLogsBlob) {
|
||||
throw new BadRequestError({ message: "Cannot update logs for sessions with existing logs" });
|
||||
}
|
||||
|
||||
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.gatewayIdentityId !== actor.id) {
|
||||
throw new ForbiddenRequestError({ message: "Identity does not have access to update logs for this session" });
|
||||
}
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: session.projectId
|
||||
});
|
||||
|
||||
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) => {
|
||||
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
|
||||
);
|
||||
|
||||
if (actor.type === ActorType.IDENTITY) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
if (session.gatewayIdentityId !== actor.id) {
|
||||
throw new ForbiddenRequestError({ message: "Identity does not have access to end this session" });
|
||||
}
|
||||
} else if (actor.type === ActorType.USER) {
|
||||
if (session.userId !== actor.id) {
|
||||
throw new ForbiddenRequestError({ message: "You are not authorized to end this session" });
|
||||
}
|
||||
} else {
|
||||
throw new ForbiddenRequestError({ message: "Only identities and users can perform this action" });
|
||||
}
|
||||
|
||||
if (session.status === PamSessionStatus.Ended) {
|
||||
return {
|
||||
session,
|
||||
projectId: project.id
|
||||
};
|
||||
}
|
||||
|
||||
if (session.status !== PamSessionStatus.Active && session.status !== PamSessionStatus.Starting) {
|
||||
throw new BadRequestError({ message: "Cannot end sessions that are not active or starting" });
|
||||
}
|
||||
|
||||
const updatedSession = await pamSessionDAL.updateById(sessionId, {
|
||||
endedAt: new Date(),
|
||||
status: PamSessionStatus.Ended
|
||||
});
|
||||
|
||||
return { session: updatedSession, projectId: project.id };
|
||||
};
|
||||
|
||||
return { getById, list, updateLogsById, endSessionById };
|
||||
};
|
||||
@@ -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."
|
||||
)
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
@@ -708,7 +708,8 @@ export const relayServiceFactory = ({
|
||||
relayPkiClientCaCertificate,
|
||||
relayPkiClientCaPrivateKey,
|
||||
relayPkiServerCaCertificate,
|
||||
relayPkiServerCaCertificateChain
|
||||
relayPkiServerCaCertificateChain,
|
||||
duration
|
||||
}: {
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
@@ -718,6 +719,7 @@ export const relayServiceFactory = ({
|
||||
relayPkiClientCaPrivateKey: Buffer;
|
||||
relayPkiServerCaCertificate: Buffer;
|
||||
relayPkiServerCaCertificateChain: Buffer;
|
||||
duration?: number;
|
||||
}) => {
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
const relayClientCaCert = new x509.X509Certificate(relayPkiClientCaCertificate);
|
||||
@@ -737,7 +739,7 @@ export const relayServiceFactory = ({
|
||||
);
|
||||
|
||||
const clientCertIssuedAt = new Date();
|
||||
const clientCertExpiration = new Date(new Date().getTime() + 5 * 60 * 1000);
|
||||
const clientCertExpiration = new Date(new Date().getTime() + (duration ?? 5 * 60 * 1000));
|
||||
const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
|
||||
const clientCertSerialNumber = createSerialNumber();
|
||||
@@ -866,13 +868,15 @@ export const relayServiceFactory = ({
|
||||
orgId,
|
||||
orgName,
|
||||
gatewayId,
|
||||
gatewayName
|
||||
gatewayName,
|
||||
duration
|
||||
}: {
|
||||
relayId: string;
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
duration?: number;
|
||||
}) => {
|
||||
const relay = await relayDAL.findOne({
|
||||
id: relayId
|
||||
@@ -896,7 +900,8 @@ export const relayServiceFactory = ({
|
||||
relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate,
|
||||
relayPkiClientCaPrivateKey: instanceCAs.instanceRelayPkiClientCaPrivateKey,
|
||||
relayPkiServerCaCertificate: instanceCAs.instanceRelayPkiServerCaCertificate,
|
||||
relayPkiServerCaCertificateChain: instanceCAs.instanceRelayPkiServerCaCertificateChain
|
||||
relayPkiServerCaCertificateChain: instanceCAs.instanceRelayPkiServerCaCertificateChain,
|
||||
duration
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -914,7 +919,8 @@ export const relayServiceFactory = ({
|
||||
relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate,
|
||||
relayPkiClientCaPrivateKey: orgCAs.relayPkiClientCaPrivateKey,
|
||||
relayPkiServerCaCertificate: orgCAs.relayPkiServerCaCertificate,
|
||||
relayPkiServerCaCertificateChain: orgCAs.relayPkiServerCaCertificateChain
|
||||
relayPkiServerCaCertificateChain: orgCAs.relayPkiServerCaCertificateChain,
|
||||
duration
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user