🚧 resource policy rules

This commit is contained in:
Fred KISSIE
2026-03-04 19:31:59 +01:00
parent f42c013f33
commit 1a5e9f1005
9 changed files with 199 additions and 3 deletions

View File

@@ -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:",

View File

@@ -145,7 +145,8 @@ export enum ActionsEnum {
setResourcePolicyPassword = "setResourcePolicyPassword",
setResourcePolicyPincode = "setResourcePolicyPincode",
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist"
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
setResourcePolicyRules = "setResourcePolicyRules"
}
export async function checkUserActionPermission(

View File

@@ -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()

View File

@@ -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,

View File

@@ -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,

View File

@@ -5,3 +5,4 @@ export * from "./setResourcePolicyPassword";
export * from "./setResourcePolicyPincode";
export * from "./setResourcePolicyHeaderAuth";
export * from "./setResourcePolicyWhitelist";
export * from "./setResourcePolicyRules";

View File

@@ -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<any> {
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")
);
}
}

View File

@@ -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
}))
}

View File

@@ -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 (
<SettingsSection>
@@ -1070,6 +1073,15 @@ export function EditPolicyRulesSectionForm({
</Table>
</div>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
// onClick={saveAllSettings}
// loading={loading}
// disabled={loading}
>
{t("rulesSave")}
</Button>
</SettingsSectionFooter>
</SettingsSection>
);
}