mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-03 01:24:58 +02:00
rules server validation, enabled toggle, fix wildcard
This commit is contained in:
parent
f14ecf50e4
commit
fdf1dfdeba
13 changed files with 467 additions and 196 deletions
|
@ -377,6 +377,8 @@ export const resourceRules = sqliteTable("resourceRules", {
|
|||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
priority: integer("priority").notNull(),
|
||||
action: text("action").notNull(), // ACCEPT, DROP
|
||||
match: text("match").notNull(), // CIDR, PATH, IP
|
||||
value: text("value").notNull()
|
||||
|
@ -414,4 +416,4 @@ export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
|||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
|
||||
export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
||||
export type ResourceRule = InferSelectModel<typeof resourceRules>;
|
||||
|
|
|
@ -8,3 +8,4 @@ export const subdomainSchema = z
|
|||
)
|
||||
.min(1, "Subdomain must be at least 1 character long")
|
||||
.transform((val) => val.toLowerCase());
|
||||
|
68
server/lib/validators.ts
Normal file
68
server/lib/validators.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
export function isValidCIDR(cidr: string): boolean {
|
||||
// Match CIDR pattern (e.g., "192.168.0.0/24")
|
||||
const cidrPattern =
|
||||
/^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
||||
|
||||
if (!cidrPattern.test(cidr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate IP address part
|
||||
const ipPart = cidr.split("/")[0];
|
||||
const octets = ipPart.split(".");
|
||||
|
||||
return octets.every((octet) => {
|
||||
const num = parseInt(octet, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
export function isValidIP(ip: string): boolean {
|
||||
const ipPattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
|
||||
if (!ipPattern.test(ip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const octets = ip.split(".");
|
||||
|
||||
return octets.every((octet) => {
|
||||
const num = parseInt(octet, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
export function isValidUrlGlobPattern(pattern: string): boolean {
|
||||
// Remove leading slash if present
|
||||
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
||||
|
||||
// Empty string is not valid
|
||||
if (!pattern) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split path into segments
|
||||
const segments = pattern.split("/");
|
||||
|
||||
// Check each segment
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
|
||||
// Empty segments are not allowed (double slashes)
|
||||
if (!segment && i !== segments.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If segment contains *, it must be exactly *
|
||||
if (segment.includes("*") && segment !== "*") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -1,36 +1,38 @@
|
|||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { response } from "@server/lib/response";
|
||||
import db from "@server/db";
|
||||
import {
|
||||
resourceRules,
|
||||
ResourceAccessToken,
|
||||
ResourcePassword,
|
||||
resourcePassword,
|
||||
ResourcePincode,
|
||||
resourcePincode,
|
||||
resources,
|
||||
sessions,
|
||||
userOrgs,
|
||||
users,
|
||||
ResourceRule
|
||||
} from "@server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie,
|
||||
validateResourceSessionToken
|
||||
} from "@server/auth/sessions/resource";
|
||||
import { Resource, roleResources, userResources } from "@server/db/schema";
|
||||
import logger from "@server/logger";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import NodeCache from "node-cache";
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import {
|
||||
Resource,
|
||||
ResourceAccessToken,
|
||||
ResourcePassword,
|
||||
resourcePassword,
|
||||
ResourcePincode,
|
||||
resourcePincode,
|
||||
ResourceRule,
|
||||
resourceRules,
|
||||
resources,
|
||||
roleResources,
|
||||
sessions,
|
||||
userOrgs,
|
||||
userResources,
|
||||
users
|
||||
} from "@server/db/schema";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr } from "@server/lib/ip";
|
||||
import { response } from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import NodeCache from "node-cache";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
// We'll see if this speeds anything up
|
||||
const cache = new NodeCache({
|
||||
|
@ -169,18 +171,16 @@ export async function verifyResourceSession(
|
|||
// otherwise its undefined and we pass
|
||||
}
|
||||
|
||||
const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
const redirectUrl = `${config.getRawConfig().app.dashboard_url}/auth/resource/${encodeURIComponent(
|
||||
resource.resourceId
|
||||
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
|
||||
// check for access token
|
||||
let validAccessToken: ResourceAccessToken | undefined;
|
||||
if (token) {
|
||||
const [accessTokenId, accessToken] = token.split(".");
|
||||
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
||||
{
|
||||
resource,
|
||||
accessTokenId,
|
||||
accessToken
|
||||
}
|
||||
{ resource, accessTokenId, accessToken }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
|
@ -190,7 +190,9 @@ export async function verifyResourceSession(
|
|||
if (!valid) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Resource access token is invalid. Resource ID: ${resource.resourceId}. IP: ${clientIp}.`
|
||||
`Resource access token is invalid. Resource ID: ${
|
||||
resource.resourceId
|
||||
}. IP: ${clientIp}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +213,9 @@ export async function verifyResourceSession(
|
|||
if (!sessions) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Missing resource sessions. Resource ID: ${resource.resourceId}. IP: ${clientIp}.`
|
||||
`Missing resource sessions. Resource ID: ${
|
||||
resource.resourceId
|
||||
}. IP: ${clientIp}.`
|
||||
);
|
||||
}
|
||||
return notAllowed(res);
|
||||
|
@ -219,7 +223,9 @@ export async function verifyResourceSession(
|
|||
|
||||
const resourceSessionToken =
|
||||
sessions[
|
||||
`${config.getRawConfig().server.session_cookie_name}${resource.ssl ? "_s" : ""}`
|
||||
`${config.getRawConfig().server.session_cookie_name}${
|
||||
resource.ssl ? "_s" : ""
|
||||
}`
|
||||
];
|
||||
|
||||
if (resourceSessionToken) {
|
||||
|
@ -242,7 +248,9 @@ export async function verifyResourceSession(
|
|||
);
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Resource session is an exchange token. Resource ID: ${resource.resourceId}. IP: ${clientIp}.`
|
||||
`Resource session is an exchange token. Resource ID: ${
|
||||
resource.resourceId
|
||||
}. IP: ${clientIp}.`
|
||||
);
|
||||
}
|
||||
return notAllowed(res);
|
||||
|
@ -281,7 +289,9 @@ export async function verifyResourceSession(
|
|||
}
|
||||
|
||||
if (resourceSession.userSessionId && sso) {
|
||||
const userAccessCacheKey = `userAccess:${resourceSession.userSessionId}:${resource.resourceId}`;
|
||||
const userAccessCacheKey = `userAccess:${
|
||||
resourceSession.userSessionId
|
||||
}:${resource.resourceId}`;
|
||||
|
||||
let isAllowed: boolean | undefined =
|
||||
cache.get(userAccessCacheKey);
|
||||
|
@ -305,8 +315,8 @@ export async function verifyResourceSession(
|
|||
}
|
||||
}
|
||||
|
||||
// At this point we have checked all sessions, but since the access token is valid, we should allow access
|
||||
// and create a new session.
|
||||
// At this point we have checked all sessions, but since the access token is
|
||||
// valid, we should allow access and create a new session.
|
||||
if (validAccessToken) {
|
||||
return await createAccessTokenSession(
|
||||
res,
|
||||
|
@ -319,7 +329,9 @@ export async function verifyResourceSession(
|
|||
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Resource access not allowed. Resource ID: ${resource.resourceId}. IP: ${clientIp}.`
|
||||
`Resource access not allowed. Resource ID: ${
|
||||
resource.resourceId
|
||||
}. IP: ${clientIp}.`
|
||||
);
|
||||
}
|
||||
return notAllowed(res, redirectUrl);
|
||||
|
@ -485,69 +497,123 @@ async function checkRules(
|
|||
return;
|
||||
}
|
||||
|
||||
let hasAcceptRule = false;
|
||||
// sort rules by priority in ascending order
|
||||
rules = rules.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
// First pass: look for DROP rules
|
||||
for (const rule of rules) {
|
||||
if (!rule.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(clientIp &&
|
||||
rule.match == "CIDR" &&
|
||||
isIpInCidr(clientIp, rule.value) &&
|
||||
rule.action === "DROP") ||
|
||||
(clientIp &&
|
||||
rule.match == "IP" &&
|
||||
clientIp == rule.value &&
|
||||
rule.action === "DROP") ||
|
||||
(path &&
|
||||
rule.match == "PATH" &&
|
||||
urlGlobToRegex(rule.value).test(path) &&
|
||||
rule.action === "DROP")
|
||||
clientIp &&
|
||||
rule.match == "CIDR" &&
|
||||
isIpInCidr(clientIp, rule.value)
|
||||
) {
|
||||
return "DROP";
|
||||
}
|
||||
// Track if we see any ACCEPT rules for the second pass
|
||||
if (rule.action === "ACCEPT") {
|
||||
hasAcceptRule = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: only check ACCEPT rules if we found one and didn't find a DROP
|
||||
if (hasAcceptRule) {
|
||||
for (const rule of rules) {
|
||||
if (rule.action !== "ACCEPT") continue;
|
||||
|
||||
if (
|
||||
(clientIp &&
|
||||
rule.match == "CIDR" &&
|
||||
isIpInCidr(clientIp, rule.value)) ||
|
||||
(clientIp &&
|
||||
rule.match == "IP" &&
|
||||
clientIp == rule.value) ||
|
||||
(path &&
|
||||
rule.match == "PATH" &&
|
||||
urlGlobToRegex(rule.value).test(path))
|
||||
) {
|
||||
return "ACCEPT";
|
||||
}
|
||||
return rule.action as any;
|
||||
} else if (clientIp && rule.match == "IP" && clientIp == rule.value) {
|
||||
return rule.action as any;
|
||||
} else if (
|
||||
path &&
|
||||
rule.match == "PATH" &&
|
||||
isPathAllowed(rule.value, path)
|
||||
) {
|
||||
return rule.action as any;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function urlGlobToRegex(pattern: string): RegExp {
|
||||
// Trim any leading or trailing slashes
|
||||
pattern = pattern.replace(/^\/+|\/+$/g, "");
|
||||
function isPathAllowed(pattern: string, path: string): boolean {
|
||||
logger.debug(`\nMatching path "${path}" against pattern "${pattern}"`);
|
||||
|
||||
// Escape special regex characters except *
|
||||
const escapedPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
||||
// Normalize and split paths into segments
|
||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||
const patternParts = normalize(pattern);
|
||||
const pathParts = normalize(path);
|
||||
|
||||
// Replace * with regex pattern for any valid URL segment characters
|
||||
const regexPattern = escapedPattern.replace(/\*/g, "[a-zA-Z0-9_-]+");
|
||||
logger.debug(`Normalized pattern parts: [${patternParts.join(", ")}]`);
|
||||
logger.debug(`Normalized path parts: [${pathParts.join(", ")}]`);
|
||||
|
||||
// Create the final pattern that:
|
||||
// 1. Optionally matches leading slash
|
||||
// 2. Matches the pattern
|
||||
// 3. Optionally matches trailing slash
|
||||
return new RegExp(`^/?${regexPattern}/?$`);
|
||||
}
|
||||
// Recursive function to try different wildcard matches
|
||||
function matchSegments(patternIndex: number, pathIndex: number): boolean {
|
||||
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
|
||||
const currentPatternPart = patternParts[patternIndex];
|
||||
const currentPathPart = pathParts[pathIndex];
|
||||
|
||||
logger.debug(
|
||||
`${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"})`
|
||||
);
|
||||
|
||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
||||
if (patternIndex >= patternParts.length) {
|
||||
const result = pathIndex >= pathParts.length;
|
||||
logger.debug(
|
||||
`${indent}Reached end of pattern, remaining path: ${pathParts.slice(pathIndex).join("/")} -> ${result}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we've consumed all path parts but still have pattern parts
|
||||
if (pathIndex >= pathParts.length) {
|
||||
// The only way this can match is if all remaining pattern parts are wildcards
|
||||
const remainingPattern = patternParts.slice(patternIndex);
|
||||
const result = remainingPattern.every((p) => p === "*");
|
||||
logger.debug(
|
||||
`${indent}Reached end of path, remaining pattern: ${remainingPattern.join("/")} -> ${result}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// For wildcards, try consuming different numbers of path segments
|
||||
if (currentPatternPart === "*") {
|
||||
logger.debug(
|
||||
`${indent}Found wildcard at pattern index ${patternIndex}`
|
||||
);
|
||||
|
||||
// Try consuming 0 segments (skip the wildcard)
|
||||
logger.debug(
|
||||
`${indent}Trying to skip wildcard (consume 0 segments)`
|
||||
);
|
||||
if (matchSegments(patternIndex + 1, pathIndex)) {
|
||||
logger.debug(
|
||||
`${indent}Successfully matched by skipping wildcard`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try consuming current segment and recursively try rest
|
||||
logger.debug(
|
||||
`${indent}Trying to consume segment "${currentPathPart}" for wildcard`
|
||||
);
|
||||
if (matchSegments(patternIndex, pathIndex + 1)) {
|
||||
logger.debug(
|
||||
`${indent}Successfully matched by consuming segment for wildcard`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug(`${indent}Failed to match wildcard`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// For regular segments, they must match exactly
|
||||
if (currentPatternPart !== currentPathPart) {
|
||||
logger.debug(
|
||||
`${indent}Segment mismatch: "${currentPatternPart}" != "${currentPathPart}"`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
|
||||
);
|
||||
// Move to next segments in both pattern and path
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
||||
}
|
||||
|
||||
const result = matchSegments(0, 0);
|
||||
logger.debug(`Final result: ${result}`);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { eq, and } from "drizzle-orm";
|
|||
import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||
import { subdomainSchema } from "@server/lib/schemas";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const createResourceParamsSchema = z
|
||||
|
|
|
@ -8,12 +8,19 @@ 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";
|
||||
|
||||
const createResourceRuleSchema = z
|
||||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]),
|
||||
value: z.string().min(1)
|
||||
value: z.string().min(1),
|
||||
priority: z.number().int(),
|
||||
enabled: z.boolean().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
@ -42,7 +49,7 @@ export async function createResourceRule(
|
|||
);
|
||||
}
|
||||
|
||||
const { action, match, value } = parsedBody.data;
|
||||
const { action, match, value, priority, enabled } = parsedBody.data;
|
||||
|
||||
const parsedParams = createResourceRuleParamsSchema.safeParse(
|
||||
req.params
|
||||
|
@ -74,6 +81,41 @@ export async function createResourceRule(
|
|||
);
|
||||
}
|
||||
|
||||
if (!resource.http) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Cannot create rule for non-http resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (match === "CIDR") {
|
||||
if (!isValidCIDR(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "IP") {
|
||||
if (!isValidIP(value)) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided")
|
||||
);
|
||||
}
|
||||
} else if (match === "PATH") {
|
||||
if (!isValidUrlGlobPattern(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL glob pattern provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new resource rule
|
||||
const [newRule] = await db
|
||||
.insert(resourceRules)
|
||||
|
@ -81,7 +123,9 @@ export async function createResourceRule(
|
|||
resourceId,
|
||||
action,
|
||||
match,
|
||||
value
|
||||
value,
|
||||
priority,
|
||||
enabled
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
@ -40,12 +40,14 @@ function queryResourceRules(resourceId: number) {
|
|||
resourceId: resourceRules.resourceId,
|
||||
action: resourceRules.action,
|
||||
match: resourceRules.match,
|
||||
value: resourceRules.value
|
||||
value: resourceRules.value,
|
||||
priority: resourceRules.priority,
|
||||
enabled: resourceRules.enabled
|
||||
})
|
||||
.from(resourceRules)
|
||||
.leftJoin(resources, eq(resourceRules.resourceId, resources.resourceId))
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
|
||||
|
@ -71,7 +73,9 @@ export async function listResourceRules(
|
|||
}
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
const parsedParams = listResourceRulesParamsSchema.safeParse(req.params);
|
||||
const parsedParams = listResourceRulesParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
@ -99,16 +103,19 @@ export async function listResourceRules(
|
|||
}
|
||||
|
||||
const baseQuery = queryResourceRules(resourceId);
|
||||
|
||||
|
||||
let countQuery = db
|
||||
.select({ count: sql<number>`cast(count(*) as integer)` })
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.resourceId, resourceId));
|
||||
|
||||
const rulesList = await baseQuery.limit(limit).offset(offset);
|
||||
let rulesList = await baseQuery.limit(limit).offset(offset);
|
||||
const totalCountResult = await countQuery;
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
// sort rules list by the priority in ascending order
|
||||
rulesList = rulesList.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
return response<ListResourceRulesResponse>(res, {
|
||||
data: {
|
||||
rules: rulesList,
|
||||
|
@ -129,4 +136,4 @@ export async function listResourceRules(
|
|||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import HttpCode from "@server/types/HttpCode";
|
|||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||
import config from "@server/lib/config";
|
||||
import { subdomainSchema } from "@server/lib/schemas";
|
||||
|
||||
const updateResourceParamsSchema = z
|
||||
.object({
|
||||
|
|
|
@ -8,14 +8,16 @@ 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";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const updateResourceRuleParamsSchema = z
|
||||
.object({
|
||||
ruleId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
ruleId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
|
@ -28,7 +30,9 @@ const updateResourceRuleSchema = z
|
|||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH"]).optional(),
|
||||
value: z.string().min(1).optional()
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.number().int(),
|
||||
enabled: z.boolean().optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
|
@ -42,7 +46,9 @@ export async function updateResourceRule(
|
|||
): Promise<any> {
|
||||
try {
|
||||
// Validate path parameters
|
||||
const parsedParams = updateResourceRuleParamsSchema.safeParse(req.params);
|
||||
const parsedParams = updateResourceRuleParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
@ -82,6 +88,15 @@ export async function updateResourceRule(
|
|||
);
|
||||
}
|
||||
|
||||
if (!resource.http) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Cannot create rule for non-http resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the rule exists and belongs to the specified resource
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
|
@ -107,6 +122,40 @@ export async function updateResourceRule(
|
|||
);
|
||||
}
|
||||
|
||||
const match = updateData.match || existingRule.match;
|
||||
const { value } = updateData;
|
||||
|
||||
if (value !== undefined) {
|
||||
if (match === "CIDR") {
|
||||
if (!isValidCIDR(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid CIDR provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "IP") {
|
||||
if (!isValidIP(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid IP provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "PATH") {
|
||||
if (!isValidUrlGlobPattern(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid URL glob pattern provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rule
|
||||
const [updatedRule] = await db
|
||||
.update(resourceRules)
|
||||
|
@ -127,4 +176,4 @@ export async function updateResourceRule(
|
|||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ export default async function migration() {
|
|||
trx.run(sql`CREATE TABLE resourceRules (
|
||||
ruleId integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
resourceId integer NOT NULL,
|
||||
priority integer NOT NULL,
|
||||
enabled integer DEFAULT true NOT NULL,
|
||||
action text NOT NULL,
|
||||
match text NOT NULL,
|
||||
value text NOT NULL,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue