Merge branch 'heads/main' into daniel/scim-deprovisioning-ui

This commit is contained in:
Daniel Hougaard
2025-08-13 02:23:33 +04:00
21 changed files with 487 additions and 48 deletions
@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas/models";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "maxTimePeriod"))) {
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.string("maxTimePeriod").nullable(); // Ex: 1h - Null is permanent
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "maxTimePeriod")) {
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.dropColumn("maxTimePeriod");
});
}
}
@@ -17,7 +17,8 @@ export const AccessApprovalPoliciesSchema = z.object({
updatedAt: z.date(),
enforcementLevel: z.string().default("hard"),
deletedAt: z.date().nullable().optional(),
allowedSelfApprovals: z.boolean().default(true)
allowedSelfApprovals: z.boolean().default(true),
maxTimePeriod: z.string().nullable().optional()
});
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
@@ -3,12 +3,32 @@ import { z } from "zod";
import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
const maxTimePeriodSchema = z
.string()
.trim()
.nullish()
.transform((val, ctx) => {
if (val === undefined) return undefined;
if (!val || val === "permanent") return null;
const parsedMs = ms(val);
if (typeof parsedMs !== "number" || parsedMs <= 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')."
});
return z.NEVER;
}
return val;
});
export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
@@ -71,7 +91,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true)
allowedSelfApprovals: z.boolean().default(true),
maxTimePeriod: maxTimePeriodSchema
})
.refine(
(val) => Boolean(val.environment) || Boolean(val.environments),
@@ -124,7 +145,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.array()
.nullable()
.optional(),
bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array()
bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array(),
maxTimePeriod: z.string().nullable().optional()
})
.array()
.nullable()
@@ -233,7 +255,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
stepNumber: z.number().int()
})
.array()
.optional()
.optional(),
maxTimePeriod: maxTimePeriodSchema
}),
response: {
200: z.object({
@@ -314,7 +337,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
})
.array()
.nullable()
.optional()
.optional(),
maxTimePeriod: z.string().nullable().optional()
})
})
}
@@ -129,7 +129,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
envId: z.string(),
enforcementLevel: z.string(),
deletedAt: z.date().nullish(),
allowedSelfApprovals: z.boolean()
allowedSelfApprovals: z.boolean(),
maxTimePeriod: z.string().nullable().optional()
}),
reviewers: z
.object({
@@ -56,6 +56,7 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
projectId: string;
bypassers: (
| {
@@ -96,6 +97,7 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
environments: {
id: string;
name: string;
@@ -141,6 +143,7 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
}
| undefined
>;
@@ -100,7 +100,8 @@ export const accessApprovalPolicyServiceFactory = ({
environments,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
approvalsRequired,
maxTimePeriod
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -219,7 +220,8 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
name,
enforcementLevel,
allowedSelfApprovals
allowedSelfApprovals,
maxTimePeriod
},
tx
);
@@ -318,7 +320,8 @@ export const accessApprovalPolicyServiceFactory = ({
enforcementLevel,
allowedSelfApprovals,
approvalsRequired,
environments
environments,
maxTimePeriod
}: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
@@ -461,7 +464,8 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
name,
enforcementLevel,
allowedSelfApprovals
allowedSelfApprovals,
maxTimePeriod
},
tx
);
@@ -41,6 +41,7 @@ export type TCreateAccessApprovalPolicy = {
enforcementLevel: EnforcementLevel;
allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
maxTimePeriod?: string | null;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAccessApprovalPolicy = {
@@ -60,6 +61,7 @@ export type TUpdateAccessApprovalPolicy = {
allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
environments?: string[];
maxTimePeriod?: string | null;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteAccessApprovalPolicy = {
@@ -104,7 +106,8 @@ export interface TAccessApprovalPolicyServiceFactory {
environment,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
approvalsRequired,
maxTimePeriod
}: TCreateAccessApprovalPolicy) => Promise<{
environment: {
name: string;
@@ -135,6 +138,7 @@ export interface TAccessApprovalPolicyServiceFactory {
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
}>;
deleteAccessApprovalPolicy: ({
policyId,
@@ -159,6 +163,7 @@ export interface TAccessApprovalPolicyServiceFactory {
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
environment: {
id: string;
name: string;
@@ -185,7 +190,8 @@ export interface TAccessApprovalPolicyServiceFactory {
enforcementLevel,
allowedSelfApprovals,
approvalsRequired,
environments
environments,
maxTimePeriod
}: TUpdateAccessApprovalPolicy) => Promise<{
environment: {
id: string;
@@ -208,6 +214,7 @@ export interface TAccessApprovalPolicyServiceFactory {
allowedSelfApprovals: boolean;
secretPath?: string | null | undefined;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
}>;
getAccessApprovalPolicyByProjectSlug: ({
actorId,
@@ -242,6 +249,7 @@ export interface TAccessApprovalPolicyServiceFactory {
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
environment: {
id: string;
name: string;
@@ -298,6 +306,7 @@ export interface TAccessApprovalPolicyServiceFactory {
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
maxTimePeriod?: string | null;
environment: {
id: string;
name: string;
@@ -64,6 +64,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
enforcementLevel: string;
allowedSelfApprovals: boolean;
deletedAt: Date | null | undefined;
maxTimePeriod?: string | null;
};
projectId: string;
environments: string[];
@@ -164,6 +165,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
allowedSelfApprovals: boolean;
envId: string;
deletedAt: Date | null | undefined;
maxTimePeriod?: string | null;
};
projectId: string;
environment: string;
@@ -292,7 +294,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
`requestedByUser.id`
)
// I added these 3:
.leftJoin<TOrgMemberships>(
db(TableName.OrgMembership).as("approverOrgMembership"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
@@ -324,10 +325,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
// Added:
db.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
db.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive")
db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
)
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
@@ -394,7 +395,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
enforcementLevel: doc.policyEnforcementLevel,
allowedSelfApprovals: doc.policyAllowedSelfApprovals,
envId: doc.policyEnvId,
deletedAt: doc.policyDeletedAt
deletedAt: doc.policyDeletedAt,
maxTimePeriod: doc.policyMaxTimePeriod
},
requestedByUser: {
userId: doc.requestedByUserId,
@@ -615,7 +617,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
tx.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
);
const findById: TAccessApprovalRequestDALFactory["findById"] = async (id, tx) => {
@@ -636,7 +639,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel,
allowedSelfApprovals: el.policyAllowedSelfApprovals,
deletedAt: el.policyDeletedAt
deletedAt: el.policyDeletedAt,
maxTimePeriod: el.policyMaxTimePeriod
},
requestedByUser: {
userId: el.requestedByUserId,
@@ -156,6 +156,15 @@ export const accessApprovalRequestServiceFactory = ({
throw new BadRequestError({ message: "The policy linked to this request has been deleted" });
}
// Check if the requested time falls under policy.maxTimePeriod
if (policy.maxTimePeriod) {
if (!temporaryRange || ms(temporaryRange) > ms(policy.maxTimePeriod)) {
throw new BadRequestError({
message: `Requested access time range is limited to ${policy.maxTimePeriod} by policy`
});
}
}
const approverIds: string[] = [];
const approverGroupIds: string[] = [];
@@ -84,6 +84,7 @@ export interface TAccessApprovalRequestServiceFactory {
allowedSelfApprovals: boolean;
envId: string;
deletedAt: Date | null | undefined;
maxTimePeriod?: string | null;
};
projectId: string;
environment: string;
@@ -45,7 +45,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.transform(removeTrailingSlash)
.describe(FOLDERS.CREATE.path)
.optional(),
// backward compatiability with cli
// backward compatibility with cli
directory: z
.string()
.trim()
@@ -58,7 +58,9 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
folder: SecretFoldersSchema
folder: SecretFoldersSchema.extend({
path: z.string()
})
})
}
},
@@ -130,7 +132,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.transform(removeTrailingSlash)
.describe(FOLDERS.UPDATE.path)
.optional(),
// backward compatiability with cli
// backward compatibility with cli
directory: z
.string()
.trim()
@@ -143,7 +145,9 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
folder: SecretFoldersSchema
folder: SecretFoldersSchema.extend({
path: z.string()
})
})
}
},
@@ -359,7 +363,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.transform(removeTrailingSlash)
.describe(FOLDERS.LIST.path)
.optional(),
// backward compatiability with cli
// backward compatibility with cli
directory: z
.string()
.trim()
@@ -49,6 +49,19 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
}
const init = async () => {
await queueService.stopRepeatableJob(
QueueName.AuditLogPrune,
QueueJobs.AuditLogPrune,
{ pattern: "0 0 * * *", utc: true },
QueueName.AuditLogPrune // just a job id
);
await queueService.stopRepeatableJob(
QueueName.DailyResourceCleanUp,
QueueJobs.DailyResourceCleanUp,
{ pattern: "0 0 * * *", utc: true },
QueueName.DailyResourceCleanUp // just a job id
);
await queueService.startPg<QueueName.DailyResourceCleanUp>(
QueueJobs.DailyResourceCleanUp,
async () => {
@@ -238,8 +238,16 @@ export const secretFolderServiceFactory = ({
return doc;
});
const [folderWithFullPath] = await folderDAL.findSecretPathByFolderIds(projectId, [folder.id]);
if (!folderWithFullPath) {
throw new NotFoundError({
message: `Failed to retrieve path for folder with ID '${folder.id}'`
});
}
await snapshotService.performSnapshot(folder.parentId as string);
return folder;
return { ...folder, path: folderWithFullPath.path };
};
const updateManyFolders = async ({
@@ -496,8 +504,27 @@ export const secretFolderServiceFactory = ({
return doc;
});
const foldersWithFullPaths = await folderDAL.findSecretPathByFolderIds(projectId, [newFolder.id, folder.id]);
const newFolderWithFullPath = foldersWithFullPaths.find((f) => f?.id === newFolder.id);
if (!newFolderWithFullPath) {
throw new NotFoundError({
message: `Failed to retrieve path for folder with ID '${newFolder.id}'`
});
}
const folderWithFullPath = foldersWithFullPaths.find((f) => f?.id === folder.id);
if (!folderWithFullPath) {
throw new NotFoundError({
message: `Failed to retrieve path for folder with ID '${folder.id}'`
});
}
await snapshotService.performSnapshot(newFolder.parentId as string);
return { folder: newFolder, old: folder };
return {
folder: { ...newFolder, path: newFolderWithFullPath.path },
old: { ...folder, path: folderWithFullPath.path }
};
};
const $checkFolderPolicy = async ({