From 1a5e9f10053e94432abe7fb38ef539b25a128499 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 4 Mar 2026 19:31:59 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20resource=20policy=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/auth/actions.ts | 3 +- server/db/pg/schema/schema.ts | 1 + server/routers/external.ts | 9 + server/routers/integration.ts | 9 + server/routers/policy/index.ts | 1 + .../routers/policy/setResourcePolicyRules.ts | 162 ++++++++++++++++++ .../EditPolicyOtpEmailSectionForm.tsx | 2 +- .../EditPolicyRulesSectionForm.tsx | 14 +- 9 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 server/routers/policy/setResourcePolicyRules.ts diff --git a/messages/en-US.json b/messages/en-US.json index 1ad1d89be..af2eef9cb 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -661,6 +661,7 @@ "policyCreatedSuccess": "Resource policy succesfully created", "policyUpdatedSuccess": "Resource policy succesfully updated", "authMethodsSave": "Save auth methods", + "rulesSave": "Save Rules", "resourceErrorCreate": "Error creating resource", "resourceErrorCreateDescription": "An error occurred when creating the resource", "resourceErrorCreateMessage": "Error creating resource:", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 5c512181a..b34b3fe57 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -145,7 +145,8 @@ export enum ActionsEnum { setResourcePolicyPassword = "setResourcePolicyPassword", setResourcePolicyPincode = "setResourcePolicyPincode", setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth", - setResourcePolicyWhitelist = "setResourcePolicyWhitelist" + setResourcePolicyWhitelist = "setResourcePolicyWhitelist", + setResourcePolicyRules = "setResourcePolicyRules" } export async function checkUserActionPermission( diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index e4388b502..b33360b1f 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -678,6 +678,7 @@ export const policyRules = pgTable("policyRules", { export const resourcePolicies = pgTable("resourcePolicies", { resourcePolicyId: serial("resourcePolicyId").primaryKey(), sso: boolean("sso").notNull().default(true), + applyRules: boolean("applyRules").notNull().default(false), scope: varchar("scope") .$type<"global" | "resource">() .notNull() diff --git a/server/routers/external.ts b/server/routers/external.ts index 6e74e44a9..671bd4ac7 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -737,6 +737,15 @@ authenticated.put( policy.setResourcePolicyWhitelist ); +authenticated.put( + "/resource-policy/:resourcePolicyId/rules", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyRules), + logActionAudit(ActionsEnum.setResourcePolicyRules), + policy.setResourcePolicyRules +); + authenticated.post( `/resource/:resourceId/password`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 89ec2c2d7..92f1531ee 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -668,6 +668,15 @@ authenticated.put( policy.setResourcePolicyWhitelist ); +authenticated.put( + "/resource-policy/:resourcePolicyId/rules", + verifyApiKeyResourcePolicyAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules), + logActionAudit(ActionsEnum.setResourcePolicyRules), + policy.setResourcePolicyRules +); + authenticated.post( "/resource/:resourceId/roles/add", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts index 7719ffdfe..2ebe6da7e 100644 --- a/server/routers/policy/index.ts +++ b/server/routers/policy/index.ts @@ -5,3 +5,4 @@ export * from "./setResourcePolicyPassword"; export * from "./setResourcePolicyPincode"; export * from "./setResourcePolicyHeaderAuth"; export * from "./setResourcePolicyWhitelist"; +export * from "./setResourcePolicyRules"; diff --git a/server/routers/policy/setResourcePolicyRules.ts b/server/routers/policy/setResourcePolicyRules.ts new file mode 100644 index 000000000..147a67814 --- /dev/null +++ b/server/routers/policy/setResourcePolicyRules.ts @@ -0,0 +1,162 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, policyRules, resourcePolicies } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; +import { OpenAPITags, registry } from "@server/openApi"; + +const ruleSchema = z.strictObject({ + action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({ + type: "string", + enum: ["ACCEPT", "DROP", "PASS"], + description: "rule action" + }), + match: z.enum(["CIDR", "IP", "PATH"]).openapi({ + type: "string", + enum: ["CIDR", "IP", "PATH"], + description: "rule match" + }), + value: z.string().min(1), + priority: z.int(), + enabled: z.boolean().optional() +}); + +const setResourcePolicyRulesBodySchema = z.strictObject({ + applyRules: z.boolean(), + rules: z.array(ruleSchema) +}); + +const setResourcePolicyRulesParamsSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "put", + path: "/resource-policy/{resourcePolicyId}/rules", + description: + "Set all rules for a resource policy at once. This will replace all existing rules.", + tags: [OpenAPITags.Policy], + request: { + params: setResourcePolicyRulesParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyRulesBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyRules( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = setResourcePolicyRulesParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = setResourcePolicyRulesBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + const { applyRules, rules } = parsedBody.data; + + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); + + if (!policy) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource policy not found") + ); + } + + for (const rule of rules) { + if (rule.match === "CIDR" && !isValidCIDR(rule.value)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid CIDR provided" + ) + ); + } else if (rule.match === "IP" && !isValidIP(rule.value)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided") + ); + } else if ( + rule.match === "PATH" && + !isValidUrlGlobPattern(rule.value) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid URL glob pattern provided" + ) + ); + } + } + + await db.transaction(async (trx) => { + await trx + .update(resourcePolicies) + .set({ applyRules }) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + + await trx + .delete(policyRules) + .where(eq(policyRules.resourcePolicyId, resourcePolicyId)); + + if (rules.length > 0) { + await trx.insert(policyRules).values( + rules.map((rule) => ({ + resourcePolicyId, + ...rule + })) + ); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Resource policy rules set successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx index c120c13da..3a117bd8a 100644 --- a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -68,7 +68,7 @@ export function EditPolicyOtpEmailSectionForm({ defaultValues: { emailWhitelistEnabled: policy.emailWhitelistEnabled, emails: policy.emailWhiteList.map((email) => ({ - id: email.whitelistId.toString(), + id: email.whiteListId.toString(), text: email.email })) } diff --git a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx index f8f044740..23c8a1dd9 100644 --- a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -76,7 +77,7 @@ import { } from "@tanstack/react-table"; import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useState, useTransition } from "react"; import { UseFormReturn, useForm } from "react-hook-form"; // ─── PolicyRulesSection ─────────────────────────────────────────────────────── @@ -615,6 +616,8 @@ export function EditPolicyRulesSectionForm({ state: { pagination: { pageIndex: 0, pageSize: 1000 } } }); + const [isPending, startTransition] = useTransition(); + if (!isExpanded) { return ( @@ -1070,6 +1073,15 @@ export function EditPolicyRulesSectionForm({ + + + ); }