policies: split the email & password policies and add jsonschema validation of the input
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# METADATA
|
||||
# schemas:
|
||||
# - input: schema["authorization_grant_input"]
|
||||
package authorization_grant
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# METADATA
|
||||
# schemas:
|
||||
# - input: schema["client_registration_input"]
|
||||
package client_registration
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
35
policies/email.rego
Normal file
35
policies/email.rego
Normal 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
30
policies/password.rego
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
24
policies/schema/authorization_grant_input.json
Normal file
24
policies/schema/authorization_grant_input.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
14
policies/schema/client_registration_input.json
Normal file
14
policies/schema/client_registration_input.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
13
policies/schema/email_input.json
Normal file
13
policies/schema/email_input.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "EmailInput",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
policies/schema/password_input.json
Normal file
13
policies/schema/password_input.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PasswordInput",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
32
policies/schema/register_input.json
Normal file
32
policies/schema/register_input.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user