From ad4f1eaa78525d0c4afd4c859ed933b4bbe6f124 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 17 Feb 2025 15:34:46 +0100 Subject: [PATCH] Built-in support for banning IPs, user agents and email patterns --- policies/Makefile | 1 + .../authorization_grant.rego | 9 +++ policies/common/common.rego | 70 +++++++++++++++++++ policies/common/common_test.rego | 34 +++++++++ policies/email/email.rego | 22 ++++++ policies/email/email_test.rego | 24 +++++++ policies/register/register.rego | 32 ++------- policies/register/register_test.rego | 11 ++- 8 files changed, 176 insertions(+), 27 deletions(-) create mode 100644 policies/common/common.rego create mode 100644 policies/common/common_test.rego diff --git a/policies/Makefile b/policies/Makefile index 60f037e48..2659e3843 100644 --- a/policies/Makefile +++ b/policies/Makefile @@ -5,6 +5,7 @@ OPA_DOCKER_IMAGE := docker.io/openpolicyagent/opa:0.70.0-debug REGAL_DOCKER_IMAGE := ghcr.io/styrainc/regal:0.29.2 INPUTS := \ + common/common.rego \ client_registration/client_registration.rego \ register/register.rego \ authorization_grant/authorization_grant.rego \ diff --git a/policies/authorization_grant/authorization_grant.rego b/policies/authorization_grant/authorization_grant.rego index 362ba080e..72fa7ee8b 100644 --- a/policies/authorization_grant/authorization_grant.rego +++ b/policies/authorization_grant/authorization_grant.rego @@ -5,6 +5,8 @@ package authorization_grant import rego.v1 +import data.common + default allow := false allow if { @@ -82,3 +84,10 @@ violation contains {"msg": "only one device scope is allowed at a time"} if { scope_list := split(input.scope, " ") count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 1 } + +violation contains {"msg": sprintf( + "Requester [%s] isn't allowed to do this action", + [common.format_requester(input.requester)], +)} if { + common.requester_banned(input.requester, data.requester) +} diff --git a/policies/common/common.rego b/policies/common/common.rego new file mode 100644 index 000000000..2cbfb469f --- /dev/null +++ b/policies/common/common.rego @@ -0,0 +1,70 @@ +package common + +import rego.v1 + +matches_string_constraints(str, constraints) if matches_regexes(str, constraints.regexes) + +matches_string_constraints(str, constraints) if matches_substrings(str, constraints.substrings) + +matches_string_constraints(str, constraints) if matches_literals(str, constraints.literals) + +matches_regexes(str, regexes) if { + some pattern in regexes + regex.match(pattern, str) +} + +matches_substrings(str, substrings) if { + some pattern in substrings + contains(str, pattern) +} + +matches_literals(str, literals) if { + some literal in literals + str == literal +} + +# Normalize an IP address or CIDR to a CIDR +normalize_cidr(ip) := ip if contains(ip, "/") + +# If it's an IPv4, append /32 +normalize_cidr(ip) := sprintf("%s/32", [ip]) if { + not contains(ip, "/") + not contains(ip, ":") +} + +# If it's an IPv6, append /128 +normalize_cidr(ip) := sprintf("%s/128", [ip]) if { + not contains(ip, "/") + contains(ip, ":") +} + +ip_in_list(ip, list) if { + some cidr in list + net.cidr_contains(normalize_cidr(cidr), ip) +} + +mxid(username, server_name) := sprintf("@%s:%s", [username, server_name]) + +requester_banned(requester, policy) if ip_in_list(requester.ip_address, policy.banned_ips) + +requester_banned(requester, policy) if matches_string_constraints(requester.user_agent, policy.banned_user_agents) + +format_requester(requester) := "unknown" if { + not requester.ip_address + not requester.user_agent +} + +format_requester(requester) := sprintf("%s / %s", [requester.ip_address, requester.user_agent]) if { + requester.ip_address + requester.user_agent +} + +format_requester(requester) := sprintf("%s", [requester.ip_address]) if { + requester.ip_address + not requester.user_agent +} + +format_requester(requester) := sprintf("%s", [requester.user_agent]) if { + not requester.ip_address + requester.user_agent +} diff --git a/policies/common/common_test.rego b/policies/common/common_test.rego new file mode 100644 index 000000000..52b7c0844 --- /dev/null +++ b/policies/common/common_test.rego @@ -0,0 +1,34 @@ +package common_test + +import data.common +import rego.v1 + +test_match_literals if { + common.matches_string_constraints("literal", {"literals": ["literal"]}) + not common.matches_string_constraints("literal", {"literals": ["lit"]}) +} + +test_match_substring if { + common.matches_string_constraints("some string", {"substrings": ["str"]}) + not common.matches_string_constraints("some string", {"substrings": ["something"]}) +} + +test_match_regex if { + common.matches_string_constraints("some string", {"regexes": ["^some"]}) + not common.matches_string_constraints("some string", {"regexes": ["^string"]}) +} + +test_ip_in_list if { + common.ip_in_list("192.168.1.1", ["192.168.1.1"]) + common.ip_in_list("192.168.1.1", ["192.168.1.0/24"]) + common.ip_in_list("::1", ["::1"]) + common.ip_in_list("::1", ["::/64"]) + not common.ip_in_list("192.168.1.1", ["192.168.1.2/32"]) +} + +test_requester_banned if { + common.requester_banned( + {"ip_address": "192.168.1.1", "user_agent": "Mozilla/5.0"}, + {"banned_ips": ["192.168.1.1"]}, + ) +} diff --git a/policies/email/email.rego b/policies/email/email.rego index 24b1d94b4..d9c5eb778 100644 --- a/policies/email/email.rego +++ b/policies/email/email.rego @@ -5,6 +5,8 @@ package email import rego.v1 +import data.common + default allow := false allow if { @@ -23,6 +25,16 @@ domain_allowed if { glob.match(allowed_domain, ["."], domain) } +# Allow any emails if the data.emails.allowed_addresses is not set +address_allowed if { + not data.emails.allowed_addresses +} + +# Allow an email only if its address is in the list of allowed addresses +address_allowed if { + common.matches_string_constraints(input.email, data.emails.allowed_addresses) +} + # METADATA # entrypoint: true violation contains {"code": "email-domain-not-allowed", "msg": "email domain is not allowed"} if { @@ -35,3 +47,13 @@ violation contains {"code": "email-domain-banned", "msg": "email domain is banne some banned_domain in data.banned_domains glob.match(banned_domain, ["."], domain) } + +# Deny emails if it's not allowed +violation contains {"code": "email-not-allowed", "msg": "email is not allowed"} if { + not address_allowed +} + +# Deny emails which match the email ban list constraint +violation contains {"code": "email-banned", "msg": "email is not allowed"} if { + common.matches_string_constraints(input.email, data.emails.banned_addresses) +} diff --git a/policies/email/email_test.rego b/policies/email/email_test.rego index 0adcdfad2..9d3750b56 100644 --- a/policies/email/email_test.rego +++ b/policies/email/email_test.rego @@ -27,3 +27,27 @@ test_banned_subdomain if { with data.allowed_domains as ["*.element.io"] with data.banned_domains as ["staging.element.io"] } + +test_regex_banned if { + not email.allow with input.email as "hello@staging.element.io" + with data.emails.banned_addresses.regexes as ["hello@.*"] +} + +test_literal_banned if { + not email.allow with input.email as "hello@staging.element.io" + with data.emails.banned_addresses.literals as ["hello@staging.element.io"] +} + +test_regex_allowed if { + email.allow with input.email as "hello@staging.element.io" + with data.emails.allowed_addresses.regexes as ["hello@.*"] + not email.allow with input.email as "hello@staging.element.io" + with data.emails.allowed_addresses.regexes as ["hola@.*"] +} + +test_literal_allowed if { + email.allow with input.email as "hello@staging.element.io" + with data.emails.allowed_addresses.literals as ["hello@staging.element.io"] + not email.allow with input.email as "hello@staging.element.io" + with data.emails.allowed_addresses.literals as ["hola@staging.element.io"] +} diff --git a/policies/register/register.rego b/policies/register/register.rego index c3836d170..6189c3926 100644 --- a/policies/register/register.rego +++ b/policies/register/register.rego @@ -5,6 +5,7 @@ package register import rego.v1 +import data.common import data.email as email_policy default allow := false @@ -13,28 +14,6 @@ allow if { count(violation) == 0 } -# Normalize an IP address or CIDR to a CIDR -normalize_cidr(ip) := ip if contains(ip, "/") - -# If it's an IPv4, append /32 -normalize_cidr(ip) := sprintf("%s/32", [ip]) if { - not contains(ip, "/") - not contains(ip, ":") -} - -# If it's an IPv6, append /128 -normalize_cidr(ip) := sprintf("%s/128", [ip]) if { - not contains(ip, "/") - contains(ip, ":") -} - -is_ip_banned(ip) if { - some cidr in data.registration.banned_ips - net.cidr_contains(normalize_cidr(cidr), ip) -} - -mxid(username, server_name) := sprintf("@%s:%s", [username, server_name]) - # METADATA # entrypoint: true violation contains {"field": "username", "code": "username-too-short", "msg": "username too short"} if { @@ -42,7 +21,7 @@ violation contains {"field": "username", "code": "username-too-short", "msg": "u } violation contains {"field": "username", "code": "username-too-long", "msg": "username too long"} if { - user_id := mxid(input.username, data.server_name) + user_id := common.mxid(input.username, data.server_name) count(user_id) > 255 } @@ -68,8 +47,11 @@ violation contains {"msg": "unknown registration method"} if { not input.registration_method in ["password", "upstream-oauth2"] } -violation contains {"msg": "IP address is banned"} if { - is_ip_banned(input.requester.ip_address) +violation contains {"msg": sprintf( + "Requester [%s] isn't allowed to do this action", + [common.format_requester(input.requester)], +)} if { + common.requester_banned(input.requester, data.requester) } # Check that we supplied an email for password registration diff --git a/policies/register/register_test.rego b/policies/register/register_test.rego index cf4324fa2..51105ea39 100644 --- a/policies/register/register_test.rego +++ b/policies/register/register_test.rego @@ -81,12 +81,19 @@ test_ip_ban if { "registration_method": "upstream-oauth2", "requester": {"ip_address": "1.1.1.1"}, } - with data.registration.banned_ips as ["1.1.1.1"] + with data.requester.banned_ips as ["1.1.1.1"] not register.allow with input as { "username": "hello", "registration_method": "upstream-oauth2", "requester": {"ip_address": "1.1.1.1"}, } - with data.registration.banned_ips as ["1.0.0.0/8"] + with data.requester.banned_ips as ["1.0.0.0/8"] + + not register.allow with input as { + "username": "hello", + "registration_method": "upstream-oauth2", + "requester": {"user_agent": "Evil Client"}, + } + with data.requester.banned_user_agents.substrings as ["Evil"] }