diff --git a/src/registrar/assets/js/uswds-edited.js b/src/registrar/assets/js/uswds-edited.js index 9d4dd2e51..ae246b05c 100644 --- a/src/registrar/assets/js/uswds-edited.js +++ b/src/registrar/assets/js/uswds-edited.js @@ -5695,19 +5695,35 @@ const createHeaderButton = (header, headerName) => { buttonEl.setAttribute("tabindex", "0"); buttonEl.classList.add(SORT_BUTTON_CLASS); // ICON_SOURCE + // ---- END DOTGOV EDIT + // Change icons on sort, use source from arro_upward and arrow_downward + // buttonEl.innerHTML = Sanitizer.escapeHTML` + // + // + // + // + // + // + // + // + // + // + // + // `; buttonEl.innerHTML = Sanitizer.escapeHTML` - + - + `; + // ---- END DOTGOV EDIT header.appendChild(buttonEl); updateSortLabel(header, headerName); }; diff --git a/src/registrar/assets/src/js/getgov/domain-request-form.js b/src/registrar/assets/src/js/getgov/domain-request-form.js index d9b660a50..b49912fa4 100644 --- a/src/registrar/assets/src/js/getgov/domain-request-form.js +++ b/src/registrar/assets/src/js/getgov/domain-request-form.js @@ -2,11 +2,41 @@ import { submitForm } from './helpers.js'; export function initDomainRequestForm() { document.addEventListener('DOMContentLoaded', function() { - const button = document.getElementById("domain-request-form-submit-button"); - if (button) { - button.addEventListener("click", function () { - submitForm("submit-domain-request-form"); - }); - } + // These are the request steps in DomainRequestWizard, such as current_websites or review + initRequestStepCurrentWebsitesListener(); + initRequestStepReviewListener(); }); +} + +function initRequestStepReviewListener() { + const button = document.getElementById("domain-request-form-submit-button"); + if (button) { + button.addEventListener("click", function () { + submitForm("submit-domain-request-form"); + }); + } +} + +function initRequestStepCurrentWebsitesListener() { + //register-form-step + const addAnotherSiteButton = document.getElementById("submit-domain-request--site-button"); + if (addAnotherSiteButton) { + // Check for focus state in sessionStorage + const focusTarget = sessionStorage.getItem("lastFocusedElement"); + if (focusTarget) { + document.querySelector(focusTarget)?.focus(); + } + // Add form submit handler to store focus state + const form = document.querySelector("form"); + if (form) { + form.addEventListener("submit", () => { + const activeElement = document.activeElement; + if (activeElement) { + sessionStorage.setItem("lastFocusedElement", "#" + activeElement.id); + } + }); + } + // We only want to do this action once, so we clear out the session + sessionStorage.removeItem("lastFocusedElement"); + } } \ No newline at end of file diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index a15d1eabe..9a00cf022 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -520,15 +520,6 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too } } -.module--custom { - a { - font-size: 13px; - font-weight: 600; - border: solid 1px var(--darkened-bg); - background: var(--darkened-bg); - } -} - .usa-modal--django-admin .usa-prose ul > li { list-style-type: inherit; // Styling based off of the

styling in django admin @@ -839,6 +830,17 @@ div.dja__model-description{ text-transform: capitalize; } +.module caption { + // Match the old

size for django admin + font-size: 0.8125rem; +} + +// text-bold doesn't work here due to style overrides, unfortunately. +// This is a workaround. +caption.text-bold { + font-weight: font-weight('bold'); +} + .wrapped-button-group { // This button group has too many items flex-wrap: wrap; diff --git a/src/registrar/assets/src/sass/_theme/_tables.scss b/src/registrar/assets/src/sass/_theme/_tables.scss index 37ae22b1b..614d0348e 100644 --- a/src/registrar/assets/src/sass/_theme/_tables.scss +++ b/src/registrar/assets/src/sass/_theme/_tables.scss @@ -41,13 +41,8 @@ th { } } -// The member table has an extra "expand" row, which looks like a single row. -// But the DOM disagrees - so we basically need to hide the border on both rows. -#members__table-wrapper .dotgov-table tr:nth-last-child(2) td, -#members__table-wrapper .dotgov-table tr:nth-last-child(2) th { - border-bottom: none; -} - +// .dotgov-table allows us to customize .usa-table on the user-facing pages, +// while leaving the default styles for use on the admin pages .dotgov-table { width: 100%; @@ -68,7 +63,8 @@ th { border-bottom: 1px solid color('base-lighter'); } - thead th { + thead th, + thead th[aria-sort] { color: color('primary-darker'); border-bottom: 2px solid color('base-light'); } @@ -93,17 +89,46 @@ th { } } - @include at-media(tablet-lg) { - th[data-sortable] .usa-table__header__button { - right: auto; - - &[aria-sort=ascending], - &[aria-sort=descending], - &:not([aria-sort]) { - right: auto; + // Sortable headers + th[data-sortable][aria-sort=ascending], + th[data-sortable][aria-sort=descending] { + background-color: transparent; + .usa-table__header__button { + background-color: color('accent-cool-lightest'); + border-radius: units(.5); + color: color('primary-darker'); + &:hover { + background-color: color('accent-cool-lightest'); } } } + @include at-media(tablet-lg) { + th[data-sortable]:not(.left-align-sort-button) .usa-table__header__button { + // position next to the copy + right: auto; + // slide left to mock a margin between the copy and the icon + transform: translateX(units(1)); + // fix vertical alignment + top: units(1.5); + } + th[data-sortable].left-align-sort-button .usa-table__header__button { + left: 0; + } + } + + // Currently the 'flash' when sort is clicked, + // this will become persistent if the double-sort bug is fixed + td[data-sort-active], + th[data-sort-active] { + background-color: color('primary-lightest'); + } +} + +// The member table has an extra "expand" row, which looks like a single row. +// But the DOM disagrees - so we basically need to hide the border on both rows. +#members__table-wrapper .dotgov-table tr:nth-last-child(2) td, +#members__table-wrapper .dotgov-table tr:nth-last-child(2) th { + border-bottom: none; } .dotgov-table--cell-padding-2 { diff --git a/src/registrar/assets/src/sass/_theme/_uswds-theme.scss b/src/registrar/assets/src/sass/_theme/_uswds-theme.scss index 21bb48e96..951141a2b 100644 --- a/src/registrar/assets/src/sass/_theme/_uswds-theme.scss +++ b/src/registrar/assets/src/sass/_theme/_uswds-theme.scss @@ -70,6 +70,7 @@ in the form $setting: value, ----------------------------*/ $theme-font-weight-medium: 400, $theme-font-weight-semibold: 600, + $theme-font-weight-bold: 700, /*--------------------------- ## Font roles diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index fa4c2d8dc..acd319ee5 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -201,6 +201,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`) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index eb095c5ca..12efe5a9f 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -68,7 +68,7 @@ for step, view in [ (PortfolioDomainRequestStep.REQUESTING_ENTITY, views.RequestingEntity), (PortfolioDomainRequestStep.ADDITIONAL_DETAILS, views.PortfolioAdditionalDetails), ]: - domain_request_urls.append(path(f"/{step}/", view.as_view(), name=step)) + domain_request_urls.append(path(f"/{step}/", view.as_view(), name=step)) urlpatterns = [ @@ -260,27 +260,27 @@ urlpatterns = [ name="export_data_type_user", ), path( - "domain-request//edit/", + "domain-request//edit/", views.DomainRequestWizard.as_view(), name=views.DomainRequestWizard.EDIT_URL_NAME, ), path( - "domain-request/", + "domain-request/", views.DomainRequestStatus.as_view(), name="domain-request-status", ), path( - "domain-request/viewonly/", + "domain-request/viewonly/", views.PortfolioDomainRequestStatusViewOnly.as_view(), name="domain-request-status-viewonly", ), path( - "domain-request//withdraw", + "domain-request//withdraw", views.DomainRequestWithdrawConfirmation.as_view(), name="domain-request-withdraw-confirmation", ), path( - "domain-request//withdrawconfirmed", + "domain-request//withdrawconfirmed", views.DomainRequestWithdrawn.as_view(), name="domain-request-withdrawn", ), @@ -296,56 +296,60 @@ urlpatterns = [ lambda r: always_404(r, "We forgot to include this link, sorry."), name="todo", ), - path("domain/", views.DomainView.as_view(), name="domain"), - path("domain//prototype-dns", views.PrototypeDomainDNSRecordView.as_view(), name="prototype-domain-dns"), - path("domain//users", views.DomainUsersView.as_view(), name="domain-users"), + path("domain/", views.DomainView.as_view(), name="domain"), path( - "domain//dns", + "domain//prototype-dns", + views.PrototypeDomainDNSRecordView.as_view(), + name="prototype-domain-dns", + ), + path("domain//users", views.DomainUsersView.as_view(), name="domain-users"), + path( + "domain//dns", views.DomainDNSView.as_view(), name="domain-dns", ), path( - "domain//dns/nameservers", + "domain//dns/nameservers", views.DomainNameserversView.as_view(), name="domain-dns-nameservers", ), path( - "domain//dns/dnssec", + "domain//dns/dnssec", views.DomainDNSSECView.as_view(), name="domain-dns-dnssec", ), path( - "domain//dns/dnssec/dsdata", + "domain//dns/dnssec/dsdata", views.DomainDsDataView.as_view(), name="domain-dns-dnssec-dsdata", ), path( - "domain//org-name-address", + "domain//org-name-address", views.DomainOrgNameAddressView.as_view(), name="domain-org-name-address", ), path( - "domain//suborganization", + "domain//suborganization", views.DomainSubOrganizationView.as_view(), name="domain-suborganization", ), path( - "domain//senior-official", + "domain//senior-official", views.DomainSeniorOfficialView.as_view(), name="domain-senior-official", ), path( - "domain//security-email", + "domain//security-email", views.DomainSecurityEmailView.as_view(), name="domain-security-email", ), path( - "domain//renewal", + "domain//renewal", views.DomainRenewalView.as_view(), name="domain-renewal", ), path( - "domain//users/add", + "domain//users/add", views.DomainAddUserView.as_view(), name="domain-users-add", ), @@ -360,17 +364,17 @@ urlpatterns = [ name="user-profile", ), path( - "invitation//cancel", + "invitation//cancel", views.DomainInvitationCancelView.as_view(http_method_names=["post"]), name="invitation-cancel", ), path( - "domain-request//delete", + "domain-request//delete", views.DomainRequestDeleteView.as_view(http_method_names=["post"]), name="domain-request-delete", ), path( - "domain//users//delete", + "domain//users//delete", views.DomainDeleteUserView.as_view(http_method_names=["post"]), name="domain-user-delete", ), @@ -392,6 +396,7 @@ urlpatterns = [ # This way, we can share a view for djangooidc, and other pages as we see fit. handler500 = "registrar.views.utility.error_views.custom_500_error_view" handler403 = "registrar.views.utility.error_views.custom_403_error_view" +handler404 = "registrar.views.utility.error_views.custom_404_error_view" # we normally would guard these with `if settings.DEBUG` but tests run with # DEBUG = False even when these apps have been loaded because settings.DEBUG diff --git a/src/registrar/decorators.py b/src/registrar/decorators.py new file mode 100644 index 000000000..7cb2792f4 --- /dev/null +++ b/src/registrar/decorators.py @@ -0,0 +1,300 @@ +import functools +from django.core.exceptions import PermissionDenied +from django.utils.decorators import method_decorator +from registrar.models import Domain, DomainInformation, DomainInvitation, DomainRequest, UserDomainRole + +# Constants for clarity +ALL = "all" +IS_STAFF = "is_staff" +IS_DOMAIN_MANAGER = "is_domain_manager" +IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator" +IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain" +IS_PORTFOLIO_MEMBER = "is_portfolio_member" +IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager" +IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member" +HAS_PORTFOLIO_DOMAINS_ANY_PERM = "has_portfolio_domains_any_perm" +HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all" +HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM = "has_portfolio_domain_requests_any_perm" +HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all" +HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT = "has_portfolio_domain_requests_edit" +HAS_PORTFOLIO_MEMBERS_ANY_PERM = "has_portfolio_members_any_perm" +HAS_PORTFOLIO_MEMBERS_EDIT = "has_portfolio_members_edit" +HAS_PORTFOLIO_MEMBERS_VIEW = "has_portfolio_members_view" + + +def grant_access(*rules): + """ + 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): # 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 + def wrapped_dispatch(self, request, *args, **kwargs): + if not _user_has_permission(request.user, request, rules, **kwargs): + raise PermissionDenied # Deny access if the user lacks permission + return original_dispatch(self, request, *args, **kwargs) + + view.dispatch = wrapped_dispatch # Replace the dispatch method + return view + + else: # If decorating a function-based view (FBV) + 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 # Deny access if the user lacks permission + return view(request, *args, **kwargs) # Proceed with the original view + + return wrapper + + return decorator + + +def _user_has_permission(user, request, rules, **kwargs): + """ + 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 + if getattr(request, "login_not_required", False): + return True + + # Allow everyone if `ALL` is in rules + if ALL in rules: + return True + + # Ensure user is authenticated and not restricted + if not user.is_authenticated or user.is_restricted(): + return False + + # Define permission checks + permission_checks = [ + (IS_STAFF, lambda: user.is_staff), + (IS_DOMAIN_MANAGER, lambda: _is_domain_manager(user, **kwargs)), + (IS_STAFF_MANAGING_DOMAIN, lambda: _is_staff_managing_domain(request, **kwargs)), + (IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)), + ( + HAS_PORTFOLIO_DOMAINS_VIEW_ALL, + lambda: _has_portfolio_view_all_domains(request, kwargs.get("domain_pk")), + ), + ( + HAS_PORTFOLIO_DOMAINS_ANY_PERM, + lambda: user.is_org_user(request) + and user.has_any_domains_portfolio_permission(request.session.get("portfolio")), + ), + ( + IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, + lambda: _is_domain_manager(user, **kwargs) and _is_portfolio_member(request), + ), + ( + IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, + lambda: _is_domain_manager(user, **kwargs) and not _is_portfolio_member(request), + ), + ( + IS_DOMAIN_REQUEST_CREATOR, + lambda: _is_domain_request_creator(user, kwargs.get("domain_request_pk")) + and not _is_portfolio_member(request), + ), + ( + HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM, + lambda: user.is_org_user(request) + and user.has_any_requests_portfolio_permission(request.session.get("portfolio")), + ), + ( + HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL, + lambda: user.is_org_user(request) + and user.has_view_all_domain_requests_portfolio_permission(request.session.get("portfolio")), + ), + ( + HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, + lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk")), + ), + ( + HAS_PORTFOLIO_MEMBERS_ANY_PERM, + lambda: user.is_org_user(request) + and ( + user.has_view_members_portfolio_permission(request.session.get("portfolio")) + or user.has_edit_members_portfolio_permission(request.session.get("portfolio")) + ), + ), + ( + HAS_PORTFOLIO_MEMBERS_EDIT, + lambda: user.is_org_user(request) + and user.has_edit_members_portfolio_permission(request.session.get("portfolio")), + ), + ( + HAS_PORTFOLIO_MEMBERS_VIEW, + lambda: user.is_org_user(request) + and user.has_view_members_portfolio_permission(request.session.get("portfolio")), + ), + ] + + # Check conditions iteratively + return any(check() for rule, check in permission_checks if rule in rules) + + +def _has_portfolio_domain_requests_edit(user, request, domain_request_id): + if domain_request_id and not _is_domain_request_creator(user, domain_request_id): + return False + return user.is_org_user(request) and user.has_edit_request_portfolio_permission(request.session.get("portfolio")) + + +def _is_domain_manager(user, **kwargs): + """ + Determines if the given user is a domain manager for a specified domain. + + - First, it checks if 'domain_pk' is present in the URL parameters. + - If 'domain_pk' exists, it verifies if the user has a domain role for that domain. + - If 'domain_pk' is absent, it checks for 'domain_invitation_pk' to determine if the user + has domain permissions through an invitation. + + Returns: + bool: True if the user is a domain manager, False otherwise. + """ + domain_id = kwargs.get("domain_pk") + if domain_id: + return UserDomainRole.objects.filter(user=user, domain_id=domain_id).exists() + domain_invitation_id = kwargs.get("domain_invitation_pk") + if domain_invitation_id: + return DomainInvitation.objects.filter(id=domain_invitation_id, domain__permissions__user=user).exists() + return False + + +def _is_domain_request_creator(user, domain_request_pk): + """Checks to see if the user is the creator of a domain request + with domain_request_pk.""" + if domain_request_pk: + return DomainRequest.objects.filter(creator=user, id=domain_request_pk).exists() + return True + + +def _is_portfolio_member(request): + """Checks to see if the user in the request is a member of the + portfolio in the request's session.""" + return request.user.is_org_user(request) + + +def _is_staff_managing_domain(request, **kwargs): + """ + Determines whether a staff user (analyst or superuser) has permission to manage a domain + that they did not create or were not invited to. + + The function enforces: + 1. **User Authorization** - The user must have `analyst_access_permission` or `full_access_permission`. + 2. **Valid Session Context** - The user must have explicitly selected the domain for management + via an 'analyst action' (e.g., by clicking 'Manage Domain' in the admin interface). + 3. **Domain Status Check** - Only domains in specific statuses (e.g., APPROVED, IN_REVIEW, etc.) + can be managed, except in cases where the domain lacks a status due to errors. + + Process: + - First, the function retrieves the `domain_pk` from the URL parameters. + - If `domain_pk` is not provided, it attempts to resolve the domain via `domain_invitation_pk`. + - It checks if the user has the required permissions. + - It verifies that the user has an active 'analyst action' session for the domain. + - Finally, it ensures that the domain is in a status that allows management. + + Returns: + bool: True if the user is allowed to manage the domain, False otherwise. + """ + + domain_id = kwargs.get("domain_pk") + if not domain_id: + domain_invitation_id = kwargs.get("domain_invitation_pk") + domain_invitation = DomainInvitation.objects.filter(id=domain_invitation_id).first() + if domain_invitation: + domain_id = domain_invitation.domain_id + + # Check if the request user is permissioned... + user_is_analyst_or_superuser = request.user.has_perm( + "registrar.analyst_access_permission" + ) or request.user.has_perm("registrar.full_access_permission") + + if not user_is_analyst_or_superuser: + return False + + # Check if the user is attempting a valid edit action. + # In other words, if the analyst/admin did not click + # the 'Manage Domain' button in /admin, + # then they cannot access this page. + session = request.session + can_do_action = ( + "analyst_action" in session + and "analyst_action_location" in session + and session["analyst_action_location"] == domain_id + ) + + if not can_do_action: + return False + + # Analysts may manage domains, when they are in these statuses: + valid_domain_statuses = [ + DomainRequest.DomainRequestStatus.APPROVED, + DomainRequest.DomainRequestStatus.IN_REVIEW, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.DomainRequestStatus.ACTION_NEEDED, + # Edge case - some domains do not have + # a status or DomainInformation... aka a status of 'None'. + # It is necessary to access those to correct errors. + None, + ] + + requested_domain = DomainInformation.objects.filter(domain_id=domain_id).first() + + # if no domain information or domain request exist, the user + # should be able to manage the domain; however, if domain information + # and domain request exist, and domain request is not in valid status, + # user should not be able to manage domain + if ( + requested_domain + and requested_domain.domain_request + and requested_domain.domain_request.status not in valid_domain_statuses + ): + return False + + # Valid session keys exist, + # the user is permissioned, + # and it is in a valid status + return True + + +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.""" + portfolio = request.session.get("portfolio") + if request.user.has_view_all_domains_portfolio_permission(portfolio): + if Domain.objects.filter(id=domain_pk).exists(): + domain = Domain.objects.get(id=domain_pk) + if domain.domain_info.portfolio == portfolio: + return True + return False diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index ed7a4dffc..8c9c79876 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -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.core.exceptions import PermissionDenied from django.urls import reverse from django.http import HttpResponseRedirect +from django.urls import resolve from registrar.models import User from waffle.decorators import flag_is_active @@ -170,3 +174,51 @@ class CheckPortfolioMiddleware: request.session["portfolio"] = request.user.get_first_portfolio() else: request.session["portfolio"] = request.user.get_first_portfolio() + + +class RestrictAccessMiddleware: + """ + 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 requests to Django Debug Toolbar + if request.path.startswith("/__debug__/"): + return self.get_response(request) + + # Allow requests matching configured ignored paths + if any(pattern.match(request.path) for pattern in self.ignored_paths): + return self.get_response(request) + + # 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 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) + + # Automatically allow access to Django's built-in admin views (excluding custom /admin/* views) + if app_name == "admin": + return self.get_response(request) + + # 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) + + # Restrict access to views that do not explicitly declare access rules + if not getattr(view_func, "has_explicit_access", False): + raise PermissionDenied # Deny access if the view lacks explicit permission handling + + return self.get_response(request) diff --git a/src/registrar/templates/admin/app_list.html b/src/registrar/templates/admin/app_list.html index aaf3dc423..ecce12a3e 100644 --- a/src/registrar/templates/admin/app_list.html +++ b/src/registrar/templates/admin/app_list.html @@ -4,24 +4,22 @@ {% for app in app_list %}
- - {# .gov override: add headers #} - {% if show_changelinks %} - - {% else %} - - {% endif %} + {# .gov override: display the app name as a caption rather than a table header #} + - {% if show_changelinks %} - - {% else %} - - {% endif %} + {# .gov override: hide headers #} + {% comment %} + {% if show_changelinks %} + + {% else %} + + {% endif %} + {% endcomment %} @@ -45,16 +43,17 @@ {% endif %} {% if model.add_url %} - + {% comment %} Remove the 's' from the end of the string to avoid text like "Add domain requests" {% endcomment %} + {% else %} {% endif %} {% if model.admin_url and show_changelinks %} {% if model.view_only %} - + {% else %} - + {% endif %} {% elif show_changelinks %} @@ -64,9 +63,20 @@
{{ app.name }}
- {{ app.name }} - - {{ app.name }} - + {{ app.name }} + + {{ app.name }} +
Model{% translate 'Add' %}{% translate 'Add' %}{% translate 'View' %}{% translate 'View' %}{% translate 'Change' %}{% translate 'Change' %}
{% endfor %} -
-

Analytics

- Dashboard +
+ + + + + + + + + + + + +
Analytics
Reports
Dashboard
{% else %}

{% translate 'You don’t have permission to view or edit anything.' %}

diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index 04565f61e..abc549a82 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -16,10 +16,10 @@ Domains
  • - {{ domain.name }} + {{ domain.name }}
  • - Domain managers + Domain managers
  • Add a domain manager @@ -27,7 +27,7 @@ {% else %} - {% url 'domain-users' pk=domain.id as url %} + {% url 'domain-users' domain_pk=domain.id as url %}
  • - Enable DNSSEC + Enable DNSSEC {% endif %} diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 36eb811e3..95e8e3d5f 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -18,13 +18,13 @@ Domains
  • - {{ domain.name }} + {{ domain.name }}
  • - DNS + DNS
  • - DNSSEC + DNSSEC
  • DS data diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index ad8d61592..1b1a59c9e 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -19,10 +19,10 @@ Domains
  • - {{ domain.name }} + {{ domain.name }}
  • - DNS + DNS
  • DNS name servers diff --git a/src/registrar/templates/domain_renewal.html b/src/registrar/templates/domain_renewal.html index 703c2358f..8af43c6eb 100644 --- a/src/registrar/templates/domain_renewal.html +++ b/src/registrar/templates/domain_renewal.html @@ -24,7 +24,7 @@ Domains
  • - {{domain.name}} + {{domain.name}}
  • Renewal Form @@ -63,14 +63,14 @@ {% endif %} {% endif %} - {% url 'domain-security-email' pk=domain.id as url %} + {% url 'domain-security-email' domain_pk=domain.id as url %} {% if security_email is not None and security_email not in hidden_security_emails%} {% include "includes/summary_item.html" with title='Security email' value=security_email custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %} {% else %} {% include "includes/summary_item.html" with title='Security email' value='None provided' custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %} {% endif %} - {% url 'domain-users' pk=domain.id as url %} + {% url 'domain-users' domain_pk=domain.id as url %} {% if portfolio %} {% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %} {% else %} @@ -91,7 +91,7 @@ Acknowledgement of .gov domain requirements
  • -
    + {% csrf_token %}
    diff --git a/src/registrar/templates/domain_request_current_sites.html b/src/registrar/templates/domain_request_current_sites.html index 769906309..8eb5bb0d1 100644 --- a/src/registrar/templates/domain_request_current_sites.html +++ b/src/registrar/templates/domain_request_current_sites.html @@ -20,7 +20,7 @@ {% endwith %} {% endfor %} -
    diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index 38a5a43c5..e74ecf709 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -16,7 +16,7 @@ Domains
  • - {{ domain.name }} + {{ domain.name }}
  • Security email diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 3302a6a79..ed384d5cd 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -17,14 +17,14 @@ {% endif %}
  • - {% url 'domain-dns' pk=domain.id as url %} + {% url 'domain-dns' domain_pk=domain.id as url %} DNS {% if request.path|startswith:url %}