Skip to content
Merged
6 changes: 3 additions & 3 deletions routers/galaxy_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)


@router.get("/get-registration-token")
@router.get("/register/get-registration-token")
async def get_registration_token(settings: Settings = Depends(get_settings)):
return {"token": create_registration_token(settings)}

Expand All @@ -39,11 +39,11 @@ def register(

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

Expand Down
33 changes: 18 additions & 15 deletions schemas/biocommons.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@
These are the core schemas we use for storing/representing users
and their metadata
"""
import re
from datetime import datetime, timezone
from typing import List, Literal, Optional, Self
from typing import Annotated, List, Literal, Optional, Self

from pydantic import BaseModel, EmailStr, Field, HttpUrl
from pydantic import BaseModel, EmailStr, Field, HttpUrl, StringConstraints

import schemas
from schemas import Resource, Service
from schemas.bpa import BPARegistrationRequest
from schemas.galaxy import GalaxyRegistrationData
from schemas.service import Group, Identity

# From Auth0 password settings
ALLOWED_SPECIAL_CHARS = "!@#$%^&*"
VALID_PASSWORD_REGEX = re.compile(f"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[{ALLOWED_SPECIAL_CHARS}]).{{8,}}$")

AppId = Literal["biocommons", "galaxy", "bpa"]
BiocommonsUsername = Annotated[str, StringConstraints(min_length=3, max_length=128, pattern='^[-_a-z0-9]+$')]
BiocommonsPassword = Annotated[str, StringConstraints(min_length=8, max_length=128, pattern=VALID_PASSWORD_REGEX)]


class BPAMetadata(BaseModel):
registration_reason: str
username: str


class BiocommonsUserMetadata(BaseModel):
Expand All @@ -28,7 +33,6 @@ class BiocommonsUserMetadata(BaseModel):
like preferred usernames
"""
bpa: Optional[BPAMetadata] = None
galaxy_username: Optional[str] = None


class BiocommonsAppMetadata(BaseModel):
Expand Down Expand Up @@ -105,26 +109,25 @@ class BiocommonsRegisterData(BaseModel):
"""
email: EmailStr
email_verified: bool = False
password: str
password: BiocommonsPassword
connection: str = "Username-Password-Authentication"
username: str
username: BiocommonsUsername
name: Optional[str] = None
username: Optional[str] = None
user_metadata: BiocommonsUserMetadata
user_metadata: Optional[BiocommonsUserMetadata] = None
app_metadata: BiocommonsAppMetadata

@classmethod
def from_bpa_registration(
cls, registration: BPARegistrationRequest,
cls,
registration: 'schemas.bpa.BPARegistrationRequest',
bpa_service: Service) -> Self:
return cls(
email=registration.email,
password=registration.password,
username=registration.username,
name=registration.fullname,
user_metadata=BiocommonsUserMetadata(
bpa=BPAMetadata(registration_reason=registration.reason,
username=registration.username,),
bpa=BPAMetadata(registration_reason=registration.reason),
),
app_metadata=BiocommonsAppMetadata(
services=[bpa_service],
Expand All @@ -135,7 +138,7 @@ def from_bpa_registration(
@classmethod
def from_galaxy_registration(
cls,
registration: GalaxyRegistrationData):
registration: 'schemas.galaxy.GalaxyRegistrationData',):
# Galaxy registration is approved automatically
galaxy_service = Service(
name="Galaxy Australia",
Expand All @@ -147,7 +150,7 @@ def from_galaxy_registration(
)
return BiocommonsRegisterData(
email=registration.email,
user_metadata=BiocommonsUserMetadata(galaxy_username=registration.public_name),
username=registration.username,
password=registration.password,
email_verified=False,
connection="Username-Password-Authentication",
Expand Down
6 changes: 4 additions & 2 deletions schemas/bpa.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

from pydantic import BaseModel, EmailStr

from schemas.biocommons import BiocommonsPassword, BiocommonsUsername


class BPARegistrationRequest(BaseModel):
username: str
username: BiocommonsUsername
fullname: str
email: EmailStr
reason: str
password: str
password: BiocommonsPassword
organizations: Dict[str, bool]
11 changes: 7 additions & 4 deletions schemas/galaxy.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from typing import Annotated, Self
from typing import Self

from pydantic import BaseModel, EmailStr, StringConstraints, model_validator
from pydantic import BaseModel, EmailStr, model_validator

from schemas.biocommons import BiocommonsPassword, BiocommonsUsername


class GalaxyRegistrationData(BaseModel):
email: EmailStr
password: str
# TODO: Update name of this field in frontend from
username: BiocommonsUsername
password: BiocommonsPassword
password_confirmation: str
public_name: Annotated[str, StringConstraints(min_length=3, pattern=r"^[a-z0-9._-]+$")]

@model_validator(mode='after')
def check_passwords_match(self) -> Self:
Expand Down
55 changes: 55 additions & 0 deletions tests/schemas/test_biocommons_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest
from pydantic import TypeAdapter

from schemas.biocommons import BiocommonsPassword, BiocommonsUsername


@pytest.mark.parametrize("password", [
"V6Zs^B8E",
"k$M2FZa@",
"6*@s&#5Z",
"Jd9sugcfjgWXY@Dzje^83!mcfM@A$YZ8be^bUhrBZ8s$KjbbNwAHr*bdiEhmLyMPyPowFU@rX4k8h5KCh#qm9bYS5RUmtjaLmVds",
])
def test_valid_password(password: str):
password_adapter = TypeAdapter(BiocommonsPassword)
result = password_adapter.validate_python(password)
assert result == password



@pytest.mark.parametrize("password", [
# No lowercase
"ABCD1234!",
# No capital
"abcd1234!",
# Too short
"aB1!",
# No special character
"abCD1234"
])
def test_invalid_password(password: str):
password_adapter = TypeAdapter(BiocommonsPassword)
with pytest.raises(ValueError):
password_adapter.validate_python(password)


@pytest.mark.parametrize("username", [
"abc",
"a_c",
"user_n-ame"
])
def test_valid_username(username: str):
username_adapter = TypeAdapter(BiocommonsUsername)
result = username_adapter.validate_python(username)
assert result == username


@pytest.mark.parametrize("username", [
"ab",
"a.b",
"x" * 129 # Too long
])
def test_invalid_username(username: str):
username_adapter = TypeAdapter(BiocommonsUsername)
with pytest.raises(ValueError):
username_adapter.validate_python(username)
30 changes: 15 additions & 15 deletions tests/test_galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ def mock_auth_token(mocker):
def test_galaxy_registration_data_password_match():
with pytest.raises(ValidationError, match="Passwords do not match"):
GalaxyRegistrationData(email="user@example.com",
password="securepassword",
password_confirmation="insecurepassword",
public_name="valid_username")
password="SecurePassword123!",
password_confirmation="OtherPassword123!",
username="valid_username")


def test_get_registration_token(test_client, mock_settings):
"""
Test get-registration-token endpoint returns a valid JWT token.
"""
response = test_client.get("/galaxy/get-registration-token")
response = test_client.get("/galaxy/register/get-registration-token")
assert response.status_code == 200
jwt.decode(response.json()["token"], mock_settings.jwt_secret_key,
algorithms=mock_settings.auth0_algorithms)
Expand All @@ -71,18 +71,18 @@ def test_to_biocommons_register_data():
"""
data = GalaxyRegistrationData(
email="user@example.com",
password="securepassword",
password_confirmation="securepassword",
public_name="valid_username"
password="SecurePassword123!",
password_confirmation="SecurePassword123!",
username="valid_username"
)

auth0_data = BiocommonsRegisterData.from_galaxy_registration(data)

assert auth0_data.email == "user@example.com"
assert auth0_data.password == "securepassword"
assert auth0_data.password == "SecurePassword123!"
assert auth0_data.connection == "Username-Password-Authentication"
assert not auth0_data.email_verified
assert auth0_data.user_metadata.galaxy_username == "valid_username"
assert auth0_data.username == "valid_username"
assert auth0_data.app_metadata.registration_from == "galaxy"


Expand All @@ -94,14 +94,14 @@ def test_to_biocommons_register_data_empty_fields():
"""
data = GalaxyRegistrationData(
email="user@example.com",
password="securepassword",
password_confirmation="securepassword",
public_name="valid_username"
password="SecurePassword123!",
password_confirmation="SecurePassword123!",
username="valid_username"
)

auth0_data = BiocommonsRegisterData.from_galaxy_registration(data)
dumped = auth0_data.model_dump(mode="json", exclude_none=True)
assert "username" not in dumped
assert "user_metadata" not in dumped
assert "name" not in dumped


Expand All @@ -120,7 +120,7 @@ def test_register(mocker, mock_auth_token, mock_settings, test_client):
mock_resp.status_code = 201
mock_post = mocker.patch("httpx.post", return_value=mock_resp)
user_data = GalaxyRegistrationDataFactory.build()
token_resp = test_client.get("/galaxy/get-registration-token")
token_resp = test_client.get("/galaxy/register/get-registration-token")
headers = {"registration-token": token_resp.json()["token"]}
resp = test_client.post("/galaxy/register", json=user_data.model_dump(), headers=headers)
assert resp.status_code == 200
Expand Down Expand Up @@ -152,7 +152,7 @@ def test_register_json_types(respx_mock, mock_auth_token, mock_settings, test_cl
json=user.model_dump(mode="json"))
)
user_data = GalaxyRegistrationDataFactory.build()
token_resp = test_client.get("/galaxy/get-registration-token")
token_resp = test_client.get("/galaxy/register/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)
Expand Down