Feat: Send emails for access requests

This commit is contained in:
Daniel Hougaard
2024-05-07 17:30:35 +02:00
parent fb2ab200b9
commit 6ae62675be
@@ -1,22 +1,25 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
import { verifyRequestedPermissions } from "./access-approval-request-fns";
import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal";
import { import {
ApprovalStatus, ApprovalStatus,
@@ -30,12 +33,15 @@ type TSecretApprovalRequestServiceFactoryDep = {
additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory; additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory;
additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
accessApprovalRequestDAL: TAccessApprovalRequestDALFactory; accessApprovalRequestDAL: TAccessApprovalRequestDALFactory;
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory; accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory;
projectMembershipDAL: TProjectMembershipDALFactory; projectMembershipDAL: TProjectMembershipDALFactory;
smtpService: TSmtpService;
userDAL: TUserDALFactory;
}; };
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>; export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
@@ -48,20 +54,22 @@ export const accessApprovalRequestServiceFactory = ({
accessApprovalRequestReviewerDAL, accessApprovalRequestReviewerDAL,
projectMembershipDAL, projectMembershipDAL,
accessApprovalPolicyDAL, accessApprovalPolicyDAL,
additionalPrivilegeDAL accessApprovalPolicyApproverDAL,
additionalPrivilegeDAL,
smtpService,
userDAL
}: TSecretApprovalRequestServiceFactoryDep) => { }: TSecretApprovalRequestServiceFactoryDep) => {
const createAccessApprovalRequest = async ({ const createAccessApprovalRequest = async ({
isTemporary, isTemporary,
temporaryRange, temporaryRange,
actorId, actorId,
envSlug, permissions: requestedPermissions,
permissions,
secretPath,
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
projectSlug projectSlug
}: TCreateAccessApprovalRequestDTO) => { }: TCreateAccessApprovalRequestDTO) => {
const cfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" }); if (!project) throw new UnauthorizedError({ message: "Project not found" });
@@ -73,15 +81,17 @@ export const accessApprovalRequestServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id);
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
await projectDAL.checkProjectUpgradeStatus(project.id); await projectDAL.checkProjectUpgradeStatus(project.id);
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new UnauthorizedError({ message: "Environment not found" }); if (!environment) throw new UnauthorizedError({ message: "Environment not found" });
if (!secretPath) throw new UnauthorizedError({ message: "Secret path is required" });
const policy = await accessApprovalPolicyDAL.findOne({ const policy = await accessApprovalPolicyDAL.findOne({
envId: environment.id, envId: environment.id,
@@ -89,33 +99,75 @@ export const accessApprovalRequestServiceFactory = ({
}); });
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
const approvers = await accessApprovalPolicyApproverDAL.find({
policyId: policy.id
});
const approverUsers = await userDAL.findUsersByProjectMembershipIds(
approvers.map((approver) => approver.approverId)
);
if (approverUsers.length !== approvers.length) {
throw new BadRequestError({ message: "Some approvers were not found" });
}
const duplicateRequest = await accessApprovalRequestDAL.findOne({ const duplicateRequest = await accessApprovalRequestDAL.findOne({
policyId: policy.id, policyId: policy.id,
requestedBy: membership.id, requestedBy: membership.id,
permissions: JSON.stringify(permissions), permissions: JSON.stringify(requestedPermissions),
temporaryRange: temporaryRange || null,
isTemporary isTemporary
}); });
if (duplicateRequest?.privilegeId) { if (duplicateRequest) {
const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); if (duplicateRequest.privilegeId) {
const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId);
const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string));
if (!isExpired) { if (!isExpired || !privilege.isTemporary) {
throw new BadRequestError({ message: "You have already requested access with the same permissions" }); throw new BadRequestError({ message: "You already have an active privilege with the same criteria" });
}
} else {
throw new BadRequestError({ message: "You already have a pending access request with the same criteria" });
} }
} }
const approvalRequest = await accessApprovalRequestDAL.create({ const approval = await accessApprovalRequestDAL.transaction(async (tx) => {
policyId: policy.id, const approvalRequest = await accessApprovalRequestDAL.create(
requestedBy: membership.id, {
temporaryRange: temporaryRange || null, policyId: policy.id,
permissions: JSON.stringify(permissions), requestedBy: membership.id,
isTemporary temporaryRange: temporaryRange || null,
permissions: JSON.stringify(requestedPermissions),
isTemporary
},
tx
);
await smtpService.sendMail({
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
subjectLine: "Access Approval Request",
substitutions: {
projectName: project.name,
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
requesterEmail: requestedByUser.email,
isTemporary,
...(isTemporary && {
expiresIn: ms(ms(temporaryRange || ""), { long: true })
}),
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
},
template: SmtpTemplates.AccessApprovalRequest
});
return approvalRequest;
}); });
return { request: approvalRequest }; return { request: approval };
}; };
const listApprovalRequests = async ({ const listApprovalRequests = async ({
@@ -130,15 +182,14 @@ export const accessApprovalRequestServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new UnauthorizedError({ message: "Project not found" }); if (!project) throw new UnauthorizedError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission( const { membership } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
project.id, project.id,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
@@ -181,7 +232,7 @@ export const accessApprovalRequestServiceFactory = ({
accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user
!policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver !policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver
) { ) {
throw new UnauthorizedError({ message: "No access" }); throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
} }
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);