addressed reviews

This commit is contained in:
x032205
2025-10-02 18:46:14 -04:00
parent 1678fba06e
commit e77c024af5
37 changed files with 1011 additions and 856 deletions

View File

@@ -28,6 +28,7 @@ import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { TPamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service";
import { TPamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
import { TPamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-session-service";
@@ -320,6 +321,7 @@ declare module "fastify" {
offlineUsageReport: TOfflineUsageReportServiceFactory;
pamFolder: TPamFolderServiceFactory;
pamResource: TPamResourceServiceFactory;
pamAccount: TPamAccountServiceFactory;
pamSession: TPamSessionServiceFactory;
upgradePath: TUpgradePathService;
};

View File

@@ -1,6 +1,7 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
// PAM Folders
@@ -18,6 +19,19 @@ export async function up(knex: Knex): Promise<void> {
t.string("name").notNullable();
t.index("name");
// Enforce uniqueness for sub-folders
t.unique(["projectId", "parentId", "name"], {
indexName: "uidx_pam_folder_children_name",
predicate: knex.whereNotNull("parentId")
});
// Enforce uniqueness for root-level folders
t.unique(["projectId", "name"], {
indexName: "uidx_pam_folder_root_name",
predicate: knex.whereNull("parentId")
});
t.text("description").nullable();
t.timestamps(true, true, true);
@@ -35,10 +49,14 @@ export async function up(knex: Knex): Promise<void> {
t.string("name").notNullable();
t.index("name");
t.string("gatewayId").notNullable();
t.uuid("gatewayId").notNullable();
t.foreign("gatewayId").references("id").inTable(TableName.GatewayV2);
t.index("gatewayId");
t.string("resourceType").notNullable();
t.index("resourceType");
t.binary("encryptedConnectionDetails").notNullable();
t.timestamps(true, true, true);
@@ -59,13 +77,25 @@ export async function up(knex: Knex): Promise<void> {
t.index("folderId");
t.uuid("resourceId").notNullable();
t.foreign("resourceId").references("id").inTable(TableName.PamResource).onDelete("CASCADE");
t.foreign("resourceId").references("id").inTable(TableName.PamResource);
t.index("resourceId");
t.string("name").notNullable();
t.index("name");
t.text("description").nullable();
// Enforce uniqueness for folders
t.unique(["projectId", "folderId", "name"], {
indexName: "uidx_pam_account_children_name",
predicate: knex.whereNotNull("folderId")
});
// Enforce uniqueness for root-level
t.unique(["projectId", "name"], {
indexName: "uidx_pam_account_root_name",
predicate: knex.whereNull("folderId")
});
t.text("description").nullable();
t.binary("encryptedCredentials").notNullable();
t.timestamps(true, true, true);
@@ -106,7 +136,7 @@ export async function up(knex: Knex): Promise<void> {
t.binary("encryptedLogsBlob").nullable();
t.datetime("expiresAt").nullable(); // null means unlimited duration / no expiry
t.datetime("expiresAt").notNullable();
t.datetime("startedAt").nullable(); // Not when the row is created, but when the end-to-end connection between user and resource is established
t.datetime("endedAt").nullable();
@@ -115,6 +145,11 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.PamFolder);
await createOnUpdateTrigger(knex, TableName.PamResource);
await createOnUpdateTrigger(knex, TableName.PamAccount);
await createOnUpdateTrigger(knex, TableName.PamSession);
}
export async function down(knex: Knex): Promise<void> {
@@ -122,4 +157,9 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.PamAccount);
await knex.schema.dropTableIfExists(TableName.PamResource);
await knex.schema.dropTableIfExists(TableName.PamFolder);
await dropOnUpdateTrigger(knex, TableName.PamSession);
await dropOnUpdateTrigger(knex, TableName.PamAccount);
await dropOnUpdateTrigger(knex, TableName.PamResource);
await dropOnUpdateTrigger(knex, TableName.PamFolder);
}

View File

@@ -23,7 +23,8 @@ import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOidcRouter } from "./oidc-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerPamAccountRouter } from "./pam-account-router";
import { PAM_ACCOUNT_REGISTER_ROUTER_MAP } from "./pam-account-routers";
import { registerPamAccountRouter } from "./pam-account-routers/pam-account-router";
import { registerPamFolderRouter } from "./pam-folder-router";
import { PAM_RESOURCE_REGISTER_ROUTER_MAP } from "./pam-resource-routers";
import { registerPamResourceRouter } from "./pam-resource-routers/pam-resource-router";
@@ -172,21 +173,39 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/kmip" }
);
await server.register(registerPamFolderRouter, { prefix: "/pam/folders" });
await server.register(registerPamAccountRouter, { prefix: "/pam/accounts" });
await server.register(registerPamSessionRouter, { prefix: "/pam/sessions" });
await server.register(
async (pamResourceRouter) => {
await pamResourceRouter.register(registerPamResourceRouter);
async (pamRouter) => {
await pamRouter.register(registerPamFolderRouter, { prefix: "/folders" });
await pamRouter.register(registerPamSessionRouter, { prefix: "/sessions" });
// Provider-specific endpoints
await Promise.all(
Object.entries(PAM_RESOURCE_REGISTER_ROUTER_MAP).map(([provider, router]) =>
pamResourceRouter.register(router, { prefix: `/${provider}` })
)
await pamRouter.register(
async (pamAccountRouter) => {
await pamAccountRouter.register(registerPamAccountRouter);
// Provider-specific endpoints
await Promise.all(
Object.entries(PAM_ACCOUNT_REGISTER_ROUTER_MAP).map(([provider, router]) =>
pamAccountRouter.register(router, { prefix: `/${provider}` })
)
);
},
{ prefix: "/accounts" }
);
await pamRouter.register(
async (pamResourceRouter) => {
await pamResourceRouter.register(registerPamResourceRouter);
// Provider-specific endpoints
await Promise.all(
Object.entries(PAM_RESOURCE_REGISTER_ROUTER_MAP).map(([provider, router]) =>
pamResourceRouter.register(router, { prefix: `/${provider}` })
)
);
},
{ prefix: "/resources" }
);
},
{ prefix: "/pam/resources" }
{ prefix: "/pam" }
);
};

View File

@@ -0,0 +1,20 @@
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import {
CreatePostgresAccountSchema,
SanitizedPostgresAccountWithResourceSchema,
UpdatePostgresAccountSchema
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { registerPamResourceEndpoints } from "./pam-account-endpoints";
export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: FastifyZodProvider) => Promise<void>> = {
[PamResource.Postgres]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.Postgres,
accountResponseSchema: SanitizedPostgresAccountWithResourceSchema,
createAccountSchema: CreatePostgresAccountSchema,
updateAccountSchema: UpdatePostgresAccountSchema
});
}
};

View File

@@ -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 };
}
});
};

View File

@@ -34,7 +34,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.pamResource.listAccounts(req.query.projectId, req.permission);
const response = await server.services.pamAccount.list(req.query.projectId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
@@ -55,21 +55,18 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:accountId/access",
url: "/access",
config: {
rateLimit: writeLimit
},
schema: {
description: "Access PAM account",
params: z.object({
accountId: z.string().uuid()
}),
body: z.object({
accountId: z.string().uuid(),
duration: z
.string()
.optional()
.min(1)
.transform((val, ctx) => {
if (val === undefined) return undefined;
const parsedMs = ms(val);
if (typeof parsedMs !== "number" || parsedMs <= 0) {
@@ -98,13 +95,13 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
// To prevent type errors when accessing req.auth
if (req.auth.authMode !== AuthMode.JWT) {
throw new BadRequestError({ message: "You can only access PAM accounts using JWT auth tokens." });
}
const response = await server.services.pamResource.accessAccount(
const response = await server.services.pamAccount.access(
{
accountId: req.params.accountId,
actorEmail: req.auth.user.email ?? "",
actorIp: req.realIp,
actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
@@ -121,7 +118,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.PAM_ACCOUNT_ACCESS,
metadata: {
accountId: req.params.accountId,
accountId: req.body.accountId,
accountName: response.account.name,
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
}
}

View File

@@ -138,6 +138,7 @@ export const registerPamFolderRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.PAM_FOLDER_DELETE,
metadata: {
folderName: folder.name,
folderId: req.params.folderId
}
}

View File

@@ -1,10 +1,7 @@
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import {
CreatePostgresAccountSchema,
CreatePostgresResourceSchema,
PostgresResourceSchema,
SanitizedPostgresAccountWithResourceSchema,
UpdatePostgresAccountSchema,
UpdatePostgresResourceSchema
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
@@ -16,11 +13,8 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
server,
resourceType: PamResource.Postgres,
resourceResponseSchema: PostgresResourceSchema,
accountResponseSchema: SanitizedPostgresAccountWithResourceSchema,
createResourceSchema: CreatePostgresResourceSchema,
createAccountSchema: CreatePostgresAccountSchema,
updateResourceSchema: UpdatePostgresResourceSchema,
updateAccountSchema: UpdatePostgresAccountSchema
updateResourceSchema: UpdatePostgresResourceSchema
});
}
};

View File

@@ -2,20 +2,17 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { TPamAccount, TPamResource } from "@app/ee/services/pam-resource/pam-resource-types";
import { TPamResource } from "@app/ee/services/pam-resource/pam-resource-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerPamResourceEndpoints = <T extends TPamResource, C extends TPamAccount>({
export const registerPamResourceEndpoints = <T extends TPamResource>({
server,
resourceType,
createResourceSchema,
updateResourceSchema,
createAccountSchema,
updateAccountSchema,
resourceResponseSchema,
accountResponseSchema
resourceResponseSchema
}: {
server: FastifyZodProvider;
resourceType: PamResource;
@@ -25,24 +22,12 @@ export const registerPamResourceEndpoints = <T extends TPamResource, C extends T
gatewayId: T["gatewayId"];
name: T["name"];
}>;
createAccountSchema: z.ZodType<{
credentials: C["credentials"];
folderId?: C["folderId"];
name: C["name"];
description?: C["description"];
}>;
updateResourceSchema: z.ZodType<{
connectionDetails?: T["connectionDetails"];
gatewayId?: T["gatewayId"];
name?: T["name"];
}>;
updateAccountSchema: z.ZodType<{
credentials?: C["credentials"];
name?: C["name"];
description?: C["description"];
}>;
resourceResponseSchema: z.ZodTypeAny;
accountResponseSchema: z.ZodTypeAny;
}) => {
server.route({
method: "GET",
@@ -210,142 +195,4 @@ export const registerPamResourceEndpoints = <T extends TPamResource, C extends T
return { resource };
}
});
// PAM Accounts
server.route({
method: "POST",
url: "/:resourceId/accounts",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create PAM resource account",
params: z.object({
resourceId: z.string().uuid()
}),
body: createAccountSchema,
response: {
200: z.object({
account: accountResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const account = await server.services.pamResource.createAccount(
{
...req.body,
resourceId: req.params.resourceId
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: account.projectId,
event: {
type: EventType.PAM_ACCOUNT_CREATE,
metadata: {
resourceId: req.params.resourceId,
resourceType,
folderId: req.body.folderId,
name: req.body.name,
description: req.body.description
}
}
});
return { account };
}
});
server.route({
method: "PATCH",
url: "/:resourceId/accounts/:accountId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update PAM resource account",
params: z.object({
resourceId: z.string().uuid(),
accountId: z.string().uuid()
}),
body: updateAccountSchema,
response: {
200: z.object({
account: accountResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const account = await server.services.pamResource.updateAccountById(
{
...req.body,
accountId: req.params.accountId
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: account.projectId,
event: {
type: EventType.PAM_ACCOUNT_UPDATE,
metadata: {
accountId: req.params.accountId,
resourceId: req.params.resourceId,
resourceType,
name: req.body.name,
description: req.body.description
}
}
});
return { account };
}
});
server.route({
method: "DELETE",
url: "/:resourceId/accounts/:accountId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete PAM resource account",
params: z.object({
resourceId: z.string().uuid(),
accountId: z.string().uuid()
}),
response: {
200: z.object({
account: accountResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const account = await server.services.pamResource.deleteAccountById(req.params.accountId, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: account.projectId,
event: {
type: EventType.PAM_ACCOUNT_DELETE,
metadata: {
accountId: req.params.accountId,
resourceId: req.params.resourceId,
resourceType
}
}
});
return { account };
}
});
};

View File

@@ -32,7 +32,7 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { credentials, projectId } = await server.services.pamResource.getSessionCredentials(
const { credentials, projectId, account } = await server.services.pamAccount.getSessionCredentials(
req.params.sessionId,
req.permission
);
@@ -44,7 +44,8 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.PAM_SESSION_START,
metadata: {
sessionId: req.params.sessionId
sessionId: req.params.sessionId,
accountName: account.name
}
}
});
@@ -93,7 +94,8 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.PAM_SESSION_LOGS_UPDATE,
metadata: {
sessionId: req.params.sessionId
sessionId: req.params.sessionId,
accountName: session.accountName
}
}
});

View File

@@ -3710,6 +3710,7 @@ interface PamSessionStartEvent {
type: EventType.PAM_SESSION_START;
metadata: {
sessionId: string;
accountName: string;
};
}
@@ -3717,6 +3718,7 @@ interface PamSessionLogsUpdateEvent {
type: EventType.PAM_SESSION_LOGS_UPDATE;
metadata: {
sessionId: string;
accountName: string;
};
}
@@ -3763,6 +3765,7 @@ interface PamFolderDeleteEvent {
type: EventType.PAM_FOLDER_DELETE;
metadata: {
folderId: string;
folderName: string;
};
}
@@ -3778,6 +3781,7 @@ interface PamAccountAccessEvent {
type: EventType.PAM_ACCOUNT_ACCESS;
metadata: {
accountId: string;
accountName: string;
duration?: string;
};
}
@@ -3807,6 +3811,7 @@ interface PamAccountUpdateEvent {
interface PamAccountDeleteEvent {
type: EventType.PAM_ACCOUNT_DELETE;
metadata: {
accountName: string;
accountId: string;
resourceId: string;
resourceType: string;

View File

@@ -274,7 +274,6 @@ export const gatewayV2ServiceFactory = ({
gatewayId: string;
targetHost: string;
targetPort: number;
actorMetadata?: { sessionId?: string; resourceType?: string };
}) => {
const gateway = await gatewayV2DAL.findById(gatewayId);
if (!gateway) {
@@ -817,7 +816,20 @@ export const gatewayV2ServiceFactory = ({
OrgPermissionSubjects.Gateway
);
return gatewayV2DAL.deleteById(gateway.id);
try {
return await gatewayV2DAL.deleteById(gateway.id);
} catch (err) {
if (
err instanceof DatabaseError &&
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
) {
throw new BadRequestError({
message: "Failed to delete gateway because it is attached to active resources"
});
}
throw err;
}
};
const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {

View File

@@ -0,0 +1,61 @@
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
export const encryptAccountCredentials = async ({
projectId,
credentials,
kmsService
}: {
projectId: string;
credentials: TPamAccountCredentials;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
plainText: Buffer.from(JSON.stringify(credentials))
});
return encryptedCredentialsBlob;
};
export const decryptAccountCredentials = async ({
projectId,
encryptedCredentials,
kmsService
}: {
projectId: string;
encryptedCredentials: Buffer;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedCredentials
});
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
};
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
account: T,
projectId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<T & { credentials: TPamAccountCredentials }> => {
return {
...account,
credentials: await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
projectId,
kmsService
})
} as T & { credentials: TPamAccountCredentials };
};

View File

@@ -0,0 +1,520 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas";
import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory";
import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionActions,
ProjectPermissionPamAccountActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal";
import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns";
import { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal";
import { PamResource } from "../pam-resource/pam-resource-enums";
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
import { TPamSessionDALFactory } from "../pam-session/pam-session-dal";
import { PamSessionStatus } from "../pam-session/pam-session-enums";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPamAccountDALFactory } from "./pam-account-dal";
import { decryptAccount, decryptAccountCredentials, encryptAccountCredentials } from "./pam-account-fns";
import { TAccessAccountDTO, TCreateAccountDTO, TUpdateAccountDTO } from "./pam-account-types";
type TPamAccountServiceFactoryDep = {
pamResourceDAL: TPamResourceDALFactory;
pamSessionDAL: TPamSessionDALFactory;
pamAccountDAL: TPamAccountDALFactory;
pamFolderDAL: TPamFolderDALFactory;
projectDAL: TProjectDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayV2Service: Pick<
TGatewayV2ServiceFactory,
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
>;
userDAL: TUserDALFactory;
};
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
export const pamAccountServiceFactory = ({
pamResourceDAL,
pamSessionDAL,
pamAccountDAL,
pamFolderDAL,
projectDAL,
userDAL,
permissionService,
licenseService,
kmsService,
gatewayV2Service
}: TPamAccountServiceFactoryDep) => {
const create = async (
{ credentials, resourceId, name, description, folderId }: TCreateAccountDTO,
actor: OrgServiceActor
) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
const resource = await pamResourceDAL.findById(resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: resource.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId,
projectId: resource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Create,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: name,
accountPath
})
);
const connectionDetails = await decryptResourceConnectionDetails({
projectId: resource.projectId,
encryptedConnectionDetails: resource.encryptedConnectionDetails,
kmsService
});
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
resource.resourceType as PamResource,
connectionDetails,
resource.gatewayId,
gatewayV2Service
);
const validatedCredentials = await factory.validateAccountCredentials(credentials);
const encryptedCredentials = await encryptAccountCredentials({
credentials: validatedCredentials,
projectId: resource.projectId,
kmsService
});
try {
const account = await pamAccountDAL.create({
projectId: resource.projectId,
resourceId: resource.id,
encryptedCredentials,
name,
description,
folderId
});
return {
...(await decryptAccount(account, resource.projectId, kmsService)),
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
};
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `Account with name '${name}' already exists for this path`
});
}
throw err;
}
};
const updateById = async (
{ accountId, credentials, description, name }: TUpdateAccountDTO,
actor: OrgServiceActor
) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
const account = await pamAccountDAL.findById(accountId);
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: account.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Edit,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: account.name,
accountPath
})
);
const updateDoc: Partial<TPamAccounts> = {};
if (name !== undefined) {
updateDoc.name = name;
}
if (description !== undefined) {
updateDoc.description = description;
}
if (credentials !== undefined) {
const connectionDetails = await decryptResourceConnectionDetails({
projectId: account.projectId,
encryptedConnectionDetails: resource.encryptedConnectionDetails,
kmsService
});
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
resource.resourceType as PamResource,
connectionDetails,
resource.gatewayId,
gatewayV2Service
);
// Logic to prevent overwriting unedited censored values
const finalCredentials = { ...credentials };
if (credentials.password === "******") {
const decryptedCredentials = await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
projectId: account.projectId,
kmsService
});
finalCredentials.password = decryptedCredentials.password;
}
const validatedCredentials = await factory.validateAccountCredentials(finalCredentials);
const encryptedCredentials = await encryptAccountCredentials({
credentials: validatedCredentials,
projectId: account.projectId,
kmsService
});
updateDoc.encryptedCredentials = encryptedCredentials;
}
// If nothing was updated, return the fetched account
if (Object.keys(updateDoc).length === 0) {
return decryptAccount(account, account.projectId, kmsService);
}
const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc);
return {
...(await decryptAccount(updatedAccount, account.projectId, kmsService)),
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
};
};
const deleteById = async (id: string, actor: OrgServiceActor) => {
const account = await pamAccountDAL.findById(id);
if (!account) throw new NotFoundError({ message: `Account with ID '${id}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: account.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Delete,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: account.name,
accountPath
})
);
const deletedAccount = await pamAccountDAL.deleteById(id);
return {
...(await decryptAccount(deletedAccount, account.projectId, kmsService)),
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
};
};
const list = async (projectId: string, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId,
actionProjectType: ActionProjectType.PAM
});
const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId });
const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders);
const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : [];
const decryptedAndPermittedAccounts: Array<
TPamAccounts & {
resource: Pick<TPamResources, "id" | "name" | "resourceType">;
credentials: TPamAccountCredentials;
}
> = [];
for await (const account of accountsWithResourceDetails) {
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
// Check permission for each individual account
if (
permission.can(
ProjectPermissionPamAccountActions.Read,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: account.resource.name,
accountName: account.name,
accountPath
})
)
) {
// Decrypt the account only if the user has permission to read it
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
decryptedAndPermittedAccounts.push({
...decryptedAccount,
resource: {
id: account.resource.id,
name: account.resource.name,
resourceType: account.resource.resourceType
}
});
}
}
return {
accounts: decryptedAndPermittedAccounts,
folders
};
};
const access = async (
{ accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO,
actor: OrgServiceActor
) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
const account = await pamAccountDAL.findById(accountId);
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: account.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Access,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: account.name,
accountPath
})
);
const session = await pamSessionDAL.create({
accountName: account.name,
actorEmail,
actorIp,
actorName,
actorUserAgent,
projectId: account.projectId,
resourceName: resource.name,
resourceType: resource.resourceType,
status: PamSessionStatus.Starting,
accountId: account.id,
userId: actor.id,
expiresAt: new Date(Date.now() + duration)
});
const { connectionDetails, gatewayId, resourceType } = await decryptResource(
resource,
account.projectId,
kmsService
);
const user = await userDAL.findById(actor.id);
const gatewayConnectionDetails = await gatewayV2Service.getPAMConnectionDetails({
gatewayId,
duration,
sessionId: session.id,
resourceType: resource.resourceType as PamResource,
host: connectionDetails.host,
port: connectionDetails.port,
actorMetadata: {
id: actor.id,
type: actor.type,
name: user.email ?? ""
}
});
if (!gatewayConnectionDetails) {
throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` });
}
return {
sessionId: session.id,
resourceType,
relayClientCertificate: gatewayConnectionDetails.relay.clientCertificate,
relayClientPrivateKey: gatewayConnectionDetails.relay.clientPrivateKey,
relayServerCertificateChain: gatewayConnectionDetails.relay.serverCertificateChain,
gatewayClientCertificate: gatewayConnectionDetails.gateway.clientCertificate,
gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey,
gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain,
relayHost: gatewayConnectionDetails.relayHost,
projectId: account.projectId,
account
};
};
const getSessionCredentials = async (sessionId: string, actor: OrgServiceActor) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
// To be hit by gateways only
if (actor.type !== ActorType.IDENTITY) {
throw new ForbiddenRequestError({ message: "Only gateways can perform this action" });
}
const session = await pamSessionDAL.findById(sessionId);
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
const project = await projectDAL.findById(session.projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
project.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways,
OrgPermissionSubjects.Gateway
);
if (!session.accountId) throw new NotFoundError({ message: "Session is missing accountId column" });
// Verify that the session has not ended
if (session.endedAt || (session.expiresAt && session.expiresAt < new Date())) {
throw new BadRequestError({ message: "Session has ended or expired" });
}
// Verify that the session has not already had credentials fetched
if (session.status !== PamSessionStatus.Starting) {
throw new BadRequestError({ message: "Session has already been started" });
}
const account = await pamAccountDAL.findById(session.accountId);
if (!account) throw new NotFoundError({ message: `Account with ID '${session.accountId}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const decryptedAccount = await decryptAccount(account, session.projectId, kmsService);
const decryptedResource = await decryptResource(resource, session.projectId, kmsService);
// Mark session as started
await pamSessionDAL.updateById(sessionId, {
status: PamSessionStatus.Active,
startedAt: new Date()
});
return {
credentials: {
...decryptedResource.connectionDetails,
...decryptedAccount.credentials
},
projectId: project.id,
account
};
};
return {
create,
updateById,
deleteById,
list,
access,
getSessionCredentials
};
};

View File

@@ -0,0 +1,17 @@
import { TPamAccount } from "../pam-resource/pam-resource-types";
// DTOs
export type TCreateAccountDTO = Pick<TPamAccount, "name" | "description" | "credentials" | "folderId" | "resourceId">;
export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "resourceId">> & {
accountId: string;
};
export type TAccessAccountDTO = {
accountId: string;
actorEmail: string;
actorIp: string;
actorName: string;
actorUserAgent: string;
duration: number;
};

View File

@@ -3,7 +3,8 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TPamFolders } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TLicenseServiceFactory } from "../license/license-service";
@@ -50,26 +51,24 @@ export const pamFolderServiceFactory = ({
}
}
const existingFolder = await pamFolderDAL.findOne({
name,
parentId: parentId || null,
projectId
});
if (existingFolder) {
throw new BadRequestError({
message: `Folder with name '${name}' already exists for this parent`
try {
const folder = await pamFolderDAL.create({
name,
description: description ?? null,
parentId: parentId || null,
projectId
});
return folder;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `Folder with name '${name}' already exists for this path`
});
}
throw err;
}
const folder = await pamFolderDAL.create({
name,
description: description ?? null,
parentId: parentId || null,
projectId
});
return folder;
};
const updateFolder = async ({ id, name, description }: TUpdateFolderDTO, actor: OrgServiceActor) => {
@@ -104,27 +103,23 @@ export const pamFolderServiceFactory = ({
updateDoc.description = description;
}
if (name && name !== folder.name) {
const existingFolder = await pamFolderDAL.findOne({
name,
parentId: folder.parentId || null,
projectId: folder.projectId
});
if (existingFolder) {
throw new BadRequestError({
message: `Folder with name '${name}' already exists for this parent`
});
}
}
if (Object.keys(updateDoc).length === 0) {
return folder;
}
const updatedFolder = await pamFolderDAL.updateById(id, updateDoc);
try {
const updatedFolder = await pamFolderDAL.updateById(id, updateDoc);
return updatedFolder;
return updatedFolder;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `Folder with name '${name}' already exists for this path`
});
}
throw err;
}
};
const deleteFolder = async (id: string, actor: OrgServiceActor) => {

View File

@@ -2,7 +2,7 @@ import { TPamResources } from "@app/db/schemas";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TPamAccountCredentials, TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
export const listResourceOptions = () => {
@@ -11,17 +11,17 @@ export const listResourceOptions = () => {
// Resource
export const encryptResourceConnectionDetails = async ({
orgId,
projectId,
connectionDetails,
kmsService
}: {
orgId: string;
projectId: string;
connectionDetails: TPamResourceConnectionDetails;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
type: KmsDataKey.SecretManager,
projectId
});
const { cipherTextBlob: encryptedConnectionDetailsBlob } = encryptor({
@@ -32,17 +32,17 @@ export const encryptResourceConnectionDetails = async ({
};
export const decryptResourceConnectionDetails = async ({
orgId,
projectId,
encryptedConnectionDetails,
kmsService
}: {
orgId: string;
projectId: string;
encryptedConnectionDetails: Buffer;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
type: KmsDataKey.SecretManager,
projectId
});
const decryptedPlainTextBlob = decryptor({
@@ -54,73 +54,15 @@ export const decryptResourceConnectionDetails = async ({
export const decryptResource = async (
resource: TPamResources,
orgId: string,
projectId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
return {
...resource,
connectionDetails: await decryptResourceConnectionDetails({
encryptedConnectionDetails: resource.encryptedConnectionDetails,
orgId,
projectId,
kmsService
})
} as TPamResource;
};
// Account
export const encryptAccountCredentials = async ({
orgId,
credentials,
kmsService
}: {
orgId: string;
credentials: TPamAccountCredentials;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
plainText: Buffer.from(JSON.stringify(credentials))
});
return encryptedCredentialsBlob;
};
export const decryptAccountCredentials = async ({
orgId,
encryptedCredentials,
kmsService
}: {
orgId: string;
encryptedCredentials: Buffer;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedCredentials
});
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
};
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
account: T,
orgId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<T & { credentials: TPamAccountCredentials }> => {
return {
...account,
credentials: await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
orgId,
kmsService
})
} as T & { credentials: TPamAccountCredentials };
};

View File

@@ -34,12 +34,13 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
});
export const BaseCreatePamAccountSchema = z.object({
resourceId: z.string().uuid(),
folderId: z.string().uuid().optional(),
name: slugSchema({ field: "name" }),
description: z.string().max(512).optional()
description: z.string().max(512).nullable().optional()
});
export const BaseUpdatePamAccountSchema = z.object({
name: slugSchema({ field: "name" }).optional(),
description: z.string().max(512).optional()
description: z.string().max(512).nullable().optional()
});

View File

@@ -1,54 +1,23 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas";
import { ActionProjectType, TPamResources } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionActions,
ProjectPermissionPamAccountActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal";
import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns";
import { TPamSessionDALFactory } from "../pam-session/pam-session-dal";
import { PamSessionStatus } from "../pam-session/pam-session-enums";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPamAccountDALFactory } from "./pam-account-dal";
import { TPamResourceDALFactory } from "./pam-resource-dal";
import { PamResource } from "./pam-resource-enums";
import { PAM_RESOURCE_FACTORY_MAP } from "./pam-resource-factory";
import {
decryptAccount,
decryptAccountCredentials,
decryptResource,
decryptResourceConnectionDetails,
encryptAccountCredentials,
encryptResourceConnectionDetails,
listResourceOptions
} from "./pam-resource-fns";
import {
TAccessAccountDTO,
TCreateAccountDTO,
TCreateResourceDTO,
TPamAccountCredentials,
TUpdateAccountDTO,
TUpdateResourceDTO
} from "./pam-resource-types";
import { decryptResource, encryptResourceConnectionDetails, listResourceOptions } from "./pam-resource-fns";
import { TCreateResourceDTO, TUpdateResourceDTO } from "./pam-resource-types";
type TPamResourceServiceFactoryDep = {
pamResourceDAL: TPamResourceDALFactory;
pamSessionDAL: TPamSessionDALFactory;
pamAccountDAL: TPamAccountDALFactory;
pamFolderDAL: TPamFolderDALFactory;
projectDAL: TProjectDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -56,18 +25,12 @@ type TPamResourceServiceFactoryDep = {
TGatewayV2ServiceFactory,
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
>;
userDAL: TUserDALFactory;
};
export type TPamResourceServiceFactory = ReturnType<typeof pamResourceServiceFactory>;
export const pamResourceServiceFactory = ({
pamResourceDAL,
pamSessionDAL,
pamAccountDAL,
pamFolderDAL,
projectDAL,
userDAL,
permissionService,
licenseService,
kmsService,
@@ -94,7 +57,7 @@ export const pamResourceServiceFactory = ({
});
}
return decryptResource(resource, actor.orgId, kmsService);
return decryptResource(resource, resource.projectId, kmsService);
};
const create = async (
@@ -129,7 +92,7 @@ export const pamResourceServiceFactory = ({
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
connectionDetails: validatedConnectionDetails,
orgId: actor.orgId,
projectId,
kmsService
});
@@ -141,7 +104,7 @@ export const pamResourceServiceFactory = ({
projectId
});
return decryptResource(resource, actor.orgId, kmsService);
return decryptResource(resource, projectId, kmsService);
};
const updateById = async ({ connectionDetails, resourceId, name }: TUpdateResourceDTO, actor: OrgServiceActor) => {
@@ -182,7 +145,7 @@ export const pamResourceServiceFactory = ({
const validatedConnectionDetails = await factory.validateConnection();
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
connectionDetails: validatedConnectionDetails,
orgId: actor.orgId,
projectId: resource.projectId,
kmsService
});
updateDoc.encryptedConnectionDetails = encryptedConnectionDetails;
@@ -190,12 +153,12 @@ export const pamResourceServiceFactory = ({
// If nothing was updated, return the fetched resource
if (Object.keys(updateDoc).length === 0) {
return decryptResource(resource, actor.orgId, kmsService);
return decryptResource(resource, resource.projectId, kmsService);
}
const updatedResource = await pamResourceDAL.updateById(resourceId, updateDoc);
return decryptResource(updatedResource, actor.orgId, kmsService);
return decryptResource(updatedResource, resource.projectId, kmsService);
};
const deleteById = async (id: string, actor: OrgServiceActor) => {
@@ -213,9 +176,20 @@ export const pamResourceServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PamResources);
const deletedResource = await pamResourceDAL.deleteById(id);
return decryptResource(deletedResource, actor.orgId, kmsService);
try {
const deletedResource = await pamResourceDAL.deleteById(id);
return await decryptResource(deletedResource, resource.projectId, kmsService);
} catch (err) {
if (
err instanceof DatabaseError &&
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
) {
throw new BadRequestError({
message: "Failed to delete resource because it is attached to active PAM accounts"
});
}
throw err;
}
};
const list = async (projectId: string, actor: OrgServiceActor) => {
@@ -233,440 +207,7 @@ export const pamResourceServiceFactory = ({
const resources = await pamResourceDAL.find({ projectId });
return {
resources: await Promise.all(resources.map((resource) => decryptResource(resource, actor.orgId, kmsService)))
};
};
// Accounts
const createAccount = async (
{ credentials, resourceId, name, description, folderId }: TCreateAccountDTO,
actor: OrgServiceActor
) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
const resource = await pamResourceDAL.findById(resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: resource.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId,
projectId: resource.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Create,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: name,
accountPath
})
);
const connectionDetails = await decryptResourceConnectionDetails({
orgId: actor.orgId,
encryptedConnectionDetails: resource.encryptedConnectionDetails,
kmsService
});
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
resource.resourceType as PamResource,
connectionDetails,
resource.gatewayId,
gatewayV2Service
);
const validatedCredentials = await factory.validateAccountCredentials(credentials);
const encryptedCredentials = await encryptAccountCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
const account = await pamAccountDAL.create({
projectId: resource.projectId,
resourceId: resource.id,
encryptedCredentials,
name,
description,
folderId
});
return {
...(await decryptAccount(account, actor.orgId, kmsService)),
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
};
};
const updateAccountById = async (
{ accountId, credentials, description, name }: TUpdateAccountDTO,
actor: OrgServiceActor
) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
const account = await pamAccountDAL.findById(accountId);
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: account.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Edit,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: account.name,
accountPath
})
);
const updateDoc: Partial<TPamAccounts> = {};
if (name !== undefined) {
updateDoc.name = name;
}
if (description !== undefined) {
updateDoc.description = description;
}
if (credentials !== undefined) {
const connectionDetails = await decryptResourceConnectionDetails({
orgId: actor.orgId,
encryptedConnectionDetails: resource.encryptedConnectionDetails,
kmsService
});
const factory = PAM_RESOURCE_FACTORY_MAP[resource.resourceType as PamResource](
resource.resourceType as PamResource,
connectionDetails,
resource.gatewayId,
gatewayV2Service
);
// Logic to prevent overwriting unedited censored values
const finalCredentials = { ...credentials };
if (credentials.password === "******") {
const decryptedCredentials = await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
orgId: actor.orgId,
kmsService
});
finalCredentials.password = decryptedCredentials.password;
}
const validatedCredentials = await factory.validateAccountCredentials(finalCredentials);
const encryptedCredentials = await encryptAccountCredentials({
credentials: validatedCredentials,
orgId: actor.orgId,
kmsService
});
updateDoc.encryptedCredentials = encryptedCredentials;
}
// If nothing was updated, return the fetched account
if (Object.keys(updateDoc).length === 0) {
return decryptAccount(account, actor.orgId, kmsService);
}
const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc);
return {
...(await decryptAccount(updatedAccount, actor.orgId, kmsService)),
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
};
};
const deleteAccountById = async (id: string, actor: OrgServiceActor) => {
const account = await pamAccountDAL.findById(id);
if (!account) throw new NotFoundError({ message: `Account with ID '${id}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: account.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Delete,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: account.name,
accountPath
})
);
const deletedAccount = await pamAccountDAL.deleteById(id);
return {
...(await decryptAccount(deletedAccount, actor.orgId, kmsService)),
resource: { id: resource.id, name: resource.name, resourceType: resource.resourceType }
};
};
const listAccounts = async (projectId: string, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId,
actionProjectType: ActionProjectType.PAM
});
const accountsWithResourceDetails = await pamAccountDAL.findWithResourceDetails({ projectId });
const canReadFolders = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.PamFolders);
const folders = canReadFolders ? await pamFolderDAL.find({ projectId }) : [];
const decryptedAndPermittedAccounts: Array<
TPamAccounts & {
resource: Pick<TPamResources, "id" | "name" | "resourceType">;
credentials: TPamAccountCredentials;
}
> = [];
for await (const account of accountsWithResourceDetails) {
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
// Check permission for each individual account
if (
permission.can(
ProjectPermissionPamAccountActions.Read,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: account.resource.name,
accountName: account.name,
accountPath
})
)
) {
// Decrypt the account only if the user has permission to read it
const decryptedAccount = await decryptAccount(account, actor.orgId, kmsService);
decryptedAndPermittedAccounts.push({
...decryptedAccount,
resource: {
id: account.resource.id,
name: account.resource.name,
resourceType: account.resource.resourceType
}
});
}
}
return {
accounts: decryptedAndPermittedAccounts,
folders
};
};
const accessAccount = async (
{ accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO,
actor: OrgServiceActor
) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
const account = await pamAccountDAL.findById(accountId);
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorId: actor.id,
actorOrgId: actor.orgId,
projectId: account.projectId,
actionProjectType: ActionProjectType.PAM
});
const accountPath = await getFullPamFolderPath({
pamFolderDAL,
folderId: account.folderId,
projectId: account.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Access,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: account.name,
accountPath
})
);
const session = await pamSessionDAL.create({
accountName: account.name,
actorEmail,
actorIp,
actorName,
actorUserAgent,
projectId: account.projectId,
resourceName: resource.name,
resourceType: resource.resourceType,
status: PamSessionStatus.Starting,
accountId: account.id,
userId: actor.id,
expiresAt: duration ? new Date(Date.now() + duration) : null
});
const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, actor.orgId, kmsService);
const user = await userDAL.findById(actor.id);
const gatewayConnectionDetails = await gatewayV2Service.getPAMConnectionDetails({
gatewayId,
duration,
sessionId: session.id,
resourceType: resource.resourceType as PamResource,
host: connectionDetails.host,
port: connectionDetails.port,
actorMetadata: {
id: actor.id,
type: actor.type,
name: user.email ?? ""
}
});
if (!gatewayConnectionDetails) {
throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` });
}
return {
sessionId: session.id,
resourceType,
relayClientCertificate: gatewayConnectionDetails.relay.clientCertificate,
relayClientPrivateKey: gatewayConnectionDetails.relay.clientPrivateKey,
relayServerCertificateChain: gatewayConnectionDetails.relay.serverCertificateChain,
gatewayClientCertificate: gatewayConnectionDetails.gateway.clientCertificate,
gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey,
gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain,
relayHost: gatewayConnectionDetails.relayHost,
projectId: account.projectId
};
};
const getSessionCredentials = async (sessionId: string, actor: OrgServiceActor) => {
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
if (!orgLicensePlan.pam) {
throw new BadRequestError({
message: "PAM operation failed due to organization plan restrictions."
});
}
// To be hit by gateways only
if (actor.type !== ActorType.IDENTITY) {
throw new ForbiddenRequestError({ message: "Only gateways can perform this action" });
}
const session = await pamSessionDAL.findById(sessionId);
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
const project = await projectDAL.findById(session.projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
project.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways,
OrgPermissionSubjects.Gateway
);
if (!session.accountId) throw new NotFoundError({ message: "Session is missing accountId column" });
// Verify that the session has not ended
if (session.endedAt || (session.expiresAt && session.expiresAt < new Date())) {
throw new BadRequestError({ message: "Session has ended or expired" });
}
// Verify that the session has not already had credentials fetched
if (session.status !== PamSessionStatus.Starting) {
throw new BadRequestError({ message: "Session has already been started" });
}
const account = await pamAccountDAL.findById(session.accountId);
if (!account) throw new NotFoundError({ message: `Account with ID '${session.accountId}' not found` });
const resource = await pamResourceDAL.findById(account.resourceId);
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
const decryptedAccount = await decryptAccount(account, actor.orgId, kmsService);
const decryptedResource = await decryptResource(resource, actor.orgId, kmsService);
// Mark session as started
await pamSessionDAL.updateById(sessionId, {
status: PamSessionStatus.Active,
startedAt: new Date()
});
return {
credentials: {
...decryptedResource.connectionDetails,
...decryptedAccount.credentials
},
projectId: project.id
resources: await Promise.all(resources.map((resource) => decryptResource(resource, projectId, kmsService)))
};
};
@@ -676,12 +217,6 @@ export const pamResourceServiceFactory = ({
updateById,
deleteById,
list,
listResourceOptions,
createAccount,
updateAccountById,
deleteAccountById,
listAccounts,
accessAccount,
getSessionCredentials
listResourceOptions
};
};

View File

@@ -25,22 +25,6 @@ export type TUpdateResourceDTO = Partial<Omit<TCreateResourceDTO, "resourceType"
resourceId: string;
};
// Account DTOs
export type TCreateAccountDTO = Pick<TPamAccount, "name" | "description" | "credentials" | "folderId" | "resourceId">;
export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "resourceId">> & {
accountId: string;
};
export type TAccessAccountDTO = {
accountId: string;
actorEmail: string;
actorIp: string;
actorName: string;
actorUserAgent: string;
duration?: number;
};
// Resource factory
export type TPamResourceFactoryValidateConnection<T extends TPamResourceConnectionDetails> = () => Promise<T>;
export type TPamResourceFactoryValidateAccountCredentials<C extends TPamAccountCredentials> = (

View File

@@ -5,17 +5,17 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TPamSanitizedSession, TPamSessionCommandLog } from "./pam-session.types";
export const decryptSessionCommandLogs = async ({
orgId,
projectId,
encryptedLogs,
kmsService
}: {
orgId: string;
projectId: string;
encryptedLogs: Buffer;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
type: KmsDataKey.SecretManager,
projectId
});
const decryptedPlainTextBlob = decryptor({
@@ -27,14 +27,14 @@ export const decryptSessionCommandLogs = async ({
export const decryptSession = async (
session: TPamSessions,
orgId: string,
projectId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
return {
...session,
commandLogs: session.encryptedLogsBlob
? await decryptSessionCommandLogs({
orgId,
projectId,
encryptedLogs: session.encryptedLogsBlob,
kmsService
})

View File

@@ -53,7 +53,7 @@ export const pamSessionServiceFactory = ({
);
return {
session: await decryptSession(session, actor.orgId, kmsService)
session: await decryptSession(session, session.projectId, kmsService)
};
};
@@ -75,7 +75,7 @@ export const pamSessionServiceFactory = ({
const sessions = await pamSessionDAL.find({ projectId });
return {
sessions: await Promise.all(sessions.map((session) => decryptSession(session, actor.orgId, kmsService)))
sessions: await Promise.all(sessions.map((session) => decryptSession(session, projectId, kmsService)))
};
};
@@ -112,8 +112,8 @@ export const pamSessionServiceFactory = ({
);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: project.orgId
type: KmsDataKey.SecretManager,
projectId: session.projectId
});
const { cipherTextBlob } = encryptor({

View File

@@ -66,9 +66,10 @@ import { licenseDALFactory } from "@app/ee/services/license/license-dal";
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { pamAccountDALFactory } from "@app/ee/services/pam-account/pam-account-dal";
import { pamAccountServiceFactory } from "@app/ee/services/pam-account/pam-account-service";
import { pamFolderDALFactory } from "@app/ee/services/pam-folder/pam-folder-dal";
import { pamFolderServiceFactory } from "@app/ee/services/pam-folder/pam-folder-service";
import { pamAccountDALFactory } from "@app/ee/services/pam-resource/pam-account-dal";
import { pamResourceDALFactory } from "@app/ee/services/pam-resource/pam-resource-dal";
import { pamResourceServiceFactory } from "@app/ee/services/pam-resource/pam-resource-service";
import { pamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal";
@@ -2123,14 +2124,22 @@ export const registerRoutes = async (
const pamResourceService = pamResourceServiceFactory({
pamResourceDAL,
pamSessionDAL,
pamAccountDAL,
pamFolderDAL,
projectDAL,
permissionService,
licenseService,
kmsService,
gatewayV2Service
});
const pamAccountService = pamAccountServiceFactory({
pamAccountDAL,
gatewayV2Service,
kmsService,
licenseService,
pamFolderDAL,
pamResourceDAL,
pamSessionDAL,
permissionService,
projectDAL,
userDAL
});
@@ -2282,6 +2291,7 @@ export const registerRoutes = async (
notification: notificationService,
pamFolder: pamFolderService,
pamResource: pamResourceService,
pamAccount: pamAccountService,
pamSession: pamSessionService,
upgradePath: upgradePathService
});

View File

@@ -7,7 +7,7 @@ services:
restart: "always"
ports:
- 8080:80
- 8443:443
- 8444:443
volumes:
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
@@ -197,4 +197,4 @@ volumes:
driver: local
ldap_data:
ldap_config:
grafana_storage:
grafana_storage:

View File

@@ -73,9 +73,9 @@ export const useDeletePamResource = () => {
export const useCreatePamAccount = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceId, resourceType, ...params }: TCreatePamAccountDTO) => {
mutationFn: async ({ resourceType, ...params }: TCreatePamAccountDTO) => {
const { data } = await apiRequest.post<{ account: TPamAccount }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts`,
`/api/v1/pam/accounts/${resourceType}`,
params
);
@@ -90,14 +90,9 @@ export const useCreatePamAccount = () => {
export const useUpdatePamAccount = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
resourceId,
resourceType,
accountId,
...params
}: TUpdatePamAccountDTO) => {
mutationFn: async ({ resourceType, accountId, ...params }: TUpdatePamAccountDTO) => {
const { data } = await apiRequest.patch<{ account: TPamAccount }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`,
`/api/v1/pam/accounts/${resourceType}/${accountId}`,
params
);
@@ -112,9 +107,9 @@ export const useUpdatePamAccount = () => {
export const useDeletePamAccount = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ resourceId, resourceType, accountId }: TDeletePamAccountDTO) => {
mutationFn: async ({ resourceType, accountId }: TDeletePamAccountDTO) => {
const { data } = await apiRequest.delete<{ account: TPamAccount }>(
`/api/v1/pam/resources/${resourceType}/${resourceId}/accounts/${accountId}`
`/api/v1/pam/accounts/${resourceType}/${accountId}`
);
return data.account;

View File

@@ -72,13 +72,11 @@ export type TUpdatePamAccountDTO = Partial<
Pick<TPamAccount, "name" | "description" | "credentials">
> & {
accountId: string;
resourceId: string;
resourceType: PamResourceType;
};
export type TDeletePamAccountDTO = {
accountId: string;
resourceId: string;
resourceType: PamResourceType;
};

View File

@@ -100,7 +100,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
secretEvents.includes(eventType)
);
const showSecretsSection =
project?.type !== ProjectType.PAM &&
selectedProject?.type !== ProjectType.PAM &&
(hasSecretEventFilter || currentSelectedEventTypes.length === 0);
const filteredEventTypes = useMemo(() => {

View File

@@ -16,13 +16,10 @@ type Props = {
export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => {
const [duration, setDuration] = useState("4h");
const isDurationValid = useMemo(() => ms(duration || "1s") > 0, [duration]);
const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]);
const command = useMemo(
() =>
account
? `infisical pam access ${account.id}${duration ? ` --duration ${duration}` : ""}`
: "",
() => (account ? `infisical pam access ${account.id} --duration ${duration}` : ""),
[account, duration]
);

View File

@@ -79,7 +79,6 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
try {
const updatedAccount = await updatePamAccount.mutateAsync({
accountId: account.id,
resourceId: account.resourceId,
resourceType: account.resource.resourceType,
...formData
});

View File

@@ -1,6 +1,10 @@
import { z } from "zod";
export const BaseSqlAccountSchema = z.object({
username: z.string().trim().min(1, "Username required").max(255, "Username must be 255 characters or less"),
username: z
.string()
.trim()
.min(1, "Username required")
.max(255, "Username must be 255 characters or less"),
password: z.string().trim().min(1, "Password required")
});

View File

@@ -16,7 +16,6 @@ export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props)
const {
id: accountId,
name,
resourceId,
resource: { resourceType }
} = account;
@@ -24,7 +23,6 @@ export const PamDeleteAccountModal = ({ isOpen, onOpenChange, account }: Props)
try {
await deletePamAccount.mutateAsync({
accountId,
resourceId,
resourceType
});

View File

@@ -17,7 +17,7 @@ export const PamResourceHeader = ({ resourceType, onBack }: Props) => {
/>
<div>
<div className="flex items-center text-mineshaft-300">{details.name}</div>
<p className="text-sm leading-4 text-mineshaft-400">External resource</p>
<p className="text-sm leading-4 text-mineshaft-400">Resource</p>
</div>
{onBack && (
<button

View File

@@ -15,7 +15,7 @@ export const Route = createFileRoute(
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-management/$projectId/access-management",
to: "/projects/pam/$projectId/access-management",
params: {
projectId: params.projectId
},

View File

@@ -15,7 +15,7 @@ export const Route = createFileRoute(
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-management/$projectId/access-management",
to: "/projects/pam/$projectId/access-management",
params: {
projectId: params.projectId
},

View File

@@ -15,7 +15,7 @@ export const Route = createFileRoute(
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-scanning/$projectId/access-management",
to: "/projects/pam/$projectId/access-management",
params: {
projectId: params.projectId
},

View File

@@ -15,7 +15,7 @@ export const Route = createFileRoute(
{
label: "Access Control",
link: linkOptions({
to: "/projects/secret-management/$projectId/access-management",
to: "/projects/pam/$projectId/access-management",
params: {
projectId: params.projectId
},