Skip to content

Commit

Permalink
Add auth module and create_task authz
Browse files Browse the repository at this point in the history
  • Loading branch information
paulineribeyre committed Oct 8, 2024
1 parent 86d5c0b commit 47d4b74
Show file tree
Hide file tree
Showing 11 changed files with 513 additions and 112 deletions.
20 changes: 19 additions & 1 deletion gen3workflow/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from fastapi import FastAPI
import httpx
from importlib.metadata import version
import os

from cdislogging import get_logger
from gen3authz.client.arborist.async_client import ArboristClient

from gen3workflow import logger
from gen3workflow.config import config
Expand All @@ -15,6 +17,8 @@ def get_app(httpx_client=None) -> FastAPI:
config.validate()

debug = config["DEBUG"]
log_level = "debug" if debug else "info"

app = FastAPI(
title="Gen3Workflow",
version=version("gen3workflow"),
Expand All @@ -26,7 +30,21 @@ def get_app(httpx_client=None) -> FastAPI:
app.include_router(ga4gh_tes_router, tags=["GA4GH TES"])

# Following will update logger level, propagate, and handlers
get_logger("gen3workflow", log_level="debug" if debug == True else "info")
get_logger("gen3workflow", log_level=log_level)

logger.info("Initializing Arborist client")
custom_arborist_url = os.environ.get("ARBORIST_URL", config["ARBORIST_URL"])
if custom_arborist_url:
app.arborist_client = ArboristClient(
arborist_base_url=custom_arborist_url,
authz_provider="gen3-workflow",
logger=get_logger("gen3workflow.gen3authz", log_level=log_level),
)
else:
app.arborist_client = ArboristClient(
authz_provider="gen3-workflow",
logger=get_logger("gen3workflow.gen3authz", log_level=log_level),
)

return app

Expand Down
82 changes: 82 additions & 0 deletions gen3workflow/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from authutils.token.fastapi import access_token
from fastapi import HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN

from gen3authz.client.arborist.errors import ArboristError

from gen3workflow import logger


# auto_error=False prevents FastAPI from raising a 403 when the request
# is missing an Authorization header. Instead, we want to return a 401
# to signify that we did not receive valid credentials
bearer = HTTPBearer(auto_error=False)


class Auth:
def __init__(
self,
api_request: Request,
bearer_token: HTTPAuthorizationCredentials = Security(bearer),
):
self.arborist_client = api_request.app.arborist_client
self.bearer_token = bearer_token

async def get_token_claims(self) -> dict:
if not self.bearer_token:
err_msg = "Must provide an access token."
logger.error(err_msg)
raise HTTPException(
HTTP_401_UNAUTHORIZED,
err_msg,
)

try:
token_claims = await access_token(
"user", "openid", audience="openid", purpose="access"
)(self.bearer_token)
except Exception as e:
logger.error(
f"Could not get token claims:\n{e.detail if hasattr(e, 'detail') else e}",
exc_info=True,
)
raise HTTPException(
HTTP_401_UNAUTHORIZED,
"Could not verify, parse, and/or validate scope from provided access token.",
)

return token_claims

async def authorize(
self,
method: str,
resources: list,
throw: bool = True,
) -> bool:
token = (
self.bearer_token.credentials
if self.bearer_token and hasattr(self.bearer_token, "credentials")
else None
)

try:
authorized = await self.arborist_client.auth_request(
token, "gen3-workflow", method, resources
)
except ArboristError as e:
logger.error(f"Error while talking to arborist: {e}")
authorized = False

if not authorized:
logger.error(
f"Authorization error: token must have '{method}' access on {resources} for service 'gen3-workflow'."
)
if throw:
raise HTTPException(
HTTP_403_FORBIDDEN,
"Permission denied",
)

return authorized
3 changes: 3 additions & 0 deletions gen3workflow/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
DEBUG: true
DOCS_URL_PREFIX: /gen3workflow

# override the default Arborist URL; ignored if already set as an environment variable
ARBORIST_URL:

####################
# GA4GH TES #
####################
Expand Down
18 changes: 17 additions & 1 deletion gen3workflow/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os

from jsonschema import validate

from gen3config import Config

from . import logger
Expand All @@ -20,7 +22,21 @@ def validate(self) -> None:
Perform a series of sanity checks on a loaded config.
"""
logger.info("Validating configuration")
# will do more here when there is more config
self.validate_top_level_configs()

def validate_top_level_configs(self):
schema = {
"type": "object",
"additionalProperties": True,
"properties": {
"DEBUG": {"type": "boolean"},
"DOCS_URL_PREFIX": {"type": "string"},
"ARBORIST_URL": {"type": ["string", "null"]},
"TES_SERVER_URL": {"type": "string"},
},
}
validate(instance=self, schema=schema)


config = Gen3WorkflowConfig(DEFAULT_CFG_PATH)
try:
Expand Down
16 changes: 13 additions & 3 deletions gen3workflow/routes/ga4gh_tes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

import json

from fastapi import APIRouter, HTTPException, Request
from starlette.status import HTTP_200_OK
from fastapi import APIRouter, Depends, HTTPException, Request
from starlette.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED

from gen3workflow.auth import Auth
from gen3workflow.config import config


Expand All @@ -35,8 +36,17 @@ async def service_info(request: Request):


@router.post("/tasks", status_code=HTTP_200_OK)
async def create_task(request: Request):
async def create_task(request: Request, auth=Depends(Auth)):
await auth.authorize("create", ["services/workflow/gen3-workflow/task"])
body = await get_request_body(request)

# add the USER_ID tag to the task
if "tags" not in body:
body["tags"] = {}
body["tags"]["USER_ID"] = (await auth.get_token_claims()).get("sub")
if not body["tags"]["USER_ID"]:
raise HTTPException(HTTP_401_UNAUTHORIZED, "No user sub in token")

res = await request.app.async_client.post(
f"{config['TES_SERVER_URL']}/tasks", json=body
)
Expand Down
Loading

0 comments on commit 47d4b74

Please sign in to comment.