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

v3.0.0: Site roles, program authz, user authz #57

Merged
merged 47 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c3f13a3
add CANDIG_USER_KEY to initialization
daisieh Mar 6, 2024
eb472ce
interpolate VAULT_URL in all regos
daisieh Mar 6, 2024
a1eda73
clarify vault_token
daisieh Mar 6, 2024
a9d71a1
change site_admin to role-based
daisieh Mar 6, 2024
a6c31ce
user_key instead of user or email
daisieh Mar 6, 2024
4164bb9
Merge pull request #51 from CanDIG/daisieh/site-admin
daisieh Mar 8, 2024
7621cd9
change site_admin to role-based
daisieh Mar 6, 2024
2e9e807
move default files out of permissions_engine
daisieh Mar 8, 2024
3c81886
new authx for adding/removing programs
daisieh Mar 8, 2024
8a4de00
load in default programs
daisieh Mar 8, 2024
9d199bd
remove unused bits
daisieh Mar 8, 2024
9cd6908
find programs for the user
daisieh Mar 8, 2024
5153220
boolean allowed
daisieh Mar 11, 2024
7a48d0c
site admins can see all programs
daisieh Mar 13, 2024
1c6c601
oh right, we don't need access anymore
daisieh Mar 25, 2024
9ad39b2
Update README.md
daisieh Mar 25, 2024
3dc9188
Update README.md
daisieh Mar 25, 2024
a4c2e2b
Update requirements.txt
daisieh Mar 25, 2024
e8cd8d6
Update requirements.txt
daisieh Mar 25, 2024
f4e7604
Merge pull request #52 from CanDIG/daisieh/opa-update
daisieh Mar 25, 2024
66448c5
improved healthcheck
daisieh Mar 26, 2024
887d86f
interpolate default users
daisieh Apr 3, 2024
a4b32ed
Merge pull request #53 from CanDIG/daisieh/site-admin
daisieh Apr 9, 2024
0075f01
rename roles to site_roles
daisieh Apr 11, 2024
20b1315
authorize opa_service_token to get site admin
daisieh Apr 11, 2024
3133e57
move all vault accesses to vault.rego
daisieh Apr 13, 2024
81f48ee
actually, site admin w/o root token doesn't need special access
daisieh Apr 16, 2024
8e5fdfc
pick up aud from saved key
daisieh Apr 16, 2024
24950aa
install runtime opa in opa-runner (for testing)
daisieh Apr 16, 2024
bbeaef9
better healthcheck
daisieh Apr 17, 2024
b6cd636
for future use: need to be able to tell if we issued this
daisieh Apr 17, 2024
9fd0462
ha, caught a bug
daisieh Apr 17, 2024
6b7bdcf
set up testing
daisieh Apr 19, 2024
acef8ff
Merge pull request #54 from CanDIG/daisieh/unit-tests
daisieh Apr 26, 2024
58a95a7
initialize pending users
daisieh Apr 13, 2024
2ed64a1
user can read authorized programs
daisieh Apr 13, 2024
4f60556
move site_admin
daisieh Apr 19, 2024
2ba55bc
testing for user auths
daisieh Apr 19, 2024
15a5f64
delete paths should be accessible by curators
daisieh Apr 26, 2024
df90d1b
move bash entrypoint to docker-compose
daisieh Apr 26, 2024
0a57210
test github action
daisieh Apr 26, 2024
ee071fb
only initialize pending_users if it doesn't exist
daisieh Apr 26, 2024
5cf0874
consolidate and update readme
daisieh Apr 29, 2024
a18d67a
update authx to v2.3.0
daisieh Apr 30, 2024
0e9f58a
Merge pull request #55 from CanDIG/daisieh/users
kcranston May 1, 2024
9aba05c
allow service_token to view user_key
daisieh May 1, 2024
a989584
Merge pull request #56 from CanDIG/daisieh/user_id
daisieh May 1, 2024
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
6 changes: 5 additions & 1 deletion 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
kcranston marked this conversation as resolved.
Show resolved Hide resolved

RUN chmod 755 ./opa

RUN touch /app/initial_setup

ENTRYPOINT ["bash", "/app/entrypoint.sh"]
ENTRYPOINT pytest
kcranston marked this conversation as resolved.
Show resolved Hide resolved
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 `pytest etc/tests`.
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.
41 changes: 41 additions & 0 deletions defaults/paths.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"paths": {
"read": {
"get": [
"/v2/discovery/?.*",
"/v2/authorized/?.*",
"/htsget/v1/variants/?.*",
"/htsget/v1/variants/search",
"/htsget/v1/reads/?.*",
"/htsget/v1/samples/?.*",
"/ga4gh/drs/v1/objects/?.*",
"/ga4gh/drs/v1/cohorts/?.*",
"/ga4gh/drs/v1/cohorts/?.*/status",
"/beacon/v2/g_variants/?.*"
],
"post": [
"/htsget/v1/variants/search",
"/htsget/v1/samples",
"/beacon/v2/g_variants"
]
},
"curate": {
"get": [
"/htsget/v1/variants/?.*/index",
"/htsget/v1/variants/?.*/verify",
"/htsget/v1/reads/?.*/index",
"/htsget/v1/reads/?.*/verify"
],
"post": [
"/ingest/?.*",
"/ga4gh/drs/v1/?.*",
"/v2/ingest/?.*"
],
"delete": [
"/ingest/?.*",
"/ga4gh/drs/v1/?.*",
"/v2/ingest/?.*"
]
}
}
}
14 changes: 14 additions & 0 deletions defaults/programs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"SYNTHETIC-1": {
"program_id": "SYNTHETIC-1",
"program_curators": ["USER1"],
"team_members": ["USER1"],
"date_created": "2020-01-01"
},
"SYNTHETIC-2": {
"program_id": "SYNTHETIC-2",
"program_curators": ["USER2"],
"team_members": ["USER2"],
"date_created": "2020-03-01"
}
}
17 changes: 17 additions & 0 deletions defaults/site_roles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"site_roles": {
"admin": [
"SITE_ADMIN_USER"
],
"curator": [
],
"local_team": [
"USER1"
],
"mohccn_network": [
"USER1",
"USER2"
]
}
}

22 changes: 16 additions & 6 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +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
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/OPA_SITE_ADMIN_KEY/$OPA_SITE_ADMIN_KEY/ /app/permissions_engine/idp.rego && sed -i s/OPA_SITE_ADMIN_KEY/$OPA_SITE_ADMIN_KEY/ /app/permissions_engine/authz.rego
# set up our default values
sed -i s/CANDIG_USER_KEY/$CANDIG_USER_KEY/ /app/permissions_engine/idp.rego

OPA_SERVICE_TOKEN=$(cat /run/secrets/opa-service-token)
sed -i s/OPA_SERVICE_TOKEN/$OPA_SERVICE_TOKEN/ /app/permissions_engine/authz.rego
# set up default users in default jsons:
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

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

sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/authz.rego
sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/service.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
37 changes: 3 additions & 34 deletions healthcheck.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,21 @@
import json
import os
import re
import sys
import uuid
from http import HTTPStatus
from pathlib import Path
import datetime
import requests

keycloak_url = os.environ.get('KEYCLOAK_PUBLIC_URL')
opa_url = os.environ.get('OPA_URL')

# Read Docker secrets
with open("/run/secrets/client_secret") as f:
client_secret = f.read().strip()

with open("/run/secrets/password") as f:
password = f.read().strip()

def get_token(username=None, password=None, client_id=None, client_secret=None):
payload = {
"client_id": client_id,
"client_secret": client_secret,
"grant_type": "password",
"username": username,
"password": password,
"scope": "openid",
}
response = requests.post(
f"{keycloak_url}/auth/realms/candig/protocol/openid-connect/token",
data=payload,
)
if response.status_code == 200:
return response.json()["access_token"]

def perform_healthcheck():
auth_token = get_token(username="user2", password=password, client_id="local_candig", client_secret=client_secret)
headers = {"Authorization": f"Bearer {auth_token}"}
url = os.environ.get('OPA_URL')

try:
response = requests.get(url, headers=headers)
response = requests.get(f"{opa_url}/v1/data/service/service-info")
response.raise_for_status()
print("Health check passed!")
return True
except requests.exceptions.RequestException as e:
print(f"Health check failed: {e}")
return False


if __name__ == "__main__":
health_status = perform_healthcheck()
if not health_status:
Expand Down
31 changes: 23 additions & 8 deletions initialize_vault_store.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import json
import os
from authx.auth import set_service_store_secret, add_provider_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 access.json and paths.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 @@ -18,22 +18,37 @@
print(str(e))
sys.exit(1)

with open('/app/permissions_engine/access.json') as f:
with open('/app/defaults/paths.json') as f:
data = f.read()
response, status_code = set_service_store_secret("opa", key="access", value=data)
response, status_code = set_service_store_secret("opa", key="paths", value=data)
if status_code != 200:
sys.exit(2)
sys.exit(3)
results.append(response)

with open('/app/permissions_engine/paths.json') as f:
with open('/app/defaults/site_roles.json') as f:
data = f.read()
response, status_code = set_service_store_secret("opa", key="paths", value=data)
response, status_code = set_service_store_secret("opa", key="site_roles", value=data)
if status_code != 200:
sys.exit(3)
sys.exit(2)
results.append(response)

with open('/app/defaults/programs.json') as f:
programs = json.load(f)
for program in programs:
response, status_code = add_program_to_opa(programs[program])
if status_code != 200:
sys.exit(2)
results.append(response)
except Exception as e:
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)
20 changes: 0 additions & 20 deletions permissions_engine/README.md

This file was deleted.

23 changes: 0 additions & 23 deletions permissions_engine/access.json

This file was deleted.

Loading