diff --git a/.github/workflows/build-pr-images.yml b/.github/workflows/build-pr-images.yml index f5d54b2..0b33a99 100644 --- a/.github/workflows/build-pr-images.yml +++ b/.github/workflows/build-pr-images.yml @@ -27,15 +27,18 @@ jobs: id: pr run: echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV + - name: Prepare image name + run: | + IMAGE_NAME="ghcr.io/${GITHUB_REPOSITORY,,}/authz:pr-${{ github.event.pull_request.number }}" + echo "IMAGE_NAME=$IMAGE_NAME" >> $GITHUB_ENV + - name: Build Docker image run: | - IMAGE_NAME=ghcr.io/${{ github.repository }}/authz:pr-${PR_NUMBER} echo "Building image: $IMAGE_NAME" docker build -t $IMAGE_NAME . - name: Push Docker image run: | - IMAGE_NAME=ghcr.io/${{ github.repository }}/authz:pr-${PR_NUMBER} echo "Pushing image: $IMAGE_NAME" docker push $IMAGE_NAME @@ -48,8 +51,8 @@ jobs: steps: - name: Delete Docker image from GHCR run: | - IMAGE_NAME=ghcr.io/${{ github.repository }}/authz:pr-${{ github.event.pull_request.number }} + IMAGE_NAME="ghcr.io/${GITHUB_REPOSITORY,,}/authz:pr-${{ github.event.pull_request.number }}" echo "Deleting image: $IMAGE_NAME" curl -X DELETE \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - "https://ghcr.io/v2/${{ github.repository }}/authz/manifests/pr-${{ github.event.pull_request.number }}" \ No newline at end of file + "https://ghcr.io/${GITHUB_REPOSITORY,,}/authz/manifests/pr-${{ github.event.pull_request.number }}" diff --git a/Dockerfile b/Dockerfile index e08dd68..00f4e1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,11 +25,14 @@ WORKDIR /vault/ RUN mkdir -p /vault/config RUN mkdir -p /vault/data RUN chmod 777 /vault/data + +WORKDIR /app/ RUN chown -R pcgl:pcgl /app -USER pcgl +RUN mkdir -p /permissions_engine +RUN chown -R pcgl:pcgl /permissions_engine -WORKDIR /app/ +USER pcgl RUN curl -L -o opa https://openpolicyagent.org/downloads/v1.1.0/opa_linux_amd64_static diff --git a/README.md b/README.md index f96a5d3..d0634f4 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ curl -X "POST" "https://cilogon.org/oauth2/token" \ 3. Use the access token in an Authorization header for any of the API calls: ``` -curl "http://localhost:1235/authz/group/admin" \ +curl "http://localhost:1235/group/admin" \ -H 'Authorization: Bearer ' ``` diff --git a/app/dev.entrypoint.sh b/app/dev.entrypoint.sh new file mode 100644 index 0000000..e2d856a --- /dev/null +++ b/app/dev.entrypoint.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# Container entrypoint for local development with root access to a local Vault server. + +set -Euo pipefail + + +if [[ -f "/app/initial_setup" ]]; then + + cp -r /app/permissions_engine/* /permissions_engine/ + chmod 777 /permissions_engine + + mkdir /app/data + chmod 777 /app/data + + # set up our default values + sed -i s@PCGL_ADMIN_GROUP@$PCGL_ADMIN_GROUP@ /permissions_engine/calculate.rego + + token=$(dd if=/dev/urandom bs=1 count=16 2>/dev/null | base64 | tr -d '\n\r+' | sed s/[^A-Za-z0-9]//g) + echo { \"opa_secret\": \"$token\" } > /permissions_engine/opa_secret.json + # set up vault URL and namespace + sed -i s@VAULT_URL@$VAULT_URL@ /permissions_engine/vault.rego + sed -i s@VAULT_NAMESPACE@$VAULT_NAMESPACE@ /permissions_engine/vault.rego + + echo "initializing stores" + python3 /app/initialize_vault_store.py + if [[ $? -eq 0 ]]; then + rm /app/initial_setup + echo "setup complete" + else + echo "!!!!!! INITIALIZATION FAILED, TRY AGAIN !!!!!!" + fi + +else + sleep 10 + # unseal vault + KEY=$(head -n 2 /app/config/keys.txt | tail -n 1) + echo '{ "key": "'$KEY'" }' > payload.json + curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal + KEY=$(head -n 3 /app/config/keys.txt | tail -n 1) + echo '{ "key": "'$KEY'" }' > payload.json + curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal + KEY=$(head -n 4 /app/config/keys.txt | tail -n 1) + echo '{ "key": "'$KEY'" }' > payload.json + curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal +fi + +# make sure that our vault stores have the latest values +python3 /app/refresh_stores.py + + +# start server +cd /app/src +gunicorn -k uvicorn.workers.UvicornWorker server:app & + + +while [ 0 -eq 0 ] +do + echo "storing vault token" + date + bash /app/renew_token.sh + python3 /app/refresh_stores.py + if [[ $? -eq 0 ]]; then + echo "vault token stored" + sleep 300 + else + echo "vault token not stored" + sleep 30 + fi +done diff --git a/app/entrypoint.sh b/app/entrypoint.sh index 258fd19..0f3a8f4 100644 --- a/app/entrypoint.sh +++ b/app/entrypoint.sh @@ -4,16 +4,21 @@ set -Euo pipefail if [[ -f "/app/initial_setup" ]]; then + + cp -r /app/permissions_engine/* /permissions_engine/ + chmod 777 /permissions_engine + mkdir /app/data chmod 777 /app/data # set up our default values - sed -i s@PCGL_ADMIN_GROUP@$PCGL_ADMIN_GROUP@ /app/permissions_engine/calculate.rego + sed -i s@PCGL_ADMIN_GROUP@$PCGL_ADMIN_GROUP@ /permissions_engine/calculate.rego token=$(dd if=/dev/urandom bs=1 count=16 2>/dev/null | base64 | tr -d '\n\r+' | sed s/[^A-Za-z0-9]//g) - echo { \"opa_secret\": \"$token\" } > /app/permissions_engine/opa_secret.json - # set up vault URL - sed -i s@VAULT_URL@$VAULT_URL@ /app/permissions_engine/vault.rego + echo { \"opa_secret\": \"$token\" } > /permissions_engine/opa_secret.json + # set up vault URL and namespace + sed -i s@VAULT_URL@$VAULT_URL@ /permissions_engine/vault.rego + sed -i s@VAULT_NAMESPACE@$VAULT_NAMESPACE@ /permissions_engine/vault.rego echo "initializing stores" python3 /app/initialize_vault_store.py @@ -23,18 +28,6 @@ if [[ -f "/app/initial_setup" ]]; then else echo "!!!!!! INITIALIZATION FAILED, TRY AGAIN !!!!!!" fi -else - sleep 10 - # unseal vault - KEY=$(head -n 2 /app/config/keys.txt | tail -n 1) - echo '{ "key": "'$KEY'" }' > payload.json - curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal - KEY=$(head -n 3 /app/config/keys.txt | tail -n 1) - echo '{ "key": "'$KEY'" }' > payload.json - curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal - KEY=$(head -n 4 /app/config/keys.txt | tail -n 1) - echo '{ "key": "'$KEY'" }' > payload.json - curl --request POST --data @payload.json http://vault:8200/v1/sys/unseal fi # make sure that our vault stores have the latest values diff --git a/app/permissions_engine/vault.rego b/app/permissions_engine/vault.rego index e712ab0..bdb1544 100644 --- a/app/permissions_engine/vault.rego +++ b/app/permissions_engine/vault.rego @@ -17,20 +17,39 @@ test := "test" if { else := "opa" +ns := "VAULT_NAMESPACE" + +vault_headers := {"X-Vault-Token": vault_token} if { + ns == "" +} + +vault_headers := {"X-Vault-Token": vault_token, "X-Vault-Namespace": ns} if { + ns != "" +} + +vault_service_headers := {"X-Vault-Token": input.token} if { + ns == "" +} + +vault_service_headers := {"X-Vault-Token": input.token, "X-Vault-Namespace": ns} if { + ns != "" +} + + # paths are the paths authorized for methods, used by permissions.rego -paths := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "paths"]), "headers": {"X-Vault-Token": vault_token}}).body.data.paths +paths := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "paths"]), "headers": vault_headers}).body.data.paths # groups are site-wide authorizations, used by permissions.rego and authz.rego -groups := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "groups"]), "headers": {"X-Vault-Token": vault_token}}).body.data +groups := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "groups"]), "headers": vault_headers}).body.data -all_studies := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "studies"]), "headers": {"X-Vault-Token": vault_token}}).body.data.studies +all_studies := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "studies"]), "headers": vault_headers}).body.data.studies study_auths[p] := study if { some p in all_studies - study := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "studies", p]), "headers": {"X-Vault-Token": vault_token}}).body.data[p] + study := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "studies", p]), "headers": vault_headers}).body.data[p] } -user_index := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "users/index"]), "headers": {"X-Vault-Token": vault_token}, "raise_error": false}).body.data +user_index := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "users/index"]), "headers": vault_headers, "raise_error": false}).body.data user_id := user_index[data.idp.user_sub] if { not input.body.user_pcglid @@ -39,7 +58,7 @@ user_id := user_index[data.idp.user_sub] if { else := user_index[input.body.user_pcglid] # check to see if the user is authorized for any other studies via DACs -user_auth := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "users", user_id]), "headers": {"X-Vault-Token": vault_token}, "raise_error": false}) +user_auth := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", test, "users", user_id]), "headers": vault_headers, "raise_error": false}) user_pcglid := user_auth.body.data.pcglid @@ -51,4 +70,4 @@ user_studies := user_auth.body.data.study_authorizations if { default service := "" # if there is a service associated with this token: -service := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/cubbyhole", input.token]), "headers": {"X-Vault-Token": input.token}}).body.data.service +service := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/cubbyhole", input.token]), "headers": vault_service_headers}).body.data.service diff --git a/app/refresh_stores.py b/app/refresh_stores.py index ac0c606..a26b72e 100644 --- a/app/refresh_stores.py +++ b/app/refresh_stores.py @@ -1,26 +1,29 @@ import json import os -from src.auth import get_vault_token_for_service, reload_comanage +from src.auth import get_secret_file_value_or_env, get_vault_token_for_service, reload_comanage import sys import requests - # get the token for the opa store try: - with open("/app/permissions_engine/opa_secret.json") as f: + with open("/permissions_engine/opa_secret.json") as f: opa_json = json.load(f) headers = { "X-Opa": opa_json["opa_secret"], "Content-Type": "application/json; charset=utf-8" } - with open("/home/pcgl/opa-roleid") as f: - role_id = f.read().strip() - payload = f"{{\"token\": \"{get_vault_token_for_service("opa", role_id=role_id)}\"}}" - response = requests.put(url=f"{os.getenv('OPA_URL')}/v1/data/opa_token", headers=headers, data=payload) - print(response.text) - with open("/home/pcgl/test-roleid") as f: - role_id = f.read().strip() - payload = f"{{\"token\": \"{get_vault_token_for_service("test", role_id=role_id)}\"}}" + + # OPA Role-ID + opa_role_id = get_secret_file_value_or_env("/home/pcgl/opa-roleid", "OPA_ROLEID") + payload = f"{{\"token\": \"{get_vault_token_for_service("opa", role_id=opa_role_id)}\"}}" + response = requests.put(url=f"{os.getenv('OPA_URL')}/v1/data/opa_token", headers=headers, data=payload) + print(response.text) + + # TODO: can we remove this? Feel that this should be in tests rather than baked in + # Test Role-ID + test_role_id = get_secret_file_value_or_env("/home/pcgl/test-roleid", "TEST_ROLEID") + if test_role_id: + payload = f"{{\"token\": \"{get_vault_token_for_service("test", role_id=test_role_id)}\"}}" response = requests.put(url=f"{os.getenv('OPA_URL')}/v1/data/test_token", headers=headers, data=payload) print(response.text) diff --git a/app/renew_token.sh b/app/renew_token.sh index 1806770..22ba324 100644 --- a/app/renew_token.sh +++ b/app/renew_token.sh @@ -1,18 +1,16 @@ #!/usr/bin/env bash export VAULT_APPROLE_TOKEN=$(cat /vault/config/approle-token) -export KEY_ROOT=$(tail -n 1 /vault/config/keys.txt) - echo "renewing approle token" -curl --request POST --header "X-Vault-Token: ${VAULT_APPROLE_TOKEN}" $VAULT_URL/v1/auth/token/renew-self > finish.json +curl --request POST \ + --header "X-Vault-Token: ${VAULT_APPROLE_TOKEN}" \ + --header "X-Vault-Namespace: ${VAULT_NAMESPACE}" \ + $VAULT_URL/v1/auth/token/renew-self > finish.json cat finish.json | jq grep "error" finish.json if [[ $? -eq 0 ]]; then - echo "creating approle token" - date - echo "{\"id\": \"${VAULT_APPROLE_TOKEN}\", \"policies\": [\"approle\"], \"periodic\": \"24h\"}" > token.json - curl --request POST --header "X-Vault-Token: ${KEY_ROOT}" --data @token.json $VAULT_URL/v1/auth/token/create/approle > finish.json + echo "Approle token renewal error:" cat finish.json | jq fi rm finish.json diff --git a/app/src/auth.py b/app/src/auth.py index d9c5ae2..387817a 100644 --- a/app/src/auth.py +++ b/app/src/auth.py @@ -9,6 +9,7 @@ ## Env vars for most auth methods: OPA_URL = os.getenv('OPA_URL', "http://localhost:8181") VAULT_URL = os.getenv('VAULT_URL', "http://localhost:8200") +VAULT_NAMESPACE = os.getenv("VAULT_NAMESPACE", "") SERVICE_NAME = os.getenv("SERVICE_NAME") APPROLE_TOKEN_FILE = os.getenv("APPROLE_TOKEN_FILE", "/home/pcgl/approle-token") VERIFY_ROLE_ID_FILE = "/home/pcgl/verify-roleid" @@ -114,6 +115,41 @@ def exchange_refresh_token(refresh_token, client_id=PCGL_CLIENT_ID, client_secre raise AuthzError(response.text) +def get_secret_file_value_or_env(file_path: str, env_var: str) -> str: + """ + Tries to reads and return the value contained at `file_path` if it exists.\n + Otherwise tries to read the value from the `env_var` environment variable if it exists.\n + Raises an `AuthzError` if neither can be found. + """ + if file_path and os.path.exists(file_path): + with open(file_path, 'r') as f: + secret = f.read().strip() + return secret + if env_var: + return os.getenv(env_var) + raise AuthzError(f"Couldn't read the secret from the {file_path} path or the {env_var} env variable.") + + +def get_vault_namespace_header() -> dict: + if VAULT_NAMESPACE is not None and VAULT_NAMESPACE != "": + return { + "X-Vault-Namespace": VAULT_NAMESPACE + } + return {} + + +def get_vault_headers(token: str) -> dict: + """ + Returns authentication headers for Vault API HTTP requests.\n + The `token` argument is passed to the `X-Vault-Token` header.\n + If the `VAULT_NAMESPACE` env var is set, includes its value in the `X-Vault-Namespace` header. + """ + return { + "X-Vault-Token": token, + **get_vault_namespace_header() + } + + ###### # General authorization methods ###### @@ -520,16 +556,14 @@ def get_vault_token_for_service(service=SERVICE_NAME, approle_token=None, role_i raise AuthzError("no SERVICE_NAME specified") # in CanDIGv2 docker stack, approle token should have been passed in if approle_token is None: - with open(APPROLE_TOKEN_FILE) as f: - approle_token = f.read().strip() + approle_token = get_secret_file_value_or_env(APPROLE_TOKEN_FILE, "APPROLE_TOKEN") if approle_token is None: raise AuthzError("no approle token found") # in CanDIGv2 docker stack, roleid should have been passed in if role_id is None: try: - with open(f"/home/pcgl/{service}-roleid") as f: - role_id = f.read().strip() + role_id = get_secret_file_value_or_env(f"/home/pcgl/{service}-roleid", f"{service.upper()}-ROLEID") except Exception as e: raise AuthzError(str(e)) if role_id is None: @@ -538,7 +572,7 @@ def get_vault_token_for_service(service=SERVICE_NAME, approle_token=None, role_i # get the secret_id if secret_id is None: url = f"{VAULT_URL}/v1/auth/approle/role/{service}/secret-id" - headers = { "X-Vault-Token": approle_token } + headers = get_vault_headers(token=approle_token) response = requests.post(url=url, headers=headers) if response.status_code == 200: secret_id = response.json()["data"]["secret_id"] @@ -551,7 +585,7 @@ def get_vault_token_for_service(service=SERVICE_NAME, approle_token=None, role_i "secret_id": secret_id } url = f"{VAULT_URL}/v1/auth/approle/login" - response = requests.post(url, json=data) + response = requests.post(url, json=data, headers=get_vault_namespace_header()) if response.status_code == 200: return response.json()["auth"]["client_token"] else: @@ -573,9 +607,7 @@ def set_service_store_secret(service=SERVICE_NAME, key=None, value=None, role_id if key is None: return {"error": "no key specified"}, 400 - headers = { - "X-Vault-Token": token - } + headers = get_vault_headers(token=token) url = f"{VAULT_URL}/v1/{service}/{key}" if "dict" in str(type(value)): value = json.dumps(value) @@ -599,9 +631,7 @@ def get_service_store_secret(service=SERVICE_NAME, key=None, role_id=None, secre if key is None: return {"error": "no key specified"}, 400 - headers = { - "X-Vault-Token": token - } + headers = get_vault_headers(token) url = f"{VAULT_URL}/v1/{service}/{key}" response = requests.get(url, headers=headers) if response.status_code == 200: @@ -624,9 +654,7 @@ def delete_service_store_secret(service=SERVICE_NAME, key=None, role_id=None, se if key is None: return {"error": "no key specified"}, 400 - headers = { - "X-Vault-Token": token - } + headers = get_vault_headers(token) url = f"{VAULT_URL}/v1/{service}/{key}" response = requests.delete(url, headers=headers) return response.text, response.status_code @@ -639,16 +667,13 @@ def create_service_token(service_uuid): # this will get us a fresh approle token for this service role_id = None try: - with open(VERIFY_ROLE_ID_FILE) as f: - role_id = f.read().strip() + role_id = get_secret_file_value_or_env(VERIFY_ROLE_ID_FILE, "VERIFY_ROLE_ID") except Exception as e: raise AuthzError(str(e)) token = get_vault_token_for_service("verify", role_id=role_id) - headers = { - "X-Vault-Token": token - } + headers = get_vault_headers(token) # create the random service-token: url = f"{VAULT_URL}/v1/cubbyhole/{token}" diff --git a/app/src/authz_openapi.yaml b/app/src/authz_openapi.yaml index 7212f7a..8c887eb 100644 --- a/app/src/authz_openapi.yaml +++ b/app/src/authz_openapi.yaml @@ -4,8 +4,8 @@ info: title: 'PCGL Authorization Service' description: 'API for determining authorization in PCGL' servers: - - url: http://localhost:1235/authz - - url: /authz + - url: http://localhost:1235/ + - url: / paths: /service-info: get: diff --git a/app/src/authz_operations.py b/app/src/authz_operations.py index 0256ce1..2625807 100644 --- a/app/src/authz_operations.py +++ b/app/src/authz_operations.py @@ -37,7 +37,7 @@ def list_group(group_id): if "X-Test-Mode" in connexion.request.headers and connexion.request.headers["X-Test-Mode"] == os.getenv("TEST_KEY"): service = "test" try: - if auth.is_action_allowed_for_study(connexion.request, method="GET", path=f"authz/group/{group_id}", service=service): + if auth.is_action_allowed_for_study(connexion.request, method="GET", path=f"group/{group_id}", service=service): return auth.list_group() return {"error": "User is not authorized to list groups"}, 403 except auth.UserTokenError as e: @@ -179,7 +179,7 @@ def list_study_authorizations(): if "X-Test-Mode" in connexion.request.headers and connexion.request.headers["X-Test-Mode"] == os.getenv("TEST_KEY"): service = "test" try: - if auth.is_action_allowed_for_study(connexion.request, method="GET", path=f"authz/study"): + if auth.is_action_allowed_for_study(connexion.request, method="GET", path="study"): response, status_code = auth.list_studies(service=service) return response, status_code return {"error": "User is not authorized to list studies"}, 403 diff --git a/app/tests/test_authz.py b/app/tests/test_authz.py index 403d0d5..26b9da8 100644 --- a/app/tests/test_authz.py +++ b/app/tests/test_authz.py @@ -87,7 +87,7 @@ def test_add_service(service_uuid): } ] } - response = requests.post(f"{HOST}/authz/service", headers=headers, json=service_body) + response = requests.post(f"{HOST}/service", headers=headers, json=service_body) print(response.text) service_dict = response.json() assert "service_uuid" in service_dict @@ -124,20 +124,20 @@ def test_remove_service(): } # add service - response = requests.post(f"{HOST}/authz/service", headers=headers, json=service_body) + response = requests.post(f"{HOST}/service", headers=headers, json=service_body) print(response.text) service_dict = response.json() assert "service_uuid" in service_dict # list services: should have two, test and remove_me - response = requests.get(f"{HOST}/authz/service", headers=headers) + response = requests.get(f"{HOST}/service", headers=headers) assert len(response.json()) == 2 # remove remove_me - response = requests.delete(f"{HOST}/authz/service/remove_me", headers=headers) + response = requests.delete(f"{HOST}/service/remove_me", headers=headers) # list services: should have one, test - response = requests.get(f"{HOST}/authz/service", headers=headers) + response = requests.get(f"{HOST}/service", headers=headers) print(response.text) assert len(response.json()) == 1 @@ -148,7 +148,7 @@ def get_service_token(service_uuid): "X-Test-Mode": os.getenv("TEST_KEY") } - response = requests.post(f"{HOST}/authz/service/test/verify", headers=headers, json={"service_uuid": service_uuid}) + response = requests.post(f"{HOST}/service/test/verify", headers=headers, json={"service_uuid": service_uuid}) service_token = response.json()["token"] return service_token @@ -160,7 +160,7 @@ def test_service_token(service_uuid): } # if there is no service provided, this won't work - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) assert response.status_code == 400 @@ -168,14 +168,14 @@ def test_service_token(service_uuid): headers["X-Service-Token"] = get_service_token(service_uuid) # if there is a service provided, should work - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) assert response.status_code == 200 # if there is an invalid service provided, this won't work headers["X-Service-Id"] = "testtest" - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) assert response.status_code == 403 @@ -183,7 +183,7 @@ def test_service_token(service_uuid): headers["X-Service-Id"] = "test" headers["X-Service-Token"] = "notatoken" - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) assert response.status_code == 403 @@ -197,11 +197,11 @@ def test_add_studies(studies, service_uuid): headers["X-Service-Token"] = get_service_token(service_uuid) for study in studies: - response = requests.post(f"{HOST}/authz/study", headers=headers, json=studies[study]) + response = requests.post(f"{HOST}/study", headers=headers, json=studies[study]) print(response.text) assert response.status_code == 200 - response = requests.get(f"{HOST}/authz/study", headers=headers) + response = requests.get(f"{HOST}/study", headers=headers) print(response.text) assert len(response.json()) == len(studies) @@ -215,11 +215,11 @@ def test_remove_study(studies, service_uuid): headers["X-Service-Token"] = get_service_token(service_uuid) study = "SYNTHETIC-0" - response = requests.delete(f"{HOST}/authz/study/{study}", headers=headers) + response = requests.delete(f"{HOST}/study/{study}", headers=headers) print(response.text) assert response.status_code == 200 - response = requests.get(f"{HOST}/authz/study", headers=headers) + response = requests.get(f"{HOST}/study", headers=headers) print(response.text) assert len(response.json()) == len(studies) - 1 @@ -245,13 +245,13 @@ def test_get_users(service_uuid, users, user): headers["X-Service-Token"] = get_service_token(service_uuid) # get their own info - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) assert "userinfo" in response.json() assert response.json()["userinfo"]["pcgl_id"] == users[user]["pcglid"] # get a user's info - response = requests.get(f"{HOST}/authz/user/{user}", headers=headers) + response = requests.get(f"{HOST}/user/{user}", headers=headers) print(response.text) assert "userinfo" in response.json() assert response.json()["userinfo"]["pcgl_id"] == users[user]["pcglid"] @@ -309,18 +309,18 @@ def test_add_dacs(user, input, service_uuid): headers["X-Service-Id"] = "test" headers["X-Service-Token"] = get_service_token(service_uuid) - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) pcglid = response.json()["userinfo"]["pcgl_id"] headers["Authorization"] = f"Bearer admin" for study in input: - response = requests.post(f"{HOST}/authz/user/{pcglid}", headers=headers, json=study) + response = requests.post(f"{HOST}/user/{pcglid}", headers=headers, json=study) print(response.text) assert response.status_code == 200 headers["Authorization"] = f"Bearer {user}" - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) for study in input: if TODAY >= date.fromisoformat(study["start_date"]) and TODAY <= date.fromisoformat(study["end_date"]): @@ -363,9 +363,9 @@ def test_user_studies(user, input, expected_result, service_uuid): "studies": expected_result } - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) - response = requests.post(f"{HOST}/authz/allowed", headers=headers, json=body) + response = requests.post(f"{HOST}/allowed", headers=headers, json=body) print(response.text) for i in range(len(expected_result)): assert response.json()[i] == True @@ -380,15 +380,15 @@ def test_remove_dac(service_uuid): headers["X-Service-Id"] = "test" headers["X-Service-Token"] = get_service_token(service_uuid) - response = requests.get(f"{HOST}/authz/user/me", headers=headers) + response = requests.get(f"{HOST}/user/me", headers=headers) print(response.text) pcglid = response.json()["userinfo"]["pcgl_id"] headers["Authorization"] = f"Bearer admin" - response = requests.delete(f"{HOST}/authz/user/{pcglid}/study/SYNTHETIC-1", headers=headers) + response = requests.delete(f"{HOST}/user/{pcglid}/study/SYNTHETIC-1", headers=headers) print(response.text) - response = requests.get(f"{HOST}/authz/user/{pcglid}", headers=headers) + response = requests.get(f"{HOST}/user/{pcglid}", headers=headers) print(response.text) assert "SYNTHETIC-1" not in response.json()["study_authorizations"]["readable_studies"] diff --git a/docker-compose.yml b/docker-compose.yml index 8f9255c..d71d261 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,11 +13,12 @@ services: ports: - 1235:1235 volumes: - - auth-data:/app + - auth-data:/permissions_engine - vault-data:/vault environment: OPA_URL: http://opa:8181 VAULT_URL: http://vault:8200 + VAULT_NAMESPACE: ${VAULT_NAMESPACE} SERVICE_NAME: opa PCGL_USER_KEY: email PCGL_ISSUER: ${PCGL_ISSUER} @@ -33,7 +34,7 @@ services: HOST: http://localhost:1235 entrypoint: - "bash" - - "/app/entrypoint.sh" + - "/app/dev.entrypoint.sh" opa: image: openpolicyagent/opa:1.1.0-static @@ -42,7 +43,7 @@ services: labels: - "pcgl=opa" volumes: - - auth-data:/app + - auth-data:/permissions_engine command: - "run" - "--set=decision_logs.console=true" @@ -51,7 +52,7 @@ services: - "--authorization=basic" - "-s" - "--addr=0.0.0.0:8181" - - "app/permissions_engine/" + - "/permissions_engine/" vault: image: hashicorp/vault:1.13 diff --git a/run.sh b/run.sh index 928c823..d719633 100644 --- a/run.sh +++ b/run.sh @@ -9,11 +9,11 @@ mv tmp/vault/backup.tar.gz tmp/vault/restore.tar.gz bash vault_setup.sh bash opa_setup.sh echo ">> waiting for flask to start" -curl "http://localhost:1235/authz/service-info" +curl "http://localhost:1235/service-info" while [ $? -ne 0 ] do echo "..." sleep 1 - curl "http://localhost:1235/authz/service-info" + curl "http://localhost:1235/service-info" done echo "setup complete" diff --git a/secrets.sh.example b/secrets.sh.example index d32a319..0bd2f59 100644 --- a/secrets.sh.example +++ b/secrets.sh.example @@ -12,3 +12,6 @@ export PCGL_MEMBER_GROUP=CO:members:active # Base URL to serve the Authz API over HTTPS export PCGL_AUTHZ_DOMAIN=https://auth.dev.pcgl.sd4h.ca + +##### Vault variables +export VAULT_NAMESPACE="" # leave empty to use the "root" namespace in local dev/tests