mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-31 15:06:42 +00:00
Add region-based resource rule
This commit is contained in:
@@ -1,4 +1,37 @@
|
||||
import { assertEquals } from "@test/assert";
|
||||
import { REGIONS } from "@server/db/regions";
|
||||
|
||||
function isIpInRegion(
|
||||
ipCountryCode: string | undefined,
|
||||
checkRegionCode: string
|
||||
): boolean {
|
||||
if (!ipCountryCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const upperCode = ipCountryCode.toUpperCase();
|
||||
|
||||
for (const region of REGIONS) {
|
||||
// Check if it's a top-level region (continent)
|
||||
if (region.id === checkRegionCode) {
|
||||
for (const subregion of region.includes) {
|
||||
if (subregion.countries.includes(upperCode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check subregions
|
||||
for (const subregion of region.includes) {
|
||||
if (subregion.id === checkRegionCode) {
|
||||
return subregion.countries.includes(upperCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPathAllowed(pattern: string, path: string): boolean {
|
||||
// Normalize and split paths into segments
|
||||
@@ -272,12 +305,71 @@ function runTests() {
|
||||
"Root path should not match non-root path"
|
||||
);
|
||||
|
||||
console.log("All tests passed!");
|
||||
console.log("All path matching tests passed!");
|
||||
}
|
||||
|
||||
function runRegionTests() {
|
||||
console.log("\nRunning isIpInRegion tests...");
|
||||
|
||||
// Test undefined country code
|
||||
assertEquals(
|
||||
isIpInRegion(undefined, "150"),
|
||||
false,
|
||||
"Undefined country code should return false"
|
||||
);
|
||||
|
||||
// Test subregion matching (Western Europe)
|
||||
assertEquals(
|
||||
isIpInRegion("DE", "155"),
|
||||
true,
|
||||
"Country should match its subregion"
|
||||
);
|
||||
assertEquals(
|
||||
isIpInRegion("GB", "155"),
|
||||
false,
|
||||
"Country should NOT match wrong subregion"
|
||||
);
|
||||
|
||||
// Test continent matching (Europe)
|
||||
assertEquals(
|
||||
isIpInRegion("DE", "150"),
|
||||
true,
|
||||
"Country should match its continent"
|
||||
);
|
||||
assertEquals(
|
||||
isIpInRegion("GB", "150"),
|
||||
true,
|
||||
"Different European country should match Europe"
|
||||
);
|
||||
assertEquals(
|
||||
isIpInRegion("US", "150"),
|
||||
false,
|
||||
"Non-European country should NOT match Europe"
|
||||
);
|
||||
|
||||
// Test case insensitivity
|
||||
assertEquals(
|
||||
isIpInRegion("de", "155"),
|
||||
true,
|
||||
"Lowercase country code should work"
|
||||
);
|
||||
|
||||
// Test invalid region code
|
||||
assertEquals(
|
||||
isIpInRegion("DE", "999"),
|
||||
false,
|
||||
"Invalid region code should return false"
|
||||
);
|
||||
|
||||
console.log("All region tests passed!");
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
try {
|
||||
runTests();
|
||||
runRegionTests();
|
||||
console.log("\n✅ All tests passed!");
|
||||
} catch (error) {
|
||||
console.error("Test failed:", error);
|
||||
console.error("❌ Test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { logRequestAudit } from "./logRequestAudit";
|
||||
import cache from "@server/lib/cache";
|
||||
import { REGIONS } from "@server/db/regions";
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string(), z.string()).optional(),
|
||||
@@ -967,6 +968,12 @@ async function checkRules(
|
||||
(await isIpInAsn(ipAsn, rule.value))
|
||||
) {
|
||||
return rule.action as any;
|
||||
} else if (
|
||||
clientIp &&
|
||||
rule.match == "REGION" &&
|
||||
(await isIpInRegion(ipCC, rule.value))
|
||||
) {
|
||||
return rule.action as any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,6 +1140,45 @@ async function isIpInAsn(
|
||||
return match;
|
||||
}
|
||||
|
||||
export async function isIpInRegion(
|
||||
ipCountryCode: string | undefined,
|
||||
checkRegionCode: string
|
||||
): Promise<boolean> {
|
||||
if (!ipCountryCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const upperCode = ipCountryCode.toUpperCase();
|
||||
|
||||
for (const region of REGIONS) {
|
||||
// Check if it's a top-level region (continent)
|
||||
if (region.id === checkRegionCode) {
|
||||
for (const subregion of region.includes) {
|
||||
if (subregion.countries.includes(upperCode)) {
|
||||
logger.debug(`Country ${upperCode} is in region ${region.id} (${region.name})`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
logger.debug(`Country ${upperCode} is not in region ${region.id} (${region.name})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check subregions
|
||||
for (const subregion of region.includes) {
|
||||
if (subregion.id === checkRegionCode) {
|
||||
if (subregion.countries.includes(upperCode)) {
|
||||
logger.debug(`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`);
|
||||
return true;
|
||||
}
|
||||
logger.debug(`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getAsnFromIp(ip: string): Promise<number | undefined> {
|
||||
const asnCacheKey = `asn:${ip}`;
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
isValidUrlGlobPattern
|
||||
} from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isValidRegionId } from "@server/db/regions";
|
||||
|
||||
const createResourceRuleSchema = z.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"]),
|
||||
value: z.string().min(1),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
@@ -126,6 +127,15 @@ export async function createResourceRule(
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "REGION") {
|
||||
if (!isValidRegionId(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid region ID provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new resource rule
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isValidUrlGlobPattern
|
||||
} from "@server/lib/validators";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isValidRegionId } from "@server/db/regions";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const updateResourceRuleParamsSchema = z.strictObject({
|
||||
@@ -25,7 +26,7 @@ const updateResourceRuleParamsSchema = z.strictObject({
|
||||
const updateResourceRuleSchema = z
|
||||
.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"]).optional(),
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
@@ -166,6 +167,15 @@ export async function updateResourceRule(
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (match === "REGION") {
|
||||
if (!isValidRegionId(value)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid region ID provided"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user