Built-in support for banning IPs, user agents and email patterns
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
70
policies/common/common.rego
Normal file
70
policies/common/common.rego
Normal 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
|
||||
}
|
||||
34
policies/common/common_test.rego
Normal file
34
policies/common/common_test.rego
Normal 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"]},
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user