Merge branch 'main' into za/3662-uat-testing-issues

This commit is contained in:
zandercymatics 2025-03-27 14:36:15 -06:00
commit 2e1e1e1c20
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
10 changed files with 443 additions and 3 deletions

View file

@ -72,6 +72,7 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
@ -3144,9 +3145,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
Returns a tuple: (obj: DomainRequest, should_proceed: bool)
"""
should_proceed = True
error_message = None
domain_name = original_obj.requested_domain.name
# Get the method that should be run given the status
selected_method = self.get_status_method_mapping(obj)
@ -3168,6 +3169,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
# duplicated in the model and the error is raised from the model.
# This avoids an ugly Django error screen.
error_message = "This action is not permitted. The domain is already active."
elif (
original_obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
and obj.status == models.DomainRequest.DomainRequestStatus.APPROVED
and original_obj.requested_domain is not None
and Domain.is_pending_delete(domain_name)
):
# 1. If the domain request is not approved in previous state (original status)
# 2. If the new status that's supposed to be triggered IS approved
# 3. That it's a valid domain
# 4. AND that the domain is currently in pendingDelete state
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.DOMAIN_IS_PENDING_DELETE)
elif (
original_obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
and obj.status == models.DomainRequest.DomainRequestStatus.APPROVED

View file

@ -171,6 +171,7 @@ urlpatterns = [
path(
"admin/logout/",
RedirectView.as_view(pattern_name="logout", permanent=False),
name="logout",
),
path(
"admin/analytics/export_data_type/",

View file

@ -256,6 +256,29 @@ class Domain(TimeStampedModel, DomainHelper):
req = commands.CheckDomain([domain_name])
return registry.send(req, cleaned=True).res_data[0].avail
@classmethod
def is_pending_delete(cls, domain: str) -> bool:
"""Check if domain is pendingDelete state via response from registry."""
domain_name = domain.lower()
try:
info_req = commands.InfoDomain(domain_name)
info_response = registry.send(info_req, cleaned=True)
# Ensure res_data exists and is not empty
if info_response and info_response.res_data:
# Use _extract_data_from_response bc it's same thing but jsonified
domain_response = cls._extract_data_from_response(cls, info_response) # type: ignore
domain_status_state = domain_response.get("statuses")
if "pendingDelete" in str(domain_status_state):
return True
except RegistryError as err:
if not err.is_connection_error():
logger.info(f"Domain does not exist yet so it won't be in pending delete -- {err}")
return False
else:
raise err
return False
@classmethod
def registered(cls, domain: str) -> bool:
"""Check if a domain is _not_ available."""
@ -2026,7 +2049,9 @@ class Domain(TimeStampedModel, DomainHelper):
def _extract_data_from_response(self, data_response):
"""extract data from response from registry"""
data = data_response.res_data[0]
return {
"auth_info": getattr(data, "auth_info", ...),
"_contacts": getattr(data, "contacts", ...),

View file

@ -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

View file

@ -1092,7 +1092,7 @@ def completed_domain_request( # noqa
email="testy@town.com",
phone="(555) 555 5555",
)
domain, _ = DraftDomain.objects.get_or_create(name=name)
domain = DraftDomain.objects.create(name=name)
other, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",

View file

@ -50,6 +50,7 @@ from unittest.mock import ANY, patch
from django.conf import settings
import boto3_mocking # type: ignore
import logging
from django.contrib.messages.storage.fallback import FallbackStorage
logger = logging.getLogger(__name__)
@ -2315,7 +2316,6 @@ class TestDomainRequestAdmin(MockEppLib):
Used to test errors when saving a change with an active domain, also used to test side effects
when saving a change goes through."""
with less_console_noise():
# Create an instance of the model
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED)
@ -2428,6 +2428,44 @@ class TestDomainRequestAdmin(MockEppLib):
"Cannot approve. Requested domain is already in use.",
)
def test_error_when_approving_domain_in_pending_delete_state(self):
"""If in pendingDelete state from in review -> approve not allowed."""
# 1. Create domain
to_be_in_pending_deleted = completed_domain_request(
status=DomainRequest.DomainRequestStatus.SUBMITTED, name="meoward1.gov"
)
to_be_in_pending_deleted.creator = self.superuser
# 2. Put domain into in-review state
to_be_in_pending_deleted.status = DomainRequest.DomainRequestStatus.IN_REVIEW
to_be_in_pending_deleted.save()
# 3. Update request as a superuser
request = self.factory.post(f"/admin/registrar/domainrequest/{to_be_in_pending_deleted.pk}/change/")
request.user = self.superuser
request.session = {}
setattr(request, "_messages", FallbackStorage(request))
# 4. Use ExitStack for combine patching like above
with boto3_mocking.clients.handler_for("sesv2", self.mock_client), ExitStack() as stack:
# Patch django.contrib.messages.error
stack.enter_context(patch.object(messages, "error"))
with patch("registrar.models.domain.Domain.is_pending_delete", return_value=True):
# Attempt to approve to_be_in_pending_deleted
# (which should fail due is_pending_delete == True so nothing can get approved)
to_be_in_pending_deleted.status = DomainRequest.DomainRequestStatus.APPROVED
# Save the model with the patched request
self.admin.save_model(request, to_be_in_pending_deleted, None, True)
messages.error.assert_called_once_with(
request,
"Domain of same name is currently in pending delete state.",
)
@less_console_noise
def test_no_error_when_saving_to_approved_and_domain_exists(self):
"""The negative of the redundant admin check on model transition not allowed."""

View file

@ -666,6 +666,66 @@ class TestDomainAvailable(MockEppLib):
self.assertFalse(available)
patcher.stop()
def test_is_pending_delete(self):
"""
Scenario: Testing if a domain is in pendingDelete status from the registry
Should return True
* Mock EPP response with pendingDelete status
* Validate InfoDomain command is called
* Validate response given mock
"""
with patch("registrar.models.domain.registry.send") as mocked_send, patch(
"registrar.models.domain.Domain._extract_data_from_response"
) as mocked_extract:
# Mock the registry response
mock_response = MagicMock()
mock_response.res_data = [MagicMock(statuses=[MagicMock(state="pendingDelete")])]
mocked_send.return_value = mock_response
# Mock JSONified response
mocked_extract.return_value = {"statuses": ["pendingDelete"]}
result = Domain.is_pending_delete("is-pending-delete.gov")
mocked_send.assert_called_once_with(commands.InfoDomain("is-pending-delete.gov"), cleaned=True)
mocked_extract.assert_called_once()
self.assertTrue(result)
def test_is_not_pending_delete(self):
"""
Scenario: Testing if a domain is NOT in pendingDelete status.
Should return False.
* Mock EPP response without pendingDelete status (isserverTransferProhibited)
* Validate response given mock
"""
with patch("registrar.models.domain.registry.send") as mocked_send, patch(
"registrar.models.domain.Domain._extract_data_from_response"
) as mocked_extract:
# Mock the registry response
mock_response = MagicMock()
mock_response.res_data = [
MagicMock(statuses=[MagicMock(state="serverTransferProhibited"), MagicMock(state="ok")])
]
mocked_send.return_value = mock_response
# Mock JSONified response
mocked_extract.return_value = {"statuses": ["serverTransferProhibited", "ok"]}
result = Domain.is_pending_delete("is-not-pending.gov")
# Assertions
mocked_send.assert_called_once_with(commands.InfoDomain("is-not-pending.gov"), cleaned=True)
mocked_extract.assert_called_once()
self.assertFalse(result)
def test_domain_available_with_invalid_error(self):
"""
Scenario: Testing whether an invalid domain is available

View file

@ -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))

View file

@ -129,6 +129,7 @@ class FSMErrorCodes(IntEnum):
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
- 4 NO_REJECTION_REASON No rejection reason is specified
- 5 NO_ACTION_NEEDED_REASON No action needed reason is specified
- 6 DOMAIN_IS_PENDING_DELETE Domain is in pending delete state
"""
APPROVE_DOMAIN_IN_USE = 1
@ -136,6 +137,7 @@ class FSMErrorCodes(IntEnum):
INVESTIGATOR_NOT_STAFF = 3
NO_REJECTION_REASON = 4
NO_ACTION_NEEDED_REASON = 5
DOMAIN_IS_PENDING_DELETE = 6
class FSMDomainRequestError(Exception):
@ -150,6 +152,7 @@ class FSMDomainRequestError(Exception):
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
FSMErrorCodes.NO_REJECTION_REASON: ("A reason is required for this status."),
FSMErrorCodes.NO_ACTION_NEEDED_REASON: ("A reason is required for this status."),
FSMErrorCodes.DOMAIN_IS_PENDING_DELETE: ("Domain of same name is currently in pending delete state."),
}
def __init__(self, *args, code=None, **kwargs):

View file

@ -15,6 +15,7 @@ from registrar.decorators import (
IS_DOMAIN_MANAGER,
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
IS_STAFF,
IS_STAFF_MANAGING_DOMAIN,
grant_access,
)
@ -710,6 +711,7 @@ class PrototypeDomainDNSRecordForm(forms.Form):
)
@grant_access(IS_STAFF)
class PrototypeDomainDNSRecordView(DomainFormBaseView):
template_name = "prototype_domain_dns.html"
form_class = PrototypeDomainDNSRecordForm