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

View File

@@ -19,7 +19,7 @@ export async function up(knex: Knex): Promise<void> {
t.boolean("isActive").defaultTo(true);
t.integer("maxRequestTtlSeconds").nullable();
t.string("maxRequestTtl").nullable(); // 1hour, 30seconds, etc
t.jsonb("conditions").notNullable();
t.jsonb("constraints").notNullable();
@@ -71,11 +71,11 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("organizationId").notNullable().index();
t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.uuid("policyId").notNullable().index();
t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("CASCADE");
t.uuid("policyId").nullable().index();
t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("SET NULL");
t.uuid("requesterId").notNullable().index();
t.foreign("requesterId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("requesterId").nullable().index();
t.foreign("requesterId").references("id").inTable(TableName.Users).onDelete("SET NULL");
// To be used in the event of requester deletion
t.string("requesterName").notNullable();
@@ -156,11 +156,11 @@ export async function up(knex: Knex): Promise<void> {
t.string("projectId").notNullable().index();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("requestId").notNullable().index();
t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("CASCADE");
t.uuid("requestId").nullable().index();
t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("SET NULL");
t.uuid("granteeUserId").notNullable().index();
t.foreign("granteeUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("granteeUserId").nullable().index();
t.foreign("granteeUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
t.uuid("revokedByUserId").nullable().index();
t.foreign("revokedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");

View File

@@ -14,7 +14,7 @@ export const ApprovalPoliciesSchema = z.object({
type: z.string(),
name: z.string(),
isActive: z.boolean().default(true).nullable().optional(),
maxRequestTtlSeconds: z.number().nullable().optional(),
maxRequestTtl: z.string().nullable().optional(),
conditions: z.unknown(),
constraints: z.unknown(),
createdAt: z.date(),

View File

@@ -10,8 +10,8 @@ import { TImmutableDBKeys } from "./models";
export const ApprovalRequestGrantsSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
requestId: z.string().uuid(),
granteeUserId: z.string().uuid(),
requestId: z.string().uuid().nullable().optional(),
granteeUserId: z.string().uuid().nullable().optional(),
revokedByUserId: z.string().uuid().nullable().optional(),
revocationReason: z.string().nullable().optional(),
status: z.string(),

View File

@@ -11,8 +11,8 @@ export const ApprovalRequestsSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
organizationId: z.string().uuid(),
policyId: z.string().uuid(),
requesterId: z.string().uuid(),
policyId: z.string().uuid().nullable().optional(),
requesterId: z.string().uuid().nullable().optional(),
requesterName: z.string(),
requesterEmail: z.string(),
type: z.string(),

View File

@@ -8,6 +8,7 @@ import {
} from "@app/db/schemas";
import { ApproverType } from "./approval-policy-enums";
import { ms } from "@app/lib/ms";
const ApprovalPolicyStepSchema = z.object({
name: z.string().min(1).max(128).nullable().optional(),
@@ -21,6 +22,16 @@ const ApprovalPolicyStepSchema = z.object({
.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
export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({
steps: ApprovalPolicyStepSchema.array()
@@ -29,13 +40,13 @@ export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({
export const BaseCreateApprovalPolicySchema = z.object({
projectId: z.string().uuid(),
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()
});
export const BaseUpdateApprovalPolicySchema = z.object({
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()
});
@@ -64,5 +75,17 @@ export const BaseApprovalRequestSchema = ApprovalRequestsSchema.extend({
export const BaseCreateApprovalRequestSchema = z.object({
projectId: z.string().uuid(),
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()
});

View File

@@ -31,6 +31,7 @@ import {
TCreateRequestDTO,
TUpdatePolicyDTO
} from "./approval-policy-types";
import { ms } from "@app/lib/ms";
type TApprovalPolicyServiceFactoryDep = {
approvalPolicyDAL: TApprovalPolicyDALFactory;
@@ -106,7 +107,7 @@ export const approvalPolicyServiceFactory = ({
const create = async (
policyType: ApprovalPolicyType,
{ projectId, name, maxRequestTtlSeconds, conditions, constraints, steps }: TCreatePolicyDTO,
{ projectId, name, maxRequestTtl, conditions, constraints, steps }: TCreatePolicyDTO,
actor: OrgServiceActor
) => {
const { hasRole } = await permissionService.getProjectPermission({
@@ -135,7 +136,7 @@ export const approvalPolicyServiceFactory = ({
projectId,
organizationId: actor.orgId,
name,
maxRequestTtlSeconds,
maxRequestTtl,
conditions: { version: 1, conditions },
constraints: { version: 1, constraints },
type: policyType
@@ -227,7 +228,7 @@ export const approvalPolicyServiceFactory = ({
const updateById = async (
policyId: string,
{ name, maxRequestTtlSeconds, conditions, constraints, steps }: TUpdatePolicyDTO,
{ name, maxRequestTtl, conditions, constraints, steps }: TUpdatePolicyDTO,
actor: OrgServiceActor
) => {
const policy = await approvalPolicyDAL.findById(policyId);
@@ -264,8 +265,8 @@ export const approvalPolicyServiceFactory = ({
updateDoc.name = name;
}
if (maxRequestTtlSeconds !== undefined) {
updateDoc.maxRequestTtlSeconds = maxRequestTtlSeconds;
if (maxRequestTtl !== undefined) {
updateDoc.maxRequestTtl = maxRequestTtl;
}
if (conditions !== undefined) {
@@ -352,7 +353,7 @@ export const approvalPolicyServiceFactory = ({
{
projectId,
requestData,
expiresAt,
requestDuration,
justification,
requesterName,
requesterEmail
@@ -376,18 +377,20 @@ export const approvalPolicyServiceFactory = ({
throw new ForbiddenRequestError({ message: "Policy constraints not met" });
}
if (expiresAt) {
const now = new Date();
const ttlSeconds = (new Date(expiresAt).getTime() - now.getTime()) / 1000;
let expiresAt: Date | undefined;
if (ttlSeconds < 3600) {
throw new BadRequestError({ message: "Expiration time must be at least 1 hour in the future" });
}
if (requestDuration) {
const ttlMs = ms(requestDuration);
if (policy.maxRequestTtlSeconds && ttlSeconds > policy.maxRequestTtlSeconds) {
throw new BadRequestError({
message: `Expiration time exceeds the maximum allowed TTL of ${policy.maxRequestTtlSeconds} seconds`
});
expiresAt = new Date(Date.now() + ttlMs);
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}`
});
}
}
}

View File

@@ -35,7 +35,7 @@ export interface ApprovalPolicyStep {
export interface TCreatePolicyDTO {
projectId: TApprovalPolicy["projectId"];
name: TApprovalPolicy["name"];
maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"];
maxRequestTtl?: TApprovalPolicy["maxRequestTtl"];
conditions: TApprovalPolicy["conditions"]["conditions"];
constraints: TApprovalPolicy["constraints"]["constraints"];
steps: ApprovalPolicyStep[];
@@ -43,7 +43,7 @@ export interface TCreatePolicyDTO {
export interface TUpdatePolicyDTO {
name?: TApprovalPolicy["name"];
maxRequestTtlSeconds?: TApprovalPolicy["maxRequestTtlSeconds"];
maxRequestTtl?: TApprovalPolicy["maxRequestTtl"];
conditions?: TApprovalPolicy["conditions"]["conditions"];
constraints?: TApprovalPolicy["constraints"]["constraints"];
steps?: ApprovalPolicyStep[];
@@ -54,7 +54,7 @@ export interface TCreateRequestDTO {
projectId: TApprovalRequest["projectId"];
requestData: TApprovalRequest["requestData"]["requestData"];
justification?: TApprovalRequest["justification"];
expiresAt?: TApprovalRequest["expiresAt"];
requestDuration?: string;
}
// Factory

View File

@@ -9,6 +9,7 @@ import {
TApprovalResourceFactory
} from "../approval-policy-types";
import { TPamAccessPolicy, TPamAccessPolicyInputs, TPamAccessRequestData } from "./pam-access-policy-types";
import { ms } from "@app/lib/ms";
export const pamAccessPolicyFactory: TApprovalResourceFactory<
TPamAccessPolicyInputs,
@@ -84,10 +85,10 @@ export const pamAccessPolicyFactory: TApprovalResourceFactory<
policy,
inputs
) => {
const reqDuration = inputs.requestDurationSeconds;
const durationConstraint = policy.constraints.constraints.requestDurationSeconds;
const reqDuration = ms(inputs.accessDuration);
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) => {

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { ms } from "@app/lib/ms";
import {
BaseApprovalPolicySchema,
@@ -22,11 +23,21 @@ export const PamAccessPolicyConditionsSchema = z
})
.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
export const PamAccessPolicyConstraintsSchema = z.object({
requestDurationSeconds: z.object({
min: z.number().min(30).max(604800),
max: z.number().min(30).max(604800) // 30 seconds to 7 days
accessDuration: z.object({
min: DurationSchema,
max: DurationSchema
})
});
@@ -34,7 +45,7 @@ export const PamAccessPolicyConstraintsSchema = z.object({
export const PamAccessPolicyRequestDataSchema = z.object({
resourceId: z.string().uuid(),
accountPath: z.string(),
requestDurationSeconds: z.number().min(30).max(604800) // 30 seconds to 7 days
accessDuration: DurationSchema
});
// Policy