policies: split the email & password policies and add jsonschema validation of the input

This commit is contained in:
Quentin Gliech
2023-08-30 15:01:37 +02:00
parent b783b5457e
commit 3c33923d2c
25 changed files with 547 additions and 164 deletions

View File

@@ -1,6 +1,13 @@
# Set to 1 to run OPA through Docker
DOCKER := 0
OPA_DOCKER_IMAGE := docker.io/openpolicyagent/opa:0.55.0
OPA_DOCKER_IMAGE := docker.io/openpolicyagent/opa:0.55.0-debug
INPUTS := \
client_registration.rego \
register.rego \
authorization_grant.rego \
password.rego \
email.rego
ifeq ($(DOCKER), 0)
OPA := opa
@@ -10,11 +17,13 @@ else
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
policy.wasm: $(INPUTS)
$(OPA_RW) build -t wasm \
-e "client_registration/violation" \
-e "register/violation" \
-e "authorization_grant/violation" \
-e "password/violation" \
-e "email/violation" \
$^
tar xzf bundle.tar.gz /policy.wasm
$(RM) bundle.tar.gz
@@ -26,7 +35,7 @@ fmt:
.PHONY: test
test:
$(OPA) test -v ./*.rego
$(OPA) test --schema ./schema/ -v ./*.rego
.PHONY: coverage
coverage:

View File

@@ -1,3 +1,6 @@
# METADATA
# schemas:
# - input: schema["authorization_grant_input"]
package authorization_grant
import future.keywords.in

View File

@@ -1,3 +1,6 @@
# METADATA
# schemas:
# - input: schema["client_registration_input"]
package client_registration
import future.keywords.in

35
policies/email.rego Normal file
View File

@@ -0,0 +1,35 @@
# METADATA
# schemas:
# - input: schema["email_input"]
package email
import future.keywords.in
default allow := false
allow {
count(violation) == 0
}
# 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.email, "@")
some allowed_domain in data.allowed_domains
glob.match(allowed_domain, ["."], domain)
}
violation[{"msg": "email domain is not allowed"}] {
not email_domain_allowed
}
# Deny emails with their domain in the domains banlist
violation[{"msg": "email domain is banned"}] {
[_, domain] := split(input.email, "@")
some banned_domain in data.banned_domains
glob.match(banned_domain, ["."], domain)
}

30
policies/password.rego Normal file
View File

@@ -0,0 +1,30 @@
# METADATA
# schemas:
# - input: schema["password_input"]
package password
default allow := false
allow {
count(violation) == 0
}
violation[{"msg": msg}] {
count(input.password) < data.passwords.min_length
msg := sprintf("needs to be at least %d characters", [data.passwords.min_length])
}
violation[{"msg": "requires at least one number"}] {
data.passwords.require_number
not regex.match("[0-9]", input.password)
}
violation[{"msg": "requires at least one lowercase letter"}] {
data.passwords.require_lowercase
not regex.match("[a-z]", input.password)
}
violation[{"msg": "requires at least one uppercase letter"}] {
data.passwords.require_uppercase
not regex.match("[A-Z]", input.password)
}

View File

@@ -1,5 +1,11 @@
# METADATA
# schemas:
# - input: schema["register_input"]
package register
import data.email as email_policy
import data.password as password_policy
import future.keywords.in
default allow := false
@@ -9,52 +15,24 @@ allow {
}
violation[{"field": "username", "msg": "username too short"}] {
count(input.user.username) <= 2
count(input.username) <= 2
}
violation[{"field": "username", "msg": "username too long"}] {
count(input.user.username) >= 15
count(input.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[object.union({"field": "password"}, v)] {
# Check if the registration method is password
input.registration_method == "password"
# Get the violation object from the password policy
some v in password_policy.violation
}
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)
# Check if the email is valid using the email policy
# and add the email field to the violation object
violation[object.union({"field": "email"}, v)] {
# Get the violation object from the email policy
some v in email_policy.violation
}

View File

@@ -1,72 +1,85 @@
package register
mock_user := {"username": "hello", "password": "Hunter2", "email": "hello@staging.element.io"}
mock_registration := {
"registration_method": "password",
"username": "hello",
"password": "Hunter2",
"email": "hello@staging.element.io",
}
test_allow_all_domains {
allow with input.user as mock_user
allow with input as mock_registration
}
test_allowed_domain {
allow with input.user as mock_user
allow with input as mock_registration
with data.allowed_domains as ["*.element.io"]
}
test_not_allowed_domain {
not allow with input.user as mock_user
not allow with input as mock_registration
with data.allowed_domains as ["example.com"]
}
test_banned_domain {
not allow with input.user as mock_user
not allow with input as mock_registration
with data.banned_domains as ["*.element.io"]
}
test_banned_subdomain {
not allow with input.user as mock_user
not allow with input as mock_registration
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"}
not allow with input as {"username": "a", "email": "hello@element.io"}
}
test_long_username {
not allow with input.user as {"username": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "email": "hello@element.io"}
not allow with input as {"username": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "email": "hello@element.io"}
}
test_password_require_number {
allow with input.user as mock_user
allow with input as mock_registration
with input.registration_method as "password"
with data.passwords.require_number as true
not allow with input.user as mock_user
with input.user.password as "hunter"
not allow with input as mock_registration
with input.registration_method as "password"
with input.password as "hunter"
with data.passwords.require_number as true
}
test_password_require_lowercase {
allow with input.user as mock_user
allow with input as mock_registration
with input.registration_method as "password"
with data.passwords.require_lowercase as true
not allow with input.user as mock_user
with input.user.password as "HUNTER2"
not allow with input as mock_registration
with input.registration_method as "password"
with input.password as "HUNTER2"
with data.passwords.require_lowercase as true
}
test_password_require_uppercase {
allow with input.user as mock_user
allow with input as mock_registration
with input.registration_method as "password"
with data.passwords.require_uppercase as true
not allow with input.user as mock_user
with input.user.password as "hunter2"
not allow with input as mock_registration
with input.registration_method as "password"
with input.password as "hunter2"
with data.passwords.require_uppercase as true
}
test_password_min_length {
allow with input.user as mock_user
allow with input as mock_registration
with input.registration_method as "password"
with data.passwords.min_length as 6
not allow with input.user as mock_user
with input.user.password as "short"
not allow with input as mock_registration
with input.registration_method as "password"
with input.password as "short"
with data.passwords.min_length as 6
}

View File

@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorizationGrantInput",
"type": "object",
"required": [
"authorization_grant",
"client",
"user"
],
"properties": {
"authorization_grant": {
"type": "object",
"additionalProperties": true
},
"client": {
"type": "object",
"additionalProperties": true
},
"user": {
"type": "object",
"additionalProperties": true
}
}
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ClientRegistrationInput",
"type": "object",
"required": [
"client_metadata"
],
"properties": {
"client_metadata": {
"type": "object",
"additionalProperties": true
}
}
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EmailInput",
"type": "object",
"required": [
"email"
],
"properties": {
"email": {
"type": "string"
}
}
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PasswordInput",
"type": "object",
"required": [
"password"
],
"properties": {
"password": {
"type": "string"
}
}
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RegisterInput",
"oneOf": [
{
"type": "object",
"required": [
"email",
"password",
"registration_method",
"username"
],
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"registration_method": {
"type": "string",
"enum": [
"password"
]
},
"username": {
"type": "string"
}
}
}
]
}