Repository: cisagov/manage.get.gov
Pull request #3682: #3599: New permissions tests - [AB]
Author: login: Matt-Spence
Created: 2025-03-20T16:28:45Z | Merged: 2025-03-27T17:51:09Z

## Ticket

<!-- PR title format: `#issue_number: Descriptive name ideally matching ticket name - [sandbox]`-->
Resolves #3599

## Changes

<!-- What was added, updated, or removed in this PR. -->
- A new dictionary mapping the names of urls from urls.py to their corresponding set of access control rules was created
- Utility function to check that all urls exist inside the mapping
- Tests that ensure all urls are in the mapping and that the mapping reflects what is actually in our code

<!--
    Please add/remove/edit any of the template below to fit the needs
    of this specific PR.
--->

## Context for reviewers
There were a lot of ways this could have been approached, but I chose to do what I thought was the smallest change for the greatest value. With these changes, we now have a central location where we can review and set permissions that will be enforced in tests. Additionally, any new urls added in urls.py will be required to be added to this central location, providing further assurance that developers are considering access controls when adding new endpoints.

<!--Background context, more in-depth details of the implementation, and anything else you'd like to call out or ask reviewers.  -->

## Setup
This is best tested locally as it requires making code changes to make sure the new tests work properly. First, pick a random view and add or remove an access control rule. Run the test and make sure it breaks. Then add/remove a rule from the permissions map in permissions.py and make sure the test breaks again. Then, write a dummy view and map it to some random endpoint in urls.py. Run the tests and make sure they fail. Repeat until you're satisfied that these tests are correct.

<!--  Add any steps or code to run in this section to help others run your code.
    
    Example 1:
    ```sh
    echo "Code goes here"
    ``` 
    
    Example 2: If the PR was to add a new link with a redirect, this section could simply be:
    -go to /path/to/start/page
    -click the blue link in the <insert location>
    -notice user is redirected to <proper end location>
-->

## Code Review Verification Steps

### As the original developer, I have

#### Satisfied acceptance criteria and met development standards

- [ ] Met the acceptance criteria, or will meet them in a subsequent PR
- [ ] Created/modified automated tests
- [ ] Update documentation in READMEs and/or onboarding guide

#### Ensured code standards are met (Original Developer)
<!-- Mark "- N/A" and check at the end of each check that is not applicable to your PR -->
- [ ] If any updated dependencies on Pipfile, also update dependencies in requirements.txt.
- [ ] Interactions with external systems are wrapped in try/except
- [ ] Error handling exists for unusual or missing values

#### Validated user-facing changes (if applicable)

- [ ] Tag @dotgov-designers in this PR's Reviewers for design review. If code is not user-facing, delete design reviewer checklist
- [ ] Verify new pages have been added to .pa11yci file so that they will be tested with our automated accessibility testing
- [ ] Checked keyboard navigability
- [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI)

### As a code reviewer, I have

#### Reviewed, tested, and left feedback about the changes

- [ ] Pulled this branch locally and tested it
- [ ] Verified code meets all checks above. Address any checks that are not satisfied
- [ ] Reviewed this code and left comments. Indicate if comments must be addressed before code is merged
- [ ] Checked that all code is adequately covered by tests
- [ ] Verify migrations are valid and do not conflict with existing migrations

#### Validated user-facing changes as a developer
**Note:** Multiple code reviewers can share the checklists above, a second reviewer should not make a duplicate checklist. All checks should be checked before approving, even those labeled N/A. 

- [ ] New pages have been added to .pa11yci file so that they will be tested with our automated accessibility testing
- [ ] Checked keyboard navigability
- [ ] Meets all designs and user flows provided by design/product
- [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI)
- [ ] (Rarely needed) Tested as both an analyst and applicant user

### As a designer reviewer, I have

#### Verified that the changes match the design intention

- [ ] Checked that the design translated visually
- [ ] Checked behavior. Comment any found issues or broken flows.
- [ ] Checked different states (empty, one, some, error)
- [ ] Checked for landmarks, page heading structure, and links

#### Validated user-facing changes as a designer

- [ ] Checked keyboard navigability
- [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI)
- [ ] Tested with multiple browsers (check off which ones were used)
  - [ ] Chrome
  - [ ] Microsoft Edge
  - [ ] FireFox
  - [ ] Safari
- [ ] (Rarely needed) Tested as both an analyst and applicant user

### References
- [Code review best practices](../docs/dev-practices/code_review.md)

## Screenshots

<!-- If this PR makes visible interface changes, an image of the finished interface can help reviewers
and casual observers understand the context of the changes.
A before image is optional and can be included at the submitter's discretion.

Consider using an animated image to show an entire workflow.
You may want to use [GIPHY Capture](https://giphy.com/apps/giphycapture) for this! 📸

_Please frame images to show useful context but also highlight the affected regions._
--->


Linked issue #3599: Test coverage of access controls
### Issue description

Right now our testing for permissions consists of two methods: a full check on all urls in test_urls_auth.py, or ad-hoc permissions tests scattered around our testing files. The problem with the former is that it only checks for authentication, ensuring endpoints require a valid login.gov account before providing access. It doesn't check specific access controls (permission restrictions) beyond that. The latter,  on the other hand, is not systemic in any way and likely has large gaps.

This is an opportunity to improve our test coverage. We should implement a system where all endpoints are checked for access controls against some central source of truth. This should ideally be done in a way that will automatically cover any new urls we create, so that tests cannot pass if access controls on all urls don't match the source of truth.

### Acceptance criteria

- [ ] New unit tests are added that check that the correct permissions are required to access all django admin, including analytics, pages mentioned in urls.py and the "domain/" pages
- [ ] all urls in urls.py are checked
- [ ] a developer would get python-test errors if a new url is added whose permissions are not explicitly set in the unit test(s) (such as a central source of truth used by the unit tests) 


Limiting scope to the admin pages (including analytics) and domain pages due to the sheer number of urls we have. Follow on tickets would build on this for the other pages
### Additional context
The approach chosen should be expandable in the future as we will build on this.

Ideally, we would have a central source of truth is created where all access controls for all urls are listed in some format. New test(s)would then check access controls against the central source of truth. Then, if If a url is not found in the source of truth the test should fail, to enforce that all urls should be in the source of truth.

Our recent permissions audit should provide an excellent starting point for the central source of truth. Once that is created in some machine-readable format, it should be possible to loop through urls.py and check every view there against the source of truth.

In Eng huddle, Zander also mentioned it would be nice if pages inherited the permissions in the unit test. That way if the members table(for example) required people to have Member permission and permission to this portfolio, then each unitest for anything members/<etc> would naturally inherit this check and also check that member permissions & portfolio perms are present.

[link to slack thread](https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1740681033816139)
### Links to other issues

_No response_

Diff:
diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py
index 56f0cfd0f4..ba9661d1cc 100644
--- a/src/registrar/config/urls.py
+++ b/src/registrar/config/urls.py
@@ -171,6 +171,7 @@
     path(
         "admin/logout/",
         RedirectView.as_view(pattern_name="logout", permanent=False),
+        name="logout",
     ),
     path(
         "admin/analytics/export_data_type/",
diff --git a/src/registrar/permissions.py b/src/registrar/permissions.py
new file mode 100644
index 0000000000..6847c16d5e
--- /dev/null
+++ b/src/registrar/permissions.py
@@ -0,0 +1,229 @@
+"""
+Centralized permissions management for the registrar.
+"""
+
+from django.urls import URLResolver, get_resolver, URLPattern
+from registrar.decorators import (
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
+    IS_STAFF,
+    IS_DOMAIN_MANAGER,
+    IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
+    IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_CISA_ANALYST,
+    IS_OMB_ANALYST,
+    IS_FULL_ACCESS,
+    IS_DOMAIN_REQUEST_CREATOR,
+    IS_STAFF_MANAGING_DOMAIN,
+    IS_PORTFOLIO_MEMBER,
+    HAS_PORTFOLIO_DOMAINS_ANY_PERM,
+    HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
+    HAS_PORTFOLIO_MEMBERS_EDIT,
+    HAS_PORTFOLIO_MEMBERS_ANY_PERM,
+    HAS_PORTFOLIO_MEMBERS_VIEW,
+    ALL,
+)
+
+# Define permissions for each URL pattern by name
+URL_PERMISSIONS = {
+    # Home & general pages
+    "home": [ALL],
+    "health": [ALL],  # Intentionally no decorator
+    # Domain management
+    "domain": [HAS_PORTFOLIO_DOMAINS_VIEW_ALL, IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-nameservers": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec-dsdata": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-org-name-address": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-suborganization": [IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-senior-official": [IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-security-email": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-renewal": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users-add": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-user-delete": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # Portfolio management
+    "domains": [HAS_PORTFOLIO_DOMAINS_ANY_PERM],
+    "no-portfolio-domains": [IS_PORTFOLIO_MEMBER],
+    "no-organization-domains": [IS_PORTFOLIO_MEMBER],
+    "members": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "new-member": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "domain-requests": [HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM],
+    "no-portfolio-requests": [IS_PORTFOLIO_MEMBER],
+    "organization": [IS_PORTFOLIO_MEMBER],
+    "senior-official": [IS_PORTFOLIO_MEMBER],
+    # Domain requests
+    "domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
+    "domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "edit-domain-request": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    # Admin functions
+    "analytics": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_type": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_domain_requests_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_federal": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_domains_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_requests_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_managed_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_unmanaged_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "transfer_user": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    # Analytics
+    "all-domain-metadata": [IS_STAFF],
+    "current-full": [IS_STAFF],
+    "all-domain-requests-metadata": [IS_STAFF],
+    "domain-growth": [IS_STAFF],
+    "request-growth": [IS_STAFF],
+    "managed-domains": [IS_STAFF],
+    "unmanaged-domains": [IS_STAFF],
+    # Reports
+    "export-user-domains-as-csv": [IS_STAFF],
+    "export-portfolio-members-as-csv": [IS_STAFF],
+    "export_members_portfolio": [HAS_PORTFOLIO_MEMBERS_VIEW],
+    "export_data_type_user": [ALL],
+    # API endpoints
+    "get-senior-official-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-portfolio-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-suborganization-list-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-federal-and-portfolio-types-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-action-needed-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-rejection-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get_domains_json": [ALL],
+    "get_domain_requests_json": [ALL],
+    "get_portfolio_members_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "get_member_domains_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    # User profile
+    "finish-user-profile-setup": [ALL],
+    "user-profile": [ALL],
+    # Invitation
+    "invitation-cancel": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # DNS Hosting
+    "prototype-domain-dns": [IS_STAFF],
+    # Domain request wizard
+    "start": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "finished": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "generic_org_type": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "tribal_government": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_federal": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_election": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_contact": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "about_your_organization": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "senior_official": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "current_sites": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "dotgov_domain": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "purpose": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "other_contacts": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "requirements": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "review": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_requesting_entity": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+}
+
+UNCHECKED_URLS = [
+    "health",
+    "openid/",
+    "get-current-federal",
+    "get-current-full",
+    "available",
+    "rdap",
+    "todo",
+    "logout",
+]
+
+
+def verify_all_urls_have_permissions():
+    """
+    Utility function to verify that all URLs in the application have defined permissions
+    in the permissions mapping.
+    """
+
+    resolver = get_resolver()
+    missing_permissions = []
+    missing_names = []
+
+    # Collect all URL pattern names
+    for pattern in resolver.url_patterns:
+        # Skip URLResolver objects (like admin.site.urls)
+        if isinstance(pattern, URLResolver):
+            continue
+
+        if hasattr(pattern, "name") and pattern.name:
+            if pattern.name not in URL_PERMISSIONS and pattern.name not in UNCHECKED_URLS:
+                missing_permissions.append(pattern.name)
+        else:
+            raise ValueError(f"URL pattern {pattern} has no name")
+
+    if missing_names:
+        raise ValueError(f"The following URL patterns have no name: {missing_names}")
+
+    return missing_permissions
+
+
+def validate_permissions():  # noqa: C901
+    """
+    Validates that all URL patterns have consistent permission rules between
+    the centralized mapping and view decorators.
+
+    Returns a dictionary of issues found.
+    """
+
+    resolver = get_resolver()
+    issues = {
+        "missing_in_mapping": [],  # URLs with decorators but not in mapping
+        "missing_decorator": [],  # URLs in mapping but missing decorators
+        "permission_mismatch": [],  # URLs with different permissions
+    }
+
+    def check_url_pattern(pattern, parent_path=""):
+        if isinstance(pattern, URLPattern):
+            view_func = pattern.callback
+            path = f"{parent_path}/{pattern.pattern}"
+            url_name = pattern.name
+
+            if url_name:
+                # Skip check for endpoints that intentionally have no decorator
+                if url_name in UNCHECKED_URLS:
+                    return
+
+                # Check if view has decorator but missing from mapping
+                if getattr(view_func, "has_explicit_access", False) and url_name not in URL_PERMISSIONS:
+                    issues["missing_in_mapping"].append((url_name, path))
+
+                # Check if view is in mapping but missing decorator
+                elif url_name in URL_PERMISSIONS and not getattr(view_func, "has_explicit_access", False):
+                    issues["missing_decorator"].append((url_name, path))
+
+                # Check if permissions match (more complex, may need refinement)
+                elif getattr(view_func, "has_explicit_access", False) and url_name in URL_PERMISSIONS:
+                    view_permissions = getattr(view_func, "_access_rules", set())
+                    mapping_permissions = set(URL_PERMISSIONS[url_name])
+
+                    if view_permissions != mapping_permissions:
+                        issues["permission_mismatch"].append((url_name, path, view_permissions, mapping_permissions))
+
+        elif isinstance(pattern, URLResolver):
+            # Handle included URL patterns (nested)
+            new_parent = f"{parent_path}/{pattern.pattern}"
+            for p in pattern.url_patterns:
+                check_url_pattern(p, new_parent)
+
+    # Check all URL patterns
+    for pattern in resolver.url_patterns:
+        check_url_pattern(pattern)
+
+    return issues
diff --git a/src/registrar/tests/test_permissions.py b/src/registrar/tests/test_permissions.py
new file mode 100644
index 0000000000..380518fdbd
--- /dev/null
+++ b/src/registrar/tests/test_permissions.py
@@ -0,0 +1,70 @@
+"""
+Tests for validating the permissions system consistency.
+
+These tests ensure that:
+1. All URLs have permissions defined in the centralized mapping
+2. All views with permission decorators are in the mapping
+3. The permissions in the decorators match those in the mapping
+"""
+
+from django.test import TestCase
+from registrar.permissions import verify_all_urls_have_permissions, validate_permissions
+
+
+class TestPermissionsMapping(TestCase):
+    """Test the centralized permissions mapping for completeness and consistency."""
+
+    def test_all_urls_have_permissions(self):
+        """Verify that all URL patterns in the application have permissions defined in the mapping."""
+        missing_urls = verify_all_urls_have_permissions()
+
+        # Format URLs for better readability in case of failure
+        if missing_urls:
+            formatted_urls = "\n".join([f"  - {url}" for url in missing_urls])
+            self.fail(
+                f"The following URL patterns are missing from URL_PERMISSIONS mapping:\n{formatted_urls}\n"
+                f"Please add them to the URL_PERMISSIONS dictionary in registrar/permissions.py"
+            )
+
+    def test_permission_decorator_consistency(self):
+        """
+        Test that all views have consistent permission rules between
+        the centralized mapping and view decorators.
+        """
+        issues = validate_permissions()
+
+        error_messages = []
+
+        if issues["missing_in_mapping"]:
+            urls = "\n".join([f"  - {name} (at {path})" for name, path in issues["missing_in_mapping"]])
+            error_messages.append(
+                f"The following URLs have permission decorators but are missing from the mapping:\n{urls}\n"
+                "Add these URLs to the URL_PERMISSIONS dictionary in registrar/permissions.py"
+            )
+
+        if issues["missing_decorator"]:
+            urls = "\n".join([f"  - {name} (at {path})" for name, path in issues["missing_decorator"]])
+            error_messages.append(
+                f"The following URLs are in the mapping but missing @grant_access decorators:\n{urls}\n"
+                "Add appropriate @grant_access decorators to these views"
+            )
+
+        if issues["permission_mismatch"]:
+            mismatches = []
+            for name, path, view_perms, mapping_perms in issues["permission_mismatch"]:
+                view_perms_str = ", ".join(sorted(str(p) for p in view_perms))
+                mapping_perms_str = ", ".join(sorted(str(p) for p in mapping_perms))
+                mismatches.append(
+                    f"  - {name} (at {path}):\n"
+                    f"    Decorator: [{view_perms_str}]\n"
+                    f"    Mapping:   [{mapping_perms_str}]"
+                )
+
+            error_messages.append(
+                f"The following URLs have mismatched permissions between decorators and mapping:\n"
+                f"{chr(10).join(mismatches)}\n"
+                "Update either the decorator or the mapping to ensure consistency"
+            )
+
+        if error_messages:
+            self.fail("\n\n".join(error_messages))
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index c38a233853..ced23d85f9 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -15,6 +15,7 @@
     IS_DOMAIN_MANAGER,
     IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
     IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_STAFF,
     IS_STAFF_MANAGING_DOMAIN,
     grant_access,
 )
@@ -707,6 +708,7 @@ class PrototypeDomainDNSRecordForm(forms.Form):
     )
 
 
+@grant_access(IS_STAFF)
 class PrototypeDomainDNSRecordView(DomainFormBaseView):
     template_name = "prototype_domain_dns.html"
     form_class = PrototypeDomainDNSRecordForm


Reviews:
user: login: zandercymatics
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T15:05:15Z
user: login: zandercymatics
body: Nice work. Couple of questions but I don't see anything blocking (other than a debug_response file that I think was added accidentally)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:41:28Z
user: login: Matt-Spence
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:51:59Z
user: login: Matt-Spence
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:53:59Z
user: login: Matt-Spence
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:54:09Z
user: login: Matt-Spence
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:22:05Z

Review comments:
path: src/debug_response.html
user: login: zandercymatics
body: I assume this wasn't supposed to be included in the PR? 😄 
created_at: 2025-03-25T15:05:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+"""
+Centralized permissions management for the registrar.
+"""
+
+from registrar.decorators import (
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
+    IS_STAFF,
+    IS_DOMAIN_MANAGER,
+    IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
+    IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_CISA_ANALYST,
+    IS_OMB_ANALYST,
+    IS_FULL_ACCESS,
+    IS_DOMAIN_REQUEST_CREATOR,
+    IS_STAFF_MANAGING_DOMAIN,
+    IS_PORTFOLIO_MEMBER,
+    HAS_PORTFOLIO_DOMAINS_ANY_PERM,
+    HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
+    HAS_PORTFOLIO_MEMBERS_EDIT,
+    HAS_PORTFOLIO_MEMBERS_ANY_PERM,
+    HAS_PORTFOLIO_MEMBERS_VIEW,
+    ALL,
+)
+
+# Define permissions for each URL pattern by name
+URL_PERMISSIONS = {
+    # Home & general pages
+    "home": [ALL],
+    "health": [ALL],  # Intentionally no decorator
+    # Domain management
+    "domain": [HAS_PORTFOLIO_DOMAINS_VIEW_ALL, IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-nameservers": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec-dsdata": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-org-name-address": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-suborganization": [IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-senior-official": [IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-security-email": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-renewal": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users-add": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-user-delete": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # Portfolio management
+    "domains": [HAS_PORTFOLIO_DOMAINS_ANY_PERM],
+    "no-portfolio-domains": [IS_PORTFOLIO_MEMBER],
+    "no-organization-domains": [IS_PORTFOLIO_MEMBER],
+    "members": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "new-member": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "domain-requests": [HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM],
+    "no-portfolio-requests": [IS_PORTFOLIO_MEMBER],
+    "organization": [IS_PORTFOLIO_MEMBER],
+    "senior-official": [IS_PORTFOLIO_MEMBER],
+    # Domain requests
+    "domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
+    "domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "edit-domain-request": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    # Admin functions
+    "analytics": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_type": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_domain_requests_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_federal": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_domains_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_requests_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_managed_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_unmanaged_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "transfer_user": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    # Analytics
+    "all-domain-metadata": [IS_STAFF],
+    "current-full": [IS_STAFF],
+    "all-domain-requests-metadata": [IS_STAFF],
+    "domain-growth": [IS_STAFF],
+    "request-growth": [IS_STAFF],
+    "managed-domains": [IS_STAFF],
+    "unmanaged-domains": [IS_STAFF],
+    # Reports
+    "export-user-domains-as-csv": [IS_STAFF],
+    "export-portfolio-members-as-csv": [IS_STAFF],
+    "export_members_portfolio": [HAS_PORTFOLIO_MEMBERS_VIEW],
+    "export_data_type_user": [ALL],
+    # API endpoints
+    "get-senior-official-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-portfolio-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-suborganization-list-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-federal-and-portfolio-types-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-action-needed-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-rejection-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get_domains_json": [ALL],
+    "get_domain_requests_json": [ALL],
+    "get_portfolio_members_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "get_member_domains_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    # User profile
+    "finish-user-profile-setup": [ALL],
+    "user-profile": [ALL],
+    # Invitation
+    "invitation-cancel": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # DNS Hosting
+    "prototype-domain-dns": [IS_STAFF],
+    # Domain request wizard
+    "start": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "finished": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "generic_org_type": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "tribal_government": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_federal": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_election": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_contact": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "about_your_organization": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "senior_official": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "current_sites": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "dotgov_domain": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "purpose": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "other_contacts": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "requirements": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "review": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_requesting_entity": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+}
+
+UNCHECKED_URLS = [
+    "health",
+    "openid/",
+    "get-current-federal",
+    "get-current-full",
+    "available",
+    "rdap",
+    "todo",
+]
+
+
+def verify_all_urls_have_permissions():
+    """
+    Utility function to verify that all URLs in the application have defined permissions
+    in the permissions mapping.
+    """
+    from django.urls import get_resolver
+
+    resolver = get_resolver()
+    missing_permissions = []
+
+    # Collect all URL pattern names
+    for pattern in resolver.url_patterns:
+        if hasattr(pattern, "name") and pattern.name:
path: src/registrar/permissions.py
user: login: zandercymatics
body: Can there be a case where a url can be defined without a name?
created_at: 2025-03-25T15:08:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+"""
+Centralized permissions management for the registrar.
+"""
+
+from registrar.decorators import (
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
+    IS_STAFF,
+    IS_DOMAIN_MANAGER,
+    IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
+    IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_CISA_ANALYST,
+    IS_OMB_ANALYST,
+    IS_FULL_ACCESS,
+    IS_DOMAIN_REQUEST_CREATOR,
+    IS_STAFF_MANAGING_DOMAIN,
+    IS_PORTFOLIO_MEMBER,
+    HAS_PORTFOLIO_DOMAINS_ANY_PERM,
+    HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
+    HAS_PORTFOLIO_MEMBERS_EDIT,
+    HAS_PORTFOLIO_MEMBERS_ANY_PERM,
+    HAS_PORTFOLIO_MEMBERS_VIEW,
+    ALL,
+)
+
+# Define permissions for each URL pattern by name
+URL_PERMISSIONS = {
+    # Home & general pages
+    "home": [ALL],
+    "health": [ALL],  # Intentionally no decorator
+    # Domain management
+    "domain": [HAS_PORTFOLIO_DOMAINS_VIEW_ALL, IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-nameservers": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec-dsdata": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-org-name-address": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-suborganization": [IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-senior-official": [IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-security-email": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-renewal": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users-add": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-user-delete": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # Portfolio management
+    "domains": [HAS_PORTFOLIO_DOMAINS_ANY_PERM],
+    "no-portfolio-domains": [IS_PORTFOLIO_MEMBER],
+    "no-organization-domains": [IS_PORTFOLIO_MEMBER],
+    "members": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "new-member": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "domain-requests": [HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM],
+    "no-portfolio-requests": [IS_PORTFOLIO_MEMBER],
+    "organization": [IS_PORTFOLIO_MEMBER],
+    "senior-official": [IS_PORTFOLIO_MEMBER],
+    # Domain requests
+    "domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
+    "domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "edit-domain-request": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    # Admin functions
+    "analytics": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_type": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_domain_requests_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_federal": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_domains_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_requests_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_managed_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_unmanaged_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "transfer_user": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    # Analytics
+    "all-domain-metadata": [IS_STAFF],
+    "current-full": [IS_STAFF],
+    "all-domain-requests-metadata": [IS_STAFF],
+    "domain-growth": [IS_STAFF],
+    "request-growth": [IS_STAFF],
+    "managed-domains": [IS_STAFF],
+    "unmanaged-domains": [IS_STAFF],
+    # Reports
+    "export-user-domains-as-csv": [IS_STAFF],
+    "export-portfolio-members-as-csv": [IS_STAFF],
+    "export_members_portfolio": [HAS_PORTFOLIO_MEMBERS_VIEW],
+    "export_data_type_user": [ALL],
+    # API endpoints
+    "get-senior-official-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-portfolio-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-suborganization-list-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-federal-and-portfolio-types-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-action-needed-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-rejection-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get_domains_json": [ALL],
+    "get_domain_requests_json": [ALL],
+    "get_portfolio_members_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "get_member_domains_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    # User profile
+    "finish-user-profile-setup": [ALL],
+    "user-profile": [ALL],
+    # Invitation
+    "invitation-cancel": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # DNS Hosting
+    "prototype-domain-dns": [IS_STAFF],
+    # Domain request wizard
+    "start": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "finished": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "generic_org_type": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "tribal_government": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_federal": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_election": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_contact": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "about_your_organization": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "senior_official": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "current_sites": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "dotgov_domain": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "purpose": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "other_contacts": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "requirements": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "review": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_requesting_entity": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+}
+
+UNCHECKED_URLS = [
+    "health",
+    "openid/",
+    "get-current-federal",
+    "get-current-full",
+    "available",
+    "rdap",
+    "todo",
+]
+
+
+def verify_all_urls_have_permissions():
+    """
+    Utility function to verify that all URLs in the application have defined permissions
+    in the permissions mapping.
+    """
+    from django.urls import get_resolver
+
+    resolver = get_resolver()
+    missing_permissions = []
+
+    # Collect all URL pattern names
+    for pattern in resolver.url_patterns:
+        if hasattr(pattern, "name") and pattern.name:
+            if pattern.name not in URL_PERMISSIONS and pattern.name not in UNCHECKED_URLS:
+                missing_permissions.append(pattern.name)
+
+    return missing_permissions
+
+
+def validate_permissions():  # noqa: C901
+    """
+    Validates that all URL patterns have consistent permission rules between
+    the centralized mapping and view decorators.
+
+    Returns a dictionary of issues found.
+    """
+    from django.urls import get_resolver, URLPattern, URLResolver
path: src/registrar/permissions.py
user: login: zandercymatics
body: (Q) Why are these imports scoped to the function? 
created_at: 2025-03-25T21:07:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+"""
+Centralized permissions management for the registrar.
+"""
+
+from registrar.decorators import (
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
+    IS_STAFF,
+    IS_DOMAIN_MANAGER,
+    IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
+    IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_CISA_ANALYST,
+    IS_OMB_ANALYST,
+    IS_FULL_ACCESS,
+    IS_DOMAIN_REQUEST_CREATOR,
+    IS_STAFF_MANAGING_DOMAIN,
+    IS_PORTFOLIO_MEMBER,
+    HAS_PORTFOLIO_DOMAINS_ANY_PERM,
+    HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
+    HAS_PORTFOLIO_MEMBERS_EDIT,
+    HAS_PORTFOLIO_MEMBERS_ANY_PERM,
+    HAS_PORTFOLIO_MEMBERS_VIEW,
+    ALL,
+)
+
+# Define permissions for each URL pattern by name
+URL_PERMISSIONS = {
+    # Home & general pages
+    "home": [ALL],
+    "health": [ALL],  # Intentionally no decorator
+    # Domain management
+    "domain": [HAS_PORTFOLIO_DOMAINS_VIEW_ALL, IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-nameservers": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec-dsdata": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-org-name-address": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-suborganization": [IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-senior-official": [IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-security-email": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-renewal": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users-add": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-user-delete": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # Portfolio management
+    "domains": [HAS_PORTFOLIO_DOMAINS_ANY_PERM],
+    "no-portfolio-domains": [IS_PORTFOLIO_MEMBER],
+    "no-organization-domains": [IS_PORTFOLIO_MEMBER],
+    "members": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "new-member": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "domain-requests": [HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM],
+    "no-portfolio-requests": [IS_PORTFOLIO_MEMBER],
+    "organization": [IS_PORTFOLIO_MEMBER],
+    "senior-official": [IS_PORTFOLIO_MEMBER],
+    # Domain requests
+    "domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
+    "domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "edit-domain-request": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    # Admin functions
+    "analytics": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_type": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_domain_requests_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_federal": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_domains_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_requests_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_managed_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_unmanaged_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "transfer_user": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    # Analytics
+    "all-domain-metadata": [IS_STAFF],
+    "current-full": [IS_STAFF],
+    "all-domain-requests-metadata": [IS_STAFF],
+    "domain-growth": [IS_STAFF],
+    "request-growth": [IS_STAFF],
+    "managed-domains": [IS_STAFF],
+    "unmanaged-domains": [IS_STAFF],
+    # Reports
+    "export-user-domains-as-csv": [IS_STAFF],
+    "export-portfolio-members-as-csv": [IS_STAFF],
+    "export_members_portfolio": [HAS_PORTFOLIO_MEMBERS_VIEW],
+    "export_data_type_user": [ALL],
+    # API endpoints
+    "get-senior-official-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-portfolio-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-suborganization-list-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-federal-and-portfolio-types-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-action-needed-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-rejection-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get_domains_json": [ALL],
+    "get_domain_requests_json": [ALL],
+    "get_portfolio_members_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "get_member_domains_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    # User profile
+    "finish-user-profile-setup": [ALL],
+    "user-profile": [ALL],
+    # Invitation
+    "invitation-cancel": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # DNS Hosting
+    "prototype-domain-dns": [IS_STAFF],
+    # Domain request wizard
+    "start": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "finished": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "generic_org_type": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "tribal_government": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_federal": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_election": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_contact": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "about_your_organization": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "senior_official": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "current_sites": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "dotgov_domain": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "purpose": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "other_contacts": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "requirements": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "review": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_requesting_entity": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+}
+
+UNCHECKED_URLS = [
+    "health",
+    "openid/",
+    "get-current-federal",
+    "get-current-full",
+    "available",
+    "rdap",
+    "todo",
+]
+
+
+def verify_all_urls_have_permissions():
+    """
+    Utility function to verify that all URLs in the application have defined permissions
+    in the permissions mapping.
+    """
+    from django.urls import get_resolver
+
+    resolver = get_resolver()
+    missing_permissions = []
+
+    # Collect all URL pattern names
+    for pattern in resolver.url_patterns:
+        if hasattr(pattern, "name") and pattern.name:
+            if pattern.name not in URL_PERMISSIONS and pattern.name not in UNCHECKED_URLS:
+                missing_permissions.append(pattern.name)
+
+    return missing_permissions
+
+
+def validate_permissions():  # noqa: C901
+    """
+    Validates that all URL patterns have consistent permission rules between
+    the centralized mapping and view decorators.
+
+    Returns a dictionary of issues found.
+    """
+    from django.urls import get_resolver, URLPattern, URLResolver
+
+    resolver = get_resolver()
+    issues = {
+        "missing_in_mapping": [],  # URLs with decorators but not in mapping
+        "missing_decorator": [],  # URLs in mapping but missing decorators
+        "permission_mismatch": [],  # URLs with different permissions
+    }
+
+    def check_url_pattern(pattern, parent_path=""):
+        if isinstance(pattern, URLPattern):
+            view_func = pattern.callback
+            path = f"{parent_path}/{pattern.pattern}"
+            url_name = pattern.name
+
+            if url_name:
+                # Skip check for endpoints that intentionally have no decorator
+                if url_name in UNCHECKED_URLS:
+                    return
+
+                # Check if view has decorator but missing from mapping
+                if getattr(view_func, "has_explicit_access", False) and url_name not in URL_PERMISSIONS:
+                    issues["missing_in_mapping"].append((url_name, path))
path: src/registrar/permissions.py
user: login: zandercymatics
body: I like how you are checking for this case separately from missing decorator, nice touch
created_at: 2025-03-25T21:09:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -703,6 +704,7 @@ class PrototypeDomainDNSRecordForm(forms.Form):
     )
 
 
+@grant_access(IS_STAFF)
path: src/registrar/views/domain.py
user: login: zandercymatics
body: Nice add
created_at: 2025-03-27T14:37:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+"""
+Centralized permissions management for the registrar.
+"""
+
+from registrar.decorators import (
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
+    IS_STAFF,
+    IS_DOMAIN_MANAGER,
+    IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
+    IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_CISA_ANALYST,
+    IS_OMB_ANALYST,
+    IS_FULL_ACCESS,
+    IS_DOMAIN_REQUEST_CREATOR,
+    IS_STAFF_MANAGING_DOMAIN,
+    IS_PORTFOLIO_MEMBER,
+    HAS_PORTFOLIO_DOMAINS_ANY_PERM,
+    HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
+    HAS_PORTFOLIO_MEMBERS_EDIT,
+    HAS_PORTFOLIO_MEMBERS_ANY_PERM,
+    HAS_PORTFOLIO_MEMBERS_VIEW,
+    ALL,
+)
+
+# Define permissions for each URL pattern by name
+URL_PERMISSIONS = {
+    # Home & general pages
+    "home": [ALL],
+    "health": [ALL],  # Intentionally no decorator
+    # Domain management
+    "domain": [HAS_PORTFOLIO_DOMAINS_VIEW_ALL, IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-nameservers": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec-dsdata": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-org-name-address": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-suborganization": [IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-senior-official": [IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-security-email": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-renewal": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users-add": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-user-delete": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # Portfolio management
+    "domains": [HAS_PORTFOLIO_DOMAINS_ANY_PERM],
+    "no-portfolio-domains": [IS_PORTFOLIO_MEMBER],
+    "no-organization-domains": [IS_PORTFOLIO_MEMBER],
+    "members": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "new-member": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "domain-requests": [HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM],
+    "no-portfolio-requests": [IS_PORTFOLIO_MEMBER],
+    "organization": [IS_PORTFOLIO_MEMBER],
+    "senior-official": [IS_PORTFOLIO_MEMBER],
+    # Domain requests
+    "domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
+    "domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "edit-domain-request": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    # Admin functions
+    "analytics": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_type": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_domain_requests_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_federal": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_domains_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_requests_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_managed_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_unmanaged_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "transfer_user": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    # Analytics
+    "all-domain-metadata": [IS_STAFF],
+    "current-full": [IS_STAFF],
+    "all-domain-requests-metadata": [IS_STAFF],
+    "domain-growth": [IS_STAFF],
+    "request-growth": [IS_STAFF],
+    "managed-domains": [IS_STAFF],
+    "unmanaged-domains": [IS_STAFF],
+    # Reports
+    "export-user-domains-as-csv": [IS_STAFF],
+    "export-portfolio-members-as-csv": [IS_STAFF],
+    "export_members_portfolio": [HAS_PORTFOLIO_MEMBERS_VIEW],
+    "export_data_type_user": [ALL],
+    # API endpoints
+    "get-senior-official-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-portfolio-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-suborganization-list-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-federal-and-portfolio-types-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-action-needed-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-rejection-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get_domains_json": [ALL],
+    "get_domain_requests_json": [ALL],
+    "get_portfolio_members_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "get_member_domains_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    # User profile
+    "finish-user-profile-setup": [ALL],
+    "user-profile": [ALL],
+    # Invitation
+    "invitation-cancel": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # DNS Hosting
+    "prototype-domain-dns": [IS_STAFF],
+    # Domain request wizard
+    "start": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "finished": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "generic_org_type": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "tribal_government": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_federal": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_election": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_contact": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "about_your_organization": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "senior_official": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "current_sites": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "dotgov_domain": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "purpose": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "other_contacts": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "requirements": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "review": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_requesting_entity": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+}
+
+UNCHECKED_URLS = [
+    "health",
+    "openid/",
+    "get-current-federal",
+    "get-current-full",
+    "available",
+    "rdap",
+    "todo",
+]
+
+
+def verify_all_urls_have_permissions():
+    """
+    Utility function to verify that all URLs in the application have defined permissions
+    in the permissions mapping.
+    """
+    from django.urls import get_resolver
+
+    resolver = get_resolver()
+    missing_permissions = []
+
+    # Collect all URL pattern names
+    for pattern in resolver.url_patterns:
+        if hasattr(pattern, "name") and pattern.name:
+            if pattern.name not in URL_PERMISSIONS and pattern.name not in UNCHECKED_URLS:
+                missing_permissions.append(pattern.name)
+
+    return missing_permissions
+
+
+def validate_permissions():  # noqa: C901
+    """
+    Validates that all URL patterns have consistent permission rules between
+    the centralized mapping and view decorators.
+
+    Returns a dictionary of issues found.
+    """
+    from django.urls import get_resolver, URLPattern, URLResolver
path: src/registrar/permissions.py
user: login: Matt-Spence
body: Because it was a dumb testing thing I forgot to remove
created_at: 2025-03-27T15:51:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+"""
+Centralized permissions management for the registrar.
+"""
+
+from registrar.decorators import (
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
+    IS_STAFF,
+    IS_DOMAIN_MANAGER,
+    IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
+    IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_CISA_ANALYST,
+    IS_OMB_ANALYST,
+    IS_FULL_ACCESS,
+    IS_DOMAIN_REQUEST_CREATOR,
+    IS_STAFF_MANAGING_DOMAIN,
+    IS_PORTFOLIO_MEMBER,
+    HAS_PORTFOLIO_DOMAINS_ANY_PERM,
+    HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
+    HAS_PORTFOLIO_MEMBERS_EDIT,
+    HAS_PORTFOLIO_MEMBERS_ANY_PERM,
+    HAS_PORTFOLIO_MEMBERS_VIEW,
+    ALL,
+)
+
+# Define permissions for each URL pattern by name
+URL_PERMISSIONS = {
+    # Home & general pages
+    "home": [ALL],
+    "health": [ALL],  # Intentionally no decorator
+    # Domain management
+    "domain": [HAS_PORTFOLIO_DOMAINS_VIEW_ALL, IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-nameservers": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec-dsdata": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-org-name-address": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-suborganization": [IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-senior-official": [IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-security-email": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-renewal": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users-add": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-user-delete": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # Portfolio management
+    "domains": [HAS_PORTFOLIO_DOMAINS_ANY_PERM],
+    "no-portfolio-domains": [IS_PORTFOLIO_MEMBER],
+    "no-organization-domains": [IS_PORTFOLIO_MEMBER],
+    "members": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "new-member": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "domain-requests": [HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM],
+    "no-portfolio-requests": [IS_PORTFOLIO_MEMBER],
+    "organization": [IS_PORTFOLIO_MEMBER],
+    "senior-official": [IS_PORTFOLIO_MEMBER],
+    # Domain requests
+    "domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
+    "domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "edit-domain-request": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    # Admin functions
+    "analytics": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_type": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_domain_requests_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_federal": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_domains_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_requests_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_managed_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_unmanaged_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "transfer_user": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    # Analytics
+    "all-domain-metadata": [IS_STAFF],
+    "current-full": [IS_STAFF],
+    "all-domain-requests-metadata": [IS_STAFF],
+    "domain-growth": [IS_STAFF],
+    "request-growth": [IS_STAFF],
+    "managed-domains": [IS_STAFF],
+    "unmanaged-domains": [IS_STAFF],
+    # Reports
+    "export-user-domains-as-csv": [IS_STAFF],
+    "export-portfolio-members-as-csv": [IS_STAFF],
+    "export_members_portfolio": [HAS_PORTFOLIO_MEMBERS_VIEW],
+    "export_data_type_user": [ALL],
+    # API endpoints
+    "get-senior-official-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-portfolio-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-suborganization-list-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-federal-and-portfolio-types-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-action-needed-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-rejection-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get_domains_json": [ALL],
+    "get_domain_requests_json": [ALL],
+    "get_portfolio_members_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "get_member_domains_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    # User profile
+    "finish-user-profile-setup": [ALL],
+    "user-profile": [ALL],
+    # Invitation
+    "invitation-cancel": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # DNS Hosting
+    "prototype-domain-dns": [IS_STAFF],
+    # Domain request wizard
+    "start": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "finished": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "generic_org_type": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "tribal_government": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_federal": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_election": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_contact": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "about_your_organization": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "senior_official": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "current_sites": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "dotgov_domain": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "purpose": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "other_contacts": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "requirements": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "review": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_requesting_entity": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+}
+
+UNCHECKED_URLS = [
+    "health",
+    "openid/",
+    "get-current-federal",
+    "get-current-full",
+    "available",
+    "rdap",
+    "todo",
+]
+
+
+def verify_all_urls_have_permissions():
+    """
+    Utility function to verify that all URLs in the application have defined permissions
+    in the permissions mapping.
+    """
+    from django.urls import get_resolver
+
+    resolver = get_resolver()
+    missing_permissions = []
+
+    # Collect all URL pattern names
+    for pattern in resolver.url_patterns:
+        if hasattr(pattern, "name") and pattern.name:
+            if pattern.name not in URL_PERMISSIONS and pattern.name not in UNCHECKED_URLS:
+                missing_permissions.append(pattern.name)
+
+    return missing_permissions
+
+
+def validate_permissions():  # noqa: C901
+    """
+    Validates that all URL patterns have consistent permission rules between
+    the centralized mapping and view decorators.
+
+    Returns a dictionary of issues found.
+    """
+    from django.urls import get_resolver, URLPattern, URLResolver
+
+    resolver = get_resolver()
+    issues = {
+        "missing_in_mapping": [],  # URLs with decorators but not in mapping
+        "missing_decorator": [],  # URLs in mapping but missing decorators
+        "permission_mismatch": [],  # URLs with different permissions
+    }
+
+    def check_url_pattern(pattern, parent_path=""):
+        if isinstance(pattern, URLPattern):
+            view_func = pattern.callback
+            path = f"{parent_path}/{pattern.pattern}"
+            url_name = pattern.name
+
+            if url_name:
+                # Skip check for endpoints that intentionally have no decorator
+                if url_name in UNCHECKED_URLS:
+                    return
+
+                # Check if view has decorator but missing from mapping
+                if getattr(view_func, "has_explicit_access", False) and url_name not in URL_PERMISSIONS:
+                    issues["missing_in_mapping"].append((url_name, path))
path: src/registrar/permissions.py
user: login: Matt-Spence
body: The goal of this PR is to ensure that A) we have a central location to set access controls that are *enforced* and B) that any newly created urls *must* have access controls defined in that location, as a way to force the developer to remember to think about then.
created_at: 2025-03-27T15:53:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -703,6 +704,7 @@ class PrototypeDomainDNSRecordForm(forms.Form):
     )
 
 
+@grant_access(IS_STAFF)
path: src/registrar/views/domain.py
user: login: Matt-Spence
body: The test caught it!
created_at: 2025-03-27T15:54:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,219 @@
+"""
+Centralized permissions management for the registrar.
+"""
+
+from registrar.decorators import (
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
+    IS_STAFF,
+    IS_DOMAIN_MANAGER,
+    IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
+    IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
+    IS_CISA_ANALYST,
+    IS_OMB_ANALYST,
+    IS_FULL_ACCESS,
+    IS_DOMAIN_REQUEST_CREATOR,
+    IS_STAFF_MANAGING_DOMAIN,
+    IS_PORTFOLIO_MEMBER,
+    HAS_PORTFOLIO_DOMAINS_ANY_PERM,
+    HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
+    HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
+    HAS_PORTFOLIO_MEMBERS_EDIT,
+    HAS_PORTFOLIO_MEMBERS_ANY_PERM,
+    HAS_PORTFOLIO_MEMBERS_VIEW,
+    ALL,
+)
+
+# Define permissions for each URL pattern by name
+URL_PERMISSIONS = {
+    # Home & general pages
+    "home": [ALL],
+    "health": [ALL],  # Intentionally no decorator
+    # Domain management
+    "domain": [HAS_PORTFOLIO_DOMAINS_VIEW_ALL, IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-nameservers": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-dns-dnssec-dsdata": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-org-name-address": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-suborganization": [IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-senior-official": [IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-security-email": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-renewal": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-users-add": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    "domain-user-delete": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # Portfolio management
+    "domains": [HAS_PORTFOLIO_DOMAINS_ANY_PERM],
+    "no-portfolio-domains": [IS_PORTFOLIO_MEMBER],
+    "no-organization-domains": [IS_PORTFOLIO_MEMBER],
+    "members": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "member-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "member-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-delete": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-permissions": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "invitedmember-domains": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "invitedmember-domains-edit": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "new-member": [HAS_PORTFOLIO_MEMBERS_EDIT],
+    "domain-requests": [HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM],
+    "no-portfolio-requests": [IS_PORTFOLIO_MEMBER],
+    "organization": [IS_PORTFOLIO_MEMBER],
+    "senior-official": [IS_PORTFOLIO_MEMBER],
+    # Domain requests
+    "domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
+    "domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "edit-domain-request": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    # Admin functions
+    "analytics": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_type": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_domain_requests_full": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_data_federal": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_domains_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_requests_growth": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_managed_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "export_unmanaged_domains": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    "transfer_user": [IS_CISA_ANALYST, IS_FULL_ACCESS],
+    # Analytics
+    "all-domain-metadata": [IS_STAFF],
+    "current-full": [IS_STAFF],
+    "all-domain-requests-metadata": [IS_STAFF],
+    "domain-growth": [IS_STAFF],
+    "request-growth": [IS_STAFF],
+    "managed-domains": [IS_STAFF],
+    "unmanaged-domains": [IS_STAFF],
+    # Reports
+    "export-user-domains-as-csv": [IS_STAFF],
+    "export-portfolio-members-as-csv": [IS_STAFF],
+    "export_members_portfolio": [HAS_PORTFOLIO_MEMBERS_VIEW],
+    "export_data_type_user": [ALL],
+    # API endpoints
+    "get-senior-official-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-portfolio-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-suborganization-list-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-federal-and-portfolio-types-from-federal-agency-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-action-needed-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get-rejection-email-for-user-json": [IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST],
+    "get_domains_json": [ALL],
+    "get_domain_requests_json": [ALL],
+    "get_portfolio_members_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    "get_member_domains_json": [HAS_PORTFOLIO_MEMBERS_ANY_PERM],
+    # User profile
+    "finish-user-profile-setup": [ALL],
+    "user-profile": [ALL],
+    # Invitation
+    "invitation-cancel": [IS_DOMAIN_MANAGER, IS_STAFF_MANAGING_DOMAIN],
+    # DNS Hosting
+    "prototype-domain-dns": [IS_STAFF],
+    # Domain request wizard
+    "start": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "finished": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "generic_org_type": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "tribal_government": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_federal": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_election": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "organization_contact": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "about_your_organization": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "senior_official": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "current_sites": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "dotgov_domain": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "purpose": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "other_contacts": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "requirements": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "review": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_requesting_entity": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+    "portfolio_additional_details": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
+}
+
+UNCHECKED_URLS = [
+    "health",
+    "openid/",
+    "get-current-federal",
+    "get-current-full",
+    "available",
+    "rdap",
+    "todo",
+]
+
+
+def verify_all_urls_have_permissions():
+    """
+    Utility function to verify that all URLs in the application have defined permissions
+    in the permissions mapping.
+    """
+    from django.urls import get_resolver
+
+    resolver = get_resolver()
+    missing_permissions = []
+
+    # Collect all URL pattern names
+    for pattern in resolver.url_patterns:
+        if hasattr(pattern, "name") and pattern.name:
path: src/registrar/permissions.py
user: login: Matt-Spence
body: Yes there can be! I've added some handling for that, and found another small issue with handle url resolvers while I was at it. Great catch.
created_at: 2025-03-27T17:22:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-20T16:36:24Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-20T18:33:59Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-24T14:36:52Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-27T15:37:31Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-27T15:54:32Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-27T15:57:11Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-27T17:20:17Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-27T17:26:09Z
body: 🥳 Successfully deployed to developer sandbox **[ab](https://getgov-ab.app.cloud.gov/)**.
author_association: NONE