from __future__ import annotations from typing import Any from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.views import redirect_to_login from django.core.exceptions import PermissionDenied from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect from django.urls import reverse from django.conf import settings from labs.models import AdminOTPDevice from ..sudo import is_sudo_active class AdminBaseViewMixin(UserPassesTestMixin): """ Base mixin for admin views requiring staff access and OTP sudo authentication. This mixin replaces the previous VPN-based access control with TOTP-based sudo authentication. Staff users must: 1. Be authenticated with an active account 2. Have OTP set up and confirmed 3. Have an active sudo session (verified via OTP within the session duration) """ login_url = settings.LOGIN_URL redirect_field_name = "redirect_uri" def test_func(self) -> bool: """Check if user is authenticated and is staff.""" user = self.request.user return bool(user and user.is_active and user.is_staff) def handle_no_permission(self) -> HttpResponse: if not self.request.user.is_authenticated: return redirect_to_login( self.request.get_full_path(), str(self.login_url), self.redirect_field_name, ) raise PermissionDenied def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: """ Check OTP setup and sudo status before processing the request. This method intercepts requests to: 1. Redirect to OTP setup if user hasn't configured OTP 2. Redirect to OTP verification if sudo session is expired IMPORTANT: OTP checks must run BEFORE business logic to prevent unauthorized access to admin functionality. """ user = request.user # Skip OTP checks for unauthenticated users (will be handled by test_func) if not user.is_authenticated: return super().dispatch(request, *args, **kwargs) # Run test_func check first (is_active, is_staff) if not self.test_func(): return self.handle_no_permission() # Check if user has OTP device set up try: device = user.admin_otp_device if not device.is_confirmed: # OTP device exists but not confirmed, redirect to setup return redirect("labs_admin:otp_setup") except AdminOTPDevice.DoesNotExist: # No OTP device, redirect to setup return redirect("labs_admin:otp_setup") # Check if sudo session is active if not is_sudo_active(request): # Build the verify URL with next parameter verify_url = reverse("labs_admin:otp_verify") current_path = request.get_full_path() return redirect(f"{verify_url}?next={current_path}") # All checks passed, now execute the view return super().dispatch(request, *args, **kwargs) class PermissionRequiredAdminMixin(AdminBaseViewMixin): """Admin view mixin with permission check.""" permission_required: str | list[str] | None = None def test_func(self) -> bool: if not super().test_func(): return False if self.permission_required is None: return True if isinstance(self.permission_required, str): return self.request.user.has_perm(self.permission_required) return self.request.user.has_perms(self.permission_required)