Compare commits

..

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
7c15c428b3 test: add normalized ASN validation coverage 2026-06-16 23:48:28 +00:00
copilot-swe-agent[bot]
f3a52e31d1 refactor: normalize ASN validation value once 2026-06-16 23:46:44 +00:00
copilot-swe-agent[bot]
5e26ceaf02 fix: allow ALL ASN values in policy rule validation 2026-06-16 23:44:35 +00:00
copilot-swe-agent[bot]
d6fe357fcb Initial plan 2026-06-16 23:39:56 +00:00
Owen
f9cc52ece9 Remove NoNewPrivileges
Fixes https://github.com/fosrl/newt/issues/383
2026-06-14 15:02:18 -07:00
9 changed files with 123 additions and 115 deletions

View File

@@ -1,4 +1,7 @@
import { isValidUrlGlobPattern } from "./validators"; import {
getResourceRuleValueValidationError,
isValidUrlGlobPattern
} from "./validators";
import { assertEquals } from "@test/assert"; import { assertEquals } from "@test/assert";
function runTests() { function runTests() {
@@ -236,6 +239,43 @@ function runTests() {
"Path with isolated percent sign should be invalid" "Path with isolated percent sign should be invalid"
); );
// ASN validation tests
assertEquals(
getResourceRuleValueValidationError("ASN", "AS15169"),
null,
"Standard ASN should be valid"
);
assertEquals(
getResourceRuleValueValidationError("ASN", " As15169 "),
null,
"Standard ASN should be valid with mixed case and whitespace"
);
assertEquals(
getResourceRuleValueValidationError("ASN", "ALL"),
null,
"ALL ASN selector should be valid"
);
assertEquals(
getResourceRuleValueValidationError("ASN", " all "),
null,
"ALL ASN selector should be valid with mixed case and whitespace"
);
assertEquals(
getResourceRuleValueValidationError("ASN", "AS0"),
null,
"AS0 alias should be valid"
);
assertEquals(
getResourceRuleValueValidationError("ASN", " as0 "),
null,
"AS0 alias should be valid with mixed case and whitespace"
);
assertEquals(
getResourceRuleValueValidationError("ASN", "not-an-asn"),
"Invalid ASN provided",
"Invalid ASN should return an error"
);
console.log("All tests passed!"); console.log("All tests passed!");
} }

View File

@@ -100,7 +100,10 @@ export function getResourceRuleValueValidationError(
? null ? null
: "Invalid country code provided"; : "Invalid country code provided";
case "ASN": case "ASN":
return /^AS\d+$/i.test(value.trim()) const normalizedValue = value.trim().toUpperCase();
return /^AS\d+$/.test(normalizedValue) ||
normalizedValue === "ALL" ||
normalizedValue === "AS0"
? null ? null
: "Invalid ASN provided"; : "Invalid ASN provided";
default: default:

View File

@@ -154,8 +154,12 @@ export async function createResourceRule(
} }
// Create the new resource rule // Create the new resource rule
if (resource.resourcePolicyId !== null) { const isInlinePolicy =
const policyId = resource.resourcePolicyId; resource.resourcePolicyId === null &&
resource.defaultResourcePolicyId !== null;
if (isInlinePolicy) {
const policyId = resource.defaultResourcePolicyId!;
const [newRule] = await db const [newRule] = await db
.insert(resourcePolicyRules) .insert(resourcePolicyRules)
.values({ .values({

View File

@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceRules, resourcePolicyRules, resources } from "@server/db"; import { resourceRules, resourcePolicyRules, resources } from "@server/db";
import { and, eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -73,18 +73,14 @@ export async function deleteResourceRule(
); );
} }
if (resource.resourcePolicyId !== null) { const isInlinePolicy =
resource.resourcePolicyId === null &&
resource.defaultResourcePolicyId !== null;
if (isInlinePolicy) {
const [deletedRule] = await db const [deletedRule] = await db
.delete(resourcePolicyRules) .delete(resourcePolicyRules)
.where( .where(eq(resourcePolicyRules.ruleId, ruleId))
and(
eq(resourcePolicyRules.ruleId, ruleId),
eq(
resourcePolicyRules.resourcePolicyId,
resource.resourcePolicyId
)
)
)
.returning(); .returning();
if (!deletedRule) { if (!deletedRule) {

View File

@@ -141,10 +141,16 @@ export async function getResource(
); );
} }
const isInlinePolicy =
resource.resourcePolicyId === null &&
resource.defaultResourcePolicyId !== null;
let returnData = resource; let returnData = resource;
if (resource.resourcePolicyId !== null) { if (isInlinePolicy) {
// get the policy // get the policy
const policy = await queryInlinePolicy(resource.resourcePolicyId); const policy = await queryInlinePolicy(
resource.defaultResourcePolicyId!
);
returnData = { returnData = {
...returnData, ...returnData,
sso: policy?.sso || null, sso: policy?.sso || null,

View File

@@ -140,11 +140,15 @@ export async function listResourceRules(
); );
} }
const isInlinePolicy =
resource.resourcePolicyId === null &&
resource.defaultResourcePolicyId !== null;
let rulesList: Awaited<ReturnType<typeof queryResourceRules>>; let rulesList: Awaited<ReturnType<typeof queryResourceRules>>;
let totalCount: number; let totalCount: number;
if (resource.resourcePolicyId !== null) { if (isInlinePolicy) {
const policyId = resource.resourcePolicyId; const policyId = resource.defaultResourcePolicyId!;
const policyRules = await queryPolicyRules(policyId) const policyRules = await queryPolicyRules(policyId)
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);

View File

@@ -1,8 +1,8 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourcePolicyRules, resourceRules, resources } from "@server/db"; import { resourceRules, resources } from "@server/db";
import { and, eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -37,29 +37,6 @@ const updateResourceRuleSchema = z
error: "At least one field must be provided for update" error: "At least one field must be provided for update"
}); });
function getRuleValueValidationError(
match: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN" | "REGION",
value: string
): string | null {
if (match === "CIDR" && !isValidCIDR(value)) {
return "Invalid CIDR provided";
}
if (match === "IP" && !isValidIP(value)) {
return "Invalid IP provided";
}
if (match === "PATH" && !isValidUrlGlobPattern(value)) {
return "Invalid URL glob pattern provided";
}
if (match === "REGION" && !isValidRegionId(value)) {
return "Invalid region ID provided";
}
return null;
}
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/rule/{ruleId}", path: "/resource/{resourceId}/rule/{ruleId}",
@@ -151,68 +128,6 @@ export async function updateResourceRule(
); );
} }
if (resource.resourcePolicyId !== null) {
const [existingRule] = await db
.select()
.from(resourcePolicyRules)
.where(
and(
eq(resourcePolicyRules.ruleId, ruleId),
eq(
resourcePolicyRules.resourcePolicyId,
resource.resourcePolicyId
)
)
)
.limit(1);
if (!existingRule) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource rule with ID ${ruleId} not found`
)
);
}
const match = updateData.match || existingRule.match;
const { value } = updateData;
if (value !== undefined) {
const validationError = getRuleValueValidationError(
match,
value
);
if (validationError) {
return next(
createHttpError(HttpCode.BAD_REQUEST, validationError)
);
}
}
const [updatedRule] = await db
.update(resourcePolicyRules)
.set(updateData)
.where(
and(
eq(resourcePolicyRules.ruleId, ruleId),
eq(
resourcePolicyRules.resourcePolicyId,
resource.resourcePolicyId
)
)
)
.returning();
return response(res, {
data: updatedRule,
success: true,
error: false,
message: "Resource rule updated successfully",
status: HttpCode.OK
});
}
// Verify that the rule exists and belongs to the specified resource // Verify that the rule exists and belongs to the specified resource
const [existingRule] = await db const [existingRule] = await db
.select() .select()
@@ -242,11 +157,42 @@ export async function updateResourceRule(
const { value } = updateData; const { value } = updateData;
if (value !== undefined) { if (value !== undefined) {
const validationError = getRuleValueValidationError(match, value); if (match === "CIDR") {
if (validationError) { if (!isValidCIDR(value)) {
return next( return next(
createHttpError(HttpCode.BAD_REQUEST, validationError) 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"
)
);
}
} else if (match === "REGION") {
if (!isValidRegionId(value)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid region ID provided"
)
);
}
} }
} }

View File

@@ -139,7 +139,6 @@ Restart=always
RestartSec=2 RestartSec=2
UMask=0077 UMask=0077
NoNewPrivileges=true
PrivateTmp=true PrivateTmp=true
[Install] [Install]

View File

@@ -83,9 +83,19 @@ export function createPolicyRuleValueSchema(t: TranslateFn, match: string) {
{ message: t("rulesErrorInvalidCountryDescription") } { message: t("rulesErrorInvalidCountryDescription") }
); );
case "ASN": case "ASN":
return required.refine((value) => /^AS\d+$/i.test(value.trim()), { return required.refine(
message: t("rulesErrorInvalidAsnDescription") (value) => {
}); const normalizedValue = value.trim().toUpperCase();
return (
/^AS\d+$/.test(normalizedValue) ||
normalizedValue === "ALL" ||
normalizedValue === "AS0"
);
},
{
message: t("rulesErrorInvalidAsnDescription")
}
);
default: default:
return required; return required;
} }