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):
"""
Allows multiple rules in a single decorator call:
@grant_access(IS_STAFF, IS_SUPERUSER, IS_DOMAIN_MANAGER)
or multiple stacked decorators:
@grant_access(IS_SUPERUSER)
@grant_access(IS_DOMAIN_MANAGER)
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)
- Stacked decorators for separate rules:
@grant_access(IS_SUPERUSER)
@grant_access(IS_DOMAIN_MANAGER)
The decorator supports both function-based views (FBVs) and class-based views (CBVs).
"""
def decorator(view):
if isinstance(view, type): # If decorating a class-based view (CBV)
original_dispatch = view.dispatch # save original dispatch method
if isinstance(view, type): # Check if decorating a class-based view (CBV)
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):
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)
view.dispatch = wrapped_dispatch # replace dispatch with wrapped version
view.dispatch = wrapped_dispatch # Replace the dispatch method
return view
else: # If decorating a function-based view (FBV)
view.has_explicit_access = True
existing_rules = getattr(view, "_access_rules", set())
existing_rules.update(rules)
view._access_rules = existing_rules
view.has_explicit_access = True # Mark the view as having explicit access control
existing_rules = getattr(view, "_access_rules", set()) # Retrieve existing rules
existing_rules.update(rules) # Merge with new rules
view._access_rules = existing_rules # Store updated rules
@functools.wraps(view)
def wrapper(request, *args, **kwargs):
if not _user_has_permission(request.user, request, rules, **kwargs):
raise PermissionDenied
return view(request, *args, **kwargs)
raise PermissionDenied # Deny access if the user lacks permission
return view(request, *args, **kwargs) # Proceed with the original view
return wrapper
@ -63,7 +69,21 @@ def grant_access(*rules):
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
@ -86,7 +106,7 @@ def _user_has_permission(user, request, rules, **kwargs):
(IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)),
(
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,
@ -268,11 +288,9 @@ def _is_staff_managing_domain(request, **kwargs):
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
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")
if request.user.has_view_all_domains_portfolio_permission(portfolio):
if Domain.objects.filter(id=domain_pk).exists():

View file

@ -177,40 +177,48 @@ class CheckPortfolioMiddleware:
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):
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", [])]
def __call__(self, request):
# Allow Django Debug Toolbar requests
# Allow requests to Django Debug Toolbar
if request.path.startswith("/__debug__/"):
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):
return self.get_response(request)
# Try to resolve the view function
# Attempt to resolve the request path to a view function
try:
resolver_match = resolve(request.path_info)
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:
# If resolution fails, allow the request to proceed (avoid blocking non-view routes)
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":
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:
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):
raise PermissionDenied
raise PermissionDenied # Deny access if the view lacks explicit permission handling
return self.get_response(request)