From e54bd255169d574f8cd211c6fd054737f61d0a55 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 25 Jun 2026 20:28:57 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=9A=A7=20add=20country=20is=20not=20r?= =?UTF-8?q?ule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/db/pg/schema/schema.ts | 11 ++++++++++- server/db/sqlite/schema/schema.ts | 10 +++++++++- server/lib/blueprints/publicResources.ts | 2 +- server/lib/blueprints/resourcePolicies.ts | 3 ++- server/lib/validators.ts | 2 ++ .../PolicyAccessRulesTable.tsx | 19 ++++++++++++++----- .../policy-access-rule-validation.ts | 1 + 8 files changed, 40 insertions(+), 9 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 83e8a488d..3bb70a34f 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2431,6 +2431,7 @@ "noRemoteExitNodesAvailableDescription": "No nodes are available for this organization. Create a node first to use local sites.", "exitNode": "Exit Node", "country": "Country", + "countryIsNot": "Country Is Not", "rulesMatchCountry": "Currently based on source IP", "region": "Region", "selectRegion": "Select region", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 1b48aa520..934eba1f6 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -981,7 +981,15 @@ export const resourcePolicyRules = pgTable("resourcePolicyRules", { priority: integer("priority").notNull(), action: varchar("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(), match: varchar("match") - .$type<"CIDR" | "PATH" | "IP" | "COUNTRY" | "ASN" | "REGION">() + .$type< + | "CIDR" + | "PATH" + | "IP" + | "COUNTRY" + | "COUNTRY_IS_NOT" + | "ASN" + | "REGION" + >() .notNull(), value: varchar("value").notNull() }); @@ -1553,3 +1561,4 @@ export type Label = InferSelectModel; export type ResourcePolicy = InferSelectModel; export type RolePolicy = InferSelectModel; export type UserPolicy = InferSelectModel; +export type ResourcePolicyRule = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 0c4a143f5..76e6d22cf 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1252,7 +1252,15 @@ export const resourcePolicyRules = sqliteTable("resourcePolicyRules", { priority: integer("priority").notNull(), action: text("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(), match: text("match") - .$type<"CIDR" | "PATH" | "IP" | "COUNTRY" | "ASN" | "REGION">() + .$type< + | "CIDR" + | "PATH" + | "IP" + | "COUNTRY" + | "COUNTRY_IS_NOT" + | "ASN" + | "REGION" + >() .notNull(), value: text("value").notNull() }); diff --git a/server/lib/blueprints/publicResources.ts b/server/lib/blueprints/publicResources.ts index 1bbe0a4f1..a6147ea86 100644 --- a/server/lib/blueprints/publicResources.ts +++ b/server/lib/blueprints/publicResources.ts @@ -1321,7 +1321,7 @@ function getRuleAction(input: string) { function getRuleValue(match: string, value: string) { // if the match is a country, uppercase the value - if (match == "COUNTRY") { + if (match === "COUNTRY" || match === "COUNTRY_IS_NOT") { return value.toUpperCase(); } return value; diff --git a/server/lib/blueprints/resourcePolicies.ts b/server/lib/blueprints/resourcePolicies.ts index f8d8d1269..aa5f4bc9b 100644 --- a/server/lib/blueprints/resourcePolicies.ts +++ b/server/lib/blueprints/resourcePolicies.ts @@ -347,12 +347,13 @@ function getRuleAction(input: string): "ACCEPT" | "DROP" | "PASS" { function getRuleMatch( input: string -): "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN" | "REGION" { +): "CIDR" | "IP" | "PATH" | "COUNTRY" | "COUNTRY_IS_NOT" | "ASN" | "REGION" { return input.toUpperCase() as | "CIDR" | "IP" | "PATH" | "COUNTRY" + | "COUNTRY_IS_NOT" | "ASN" | "REGION"; } diff --git a/server/lib/validators.ts b/server/lib/validators.ts index ec19bd852..ff0c5fb3d 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -74,6 +74,7 @@ export const RESOURCE_RULE_MATCH_TYPES = [ "IP", "PATH", "COUNTRY", + "COUNTRY_IS_NOT", "ASN", "REGION" ] as const; @@ -96,6 +97,7 @@ export function getResourceRuleValueValidationError( case "REGION": return isValidRegionId(value) ? null : "Invalid region ID provided"; case "COUNTRY": + case "COUNTRY_IS_NOT": return COUNTRIES.some((country) => country.code === value) ? null : "Invalid country code provided"; diff --git a/src/components/resource-policy/PolicyAccessRulesTable.tsx b/src/components/resource-policy/PolicyAccessRulesTable.tsx index a701b92ff..2818f3652 100644 --- a/src/components/resource-policy/PolicyAccessRulesTable.tsx +++ b/src/components/resource-policy/PolicyAccessRulesTable.tsx @@ -229,6 +229,7 @@ export function PolicyAccessRulesTable({ IP: "IP", CIDR: t("ipAddressRange"), COUNTRY: t("country"), + COUNTRY_IS_NOT: t("countryIsNot"), ASN: "ASN", REGION: t("region") }), @@ -441,13 +442,15 @@ export function PolicyAccessRulesTable({ | "IP" | "PATH" | "COUNTRY" + | "COUNTRY_IS_NOT" | "ASN" | "REGION" ) => updateRule(row.original.ruleId, { match: value, value: - value === "COUNTRY" + value === "COUNTRY" || + value === "COUNTRY_IS_NOT" ? "US" : value === "ASN" ? "AS15169" @@ -469,9 +472,14 @@ export function PolicyAccessRulesTable({ {RuleMatch.CIDR} {isMaxmindAvailable && ( - - {RuleMatch.COUNTRY} - + <> + + {RuleMatch.COUNTRY} + + + {RuleMatch.COUNTRY_IS_NOT} + + )} {includeRegionMatch && isMaxmindAvailable && ( @@ -491,7 +499,8 @@ export function PolicyAccessRulesTable({ accessorKey: "value", header: () => {t("value")}, cell: ({ row }) => - row.original.match === "COUNTRY" ? ( + row.original.match === "COUNTRY" || + row.original.match === "COUNTRY_IS_NOT" ? (