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