swap durations to string format & a few db migration changes

This commit is contained in:
x032205
2025-12-06 16:31:34 -05:00
parent 6a292838ff
commit 3694658203
9 changed files with 81 additions and 43 deletions
@@ -19,7 +19,7 @@ export async function up(knex: Knex): Promise<void> {
t.boolean("isActive").defaultTo(true); t.boolean("isActive").defaultTo(true);
t.integer("maxRequestTtlSeconds").nullable(); t.string("maxRequestTtl").nullable(); // 1hour, 30seconds, etc
t.jsonb("conditions").notNullable(); t.jsonb("conditions").notNullable();
t.jsonb("constraints").notNullable(); t.jsonb("constraints").notNullable();
@@ -71,11 +71,11 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("organizationId").notNullable().index(); t.uuid("organizationId").notNullable().index();
t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.uuid("policyId").notNullable().index(); t.uuid("policyId").nullable().index();
t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("CASCADE"); t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("SET NULL");
t.uuid("requesterId").notNullable().index(); t.uuid("requesterId").nullable().index();
t.foreign("requesterId").references("id").inTable(TableName.Users).onDelete("CASCADE"); t.foreign("requesterId").references("id").inTable(TableName.Users).onDelete("SET NULL");
// To be used in the event of requester deletion // To be used in the event of requester deletion
t.string("requesterName").notNullable(); t.string("requesterName").notNullable();
@@ -156,11 +156,11 @@ export async function up(knex: Knex): Promise<void> {
t.string("projectId").notNullable().index(); t.string("projectId").notNullable().index();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("requestId").notNullable().index(); t.uuid("requestId").nullable().index();
t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("CASCADE"); t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("SET NULL");
t.uuid("granteeUserId").notNullable().index(); t.uuid("granteeUserId").nullable().index();
t.foreign("granteeUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); t.foreign("granteeUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
t.uuid("revokedByUserId").nullable().index(); t.uuid("revokedByUserId").nullable().index();
t.foreign("revokedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); t.foreign("revokedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
+1 -1
View File
@@ -14,7 +14,7 @@ export const ApprovalPoliciesSchema = z.object({
type: z.string(), type: z.string(),
name: z.string(), name: z.string(),
isActive: z.boolean().default(true).nullable().optional(), isActive: z.boolean().default(true).nullable().optional(),
maxRequestTtlSeconds: z.number().nullable().optional(), maxRequestTtl: z.string().nullable().optional(),
conditions: z.unknown(), conditions: z.unknown(),
constraints: z.unknown(), constraints: z.unknown(),
createdAt: z.date(), createdAt: z.date(),
@@ -10,8 +10,8 @@ import { TImmutableDBKeys } from "./models";
export const ApprovalRequestGrantsSchema = z.object({ export const ApprovalRequestGrantsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
projectId: z.string(), projectId: z.string(),
requestId: z.string().uuid(), requestId: z.string().uuid().nullable().optional(),
granteeUserId: z.string().uuid(), granteeUserId: z.string().uuid().nullable().optional(),
revokedByUserId: z.string().uuid().nullable().optional(), revokedByUserId: z.string().uuid().nullable().optional(),
revocationReason: z.string().nullable().optional(), revocationReason: z.string().nullable().optional(),
status: z.string(), status: z.string(),
+2 -2
View File
@@ -11,8 +11,8 @@ export const ApprovalRequestsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
projectId: z.string(), projectId: z.string(),
organizationId: z.string().uuid(), organizationId: z.string().uuid(),
policyId: z.string().uuid(), policyId: z.string().uuid().nullable().optional(),
requesterId: z.string().uuid(), requesterId: z.string().uuid().nullable().optional(),
requesterName: z.string(), requesterName: z.string(),
requesterEmail: z.string(), requesterEmail: z.string(),
type: z.string(), type: z.string(),
@@ -8,6 +8,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ApproverType } from "./approval-policy-enums"; import { ApproverType } from "./approval-policy-enums";
import { ms } from "@app/lib/ms";
const ApprovalPolicyStepSchema = z.object({ const ApprovalPolicyStepSchema = z.object({
name: z.string().min(1).max(128).nullable().optional(), name: z.string().min(1).max(128).nullable().optional(),
@@ -21,6 +22,16 @@ const ApprovalPolicyStepSchema = z.object({
.array() .array()
}); });
const MaxRequestTtlSchema = z.string().refine(
(val) => {
const duration = ms(val) / 1000;
// 1 hour to 30 days
return duration >= 3600 && duration <= 2592000;
},
{ message: "Duration must be between 1 hour and 30 days" }
);
// Policy // Policy
export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({ export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({
steps: ApprovalPolicyStepSchema.array() steps: ApprovalPolicyStepSchema.array()
@@ -29,13 +40,13 @@ export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({
export const BaseCreateApprovalPolicySchema = z.object({ export const BaseCreateApprovalPolicySchema = z.object({
projectId: z.string().uuid(), projectId: z.string().uuid(),
name: z.string().min(1).max(128), name: z.string().min(1).max(128),
maxRequestTtlSeconds: z.number().min(3600).max(2592000).nullable().optional(), // 1 hour to 30 days maxRequestTtl: MaxRequestTtlSchema.nullable().optional(),
steps: ApprovalPolicyStepSchema.array() steps: ApprovalPolicyStepSchema.array()
}); });
export const BaseUpdateApprovalPolicySchema = z.object({ export const BaseUpdateApprovalPolicySchema = z.object({
name: z.string().min(1).max(128).optional(), name: z.string().min(1).max(128).optional(),
maxRequestTtlSeconds: z.number().min(3600).max(2592000).nullable().optional(), // 1 hour to 30 days maxRequestTtlSeconds: MaxRequestTtlSchema.nullable().optional(),
steps: ApprovalPolicyStepSchema.array().optional() steps: ApprovalPolicyStepSchema.array().optional()
}); });
@@ -64,5 +75,17 @@ export const BaseApprovalRequestSchema = ApprovalRequestsSchema.extend({
export const BaseCreateApprovalRequestSchema = z.object({ export const BaseCreateApprovalRequestSchema = z.object({
projectId: z.string().uuid(), projectId: z.string().uuid(),
justification: z.string().max(256).nullable().optional(), justification: z.string().max(256).nullable().optional(),
expiresAt: z.coerce.date().nullable().optional() requestDuration: z
.string()
.refine(
(val) => {
const duration = ms(val) / 1000;
// 1 minute to 30 days
return duration >= 60 && duration <= 2592000;
},
{ message: "Duration must be between 1 minute and 30 days" }
)
.nullable()
.optional()
}); });
@@ -31,6 +31,7 @@ import {
TCreateRequestDTO, TCreateRequestDTO,
TUpdatePolicyDTO TUpdatePolicyDTO
} from "./approval-policy-types"; } from "./approval-policy-types";
import { ms } from "@app/lib/ms";
type TApprovalPolicyServiceFactoryDep = { type TApprovalPolicyServiceFactoryDep = {
approvalPolicyDAL: TApprovalPolicyDALFactory; approvalPolicyDAL: TApprovalPolicyDALFactory;
@@ -106,7 +107,7 @@ export const approvalPolicyServiceFactory = ({
const create = async ( const create = async (
policyType: ApprovalPolicyType, policyType: ApprovalPolicyType,
{ projectId, name, maxRequestTtlSeconds, conditions, constraints, steps }: TCreatePolicyDTO, { projectId, name, maxRequestTtl, conditions, constraints, steps }: TCreatePolicyDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const { hasRole } = await permissionService.getProjectPermission({ const { hasRole } = await permissionService.getProjectPermission({
@@ -135,7 +136,7 @@ export const approvalPolicyServiceFactory = ({
projectId, projectId,
organizationId: actor.orgId, organizationId: actor.orgId,
name, name,
maxRequestTtlSeconds, maxRequestTtl,
conditions: { version: 1, conditions }, conditions: { version: 1, conditions },
constraints: { version: 1, constraints }, constraints: { version: 1, constraints },
type: policyType type: policyType
@@ -227,7 +228,7 @@ export const approvalPolicyServiceFactory = ({
const updateById = async ( const updateById = async (
policyId: string, policyId: string,
{ name, maxRequestTtlSeconds, conditions, constraints, steps }: TUpdatePolicyDTO, { name, maxRequestTtl, conditions, constraints, steps }: TUpdatePolicyDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const policy = await approvalPolicyDAL.findById(policyId); const policy = await approvalPolicyDAL.findById(policyId);
@@ -264,8 +265,8 @@ export const approvalPolicyServiceFactory = ({
updateDoc.name = name; updateDoc.name = name;
} }
if (maxRequestTtlSeconds !== undefined) { if (maxRequestTtl !== undefined) {
updateDoc.maxRequestTtlSeconds = maxRequestTtlSeconds; updateDoc.maxRequestTtl = maxRequestTtl;
} }
if (conditions !== undefined) { if (conditions !== undefined) {
@@ -352,7 +353,7 @@ export const approvalPolicyServiceFactory = ({
{ {
projectId, projectId,
requestData, requestData,
expiresAt, requestDuration,
justification, justification,
requesterName, requesterName,
requesterEmail requesterEmail
@@ -376,18 +377,20 @@ export const approvalPolicyServiceFactory = ({
throw new ForbiddenRequestError({ message: "Policy constraints not met" }); throw new ForbiddenRequestError({ message: "Policy constraints not met" });
} }
if (expiresAt) { let expiresAt: Date | undefined;
const now = new Date();
const ttlSeconds = (new Date(expiresAt).getTime() - now.getTime()) / 1000;
if (ttlSeconds < 3600) { if (requestDuration) {
throw new BadRequestError({ message: "Expiration time must be at least 1 hour in the future" }); const ttlMs = ms(requestDuration);
}
if (policy.maxRequestTtlSeconds && ttlSeconds > policy.maxRequestTtlSeconds) { expiresAt = new Date(Date.now() + ttlMs);
throw new BadRequestError({
message: `Expiration time exceeds the maximum allowed TTL of ${policy.maxRequestTtlSeconds} seconds` if (policy.maxRequestTtl) {
}); const maxTtlMs = ms(policy.maxRequestTtl);
if (ttlMs > maxTtlMs) {
throw new BadRequestError({
message: `Expiration time exceeds the maximum allowed TTL of ${policy.maxRequestTtl}`
});
}
} }
} }
@@ -35,7 +35,7 @@ export interface ApprovalPolicyStep {
export interface TCreatePolicyDTO { export interface TCreatePolicyDTO {
projectId: TApprovalPolicy["projectId"]; projectId: TApprovalPolicy["projectId"];
name: TApprovalPolicy["name"]; name: TApprovalPolicy["name"];
maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"]; maxRequestTtl?: TApprovalPolicy["maxRequestTtl"];
conditions: TApprovalPolicy["conditions"]["conditions"]; conditions: TApprovalPolicy["conditions"]["conditions"];
constraints: TApprovalPolicy["constraints"]["constraints"]; constraints: TApprovalPolicy["constraints"]["constraints"];
steps: ApprovalPolicyStep[]; steps: ApprovalPolicyStep[];
@@ -43,7 +43,7 @@ export interface TCreatePolicyDTO {
export interface TUpdatePolicyDTO { export interface TUpdatePolicyDTO {
name?: TApprovalPolicy["name"]; name?: TApprovalPolicy["name"];
maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"]; maxRequestTtl?: TApprovalPolicy["maxRequestTtl"];
conditions?: TApprovalPolicy["conditions"]["conditions"]; conditions?: TApprovalPolicy["conditions"]["conditions"];
constraints?: TApprovalPolicy["constraints"]["constraints"]; constraints?: TApprovalPolicy["constraints"]["constraints"];
steps?: ApprovalPolicyStep[]; steps?: ApprovalPolicyStep[];
@@ -54,7 +54,7 @@ export interface TCreateRequestDTO {
projectId: TApprovalRequest["projectId"]; projectId: TApprovalRequest["projectId"];
requestData: TApprovalRequest["requestData"]["requestData"]; requestData: TApprovalRequest["requestData"]["requestData"];
justification?: TApprovalRequest["justification"]; justification?: TApprovalRequest["justification"];
expiresAt?: TApprovalRequest["expiresAt"]; requestDuration?: string;
} }
// Factory // Factory
@@ -9,6 +9,7 @@ import {
TApprovalResourceFactory TApprovalResourceFactory
} from "../approval-policy-types"; } from "../approval-policy-types";
import { TPamAccessPolicy, TPamAccessPolicyInputs, TPamAccessRequestData } from "./pam-access-policy-types"; import { TPamAccessPolicy, TPamAccessPolicyInputs, TPamAccessRequestData } from "./pam-access-policy-types";
import { ms } from "@app/lib/ms";
export const pamAccessPolicyFactory: TApprovalResourceFactory< export const pamAccessPolicyFactory: TApprovalResourceFactory<
TPamAccessPolicyInputs, TPamAccessPolicyInputs,
@@ -84,10 +85,10 @@ export const pamAccessPolicyFactory: TApprovalResourceFactory<
policy, policy,
inputs inputs
) => { ) => {
const reqDuration = inputs.requestDurationSeconds; const reqDuration = ms(inputs.accessDuration);
const durationConstraint = policy.constraints.constraints.requestDurationSeconds; const durationConstraint = policy.constraints.constraints.accessDuration;
return reqDuration >= durationConstraint.min && reqDuration <= durationConstraint.max; return reqDuration >= ms(durationConstraint.min) && reqDuration <= ms(durationConstraint.max);
}; };
const postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine = async (_request) => { const postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine = async (_request) => {
@@ -1,4 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { ms } from "@app/lib/ms";
import { import {
BaseApprovalPolicySchema, BaseApprovalPolicySchema,
@@ -22,11 +23,21 @@ export const PamAccessPolicyConditionsSchema = z
}) })
.array(); .array();
const DurationSchema = z.string().refine(
(val) => {
const duration = ms(val) / 1000;
// 30 seconds to 7 days
return duration >= 30 && duration <= 604800;
},
{ message: "Duration must be between 30 seconds and 7 days" }
);
// Constraints // Constraints
export const PamAccessPolicyConstraintsSchema = z.object({ export const PamAccessPolicyConstraintsSchema = z.object({
requestDurationSeconds: z.object({ accessDuration: z.object({
min: z.number().min(30).max(604800), min: DurationSchema,
max: z.number().min(30).max(604800) // 30 seconds to 7 days max: DurationSchema
}) })
}); });
@@ -34,7 +45,7 @@ export const PamAccessPolicyConstraintsSchema = z.object({
export const PamAccessPolicyRequestDataSchema = z.object({ export const PamAccessPolicyRequestDataSchema = z.object({
resourceId: z.string().uuid(), resourceId: z.string().uuid(),
accountPath: z.string(), accountPath: z.string(),
requestDurationSeconds: z.number().min(30).max(604800) // 30 seconds to 7 days accessDuration: DurationSchema
}); });
// Policy // Policy