mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
addressed reviews
This commit is contained in:
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -28,6 +28,7 @@ import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
|
||||
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||
import { TPamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service";
|
||||
import { TPamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
|
||||
import { TPamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
||||
import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service";
|
||||
@@ -320,6 +321,7 @@ declare module "fastify" {
|
||||
offlineUsageReport: TOfflineUsageReportServiceFactory;
|
||||
pamFolder: TPamFolderServiceFactory;
|
||||
pamResource: TPamResourceServiceFactory;
|
||||
pamAccount: TPamAccountServiceFactory;
|
||||
pamSession: TPamSessionServiceFactory;
|
||||
upgradePath: TUpgradePathService;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// PAM Folders
|
||||
@@ -18,6 +19,19 @@ export async function up(knex: Knex): Promise<void> {
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.index("name");
|
||||
|
||||
// Enforce uniqueness for sub-folders
|
||||
t.unique(["projectId", "parentId", "name"], {
|
||||
indexName: "uidx_pam_folder_children_name",
|
||||
predicate: knex.whereNotNull("parentId")
|
||||
});
|
||||
|
||||
// Enforce uniqueness for root-level folders
|
||||
t.unique(["projectId", "name"], {
|
||||
indexName: "uidx_pam_folder_root_name",
|
||||
predicate: knex.whereNull("parentId")
|
||||
});
|
||||
|
||||
t.text("description").nullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
@@ -35,10 +49,14 @@ export async function up(knex: Knex): Promise<void> {
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.index("name");
|
||||
t.string("gatewayId").notNullable();
|
||||
|
||||
t.uuid("gatewayId").notNullable();
|
||||
t.foreign("gatewayId").references("id").inTable(TableName.GatewayV2);
|
||||
t.index("gatewayId");
|
||||
|
||||
t.string("resourceType").notNullable();
|
||||
t.index("resourceType");
|
||||
|
||||
t.binary("encryptedConnectionDetails").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
@@ -59,13 +77,25 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.index("folderId");
|
||||
|
||||
t.uuid("resourceId").notNullable();
|
||||
t.foreign("resourceId").references("id").inTable(TableName.PamResource).onDelete("CASCADE");
|
||||
t.foreign("resourceId").references("id").inTable(TableName.PamResource);
|
||||
t.index("resourceId");
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.index("name");
|
||||
t.text("description").nullable();
|
||||
|
||||
// Enforce uniqueness for folders
|
||||
t.unique(["projectId", "folderId", "name"], {
|
||||
indexName: "uidx_pam_account_children_name",
|
||||
predicate: knex.whereNotNull("folderId")
|
||||
});
|
||||
|
||||
// Enforce uniqueness for root-level
|
||||
t.unique(["projectId", "name"], {
|
||||
indexName: "uidx_pam_account_root_name",
|
||||
predicate: knex.whereNull("folderId")
|
||||
});
|
||||
|
||||
t.text("description").nullable();
|
||||
t.binary("encryptedCredentials").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
@@ -106,7 +136,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
|
||||
t.binary("encryptedLogsBlob").nullable();
|
||||
|
||||
t.datetime("expiresAt").nullable(); // null means unlimited duration / no expiry
|
||||
t.datetime("expiresAt").notNullable();
|
||||
|
||||
t.datetime("startedAt").nullable(); // Not when the row is created, but when the end-to-end connection between user and resource is established
|
||||
t.datetime("endedAt").nullable();
|
||||
@@ -115,6 +145,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.PamFolder);
|
||||
await createOnUpdateTrigger(knex, TableName.PamResource);
|
||||
await createOnUpdateTrigger(knex, TableName.PamAccount);
|
||||
await createOnUpdateTrigger(knex, TableName.PamSession);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
@@ -122,4 +157,9 @@ export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.PamAccount);
|
||||
await knex.schema.dropTableIfExists(TableName.PamResource);
|
||||
await knex.schema.dropTableIfExists(TableName.PamFolder);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.PamSession);
|
||||
await dropOnUpdateTrigger(knex, TableName.PamAccount);
|
||||
await dropOnUpdateTrigger(knex, TableName.PamResource);
|
||||
await dropOnUpdateTrigger(knex, TableName.PamFolder);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import { registerLdapRouter } from "./ldap-router";
|
||||
import { registerLicenseRouter } from "./license-router";
|
||||
import { registerOidcRouter } from "./oidc-router";
|
||||
import { registerOrgRoleRouter } from "./org-role-router";
|
||||
import { registerPamAccountRouter } from "./pam-account-router";
|
||||
import { 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";
|
||||
@@ -172,21 +173,39 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
{ prefix: "/kmip" }
|
||||
);
|
||||
|
||||
await server.register(registerPamFolderRouter, { prefix: "/pam/folders" });
|
||||
await server.register(registerPamAccountRouter, { prefix: "/pam/accounts" });
|
||||
await server.register(registerPamSessionRouter, { prefix: "/pam/sessions" });
|
||||
|
||||
await server.register(
|
||||
async (pamResourceRouter) => {
|
||||
await pamResourceRouter.register(registerPamResourceRouter);
|
||||
async (pamRouter) => {
|
||||
await pamRouter.register(registerPamFolderRouter, { prefix: "/folders" });
|
||||
await pamRouter.register(registerPamSessionRouter, { prefix: "/sessions" });
|
||||
|
||||
// Provider-specific endpoints
|
||||
await Promise.all(
|
||||
Object.entries(PAM_RESOURCE_REGISTER_ROUTER_MAP).map(([provider, router]) =>
|
||||
pamResourceRouter.register(router, { prefix: `/${provider}` })
|
||||
)
|
||||
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/resources" }
|
||||
{ prefix: "/pam" }
|
||||
);
|
||||
};
|
||||
|
||||
20
backend/src/ee/routes/v1/pam-account-routers/index.ts
Normal file
20
backend/src/ee/routes/v1/pam-account-routers/index.ts
Normal file
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -34,7 +34,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const response = await server.services.pamResource.listAccounts(req.query.projectId, req.permission);
|
||||
const response = await server.services.pamAccount.list(req.query.projectId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@@ -55,21 +55,18 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:accountId/access",
|
||||
url: "/access",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Access PAM account",
|
||||
params: z.object({
|
||||
accountId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
accountId: z.string().uuid(),
|
||||
duration: z
|
||||
.string()
|
||||
.optional()
|
||||
.min(1)
|
||||
.transform((val, ctx) => {
|
||||
if (val === undefined) return undefined;
|
||||
const parsedMs = ms(val);
|
||||
|
||||
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
||||
@@ -98,13 +95,13 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
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.pamResource.accessAccount(
|
||||
const response = await server.services.pamAccount.access(
|
||||
{
|
||||
accountId: req.params.accountId,
|
||||
actorEmail: req.auth.user.email ?? "",
|
||||
actorIp: req.realIp,
|
||||
actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
|
||||
@@ -121,7 +118,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.PAM_ACCOUNT_ACCESS,
|
||||
metadata: {
|
||||
accountId: req.params.accountId,
|
||||
accountId: req.body.accountId,
|
||||
accountName: response.account.name,
|
||||
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,7 @@ export const registerPamFolderRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.PAM_FOLDER_DELETE,
|
||||
metadata: {
|
||||
folderName: folder.name,
|
||||
folderId: req.params.folderId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import {
|
||||
CreatePostgresAccountSchema,
|
||||
CreatePostgresResourceSchema,
|
||||
PostgresResourceSchema,
|
||||
SanitizedPostgresAccountWithResourceSchema,
|
||||
UpdatePostgresAccountSchema,
|
||||
UpdatePostgresResourceSchema
|
||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
|
||||
@@ -16,11 +13,8 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
||||
server,
|
||||
resourceType: PamResource.Postgres,
|
||||
resourceResponseSchema: PostgresResourceSchema,
|
||||
accountResponseSchema: SanitizedPostgresAccountWithResourceSchema,
|
||||
createResourceSchema: CreatePostgresResourceSchema,
|
||||
createAccountSchema: CreatePostgresAccountSchema,
|
||||
updateResourceSchema: UpdatePostgresResourceSchema,
|
||||
updateAccountSchema: UpdatePostgresAccountSchema
|
||||
updateResourceSchema: UpdatePostgresResourceSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,20 +2,17 @@ import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import { TPamAccount, TPamResource } from "@app/ee/services/pam-resource/pam-resource-types";
|
||||
import { TPamResource } from "@app/ee/services/pam-resource/pam-resource-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerPamResourceEndpoints = <T extends TPamResource, C extends TPamAccount>({
|
||||
export const registerPamResourceEndpoints = <T extends TPamResource>({
|
||||
server,
|
||||
resourceType,
|
||||
createResourceSchema,
|
||||
updateResourceSchema,
|
||||
createAccountSchema,
|
||||
updateAccountSchema,
|
||||
resourceResponseSchema,
|
||||
accountResponseSchema
|
||||
resourceResponseSchema
|
||||
}: {
|
||||
server: FastifyZodProvider;
|
||||
resourceType: PamResource;
|
||||
@@ -25,24 +22,12 @@ export const registerPamResourceEndpoints = <T extends TPamResource, C extends T
|
||||
gatewayId: T["gatewayId"];
|
||||
name: T["name"];
|
||||
}>;
|
||||
createAccountSchema: z.ZodType<{
|
||||
credentials: C["credentials"];
|
||||
folderId?: C["folderId"];
|
||||
name: C["name"];
|
||||
description?: C["description"];
|
||||
}>;
|
||||
updateResourceSchema: z.ZodType<{
|
||||
connectionDetails?: T["connectionDetails"];
|
||||
gatewayId?: T["gatewayId"];
|
||||
name?: T["name"];
|
||||
}>;
|
||||
updateAccountSchema: z.ZodType<{
|
||||
credentials?: C["credentials"];
|
||||
name?: C["name"];
|
||||
description?: C["description"];
|
||||
}>;
|
||||
resourceResponseSchema: z.ZodTypeAny;
|
||||
accountResponseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@@ -210,142 +195,4 @@ export const registerPamResourceEndpoints = <T extends TPamResource, C extends T
|
||||
return { resource };
|
||||
}
|
||||
});
|
||||
|
||||
// PAM Accounts
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:resourceId/accounts",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create PAM resource account",
|
||||
params: z.object({
|
||||
resourceId: z.string().uuid()
|
||||
}),
|
||||
body: createAccountSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
account: accountResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const account = await server.services.pamResource.createAccount(
|
||||
{
|
||||
...req.body,
|
||||
resourceId: req.params.resourceId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: account.projectId,
|
||||
event: {
|
||||
type: EventType.PAM_ACCOUNT_CREATE,
|
||||
metadata: {
|
||||
resourceId: req.params.resourceId,
|
||||
resourceType,
|
||||
folderId: req.body.folderId,
|
||||
name: req.body.name,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { account };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:resourceId/accounts/:accountId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update PAM resource account",
|
||||
params: z.object({
|
||||
resourceId: z.string().uuid(),
|
||||
accountId: z.string().uuid()
|
||||
}),
|
||||
body: updateAccountSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
account: accountResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const account = await server.services.pamResource.updateAccountById(
|
||||
{
|
||||
...req.body,
|
||||
accountId: req.params.accountId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: account.projectId,
|
||||
event: {
|
||||
type: EventType.PAM_ACCOUNT_UPDATE,
|
||||
metadata: {
|
||||
accountId: req.params.accountId,
|
||||
resourceId: req.params.resourceId,
|
||||
resourceType,
|
||||
name: req.body.name,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { account };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:resourceId/accounts/:accountId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete PAM resource account",
|
||||
params: z.object({
|
||||
resourceId: z.string().uuid(),
|
||||
accountId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
account: accountResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const account = await server.services.pamResource.deleteAccountById(req.params.accountId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: account.projectId,
|
||||
event: {
|
||||
type: EventType.PAM_ACCOUNT_DELETE,
|
||||
metadata: {
|
||||
accountId: req.params.accountId,
|
||||
resourceId: req.params.resourceId,
|
||||
resourceType
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { account };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { credentials, projectId } = await server.services.pamResource.getSessionCredentials(
|
||||
const { credentials, projectId, account } = await server.services.pamAccount.getSessionCredentials(
|
||||
req.params.sessionId,
|
||||
req.permission
|
||||
);
|
||||
@@ -44,7 +44,8 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.PAM_SESSION_START,
|
||||
metadata: {
|
||||
sessionId: req.params.sessionId
|
||||
sessionId: req.params.sessionId,
|
||||
accountName: account.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -93,7 +94,8 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||
event: {
|
||||
type: EventType.PAM_SESSION_LOGS_UPDATE,
|
||||
metadata: {
|
||||
sessionId: req.params.sessionId
|
||||
sessionId: req.params.sessionId,
|
||||
accountName: session.accountName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3710,6 +3710,7 @@ interface PamSessionStartEvent {
|
||||
type: EventType.PAM_SESSION_START;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
accountName: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3717,6 +3718,7 @@ interface PamSessionLogsUpdateEvent {
|
||||
type: EventType.PAM_SESSION_LOGS_UPDATE;
|
||||
metadata: {
|
||||
sessionId: string;
|
||||
accountName: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3763,6 +3765,7 @@ interface PamFolderDeleteEvent {
|
||||
type: EventType.PAM_FOLDER_DELETE;
|
||||
metadata: {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3778,6 +3781,7 @@ interface PamAccountAccessEvent {
|
||||
type: EventType.PAM_ACCOUNT_ACCESS;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
duration?: string;
|
||||
};
|
||||
}
|
||||
@@ -3807,6 +3811,7 @@ interface PamAccountUpdateEvent {
|
||||
interface PamAccountDeleteEvent {
|
||||
type: EventType.PAM_ACCOUNT_DELETE;
|
||||
metadata: {
|
||||
accountName: string;
|
||||
accountId: string;
|
||||
resourceId: string;
|
||||
resourceType: string;
|
||||
|
||||
@@ -274,7 +274,6 @@ export const gatewayV2ServiceFactory = ({
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
actorMetadata?: { sessionId?: string; resourceType?: string };
|
||||
}) => {
|
||||
const gateway = await gatewayV2DAL.findById(gatewayId);
|
||||
if (!gateway) {
|
||||
@@ -817,7 +816,20 @@ 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 }) => {
|
||||
|
||||
61
backend/src/ee/services/pam-account/pam-account-fns.ts
Normal file
61
backend/src/ee/services/pam-account/pam-account-fns.ts
Normal file
@@ -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 };
|
||||
};
|
||||
520
backend/src/ee/services/pam-account/pam-account-service.ts
Normal file
520
backend/src/ee/services/pam-account/pam-account-service.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
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);
|
||||
|
||||
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` });
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
17
backend/src/ee/services/pam-account/pam-account-types.ts
Normal file
17
backend/src/ee/services/pam-account/pam-account-types.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -3,7 +3,8 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { ActionProjectType, TPamFolders } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { 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";
|
||||
@@ -50,26 +51,24 @@ export const pamFolderServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const existingFolder = await pamFolderDAL.findOne({
|
||||
name,
|
||||
parentId: parentId || null,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (existingFolder) {
|
||||
throw new BadRequestError({
|
||||
message: `Folder with name '${name}' already exists for this parent`
|
||||
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 folder = await pamFolderDAL.create({
|
||||
name,
|
||||
description: description ?? null,
|
||||
parentId: parentId || null,
|
||||
projectId
|
||||
});
|
||||
|
||||
return folder;
|
||||
};
|
||||
|
||||
const updateFolder = async ({ id, name, description }: TUpdateFolderDTO, actor: OrgServiceActor) => {
|
||||
@@ -104,27 +103,23 @@ export const pamFolderServiceFactory = ({
|
||||
updateDoc.description = description;
|
||||
}
|
||||
|
||||
if (name && name !== folder.name) {
|
||||
const existingFolder = await pamFolderDAL.findOne({
|
||||
name,
|
||||
parentId: folder.parentId || null,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
|
||||
if (existingFolder) {
|
||||
throw new BadRequestError({
|
||||
message: `Folder with name '${name}' already exists for this parent`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return folder;
|
||||
}
|
||||
|
||||
const updatedFolder = await pamFolderDAL.updateById(id, updateDoc);
|
||||
try {
|
||||
const updatedFolder = await pamFolderDAL.updateById(id, updateDoc);
|
||||
|
||||
return updatedFolder;
|
||||
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) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TPamResources } from "@app/db/schemas";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TPamAccountCredentials, TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||
|
||||
export const listResourceOptions = () => {
|
||||
@@ -11,17 +11,17 @@ export const listResourceOptions = () => {
|
||||
|
||||
// Resource
|
||||
export const encryptResourceConnectionDetails = async ({
|
||||
orgId,
|
||||
projectId,
|
||||
connectionDetails,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
projectId: string;
|
||||
connectionDetails: TPamResourceConnectionDetails;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedConnectionDetailsBlob } = encryptor({
|
||||
@@ -32,17 +32,17 @@ export const encryptResourceConnectionDetails = async ({
|
||||
};
|
||||
|
||||
export const decryptResourceConnectionDetails = async ({
|
||||
orgId,
|
||||
projectId,
|
||||
encryptedConnectionDetails,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
projectId: string;
|
||||
encryptedConnectionDetails: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
@@ -54,73 +54,15 @@ export const decryptResourceConnectionDetails = async ({
|
||||
|
||||
export const decryptResource = async (
|
||||
resource: TPamResources,
|
||||
orgId: string,
|
||||
projectId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
return {
|
||||
...resource,
|
||||
connectionDetails: await decryptResourceConnectionDetails({
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
orgId,
|
||||
projectId,
|
||||
kmsService
|
||||
})
|
||||
} as TPamResource;
|
||||
};
|
||||
|
||||
// Account
|
||||
export const encryptAccountCredentials = async ({
|
||||
orgId,
|
||||
credentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
credentials: TPamAccountCredentials;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(credentials))
|
||||
});
|
||||
|
||||
return encryptedCredentialsBlob;
|
||||
};
|
||||
|
||||
export const decryptAccountCredentials = async ({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
encryptedCredentials: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedCredentials
|
||||
});
|
||||
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
||||
};
|
||||
|
||||
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
|
||||
account: T,
|
||||
orgId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<T & { credentials: TPamAccountCredentials }> => {
|
||||
return {
|
||||
...account,
|
||||
credentials: await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
})
|
||||
} as T & { credentials: TPamAccountCredentials };
|
||||
};
|
||||
|
||||
@@ -34,12 +34,13 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
|
||||
});
|
||||
|
||||
export const BaseCreatePamAccountSchema = z.object({
|
||||
resourceId: z.string().uuid(),
|
||||
folderId: z.string().uuid().optional(),
|
||||
name: slugSchema({ field: "name" }),
|
||||
description: z.string().max(512).optional()
|
||||
description: z.string().max(512).nullable().optional()
|
||||
});
|
||||
|
||||
export const BaseUpdatePamAccountSchema = z.object({
|
||||
name: slugSchema({ field: "name" }).optional(),
|
||||
description: z.string().max(512).optional()
|
||||
description: z.string().max(512).nullable().optional()
|
||||
});
|
||||
|
||||
@@ -1,54 +1,23 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas";
|
||||
import { ActionProjectType, TPamResources } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionPamAccountActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { 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 { 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 { TPamSessionDALFactory } from "../pam-session/pam-session-dal";
|
||||
import { PamSessionStatus } from "../pam-session/pam-session-enums";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPamAccountDALFactory } from "./pam-account-dal";
|
||||
import { TPamResourceDALFactory } from "./pam-resource-dal";
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
|
||||
import {
|
||||
decryptAccount,
|
||||
decryptAccountCredentials,
|
||||
decryptResource,
|
||||
decryptResourceConnectionDetails,
|
||||
encryptAccountCredentials,
|
||||
encryptResourceConnectionDetails,
|
||||
listResourceOptions
|
||||
} from "./pam-resource-fns";
|
||||
import {
|
||||
TAccessAccountDTO,
|
||||
TCreateAccountDTO,
|
||||
TCreateResourceDTO,
|
||||
TPamAccountCredentials,
|
||||
TUpdateAccountDTO,
|
||||
TUpdateResourceDTO
|
||||
} from "./pam-resource-types";
|
||||
import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns";
|
||||
import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types";
|
||||
|
||||
type TPamResourceServiceFactoryDep = {
|
||||
pamResourceDAL: TPamResourceDALFactory;
|
||||
pamSessionDAL: TPamSessionDALFactory;
|
||||
pamAccountDAL: TPamAccountDALFactory;
|
||||
pamFolderDAL: TPamFolderDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
@@ -56,18 +25,12 @@ type TPamResourceServiceFactoryDep = {
|
||||
TGatewayV2ServiceFactory,
|
||||
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
|
||||
>;
|
||||
userDAL: TUserDALFactory;
|
||||
};
|
||||
|
||||
export type TPamResourceServiceFactory = ReturnType<typeof pamResourceServiceFactory>;
|
||||
|
||||
export const pamResourceServiceFactory = ({
|
||||
pamResourceDAL,
|
||||
pamSessionDAL,
|
||||
pamAccountDAL,
|
||||
pamFolderDAL,
|
||||
projectDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
@@ -94,7 +57,7 @@ export const pamResourceServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
return decryptResource(resource, actor.orgId, kmsService);
|
||||
return decryptResource(resource, resource.projectId, kmsService);
|
||||
};
|
||||
|
||||
const create = async (
|
||||
@@ -129,7 +92,7 @@ export const pamResourceServiceFactory = ({
|
||||
|
||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||
connectionDetails: validatedConnectionDetails,
|
||||
orgId: actor.orgId,
|
||||
projectId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
@@ -141,7 +104,7 @@ export const pamResourceServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
|
||||
return decryptResource(resource, actor.orgId, kmsService);
|
||||
return decryptResource(resource, projectId, kmsService);
|
||||
};
|
||||
|
||||
const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => {
|
||||
@@ -182,7 +145,7 @@ export const pamResourceServiceFactory = ({
|
||||
const validatedConnectionDetails = await factory.validateConnection();
|
||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||
connectionDetails: validatedConnectionDetails,
|
||||
orgId: actor.orgId,
|
||||
projectId: resource.projectId,
|
||||
kmsService
|
||||
});
|
||||
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
|
||||
@@ -190,12 +153,12 @@ export const pamResourceServiceFactory = ({
|
||||
|
||||
// If nothing was updated, return the fetched resource
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return decryptResource(resource, actor.orgId, kmsService);
|
||||
return decryptResource(resource, resource.projectId, kmsService);
|
||||
}
|
||||
|
||||
const updatedResource = await pamResourceDAL.updateById(resourceId, updateDoc);
|
||||
|
||||
return decryptResource(updatedResource, actor.orgId, kmsService);
|
||||
return decryptResource(updatedResource, resource.projectId, kmsService);
|
||||
};
|
||||
|
||||
const deleteById = async (id: string, actor: OrgServiceActor) => {
|
||||
@@ -213,9 +176,20 @@ export const pamResourceServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamResources);
|
||||
|
||||
const deletedResource = await pamResourceDAL.deleteById(id);
|
||||
|
||||
return decryptResource(deletedResource, actor.orgId, kmsService);
|
||||
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) => {
|
||||
@@ -233,440 +207,7 @@ export const pamResourceServiceFactory = ({
|
||||
const resources = await pamResourceDAL.find({ projectId });
|
||||
|
||||
return {
|
||||
resources: await Promise.all(resources.map((resource) => decryptResource(resource, actor.orgId, kmsService)))
|
||||
};
|
||||
};
|
||||
|
||||
// Accounts
|
||||
const createAccount = async (
|
||||
{ credentials, resourceId, name, description, folderId }: TCreateAccountDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const resource = await pamResourceDAL.findById(resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: resource.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId,
|
||||
projectId: resource.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Create,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
|
||||
const connectionDetails = await decryptResourceConnectionDetails({
|
||||
orgId: actor.orgId,
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
);
|
||||
const validatedCredentials = await factory.validateAccountCredentials(credentials);
|
||||
|
||||
const encryptedCredentials = await encryptAccountCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const account = await pamAccountDAL.create({
|
||||
projectId: resource.projectId,
|
||||
resourceId: resource.id,
|
||||
encryptedCredentials,
|
||||
name,
|
||||
description,
|
||||
folderId
|
||||
});
|
||||
|
||||
return {
|
||||
...(await decryptAccount(account, actor.orgId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
const updateAccountById = async (
|
||||
{ accountId, credentials, description, name }: TUpdateAccountDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findById(accountId);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Edit,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
|
||||
const updateDoc: Partial<TPamAccounts> = {};
|
||||
|
||||
if (name !== undefined) {
|
||||
updateDoc.name = name;
|
||||
}
|
||||
|
||||
if (description !== undefined) {
|
||||
updateDoc.description = description;
|
||||
}
|
||||
|
||||
if (credentials !== undefined) {
|
||||
const connectionDetails = await decryptResourceConnectionDetails({
|
||||
orgId: actor.orgId,
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
);
|
||||
|
||||
// Logic to prevent overwriting unedited censored values
|
||||
const finalCredentials = { ...credentials };
|
||||
if (credentials.password === "******") {
|
||||
const decryptedCredentials = await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
finalCredentials.password = decryptedCredentials.password;
|
||||
}
|
||||
|
||||
const validatedCredentials = await factory.validateAccountCredentials(finalCredentials);
|
||||
const encryptedCredentials = await encryptAccountCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
updateDoc.encryptedCredentials = encryptedCredentials;
|
||||
}
|
||||
|
||||
// If nothing was updated, return the fetched account
|
||||
if (Object.keys(updateDoc).length === 0) {
|
||||
return decryptAccount(account, actor.orgId, kmsService);
|
||||
}
|
||||
|
||||
const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc);
|
||||
|
||||
return {
|
||||
...(await decryptAccount(updatedAccount, actor.orgId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
const deleteAccountById = async (id: string, actor: OrgServiceActor) => {
|
||||
const account = await pamAccountDAL.findById(id);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${id}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Delete,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
|
||||
const deletedAccount = await pamAccountDAL.deleteById(id);
|
||||
|
||||
return {
|
||||
...(await decryptAccount(deletedAccount, actor.orgId, kmsService)),
|
||||
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
|
||||
};
|
||||
};
|
||||
|
||||
const listAccounts = async (projectId: string, actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId });
|
||||
|
||||
const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders);
|
||||
|
||||
const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : [];
|
||||
|
||||
const decryptedAndPermittedAccounts: Array<
|
||||
TPamAccounts & {
|
||||
resource: Pick<TPamResources, "id" | "name" | "resourceType">;
|
||||
credentials: TPamAccountCredentials;
|
||||
}
|
||||
> = [];
|
||||
|
||||
for await (const account of accountsWithResourceDetails) {
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
// Check permission for each individual account
|
||||
if (
|
||||
permission.can(
|
||||
ProjectPermissionPamAccountActions.Read,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: account.resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
)
|
||||
) {
|
||||
// Decrypt the account only if the user has permission to read it
|
||||
const decryptedAccount = await decryptAccount(account, actor.orgId, kmsService);
|
||||
decryptedAndPermittedAccounts.push({
|
||||
...decryptedAccount,
|
||||
resource: {
|
||||
id: account.resource.id,
|
||||
name: account.resource.name,
|
||||
resourceType: account.resource.resourceType
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accounts: decryptedAndPermittedAccounts,
|
||||
folders
|
||||
};
|
||||
};
|
||||
|
||||
const accessAccount = async (
|
||||
{ accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findById(accountId);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Access,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
|
||||
const session = await pamSessionDAL.create({
|
||||
accountName: account.name,
|
||||
actorEmail,
|
||||
actorIp,
|
||||
actorName,
|
||||
actorUserAgent,
|
||||
projectId: account.projectId,
|
||||
resourceName: resource.name,
|
||||
resourceType: resource.resourceType,
|
||||
status: PamSessionStatus.Starting,
|
||||
accountId: account.id,
|
||||
userId: actor.id,
|
||||
expiresAt: duration ? new Date(Date.now() + duration) : null
|
||||
});
|
||||
|
||||
const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, actor.orgId, kmsService);
|
||||
|
||||
const user = await userDAL.findById(actor.id);
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
const getSessionCredentials = async (sessionId: string, actor: OrgServiceActor) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
if (!orgLicensePlan.pam) {
|
||||
throw new BadRequestError({
|
||||
message: "PAM operation failed due to organization plan restrictions."
|
||||
});
|
||||
}
|
||||
|
||||
// To be hit by gateways only
|
||||
if (actor.type !== ActorType.IDENTITY) {
|
||||
throw new ForbiddenRequestError({ message: "Only gateways can perform this action" });
|
||||
}
|
||||
|
||||
const session = await pamSessionDAL.findById(sessionId);
|
||||
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
|
||||
|
||||
const project = await projectDAL.findById(session.projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
project.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
if (!session.accountId) throw new NotFoundError({ message: "Session is missing accountId column" });
|
||||
|
||||
// Verify that the session has not ended
|
||||
if (session.endedAt || (session.expiresAt && session.expiresAt < new Date())) {
|
||||
throw new BadRequestError({ message: "Session has ended or expired" });
|
||||
}
|
||||
|
||||
// Verify that the session has not already had credentials fetched
|
||||
if (session.status !== PamSessionStatus.Starting) {
|
||||
throw new BadRequestError({ message: "Session has already been started" });
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findById(session.accountId);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${session.accountId}' not found` });
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const decryptedAccount = await decryptAccount(account, actor.orgId, kmsService);
|
||||
|
||||
const decryptedResource = await decryptResource(resource, actor.orgId, kmsService);
|
||||
|
||||
// Mark session as started
|
||||
await pamSessionDAL.updateById(sessionId, {
|
||||
status: PamSessionStatus.Active,
|
||||
startedAt: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
credentials: {
|
||||
...decryptedResource.connectionDetails,
|
||||
...decryptedAccount.credentials
|
||||
},
|
||||
projectId: project.id
|
||||
resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, kmsService)))
|
||||
};
|
||||
};
|
||||
|
||||
@@ -676,12 +217,6 @@ export const pamResourceServiceFactory = ({
|
||||
updateById,
|
||||
deleteById,
|
||||
list,
|
||||
listResourceOptions,
|
||||
createAccount,
|
||||
updateAccountById,
|
||||
deleteAccountById,
|
||||
listAccounts,
|
||||
accessAccount,
|
||||
getSessionCredentials
|
||||
listResourceOptions
|
||||
};
|
||||
};
|
||||
|
||||
@@ -25,22 +25,6 @@ export type TUpdateResourceDTO = Partial<Omit<TCreateResourceDTO, "resourceType"
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
// Account DTOs
|
||||
export type TCreateAccountDTO = Pick<TPamAccount, "name" | "description" | "credentials" | "folderId" | "resourceId">;
|
||||
|
||||
export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "resourceId">> & {
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
export type TAccessAccountDTO = {
|
||||
accountId: string;
|
||||
actorEmail: string;
|
||||
actorIp: string;
|
||||
actorName: string;
|
||||
actorUserAgent: string;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
// Resource factory
|
||||
export type TPamResourceFactoryValidateConnection<T extends TPamResourceConnectionDetails> = () => Promise<T>;
|
||||
export type TPamResourceFactoryValidateAccountCredentials<C extends TPamAccountCredentials> = (
|
||||
|
||||
@@ -5,17 +5,17 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TPamSanitizedSession, TPamSessionCommandLog } from "./pam-session.types";
|
||||
|
||||
export const decryptSessionCommandLogs = async ({
|
||||
orgId,
|
||||
projectId,
|
||||
encryptedLogs,
|
||||
kmsService
|
||||
}: {
|
||||
orgId: string;
|
||||
projectId: string;
|
||||
encryptedLogs: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
@@ -27,14 +27,14 @@ export const decryptSessionCommandLogs = async ({
|
||||
|
||||
export const decryptSession = async (
|
||||
session: TPamSessions,
|
||||
orgId: string,
|
||||
projectId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
return {
|
||||
...session,
|
||||
commandLogs: session.encryptedLogsBlob
|
||||
? await decryptSessionCommandLogs({
|
||||
orgId,
|
||||
projectId,
|
||||
encryptedLogs: session.encryptedLogsBlob,
|
||||
kmsService
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ export const pamSessionServiceFactory = ({
|
||||
);
|
||||
|
||||
return {
|
||||
session: await decryptSession(session, actor.orgId, kmsService)
|
||||
session: await decryptSession(session, session.projectId, kmsService)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ export const pamSessionServiceFactory = ({
|
||||
const sessions = await pamSessionDAL.find({ projectId });
|
||||
|
||||
return {
|
||||
sessions: await Promise.all(sessions.map((session) => decryptSession(session, actor.orgId, kmsService)))
|
||||
sessions: await Promise.all(sessions.map((session) => decryptSession(session, projectId, kmsService)))
|
||||
};
|
||||
};
|
||||
|
||||
@@ -112,8 +112,8 @@ export const pamSessionServiceFactory = ({
|
||||
);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: project.orgId
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: session.projectId
|
||||
});
|
||||
|
||||
const { cipherTextBlob } = encryptor({
|
||||
|
||||
@@ -66,9 +66,10 @@ import { licenseDALFactory } from "@app/ee/services/license/license-dal";
|
||||
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||
import { pamAccountDALFactory } from "@app/ee/services/pam-account/pam-account-dal";
|
||||
import { pamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service";
|
||||
import { pamFolderDALFactory } from "@app/ee/services/pam-folder/pam-folder-dal";
|
||||
import { pamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
|
||||
import { pamAccountDALFactory } from "@app/ee/services/pam-resource/pam-account-dal";
|
||||
import { pamResourceDALFactory } from "@app/ee/services/pam-resource/pam-resource-dal";
|
||||
import { pamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
||||
import { pamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal";
|
||||
@@ -2123,14 +2124,22 @@ export const registerRoutes = async (
|
||||
|
||||
const pamResourceService = pamResourceServiceFactory({
|
||||
pamResourceDAL,
|
||||
pamSessionDAL,
|
||||
pamAccountDAL,
|
||||
pamFolderDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
gatewayV2Service
|
||||
});
|
||||
|
||||
const pamAccountService = pamAccountServiceFactory({
|
||||
pamAccountDAL,
|
||||
gatewayV2Service,
|
||||
kmsService,
|
||||
licenseService,
|
||||
pamFolderDAL,
|
||||
pamResourceDAL,
|
||||
pamSessionDAL,
|
||||
permissionService,
|
||||
projectDAL,
|
||||
userDAL
|
||||
});
|
||||
|
||||
@@ -2282,6 +2291,7 @@ export const registerRoutes = async (
|
||||
notification: notificationService,
|
||||
pamFolder: pamFolderService,
|
||||
pamResource: pamResourceService,
|
||||
pamAccount: pamAccountService,
|
||||
pamSession: pamSessionService,
|
||||
upgradePath: upgradePathService
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
restart: "always"
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
- 8444:443
|
||||
volumes:
|
||||
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
@@ -197,4 +197,4 @@ volumes:
|
||||
driver: local
|
||||
ldap_data:
|
||||
ldap_config:
|
||||
grafana_storage:
|
||||
grafana_storage:
|
||||
|
||||
@@ -73,9 +73,9 @@ export const useDeletePamResource = () => {
|
||||
export const useCreatePamAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ resourceId, resourceType, ...params }: TCreatePamAccountDTO) => {
|
||||
mutationFn: async ({ resourceType, ...params }: TCreatePamAccountDTO) => {
|
||||
const { data } = await apiRequest.post<{ account: TPamAccount }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts`,
|
||||
`/api/v1/pam/accounts/${resourceType}`,
|
||||
params
|
||||
);
|
||||
|
||||
@@ -90,14 +90,9 @@ export const useCreatePamAccount = () => {
|
||||
export const useUpdatePamAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
resourceId,
|
||||
resourceType,
|
||||
accountId,
|
||||
...params
|
||||
}: TUpdatePamAccountDTO) => {
|
||||
mutationFn: async ({ resourceType, accountId, ...params }: TUpdatePamAccountDTO) => {
|
||||
const { data } = await apiRequest.patch<{ account: TPamAccount }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`,
|
||||
`/api/v1/pam/accounts/${resourceType}/${accountId}`,
|
||||
params
|
||||
);
|
||||
|
||||
@@ -112,9 +107,9 @@ export const useUpdatePamAccount = () => {
|
||||
export const useDeletePamAccount = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ resourceId, resourceType, accountId }: TDeletePamAccountDTO) => {
|
||||
mutationFn: async ({ resourceType, accountId }: TDeletePamAccountDTO) => {
|
||||
const { data } = await apiRequest.delete<{ account: TPamAccount }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`
|
||||
`/api/v1/pam/accounts/${resourceType}/${accountId}`
|
||||
);
|
||||
|
||||
return data.account;
|
||||
|
||||
@@ -72,13 +72,11 @@ export type TUpdatePamAccountDTO = Partial<
|
||||
Pick<TPamAccount, "name" | "description" | "credentials">
|
||||
> & {
|
||||
accountId: string;
|
||||
resourceId: string;
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
|
||||
export type TDeletePamAccountDTO = {
|
||||
accountId: string;
|
||||
resourceId: string;
|
||||
resourceType: PamResourceType;
|
||||
};
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
||||
secretEvents.includes(eventType)
|
||||
);
|
||||
const showSecretsSection =
|
||||
project?.type !== ProjectType.PAM &&
|
||||
selectedProject?.type !== ProjectType.PAM &&
|
||||
(hasSecretEventFilter || currentSelectedEventTypes.length === 0);
|
||||
|
||||
const filteredEventTypes = useMemo(() => {
|
||||
|
||||
@@ -16,13 +16,10 @@ type Props = {
|
||||
export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
|
||||
const [duration, setDuration] = useState("4h");
|
||||
|
||||
const isDurationValid = useMemo(() => ms(duration || "1s") > 0, [duration]);
|
||||
const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]);
|
||||
|
||||
const command = useMemo(
|
||||
() =>
|
||||
account
|
||||
? `infisical pam access ${account.id}${duration ? ` --duration ${duration}` : ""}`
|
||||
: "",
|
||||
() => (account ? `infisical pam access ${account.id} --duration ${duration}` : ""),
|
||||
[account, duration]
|
||||
);
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
|
||||
try {
|
||||
const updatedAccount = await updatePamAccount.mutateAsync({
|
||||
accountId: account.id,
|
||||
resourceId: account.resourceId,
|
||||
resourceType: account.resource.resourceType,
|
||||
...formData
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const BaseSqlAccountSchema = z.object({
|
||||
username: z.string().trim().min(1, "Username required").max(255, "Username must be 255 characters or less"),
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username required")
|
||||
.max(255, "Username must be 255 characters or less"),
|
||||
password: z.string().trim().min(1, "Password required")
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@ export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props)
|
||||
const {
|
||||
id: accountId,
|
||||
name,
|
||||
resourceId,
|
||||
resource: { resourceType }
|
||||
} = account;
|
||||
|
||||
@@ -24,7 +23,6 @@ export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props)
|
||||
try {
|
||||
await deletePamAccount.mutateAsync({
|
||||
accountId,
|
||||
resourceId,
|
||||
resourceType
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const PamResourceHeader = ({ resourceType, onBack }: Props) => {
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">{details.name}</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">External resource</p>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">Resource</p>
|
||||
</div>
|
||||
{onBack && (
|
||||
<button
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-management/$projectId/access-management",
|
||||
to: "/projects/pam/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-management/$projectId/access-management",
|
||||
to: "/projects/pam/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-scanning/$projectId/access-management",
|
||||
to: "/projects/pam/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/projects/secret-management/$projectId/access-management",
|
||||
to: "/projects/pam/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user