Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup Regal to lint policies and clean them up #3717

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ jobs:
- name: Setup OPA
uses: open-policy-agent/[email protected]
with:
version: 0.64.1
version: 0.70.0

- name: Setup Regal
uses: StyraInc/setup-regal@v1
with:
version: 0.29.2

- name: Lint policies
working-directory: ./policies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Setup OPA
uses: open-policy-agent/[email protected]
with:
version: 0.64.1
version: 0.70.0

- name: Run OPA tests with coverage
working-directory: ./policies
Expand Down
6 changes: 6 additions & 0 deletions policies/.regal/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
rules:
style:
external-reference:
level: ignore
line-length:
level: ignore
23 changes: 14 additions & 9 deletions policies/Makefile
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
# Set to 1 to run OPA through Docker
DOCKER := 0
PODMAN := 0
OPA_DOCKER_IMAGE := docker.io/openpolicyagent/opa:0.64.1-debug
OPA_DOCKER_IMAGE := docker.io/openpolicyagent/opa:0.70.0-debug
REGAL_DOCKER_IMAGE := ghcr.io/styrainc/regal:0.29.2

INPUTS := \
client_registration.rego \
register.rego \
authorization_grant.rego \
email.rego
client_registration/client_registration.rego \
register/register.rego \
authorization_grant/authorization_grant.rego \
email/email.rego

ifeq ($(DOCKER), 1)
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)
REGAL := docker run -i -v $(shell pwd):/policies:ro -w /policies --rm $(REGAL_DOCKER_IMAGE)
else ifeq ($(PODMAN), 1)
# When running rootless, the volume directory may need to be given global write permissions on the host
OPA := podman run -i -v $(shell pwd):/policies:ro:Z -w /policies --rm $(OPA_DOCKER_IMAGE)
OPA_RW := podman run -i -v $(shell pwd):/policies:Z -w /policies --rm $(OPA_DOCKER_IMAGE)
REGAL := podman run -i -v $(shell pwd):/policies:ro:Z -w /policies --rm $(REGAL_DOCKER_IMAGE)
else
OPA := opa
OPA_RW := opa
REGAL := regal
endif

policy.wasm: $(INPUTS)
Expand All @@ -38,16 +42,17 @@ fmt:

.PHONY: test
test:
$(OPA) test --schema ./schema/ -v ./*.rego
$(OPA) test --schema ./schema/ --ignore schema -v ./

.PHONY: coverage
coverage:
$(OPA) test --coverage ./*.rego | $(OPA) eval --format pretty \
$(OPA) test --coverage --schema ./schema/ --ignore schema ./ | $(OPA) eval --format pretty \
--stdin-input \
--data util/coveralls.rego \
data.coveralls.from_opa > coverage.json

.PHONY: lint
lint:
$(OPA) fmt -d --fail ./*.rego util/*.rego
$(OPA) check --strict --schema schema/ ./*.rego util/*.rego
$(OPA) fmt -d --fail .
$(OPA) check --strict --schema schema/ --ignore schema .
$(REGAL) lint .
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,39 @@
# - input: schema["authorization_grant_input"]
package authorization_grant

import future.keywords.in
import rego.v1

default allow := false

allow {
allow if {
count(violation) == 0
}

# Users can request admin scopes if either:
# 1. They are in the admin_users list
can_request_admin(user) {
can_request_admin(user) if {
some admin_user in data.admin_users
user.username == admin_user
}

# 2. They have the can_request_admin flag set to true
can_request_admin(user) {
can_request_admin(user) if {
user.can_request_admin
}

interactive_grant_type("authorization_code") = true
interactive_grant_type("authorization_code") := true

interactive_grant_type("urn:ietf:params:oauth:grant-type:device_code") = true
interactive_grant_type("urn:ietf:params:oauth:grant-type:device_code") := true

# Special case to make empty scope work
allowed_scope("") = true
allowed_scope("") := true

allowed_scope("openid") = true
allowed_scope("openid") := true

allowed_scope("email") = true
allowed_scope("email") := true

# This grants access to Synapse's admin API endpoints
allowed_scope("urn:synapse:admin:*") {
allowed_scope("urn:synapse:admin:*") if {
# Synapse doesn't support user-less tokens yet, so access to the admin API
# can only be used with an authorization_code grant or a device code grant
# as the user is present
Expand All @@ -44,39 +44,41 @@ allowed_scope("urn:synapse:admin:*") {
}

# This grants access to the /graphql API endpoint
allowed_scope("urn:mas:graphql:*") = true
allowed_scope("urn:mas:graphql:*") := true

# This makes it possible to query and do anything in the GraphQL API as an admin
allowed_scope("urn:mas:admin") {
allowed_scope("urn:mas:admin") if {
interactive_grant_type(input.grant_type)
can_request_admin(input.user)
}

# This makes it possible to get the admin scope for clients that are allowed
allowed_scope("urn:mas:admin") {
allowed_scope("urn:mas:admin") if {
input.grant_type == "client_credentials"
some client in data.admin_clients
input.client.id == client
}

allowed_scope(scope) {
allowed_scope(scope) if {
# Grant access to the C-S API only if there is a user
interactive_grant_type(input.grant_type)
regex.match("^urn:matrix:org.matrix.msc2967.client:device:[A-Za-z0-9._~!$&'()*+,;=:@/-]{10,}$", 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:*") {
allowed_scope("urn:matrix:org.matrix.msc2967.client:api:*") if {
# Grant access to the C-S API only if there is a user
interactive_grant_type(input.grant_type)
}

violation[{"msg": msg}] {
# METADATA
# entrypoint: true
violation contains {"msg": msg} if {
some scope in split(input.scope, " ")
not allowed_scope(scope)
msg := sprintf("scope '%s' not allowed", [scope])
}

violation[{"msg": "only one device scope is allowed at a time"}] {
violation contains {"msg": "only one device scope is allowed at a time"} if {
scope_list := split(input.scope, " ")
count({key | scope_list[key]; startswith(scope_list[key], "urn:matrix:org.matrix.msc2967.client:device:")}) > 1
count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 1
}
Original file line number Diff line number Diff line change
@@ -1,155 +1,134 @@
package authorization_grant
package authorization_grant_test

import data.authorization_grant
import rego.v1

user := {"username": "john"}

client := {"client_id": "client"}

test_standard_scopes {
allow with input.user as user
test_standard_scopes if {
authorization_grant.allow with input.user as user
with input.client as client
with input.scope as ""

allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with input.scope as "openid"

allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with input.scope as "email"

allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with input.scope as "openid email"

# Not supported yet
not allow with input.user as user
not authorization_grant.allow with input.user as user
with input.client as client
with input.scope as "phone"

# Not supported yet
not allow with input.user as user
not authorization_grant.allow with input.user as user
with input.client as client
with input.scope as "profile"
}

test_matrix_scopes {
allow with input.user as user
test_matrix_scopes if {
authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "authorization_code"
with input.scope as "urn:matrix:org.matrix.msc2967.client:api:*"

allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code"
with input.scope as "urn:matrix:org.matrix.msc2967.client:api:*"

not allow with input.user as user
not authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "client_credentials"
with input.scope as "urn:matrix:org.matrix.msc2967.client:api:*"
}

test_device_scopes {
allow with input.user as user
test_device_scopes if {
authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "authorization_code"
with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01"

allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "authorization_code"
with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01-asdasdsa1-2313"

# Too short
not allow with input.user as user
not authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "authorization_code"
with input.scope as "urn:matrix:org.matrix.msc2967.client:device:abcd"

# Multiple device scope
not allow with input.user as user
not authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "authorization_code"
with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01 urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd02"

# Allowed with the device code grant
allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code"
with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01"

# Not allowed for the client credentials grant
not allow with input.client as client
# Not authorization_grant.allowed for the client credentials grant
not authorization_grant.allow with input.client as client
with input.grant_type as "client_credentials"
with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01"
}

test_synapse_admin_scopes {
allow with input.user as user
with input.client as client
with data.admin_users as ["john"]
with input.grant_type as "authorization_code"
with input.scope as "urn:synapse:admin:*"
test_synapse_admin_scopes if {
some grant_type in ["authorization_code", "urn:ietf:params:oauth:grant-type:device_code"]

allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with data.admin_users as ["john"]
with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code"
with input.scope as "urn:synapse:admin:*"

not allow with input.user as user
with input.client as client
with data.admin_users as []
with input.grant_type as "authorization_code"
with input.scope as "urn:synapse:admin:*"

not allow with input.user as user
with input.client as client
with data.admin_users as []
with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code"
with input.grant_type as grant_type
with input.scope as "urn:synapse:admin:*"

allow with input.user as user
with input.user.can_request_admin as true
not authorization_grant.allow with input.user as user
with input.client as client
with data.admin_users as []
with input.grant_type as "authorization_code"
with input.grant_type as grant_type
with input.scope as "urn:synapse:admin:*"

allow with input.user as user
authorization_grant.allow with input.user as user
with input.user.can_request_admin as true
with input.client as client
with data.admin_users as []
with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code"
with input.grant_type as grant_type
with input.scope as "urn:synapse:admin:*"

not allow with input.user as user
not authorization_grant.allow with input.user as user
with input.user.can_request_admin as false
with input.client as client
with data.admin_users as []
with input.grant_type as "authorization_code"
with input.scope as "urn:synapse:admin:*"

not allow with input.user as user
with input.user.can_request_admin as false
with input.client as client
with data.admin_users as []
with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code"
with input.grant_type as grant_type
with input.scope as "urn:synapse:admin:*"
}

test_mas_scopes {
allow with input.user as user
test_mas_scopes if {
authorization_grant.allow with input.user as user
with input.client as client
with input.scope as "urn:mas:graphql:*"

allow with input.user as user
authorization_grant.allow with input.user as user
with input.client as client
with data.admin_users as ["john"]
with input.grant_type as "authorization_code"
with input.scope as "urn:mas:admin"

not allow with input.user as user
not authorization_grant.allow with input.user as user
with input.client as client
with data.admin_users as []
with input.grant_type as "authorization_code"
Expand Down
Loading
Loading