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

DIG-1502: Opa implements user-specific authorizations #55

Merged
merged 9 commits into from
May 1, 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
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
kcranston marked this conversation as resolved.
Show resolved Hide resolved
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