mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 21:27:10 +00:00
addressed reviews
This commit is contained in:
Vendored
+2
@@ -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 { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-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 { TPamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
|
||||||
import { TPamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
import { TPamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
||||||
import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service";
|
import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service";
|
||||||
@@ -320,6 +321,7 @@ declare module "fastify" {
|
|||||||
offlineUsageReport: TOfflineUsageReportServiceFactory;
|
offlineUsageReport: TOfflineUsageReportServiceFactory;
|
||||||
pamFolder: TPamFolderServiceFactory;
|
pamFolder: TPamFolderServiceFactory;
|
||||||
pamResource: TPamResourceServiceFactory;
|
pamResource: TPamResourceServiceFactory;
|
||||||
|
pamAccount: TPamAccountServiceFactory;
|
||||||
pamSession: TPamSessionServiceFactory;
|
pamSession: TPamSessionServiceFactory;
|
||||||
upgradePath: TUpgradePathService;
|
upgradePath: TUpgradePathService;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
// PAM Folders
|
// PAM Folders
|
||||||
@@ -18,6 +19,19 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
|
|
||||||
t.string("name").notNullable();
|
t.string("name").notNullable();
|
||||||
t.index("name");
|
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.text("description").nullable();
|
||||||
|
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
@@ -35,10 +49,14 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
|
|
||||||
t.string("name").notNullable();
|
t.string("name").notNullable();
|
||||||
t.index("name");
|
t.index("name");
|
||||||
t.string("gatewayId").notNullable();
|
|
||||||
|
t.uuid("gatewayId").notNullable();
|
||||||
|
t.foreign("gatewayId").references("id").inTable(TableName.GatewayV2);
|
||||||
t.index("gatewayId");
|
t.index("gatewayId");
|
||||||
|
|
||||||
t.string("resourceType").notNullable();
|
t.string("resourceType").notNullable();
|
||||||
t.index("resourceType");
|
t.index("resourceType");
|
||||||
|
|
||||||
t.binary("encryptedConnectionDetails").notNullable();
|
t.binary("encryptedConnectionDetails").notNullable();
|
||||||
|
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
@@ -59,13 +77,25 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.index("folderId");
|
t.index("folderId");
|
||||||
|
|
||||||
t.uuid("resourceId").notNullable();
|
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.index("resourceId");
|
||||||
|
|
||||||
t.string("name").notNullable();
|
t.string("name").notNullable();
|
||||||
t.index("name");
|
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.binary("encryptedCredentials").notNullable();
|
||||||
|
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
@@ -106,7 +136,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
|
|
||||||
t.binary("encryptedLogsBlob").nullable();
|
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("startedAt").nullable(); // Not when the row is created, but when the end-to-end connection between user and resource is established
|
||||||
t.datetime("endedAt").nullable();
|
t.datetime("endedAt").nullable();
|
||||||
@@ -115,6 +145,11 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.timestamps(true, true, true);
|
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> {
|
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.PamAccount);
|
||||||
await knex.schema.dropTableIfExists(TableName.PamResource);
|
await knex.schema.dropTableIfExists(TableName.PamResource);
|
||||||
await knex.schema.dropTableIfExists(TableName.PamFolder);
|
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 { registerLicenseRouter } from "./license-router";
|
||||||
import { registerOidcRouter } from "./oidc-router";
|
import { registerOidcRouter } from "./oidc-router";
|
||||||
import { registerOrgRoleRouter } from "./org-role-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 { registerPamFolderRouter } from "./pam-folder-router";
|
||||||
import { PAM_RESOURCE_REGISTER_ROUTER_MAP } from "./pam-resource-routers";
|
import { PAM_RESOURCE_REGISTER_ROUTER_MAP } from "./pam-resource-routers";
|
||||||
import { registerPamResourceRouter } from "./pam-resource-routers/pam-resource-router";
|
import { registerPamResourceRouter } from "./pam-resource-routers/pam-resource-router";
|
||||||
@@ -172,21 +173,39 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/kmip" }
|
{ 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(
|
await server.register(
|
||||||
async (pamResourceRouter) => {
|
async (pamRouter) => {
|
||||||
await pamResourceRouter.register(registerPamResourceRouter);
|
await pamRouter.register(registerPamFolderRouter, { prefix: "/folders" });
|
||||||
|
await pamRouter.register(registerPamSessionRouter, { prefix: "/sessions" });
|
||||||
|
|
||||||
// Provider-specific endpoints
|
await pamRouter.register(
|
||||||
await Promise.all(
|
async (pamAccountRouter) => {
|
||||||
Object.entries(PAM_RESOURCE_REGISTER_ROUTER_MAP).map(([provider, router]) =>
|
await pamAccountRouter.register(registerPamAccountRouter);
|
||||||
pamResourceRouter.register(router, { prefix: `/${provider}` })
|
|
||||||
)
|
// 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" }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
+8
-10
@@ -34,7 +34,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
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({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -55,21 +55,18 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:accountId/access",
|
url: "/access",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Access PAM account",
|
description: "Access PAM account",
|
||||||
params: z.object({
|
|
||||||
accountId: z.string().uuid()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
accountId: z.string().uuid(),
|
||||||
duration: z
|
duration: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.min(1)
|
||||||
.transform((val, ctx) => {
|
.transform((val, ctx) => {
|
||||||
if (val === undefined) return undefined;
|
|
||||||
const parsedMs = ms(val);
|
const parsedMs = ms(val);
|
||||||
|
|
||||||
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
if (typeof parsedMs !== "number" || parsedMs <= 0) {
|
||||||
@@ -98,13 +95,13 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
// To prevent type errors when accessing req.auth
|
||||||
if (req.auth.authMode !== AuthMode.JWT) {
|
if (req.auth.authMode !== AuthMode.JWT) {
|
||||||
throw new BadRequestError({ message: "You can only access PAM accounts using JWT auth tokens." });
|
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 ?? "",
|
actorEmail: req.auth.user.email ?? "",
|
||||||
actorIp: req.realIp,
|
actorIp: req.realIp,
|
||||||
actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
|
actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
|
||||||
@@ -121,7 +118,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.PAM_ACCOUNT_ACCESS,
|
type: EventType.PAM_ACCOUNT_ACCESS,
|
||||||
metadata: {
|
metadata: {
|
||||||
accountId: req.params.accountId,
|
accountId: req.body.accountId,
|
||||||
|
accountName: response.account.name,
|
||||||
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
|
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,6 +138,7 @@ export const registerPamFolderRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.PAM_FOLDER_DELETE,
|
type: EventType.PAM_FOLDER_DELETE,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
folderName: folder.name,
|
||||||
folderId: req.params.folderId
|
folderId: req.params.folderId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
CreatePostgresAccountSchema,
|
|
||||||
CreatePostgresResourceSchema,
|
CreatePostgresResourceSchema,
|
||||||
PostgresResourceSchema,
|
PostgresResourceSchema,
|
||||||
SanitizedPostgresAccountWithResourceSchema,
|
|
||||||
UpdatePostgresAccountSchema,
|
|
||||||
UpdatePostgresResourceSchema
|
UpdatePostgresResourceSchema
|
||||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
} 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,
|
server,
|
||||||
resourceType: PamResource.Postgres,
|
resourceType: PamResource.Postgres,
|
||||||
resourceResponseSchema: PostgresResourceSchema,
|
resourceResponseSchema: PostgresResourceSchema,
|
||||||
accountResponseSchema: SanitizedPostgresAccountWithResourceSchema,
|
|
||||||
createResourceSchema: CreatePostgresResourceSchema,
|
createResourceSchema: CreatePostgresResourceSchema,
|
||||||
createAccountSchema: CreatePostgresAccountSchema,
|
updateResourceSchema: UpdatePostgresResourceSchema
|
||||||
updateResourceSchema: UpdatePostgresResourceSchema,
|
|
||||||
updateAccountSchema: UpdatePostgresAccountSchema
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerPamResourceEndpoints = <T extends TPamResource, C extends TPamAccount>({
|
export const registerPamResourceEndpoints = <T extends TPamResource>({
|
||||||
server,
|
server,
|
||||||
resourceType,
|
resourceType,
|
||||||
createResourceSchema,
|
createResourceSchema,
|
||||||
updateResourceSchema,
|
updateResourceSchema,
|
||||||
createAccountSchema,
|
resourceResponseSchema
|
||||||
updateAccountSchema,
|
|
||||||
resourceResponseSchema,
|
|
||||||
accountResponseSchema
|
|
||||||
}: {
|
}: {
|
||||||
server: FastifyZodProvider;
|
server: FastifyZodProvider;
|
||||||
resourceType: PamResource;
|
resourceType: PamResource;
|
||||||
@@ -25,24 +22,12 @@ export const registerPamResourceEndpoints = <T extends TPamResource, C extends T
|
|||||||
gatewayId: T["gatewayId"];
|
gatewayId: T["gatewayId"];
|
||||||
name: T["name"];
|
name: T["name"];
|
||||||
}>;
|
}>;
|
||||||
createAccountSchema: z.ZodType<{
|
|
||||||
credentials: C["credentials"];
|
|
||||||
folderId?: C["folderId"];
|
|
||||||
name: C["name"];
|
|
||||||
description?: C["description"];
|
|
||||||
}>;
|
|
||||||
updateResourceSchema: z.ZodType<{
|
updateResourceSchema: z.ZodType<{
|
||||||
connectionDetails?: T["connectionDetails"];
|
connectionDetails?: T["connectionDetails"];
|
||||||
gatewayId?: T["gatewayId"];
|
gatewayId?: T["gatewayId"];
|
||||||
name?: T["name"];
|
name?: T["name"];
|
||||||
}>;
|
}>;
|
||||||
updateAccountSchema: z.ZodType<{
|
|
||||||
credentials?: C["credentials"];
|
|
||||||
name?: C["name"];
|
|
||||||
description?: C["description"];
|
|
||||||
}>;
|
|
||||||
resourceResponseSchema: z.ZodTypeAny;
|
resourceResponseSchema: z.ZodTypeAny;
|
||||||
accountResponseSchema: z.ZodTypeAny;
|
|
||||||
}) => {
|
}) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -210,142 +195,4 @@ export const registerPamResourceEndpoints = <T extends TPamResource, C extends T
|
|||||||
return { resource };
|
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]),
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { credentials, projectId } = await server.services.pamResource.getSessionCredentials(
|
const { credentials, projectId, account } = await server.services.pamAccount.getSessionCredentials(
|
||||||
req.params.sessionId,
|
req.params.sessionId,
|
||||||
req.permission
|
req.permission
|
||||||
);
|
);
|
||||||
@@ -44,7 +44,8 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.PAM_SESSION_START,
|
type: EventType.PAM_SESSION_START,
|
||||||
metadata: {
|
metadata: {
|
||||||
sessionId: req.params.sessionId
|
sessionId: req.params.sessionId,
|
||||||
|
accountName: account.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -93,7 +94,8 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.PAM_SESSION_LOGS_UPDATE,
|
type: EventType.PAM_SESSION_LOGS_UPDATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
sessionId: req.params.sessionId
|
sessionId: req.params.sessionId,
|
||||||
|
accountName: session.accountName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3710,6 +3710,7 @@ interface PamSessionStartEvent {
|
|||||||
type: EventType.PAM_SESSION_START;
|
type: EventType.PAM_SESSION_START;
|
||||||
metadata: {
|
metadata: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
accountName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3717,6 +3718,7 @@ interface PamSessionLogsUpdateEvent {
|
|||||||
type: EventType.PAM_SESSION_LOGS_UPDATE;
|
type: EventType.PAM_SESSION_LOGS_UPDATE;
|
||||||
metadata: {
|
metadata: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
accountName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3763,6 +3765,7 @@ interface PamFolderDeleteEvent {
|
|||||||
type: EventType.PAM_FOLDER_DELETE;
|
type: EventType.PAM_FOLDER_DELETE;
|
||||||
metadata: {
|
metadata: {
|
||||||
folderId: string;
|
folderId: string;
|
||||||
|
folderName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3778,6 +3781,7 @@ interface PamAccountAccessEvent {
|
|||||||
type: EventType.PAM_ACCOUNT_ACCESS;
|
type: EventType.PAM_ACCOUNT_ACCESS;
|
||||||
metadata: {
|
metadata: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
accountName: string;
|
||||||
duration?: string;
|
duration?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3807,6 +3811,7 @@ interface PamAccountUpdateEvent {
|
|||||||
interface PamAccountDeleteEvent {
|
interface PamAccountDeleteEvent {
|
||||||
type: EventType.PAM_ACCOUNT_DELETE;
|
type: EventType.PAM_ACCOUNT_DELETE;
|
||||||
metadata: {
|
metadata: {
|
||||||
|
accountName: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
resourceType: string;
|
resourceType: string;
|
||||||
|
|||||||
@@ -274,7 +274,6 @@ export const gatewayV2ServiceFactory = ({
|
|||||||
gatewayId: string;
|
gatewayId: string;
|
||||||
targetHost: string;
|
targetHost: string;
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
actorMetadata?: { sessionId?: string; resourceType?: string };
|
|
||||||
}) => {
|
}) => {
|
||||||
const gateway = await gatewayV2DAL.findById(gatewayId);
|
const gateway = await gatewayV2DAL.findById(gatewayId);
|
||||||
if (!gateway) {
|
if (!gateway) {
|
||||||
@@ -817,7 +816,20 @@ export const gatewayV2ServiceFactory = ({
|
|||||||
OrgPermissionSubjects.Gateway
|
OrgPermissionSubjects.Gateway
|
||||||
);
|
);
|
||||||
|
|
||||||
return gatewayV2DAL.deleteById(gateway.id);
|
try {
|
||||||
|
return await gatewayV2DAL.deleteById(gateway.id);
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof DatabaseError &&
|
||||||
|
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete gateway because it is attached to active resources"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
|
const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
|
||||||
|
|
||||||
|
export const encryptAccountCredentials = async ({
|
||||||
|
projectId,
|
||||||
|
credentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
credentials: TPamAccountCredentials;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
}) => {
|
||||||
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify(credentials))
|
||||||
|
});
|
||||||
|
|
||||||
|
return encryptedCredentialsBlob;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptAccountCredentials = async ({
|
||||||
|
projectId,
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
encryptedCredentials: Buffer;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
}) => {
|
||||||
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPlainTextBlob = decryptor({
|
||||||
|
cipherTextBlob: encryptedCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
|
||||||
|
account: T,
|
||||||
|
projectId: string,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
): Promise<T & { credentials: TPamAccountCredentials }> => {
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
credentials: await decryptAccountCredentials({
|
||||||
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
|
projectId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as T & { credentials: TPamAccountCredentials };
|
||||||
|
};
|
||||||
@@ -0,0 +1,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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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 { ActionProjectType, TPamFolders } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
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 { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
@@ -50,26 +51,24 @@ export const pamFolderServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingFolder = await pamFolderDAL.findOne({
|
try {
|
||||||
name,
|
const folder = await pamFolderDAL.create({
|
||||||
parentId: parentId || null,
|
name,
|
||||||
projectId
|
description: description ?? null,
|
||||||
});
|
parentId: parentId || null,
|
||||||
|
projectId
|
||||||
if (existingFolder) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Folder with name '${name}' already exists for this parent`
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
const updateFolder = async ({ id, name, description }: TUpdateFolderDTO, actor: OrgServiceActor) => {
|
||||||
@@ -104,27 +103,23 @@ export const pamFolderServiceFactory = ({
|
|||||||
updateDoc.description = description;
|
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) {
|
if (Object.keys(updateDoc).length === 0) {
|
||||||
return folder;
|
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) => {
|
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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
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";
|
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||||
|
|
||||||
export const listResourceOptions = () => {
|
export const listResourceOptions = () => {
|
||||||
@@ -11,17 +11,17 @@ export const listResourceOptions = () => {
|
|||||||
|
|
||||||
// Resource
|
// Resource
|
||||||
export const encryptResourceConnectionDetails = async ({
|
export const encryptResourceConnectionDetails = async ({
|
||||||
orgId,
|
projectId,
|
||||||
connectionDetails,
|
connectionDetails,
|
||||||
kmsService
|
kmsService
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
projectId: string;
|
||||||
connectionDetails: TPamResourceConnectionDetails;
|
connectionDetails: TPamResourceConnectionDetails;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
}) => {
|
}) => {
|
||||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.SecretManager,
|
||||||
orgId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedConnectionDetailsBlob } = encryptor({
|
const { cipherTextBlob: encryptedConnectionDetailsBlob } = encryptor({
|
||||||
@@ -32,17 +32,17 @@ export const encryptResourceConnectionDetails = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const decryptResourceConnectionDetails = async ({
|
export const decryptResourceConnectionDetails = async ({
|
||||||
orgId,
|
projectId,
|
||||||
encryptedConnectionDetails,
|
encryptedConnectionDetails,
|
||||||
kmsService
|
kmsService
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
projectId: string;
|
||||||
encryptedConnectionDetails: Buffer;
|
encryptedConnectionDetails: Buffer;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
}) => {
|
}) => {
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.SecretManager,
|
||||||
orgId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedPlainTextBlob = decryptor({
|
const decryptedPlainTextBlob = decryptor({
|
||||||
@@ -54,73 +54,15 @@ export const decryptResourceConnectionDetails = async ({
|
|||||||
|
|
||||||
export const decryptResource = async (
|
export const decryptResource = async (
|
||||||
resource: TPamResources,
|
resource: TPamResources,
|
||||||
orgId: string,
|
projectId: string,
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...resource,
|
...resource,
|
||||||
connectionDetails: await decryptResourceConnectionDetails({
|
connectionDetails: await decryptResourceConnectionDetails({
|
||||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||||
orgId,
|
projectId,
|
||||||
kmsService
|
kmsService
|
||||||
})
|
})
|
||||||
} as TPamResource;
|
} 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({
|
export const BaseCreatePamAccountSchema = z.object({
|
||||||
|
resourceId: z.string().uuid(),
|
||||||
folderId: z.string().uuid().optional(),
|
folderId: z.string().uuid().optional(),
|
||||||
name: slugSchema({ field: "name" }),
|
name: slugSchema({ field: "name" }),
|
||||||
description: z.string().max(512).optional()
|
description: z.string().max(512).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BaseUpdatePamAccountSchema = z.object({
|
export const BaseUpdatePamAccountSchema = z.object({
|
||||||
name: slugSchema({ field: "name" }).optional(),
|
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import {
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionActions,
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
ProjectPermissionPamAccountActions,
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
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 { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||||
import { TLicenseServiceFactory } from "../license/license-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 { TPamResourceDALFactory } from "./pam-resource-dal";
|
||||||
import { PamResource } from "./pam-resource-enums";
|
import { PamResource } from "./pam-resource-enums";
|
||||||
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
|
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
|
||||||
import {
|
import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns";
|
||||||
decryptAccount,
|
import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types";
|
||||||
decryptAccountCredentials,
|
|
||||||
decryptResource,
|
|
||||||
decryptResourceConnectionDetails,
|
|
||||||
encryptAccountCredentials,
|
|
||||||
encryptResourceConnectionDetails,
|
|
||||||
listResourceOptions
|
|
||||||
} from "./pam-resource-fns";
|
|
||||||
import {
|
|
||||||
TAccessAccountDTO,
|
|
||||||
TCreateAccountDTO,
|
|
||||||
TCreateResourceDTO,
|
|
||||||
TPamAccountCredentials,
|
|
||||||
TUpdateAccountDTO,
|
|
||||||
TUpdateResourceDTO
|
|
||||||
} from "./pam-resource-types";
|
|
||||||
|
|
||||||
type TPamResourceServiceFactoryDep = {
|
type TPamResourceServiceFactoryDep = {
|
||||||
pamResourceDAL: TPamResourceDALFactory;
|
pamResourceDAL: TPamResourceDALFactory;
|
||||||
pamSessionDAL: TPamSessionDALFactory;
|
|
||||||
pamAccountDAL: TPamAccountDALFactory;
|
|
||||||
pamFolderDAL: TPamFolderDALFactory;
|
|
||||||
projectDAL: TProjectDALFactory;
|
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
@@ -56,18 +25,12 @@ type TPamResourceServiceFactoryDep = {
|
|||||||
TGatewayV2ServiceFactory,
|
TGatewayV2ServiceFactory,
|
||||||
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
|
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
|
||||||
>;
|
>;
|
||||||
userDAL: TUserDALFactory;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPamResourceServiceFactory = ReturnType<typeof pamResourceServiceFactory>;
|
export type TPamResourceServiceFactory = ReturnType<typeof pamResourceServiceFactory>;
|
||||||
|
|
||||||
export const pamResourceServiceFactory = ({
|
export const pamResourceServiceFactory = ({
|
||||||
pamResourceDAL,
|
pamResourceDAL,
|
||||||
pamSessionDAL,
|
|
||||||
pamAccountDAL,
|
|
||||||
pamFolderDAL,
|
|
||||||
projectDAL,
|
|
||||||
userDAL,
|
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
@@ -94,7 +57,7 @@ export const pamResourceServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return decryptResource(resource, actor.orgId, kmsService);
|
return decryptResource(resource, resource.projectId, kmsService);
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = async (
|
const create = async (
|
||||||
@@ -129,7 +92,7 @@ export const pamResourceServiceFactory = ({
|
|||||||
|
|
||||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||||
connectionDetails: validatedConnectionDetails,
|
connectionDetails: validatedConnectionDetails,
|
||||||
orgId: actor.orgId,
|
projectId,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,7 +104,7 @@ export const pamResourceServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return decryptResource(resource, actor.orgId, kmsService);
|
return decryptResource(resource, projectId, kmsService);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => {
|
const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => {
|
||||||
@@ -182,7 +145,7 @@ export const pamResourceServiceFactory = ({
|
|||||||
const validatedConnectionDetails = await factory.validateConnection();
|
const validatedConnectionDetails = await factory.validateConnection();
|
||||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||||
connectionDetails: validatedConnectionDetails,
|
connectionDetails: validatedConnectionDetails,
|
||||||
orgId: actor.orgId,
|
projectId: resource.projectId,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
|
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
|
||||||
@@ -190,12 +153,12 @@ export const pamResourceServiceFactory = ({
|
|||||||
|
|
||||||
// If nothing was updated, return the fetched resource
|
// If nothing was updated, return the fetched resource
|
||||||
if (Object.keys(updateDoc).length === 0) {
|
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);
|
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) => {
|
const deleteById = async (id: string, actor: OrgServiceActor) => {
|
||||||
@@ -213,9 +176,20 @@ export const pamResourceServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamResources);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamResources);
|
||||||
|
|
||||||
const deletedResource = await pamResourceDAL.deleteById(id);
|
try {
|
||||||
|
const deletedResource = await pamResourceDAL.deleteById(id);
|
||||||
return decryptResource(deletedResource, actor.orgId, kmsService);
|
return await decryptResource(deletedResource, resource.projectId, kmsService);
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof DatabaseError &&
|
||||||
|
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete resource because it is attached to active PAM accounts"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const list = async (projectId: string, actor: OrgServiceActor) => {
|
const list = async (projectId: string, actor: OrgServiceActor) => {
|
||||||
@@ -233,440 +207,7 @@ export const pamResourceServiceFactory = ({
|
|||||||
const resources = await pamResourceDAL.find({ projectId });
|
const resources = await pamResourceDAL.find({ projectId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resources: await Promise.all(resources.map((resource) => decryptResource(resource, actor.orgId, kmsService)))
|
resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, 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
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -676,12 +217,6 @@ export const pamResourceServiceFactory = ({
|
|||||||
updateById,
|
updateById,
|
||||||
deleteById,
|
deleteById,
|
||||||
list,
|
list,
|
||||||
listResourceOptions,
|
listResourceOptions
|
||||||
createAccount,
|
|
||||||
updateAccountById,
|
|
||||||
deleteAccountById,
|
|
||||||
listAccounts,
|
|
||||||
accessAccount,
|
|
||||||
getSessionCredentials
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,22 +25,6 @@ export type TUpdateResourceDTO = Partial<Omit<TCreateResourceDTO, "resourceType"
|
|||||||
resourceId: string;
|
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
|
// Resource factory
|
||||||
export type TPamResourceFactoryValidateConnection<T extends TPamResourceConnectionDetails> = () => Promise<T>;
|
export type TPamResourceFactoryValidateConnection<T extends TPamResourceConnectionDetails> = () => Promise<T>;
|
||||||
export type TPamResourceFactoryValidateAccountCredentials<C extends TPamAccountCredentials> = (
|
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";
|
import { TPamSanitizedSession, TPamSessionCommandLog } from "./pam-session.types";
|
||||||
|
|
||||||
export const decryptSessionCommandLogs = async ({
|
export const decryptSessionCommandLogs = async ({
|
||||||
orgId,
|
projectId,
|
||||||
encryptedLogs,
|
encryptedLogs,
|
||||||
kmsService
|
kmsService
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
projectId: string;
|
||||||
encryptedLogs: Buffer;
|
encryptedLogs: Buffer;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
}) => {
|
}) => {
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.SecretManager,
|
||||||
orgId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const decryptedPlainTextBlob = decryptor({
|
const decryptedPlainTextBlob = decryptor({
|
||||||
@@ -27,14 +27,14 @@ export const decryptSessionCommandLogs = async ({
|
|||||||
|
|
||||||
export const decryptSession = async (
|
export const decryptSession = async (
|
||||||
session: TPamSessions,
|
session: TPamSessions,
|
||||||
orgId: string,
|
projectId: string,
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
commandLogs: session.encryptedLogsBlob
|
commandLogs: session.encryptedLogsBlob
|
||||||
? await decryptSessionCommandLogs({
|
? await decryptSessionCommandLogs({
|
||||||
orgId,
|
projectId,
|
||||||
encryptedLogs: session.encryptedLogsBlob,
|
encryptedLogs: session.encryptedLogsBlob,
|
||||||
kmsService
|
kmsService
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const pamSessionServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
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 });
|
const sessions = await pamSessionDAL.find({ projectId });
|
||||||
|
|
||||||
return {
|
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({
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.SecretManager,
|
||||||
orgId: project.orgId
|
projectId: session.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cipherTextBlob } = encryptor({
|
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 { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||||
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
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 { pamFolderDALFactory } from "@app/ee/services/pam-folder/pam-folder-dal";
|
||||||
import { pamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
|
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 { pamResourceDALFactory } from "@app/ee/services/pam-resource/pam-resource-dal";
|
||||||
import { pamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
import { pamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
|
||||||
import { pamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal";
|
import { pamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal";
|
||||||
@@ -2123,14 +2124,22 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const pamResourceService = pamResourceServiceFactory({
|
const pamResourceService = pamResourceServiceFactory({
|
||||||
pamResourceDAL,
|
pamResourceDAL,
|
||||||
pamSessionDAL,
|
|
||||||
pamAccountDAL,
|
|
||||||
pamFolderDAL,
|
|
||||||
projectDAL,
|
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
|
gatewayV2Service
|
||||||
|
});
|
||||||
|
|
||||||
|
const pamAccountService = pamAccountServiceFactory({
|
||||||
|
pamAccountDAL,
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
|
kmsService,
|
||||||
|
licenseService,
|
||||||
|
pamFolderDAL,
|
||||||
|
pamResourceDAL,
|
||||||
|
pamSessionDAL,
|
||||||
|
permissionService,
|
||||||
|
projectDAL,
|
||||||
userDAL
|
userDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2282,6 +2291,7 @@ export const registerRoutes = async (
|
|||||||
notification: notificationService,
|
notification: notificationService,
|
||||||
pamFolder: pamFolderService,
|
pamFolder: pamFolderService,
|
||||||
pamResource: pamResourceService,
|
pamResource: pamResourceService,
|
||||||
|
pamAccount: pamAccountService,
|
||||||
pamSession: pamSessionService,
|
pamSession: pamSessionService,
|
||||||
upgradePath: upgradePathService
|
upgradePath: upgradePathService
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
restart: "always"
|
restart: "always"
|
||||||
ports:
|
ports:
|
||||||
- 8080:80
|
- 8080:80
|
||||||
- 8443:443
|
- 8444:443
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -197,4 +197,4 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
ldap_data:
|
ldap_data:
|
||||||
ldap_config:
|
ldap_config:
|
||||||
grafana_storage:
|
grafana_storage:
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ export const useDeletePamResource = () => {
|
|||||||
export const useCreatePamAccount = () => {
|
export const useCreatePamAccount = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ resourceId, resourceType, ...params }: TCreatePamAccountDTO) => {
|
mutationFn: async ({ resourceType, ...params }: TCreatePamAccountDTO) => {
|
||||||
const { data } = await apiRequest.post<{ account: TPamAccount }>(
|
const { data } = await apiRequest.post<{ account: TPamAccount }>(
|
||||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts`,
|
`/api/v1/pam/accounts/${resourceType}`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -90,14 +90,9 @@ export const useCreatePamAccount = () => {
|
|||||||
export const useUpdatePamAccount = () => {
|
export const useUpdatePamAccount = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ resourceType, accountId, ...params }: TUpdatePamAccountDTO) => {
|
||||||
resourceId,
|
|
||||||
resourceType,
|
|
||||||
accountId,
|
|
||||||
...params
|
|
||||||
}: TUpdatePamAccountDTO) => {
|
|
||||||
const { data } = await apiRequest.patch<{ account: TPamAccount }>(
|
const { data } = await apiRequest.patch<{ account: TPamAccount }>(
|
||||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`,
|
`/api/v1/pam/accounts/${resourceType}/${accountId}`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -112,9 +107,9 @@ export const useUpdatePamAccount = () => {
|
|||||||
export const useDeletePamAccount = () => {
|
export const useDeletePamAccount = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ resourceId, resourceType, accountId }: TDeletePamAccountDTO) => {
|
mutationFn: async ({ resourceType, accountId }: TDeletePamAccountDTO) => {
|
||||||
const { data } = await apiRequest.delete<{ account: TPamAccount }>(
|
const { data } = await apiRequest.delete<{ account: TPamAccount }>(
|
||||||
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`
|
`/api/v1/pam/accounts/${resourceType}/${accountId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.account;
|
return data.account;
|
||||||
|
|||||||
@@ -72,13 +72,11 @@ export type TUpdatePamAccountDTO = Partial<
|
|||||||
Pick<TPamAccount, "name" | "description" | "credentials">
|
Pick<TPamAccount, "name" | "description" | "credentials">
|
||||||
> & {
|
> & {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
resourceId: string;
|
|
||||||
resourceType: PamResourceType;
|
resourceType: PamResourceType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeletePamAccountDTO = {
|
export type TDeletePamAccountDTO = {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
resourceId: string;
|
|
||||||
resourceType: PamResourceType;
|
resourceType: PamResourceType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
|
|||||||
secretEvents.includes(eventType)
|
secretEvents.includes(eventType)
|
||||||
);
|
);
|
||||||
const showSecretsSection =
|
const showSecretsSection =
|
||||||
project?.type !== ProjectType.PAM &&
|
selectedProject?.type !== ProjectType.PAM &&
|
||||||
(hasSecretEventFilter || currentSelectedEventTypes.length === 0);
|
(hasSecretEventFilter || currentSelectedEventTypes.length === 0);
|
||||||
|
|
||||||
const filteredEventTypes = useMemo(() => {
|
const filteredEventTypes = useMemo(() => {
|
||||||
|
|||||||
@@ -16,13 +16,10 @@ type Props = {
|
|||||||
export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
|
export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
|
||||||
const [duration, setDuration] = useState("4h");
|
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(
|
const command = useMemo(
|
||||||
() =>
|
() => (account ? `infisical pam access ${account.id} --duration ${duration}` : ""),
|
||||||
account
|
|
||||||
? `infisical pam access ${account.id}${duration ? ` --duration ${duration}` : ""}`
|
|
||||||
: "",
|
|
||||||
[account, duration]
|
[account, duration]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
|
|||||||
try {
|
try {
|
||||||
const updatedAccount = await updatePamAccount.mutateAsync({
|
const updatedAccount = await updatePamAccount.mutateAsync({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
resourceId: account.resourceId,
|
|
||||||
resourceType: account.resource.resourceType,
|
resourceType: account.resource.resourceType,
|
||||||
...formData
|
...formData
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const BaseSqlAccountSchema = z.object({
|
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")
|
password: z.string().trim().min(1, "Password required")
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props)
|
|||||||
const {
|
const {
|
||||||
id: accountId,
|
id: accountId,
|
||||||
name,
|
name,
|
||||||
resourceId,
|
|
||||||
resource: { resourceType }
|
resource: { resourceType }
|
||||||
} = account;
|
} = account;
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props)
|
|||||||
try {
|
try {
|
||||||
await deletePamAccount.mutateAsync({
|
await deletePamAccount.mutateAsync({
|
||||||
accountId,
|
accountId,
|
||||||
resourceId,
|
|
||||||
resourceType
|
resourceType
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const PamResourceHeader = ({ resourceType, onBack }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center text-mineshaft-300">{details.name}</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>
|
</div>
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
|||||||
{
|
{
|
||||||
label: "Access Control",
|
label: "Access Control",
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
to: "/projects/secret-management/$projectId/access-management",
|
to: "/projects/pam/$projectId/access-management",
|
||||||
params: {
|
params: {
|
||||||
projectId: params.projectId
|
projectId: params.projectId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
|||||||
{
|
{
|
||||||
label: "Access Control",
|
label: "Access Control",
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
to: "/projects/secret-management/$projectId/access-management",
|
to: "/projects/pam/$projectId/access-management",
|
||||||
params: {
|
params: {
|
||||||
projectId: params.projectId
|
projectId: params.projectId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
|||||||
{
|
{
|
||||||
label: "Access Control",
|
label: "Access Control",
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
to: "/projects/secret-scanning/$projectId/access-management",
|
to: "/projects/pam/$projectId/access-management",
|
||||||
params: {
|
params: {
|
||||||
projectId: params.projectId
|
projectId: params.projectId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
|||||||
{
|
{
|
||||||
label: "Access Control",
|
label: "Access Control",
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
to: "/projects/secret-management/$projectId/access-management",
|
to: "/projects/pam/$projectId/access-management",
|
||||||
params: {
|
params: {
|
||||||
projectId: params.projectId
|
projectId: params.projectId
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user