Skip to content

Commit

Permalink
Merge pull request #54 from CanDIG/daisieh/unit-tests
Browse files Browse the repository at this point in the history
DIG-1546: Opa unit tests
  • Loading branch information
daisieh authored Apr 26, 2024
2 parents a4b32ed + 6b7bdcf commit acef8ff
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 77 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ USER candig

WORKDIR /app/

RUN curl -L -o opa https://openpolicyagent.org/downloads/v0.63.0/opa_linux_amd64_static

RUN chmod 755 ./opa

RUN touch /app/initial_setup

ENTRYPOINT ["bash", "/app/entrypoint.sh"]
6 changes: 3 additions & 3 deletions defaults/roles.json → defaults/site_roles.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"roles": {
"site_admin": [
"site_roles": {
"admin": [
"SITE_ADMIN_USER"
],
"site_curator": [
"curator": [
],
"local_team": [
"USER1"
Expand Down
18 changes: 7 additions & 11 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,28 @@
set -Euo pipefail

OPA_ROOT_TOKEN=$(cat /run/secrets/opa-root-token)
OPA_SERVICE_TOKEN=$(cat /run/secrets/opa-service-token)
SITE_ADMIN_USER=$(cat /run/secrets/site_admin_name)
USER1=$(cat /run/secrets/user1_name)
USER2=$(cat /run/secrets/user2_name)

if [[ -f "/app/initial_setup" ]]; then
# set up our default values
sed -i s/CLIENT_ID/$KEYCLOAK_CLIENT_ID/ /app/permissions_engine/idp.rego && sed -i s/CLIENT_ID/$KEYCLOAK_CLIENT_ID/ /app/permissions_engine/authz.rego
sed -i s/CANDIG_USER_KEY/$CANDIG_USER_KEY/ /app/permissions_engine/idp.rego && sed -i s/CANDIG_USER_KEY/$CANDIG_USER_KEY/ /app/permissions_engine/authz.rego
sed -i s/CANDIG_USER_KEY/$CANDIG_USER_KEY/ /app/permissions_engine/idp.rego

# set up default users in default jsons:
sed -i s/SITE_ADMIN_USER/$SITE_ADMIN_USER/ /app/defaults/roles.json
sed -i s/USER1/$USER1/ /app/defaults/roles.json
sed -i s/USER2/$USER2/ /app/defaults/roles.json
sed -i s/SITE_ADMIN_USER/$SITE_ADMIN_USER/ /app/defaults/site_roles.json
sed -i s/USER1/$USER1/ /app/defaults/site_roles.json
sed -i s/USER2/$USER2/ /app/defaults/site_roles.json
sed -i s/SITE_ADMIN_USER/$SITE_ADMIN_USER/ /app/defaults/programs.json
sed -i s/USER1/$USER1/ /app/defaults/programs.json
sed -i s/USER2/$USER2/ /app/defaults/programs.json

OPA_SERVICE_TOKEN=$(cat /run/secrets/opa-service-token)
sed -i s/OPA_SERVICE_TOKEN/$OPA_SERVICE_TOKEN/ /app/permissions_engine/authz.rego
sed -i s/OPA_ROOT_TOKEN/$OPA_ROOT_TOKEN/ /app/permissions_engine/authz.rego

# set up vault URL everywhere
sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/authz.rego
sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/service.rego
sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/idp.rego
sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/permissions.rego
# set up vault URL
sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/vault.rego

echo "initializing stores"
python3 /app/initialize_vault_store.py
Expand Down
8 changes: 1 addition & 7 deletions healthcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@

def perform_healthcheck():
try:
body = {
"input": {
"service": "opa",
"token": "token" # this isn't important; even if it's wrong, it returns 200
}
}
response = requests.post(f"{opa_url}/v1/data/service/verified", json=body)
response = requests.get(f"{opa_url}/v1/data/service/service-info")
response.raise_for_status()
print("Health check passed!")
return True
Expand Down
6 changes: 3 additions & 3 deletions initialize_vault_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from authx.auth import set_service_store_secret, add_provider_to_opa, add_program_to_opa
import sys

## Initializes Vault's opa service store with the information for our IDP and the data in roles.json, paths.json, programs.json
## Initializes Vault's opa service store with the information for our IDP and the data in site_roles.json, paths.json, programs.json

results = []

Expand All @@ -25,9 +25,9 @@
sys.exit(3)
results.append(response)

with open('/app/defaults/roles.json') as f:
with open('/app/defaults/site_roles.json') as f:
data = f.read()
response, status_code = set_service_store_secret("opa", key="roles", value=data)
response, status_code = set_service_store_secret("opa", key="site_roles", value=data)
if status_code != 200:
sys.exit(2)
results.append(response)
Expand Down
40 changes: 11 additions & 29 deletions permissions_engine/authz.rego
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ rights = {
"allowed": {
"path": ["v1", "data", "permissions", "allowed"]
},
"site_admin": {
"path": ["v1", "data", "permissions", "site_admin"]
},
"tokenControlledAccessREMS": {
"path": ["v1", "data", "ga4ghPassport", "tokenControlledAccessREMS"]
}
Expand All @@ -26,7 +29,7 @@ tokens = {
"roles": ["admin"]
},
service_token : {
"roles": ["datasets", "allowed", "tokenControlledAccessREMS"]
"roles": ["datasets", "allowed", "site_admin", "tokenControlledAccessREMS"]
}
}

Expand All @@ -52,35 +55,14 @@ identity_rights[right] { # Right is in the identity_rights set if...
right := rights[role] # Role has rights defined.
}

import data.store_token.token as vault_token

# If user is site_admin, allow always
import future.keywords.in

roles = http.send({"method": "get", "url": "VAULT_URL/v1/opa/roles", "headers": {"X-Vault-Token": vault_token}}).body.data.roles
user_key := decode_verify_token_output[_][2].CANDIG_USER_KEY # get user key from the token payload

allow {
user_key in roles.site_admin
}

keys = http.send({"method": "get", "url": "VAULT_URL/v1/opa/data", "headers": {"X-Vault-Token": vault_token}}).body.data.keys
decode_verify_token_output[issuer] := output {
some i
issuer := keys[i].iss
cert := keys[i].cert
output := io.jwt.decode_verify( # Decode and verify in one-step
input.identity,
{ # With the supplied constraints:
"cert": cert,
"iss": issuer,
"aud": "CLIENT_ID"
}
)
}

# Any service should be able to verify that a service is who it says it is:
allow {
input.path == ["v1", "data", "service", "verified"]
input.method == "POST"
}
}

# Service-info path for healthcheck
allow {
input.path == ["v1", "data", "service", "service-info"]
input.method == "GET"
}
16 changes: 7 additions & 9 deletions permissions_engine/idp.rego
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ package idp
# Store decode and verified token
#

import data.store_token.token as token
keys = http.send({"method": "get", "url": "VAULT_URL/v1/opa/data", "headers": {"X-Vault-Token": token}}).body.data.keys
import data.vault.keys as keys
import future.keywords.in

decode_verify_token_output[issuer] := output {
some i
issuer := keys[i].iss
cert := keys[i].cert
aud := keys[i].aud[_]
output := io.jwt.decode_verify( # Decode and verify in one-step
input.token,
{ # With the supplied constraints:
"cert": cert,
"iss": issuer,
"aud": "CLIENT_ID"
"aud": aud
}
)
}
Expand All @@ -39,11 +40,8 @@ trusted_researcher = true {
}

#
# This user is a site admin if they have the site_admin role
# If the issuer in the token is the same as the first listed in keys, this is issued by the local issuer
#
import future.keywords.in

roles = http.send({"method": "get", "url": "VAULT_URL/v1/opa/roles", "headers": {"X-Vault-Token": token}}).body.data.roles
site_admin = true {
user_key in roles.site_admin
is_local_token = true {
keys[i].iss in object.keys(decode_verify_token_output)
}
40 changes: 31 additions & 9 deletions permissions_engine/permissions.rego
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package permissions
# This is the set of policy definitions for the permissions engine.
#

import data.store_token.token as token
#
# Provided:
# input = {
Expand All @@ -15,19 +14,15 @@ import data.store_token.token as token
#
import data.idp.valid_token
import data.idp.user_key
import data.idp.site_admin

#
# what programs are available to this user?
#

import future.keywords.in

all_programs = http.send({"method": "get", "url": "VAULT_URL/v1/opa/programs", "headers": {"X-Vault-Token": token}}).body.data.programs
program_auths[p] := program {
some p in all_programs
program := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/opa/programs", p]) , "headers": {"X-Vault-Token": token}}).body.data[p]
}
import data.vault.all_programs as all_programs
import data.vault.program_auths as program_auths

readable_programs[p] {
some p in all_programs
Expand All @@ -39,7 +34,26 @@ curateable_programs[p] {
user_key in program_auths[p].program_curators
}

paths = http.send({"method": "get", "url": "VAULT_URL/v1/opa/paths", "headers": {"X-Vault-Token": token}}).body.data.paths
import data.vault.paths as paths

# debugging
valid_token := valid_token
readable_get[p] := output {
some p in paths.read.get
output := regex.match(p, input.body.path)
}
readable_post[p] := output {
some p in paths.read.post
output := regex.match(p, input.body.path)
}
curateable_get[p] := output {
some p in paths.curate.get
output := regex.match(p, input.body.path)
}
curateable_post[p] := output {
some p in paths.curate.post
output := regex.match(p, input.body.path)
}

# which datasets can this user see for this method, path
default datasets = []
Expand Down Expand Up @@ -70,7 +84,7 @@ else := curateable_programs
{
valid_token
input.body.method = "GET"
regex.match(paths.read.get[_], input.body.path) == true
regex.match(paths.curate.get[_], input.body.path) == true
}

else := curateable_programs
Expand All @@ -88,4 +102,12 @@ allowed := true
else := true
{
site_admin
}

#
# This user is a site admin if they have the site_admin role
#
import data.vault.site_roles as site_roles
site_admin = true {
user_key in site_roles.admin
}
9 changes: 4 additions & 5 deletions permissions_engine/service.rego
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ package service
# Verifies that a service is who it says it is
#

import data.store_token.token as token

url = concat("/", ["VAULT_URL/v1", input.service, "token", input.token])
service_token = http.send({"method": "get", "url": url, "headers": {"X-Vault-Token": token}}).body.data.token
import data.vault.service_token as service_token

verified {
service_token == input.token
}
}

service-info := "opa service is running"
26 changes: 26 additions & 0 deletions permissions_engine/vault.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package vault
#
# Obtain secrets from Opa's service secret store in Vault
#

import future.keywords.in
import data.store_token.token as vault_token
import data.idp.user_key

# keys are the IDP keys for verifying JWTs, used by idp.rego and authz.rego
keys = http.send({"method": "get", "url": "VAULT_URL/v1/opa/data", "headers": {"X-Vault-Token": vault_token}}).body.data.keys

# paths are the paths authorized for methods, used by permissions.rego
paths = http.send({"method": "get", "url": "VAULT_URL/v1/opa/paths", "headers": {"X-Vault-Token": vault_token}}).body.data.paths

# service_token gets the token saved for a service, used by service.rego
service_token = http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", input.service, "token", input.token]), "headers": {"X-Vault-Token": vault_token}}).body.data.token

# site_roles are site-wide authorizations, used by permissions.rego and authz.rego
site_roles = http.send({"method": "get", "url": "VAULT_URL/v1/opa/site_roles", "headers": {"X-Vault-Token": vault_token}}).body.data.site_roles

all_programs = http.send({"method": "get", "url": "VAULT_URL/v1/opa/programs", "headers": {"X-Vault-Token": vault_token}}).body.data.programs
program_auths[p] := program {
some p in all_programs
program := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/opa/programs", p]) , "headers": {"X-Vault-Token": vault_token}}).body.data[p]
}
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
requests
jq
candigv2-authx@git+https://github.com/CanDIG/[email protected]
pytest==7.2.0
candigv2-authx@git+https://github.com/CanDIG/candigv2-authx.git@daisieh/aud

Loading

0 comments on commit acef8ff

Please sign in to comment.