mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
approval requests
This commit is contained in:
@@ -108,6 +108,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.string("status").notNullable().index();
|
||||
|
||||
t.integer("requiredApprovals").notNullable();
|
||||
t.boolean("notifyApprovers").defaultTo(false);
|
||||
|
||||
t.timestamp("startedAt").nullable();
|
||||
t.timestamp("completedAt").nullable();
|
||||
|
||||
@@ -14,6 +14,7 @@ export const ApprovalRequestStepsSchema = z.object({
|
||||
name: z.string().nullable().optional(),
|
||||
status: z.string(),
|
||||
requiredApprovals: z.number(),
|
||||
notifyApprovers: z.boolean().default(false).nullable().optional(),
|
||||
startedAt: z.date().nullable().optional(),
|
||||
completedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
@@ -163,7 +163,11 @@ import {
|
||||
approvalPolicyDALFactory,
|
||||
approvalPolicyStepApproversDALFactory,
|
||||
approvalPolicyStepsDALFactory,
|
||||
approvalRequestGrantsDALFactory
|
||||
approvalRequestApprovalsDALFactory,
|
||||
approvalRequestDALFactory,
|
||||
approvalRequestGrantsDALFactory,
|
||||
approvalRequestStepEligibleApproversDALFactory,
|
||||
approvalRequestStepsDALFactory
|
||||
} from "@app/services/approval-policy/approval-policy-dal";
|
||||
import { approvalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service";
|
||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||
@@ -2465,13 +2469,23 @@ export const registerRoutes = async (
|
||||
|
||||
const approvalPolicyStepsDAL = approvalPolicyStepsDALFactory(db);
|
||||
const approvalPolicyStepApproversDAL = approvalPolicyStepApproversDALFactory(db);
|
||||
const approvalRequestDAL = approvalRequestDALFactory(db);
|
||||
const approvalRequestStepsDAL = approvalRequestStepsDALFactory(db);
|
||||
const approvalRequestStepEligibleApproversDAL = approvalRequestStepEligibleApproversDALFactory(db);
|
||||
const approvalRequestApprovalsDAL = approvalRequestApprovalsDALFactory(db);
|
||||
|
||||
const approvalPolicyService = approvalPolicyServiceFactory({
|
||||
approvalPolicyDAL,
|
||||
approvalPolicyStepsDAL,
|
||||
approvalPolicyStepApproversDAL,
|
||||
permissionService,
|
||||
projectMembershipDAL
|
||||
projectMembershipDAL,
|
||||
approvalRequestDAL,
|
||||
approvalRequestStepsDAL,
|
||||
approvalRequestStepEligibleApproversDAL,
|
||||
approvalRequestApprovalsDAL,
|
||||
userGroupMembershipDAL,
|
||||
notificationService
|
||||
});
|
||||
|
||||
// setup the communication with license key server
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||
import {
|
||||
TApprovalPolicy,
|
||||
TCreatePolicyDTO,
|
||||
TCreateRequestDTO,
|
||||
TUpdatePolicyDTO
|
||||
} from "@app/services/approval-policy/approval-policy-types";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -15,7 +17,9 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
policyType,
|
||||
createPolicySchema,
|
||||
updatePolicySchema,
|
||||
policyResponseSchema
|
||||
policyResponseSchema,
|
||||
createRequestSchema,
|
||||
requestResponseSchema
|
||||
}: {
|
||||
server: FastifyZodProvider;
|
||||
policyType: ApprovalPolicyType;
|
||||
@@ -32,7 +36,10 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
}
|
||||
>;
|
||||
policyResponseSchema: z.ZodTypeAny;
|
||||
createRequestSchema: z.ZodType<TCreateRequestDTO>;
|
||||
requestResponseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
// Policies
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
@@ -52,7 +59,7 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
handler: async (req) => {
|
||||
const { policy } = await server.services.approvalPolicy.create(policyType, req.body, req.permission);
|
||||
|
||||
// TODO: Audit log
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { policy };
|
||||
}
|
||||
@@ -79,7 +86,7 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
handler: async (req) => {
|
||||
const { policies } = await server.services.approvalPolicy.list(policyType, req.query.projectId, req.permission);
|
||||
|
||||
// TODO: Audit log
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { policies };
|
||||
}
|
||||
@@ -106,7 +113,7 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
handler: async (req) => {
|
||||
const { policy } = await server.services.approvalPolicy.getById(req.params.policyId, req.permission);
|
||||
|
||||
// TODO: Audit log
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { policy };
|
||||
}
|
||||
@@ -134,7 +141,7 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
handler: async (req) => {
|
||||
const { policy } = await server.services.approvalPolicy.updateById(req.params.policyId, req.body, req.permission);
|
||||
|
||||
// TODO: Audit log
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { policy };
|
||||
}
|
||||
@@ -161,9 +168,174 @@ export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
handler: async (req) => {
|
||||
const { policyId } = await server.services.approvalPolicy.deleteById(req.params.policyId, req.permission);
|
||||
|
||||
// TODO: Audit log
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { policyId };
|
||||
}
|
||||
});
|
||||
|
||||
// Requests
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/requests",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List approval requests",
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
requests: z.array(requestResponseSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { requests } = await server.services.approvalPolicy.listRequests(
|
||||
policyType,
|
||||
req.query.projectId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { requests };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/requests",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create approval request",
|
||||
body: createRequestSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
// To prevent type errors when accessing req.auth.user
|
||||
if (req.auth.authMode !== AuthMode.JWT) {
|
||||
throw new BadRequestError({ message: "You can only request access using JWT auth tokens." });
|
||||
}
|
||||
|
||||
const { request } = await server.services.approvalPolicy.createRequest(
|
||||
policyType,
|
||||
{
|
||||
requesterName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
|
||||
requesterEmail: req.auth.user.email ?? "",
|
||||
...req.body
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/requests/:requestId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get approval request",
|
||||
params: z.object({
|
||||
requestId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.approvalPolicy.getRequestById(req.params.requestId, req.permission);
|
||||
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/requests/:requestId/approve",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Approve approval request",
|
||||
params: z.object({
|
||||
requestId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
comment: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.approvalPolicy.approveRequest(
|
||||
req.params.requestId,
|
||||
req.body,
|
||||
req.permission
|
||||
);
|
||||
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/requests/:requestId/reject",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Reject approval request",
|
||||
params: z.object({
|
||||
requestId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
comment: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.approvalPolicy.rejectRequest(
|
||||
req.params.requestId,
|
||||
req.body,
|
||||
req.permission
|
||||
);
|
||||
|
||||
// TODO(andrey): Audit log
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||
import {
|
||||
CreatePamAccessPolicySchema,
|
||||
CreatePamAccessRequestSchema,
|
||||
PamAccessPolicySchema,
|
||||
PamAccessRequestSchema,
|
||||
UpdatePamAccessPolicySchema
|
||||
} from "@app/services/approval-policy/pam-access/pam-access-policy-schemas";
|
||||
|
||||
@@ -17,7 +19,9 @@ export const APPROVAL_POLICY_REGISTER_ROUTER_MAP: Record<
|
||||
policyType: ApprovalPolicyType.PamAccess,
|
||||
createPolicySchema: CreatePamAccessPolicySchema,
|
||||
updatePolicySchema: UpdatePamAccessPolicySchema,
|
||||
policyResponseSchema: PamAccessPolicySchema
|
||||
policyResponseSchema: PamAccessPolicySchema,
|
||||
createRequestSchema: CreatePamAccessRequestSchema,
|
||||
requestResponseSchema: PamAccessRequestSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TApprovalRequestApprovals } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
@@ -149,9 +149,160 @@ export const approvalPolicyStepApproversDALFactory = (db: TDbClient) => {
|
||||
return orm;
|
||||
};
|
||||
|
||||
// Approval Policy Grants
|
||||
// Approval Request
|
||||
export type TApprovalRequestDALFactory = ReturnType<typeof approvalRequestDALFactory>;
|
||||
export const approvalRequestDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ApprovalRequests);
|
||||
|
||||
const findStepsByRequestId = async (requestId: string) => {
|
||||
try {
|
||||
const dbInstance = db.replicaNode();
|
||||
const steps = await dbInstance(TableName.ApprovalRequestSteps).where({ requestId }).orderBy("stepNumber", "asc");
|
||||
|
||||
if (!steps.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stepIds = steps.map((step) => step.id);
|
||||
|
||||
const [approvers, approvals] = await Promise.all([
|
||||
dbInstance(TableName.ApprovalRequestStepEligibleApprovers)
|
||||
.whereIn("stepId", stepIds)
|
||||
.select("stepId", "userId", "groupId"),
|
||||
dbInstance(TableName.ApprovalRequestApprovals).whereIn("stepId", stepIds)
|
||||
]);
|
||||
|
||||
const approversByStepId = approvers.reduce<Record<string, { type: ApproverType; id: string }[]>>(
|
||||
(acc, approver) => {
|
||||
const stepApprovers = acc[approver.stepId] || [];
|
||||
stepApprovers.push({
|
||||
type: approver.userId ? ApproverType.User : ApproverType.Group,
|
||||
id: (approver.userId || approver.groupId) as string
|
||||
});
|
||||
acc[approver.stepId] = stepApprovers;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const approvalsByStepId = approvals.reduce<Record<string, TApprovalRequestApprovals[]>>((acc, approval) => {
|
||||
const stepApprovals = acc[approval.stepId] || [];
|
||||
stepApprovals.push(approval);
|
||||
acc[approval.stepId] = stepApprovals;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return steps.map((step) => {
|
||||
return {
|
||||
...step,
|
||||
approvers: approversByStepId[step.id] || [],
|
||||
approvals: approvalsByStepId[step.id] || []
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find approval request steps" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (policyType: ApprovalPolicyType, projectId: string) => {
|
||||
try {
|
||||
const dbInstance = db.replicaNode();
|
||||
const requests = await dbInstance(TableName.ApprovalRequests).where({ type: policyType, projectId });
|
||||
|
||||
if (!requests.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const requestIds = requests.map((req) => req.id);
|
||||
|
||||
const steps = await dbInstance(TableName.ApprovalRequestSteps)
|
||||
.whereIn("requestId", requestIds)
|
||||
.orderBy("stepNumber", "asc");
|
||||
|
||||
const stepsByRequestId: Record<string, any[]> = {};
|
||||
|
||||
if (steps.length) {
|
||||
const stepIds = steps.map((step) => step.id);
|
||||
|
||||
const [approvers, approvals] = await Promise.all([
|
||||
dbInstance(TableName.ApprovalRequestStepEligibleApprovers)
|
||||
.whereIn("stepId", stepIds)
|
||||
.select("stepId", "userId", "groupId"),
|
||||
dbInstance(TableName.ApprovalRequestApprovals).whereIn("stepId", stepIds)
|
||||
]);
|
||||
|
||||
const approversByStepId = approvers.reduce<Record<string, { type: ApproverType; id: string }[]>>(
|
||||
(acc, approver) => {
|
||||
const stepApprovers = acc[approver.stepId] || [];
|
||||
stepApprovers.push({
|
||||
type: approver.userId ? ApproverType.User : ApproverType.Group,
|
||||
id: (approver.userId || approver.groupId) as string
|
||||
});
|
||||
acc[approver.stepId] = stepApprovers;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const approvalsByStepId = approvals.reduce<Record<string, TApprovalRequestApprovals[]>>((acc, approval) => {
|
||||
const stepApprovals = acc[approval.stepId] || [];
|
||||
stepApprovals.push(approval);
|
||||
acc[approval.stepId] = stepApprovals;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
steps.forEach((step) => {
|
||||
const formattedStep = {
|
||||
...step,
|
||||
approvers: approversByStepId[step.id] || [],
|
||||
approvals: approvalsByStepId[step.id] || []
|
||||
};
|
||||
|
||||
if (!stepsByRequestId[step.requestId]) {
|
||||
stepsByRequestId[step.requestId] = [];
|
||||
}
|
||||
stepsByRequestId[step.requestId].push(formattedStep);
|
||||
});
|
||||
}
|
||||
|
||||
return requests.map((req) => ({
|
||||
...req,
|
||||
steps: stepsByRequestId[req.id] || []
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find approval requests by project id" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, findStepsByRequestId, findByProjectId };
|
||||
};
|
||||
|
||||
// Approval Request Steps
|
||||
export type TApprovalRequestStepsDALFactory = ReturnType<typeof approvalRequestStepsDALFactory>;
|
||||
export const approvalRequestStepsDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ApprovalRequestSteps);
|
||||
return orm;
|
||||
};
|
||||
|
||||
// Approval Request Step Eligible Approvers
|
||||
export type TApprovalRequestStepEligibleApproversDALFactory = ReturnType<
|
||||
typeof approvalRequestStepEligibleApproversDALFactory
|
||||
>;
|
||||
export const approvalRequestStepEligibleApproversDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ApprovalRequestStepEligibleApprovers);
|
||||
return orm;
|
||||
};
|
||||
|
||||
// Approval Request Grants
|
||||
export type TApprovalRequestGrantsDALFactory = ReturnType<typeof approvalRequestGrantsDALFactory>;
|
||||
export const approvalRequestGrantsDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ApprovalRequestGrants);
|
||||
return orm;
|
||||
};
|
||||
|
||||
// Approval Request Approvals
|
||||
export type TApprovalRequestApprovalsDALFactory = ReturnType<typeof approvalRequestApprovalsDALFactory>;
|
||||
export const approvalRequestApprovalsDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ApprovalRequestApprovals);
|
||||
return orm;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,25 @@ export enum ApproverType {
|
||||
User = "user"
|
||||
}
|
||||
|
||||
export enum ApprovalRequestStatus {
|
||||
Pending = "pending",
|
||||
Approved = "approved",
|
||||
Rejected = "rejected",
|
||||
Expired = "expired",
|
||||
Cancelled = "cancelled"
|
||||
}
|
||||
|
||||
export enum ApprovalRequestStepStatus {
|
||||
Pending = "pending",
|
||||
InProgress = "in-progress",
|
||||
Completed = "completed"
|
||||
}
|
||||
|
||||
export enum ApprovalRequestApprovalDecision {
|
||||
Approved = "approved",
|
||||
Rejected = "rejected"
|
||||
}
|
||||
|
||||
export enum ApprovalRequestGrantStatus {
|
||||
Active = "active",
|
||||
Expired = "expired",
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { ApprovalPolicyType } from "./approval-policy-enums";
|
||||
import { TApprovalPolicy, TApprovalPolicyInputs, TApprovalResourceFactory } from "./approval-policy-types";
|
||||
import {
|
||||
TApprovalPolicy,
|
||||
TApprovalPolicyInputs,
|
||||
TApprovalRequestData,
|
||||
TApprovalResourceFactory
|
||||
} from "./approval-policy-types";
|
||||
import { pamAccessPolicyFactory } from "./pam-access/pam-access-policy-factory";
|
||||
|
||||
type TApprovalPolicyFactoryImplementation = TApprovalResourceFactory<TApprovalPolicyInputs, TApprovalPolicy>;
|
||||
type TApprovalPolicyFactoryImplementation = TApprovalResourceFactory<
|
||||
TApprovalPolicyInputs,
|
||||
TApprovalPolicy,
|
||||
TApprovalRequestData
|
||||
>;
|
||||
|
||||
export const APPROVAL_POLICY_FACTORY_MAP: Record<ApprovalPolicyType, TApprovalPolicyFactoryImplementation> = {
|
||||
[ApprovalPolicyType.PamAccess]: pamAccessPolicyFactory as TApprovalPolicyFactoryImplementation
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ApprovalPoliciesSchema } from "@app/db/schemas";
|
||||
import {
|
||||
ApprovalPoliciesSchema,
|
||||
ApprovalRequestApprovalsSchema,
|
||||
ApprovalRequestsSchema,
|
||||
ApprovalRequestStepsSchema
|
||||
} from "@app/db/schemas";
|
||||
|
||||
import { ApproverType } from "./approval-policy-enums";
|
||||
|
||||
const ApprovalPolicyStepSchema = z.object({
|
||||
name: z.string().min(1).max(128).nullable().optional(),
|
||||
requiredApprovals: z.number().min(1).max(100),
|
||||
notifyApprovers: z.boolean().optional(),
|
||||
notifyApprovers: z.boolean().nullable().optional(),
|
||||
approvers: z
|
||||
.object({
|
||||
type: z.nativeEnum(ApproverType),
|
||||
@@ -16,6 +21,7 @@ const ApprovalPolicyStepSchema = z.object({
|
||||
.array()
|
||||
});
|
||||
|
||||
// Policy
|
||||
export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({
|
||||
steps: ApprovalPolicyStepSchema.array()
|
||||
});
|
||||
@@ -32,3 +38,31 @@ export const BaseUpdateApprovalPolicySchema = z.object({
|
||||
maxRequestTtlSeconds: z.number().min(3600).max(2592000).nullable().optional(), // 1 hour to 30 days
|
||||
steps: ApprovalPolicyStepSchema.array().optional()
|
||||
});
|
||||
|
||||
// Request
|
||||
const ApprovalRequestStepSchema = ApprovalRequestStepsSchema.extend({
|
||||
name: z.string().min(1).max(128).nullable().optional(),
|
||||
requiredApprovals: z.number().min(1).max(100),
|
||||
notifyApprovers: z.boolean().nullable().optional(),
|
||||
stepNumber: z.number(),
|
||||
status: z.string(),
|
||||
startedAt: z.date().nullable().optional(),
|
||||
completedAt: z.date().nullable().optional(),
|
||||
approvers: z
|
||||
.object({
|
||||
type: z.nativeEnum(ApproverType),
|
||||
id: z.string().uuid()
|
||||
})
|
||||
.array(),
|
||||
approvals: ApprovalRequestApprovalsSchema.array()
|
||||
});
|
||||
|
||||
export const BaseApprovalRequestSchema = ApprovalRequestsSchema.extend({
|
||||
steps: ApprovalRequestStepSchema.array()
|
||||
});
|
||||
|
||||
export const BaseCreateApprovalRequestSchema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
justification: z.string().max(256).nullable().optional(),
|
||||
expiresAt: z.coerce.date().nullable().optional()
|
||||
});
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
import { ActionProjectType, ProjectMembershipRole, TApprovalPolicies } from "@app/db/schemas";
|
||||
import { ActionProjectType, ProjectMembershipRole, TApprovalPolicies, TApprovalRequests } from "@app/db/schemas";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import {
|
||||
TApprovalPolicyDALFactory,
|
||||
TApprovalPolicyStepApproversDALFactory,
|
||||
TApprovalPolicyStepsDALFactory
|
||||
TApprovalPolicyStepsDALFactory,
|
||||
TApprovalRequestApprovalsDALFactory,
|
||||
TApprovalRequestDALFactory,
|
||||
TApprovalRequestStepEligibleApproversDALFactory,
|
||||
TApprovalRequestStepsDALFactory
|
||||
} from "./approval-policy-dal";
|
||||
import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums";
|
||||
import { TCreatePolicyDTO, TUpdatePolicyDTO } from "./approval-policy-types";
|
||||
import {
|
||||
ApprovalPolicyType,
|
||||
ApprovalRequestApprovalDecision,
|
||||
ApprovalRequestStatus,
|
||||
ApprovalRequestStepStatus,
|
||||
ApproverType
|
||||
} from "./approval-policy-enums";
|
||||
import { APPROVAL_POLICY_FACTORY_MAP } from "./approval-policy-factory";
|
||||
import {
|
||||
ApprovalPolicyStep,
|
||||
TApprovalRequest,
|
||||
TCreatePolicyDTO,
|
||||
TCreateRequestDTO,
|
||||
TUpdatePolicyDTO
|
||||
} from "./approval-policy-types";
|
||||
|
||||
type TApprovalPolicyServiceFactoryDep = {
|
||||
approvalPolicyDAL: TApprovalPolicyDALFactory;
|
||||
approvalPolicyStepsDAL: TApprovalPolicyStepsDALFactory;
|
||||
approvalPolicyStepApproversDAL: TApprovalPolicyStepApproversDALFactory;
|
||||
approvalRequestApprovalsDAL: TApprovalRequestApprovalsDALFactory;
|
||||
approvalRequestDAL: TApprovalRequestDALFactory;
|
||||
approvalRequestStepsDAL: TApprovalRequestStepsDALFactory;
|
||||
approvalRequestStepEligibleApproversDAL: TApprovalRequestStepEligibleApproversDALFactory;
|
||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||
notificationService: TNotificationServiceFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserIds">;
|
||||
};
|
||||
@@ -25,9 +51,43 @@ export const approvalPolicyServiceFactory = ({
|
||||
approvalPolicyDAL,
|
||||
approvalPolicyStepsDAL,
|
||||
approvalPolicyStepApproversDAL,
|
||||
approvalRequestApprovalsDAL,
|
||||
approvalRequestDAL,
|
||||
approvalRequestStepsDAL,
|
||||
approvalRequestStepEligibleApproversDAL,
|
||||
userGroupMembershipDAL,
|
||||
notificationService,
|
||||
permissionService,
|
||||
projectMembershipDAL
|
||||
}: TApprovalPolicyServiceFactoryDep) => {
|
||||
const $notifyApproversForStep = async (step: ApprovalPolicyStep, request: TApprovalRequests) => {
|
||||
if (!step.notifyApprovers) return;
|
||||
|
||||
const userIdsToNotify = new Set<string>();
|
||||
|
||||
for (const approver of step.approvers) {
|
||||
if (approver.type === ApproverType.User) {
|
||||
userIdsToNotify.add(approver.id);
|
||||
} else if (approver.type === ApproverType.Group) {
|
||||
const members = await userGroupMembershipDAL.find({ groupId: approver.id });
|
||||
members.forEach((member) => userIdsToNotify.add(member.userId));
|
||||
}
|
||||
}
|
||||
|
||||
if (userIdsToNotify.size === 0) return;
|
||||
|
||||
// TODO: Potentially link to requests in the future?
|
||||
await notificationService.createUserNotifications(
|
||||
Array.from(userIdsToNotify).map((userId) => ({
|
||||
userId,
|
||||
orgId: request.organizationId,
|
||||
type: NotificationType.APPROVAL_REQUIRED,
|
||||
title: "Approval Required",
|
||||
body: `You have a new approval request for ${request.type} from ${request.requesterName}.`
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const $verifyProjectUserMembership = async (userIds: string[], orgId: string, projectId: string) => {
|
||||
const uniqueUserIds = [...new Set(userIds)];
|
||||
if (uniqueUserIds.length === 0) return;
|
||||
@@ -287,11 +347,339 @@ export const approvalPolicyServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const createRequest = async (
|
||||
policyType: ApprovalPolicyType,
|
||||
{
|
||||
projectId,
|
||||
requestData,
|
||||
expiresAt,
|
||||
justification,
|
||||
requesterName,
|
||||
requesterEmail
|
||||
}: TCreateRequestDTO & {
|
||||
requesterName: string;
|
||||
requesterEmail: string;
|
||||
},
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
// TODO(andrey): Perm check
|
||||
|
||||
const fac = APPROVAL_POLICY_FACTORY_MAP[policyType](policyType);
|
||||
|
||||
const policy = await fac.matchPolicy(approvalPolicyDAL, projectId, requestData);
|
||||
|
||||
if (!policy) {
|
||||
throw new ForbiddenRequestError({ message: "Policy not found" });
|
||||
}
|
||||
|
||||
if (!fac.validateConstraints(policy, requestData)) {
|
||||
throw new ForbiddenRequestError({ message: "Policy constraints not met" });
|
||||
}
|
||||
|
||||
if (expiresAt) {
|
||||
const now = new Date();
|
||||
const ttlSeconds = (new Date(expiresAt).getTime() - now.getTime()) / 1000;
|
||||
|
||||
if (ttlSeconds < 3600) {
|
||||
throw new BadRequestError({ message: "Expiration time must be at least 1 hour in the future" });
|
||||
}
|
||||
|
||||
if (policy.maxRequestTtlSeconds && ttlSeconds > policy.maxRequestTtlSeconds) {
|
||||
throw new BadRequestError({
|
||||
message: `Expiration time exceeds the maximum allowed TTL of ${policy.maxRequestTtlSeconds} seconds`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { request, steps } = await approvalRequestDAL.transaction(async (tx) => {
|
||||
const newRequest = await approvalRequestDAL.create(
|
||||
{
|
||||
projectId,
|
||||
organizationId: actor.orgId,
|
||||
policyId: policy.id,
|
||||
requesterId: actor.id,
|
||||
requesterName,
|
||||
requesterEmail,
|
||||
type: policyType,
|
||||
status: ApprovalRequestStatus.Pending,
|
||||
justification,
|
||||
currentStep: 1,
|
||||
requestData: { version: 1, requestData },
|
||||
expiresAt
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const newSteps = await Promise.all(
|
||||
policy.steps.map(async (step, i) => {
|
||||
const stepNum = i + 1;
|
||||
const newStep = await approvalRequestStepsDAL.create(
|
||||
{
|
||||
requestId: newRequest.id,
|
||||
stepNumber: stepNum,
|
||||
name: step.name,
|
||||
status: stepNum === 1 ? ApprovalRequestStepStatus.InProgress : ApprovalRequestStepStatus.Pending,
|
||||
requiredApprovals: step.requiredApprovals,
|
||||
notifyApprovers: step.notifyApprovers,
|
||||
startedAt: stepNum === 1 ? new Date() : null
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
step.approvers.map((approver) =>
|
||||
approvalRequestStepEligibleApproversDAL.create(
|
||||
{
|
||||
stepId: newStep.id,
|
||||
userId: approver.type === ApproverType.User ? approver.id : null,
|
||||
groupId: approver.type === ApproverType.Group ? approver.id : null
|
||||
},
|
||||
tx
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
...newStep,
|
||||
approvers: step.approvers,
|
||||
approvals: []
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return { request: newRequest, steps: newSteps };
|
||||
});
|
||||
|
||||
if (steps.length > 0) {
|
||||
await $notifyApproversForStep(steps[0], request);
|
||||
}
|
||||
|
||||
return {
|
||||
request: { ...request, steps }
|
||||
};
|
||||
};
|
||||
|
||||
const getRequestById = async (requestId: string, _actor: OrgServiceActor) => {
|
||||
// TODO(andrey): Perm check
|
||||
|
||||
const request = await approvalRequestDAL.findById(requestId);
|
||||
if (!request) {
|
||||
throw new ForbiddenRequestError({ message: "Request not found" });
|
||||
}
|
||||
|
||||
const steps = await approvalRequestDAL.findStepsByRequestId(requestId);
|
||||
|
||||
return {
|
||||
request: { ...request, steps }
|
||||
};
|
||||
};
|
||||
|
||||
const approveRequest = async (requestId: string, { comment }: { comment?: string }, actor: OrgServiceActor) => {
|
||||
const request = await approvalRequestDAL.findById(requestId);
|
||||
if (!request) {
|
||||
throw new ForbiddenRequestError({ message: "Request not found" });
|
||||
}
|
||||
|
||||
if (request.status !== ApprovalRequestStatus.Pending) {
|
||||
throw new BadRequestError({ message: "Request is not pending" });
|
||||
}
|
||||
|
||||
if (request.expiresAt && new Date(request.expiresAt) < new Date()) {
|
||||
await approvalRequestDAL.updateById(requestId, { status: ApprovalRequestStatus.Expired });
|
||||
throw new BadRequestError({ message: "Request has expired" });
|
||||
}
|
||||
|
||||
const steps = await approvalRequestDAL.findStepsByRequestId(requestId);
|
||||
const currentStepIndex = steps.findIndex((s) => s.stepNumber === request.currentStep);
|
||||
if (currentStepIndex === -1) {
|
||||
throw new BadRequestError({ message: "Current step not found" });
|
||||
}
|
||||
|
||||
const currentStep = steps[currentStepIndex];
|
||||
|
||||
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId);
|
||||
const userGroupIds = new Set(userGroups.map((g) => g.groupId));
|
||||
|
||||
const isEligible = currentStep.approvers.some(
|
||||
(approver) =>
|
||||
(approver.type === ApproverType.User && approver.id === actor.id) ||
|
||||
(approver.type === ApproverType.Group && userGroupIds.has(approver.id))
|
||||
);
|
||||
|
||||
if (!isEligible) {
|
||||
throw new ForbiddenRequestError({ message: "You are not an eligible approver for this step" });
|
||||
}
|
||||
|
||||
const hasApproved = currentStep.approvals.some((a) => a.approverUserId === actor.id);
|
||||
if (hasApproved) {
|
||||
throw new BadRequestError({ message: "You have already approved this request" });
|
||||
}
|
||||
|
||||
const { updatedRequest, nextStepToNotify } = await approvalRequestDAL.transaction(async (tx) => {
|
||||
let nextStepToNotify = null;
|
||||
|
||||
// Create approval
|
||||
await approvalRequestApprovalsDAL.create(
|
||||
{
|
||||
stepId: currentStep.id,
|
||||
approverUserId: actor.id,
|
||||
decision: ApprovalRequestApprovalDecision.Approved,
|
||||
comment
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const newApprovalCount = currentStep.approvals.length + 1;
|
||||
if (newApprovalCount >= currentStep.requiredApprovals) {
|
||||
// Step completed
|
||||
await approvalRequestStepsDAL.updateById(
|
||||
currentStep.id,
|
||||
{
|
||||
status: ApprovalRequestStepStatus.Completed,
|
||||
completedAt: new Date()
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const nextStep = steps[currentStepIndex + 1];
|
||||
if (nextStep) {
|
||||
// Move to next step
|
||||
await approvalRequestDAL.updateById(
|
||||
requestId,
|
||||
{
|
||||
currentStep: request.currentStep + 1
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await approvalRequestStepsDAL.updateById(
|
||||
nextStep.id,
|
||||
{
|
||||
status: ApprovalRequestStepStatus.InProgress,
|
||||
startedAt: new Date()
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (nextStep.notifyApprovers) {
|
||||
nextStepToNotify = nextStep;
|
||||
}
|
||||
} else {
|
||||
// All steps completed
|
||||
const completedReq = await approvalRequestDAL.updateById(
|
||||
requestId,
|
||||
{
|
||||
status: ApprovalRequestStatus.Approved
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return { updatedRequest: completedReq, nextStepToNotify: null };
|
||||
}
|
||||
}
|
||||
|
||||
return { updatedRequest: request, nextStepToNotify };
|
||||
});
|
||||
|
||||
if (nextStepToNotify) {
|
||||
await $notifyApproversForStep(nextStepToNotify, updatedRequest);
|
||||
}
|
||||
|
||||
// Fetch fresh state
|
||||
const finalSteps = await approvalRequestDAL.findStepsByRequestId(requestId);
|
||||
const finalRequest = await approvalRequestDAL.findById(requestId);
|
||||
|
||||
const newRequest = { ...finalRequest, steps: finalSteps };
|
||||
|
||||
if (updatedRequest.status === ApprovalRequestStatus.Approved) {
|
||||
const fac = APPROVAL_POLICY_FACTORY_MAP[updatedRequest.type as ApprovalPolicyType](
|
||||
updatedRequest.type as ApprovalPolicyType
|
||||
);
|
||||
await fac.postApprovalRoutine(newRequest as TApprovalRequest);
|
||||
}
|
||||
|
||||
return { request: newRequest };
|
||||
};
|
||||
|
||||
const rejectRequest = async (requestId: string, { comment }: { comment?: string }, actor: OrgServiceActor) => {
|
||||
const request = await approvalRequestDAL.findById(requestId);
|
||||
if (!request) {
|
||||
throw new ForbiddenRequestError({ message: "Request not found" });
|
||||
}
|
||||
|
||||
if (request.status !== ApprovalRequestStatus.Pending) {
|
||||
throw new BadRequestError({ message: "Request is not pending" });
|
||||
}
|
||||
|
||||
if (request.expiresAt && new Date(request.expiresAt) < new Date()) {
|
||||
await approvalRequestDAL.updateById(requestId, { status: ApprovalRequestStatus.Expired });
|
||||
throw new BadRequestError({ message: "Request has expired" });
|
||||
}
|
||||
|
||||
const steps = await approvalRequestDAL.findStepsByRequestId(requestId);
|
||||
const currentStep = steps.find((s) => s.stepNumber === request.currentStep);
|
||||
|
||||
if (!currentStep) {
|
||||
throw new BadRequestError({ message: "Current step not found" });
|
||||
}
|
||||
|
||||
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId);
|
||||
const userGroupIds = new Set(userGroups.map((g) => g.groupId));
|
||||
|
||||
const isEligible = currentStep.approvers.some(
|
||||
(approver) =>
|
||||
(approver.type === ApproverType.User && approver.id === actor.id) ||
|
||||
(approver.type === ApproverType.Group && userGroupIds.has(approver.id))
|
||||
);
|
||||
|
||||
if (!isEligible) {
|
||||
throw new ForbiddenRequestError({ message: "You are not an eligible approver for this step" });
|
||||
}
|
||||
|
||||
await approvalRequestDAL.transaction(async (tx) => {
|
||||
await approvalRequestApprovalsDAL.create(
|
||||
{
|
||||
stepId: currentStep.id,
|
||||
approverUserId: actor.id,
|
||||
decision: ApprovalRequestApprovalDecision.Rejected,
|
||||
comment
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await approvalRequestDAL.updateById(
|
||||
requestId,
|
||||
{
|
||||
status: ApprovalRequestStatus.Rejected
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
const finalSteps = await approvalRequestDAL.findStepsByRequestId(requestId);
|
||||
const finalRequest = await approvalRequestDAL.findById(requestId);
|
||||
|
||||
return { request: { ...finalRequest, steps: finalSteps } };
|
||||
};
|
||||
|
||||
const listRequests = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => {
|
||||
// TODO(andrey): Perm check
|
||||
|
||||
const requests = await approvalRequestDAL.findByProjectId(policyType, projectId);
|
||||
|
||||
return { requests };
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
list,
|
||||
getById,
|
||||
updateById,
|
||||
deleteById
|
||||
deleteById,
|
||||
createRequest,
|
||||
listRequests,
|
||||
getRequestById,
|
||||
approveRequest,
|
||||
rejectRequest
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
TPamAccessPolicy,
|
||||
TPamAccessPolicyConditions,
|
||||
TPamAccessPolicyConstraints,
|
||||
TPamAccessPolicyInputs
|
||||
TPamAccessPolicyInputs,
|
||||
TPamAccessRequest,
|
||||
TPamAccessRequestData
|
||||
} from "./pam-access/pam-access-policy-types";
|
||||
|
||||
export type TApprovalPolicy = TPamAccessPolicy;
|
||||
@@ -16,17 +18,20 @@ export type TApprovalPolicyInputs = TPamAccessPolicyInputs;
|
||||
export type TApprovalPolicyConditions = TPamAccessPolicyConditions;
|
||||
export type TApprovalPolicyConstraints = TPamAccessPolicyConstraints;
|
||||
|
||||
export type TApprovalRequest = TPamAccessRequest;
|
||||
export type TApprovalRequestData = TPamAccessRequestData;
|
||||
|
||||
export interface ApprovalPolicyStep {
|
||||
name?: string | null;
|
||||
requiredApprovals: number;
|
||||
notifyApprovers?: boolean;
|
||||
notifyApprovers?: boolean | null;
|
||||
approvers: {
|
||||
type: ApproverType;
|
||||
id: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// DTOs
|
||||
// Policy DTOs
|
||||
export interface TCreatePolicyDTO {
|
||||
projectId: TApprovalPolicy["projectId"];
|
||||
name: TApprovalPolicy["name"];
|
||||
@@ -44,6 +49,14 @@ export interface TUpdatePolicyDTO {
|
||||
steps?: ApprovalPolicyStep[];
|
||||
}
|
||||
|
||||
// Request DTOs
|
||||
export interface TCreateRequestDTO {
|
||||
projectId: TApprovalRequest["projectId"];
|
||||
requestData: TApprovalRequest["requestData"]["requestData"];
|
||||
justification?: TApprovalRequest["justification"];
|
||||
expiresAt?: TApprovalRequest["expiresAt"];
|
||||
}
|
||||
|
||||
// Factory
|
||||
export type TApprovalRequestFactoryMatchPolicy<I extends TApprovalPolicyInputs, P extends TApprovalPolicy> = (
|
||||
approvalPolicyDAL: TApprovalPolicyDALFactory,
|
||||
@@ -56,10 +69,19 @@ export type TApprovalRequestFactoryCanAccess<I extends TApprovalPolicyInputs> =
|
||||
userId: string,
|
||||
inputs: I
|
||||
) => Promise<boolean>;
|
||||
export type TApprovalRequestFactoryValidateConstraints<P extends TApprovalPolicy, R extends TApprovalRequestData> = (
|
||||
policy: P,
|
||||
inputs: R
|
||||
) => boolean;
|
||||
export type TApprovalRequestFactoryPostApprovalRoutine = (request: TApprovalRequest) => Promise<void>;
|
||||
|
||||
export type TApprovalResourceFactory<I extends TApprovalPolicyInputs, P extends TApprovalPolicy> = (
|
||||
policyType: ApprovalPolicyType
|
||||
) => {
|
||||
export type TApprovalResourceFactory<
|
||||
I extends TApprovalPolicyInputs,
|
||||
P extends TApprovalPolicy,
|
||||
R extends TApprovalRequestData
|
||||
> = (policyType: ApprovalPolicyType) => {
|
||||
matchPolicy: TApprovalRequestFactoryMatchPolicy<I, P>;
|
||||
canAccess: TApprovalRequestFactoryCanAccess<I>;
|
||||
validateConstraints: TApprovalRequestFactoryValidateConstraints<P, R>;
|
||||
postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine;
|
||||
};
|
||||
|
||||
@@ -4,22 +4,23 @@ import { ApprovalRequestGrantStatus } from "../approval-policy-enums";
|
||||
import {
|
||||
TApprovalRequestFactoryCanAccess,
|
||||
TApprovalRequestFactoryMatchPolicy,
|
||||
TApprovalRequestFactoryPostApprovalRoutine,
|
||||
TApprovalRequestFactoryValidateConstraints,
|
||||
TApprovalResourceFactory
|
||||
} from "../approval-policy-types";
|
||||
import { TPamAccessPolicy, TPamAccessPolicyInputs } from "./pam-access-policy-types";
|
||||
import { TPamAccessPolicy, TPamAccessPolicyInputs, TPamAccessRequestData } from "./pam-access-policy-types";
|
||||
|
||||
export const pamAccessPolicyFactory: TApprovalResourceFactory<TPamAccessPolicyInputs, TPamAccessPolicy> = (
|
||||
policyType
|
||||
) => {
|
||||
export const pamAccessPolicyFactory: TApprovalResourceFactory<
|
||||
TPamAccessPolicyInputs,
|
||||
TPamAccessPolicy,
|
||||
TPamAccessRequestData
|
||||
> = (policyType) => {
|
||||
const matchPolicy: TApprovalRequestFactoryMatchPolicy<TPamAccessPolicyInputs, TPamAccessPolicy> = async (
|
||||
approvalPolicyDAL,
|
||||
projectId,
|
||||
inputs
|
||||
) => {
|
||||
const policies = await approvalPolicyDAL.find({
|
||||
type: policyType,
|
||||
projectId
|
||||
});
|
||||
const policies = await approvalPolicyDAL.findByProjectId(policyType, projectId);
|
||||
|
||||
let bestMatch: { policy: TPamAccessPolicy; wildcardCount: number; pathLength: number } | null = null;
|
||||
|
||||
@@ -32,7 +33,7 @@ export const pamAccessPolicyFactory: TApprovalResourceFactory<TPamAccessPolicyIn
|
||||
}
|
||||
|
||||
// Find the most specific path pattern
|
||||
// TODO: Make matching logic more advanced by accounting for wildcard positions
|
||||
// TODO(andrey): Make matching logic more advanced by accounting for wildcard positions
|
||||
for (const pathPattern of c.accountPaths) {
|
||||
if (picomatch(pathPattern)(inputs.accountPath)) {
|
||||
const wildcardCount = (pathPattern.match(/\*/g) || []).length;
|
||||
@@ -67,7 +68,7 @@ export const pamAccessPolicyFactory: TApprovalResourceFactory<TPamAccessPolicyIn
|
||||
revokedAt: null
|
||||
});
|
||||
|
||||
// TODO: Move some of this check to be part of SQL query
|
||||
// TODO(andrey): Move some of this check to be part of SQL query
|
||||
return grants.some((grant) => {
|
||||
const grantAttributes = grant.attributes as TPamAccessPolicyInputs;
|
||||
const isMatch = picomatch(grantAttributes.accountPath);
|
||||
@@ -79,8 +80,24 @@ export const pamAccessPolicyFactory: TApprovalResourceFactory<TPamAccessPolicyIn
|
||||
});
|
||||
};
|
||||
|
||||
const validateConstraints: TApprovalRequestFactoryValidateConstraints<TPamAccessPolicy, TPamAccessRequestData> = (
|
||||
policy,
|
||||
inputs
|
||||
) => {
|
||||
const reqDuration = inputs.requestDurationSeconds;
|
||||
const durationConstraint = policy.constraints.constraints.requestDurationSeconds;
|
||||
|
||||
return reqDuration >= durationConstraint.min && reqDuration <= durationConstraint.max;
|
||||
};
|
||||
|
||||
const postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine = async (_request) => {
|
||||
// Placeholder
|
||||
};
|
||||
|
||||
return {
|
||||
matchPolicy,
|
||||
canAccess
|
||||
canAccess,
|
||||
validateConstraints,
|
||||
postApprovalRoutine
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,9 @@ import { z } from "zod";
|
||||
|
||||
import {
|
||||
BaseApprovalPolicySchema,
|
||||
BaseApprovalRequestSchema,
|
||||
BaseCreateApprovalPolicySchema,
|
||||
BaseCreateApprovalRequestSchema,
|
||||
BaseUpdateApprovalPolicySchema
|
||||
} from "../approval-policy-schemas";
|
||||
|
||||
@@ -16,19 +18,25 @@ export const PamAccessPolicyInputsSchema = z.object({
|
||||
export const PamAccessPolicyConditionsSchema = z
|
||||
.object({
|
||||
resourceIds: z.string().uuid().array(),
|
||||
accountPaths: z.string().array() // TODO: Add path & wildcard validation
|
||||
accountPaths: z.string().array() // TODO(andrey): Add path & wildcard validation
|
||||
})
|
||||
.array();
|
||||
|
||||
// Constraints
|
||||
export const PamAccessPolicyConstraintsSchema = z.object({
|
||||
requestDurationHours: z.object({
|
||||
// 168 hours = 7 days
|
||||
min: z.number().min(0).max(168),
|
||||
max: z.number().min(1).max(168)
|
||||
requestDurationSeconds: z.object({
|
||||
min: z.number().min(30).max(604800),
|
||||
max: z.number().min(30).max(604800) // 30 seconds to 7 days
|
||||
})
|
||||
});
|
||||
|
||||
// Request Data
|
||||
export const PamAccessPolicyRequestDataSchema = z.object({
|
||||
resourceId: z.string().uuid(),
|
||||
accountPath: z.string(),
|
||||
requestDurationSeconds: z.number().min(30).max(604800) // 30 seconds to 7 days
|
||||
});
|
||||
|
||||
// Policy
|
||||
export const PamAccessPolicySchema = BaseApprovalPolicySchema.extend({
|
||||
conditions: z.object({
|
||||
@@ -50,3 +58,15 @@ export const UpdatePamAccessPolicySchema = BaseUpdateApprovalPolicySchema.extend
|
||||
conditions: PamAccessPolicyConditionsSchema.optional(),
|
||||
constraints: PamAccessPolicyConstraintsSchema.optional()
|
||||
});
|
||||
|
||||
// Request
|
||||
export const PamAccessRequestSchema = BaseApprovalRequestSchema.extend({
|
||||
requestData: z.object({
|
||||
version: z.literal(1),
|
||||
requestData: PamAccessPolicyRequestDataSchema
|
||||
})
|
||||
});
|
||||
|
||||
export const CreatePamAccessRequestSchema = BaseCreateApprovalRequestSchema.extend({
|
||||
requestData: PamAccessPolicyRequestDataSchema
|
||||
});
|
||||
|
||||
@@ -4,10 +4,17 @@ import {
|
||||
PamAccessPolicyConditionsSchema,
|
||||
PamAccessPolicyConstraintsSchema,
|
||||
PamAccessPolicyInputsSchema,
|
||||
PamAccessPolicySchema
|
||||
PamAccessPolicyRequestDataSchema,
|
||||
PamAccessPolicySchema,
|
||||
PamAccessRequestSchema
|
||||
} from "./pam-access-policy-schemas";
|
||||
|
||||
// Policy
|
||||
export type TPamAccessPolicy = z.infer<typeof PamAccessPolicySchema>;
|
||||
export type TPamAccessPolicyInputs = z.infer<typeof PamAccessPolicyInputsSchema>;
|
||||
export type TPamAccessPolicyConditions = z.infer<typeof PamAccessPolicyConditionsSchema>;
|
||||
export type TPamAccessPolicyConstraints = z.infer<typeof PamAccessPolicyConstraintsSchema>;
|
||||
|
||||
// Request
|
||||
export type TPamAccessRequest = z.infer<typeof PamAccessRequestSchema>;
|
||||
export type TPamAccessRequestData = z.infer<typeof PamAccessPolicyRequestDataSchema>;
|
||||
|
||||
@@ -17,7 +17,8 @@ export enum NotificationType {
|
||||
PROJECT_INVITATION = "project-invitation",
|
||||
SECRET_SYNC_FAILED = "secret-sync-failed",
|
||||
GATEWAY_HEALTH_ALERT = "gateway-health-alert",
|
||||
RELAY_HEALTH_ALERT = "relay-health-alert"
|
||||
RELAY_HEALTH_ALERT = "relay-health-alert",
|
||||
APPROVAL_REQUIRED = "approval-required"
|
||||
}
|
||||
|
||||
export interface TCreateUserNotificationDTO {
|
||||
|
||||
Reference in New Issue
Block a user