Built-in support for banning IPs, user agents and email patterns

This commit is contained in:
Quentin Gliech
2025-02-17 15:34:46 +01:00
parent 3a4aba049c
commit ad4f1eaa78
8 changed files with 176 additions and 27 deletions

View File

@@ -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 \

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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"]},
)
}

View File

@@ -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)
}

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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"]
}