Skip to content

Commit

Permalink
fix: auhz
Browse files Browse the repository at this point in the history
  • Loading branch information
majkshkurti committed Sep 17, 2024
1 parent 83a0ff5 commit 76500b9
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 69 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ morecantile = "^2.1.4"
cppimport = "^21.3.7"
pandas = "^2.0.2"
pytest = "^7.1.1"
sentry-sdk = "^1.5.0"
alembic_utils = "^0.7.4"
rich = "^11.0.0"
sqlmodel = "^0.0.8"
Expand Down Expand Up @@ -96,6 +95,7 @@ pymgl = [
matplotlib = "^3.8.2"
cairosvg = "^2.7.1"

sentry-sdk = {extras = ["fastapi"], version = "^2.14.0"}
[tool.poetry.group.dev.dependencies]
sqlalchemy-stubs = "^0.3"
debugpy = "^1.4.1"
Expand Down
2 changes: 2 additions & 0 deletions src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def goat_routing_authorization(
return None

SAMPLE_AUTHORIZATION = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0OG80Z1JXelh3YXBTY3NTdHdTMXZvREFJRlNOa0NtSVFpaDhzcEJTc2kwIn0.eyJleHAiOjE2OTEwMDQ1NTYsImlhdCI6MTY5MTAwNDQ5NiwiYXV0aF90aW1lIjoxNjkxMDAyNjIzLCJqdGkiOiI1MjBiN2RhNC0xYmM0LTRiM2QtODY2ZC00NDU0ODY2YThiYjIiLCJpc3MiOiJodHRwczovL2Rldi5hdXRoLnBsYW40YmV0dGVyLmRlL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiI3NDRlNGZkMS02ODVjLTQ5NWMtOGIwMi1lZmViY2U4NzUzNTkiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzZWN1cml0eS1hZG1pbi1jb25zb2xlIiwibm9uY2UiOiJjNGIzMDQ3Yi0xODVmLTQyOWEtOGZlNS1lNDliNTVhMzE3MzIiLCJzZXNzaW9uX3N0YXRlIjoiMzk5ZTc2NWMtYjM1MC00NDEwLTg4YTMtYjU5NDIyMmJkZDlhIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL2Rldi5hdXRoLnBsYW40YmV0dGVyLmRlIl0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiIzOTllNzY1Yy1iMzUwLTQ0MTAtODhhMy1iNTk0MjIyYmRkOWEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InA0YiJ9.mjywr9Dv19egsXwM1fK6g3sZ0trk87X0tEfK7oOizuBuCdkr6PZN1Eg58FCdjIgEBXqjltOWV43UIkXde4iPVa-KU5Q34Qjv6w0STa3Aq9vFbaUfSm_690qCdr8XSKMJUWQXWYwD2cjck5UCqf7-QqsF2Ab56i40_CJLZkJOi25WKIC855qPDi8BkJgh5eWoxobdyCbwJMEeoM-3QnxY5ikib5a2_AASEN3_5MYmT6-fvpW2t-MS6u4vtcG-WfqriK8YNoGPS2a1pFjLqQLHkM__j0O_t4wXP56x9yjkUdHCXqVcSlDvZYNWrv5CLqecqjOoliNMs6RTu9gV0Gr-cA"
KEYCLOAK_SERVER_URL: Optional[str] = "http://auth-keycloak:8080"
REALM_NAME: Optional[str] = "p4b"
CELERY_TASK_TIME_LIMIT: Optional[int] = 60 # seconds
RUN_AS_BACKGROUND_TASK: Optional[bool] = True
MAX_NUMBER_PARALLEL_JOBS: Optional[int] = 6
Expand Down
105 changes: 105 additions & 0 deletions src/deps/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import requests
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
from jose import JOSEError, jwt
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from src.core.config import settings
from src.endpoints.deps import get_db

auth_key = None
try:
ISSUER_URL = f"{settings.KEYCLOAK_SERVER_URL}/realms/{settings.REALM_NAME}"

_auth_server_public_key = requests.get(ISSUER_URL).json().get("public_key")
auth_key = (
"-----BEGIN PUBLIC KEY-----\n"
+ _auth_server_public_key
+ "\n-----END PUBLIC KEY-----"
) # noqa: E501
except Exception:
print("Error getting public key from Keycloak")

oauth2_scheme = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V2_STR}/auth/access-token",
)


def decode_token(token: str):
"""
Decodes a JWT token.
"""
user_token = jwt.decode(
token,
key=auth_key,
options={
"verify_signature": True,
"verify_aud": False,
"verify_iss": ISSUER_URL,
},
)

return user_token


async def auth(token: str = Depends(oauth2_scheme)) -> str:
try:
decode_token(token)
except JOSEError as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))

return token


def user_token(token: str = Depends(auth)) -> dict:
payload = decode_token(token)
return payload


def is_superuser(user_token: dict = Depends(user_token), throw_error: bool = True):
is_superuser = False
if user_token["realm_access"] and user_token["realm_access"]["roles"]:
is_superuser = "superuser" in user_token["realm_access"]["roles"]

if not is_superuser and throw_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized"
)

return is_superuser


def clean_path(path: str) -> str:
return path.replace(settings.API_V2_STR + "/", "")


async def auth_z(
request: Request,
user_token: dict = Depends(user_token),
async_session: AsyncSession = Depends(get_db),
) -> bool:
try:
user_id = user_token["sub"]
path = request.scope.get("path")
route = request.scope.get("route")
method = request.scope.get("method")
if path and route and method and user_id:
cleaned_path = clean_path(
path
) # e.g /organizations/b65e040a-f8f0-453f-9888-baa2b9342cce
cleaned_route_path = clean_path(
route.path
) # e.g /organizations/{organization_id}
authz_query = text(
f"SELECT * FROM {settings.ACCOUNTS_SCHEMA}.authorization('{user_id}', '{cleaned_route_path}', '{cleaned_path}', '{method}');"
)
response = await async_session.execute(authz_query)
state = response.scalars().all()
if not state or not len(state) or state[0] is False:
raise ValueError("Unauthorized")
return True
else:
raise ValueError("Missing path, route, or method in request scope")
except Exception as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
15 changes: 11 additions & 4 deletions src/endpoints/v2/active_mobility.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@

from src.core.tool import start_calculation
from src.crud.crud_catchment_area import CRUDCatchmentAreaActiveMobility
from src.crud.crud_heatmap_closest_average import CRUDHeatmapClosestAverage
from src.crud.crud_heatmap_connectivity import CRUDHeatmapConnectivity
from src.crud.crud_heatmap_gravity import CRUDHeatmapGravity
from src.deps.auth import auth_z
from src.endpoints.deps import get_http_client
from src.schemas.catchment_area import (
ICatchmentAreaActiveMobility,
)
from src.schemas.catchment_area import (
request_examples_catchment_area_active_mobility as active_mobility_request_examples,
)
from src.schemas.heatmap import (
IHeatmapGravityActive,
IHeatmapClosestAverageActive,
IHeatmapConnectivityActive,
IHeatmapGravityActive,
)
from src.crud.crud_heatmap_gravity import CRUDHeatmapGravity
from src.crud.crud_heatmap_closest_average import CRUDHeatmapClosestAverage
from src.crud.crud_heatmap_connectivity import CRUDHeatmapConnectivity
from src.schemas.job import JobType
from src.schemas.toolbox_base import CommonToolParams, IToolResponse

Expand All @@ -26,6 +29,7 @@
summary="Compute catchment areas for active mobility",
response_model=IToolResponse,
status_code=201,
dependencies=[Depends(auth_z)],
)
async def compute_active_mobility_catchment_area(
*,
Expand Down Expand Up @@ -56,6 +60,7 @@ async def compute_active_mobility_catchment_area(
summary="Compute heatmap gravity for active mobility",
response_model=IToolResponse,
status_code=201,
dependencies=[Depends(auth_z)],
)
async def compute_active_mobility_heatmap_gravity(
*,
Expand Down Expand Up @@ -85,6 +90,7 @@ async def compute_active_mobility_heatmap_gravity(
summary="Compute heatmap closest-average for active mobility",
response_model=IToolResponse,
status_code=201,
dependencies=[Depends(auth_z)],
)
async def compute_active_mobility_heatmap_closest_average(
*,
Expand Down Expand Up @@ -114,6 +120,7 @@ async def compute_active_mobility_heatmap_closest_average(
summary="Compute heatmap connectivity for active mobility",
response_model=IToolResponse,
status_code=201,
dependencies=[Depends(auth_z)],
)
async def compute_active_mobility_heatmap_connectivity(
*,
Expand Down
27 changes: 23 additions & 4 deletions src/endpoints/v2/folder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
from typing import List
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status, BackgroundTasks

from fastapi import (
APIRouter,
BackgroundTasks,
Body,
Depends,
HTTPException,
Path,
Query,
status,
)
from pydantic import UUID4
from sqlalchemy import select, func
from sqlalchemy import func, select

from src.core.config import settings
from src.crud.crud_folder import folder as crud_folder
from src.db.models.folder import Folder
from src.db.session import AsyncSession
from src.deps.auth import auth_z
from src.endpoints.deps import get_db, get_user_id
from src.schemas.common import OrderEnum
from src.schemas.folder import (
Expand All @@ -15,7 +28,6 @@
from src.schemas.folder import (
request_examples as folder_request_examples,
)
from src.core.config import settings

router = APIRouter()

Expand All @@ -26,6 +38,7 @@
summary="Create a new folder",
response_model=FolderRead,
status_code=201,
dependencies=[Depends(auth_z)],
)
async def create_folder(
*,
Expand Down Expand Up @@ -57,6 +70,7 @@ async def create_folder(
summary="Retrieve a folder by its ID",
response_model=FolderRead,
status_code=200,
dependencies=[Depends(auth_z)],
)
async def read_folder(
*,
Expand Down Expand Up @@ -87,6 +101,7 @@ async def read_folder(
response_model=List[FolderRead],
response_model_exclude_none=True,
status_code=200,
dependencies=[Depends(auth_z)],
)
async def read_folders(
*,
Expand Down Expand Up @@ -123,6 +138,7 @@ async def read_folders(
summary="Update a folder with new data",
response_model=FolderUpdate,
status_code=200,
dependencies=[Depends(auth_z)],
)
async def update_folder(
*,
Expand Down Expand Up @@ -154,6 +170,7 @@ async def update_folder(
summary="Delete a folder and all its contents",
response_model=None,
status_code=204,
dependencies=[Depends(auth_z)],
)
async def delete_folder(
*,
Expand All @@ -168,5 +185,7 @@ async def delete_folder(
):
"""Delete a folder and all its contents"""

await crud_folder.delete(async_session, background_tasks=background_tasks, id=folder_id, user_id=user_id)
await crud_folder.delete(
async_session, background_tasks=background_tasks, id=folder_id, user_id=user_id
)
return
5 changes: 5 additions & 0 deletions src/endpoints/v2/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from src.crud.crud_job import job as crud_job
from src.db.models.job import Job
from src.deps.auth import auth_z
from src.endpoints.deps import get_db, get_user_id
from src.schemas.common import OrderEnum
from src.schemas.job import JobStatusType, JobType
Expand All @@ -22,6 +23,7 @@
response_model_exclude_none=True,
status_code=200,
summary="Get a job by its ID.",
dependencies=[Depends(auth_z)],
)
async def get_job(
async_session: AsyncSession = Depends(get_db),
Expand Down Expand Up @@ -49,6 +51,7 @@ async def get_job(
response_model_exclude_none=True,
status_code=200,
summary="Retrieve a list of jobs using different filters.",
dependencies=[Depends(auth_z)],
)
async def read_jobs(
async_session: AsyncSession = Depends(get_db),
Expand Down Expand Up @@ -107,6 +110,7 @@ async def read_jobs(
response_model_exclude_none=True,
status_code=200,
summary="Mark jobs as read.",
dependencies=[Depends(auth_z)],
)
async def mark_jobs_as_read(
async_session: AsyncSession = Depends(get_db),
Expand All @@ -132,6 +136,7 @@ async def mark_jobs_as_read(
response_model_exclude_none=True,
status_code=200,
summary="Kill a job.",
dependencies=[Depends(auth_z)],
)
async def kill_job(
async_session: AsyncSession = Depends(get_db),
Expand Down
Loading

0 comments on commit 76500b9

Please sign in to comment.