Do not embed the WASM-compiled policies in the binary
This commit is contained in:
3
policies/.gitignore
vendored
Normal file
3
policies/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/policy.wasm
|
||||
/bundle.tar.gz
|
||||
/coverage.json
|
||||
41
policies/Makefile
Normal file
41
policies/Makefile
Normal file
@@ -0,0 +1,41 @@
|
||||
# Set to 1 to run OPA through Docker
|
||||
DOCKER := 0
|
||||
OPA_DOCKER_IMAGE := docker.io/openpolicyagent/opa:0.45.0
|
||||
|
||||
ifeq ($(DOCKER), 0)
|
||||
OPA := opa
|
||||
OPA_RW := opa
|
||||
else
|
||||
OPA := docker run -i -v $(shell pwd):/policies:ro -w /policies --rm $(OPA_DOCKER_IMAGE)
|
||||
OPA_RW := docker run -i -v $(shell pwd):/policies -w /policies --rm $(OPA_DOCKER_IMAGE)
|
||||
endif
|
||||
|
||||
policy.wasm: client_registration.rego register.rego authorization_grant.rego
|
||||
$(OPA_RW) build -t wasm \
|
||||
-e "client_registration/violation" \
|
||||
-e "register/violation" \
|
||||
-e "authorization_grant/violation" \
|
||||
$^
|
||||
tar xzf bundle.tar.gz /policy.wasm
|
||||
$(RM) bundle.tar.gz
|
||||
touch $@
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
$(OPA_RW) fmt -w .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(OPA) test -v ./*.rego
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(OPA) test --coverage ./*.rego | $(OPA) eval --format pretty \
|
||||
--stdin-input \
|
||||
--data util/coveralls.rego \
|
||||
data.coveralls.from_opa > coverage.json
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(OPA) fmt -d --fail .
|
||||
$(OPA) check --strict .
|
||||
38
policies/authorization_grant.rego
Normal file
38
policies/authorization_grant.rego
Normal file
@@ -0,0 +1,38 @@
|
||||
package authorization_grant
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
count(violation) == 0
|
||||
}
|
||||
|
||||
# Special case to make empty scope work
|
||||
allowed_scope("") = true
|
||||
|
||||
allowed_scope("openid") = true
|
||||
|
||||
allowed_scope("email") = true
|
||||
|
||||
allowed_scope("urn:synapse:admin:*") {
|
||||
some user in data.admin_users
|
||||
input.user.username == user
|
||||
}
|
||||
|
||||
allowed_scope(scope) {
|
||||
regex.match("urn:matrix:org.matrix.msc2967.client:device:[A-Za-z0-9-]{10,}", scope)
|
||||
}
|
||||
|
||||
allowed_scope("urn:matrix:org.matrix.msc2967.client:api:*") = true
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
some scope in split(input.authorization_grant.scope, " ")
|
||||
not allowed_scope(scope)
|
||||
msg := sprintf("scope '%s' not allowed", [scope])
|
||||
}
|
||||
|
||||
violation[{"msg": "only one device scope is allowed at a time"}] {
|
||||
scope_list := split(input.authorization_grant.scope, " ")
|
||||
count({key | scope_list[key]; startswith(scope_list[key], "urn:matrix:org.matrix.msc2967.client:device:")}) > 1
|
||||
}
|
||||
63
policies/authorization_grant_test.rego
Normal file
63
policies/authorization_grant_test.rego
Normal file
@@ -0,0 +1,63 @@
|
||||
package authorization_grant
|
||||
|
||||
user := {"username": "john"}
|
||||
|
||||
test_standard_scopes {
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "openid"}
|
||||
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "email"}
|
||||
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "openid email"}
|
||||
|
||||
# Not supported yet
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "phone"}
|
||||
|
||||
# Not supported yet
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "profile"}
|
||||
}
|
||||
|
||||
test_matrix_scopes {
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:api:*"}
|
||||
}
|
||||
|
||||
test_device_scopes {
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01"}
|
||||
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01-asdasdsa1-2313"}
|
||||
|
||||
# Invalid characters
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AABB:CCDDEE"}
|
||||
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AABB*CCDDEE"}
|
||||
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AABB!CCDDEE"}
|
||||
|
||||
# Too short
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:abcd"}
|
||||
|
||||
# Multiple device scope
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01 urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd02"}
|
||||
}
|
||||
|
||||
test_synapse_admin_scopes {
|
||||
allow with input.user as user
|
||||
with data.admin_users as ["john"]
|
||||
with input.authorization_grant as {"scope": "urn:synapse:admin:*"}
|
||||
|
||||
not allow with input.user as user
|
||||
with data.admin_users as []
|
||||
with input.authorization_grant as {"scope": "urn:synapse:admin:*"}
|
||||
}
|
||||
180
policies/client_registration.rego
Normal file
180
policies/client_registration.rego
Normal file
@@ -0,0 +1,180 @@
|
||||
package client_registration
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
count(violation) == 0
|
||||
}
|
||||
|
||||
parse_uri(url) = obj {
|
||||
is_string(url)
|
||||
[matches] := regex.find_all_string_submatch_n("^(?P<scheme>[a-z][a-z0-9+.-]*):(?://(?P<host>((?:(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])|127.0.0.1|0.0.0.0|\\[::1\\])(?::(?P<port>[0-9]+))?))?(?P<path>/[A-Za-z0-9/.-]*)$", url, 1)
|
||||
obj := {"scheme": matches[1], "authority": matches[2], "host": matches[3], "port": matches[4], "path": matches[5]}
|
||||
}
|
||||
|
||||
secure_url(x) {
|
||||
url := parse_uri(x)
|
||||
url.scheme == "https"
|
||||
|
||||
# Disallow localhost variants
|
||||
url.host != "localhost"
|
||||
url.host != "127.0.0.1"
|
||||
url.host != "0.0.0.0"
|
||||
url.host != "[::1]"
|
||||
|
||||
# Must be standard port for HTTPS
|
||||
url.port == ""
|
||||
}
|
||||
|
||||
host_matches_client_uri(x) {
|
||||
client_uri := parse_uri(input.client_metadata.client_uri)
|
||||
uri := parse_uri(x)
|
||||
uri.host == client_uri.host
|
||||
}
|
||||
|
||||
violation[{"msg": "missing client_uri"}] {
|
||||
not data.client_registration.allow_missing_client_uri
|
||||
not input.client_metadata.client_uri
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid client_uri"}] {
|
||||
not data.client_registration.allow_insecure_uris
|
||||
not secure_url(input.client_metadata.client_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid tos_uri"}] {
|
||||
input.client_metadata.tos_uri
|
||||
not data.client_registration.allow_insecure_uris
|
||||
not secure_url(input.client_metadata.tos_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "tos_uri not on the same host as the client_uri"}] {
|
||||
input.client_metadata.tos_uri
|
||||
not data.client_registration.allow_host_mismatch
|
||||
not host_matches_client_uri(input.client_metadata.tos_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid policy_uri"}] {
|
||||
input.client_metadata.policy_uri
|
||||
not data.client_registration.allow_insecure_uris
|
||||
not secure_url(input.client_metadata.policy_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "policy_uri not on the same host as the client_uri"}] {
|
||||
input.client_metadata.policy_uri
|
||||
not data.client_registration.allow_host_mismatch
|
||||
not host_matches_client_uri(input.client_metadata.policy_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid logo_uri"}] {
|
||||
input.client_metadata.logo_uri
|
||||
not data.client_registration.allow_insecure_uris
|
||||
not secure_url(input.client_metadata.logo_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "logo_uri not on the same host as the client_uri"}] {
|
||||
input.client_metadata.logo_uri
|
||||
not data.client_registration.allow_host_mismatch
|
||||
not host_matches_client_uri(input.client_metadata.logo_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "missing contacts"}] {
|
||||
not data.client_registration.allow_missing_contacts
|
||||
not input.client_metadata.contacts
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid contacts"}] {
|
||||
not is_array(input.client_metadata.contacts)
|
||||
}
|
||||
|
||||
violation[{"msg": "empty contacts"}] {
|
||||
count(input.client_metadata.contacts) == 0
|
||||
}
|
||||
|
||||
violation[{"msg": "missing redirect_uris"}] {
|
||||
not input.client_metadata.redirect_uris
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid redirect_uris"}] {
|
||||
not is_array(input.client_metadata.redirect_uris)
|
||||
}
|
||||
|
||||
violation[{"msg": "empty redirect_uris"}] {
|
||||
count(input.client_metadata.redirect_uris) == 0
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid redirect_uri", "redirect_uri": redirect_uri}] {
|
||||
# For 'web' apps, we should verify that redirect_uris are secure
|
||||
input.client_metadata.application_type != "native"
|
||||
some redirect_uri in input.client_metadata.redirect_uris
|
||||
not data.client_registration.allow_host_mismatch
|
||||
not host_matches_client_uri(redirect_uri)
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid redirect_uri"}] {
|
||||
# For 'web' apps, we should verify that redirect_uris are secure
|
||||
input.client_metadata.application_type != "native"
|
||||
some redirect_uri in input.client_metadata.redirect_uris
|
||||
not data.client_registration.allow_insecure_uris
|
||||
not secure_url(redirect_uri)
|
||||
}
|
||||
|
||||
# Used to verify that a reverse-dns formatted scheme is a strict subdomain of
|
||||
# another host.
|
||||
# This is used so a redirect_uri like 'com.example.app:/' works for
|
||||
# a 'client_uri' of 'https://example.com/'
|
||||
reverse_dns_match(host, reverse_dns) {
|
||||
is_string(host)
|
||||
is_string(reverse_dns)
|
||||
|
||||
# Reverse the host
|
||||
host_parts := array.reverse(split(host, "."))
|
||||
|
||||
# Split the already reversed DNS
|
||||
dns_parts := split(reverse_dns, ".")
|
||||
|
||||
# Check that the reverse_dns strictly is a subdomain of the host
|
||||
array.slice(dns_parts, 0, count(host_parts)) == host_parts
|
||||
}
|
||||
|
||||
valid_native_redirector(x) {
|
||||
url := parse_uri(x)
|
||||
is_localhost(url.host)
|
||||
url.scheme == "http"
|
||||
}
|
||||
|
||||
is_localhost(host) {
|
||||
host == "localhost"
|
||||
}
|
||||
|
||||
is_localhost(host) {
|
||||
host == "127.0.0.1"
|
||||
}
|
||||
|
||||
is_localhost(host) {
|
||||
host == "[::1]"
|
||||
}
|
||||
|
||||
# Custom schemes should match the client_uri, reverse-dns style
|
||||
# e.g. io.element.app:/ matches https://app.element.io/
|
||||
valid_native_redirector(x) {
|
||||
url := parse_uri(x)
|
||||
url.scheme != "http"
|
||||
url.scheme != "https"
|
||||
|
||||
# They should have no host/port
|
||||
url.authority == ""
|
||||
client_uri := parse_uri(input.client_metadata.client_uri)
|
||||
reverse_dns_match(client_uri.host, url.scheme)
|
||||
}
|
||||
|
||||
violation[{"msg": "invalid redirect_uri"}] {
|
||||
# For 'native' apps, we need to check that the redirect_uri is either
|
||||
# a custom scheme, or localhost
|
||||
# TODO: this might not be right, because of app-associated domains on mobile?
|
||||
input.client_metadata.application_type == "native"
|
||||
some redirect_uri in input.client_metadata.redirect_uris
|
||||
not valid_native_redirector(redirect_uri)
|
||||
}
|
||||
348
policies/client_registration_test.rego
Normal file
348
policies/client_registration_test.rego
Normal file
@@ -0,0 +1,348 @@
|
||||
package client_registration
|
||||
|
||||
test_valid {
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
}
|
||||
|
||||
test_missing_client_uri {
|
||||
not allow with input.client_metadata as {
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
allow with input.client_metadata as {
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_missing_client_uri as true
|
||||
}
|
||||
|
||||
test_insecure_client_uri {
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "http://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
}
|
||||
|
||||
test_tos_uri {
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"tos_uri": "https://example.com/tos",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"tos_uri": "http://example.com/tos",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"tos_uri": "http://example.com/tos",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_insecure_uris as true
|
||||
|
||||
# Host mistmatch
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"tos_uri": "https://example.org/tos",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Host mistmatch, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"tos_uri": "https://example.org/tos",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_host_mismatch as true
|
||||
}
|
||||
|
||||
test_logo_uri {
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"logo_uri": "https://example.com/logo.png",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"logo_uri": "http://example.com/logo.png",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"logo_uri": "http://example.com/logo.png",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_insecure_uris as true
|
||||
|
||||
# Host mistmatch
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"logo_uri": "https://example.org/logo.png",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Host mistmatch, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"logo_uri": "https://example.org/logo.png",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_host_mismatch as true
|
||||
}
|
||||
|
||||
test_policy_uri {
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"policy_uri": "https://example.com/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"policy_uri": "http://example.com/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"policy_uri": "http://example.com/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_insecure_uris as true
|
||||
|
||||
# Host mistmatch
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"policy_uri": "https://example.org/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Host mistmatch, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"policy_uri": "https://example.org/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_host_mismatch as true
|
||||
}
|
||||
|
||||
test_redirect_uris {
|
||||
# Missing redirect_uris
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# redirect_uris is not an array
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": "https://example.com/callback",
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Empty redirect_uris
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": [],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
}
|
||||
|
||||
test_web_redirect_uri {
|
||||
allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/second/callback", "https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure URL
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Insecure URL, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["http://example.com/callback", "https://example.com/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_insecure_uris as true
|
||||
|
||||
# Host mismatch
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Host mismatch, but allowed by the config
|
||||
allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/second/callback", "https://example.org/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
with data.client_registration.allow_host_mismatch as true
|
||||
|
||||
# No custom scheme allowed
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["com.example.app:/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# localhost not allowed
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["http://locahost:1234/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# localhost not allowed
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["http://127.0.0.1:1234/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# localhost not allowed
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "web",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["http://[::1]:1234/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
}
|
||||
|
||||
test_native_redirect_uri {
|
||||
# This has all the redirect URIs types we're supporting for native apps
|
||||
allow with input.client_metadata as {
|
||||
"application_type": "native",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": [
|
||||
"com.example.app:/callback",
|
||||
"http://localhost/callback",
|
||||
"http://localhost:1234/callback",
|
||||
"http://127.0.0.1/callback",
|
||||
"http://127.0.0.1:1234/callback",
|
||||
"http://[::1]/callback",
|
||||
"http://[::1]:1234/callback",
|
||||
],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# We don't allow HTTP URLs other than localhost
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "native",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "native",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["http://example.com/"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# We don't allow HTTPS on localhost
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "native",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://localhost:1234/"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# Ensure we're not allowing localhost as a prefix
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "native",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["http://localhost.com/"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
|
||||
# For custom schemes, it should match the client_uri hostname
|
||||
not allow with input.client_metadata as {
|
||||
"application_type": "native",
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["org.example.app:/callback"],
|
||||
"contacts": ["contact@example.com"],
|
||||
}
|
||||
}
|
||||
|
||||
test_reverse_dns_match {
|
||||
client_uri := parse_uri("https://element.io/")
|
||||
redirect_uri := parse_uri("io.element.app:/callback")
|
||||
reverse_dns_match(client_uri.host, redirect_uri.scheme)
|
||||
}
|
||||
|
||||
test_contacts {
|
||||
# Missing contacts
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
}
|
||||
|
||||
# Missing contacts, but allowed by config
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
}
|
||||
with data.client_registration.allow_missing_contacts as true
|
||||
|
||||
# contacts is not an array
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": "contact@example.com",
|
||||
}
|
||||
|
||||
# Empty contacts
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"contacts": [],
|
||||
}
|
||||
}
|
||||
60
policies/register.rego
Normal file
60
policies/register.rego
Normal file
@@ -0,0 +1,60 @@
|
||||
package register
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
count(violation) == 0
|
||||
}
|
||||
|
||||
violation[{"field": "username", "msg": "username too short"}] {
|
||||
count(input.user.username) <= 2
|
||||
}
|
||||
|
||||
violation[{"field": "username", "msg": "username too long"}] {
|
||||
count(input.user.username) >= 15
|
||||
}
|
||||
|
||||
violation[{"field": "password", "msg": msg}] {
|
||||
count(input.user.password) < data.passwords.min_length
|
||||
msg := sprintf("needs to be at least %d characters", [data.passwords.min_length])
|
||||
}
|
||||
|
||||
violation[{"field": "password", "msg": "requires at least one number"}] {
|
||||
data.passwords.require_number
|
||||
not regex.match("[0-9]", input.user.password)
|
||||
}
|
||||
|
||||
violation[{"field": "password", "msg": "requires at least one lowercase letter"}] {
|
||||
data.passwords.require_lowercase
|
||||
not regex.match("[a-z]", input.user.password)
|
||||
}
|
||||
|
||||
violation[{"field": "password", "msg": "requires at least one uppercase letter"}] {
|
||||
data.passwords.require_uppercase
|
||||
not regex.match("[A-Z]", input.user.password)
|
||||
}
|
||||
|
||||
# Allow any domains if the data.allowed_domains array is not set
|
||||
email_domain_allowed {
|
||||
not data.allowed_domains
|
||||
}
|
||||
|
||||
# Allow an email only if its domain is in the list of allowed domains
|
||||
email_domain_allowed {
|
||||
[_, domain] := split(input.user.email, "@")
|
||||
some allowed_domain in data.allowed_domains
|
||||
glob.match(allowed_domain, ["."], domain)
|
||||
}
|
||||
|
||||
violation[{"field": "email", "msg": "email domain not allowed"}] {
|
||||
not email_domain_allowed
|
||||
}
|
||||
|
||||
# Deny emails with their domain in the domains banlist
|
||||
violation[{"field": "email", "msg": "email domain not allowed"}] {
|
||||
[_, domain] := split(input.user.email, "@")
|
||||
some banned_domain in data.banned_domains
|
||||
glob.match(banned_domain, ["."], domain)
|
||||
}
|
||||
72
policies/register_test.rego
Normal file
72
policies/register_test.rego
Normal file
@@ -0,0 +1,72 @@
|
||||
package register
|
||||
|
||||
mock_user := {"username": "hello", "password": "Hunter2", "email": "hello@staging.element.io"}
|
||||
|
||||
test_allow_all_domains {
|
||||
allow with input.user as mock_user
|
||||
}
|
||||
|
||||
test_allowed_domain {
|
||||
allow with input.user as mock_user
|
||||
with data.allowed_domains as ["*.element.io"]
|
||||
}
|
||||
|
||||
test_not_allowed_domain {
|
||||
not allow with input.user as mock_user
|
||||
with data.allowed_domains as ["example.com"]
|
||||
}
|
||||
|
||||
test_banned_domain {
|
||||
not allow with input.user as mock_user
|
||||
with data.banned_domains as ["*.element.io"]
|
||||
}
|
||||
|
||||
test_banned_subdomain {
|
||||
not allow with input.user as mock_user
|
||||
with data.allowed_domains as ["*.element.io"]
|
||||
with data.banned_domains as ["staging.element.io"]
|
||||
}
|
||||
|
||||
test_short_username {
|
||||
not allow with input.user as {"username": "a", "email": "hello@element.io"}
|
||||
}
|
||||
|
||||
test_long_username {
|
||||
not allow with input.user as {"username": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "email": "hello@element.io"}
|
||||
}
|
||||
|
||||
test_password_require_number {
|
||||
allow with input.user as mock_user
|
||||
with data.passwords.require_number as true
|
||||
|
||||
not allow with input.user as mock_user
|
||||
with input.user.password as "hunter"
|
||||
with data.passwords.require_number as true
|
||||
}
|
||||
|
||||
test_password_require_lowercase {
|
||||
allow with input.user as mock_user
|
||||
with data.passwords.require_lowercase as true
|
||||
|
||||
not allow with input.user as mock_user
|
||||
with input.user.password as "HUNTER2"
|
||||
with data.passwords.require_lowercase as true
|
||||
}
|
||||
|
||||
test_password_require_uppercase {
|
||||
allow with input.user as mock_user
|
||||
with data.passwords.require_uppercase as true
|
||||
|
||||
not allow with input.user as mock_user
|
||||
with input.user.password as "hunter2"
|
||||
with data.passwords.require_uppercase as true
|
||||
}
|
||||
|
||||
test_password_min_length {
|
||||
allow with input.user as mock_user
|
||||
with data.passwords.min_length as 6
|
||||
|
||||
not allow with input.user as mock_user
|
||||
with input.user.password as "short"
|
||||
with data.passwords.min_length as 6
|
||||
}
|
||||
51
policies/util/coveralls.rego
Normal file
51
policies/util/coveralls.rego
Normal file
@@ -0,0 +1,51 @@
|
||||
package coveralls
|
||||
|
||||
import future.keywords
|
||||
|
||||
from_opa := {"source_files": coverage}
|
||||
|
||||
coverage contains obj if {
|
||||
some file, report in input.files
|
||||
obj := {"name": file, "coverage": to_lines(report)}
|
||||
}
|
||||
|
||||
covered_map(report) = cm if {
|
||||
covered := object.get(report, "covered", [])
|
||||
cm := {line: 1 |
|
||||
some item in covered
|
||||
some line in numbers.range(item.start.row, item.end.row)
|
||||
}
|
||||
}
|
||||
|
||||
not_covered_map(report) = ncm if {
|
||||
not_covered := object.get(report, "not_covered", [])
|
||||
ncm := {line: 0 |
|
||||
some item in not_covered
|
||||
some line in numbers.range(item.start.row, item.end.row)
|
||||
}
|
||||
}
|
||||
|
||||
to_lines(report) = lines if {
|
||||
cm := covered_map(report)
|
||||
ncm := not_covered_map(report)
|
||||
keys := sort([line | some line, _ in object.union(cm, ncm)])
|
||||
last := keys[count(keys) - 1]
|
||||
|
||||
lines := [value |
|
||||
some i in numbers.range(1, last)
|
||||
value := to_value(cm, ncm, i)
|
||||
]
|
||||
}
|
||||
|
||||
to_value(cm, _, line) = 1 if {
|
||||
cm[line]
|
||||
}
|
||||
|
||||
to_value(_, ncm, line) = 0 if {
|
||||
ncm[line]
|
||||
}
|
||||
|
||||
to_value(cm, ncm, line) = null if {
|
||||
not cm[line]
|
||||
not ncm[line]
|
||||
}
|
||||
Reference in New Issue
Block a user