Major changes to server startup. Admin page tuning.
This commit is contained in:
@@ -105,6 +105,14 @@ class DatabaseInterface(ABC):
|
||||
async def create_role(self, role: Role) -> None:
|
||||
"""Create new role."""
|
||||
|
||||
@abstractmethod
|
||||
async def update_role(self, role: Role) -> None:
|
||||
"""Update a role's display name and synchronize its permissions."""
|
||||
|
||||
@abstractmethod
|
||||
async def delete_role(self, role_uuid: UUID) -> None:
|
||||
"""Delete a role by UUID. Implementations may prevent deletion if users exist."""
|
||||
|
||||
# Credential operations
|
||||
@abstractmethod
|
||||
async def create_credential(self, credential: Credential) -> None:
|
||||
@@ -165,6 +173,10 @@ class DatabaseInterface(ABC):
|
||||
async def get_organization(self, org_id: str) -> Org:
|
||||
"""Get organization by ID, including its permission IDs and roles (with their permission IDs)."""
|
||||
|
||||
@abstractmethod
|
||||
async def list_organizations(self) -> list[Org]:
|
||||
"""List all organizations with their roles and permission IDs."""
|
||||
|
||||
@abstractmethod
|
||||
async def update_organization(self, org: Org) -> None:
|
||||
"""Update organization options."""
|
||||
@@ -175,7 +187,7 @@ class DatabaseInterface(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def add_user_to_organization(
|
||||
self, user_uuid: UUID, org_id: str, role: str
|
||||
self, user_uuid: UUID, org_id: str, role: str
|
||||
) -> None:
|
||||
"""Set a user's organization and role."""
|
||||
|
||||
@@ -214,6 +226,10 @@ class DatabaseInterface(ABC):
|
||||
async def get_permission(self, permission_id: str) -> Permission:
|
||||
"""Get permission by ID."""
|
||||
|
||||
@abstractmethod
|
||||
async def list_permissions(self) -> list[Permission]:
|
||||
"""List all permissions."""
|
||||
|
||||
@abstractmethod
|
||||
async def update_permission(self, permission: Permission) -> None:
|
||||
"""Update permission details."""
|
||||
@@ -248,7 +264,9 @@ class DatabaseInterface(ABC):
|
||||
"""Add a permission to a role."""
|
||||
|
||||
@abstractmethod
|
||||
async def remove_permission_from_role(self, role_uuid: UUID, permission_id: str) -> None:
|
||||
async def remove_permission_from_role(
|
||||
self, role_uuid: UUID, permission_id: str
|
||||
) -> None:
|
||||
"""Remove a permission from a role."""
|
||||
|
||||
@abstractmethod
|
||||
@@ -259,6 +277,10 @@ class DatabaseInterface(ABC):
|
||||
async def get_permission_roles(self, permission_id: str) -> list[Role]:
|
||||
"""List all roles that grant a permission."""
|
||||
|
||||
@abstractmethod
|
||||
async def get_role(self, role_uuid: UUID) -> Role:
|
||||
"""Get a role by UUID, including its permission IDs."""
|
||||
|
||||
# Combined operations
|
||||
@abstractmethod
|
||||
async def login(self, user_uuid: UUID, credential: Credential) -> None:
|
||||
|
||||
@@ -441,13 +441,42 @@ class DB(DatabaseInterface):
|
||||
display_name=org.display_name,
|
||||
)
|
||||
session.add(org_model)
|
||||
# Persist org permissions the org is allowed to grant
|
||||
# Persist any explicitly provided org grantable permissions
|
||||
if org.permissions:
|
||||
for perm_id in org.permissions:
|
||||
for perm_id in set(org.permissions):
|
||||
session.add(
|
||||
OrgPermission(org_uuid=org.uuid.bytes, permission_id=perm_id)
|
||||
)
|
||||
|
||||
# Automatically create an organization admin permission if not present.
|
||||
# Pattern: auth/org:<org-uuid>
|
||||
auto_perm_id = f"auth/org:{org.uuid}"
|
||||
# Only create if it does not already exist (in case caller passed it)
|
||||
existing_perm = await session.execute(
|
||||
select(PermissionModel).where(PermissionModel.id == auto_perm_id)
|
||||
)
|
||||
if not existing_perm.scalar_one_or_none():
|
||||
session.add(
|
||||
PermissionModel(
|
||||
id=auto_perm_id,
|
||||
display_name=f"{org.display_name} Admin",
|
||||
)
|
||||
)
|
||||
# Ensure org is allowed to grant its own admin permission (insert if missing)
|
||||
existing_org_perm = await session.execute(
|
||||
select(OrgPermission).where(
|
||||
OrgPermission.org_uuid == org.uuid.bytes,
|
||||
OrgPermission.permission_id == auto_perm_id,
|
||||
)
|
||||
)
|
||||
if not existing_org_perm.scalar_one_or_none():
|
||||
session.add(
|
||||
OrgPermission(org_uuid=org.uuid.bytes, permission_id=auto_perm_id)
|
||||
)
|
||||
# Reflect the automatically added permission in the dataclass instance
|
||||
if auto_perm_id not in org.permissions:
|
||||
org.permissions.append(auto_perm_id)
|
||||
|
||||
async def get_organization(self, org_id: str) -> Org:
|
||||
async with self.session() as session:
|
||||
# Convert string ID to UUID bytes for lookup
|
||||
@@ -488,6 +517,48 @@ class DB(DatabaseInterface):
|
||||
|
||||
return org_dc
|
||||
|
||||
async def list_organizations(self) -> list[Org]:
|
||||
async with self.session() as session:
|
||||
# Load all orgs
|
||||
orgs_result = await session.execute(select(OrgModel))
|
||||
org_models = orgs_result.scalars().all()
|
||||
if not org_models:
|
||||
return []
|
||||
|
||||
# Preload org permissions mapping
|
||||
org_perms_result = await session.execute(select(OrgPermission))
|
||||
org_perms = org_perms_result.scalars().all()
|
||||
perms_by_org: dict[bytes, list[str]] = {}
|
||||
for op in org_perms:
|
||||
perms_by_org.setdefault(op.org_uuid, []).append(op.permission_id)
|
||||
|
||||
# Preload roles
|
||||
roles_result = await session.execute(select(RoleModel))
|
||||
role_models = roles_result.scalars().all()
|
||||
|
||||
# Preload role permissions mapping
|
||||
rp_result = await session.execute(select(RolePermission))
|
||||
rps = rp_result.scalars().all()
|
||||
perms_by_role: dict[bytes, list[str]] = {}
|
||||
for rp in rps:
|
||||
perms_by_role.setdefault(rp.role_uuid, []).append(rp.permission_id)
|
||||
|
||||
# Build org dataclasses with roles and permission IDs
|
||||
roles_by_org: dict[bytes, list[Role]] = {}
|
||||
for rm in role_models:
|
||||
r_dc = rm.as_dataclass()
|
||||
r_dc.permissions = perms_by_role.get(rm.uuid, [])
|
||||
roles_by_org.setdefault(rm.org_uuid, []).append(r_dc)
|
||||
|
||||
orgs: list[Org] = []
|
||||
for om in org_models:
|
||||
o_dc = om.as_dataclass()
|
||||
o_dc.permissions = perms_by_org.get(om.uuid, [])
|
||||
o_dc.roles = roles_by_org.get(om.uuid, [])
|
||||
orgs.append(o_dc)
|
||||
|
||||
return orgs
|
||||
|
||||
async def update_organization(self, org: Org) -> None:
|
||||
async with self.session() as session:
|
||||
stmt = (
|
||||
@@ -505,9 +576,7 @@ class DB(DatabaseInterface):
|
||||
if org.permissions:
|
||||
for perm_id in org.permissions:
|
||||
await session.merge(
|
||||
OrgPermission(
|
||||
org_uuid=org.uuid.bytes, permission_id=perm_id
|
||||
)
|
||||
OrgPermission(org_uuid=org.uuid.bytes, permission_id=perm_id)
|
||||
)
|
||||
|
||||
async def delete_organization(self, org_uuid: UUID) -> None:
|
||||
@@ -557,9 +626,9 @@ class DB(DatabaseInterface):
|
||||
async def transfer_user_to_organization(
|
||||
self, user_uuid: UUID, new_org_id: str, new_role: str | None = None
|
||||
) -> None:
|
||||
# Users are members of an org that never changes after creation.
|
||||
# Disallow transfers across organizations to enforce invariant.
|
||||
raise ValueError("Users cannot be transferred to a different organization")
|
||||
# Users are members of an org that never changes after creation.
|
||||
# Disallow transfers across organizations to enforce invariant.
|
||||
raise ValueError("Users cannot be transferred to a different organization")
|
||||
|
||||
async def get_user_organization(self, user_uuid: UUID) -> tuple[Org, str]:
|
||||
async with self.session() as session:
|
||||
@@ -686,6 +755,11 @@ class DB(DatabaseInterface):
|
||||
stmt = delete(PermissionModel).where(PermissionModel.id == permission_id)
|
||||
await session.execute(stmt)
|
||||
|
||||
async def list_permissions(self) -> list[Permission]:
|
||||
async with self.session() as session:
|
||||
result = await session.execute(select(PermissionModel))
|
||||
return [p.as_dataclass() for p in result.scalars().all()]
|
||||
|
||||
async def add_permission_to_role(self, role_uuid: UUID, permission_id: str) -> None:
|
||||
async with self.session() as session:
|
||||
# Ensure role exists
|
||||
@@ -696,7 +770,9 @@ class DB(DatabaseInterface):
|
||||
raise ValueError("Role not found")
|
||||
|
||||
# Ensure permission exists
|
||||
perm_stmt = select(PermissionModel).where(PermissionModel.id == permission_id)
|
||||
perm_stmt = select(PermissionModel).where(
|
||||
PermissionModel.id == permission_id
|
||||
)
|
||||
perm_result = await session.execute(perm_stmt)
|
||||
if not perm_result.scalar_one_or_none():
|
||||
raise ValueError("Permission not found")
|
||||
@@ -705,7 +781,9 @@ class DB(DatabaseInterface):
|
||||
RolePermission(role_uuid=role_uuid.bytes, permission_id=permission_id)
|
||||
)
|
||||
|
||||
async def remove_permission_from_role(self, role_uuid: UUID, permission_id: str) -> None:
|
||||
async def remove_permission_from_role(
|
||||
self, role_uuid: UUID, permission_id: str
|
||||
) -> None:
|
||||
async with self.session() as session:
|
||||
await session.execute(
|
||||
delete(RolePermission)
|
||||
@@ -717,7 +795,9 @@ class DB(DatabaseInterface):
|
||||
async with self.session() as session:
|
||||
stmt = (
|
||||
select(PermissionModel)
|
||||
.join(RolePermission, PermissionModel.id == RolePermission.permission_id)
|
||||
.join(
|
||||
RolePermission, PermissionModel.id == RolePermission.permission_id
|
||||
)
|
||||
.where(RolePermission.role_uuid == role_uuid.bytes)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
@@ -733,6 +813,57 @@ class DB(DatabaseInterface):
|
||||
result = await session.execute(stmt)
|
||||
return [r.as_dataclass() for r in result.scalars().all()]
|
||||
|
||||
async def update_role(self, role: Role) -> None:
|
||||
async with self.session() as session:
|
||||
# Update role display_name
|
||||
await session.execute(
|
||||
update(RoleModel)
|
||||
.where(RoleModel.uuid == role.uuid.bytes)
|
||||
.values(display_name=role.display_name)
|
||||
)
|
||||
# Sync role permissions: delete all then insert current set
|
||||
await session.execute(
|
||||
delete(RolePermission).where(
|
||||
RolePermission.role_uuid == role.uuid.bytes
|
||||
)
|
||||
)
|
||||
if role.permissions:
|
||||
for perm_id in set(role.permissions):
|
||||
session.add(
|
||||
RolePermission(role_uuid=role.uuid.bytes, permission_id=perm_id)
|
||||
)
|
||||
|
||||
async def delete_role(self, role_uuid: UUID) -> None:
|
||||
async with self.session() as session:
|
||||
# Prevent deleting a role that still has users
|
||||
# Quick existence check for users assigned to the role
|
||||
existing_user = await session.execute(
|
||||
select(UserModel.uuid).where(UserModel.role_uuid == role_uuid.bytes)
|
||||
)
|
||||
if existing_user.first() is not None:
|
||||
raise ValueError("Cannot delete role with assigned users")
|
||||
|
||||
await session.execute(
|
||||
delete(RoleModel).where(RoleModel.uuid == role_uuid.bytes)
|
||||
)
|
||||
|
||||
async def get_role(self, role_uuid: UUID) -> Role:
|
||||
async with self.session() as session:
|
||||
result = await session.execute(
|
||||
select(RoleModel).where(RoleModel.uuid == role_uuid.bytes)
|
||||
)
|
||||
role_model = result.scalar_one_or_none()
|
||||
if not role_model:
|
||||
raise ValueError("Role not found")
|
||||
r_dc = role_model.as_dataclass()
|
||||
perms_result = await session.execute(
|
||||
select(RolePermission.permission_id).where(
|
||||
RolePermission.role_uuid == role_uuid.bytes
|
||||
)
|
||||
)
|
||||
r_dc.permissions = [row[0] for row in perms_result.fetchall()]
|
||||
return r_dc
|
||||
|
||||
async def add_permission_to_organization(
|
||||
self, org_id: str, permission_id: str
|
||||
) -> None:
|
||||
@@ -844,7 +975,9 @@ class DB(DatabaseInterface):
|
||||
.join(RoleModel, UserModel.role_uuid == RoleModel.uuid)
|
||||
.join(OrgModel, RoleModel.org_uuid == OrgModel.uuid)
|
||||
.outerjoin(RolePermission, RoleModel.uuid == RolePermission.role_uuid)
|
||||
.outerjoin(PermissionModel, RolePermission.permission_id == PermissionModel.id)
|
||||
.outerjoin(
|
||||
PermissionModel, RolePermission.permission_id == PermissionModel.id
|
||||
)
|
||||
.where(SessionModel.key == session_key)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user