Skip to content

Commit

Permalink
Merge pull request #55 from CanDIG/daisieh/users
Browse files Browse the repository at this point in the history
DIG-1502: Opa implements user-specific authorizations
  • Loading branch information
kcranston authored May 1, 2024
2 parents a18d67a + 5cf0874 commit 0e9f58a
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 52 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Github Actions Test

on: [push]

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
env:
CANDIG_URL: "http://localhost"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build Docker image
run: docker image build --build-arg venv_python=${{ matrix.python-version }} --iidfile image.txt .
- name: Test with pytest
run: docker run `cat image.txt`
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
.pytest_cache
*/__pycache__
*/*/__pycache__
permissions_engine/data.json
permissions_engine/data.json
image.txt
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ RUN chmod 755 ./opa

RUN touch /app/initial_setup

ENTRYPOINT ["bash", "/app/entrypoint.sh"]
ENTRYPOINT pytest
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Open Policy Agent for CanDIGv2
CanDIG uses Opa as the policy authorization engine. Its policies are defined in [permissions_engine](permissions_engine)
The User is defined in the jwt presented in the authorization header.

This is the implementation of [OPA](https://www.openpolicyagent.org/) for CanDIGv2. The OPA service provides a unified policy engine across CanDIG services.
Interactions with the IdP are handled by rego code in [idp.rego](permissions_engine/idp.rego). This fetches
the appropriate endpoints from the IdP's `openid_configuration` service, then queries
`introspection` on the token and gets the users `userinfo`. The user is decoded and verified at the `/idp` endpoints.

Opa can be tested as part of the CanDIGv2 stack: from the CanDIGv2 repo directory, run `make test-integration`.
Interactions with Vault are handled by [vault.rego](permissions_engine/vault.rego). Secrets stored in the opa's service store are retrieved here.

Authorization to endpoints in the OPA service itself is defined in [authz.rego](permissions_engine/authz.rego).

* Token-based auth: There are two api tokens defined: the root token allows any path to be accessed, while the service token only allows the `permissions/datasets` and `permissions/allowed` endpoints to be viewed.

* Role-based auth: Roles for the site are defined in the format given in [site_roles.json](defaults/site_roles.json). if the User is defined as a site admin, they are allowed to view any endpoint. Other site-based roles can be similarly defined.

* Endpoint-based auth: Any service can use the `/service/verified` endpoint. Other specific endpoints can be similarly allowed.

* Program-based and user-based authorizations are defined at the `permissions` path: For a given User and the method of accessing a service (method, path), the `/permissions/datasets` endpoint returns the list of programs that user is allowed to access for that method/path, while the `/permissions/allowed` endpoint returns True if either the user is a site admin or the user is allowed to access that method/path. The following two types of authorizations are available:

* Authorizations for roles in particular programs: users defined as team_members for a program are allowed to access the read paths specified in [paths.json](defaults/paths.json), while users defined as program_curators are allowed to access the curate and delete paths. Note: read and curate paths are separately allowed: if a user should be allowed to both read and curate, they should be in both the team_members and program_curators groups. Program authorizations can be created, edited, and deleted through the ingest microservice. Default test examples can be found in [programs.json](defaults/programs.json).

* Users can also be specifically authorized to read data for a particular program through a data access authorization. User Read authorizations can be created, edited, and revoked through the ingest microservice.
5 changes: 5 additions & 0 deletions defaults/paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"/ingest/?.*",
"/ga4gh/drs/v1/?.*",
"/v2/ingest/?.*"
],
"delete": [
"/ingest/?.*",
"/ga4gh/drs/v1/?.*",
"/v2/ingest/?.*"
]
}
}
Expand Down
9 changes: 8 additions & 1 deletion initialize_vault_store.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import os
from authx.auth import set_service_store_secret, add_provider_to_opa, add_program_to_opa
from authx.auth import get_service_store_secret, 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 site_roles.json, paths.json, programs.json
Expand Down Expand Up @@ -43,5 +43,12 @@
print(str(e))
sys.exit(4)

# initialize pending users
response, status_code = get_service_store_secret("opa", key="pending_users")
if status_code == 404:
response, status_code = set_service_store_secret("opa", key="pending_users", value=json.dumps({"pending_users": {}}))
if status_code != 200:
sys.exit(2)

# print(json.dumps(results, indent=4))
sys.exit(0)
19 changes: 0 additions & 19 deletions permissions_engine/README.md

This file was deleted.

42 changes: 32 additions & 10 deletions permissions_engine/permissions.rego
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,43 @@ package permissions
#
import data.idp.valid_token
import data.idp.user_key
import future.keywords.in

#
# what programs are available to this user?
# 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
}

import future.keywords.in
#
# what programs are available to this user?
#

import data.vault.all_programs as all_programs
import data.vault.program_auths as program_auths
import data.vault.user_programs as user_programs

# compile list of programs specifically authorized for the user by DACs and within the authorized time period
user_readable_programs[p["program_id"]] := output {
some p in user_programs
time.parse_ns("2006-01-02", p["start_date"]) <= time.now_ns()
time.parse_ns("2006-01-02", p["end_date"]) >= time.now_ns()
output := p
}

readable_programs[p] {
# compile list of programs that list the user as a team member
team_readable_programs[p] := output {
some p in all_programs
user_key in program_auths[p].team_members
output := program_auths[p].team_members
}

# user can read programs that are either team-readable or user-readable
readable_programs := object.keys(object.union(team_readable_programs, user_readable_programs))

# user can curate programs that list the user as a program curator
curateable_programs[p] {
some p in all_programs
user_key in program_auths[p].program_curators
Expand Down Expand Up @@ -94,6 +116,13 @@ else := curateable_programs
regex.match(paths.curate.post[_], input.body.path) == true
}

else := curateable_programs
{
valid_token
input.body.method = "DELETE"
regex.match(paths.curate.delete[_], input.body.path) == true
}

# convenience path: if a specific program is in the body, allowed = true if that program is in datasets
allowed := true
{
Expand All @@ -104,10 +133,3 @@ 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
}
8 changes: 8 additions & 0 deletions permissions_engine/vault.rego
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ 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]
}

# check to see if the user is authorized for any other programs via DACs
user_auth = http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/opa/users", urlquery.encode(user_key)]), "headers": {"X-Vault-Token": vault_token}, "raise_error": false})

default user_programs = []
user_programs = user_auth.body.data.programs {
user_auth.status_code = 200
}
Loading

0 comments on commit 0e9f58a

Please sign in to comment.