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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ JWT_SECRET_KEY=secret-key
ADMIN_ROLES='["Admin", "GalaxyAdmin"]'
# Enable sending emails (for approving group memberships etc.)
SEND_EMAIL=False
# AAI Portal URL for admin links in emails
AAI_PORTAL_URL=https://aaiportal.example.com
# URL of Galaxy instance, for making calls to Galaxy API
GALAXY_URL=https://galaxy.example.com
GALAXY_API_KEY=api-key
Expand Down
2 changes: 2 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class Settings(BaseSettings):
# Note we process this separately in app startup as it needs
# to be available before the app starts
cors_allowed_origins: str
# AAI Portal URL for admin links in emails
aai_portal_url: str = "https://aaiportal.test.biocommons.org.au"

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

Expand Down
1 change: 1 addition & 0 deletions db/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ApprovalStatusEnum(str, Enum):
class PlatformEnum(str, Enum):
GALAXY = "galaxy"
BPA_DATA_PORTAL = "bpa_data_portal"
SBP = "sbp"


class PlatformMembershipData(BaseModel):
Expand Down
2 changes: 2 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
biocommons_register,
bpa_register,
galaxy_register,
sbp_register,
user,
utils,
)
Expand Down Expand Up @@ -61,5 +62,6 @@ def public_route():
app.include_router(biocommons_register.router)
app.include_router(bpa_register.router)
app.include_router(galaxy_register.router)
app.include_router(sbp_register.router)
app.include_router(utils.router)
app.include_router(biocommons_groups.router)
1 change: 1 addition & 0 deletions routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
PLATFORM_MAPPING = {
"galaxy": {"enum": PlatformEnum.GALAXY, "name": "Galaxy Australia"},
"bpa_data_portal": {"enum": PlatformEnum.BPA_DATA_PORTAL, "name": "Bioplatforms Australia Data Portal"},
"sbp": {"enum": PlatformEnum.SBP, "name": "Structural Biology Platform"},
}

GROUP_MAPPING = {
Expand Down
6 changes: 3 additions & 3 deletions routers/biocommons_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def request_group_access(
admin_emails = membership.group.get_admins(auth0_client=auth0_client)
for email in admin_emails:
background_tasks.add_task(send_group_approval_email,
approver_email=email, request=membership, email_service=email_service)
approver_email=email, request=membership, email_service=email_service, settings=settings)
return {"message": f"Group membership for {group_id} requested successfully."}


Expand Down Expand Up @@ -148,13 +148,13 @@ def approve_group_access(
return {"message": f"Group membership for {group.name} approved successfully."}


def send_group_approval_email(approver_email: str, request: GroupMembership, email_service: EmailService):
def send_group_approval_email(approver_email: str, request: GroupMembership, email_service: EmailService, settings: Settings):
subject = f"New request to join {request.group.name}"

body_html = f"""
<p>A new user has requested access to the {request.group.name} group.</p>
<p><strong>User:</strong> {request.user.email}</p>
<p>Please <a href='https://aaiportal.test.biocommons.org.au/requests'>log into the BioCommons account dashboard</a> to review and approve access.</p>
<p>Please <a href='{settings.aai_portal_url}/requests'>log into the BioCommons account dashboard</a> to review and approve access.</p>
"""

email_service.send(approver_email, subject, body_html)
Expand Down
6 changes: 3 additions & 3 deletions routers/biocommons_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
router = APIRouter(prefix="/biocommons", tags=["biocommons", "registration"], route_class=RegistrationRoute)


def send_approval_email(registration: BiocommonsRegistrationRequest):
def send_approval_email(registration: BiocommonsRegistrationRequest, settings: Settings):
"""Send email notification about new biocommons registration."""
email_service = EmailService()
approver_email = "aai-dev@biocommons.org.au"
Expand All @@ -48,7 +48,7 @@ def send_approval_email(registration: BiocommonsRegistrationRequest):
<p><strong>Username:</strong> {registration.username}</p>
<p><strong>Selected Bundle:</strong> {registration.bundle}</p>
<p><strong>Requested Access:</strong> BPA Data Portal & Galaxy Australia</p>
<p>Please <a href='https://aaiportal.test.biocommons.org.au/requests'>log into the AAI Admin Portal</a> to review and approve access.</p>
<p>Please <a href='{settings.aai_portal_url}/requests'>log into the AAI Admin Portal</a> to review and approve access.</p>
"""

email_service.send(approver_email, subject, body_html)
Expand Down Expand Up @@ -82,7 +82,7 @@ async def register_biocommons_user(

# Send approval email in background
if settings.send_email:
background_tasks.add_task(send_approval_email, registration)
background_tasks.add_task(send_approval_email, registration, settings)

logger.info(
f"Successfully registered biocommons user: {auth0_user_data.user_id}"
Expand Down
109 changes: 109 additions & 0 deletions routers/sbp_register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import logging

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from httpx import HTTPStatusError
from sqlmodel import Session
from starlette.responses import JSONResponse

from auth.ses import EmailService
from auth0.client import Auth0Client, get_auth0_client
from config import Settings, get_settings
from db.models import BiocommonsUser, PlatformEnum
from db.setup import get_db_session
from routers.errors import RegistrationRoute
from schemas.biocommons import Auth0UserData, BiocommonsRegisterData
from schemas.responses import RegistrationErrorResponse, RegistrationResponse
from schemas.sbp import SBPRegistrationRequest

logger = logging.getLogger(__name__)

router = APIRouter(
prefix="/sbp",
tags=["sbp", "registration"],
# Overriding route class to handle registration errors
route_class=RegistrationRoute
)


def send_approval_email(registration: SBPRegistrationRequest, settings: Settings):
"""Send email notification about new SBP registration."""
email_service = EmailService()
approver_email = "aai-dev@biocommons.org.au"
subject = "New Structural Biology Platform User Registration"

body_html = f"""
<p>A new user has registered for the Structural Biology Platform.</p>
<p><strong>User:</strong> {registration.first_name} {registration.last_name} ({registration.email})</p>
<p><strong>Username:</strong> {registration.username}</p>
<p><strong>Registration Reason:</strong> {registration.reason}</p>
<p>Please <a href='{settings.aai_portal_url}/requests'>log into the AAI Admin Portal</a> to review and approve access.</p>
"""

email_service.send_email(
to_email=approver_email,
subject=subject,
body_html=body_html
)


@router.post(
"/register",
responses={
200: {"model": RegistrationResponse},
400: {"model": RegistrationErrorResponse},
},
)
async def register_sbp_user(
registration: SBPRegistrationRequest,
background_tasks: BackgroundTasks,
db_session: Session = Depends(get_db_session),
auth0_client: Auth0Client = Depends(get_auth0_client),
settings: Settings = Depends(get_settings)
):
"""Register a new SBP user."""

# Create Auth0 user data
user_data = BiocommonsRegisterData.from_sbp_registration(
registration=registration
)

try:
logger.info("Registering user with Auth0")
auth0_user_data = auth0_client.create_user(user_data)

logger.info("Adding user to DB")
_create_sbp_user_record(auth0_user_data, db_session)

# Send approval email in the background
if settings.send_email:
background_tasks.add_task(send_approval_email, registration, settings)
logger.info("Approval email queued for sending")

return {"message": "User registered successfully. Approval pending.", "user": auth0_user_data.model_dump(mode="json")}

# Return HTTP status errors as RegistrationErrorResponse
except HTTPStatusError as e:
# Catch specific errors where possible and return a useful error message
if e.response.status_code == 409:
response = RegistrationErrorResponse(message="Username or email already in use")
else:
response = RegistrationErrorResponse(message=f"Auth0 error: {str(e.response.text)}")
return JSONResponse(status_code=400, content=response.model_dump(mode="json"))
# Unknown errors should return 500
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Failed to register user: {str(e)}"
)


def _create_sbp_user_record(auth0_user_data: Auth0UserData, session: Session) -> BiocommonsUser:
db_user = BiocommonsUser.from_auth0_data(data=auth0_user_data)
sbp_membership = db_user.add_platform_membership(
platform=PlatformEnum.SBP,
db_session=session,
auto_approve=False
Comment thread
marius-mather marked this conversation as resolved.
)
session.add(db_user)
session.add(sbp_membership)
session.commit()
return db_user
24 changes: 23 additions & 1 deletion schemas/biocommons.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _check(v: str) -> str:
return Annotated[str, AfterValidator(_check)]


AppId = Literal["biocommons", "galaxy", "bpa"]
AppId = Literal["biocommons", "galaxy", "bpa", "sbp"]
BiocommonsUsername = ValidatedString(min_length=3, max_length=128, pattern="^[-_a-z0-9]+$", messages={
"min_length": "Username must be at least 3 characters.",
"max_length": "Username must be 128 characters or less.",
Expand All @@ -83,13 +83,18 @@ class BPAMetadata(BaseModel):
registration_reason: str


class SBPMetadata(BaseModel):
registration_reason: str


class BiocommonsUserMetadata(BaseModel):
"""
User metadata we use for user-changeable data
like preferred usernames
"""

bpa: Optional[BPAMetadata] = None
sbp: Optional[SBPMetadata] = None


class BiocommonsAppMetadata(BaseModel):
Expand Down Expand Up @@ -143,6 +148,23 @@ def from_bpa_registration(
),
)

@classmethod
def from_sbp_registration(
cls, registration: "schemas.sbp.SBPRegistrationRequest"
) -> Self:
return cls(
email=registration.email,
password=registration.password,
username=registration.username,
name=f"{registration.first_name} {registration.last_name}",
user_metadata=BiocommonsUserMetadata(
sbp=SBPMetadata(registration_reason=registration.reason),
),
app_metadata=BiocommonsAppMetadata(
registration_from="sbp"
),
)

@classmethod
def from_galaxy_registration(
cls,
Expand Down
12 changes: 12 additions & 0 deletions schemas/sbp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pydantic import BaseModel, EmailStr

from schemas.biocommons import BiocommonsPassword, BiocommonsUsername


class SBPRegistrationRequest(BaseModel):
first_name: str
last_name: str
username: BiocommonsUsername
email: EmailStr
reason: str
password: BiocommonsPassword
8 changes: 8 additions & 0 deletions tests/datagen.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from schemas.biocommons_register import BiocommonsRegistrationRequest
from schemas.bpa import BPARegistrationRequest
from schemas.galaxy import GalaxyRegistrationData
from schemas.sbp import SBPRegistrationRequest
from schemas.tokens import AccessTokenPayload
from schemas.user import SessionUser

Expand Down Expand Up @@ -113,6 +114,13 @@ class BPARegistrationDataFactory(ModelFactory[BPARegistrationRequest]):
username = BiocommonsProviders.biocommons_username


class SBPRegistrationDataFactory(ModelFactory[SBPRegistrationRequest]):
"""Factory for generating SBP registration test data."""

password = BiocommonsProviders.biocommons_password
username = BiocommonsProviders.biocommons_username


class AppMetadataFactory(ModelFactory[BiocommonsAppMetadata]): ...


Expand Down
5 changes: 3 additions & 2 deletions tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def test_get_filter_options(test_client, as_admin_user):

options = resp.json()
assert isinstance(options, list)
assert len(options) == 4
assert len(options) == 5

for option in options:
assert "id" in option
Expand All @@ -345,12 +345,13 @@ def test_get_filter_options(test_client, as_admin_user):
assert isinstance(option["name"], str)

option_ids = {opt["id"] for opt in options}
expected_ids = {"galaxy", "bpa_data_portal", "tsi", "bpa_galaxy"}
expected_ids = {"galaxy", "bpa_data_portal", "sbp", "tsi", "bpa_galaxy"}
assert option_ids == expected_ids

option_dict = {opt["id"]: opt["name"] for opt in options}
assert option_dict["galaxy"] == "Galaxy Australia"
assert option_dict["bpa_data_portal"] == "Bioplatforms Australia Data Portal"
assert option_dict["sbp"] == "Structural Biology Platform"
assert option_dict["tsi"] == "Threatened Species Initiative Bundle"
assert option_dict["bpa_galaxy"] == "Bioplatforms Australia Data Portal & Galaxy Australia Bundle"

Expand Down
Loading