Skip to content

Commit af569d9

Browse files
committed
Built-in support for banning IPs, user agents and email patterns
1 parent 0eb6638 commit af569d9

File tree

8 files changed

+176
-27
lines changed

8 files changed

+176
-27
lines changed

policies/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ OPA_DOCKER_IMAGE := docker.io/openpolicyagent/opa:0.70.0-debug
55
REGAL_DOCKER_IMAGE := ghcr.io/styrainc/regal:0.29.2
66

77
INPUTS := \
8+
common/common.rego \
89
client_registration/client_registration.rego \
910
register/register.rego \
1011
authorization_grant/authorization_grant.rego \

policies/authorization_grant/authorization_grant.rego

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package authorization_grant
55

66
import rego.v1
77

8+
import data.common
9+
810
default allow := false
911

1012
allow if {
@@ -82,3 +84,10 @@ violation contains {"msg": "only one device scope is allowed at a time"} if {
8284
scope_list := split(input.scope, " ")
8385
count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 1
8486
}
87+
88+
violation contains {"msg": sprintf(
89+
"Requester [%s] isn't allowed to do this action",
90+
[common.format_requester(input.requester)],
91+
)} if {
92+
common.requester_banned(input.requester, data.requester)
93+
}

policies/common/common.rego

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package common
2+
3+
import rego.v1
4+
5+
matches_string_constraints(str, constraints) if matches_regexes(str, constraints.regexes)
6+
7+
matches_string_constraints(str, constraints) if matches_substrings(str, constraints.substrings)
8+
9+
matches_string_constraints(str, constraints) if matches_literals(str, constraints.literals)
10+
11+
matches_regexes(str, regexes) if {
12+
some pattern in regexes
13+
regex.match(pattern, str)
14+
}
15+
16+
matches_substrings(str, substrings) if {
17+
some pattern in substrings
18+
contains(str, pattern)
19+
}
20+
21+
matches_literals(str, literals) if {
22+
some literal in literals
23+
str == literal
24+
}
25+
26+
# Normalize an IP address or CIDR to a CIDR
27+
normalize_cidr(ip) := ip if contains(ip, "/")
28+
29+
# If it's an IPv4, append /32
30+
normalize_cidr(ip) := sprintf("%s/32", [ip]) if {
31+
not contains(ip, "/")
32+
not contains(ip, ":")
33+
}
34+
35+
# If it's an IPv6, append /128
36+
normalize_cidr(ip) := sprintf("%s/128", [ip]) if {
37+
not contains(ip, "/")
38+
contains(ip, ":")
39+
}
40+
41+
ip_in_list(ip, list) if {
42+
some cidr in list
43+
net.cidr_contains(normalize_cidr(cidr), ip)
44+
}
45+
46+
mxid(username, server_name) := sprintf("@%s:%s", [username, server_name])
47+
48+
requester_banned(requester, policy) if ip_in_list(requester.ip_address, policy.banned_ips)
49+
50+
requester_banned(requester, policy) if matches_string_constraints(requester.user_agent, policy.banned_user_agents)
51+
52+
format_requester(requester) := "unknown" if {
53+
not requester.ip_address
54+
not requester.user_agent
55+
}
56+
57+
format_requester(requester) := sprintf("%s / %s", [requester.ip_address, requester.user_agent]) if {
58+
requester.ip_address
59+
requester.user_agent
60+
}
61+
62+
format_requester(requester) := sprintf("%s", [requester.ip_address]) if {
63+
requester.ip_address
64+
not requester.user_agent
65+
}
66+
67+
format_requester(requester) := sprintf("%s", [requester.user_agent]) if {
68+
not requester.ip_address
69+
requester.user_agent
70+
}

policies/common/common_test.rego

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package common_test
2+
3+
import data.common
4+
import rego.v1
5+
6+
test_match_literals if {
7+
common.matches_string_constraints("literal", {"literals": ["literal"]})
8+
not common.matches_string_constraints("literal", {"literals": ["lit"]})
9+
}
10+
11+
test_match_substring if {
12+
common.matches_string_constraints("some string", {"substrings": ["str"]})
13+
not common.matches_string_constraints("some string", {"substrings": ["something"]})
14+
}
15+
16+
test_match_regex if {
17+
common.matches_string_constraints("some string", {"regexes": ["^some"]})
18+
not common.matches_string_constraints("some string", {"regexes": ["^string"]})
19+
}
20+
21+
test_ip_in_list if {
22+
common.ip_in_list("192.168.1.1", ["192.168.1.1"])
23+
common.ip_in_list("192.168.1.1", ["192.168.1.0/24"])
24+
common.ip_in_list("::1", ["::1"])
25+
common.ip_in_list("::1", ["::/64"])
26+
not common.ip_in_list("192.168.1.1", ["192.168.1.2/32"])
27+
}
28+
29+
test_requester_banned if {
30+
common.requester_banned(
31+
{"ip_address": "192.168.1.1", "user_agent": "Mozilla/5.0"},
32+
{"banned_ips": ["192.168.1.1"]},
33+
)
34+
}

policies/email/email.rego

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package email
55

66
import rego.v1
77

8+
import data.common
9+
810
default allow := false
911

1012
allow if {
@@ -23,6 +25,16 @@ domain_allowed if {
2325
glob.match(allowed_domain, ["."], domain)
2426
}
2527

28+
# Allow any emails if the data.emails.allowed_addresses is not set
29+
address_allowed if {
30+
not data.emails.allowed_addresses
31+
}
32+
33+
# Allow an email only if its address is in the list of allowed addresses
34+
address_allowed if {
35+
common.matches_string_constraints(input.email, data.emails.allowed_addresses)
36+
}
37+
2638
# METADATA
2739
# entrypoint: true
2840
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
3547
some banned_domain in data.banned_domains
3648
glob.match(banned_domain, ["."], domain)
3749
}
50+
51+
# Deny emails if it's not allowed
52+
violation contains {"code": "email-not-allowed", "msg": "email is not allowed"} if {
53+
not address_allowed
54+
}
55+
56+
# Deny emails which match the email ban list constraint
57+
violation contains {"code": "email-banned", "msg": "email is not allowed"} if {
58+
common.matches_string_constraints(input.email, data.emails.banned_addresses)
59+
}

policies/email/email_test.rego

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,27 @@ test_banned_subdomain if {
2727
with data.allowed_domains as ["*.element.io"]
2828
with data.banned_domains as ["staging.element.io"]
2929
}
30+
31+
test_regex_banned if {
32+
not email.allow with input.email as "[email protected]"
33+
with data.emails.banned_addresses.regexes as ["hello@.*"]
34+
}
35+
36+
test_literal_banned if {
37+
not email.allow with input.email as "[email protected]"
38+
with data.emails.banned_addresses.literals as ["[email protected]"]
39+
}
40+
41+
test_regex_allowed if {
42+
email.allow with input.email as "[email protected]"
43+
with data.emails.allowed_addresses.regexes as ["hello@.*"]
44+
not email.allow with input.email as "[email protected]"
45+
with data.emails.allowed_addresses.regexes as ["hola@.*"]
46+
}
47+
48+
test_literal_allowed if {
49+
email.allow with input.email as "[email protected]"
50+
with data.emails.allowed_addresses.literals as ["[email protected]"]
51+
not email.allow with input.email as "[email protected]"
52+
with data.emails.allowed_addresses.literals as ["[email protected]"]
53+
}

policies/register/register.rego

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package register
55

66
import rego.v1
77

8+
import data.common
89
import data.email as email_policy
910

1011
default allow := false
@@ -13,36 +14,14 @@ allow if {
1314
count(violation) == 0
1415
}
1516

16-
# Normalize an IP address or CIDR to a CIDR
17-
normalize_cidr(ip) := ip if contains(ip, "/")
18-
19-
# If it's an IPv4, append /32
20-
normalize_cidr(ip) := sprintf("%s/32", [ip]) if {
21-
not contains(ip, "/")
22-
not contains(ip, ":")
23-
}
24-
25-
# If it's an IPv6, append /128
26-
normalize_cidr(ip) := sprintf("%s/128", [ip]) if {
27-
not contains(ip, "/")
28-
contains(ip, ":")
29-
}
30-
31-
is_ip_banned(ip) if {
32-
some cidr in data.registration.banned_ips
33-
net.cidr_contains(normalize_cidr(cidr), ip)
34-
}
35-
36-
mxid(username, server_name) := sprintf("@%s:%s", [username, server_name])
37-
3817
# METADATA
3918
# entrypoint: true
4019
violation contains {"field": "username", "code": "username-too-short", "msg": "username too short"} if {
4120
count(input.username) == 0
4221
}
4322

4423
violation contains {"field": "username", "code": "username-too-long", "msg": "username too long"} if {
45-
user_id := mxid(input.username, data.server_name)
24+
user_id := common.mxid(input.username, data.server_name)
4625
count(user_id) > 255
4726
}
4827

@@ -68,8 +47,11 @@ violation contains {"msg": "unknown registration method"} if {
6847
not input.registration_method in ["password", "upstream-oauth2"]
6948
}
7049

71-
violation contains {"msg": "IP address is banned"} if {
72-
is_ip_banned(input.requester.ip_address)
50+
violation contains {"msg": sprintf(
51+
"Requester [%s] isn't allowed to do this action",
52+
[common.format_requester(input.requester)],
53+
)} if {
54+
common.requester_banned(input.requester, data.requester)
7355
}
7456

7557
# Check that we supplied an email for password registration

policies/register/register_test.rego

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,19 @@ test_ip_ban if {
8181
"registration_method": "upstream-oauth2",
8282
"requester": {"ip_address": "1.1.1.1"},
8383
}
84-
with data.registration.banned_ips as ["1.1.1.1"]
84+
with data.requester.banned_ips as ["1.1.1.1"]
8585

8686
not register.allow with input as {
8787
"username": "hello",
8888
"registration_method": "upstream-oauth2",
8989
"requester": {"ip_address": "1.1.1.1"},
9090
}
91-
with data.registration.banned_ips as ["1.0.0.0/8"]
91+
with data.requester.banned_ips as ["1.0.0.0/8"]
92+
93+
not register.allow with input as {
94+
"username": "hello",
95+
"registration_method": "upstream-oauth2",
96+
"requester": {"user_agent": "Evil Client"},
97+
}
98+
with data.requester.banned_user_agents.substrings as ["Evil"]
9299
}

0 commit comments

Comments
 (0)