mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-25 20:18:38 +02:00
wip
This commit is contained in:
parent
303b74c458
commit
d84aa421d9
8 changed files with 121 additions and 2 deletions
|
@ -5,12 +5,14 @@ import logging
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth import authenticate, login
|
||||
from login_required import login_not_required
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
|
||||
from djangooidc.oidc import Client
|
||||
from djangooidc import exceptions as o_e
|
||||
from registrar.decorators import grant_access
|
||||
from registrar.models import User
|
||||
from registrar.views.utility.error_views import custom_500_error_view, custom_401_error_view
|
||||
|
||||
|
|
|
@ -200,6 +200,8 @@ MIDDLEWARE = [
|
|||
"waffle.middleware.WaffleMiddleware",
|
||||
"registrar.registrar_middleware.CheckUserProfileMiddleware",
|
||||
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||
# Restrict access using Opt-Out approach
|
||||
"registrar.registrar_middleware.RestrictAccessMiddleware",
|
||||
]
|
||||
|
||||
# application object used by Django's built-in servers (e.g. `runserver`)
|
||||
|
|
72
src/registrar/decorators.py
Normal file
72
src/registrar/decorators.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from functools import wraps
|
||||
from django.http import JsonResponse
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
|
||||
# Constants for clarity
|
||||
ALL = "all"
|
||||
IS_SUPERUSER = "is_superuser"
|
||||
IS_STAFF = "is_staff"
|
||||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||
|
||||
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)
|
||||
"""
|
||||
|
||||
def decorator(view_func):
|
||||
view_func.has_explicit_access = True # Mark as explicitly access-controlled
|
||||
existing_rules = getattr(view_func, "_access_rules", set())
|
||||
existing_rules.update(rules) # Support multiple rules in one call
|
||||
view_func._access_rules = existing_rules # Store rules on the function
|
||||
|
||||
@wraps(view_func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
user = request.user
|
||||
|
||||
# Skip authentication if @login_not_required is applied
|
||||
if getattr(view_func, "login_not_required", False):
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
# Allow everyone if `ALL` is in rules
|
||||
if ALL in view_func._access_rules:
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
# Ensure user is authenticated
|
||||
if not user.is_authenticated:
|
||||
return JsonResponse({"error": "Authentication required"}, status=403)
|
||||
|
||||
conditions_met = []
|
||||
|
||||
if IS_STAFF in view_func._access_rules:
|
||||
conditions_met.append(user.is_staff)
|
||||
|
||||
if not any(conditions_met) and IS_SUPERUSER in view_func._access_rules:
|
||||
conditions_met.append(user.is_superuser)
|
||||
|
||||
if not any(conditions_met) and IS_DOMAIN_MANAGER in view_func._access_rules:
|
||||
domain_id = kwargs.get('pk') or kwargs.get('domain_id')
|
||||
if not domain_id:
|
||||
return JsonResponse({"error": "Domain ID missing"}, status=400)
|
||||
try:
|
||||
domain = Domain.objects.get(pk=domain_id)
|
||||
has_permission = UserDomainRole.objects.filter(
|
||||
user=user, domain=domain
|
||||
).exists()
|
||||
conditions_met.append(has_permission)
|
||||
except ObjectDoesNotExist:
|
||||
return JsonResponse({"error": "Invalid Domain"}, status=404)
|
||||
|
||||
if not any(conditions_met):
|
||||
return JsonResponse({"error": "Access Denied"}, status=403)
|
||||
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
|
@ -3,9 +3,13 @@ Contains middleware used in settings.py
|
|||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from urllib.parse import parse_qs
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.http import JsonResponse
|
||||
from django.urls import resolve
|
||||
from registrar.models import User
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
|
@ -170,3 +174,38 @@ class CheckPortfolioMiddleware:
|
|||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
else:
|
||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
|
||||
|
||||
class RestrictAccessMiddleware:
|
||||
""" Middleware that blocks all views unless explicitly permitted """
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])]
|
||||
|
||||
def __call__(self, request):
|
||||
# Allow requests that match LOGIN_REQUIRED_IGNORE_PATHS
|
||||
if any(pattern.match(request.path) for pattern in self.ignored_paths):
|
||||
return self.get_response(request)
|
||||
|
||||
# Try to resolve the 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
|
||||
except Exception:
|
||||
return JsonResponse({"error": "Not Found"}, status=404)
|
||||
|
||||
# Auto-allow Django's built-in admin views (but NOT custom /admin/* views)
|
||||
if app_name == "admin":
|
||||
return self.get_response(request)
|
||||
|
||||
# Skip access restriction if the view explicitly allows unauthenticated access
|
||||
if getattr(view_func, "login_required", True) is False:
|
||||
return self.get_response(request)
|
||||
|
||||
# Enforce explicit access fules for other views
|
||||
if not getattr(view_func, "has_explicit_access", False):
|
||||
return JsonResponse({"error": "Access Denied"}, status=403)
|
||||
|
||||
return self.get_response(request)
|
0
src/registrar/utility/domain_cache_helper.py
Normal file
0
src/registrar/utility/domain_cache_helper.py
Normal file
|
@ -1,5 +1,6 @@
|
|||
from django.http import JsonResponse
|
||||
from django.core.paginator import Paginator
|
||||
from registrar.decorators import grant_access, ALL
|
||||
from registrar.models import DomainRequest
|
||||
from django.utils.dateformat import format
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
@ -7,7 +8,7 @@ from django.urls import reverse
|
|||
from django.db.models import Q
|
||||
|
||||
|
||||
@login_required
|
||||
@grant_access(ALL)
|
||||
def get_domain_requests_json(request):
|
||||
"""Given the current request,
|
||||
get all domain requests that are associated with the request user and exclude the APPROVED ones.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.core.paginator import Paginator
|
||||
from registrar.decorators import grant_access, ALL
|
||||
from registrar.models import UserDomainRole, Domain, DomainInformation, User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
|
@ -9,7 +10,7 @@ from django.db.models import Q
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
@grant_access(ALL)
|
||||
def get_domains_json(request):
|
||||
"""Given the current request,
|
||||
get all domains that are associated with the UserDomainRole object"""
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
from registrar.decorators import grant_access, ALL
|
||||
|
||||
@grant_access(ALL)
|
||||
def index(request):
|
||||
"""This page is available to anyone without logging in."""
|
||||
context = {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue