# app/modules/tenancy/services/admin_platform_service.py """ Admin Platform service for managing admin-platform assignments. This module provides: - Assigning platform admins to platforms - Removing platform admin access - Listing platforms for an admin - Listing admins for a platform - Promoting/demoting super admin status """ import logging from datetime import UTC, datetime from sqlalchemy.orm import Session, joinedload from app.modules.tenancy.exceptions import ( AdminOperationException, CannotModifySelfException, PlatformNotFoundException, UserAlreadyExistsException, UserNotFoundException, ) from app.modules.tenancy.models import AdminPlatform, Platform, User from app.modules.tenancy.schemas.auth import UserContext logger = logging.getLogger(__name__) class AdminPlatformService: """Service class for admin-platform assignment operations.""" # ============================================================================ # ADMIN-PLATFORM ASSIGNMENTS # ============================================================================ def assign_admin_to_platform( self, db: Session, admin_user_id: int, platform_id: int, assigned_by_user_id: int, ) -> AdminPlatform: """ Assign a platform admin to a platform. Args: db: Database session admin_user_id: User ID of the admin to assign platform_id: Platform ID to assign to assigned_by_user_id: Super admin making the assignment Returns: AdminPlatform: The created assignment Raises: ValidationException: If user is not an admin or is a super admin AdminOperationException: If assignment already exists """ # Verify target user exists and is an admin user = db.query(User).filter(User.id == admin_user_id).first() if not user: raise UserNotFoundException(str(admin_user_id)) if not user.is_admin: raise AdminOperationException( operation="assign_admin_to_platform", reason="User must be an admin to be assigned to platforms", ) if user.is_super_admin: raise AdminOperationException( operation="assign_admin_to_platform", reason="Super admins don't need platform assignments - they have access to all platforms", ) # Verify platform exists platform = db.query(Platform).filter(Platform.id == platform_id).first() if not platform: raise PlatformNotFoundException(code=str(platform_id)) # Check if assignment already exists existing = ( db.query(AdminPlatform) .filter( AdminPlatform.user_id == admin_user_id, AdminPlatform.platform_id == platform_id, ) .first() ) if existing: if existing.is_active: raise AdminOperationException( operation="assign_admin_to_platform", reason=f"Admin already assigned to platform '{platform.code}'", ) # Reactivate existing assignment existing.is_active = True existing.assigned_at = datetime.now(UTC) existing.assigned_by_user_id = assigned_by_user_id existing.updated_at = datetime.now(UTC) db.flush() db.refresh(existing) logger.info( f"Reactivated admin {admin_user_id} access to platform {platform.code} " f"by admin {assigned_by_user_id}" ) return existing # Create new assignment assignment = AdminPlatform( user_id=admin_user_id, platform_id=platform_id, assigned_by_user_id=assigned_by_user_id, is_active=True, ) db.add(assignment) db.flush() db.refresh(assignment) logger.info( f"Assigned admin {admin_user_id} to platform {platform.code} " f"by admin {assigned_by_user_id}" ) return assignment def remove_admin_from_platform( self, db: Session, admin_user_id: int, platform_id: int, removed_by_user_id: int, ) -> None: """ Remove admin's access to a platform. This soft-deletes by setting is_active=False for audit purposes. Args: db: Database session admin_user_id: User ID of the admin to remove platform_id: Platform ID to remove from removed_by_user_id: Super admin making the removal Raises: ValidationException: If assignment doesn't exist """ assignment = ( db.query(AdminPlatform) .filter( AdminPlatform.user_id == admin_user_id, AdminPlatform.platform_id == platform_id, ) .first() ) if not assignment: raise AdminOperationException( operation="remove_admin_from_platform", reason="Admin is not assigned to this platform", ) assignment.is_active = False assignment.updated_at = datetime.now(UTC) db.flush() logger.info( f"Removed admin {admin_user_id} from platform {platform_id} " f"by admin {removed_by_user_id}" ) def get_platforms_for_admin( self, db: Session, admin_user_id: int, include_inactive: bool = False, ) -> list[Platform]: """ Get all platforms an admin can access. Args: db: Database session admin_user_id: User ID of the admin include_inactive: Whether to include inactive assignments Returns: List of Platform objects the admin can access """ query = ( db.query(Platform) .join(AdminPlatform) .filter(AdminPlatform.user_id == admin_user_id) ) if not include_inactive: query = query.filter(AdminPlatform.is_active == True) return query.all() def get_all_active_platforms(self, db: Session) -> list[Platform]: """ Get all active platforms (for super admin access). Args: db: Database session Returns: List of all active Platform objects """ return db.query(Platform).filter(Platform.is_active == True).all() def get_platform_by_id(self, db: Session, platform_id: int) -> Platform | None: """ Get a platform by ID. Args: db: Database session platform_id: Platform ID Returns: Platform object or None if not found """ return db.query(Platform).filter(Platform.id == platform_id).first() def validate_admin_platform_access( self, user: User | UserContext, platform_id: int, ) -> None: """ Validate that an admin has access to a platform. Args: user: User model or UserContext schema (both have can_access_platform) platform_id: Platform ID to check Raises: InsufficientPermissionsException: If user doesn't have access """ from app.modules.tenancy.exceptions import InsufficientPermissionsException if not user.can_access_platform(platform_id): raise InsufficientPermissionsException( "You don't have access to this platform" ) def get_admins_for_platform( self, db: Session, platform_id: int, include_inactive: bool = False, ) -> list[User]: """ Get all admins assigned to a platform. Args: db: Database session platform_id: Platform ID include_inactive: Whether to include inactive assignments Returns: List of User objects assigned to the platform """ # Explicit join condition needed because AdminPlatform has two FKs to User # (user_id and assigned_by_user_id) query = ( db.query(User) .join(AdminPlatform, AdminPlatform.user_id == User.id) .filter(AdminPlatform.platform_id == platform_id) ) if not include_inactive: query = query.filter(AdminPlatform.is_active == True) return query.all() def get_admin_assignments( self, db: Session, admin_user_id: int, ) -> list[AdminPlatform]: """ Get all platform assignments for an admin with platform details. Args: db: Database session admin_user_id: User ID of the admin Returns: List of AdminPlatform objects with platform relationship loaded """ return ( db.query(AdminPlatform) .options(joinedload(AdminPlatform.platform)) .filter( AdminPlatform.user_id == admin_user_id, AdminPlatform.is_active == True, ) .all() ) # ============================================================================ # SUPER ADMIN MANAGEMENT # ============================================================================ def toggle_super_admin( self, db: Session, user_id: int, is_super_admin: bool, current_admin_id: int, ) -> User: """ Promote or demote a user to/from super admin. When demoting from super admin, the admin will have no platform access until explicitly assigned via assign_admin_to_platform. Args: db: Database session user_id: User ID to modify is_super_admin: True to promote, False to demote current_admin_id: Super admin making the change Returns: Updated User object Raises: CannotModifySelfException: If trying to demote self ValidationException: If user is not an admin """ if user_id == current_admin_id and not is_super_admin: raise CannotModifySelfException( user_id=user_id, operation="demote from super admin", ) user = db.query(User).filter(User.id == user_id).first() if not user: raise UserNotFoundException(str(user_id)) if not user.is_admin: raise AdminOperationException( operation="toggle_super_admin", reason="User must be an admin to be promoted to super admin", ) user.role = "super_admin" if is_super_admin else "platform_admin" user.updated_at = datetime.now(UTC) db.flush() db.refresh(user) action = "promoted to" if is_super_admin else "demoted from" logger.info( f"User {user.username} {action} super admin by admin {current_admin_id}" ) return user def create_platform_admin( self, db: Session, email: str, username: str, password: str, platform_ids: list[int], created_by_user_id: int, first_name: str | None = None, last_name: str | None = None, ) -> tuple[User, list[AdminPlatform]]: """ Create a new platform admin with platform assignments. Args: db: Database session email: Admin email username: Admin username password: Admin password platform_ids: List of platform IDs to assign created_by_user_id: Super admin creating the account first_name: Optional first name last_name: Optional last name Returns: Tuple of (User, list of AdminPlatform assignments) """ from middleware.auth import AuthManager auth_manager = AuthManager() # Check for existing user existing = db.query(User).filter( (User.email == email) | (User.username == username) ).first() if existing: field = "email" if existing.email == email else "username" raise UserAlreadyExistsException(f"{field.capitalize()} already exists", field=field) # Create admin user user = User( email=email, username=username, hashed_password=auth_manager.hash_password(password), first_name=first_name, last_name=last_name, role="platform_admin", is_active=True, ) db.add(user) db.flush() # Create platform assignments assignments = [] for platform_id in platform_ids: assignment = AdminPlatform( user_id=user.id, platform_id=platform_id, assigned_by_user_id=created_by_user_id, is_active=True, ) db.add(assignment) # noqa: PERF006 assignments.append(assignment) db.flush() db.refresh(user) logger.info( f"Created platform admin {username} with access to platforms " f"{platform_ids} by admin {created_by_user_id}" ) return user, assignments # ============================================================================ # ADMIN USER CRUD OPERATIONS # ============================================================================ def list_admin_users( self, db: Session, skip: int = 0, limit: int = 100, include_super_admins: bool = True, is_active: bool | None = None, search: str | None = None, ) -> tuple[list[User], int]: """ List all admin users with optional filtering. Args: db: Database session skip: Number of records to skip limit: Maximum records to return include_super_admins: Whether to include super admins is_active: Filter by active status search: Search term for username/email/name Returns: Tuple of (list of User objects, total count) """ query = db.query(User).filter( User.role.in_(["super_admin", "platform_admin"]) ) if not include_super_admins: query = query.filter(User.role == "platform_admin") if is_active is not None: query = query.filter(User.is_active == is_active) if search: search_term = f"%{search}%" query = query.filter( (User.username.ilike(search_term)) | (User.email.ilike(search_term)) | (User.first_name.ilike(search_term)) | (User.last_name.ilike(search_term)) ) total = query.count() admins = ( query.options(joinedload(User.admin_platforms)) .offset(skip) .limit(limit) .all() ) return admins, total def get_admin_user( self, db: Session, user_id: int, ) -> User: """ Get a single admin user by ID with platform assignments. Args: db: Database session user_id: User ID Returns: User object with admin_platforms loaded Raises: ValidationException: If user not found or not an admin """ admin = ( db.query(User) .options(joinedload(User.admin_platforms)) .filter( User.id == user_id, User.role.in_(["super_admin", "platform_admin"]), ) .first() ) if not admin: raise UserNotFoundException(str(user_id)) return admin def create_super_admin( self, db: Session, email: str, username: str, password: str, created_by_user_id: int, first_name: str | None = None, last_name: str | None = None, ) -> User: """ Create a new super admin user. Args: db: Database session email: Admin email username: Admin username password: Admin password created_by_user_id: Super admin creating the account first_name: Optional first name last_name: Optional last name Returns: Created User object """ from app.core.security import get_password_hash # Check for existing user existing = ( db.query(User) .filter((User.email == email) | (User.username == username)) .first() ) if existing: field = "email" if existing.email == email else "username" raise UserAlreadyExistsException(f"{field.capitalize()} already exists", field=field) user = User( email=email, username=username, hashed_password=get_password_hash(password), first_name=first_name, last_name=last_name, role="super_admin", is_active=True, ) db.add(user) db.flush() db.refresh(user) logger.info( f"Created super admin {username} by admin {created_by_user_id}" ) return user def toggle_admin_status( self, db: Session, user_id: int, current_admin_id: int, ) -> User: """ Toggle admin user active status. Args: db: Database session user_id: User ID to toggle current_admin_id: Super admin making the change Returns: Updated User object Raises: CannotModifySelfException: If trying to deactivate self ValidationException: If user not found or not an admin """ if user_id == current_admin_id: raise CannotModifySelfException( user_id=user_id, operation="deactivate own account", ) admin = db.query(User).filter( User.id == user_id, User.role.in_(["super_admin", "platform_admin"]), ).first() if not admin: raise UserNotFoundException(str(user_id)) admin.is_active = not admin.is_active admin.updated_at = datetime.now(UTC) db.flush() db.refresh(admin) action = "activated" if admin.is_active else "deactivated" logger.info( f"Admin {admin.username} {action} by admin {current_admin_id}" ) return admin def delete_admin_user( self, db: Session, user_id: int, current_admin_id: int, ) -> None: """ Delete an admin user and their platform assignments. Args: db: Database session user_id: User ID to delete current_admin_id: Super admin making the deletion Raises: CannotModifySelfException: If trying to delete self ValidationException: If user not found or not an admin """ if user_id == current_admin_id: raise CannotModifySelfException( user_id=user_id, operation="delete own account", ) admin = db.query(User).filter( User.id == user_id, User.role.in_(["super_admin", "platform_admin"]), ).first() if not admin: raise UserNotFoundException(str(user_id)) username = admin.username # Delete admin platform assignments first db.query(AdminPlatform).filter(AdminPlatform.user_id == user_id).delete() # Delete the admin user db.delete(admin) db.flush() logger.info(f"Admin {username} deleted by admin {current_admin_id}") # Singleton instance admin_platform_service = AdminPlatformService()