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
13 changes: 13 additions & 0 deletions db/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
BiocommonsUser,
GroupMembership,
GroupMembershipHistory,
Platform,
PlatformMembership,
PlatformMembershipHistory,
)
Expand Down Expand Up @@ -130,6 +131,17 @@ class GroupMembershipHistoryAdmin(ModelView, model=GroupMembershipHistory):
column_default_sort = ("updated_at", True)


class PlatformAdmin(ModelView, model=Platform):
can_edit = True
can_create = True
can_delete = True
form_include_pk = True
column_list = ["id", "name", "admin_roles"]
column_details_list = ["id", "name", "admin_roles", "members"]
column_default_sort = ("id", True)



class PlatformMembershipAdmin(ModelView, model=PlatformMembership):
can_edit = False
can_create = False
Expand Down Expand Up @@ -171,6 +183,7 @@ class DatabaseAdmin:
Auth0RoleAdmin,
GroupMembershipAdmin,
GroupMembershipHistoryAdmin,
PlatformAdmin,
PlatformMembershipAdmin,
PlatformMembershipHistoryAdmin,
)
Expand Down
26 changes: 23 additions & 3 deletions db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,28 @@ def add_group_membership(
return membership


class PlatformRoleLink(BaseModel, table=True):
platform_id: PlatformEnum = Field(primary_key=True, foreign_key="platform.id", sa_type=DbEnum(PlatformEnum, name="PlatformEnum"))
role_id: str = Field(primary_key=True, foreign_key="auth0role.id")


class Platform(BaseModel, table=True):
id: PlatformEnum = Field(primary_key=True, unique=True, sa_type=DbEnum(PlatformEnum, name="PlatformEnum"))
# Human-readable name for the platform
name: str = Field(unique=True)
admin_roles: list["Auth0Role"] = Relationship(
back_populates="admin_platforms", link_model=PlatformRoleLink,
)
members: list["PlatformMembership"] = Relationship(back_populates="platform")


class PlatformMembership(BaseModel, table=True):
__table_args__ = (
UniqueConstraint("platform_id", "user_id", name="platform_user_id_platform_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
platform_id: PlatformEnum = Field(sa_type=DbEnum(PlatformEnum, name="PlatformEnum"))
platform_id: PlatformEnum = Field(foreign_key="platform.id", sa_type=DbEnum(PlatformEnum, name="PlatformEnum"))
platform: Platform = Relationship(back_populates="members")
user_id: str = Field(foreign_key="biocommons_user.id")
user: "BiocommonsUser" = Relationship(
back_populates="platform_memberships",
Expand Down Expand Up @@ -262,8 +278,8 @@ def save_history(
session.flush()

history = GroupMembershipHistory(
group=self.group,
user=self.user,
group_id=self.group_id,
user_id=self.user_id,
Comment thread
marius-mather marked this conversation as resolved.
approval_status=self.approval_status,
updated_at=self.updated_at,
updated_by=self.updated_by,
Expand Down Expand Up @@ -351,6 +367,9 @@ class Auth0Role(BaseModel, table=True):
admin_groups: list["BiocommonsGroup"] = Relationship(
back_populates="admin_roles", link_model=GroupRoleLink
)
admin_platforms: list["Platform"] = Relationship(
back_populates="admin_roles", link_model=PlatformRoleLink
)

@classmethod
def get_or_create_by_id(
Expand Down Expand Up @@ -423,6 +442,7 @@ def user_is_admin(self, user: SessionUser) -> bool:

# Update all model references
BiocommonsUser.model_rebuild()
Platform.model_rebuild()
PlatformMembership.model_rebuild()
PlatformMembershipHistory.model_rebuild()
GroupMembership.model_rebuild()
Expand Down
31 changes: 31 additions & 0 deletions migrations/versions/08a3d0593418_platform_constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""platform_constraints

Revision ID: 08a3d0593418
Revises: 575a146957f2
Create Date: 2025-09-24 10:38:24.506817

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision: str = '08a3d0593418'
down_revision: Union[str, None] = '575a146957f2'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(op.f('uq_platform_id'), 'platform', ['id'])
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('uq_platform_id'), 'platform', type_='unique')
# ### end Alembic commands ###
56 changes: 56 additions & 0 deletions migrations/versions/575a146957f2_platform_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""platform_model

Revision ID: 575a146957f2
Revises: 1546c07b9d78
Create Date: 2025-09-24 10:07:01.958231

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision: str = '575a146957f2'
down_revision: Union[str, None] = '1546c07b9d78'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# NOTE: alembic doesn't automatically add new enum values to existing types
op.execute('ALTER TYPE "PlatformEnum" ADD VALUE \'SBP\'')
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('platform',
sa.Column('id', sa.Enum('GALAXY', 'BPA_DATA_PORTAL', 'SBP', name='PlatformEnum'), nullable=False),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_platform')),
sa.UniqueConstraint('id', name=op.f('uq_platform_id')),
sa.UniqueConstraint('name', name=op.f('uq_platform_name'))
)

# Insert the platform records that might be referenced by existing platformmembership records
op.execute("INSERT INTO platform (id, name) VALUES ('GALAXY', 'Galaxy Australia')")
op.execute("INSERT INTO platform (id, name) VALUES ('BPA_DATA_PORTAL', 'Bioplatforms Australia Data Portal')")
op.execute("INSERT INTO platform (id, name) VALUES ('SBP', 'Structural Biology Platform')")

op.create_table('platformrolelink',
sa.Column('platform_id', sa.Enum('GALAXY', 'BPA_DATA_PORTAL', 'SBP', name='PlatformEnum'), nullable=False),
sa.Column('role_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.ForeignKeyConstraint(['platform_id'], ['platform.id'], name=op.f('fk_platformrolelink_platform_id_platform')),
sa.ForeignKeyConstraint(['role_id'], ['auth0role.id'], name=op.f('fk_platformrolelink_role_id_auth0role')),
sa.PrimaryKeyConstraint('platform_id', 'role_id', name=op.f('pk_platformrolelink'))
)
# Create foreign key constraint for platformmembership.platform_id
op.create_foreign_key(op.f('fk_platformmembership_platform_id_platform'), 'platformmembership', 'platform', ['platform_id'], ['id'])
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('fk_platformmembership_platform_id_platform'), 'platformmembership', type_='foreignkey')
op.drop_table('platformrolelink')
op.drop_table('platform')
# ### end Alembic commands ###
26 changes: 22 additions & 4 deletions routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@
from fastapi import APIRouter, Depends, HTTPException, Path
from fastapi.params import Query
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy import false, func, or_
from sqlalchemy import alias, false, func, or_
from sqlmodel import Session, select

from auth.validator import user_is_admin
from auth.validator import get_current_user, user_is_admin
from auth0.client import Auth0Client, get_auth0_client
from db.models import (
Auth0Role,
BiocommonsGroup,
BiocommonsUser,
GroupMembership,
Platform,
PlatformEnum,
PlatformMembership,
)
from db.setup import get_db_session
from db.types import ApprovalStatusEnum, GroupEnum
from schemas.biocommons import Auth0UserDataWithMemberships
from schemas.user import SessionUser

logger = logging.getLogger('uvicorn.error')

Expand Down Expand Up @@ -105,7 +108,8 @@ def get_filter_options():

@router.get("/users",
response_model=list[BiocommonsUserResponse])
def get_users(db_session: Annotated[Session, Depends(get_db_session)],
def get_users(admin_user: Annotated[SessionUser, Depends(get_current_user)],
db_session: Annotated[Session, Depends(get_db_session)],
pagination: Annotated[PaginationParams, Depends(get_pagination_params)],
filter_by: str = Query(None, description="Filter users by group ('tsi', 'bpa_galaxy') or platform ('galaxy', 'bpa_data_portal')"),
search: str = Query(None, description="Search users by username or email")):
Expand All @@ -118,7 +122,21 @@ def get_users(db_session: Annotated[Session, Depends(get_db_session)],
- Platform names: 'galaxy', 'bpa_data_portal'
search: Optional search parameter for username or email
"""
base_query = select(BiocommonsUser)
admin_roles = admin_user.access_token.biocommons_roles
# Base query with platform access filtering built-in
allowed_platforms_subquery = (
select(Platform.id)
.join(Platform.admin_roles)
.where(Auth0Role.name.in_(admin_roles))
).alias("allowed_platforms")
# Need an alias or SQLAlchemy complains about duplicate column names
pm = alias(PlatformMembership, name="pm")
base_query = (
select(BiocommonsUser)
.join(pm, BiocommonsUser.id == pm.c.user_id)
.where(pm.c.platform_id.in_(allowed_platforms_subquery))
.distinct()
)

if filter_by:
if filter_by in GROUP_MAPPING:
Expand Down
20 changes: 19 additions & 1 deletion routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from auth.management import get_management_token
from auth.validator import get_current_user
from config import Settings, get_settings
from db.models import GroupMembership, PlatformMembership
from db.models import Auth0Role, GroupMembership, Platform, PlatformMembership
from db.setup import get_db_session
from db.types import ApprovalStatusEnum
from schemas.biocommons import Auth0UserData
Expand Down Expand Up @@ -155,6 +155,24 @@ async def get_pending_platforms(
return db_session.exec(query).all()


@router.get(
"/platforms/admin-roles",
description="Get platforms for which the current user has admin privileges.",
)
async def get_admin_platforms(
user: Annotated[SessionUser, Depends(get_current_user)],
db_session: Annotated[Session, Depends(get_db_session)],
):
"""Get platforms for which the current user has admin privileges."""
user_roles = user.access_token.biocommons_roles
query = (
select(Platform)
.join(Platform.admin_roles)
.where(Auth0Role.name.in_(user_roles))
)
return db_session.exec(query).all()


@router.get("/groups",
response_model=list[GroupMembershipData],)
async def get_groups(
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
BiocommonsGroupFactory,
BiocommonsUserFactory,
GroupMembershipFactory,
PlatformFactory,
PlatformMembershipFactory,
)

Expand Down Expand Up @@ -252,6 +253,7 @@ def persistent_factories(test_db_session):
BiocommonsGroupFactory,
BiocommonsUserFactory,
GroupMembershipFactory,
PlatformFactory,
PlatformMembershipFactory,
]
for factory in factories:
Expand Down
13 changes: 9 additions & 4 deletions tests/db/datagen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
BiocommonsGroup,
BiocommonsUser,
GroupMembership,
Platform,
PlatformMembership,
)
from tests.datagen import random_auth0_id
Expand All @@ -19,16 +20,20 @@ def id(cls) -> str:


class Auth0RoleFactory(SQLAlchemyFactory[Auth0Role]):
__set_relationships__ = True
__set_relationships__ = False


class BiocommonsGroupFactory(SQLAlchemyFactory[BiocommonsGroup]):
__set_relationships__ = True
__set_relationships__ = False


class GroupMembershipFactory(SQLAlchemyFactory[GroupMembership]):
__set_relationships__ = True
__set_relationships__ = False


class PlatformMembershipFactory(SQLAlchemyFactory[PlatformMembership]):
__set_relationships__ = True
__set_relationships__ = False


class PlatformFactory(SQLAlchemyFactory[Platform]):
__set_relationships__ = False
Loading
Loading