updated comments

This commit is contained in:
David Kennedy 2025-02-13 06:47:11 -05:00
parent ed683baaab
commit 37c6731001
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
2 changed files with 56 additions and 30 deletions

View file

@ -24,37 +24,43 @@ HAS_PORTFOLIO_MEMBERS_VIEW = "has_portfolio_members_view"
def grant_access(*rules): def grant_access(*rules):
""" """
Allows multiple rules in a single decorator call: A decorator that enforces access control based on specified rules.
Usage:
- Multiple rules in a single decorator:
@grant_access(IS_STAFF, IS_SUPERUSER, IS_DOMAIN_MANAGER) @grant_access(IS_STAFF, IS_SUPERUSER, IS_DOMAIN_MANAGER)
or multiple stacked decorators:
- Stacked decorators for separate rules:
@grant_access(IS_SUPERUSER) @grant_access(IS_SUPERUSER)
@grant_access(IS_DOMAIN_MANAGER) @grant_access(IS_DOMAIN_MANAGER)
The decorator supports both function-based views (FBVs) and class-based views (CBVs).
""" """
def decorator(view): def decorator(view):
if isinstance(view, type): # If decorating a class-based view (CBV) if isinstance(view, type): # Check if decorating a class-based view (CBV)
original_dispatch = view.dispatch # save original dispatch method original_dispatch = view.dispatch # Store the original dispatch method
@method_decorator(grant_access(*rules)) # apply the decorator to dispatch @method_decorator(grant_access(*rules)) # Apply the decorator to dispatch
def wrapped_dispatch(self, request, *args, **kwargs): def wrapped_dispatch(self, request, *args, **kwargs):
if not _user_has_permission(request.user, request, rules, **kwargs): if not _user_has_permission(request.user, request, rules, **kwargs):
raise PermissionDenied raise PermissionDenied # Deny access if the user lacks permission
return original_dispatch(self, request, *args, **kwargs) return original_dispatch(self, request, *args, **kwargs)
view.dispatch = wrapped_dispatch # replace dispatch with wrapped version view.dispatch = wrapped_dispatch # Replace the dispatch method
return view return view
else: # If decorating a function-based view (FBV) else: # If decorating a function-based view (FBV)
view.has_explicit_access = True view.has_explicit_access = True # Mark the view as having explicit access control
existing_rules = getattr(view, "_access_rules", set()) existing_rules = getattr(view, "_access_rules", set()) # Retrieve existing rules
existing_rules.update(rules) existing_rules.update(rules) # Merge with new rules
view._access_rules = existing_rules view._access_rules = existing_rules # Store updated rules
@functools.wraps(view) @functools.wraps(view)
def wrapper(request, *args, **kwargs): def wrapper(request, *args, **kwargs):
if not _user_has_permission(request.user, request, rules, **kwargs): if not _user_has_permission(request.user, request, rules, **kwargs):
raise PermissionDenied raise PermissionDenied # Deny access if the user lacks permission
return view(request, *args, **kwargs) return view(request, *args, **kwargs) # Proceed with the original view
return wrapper return wrapper
@ -63,7 +69,21 @@ def grant_access(*rules):
def _user_has_permission(user, request, rules, **kwargs): def _user_has_permission(user, request, rules, **kwargs):
""" """
Checks if the user meets the permission requirements. Determines if the user meets the required permission rules.
This function evaluates a set of predefined permission rules to check whether a user has access
to a specific view. It supports various access control conditions, including staff status,
domain management roles, and portfolio-related permissions.
Parameters:
- user: The user requesting access.
- request: The HTTP request object.
- rules: A set of access control rules to evaluate.
- **kwargs: Additional keyword arguments used in specific permission checks.
Returns:
- True if the user satisfies any of the specified rules.
- False otherwise.
""" """
# Skip authentication if @login_not_required is applied # Skip authentication if @login_not_required is applied
@ -86,7 +106,7 @@ def _user_has_permission(user, request, rules, **kwargs):
(IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)), (IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)),
( (
HAS_PORTFOLIO_DOMAINS_VIEW_ALL, HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
lambda: _can_access_domain_via_portfolio_view_all_domains(request, kwargs.get("domain_pk")), lambda: _has_portfolio_view_all_domains(request, kwargs.get("domain_pk")),
), ),
( (
HAS_PORTFOLIO_DOMAINS_ANY_PERM, HAS_PORTFOLIO_DOMAINS_ANY_PERM,
@ -268,11 +288,9 @@ def _is_staff_managing_domain(request, **kwargs):
return True return True
def _can_access_domain_via_portfolio_view_all_domains(request, domain_pk): def _has_portfolio_view_all_domains(request, domain_pk):
"""Returns whether the user in the request can access the domain """Returns whether the user in the request can access the domain
via portfolio view all domains permission.""" via portfolio view all domains permission."""
# NOTE: determine if in practice this ever needs to be called on its own
# or if it can be combined with view_managed_domains
portfolio = request.session.get("portfolio") portfolio = request.session.get("portfolio")
if request.user.has_view_all_domains_portfolio_permission(portfolio): if request.user.has_view_all_domains_portfolio_permission(portfolio):
if Domain.objects.filter(id=domain_pk).exists(): if Domain.objects.filter(id=domain_pk).exists():

View file

@ -177,40 +177,48 @@ class CheckPortfolioMiddleware:
class RestrictAccessMiddleware: class RestrictAccessMiddleware:
"""Middleware that blocks all views unless explicitly permitted""" """
Middleware that blocks access to all views unless explicitly permitted.
This middleware enforces authentication by default. Views must explicitly allow access
using access control mechanisms such as the `@grant_access` decorator. Exceptions are made
for Django admin views, explicitly ignored paths, and views that opt out of login requirements.
"""
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
# Compile regex patterns from settings to identify paths that bypass login requirements
self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])] self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])]
def __call__(self, request): def __call__(self, request):
# Allow Django Debug Toolbar requests # Allow requests to Django Debug Toolbar
if request.path.startswith("/__debug__/"): if request.path.startswith("/__debug__/"):
return self.get_response(request) return self.get_response(request)
# Allow requests that match LOGIN_REQUIRED_IGNORE_PATHS # Allow requests matching configured ignored paths
if any(pattern.match(request.path) for pattern in self.ignored_paths): if any(pattern.match(request.path) for pattern in self.ignored_paths):
return self.get_response(request) return self.get_response(request)
# Try to resolve the view function # Attempt to resolve the request path to a view function
try: try:
resolver_match = resolve(request.path_info) resolver_match = resolve(request.path_info)
view_func = resolver_match.func view_func = resolver_match.func
app_name = resolver_match.app_name # Get app name of resolved view app_name = resolver_match.app_name # Get the app name of the resolved view
except Exception: except Exception:
# If resolution fails, allow the request to proceed (avoid blocking non-view routes)
return self.get_response(request) return self.get_response(request)
# Auto-allow Django's built-in admin views (but NOT custom /admin/* views) # Automatically allow access to Django's built-in admin views (excluding custom /admin/* views)
if app_name == "admin": if app_name == "admin":
return self.get_response(request) return self.get_response(request)
# Skip access restriction if the view explicitly allows unauthenticated access # Allow access if the view explicitly opts out of login requirements
if getattr(view_func, "login_required", True) is False: if getattr(view_func, "login_required", True) is False:
return self.get_response(request) return self.get_response(request)
# Enforce explicit access fules for other views # Restrict access to views that do not explicitly declare access rules
if not getattr(view_func, "has_explicit_access", False): if not getattr(view_func, "has_explicit_access", False):
raise PermissionDenied raise PermissionDenied # Deny access if the view lacks explicit permission handling
return self.get_response(request) return self.get_response(request)