Skip to content
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ AUTH0_AUDIENCE=https://audience.com/api
JWT_SECRET_KEY=secret-key
# Note the list syntax pydantic-settings uses
ADMIN_ROLES='["Admin", "GalaxyAdmin"]'
# URL of Galaxy instance, for making calls to Galaxy API
GALAXY_URL=https://galaxy.example.com
GALAXY_API_KEY=api-key
# Comma-separated list of allowed origins. Note we
# don't process this with pydantic-settings as it needs
# to be used before the FastAPI app loads
Expand Down
2 changes: 1 addition & 1 deletion auth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Settings(BaseSettings):
"bpa-wheat-pathogens-transcript": "Wheat Pathogens Transcript",
}

model_config = SettingsConfigDict(env_file=".env")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")


@lru_cache()
Expand Down
Empty file added galaxy/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions galaxy/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Annotated

from fastapi import Depends
from httpx import Client

from galaxy.config import GalaxySettings, get_galaxy_settings
from galaxy.schemas import GalaxyUserModel


class GalaxyClient:
"""
Client for making requests to the Galaxy API.
"""

def __init__(self, galaxy_url: str, api_key: str):
"""
:param galaxy_url: Base URL of the Galaxy instance.
:param api_key: API key for a galaxy user.
"""
self.url = galaxy_url
self.api_key = api_key
self.client = Client(base_url=self.url, headers={'x-api-key': self.api_key})

def username_exists(self, username: str) -> bool:
"""
Check if a username already exists in Galaxy.
Note the user search in the current Galaxy API will return
partial matches, so we need to check the returned users
for an exact match.
"""
resp = self.client.get("/api/users", params={"f_name": username})
returned_users = [GalaxyUserModel(**u) for u in resp.json()]
for user in returned_users:
if user.username == username:
return True
return False


def get_galaxy_client(settings: Annotated[GalaxySettings, Depends(get_galaxy_settings)]):
return GalaxyClient(galaxy_url=settings.galaxy_url, api_key=settings.galaxy_api_key)
21 changes: 21 additions & 0 deletions galaxy/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class GalaxySettings(BaseSettings):
"""
Settings for the Galaxy API.

Note: these are currently read from the same .env file
as other settings (which pydantic-settings supports).
"""
galaxy_url: str
galaxy_api_key: str

model_config = SettingsConfigDict(env_file=".env", extra="ignore")


@lru_cache
def get_galaxy_settings():
return GalaxySettings()
16 changes: 16 additions & 0 deletions galaxy/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Optional

from pydantic import BaseModel, NaiveDatetime
Comment thread
marius-mather marked this conversation as resolved.


class GalaxyUserModel(BaseModel):
"""
User data returned by Galaxy's users API
"""
model_class: str
id: str
username: str
email: str
deleted: bool
active: bool
last_password_change: Optional[NaiveDatetime]
17 changes: 14 additions & 3 deletions routers/galaxy_register.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import logging
from typing import Optional
from typing import Annotated, Optional

import httpx
from fastapi import APIRouter, Header, HTTPException
from fastapi.params import Depends

from auth.config import Settings, get_settings
from auth.management import get_management_token
from galaxy.client import GalaxyClient, get_galaxy_client
from register.tokens import create_registration_token, verify_registration_token
from schemas.biocommons import BiocommonsRegisterData
from schemas.galaxy import GalaxyRegistrationData
Expand All @@ -26,20 +27,30 @@ async def get_registration_token(settings: Settings = Depends(get_settings)):
@router.post("/register")
def register(
registration_data: GalaxyRegistrationData,
settings: Annotated[Settings, Depends(get_settings)],
galaxy_client: Annotated[GalaxyClient, Depends(get_galaxy_client)],
registration_token: Optional[str] = Header(None),
settings: Settings = Depends(get_settings),
):
if not registration_token:
raise HTTPException(status_code=400, detail="Missing registration token")

verify_registration_token(registration_token, settings=settings)
logger.debug("Registration token verified.")

user_data = BiocommonsRegisterData.from_galaxy_registration(registration_data)
logger.debug("Checking if username exists in Galaxy")
galaxy_username = user_data.user_metadata.galaxy_username
try:
existing = galaxy_client.username_exists(galaxy_username)
if existing:
raise HTTPException(status_code=400, detail="Username already exists")
except httpx.HTTPError as e:
logger.warning(f"Failed to check username in Galaxy: {e}")

url = f"https://{settings.auth0_domain}/api/v2/users"
logger.debug("Getting management token.")
management_token = get_management_token(settings=settings)
headers = {"Authorization": f"Bearer {management_token}"}
user_data = BiocommonsRegisterData.from_galaxy_registration(registration_data)
logger.debug("Registering with Auth0 management API")
resp = httpx.post(
url,
Expand Down
25 changes: 24 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from unittest.mock import MagicMock

import pytest
from fastapi.testclient import TestClient

from auth.config import Settings, get_settings
from auth.management import get_management_token
from auth.validator import get_current_user
from galaxy.client import GalaxyClient, get_galaxy_client
from galaxy.config import GalaxySettings, get_galaxy_settings
from main import app
from tests.datagen import AccessTokenPayloadFactory, SessionUserFactory

Expand All @@ -16,7 +20,10 @@ def ignore_env_file():
"""
def get_settings_no_env_file():
return Settings(_env_file=None)
def get_galaxy_settings_no_env_file():
return GalaxySettings(_env_file=None)
app.dependency_overrides[get_settings] = get_settings_no_env_file
app.dependency_overrides[get_galaxy_settings] = get_galaxy_settings_no_env_file


@pytest.fixture
Expand All @@ -33,8 +40,15 @@ def mock_settings():
auth0_algorithms=["HS256"]
)


@pytest.fixture
def mock_galaxy_settings():
"""Dummy settings values for GalaxySettings"""
return GalaxySettings(galaxy_url="http://mock-url", galaxy_api_key="mock-key")


@pytest.fixture
def test_client(mock_settings):
def test_client(mock_settings, mock_galaxy_settings):
"""
Override the get_settings dependency to return a mocked Settings object.
"""
Expand All @@ -44,6 +58,7 @@ def override_settings():

# Apply override
app.dependency_overrides[get_settings] = override_settings
app.dependency_overrides[get_galaxy_settings] = lambda: mock_galaxy_settings

# Create client
client = TestClient(app)
Expand All @@ -67,3 +82,11 @@ def override_user():
app.dependency_overrides[get_management_token] = lambda: "mock_token"
yield
app.dependency_overrides.clear()


@pytest.fixture
def mock_galaxy_client():
client = MagicMock(GalaxyClient)
app.dependency_overrides[get_galaxy_client] = lambda: client
yield client
app.dependency_overrides.clear()
Empty file added tests/galaxy/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions tests/galaxy/data_generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from polyfactory.factories.pydantic_factory import ModelFactory

from galaxy.schemas import GalaxyUserModel


class GalaxyUserFactory(ModelFactory[GalaxyUserModel]): ...
25 changes: 25 additions & 0 deletions tests/galaxy/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import httpx
import pytest

from galaxy.client import GalaxyClient
from tests.galaxy.data_generation import GalaxyUserFactory


@pytest.fixture
def galaxy_client():
return GalaxyClient(galaxy_url="https://galaxy.example.com",
api_key="dummy-key")


def test_username_exists(galaxy_client, respx_mock):
user1 = GalaxyUserFactory.build(username="user1")
user2 = GalaxyUserFactory.build(username="user2")
respx_mock.get("https://galaxy.example.com/api/users").mock(
return_value=httpx.Response(
200,
json=[user1.model_dump(mode="json"),
user2.model_dump(mode="json")]
)
)
assert galaxy_client.username_exists("user1")
assert not galaxy_client.username_exists("other_user")
3 changes: 2 additions & 1 deletion tests/test_galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_register(mocker, mock_auth_token, mock_settings, test_client):


@pytest.mark.respx(base_url="https://mock-domain")
def test_register_json_types(respx_mock, mock_auth_token, mock_settings, test_client):
def test_register_json_types(respx_mock, mock_auth_token, mock_settings, test_client, mock_galaxy_client):
"""
Test how we handle datetimes in the response data: if we don't
use model_dump(mode="json") when providing json data, we can get errors
Expand All @@ -154,6 +154,7 @@ def test_register_json_types(respx_mock, mock_auth_token, mock_settings, test_cl
user_data = GalaxyRegistrationDataFactory.build()
token_resp = test_client.get("/galaxy/get-registration-token")
headers = {"registration-token": token_resp.json()["token"]}
mock_galaxy_client.username_exists.return_value = False
resp = test_client.post("/galaxy/register", json=user_data.model_dump(), headers=headers)
assert resp.status_code == 200

Expand Down