merged with main and resolved conflict

This commit is contained in:
Alysia Broddrick 2023-09-10 15:47:49 -07:00
commit 1a680f6470
No known key found for this signature in database
GPG key ID: 03917052CD0F06B7
52 changed files with 1360 additions and 160 deletions

View file

@ -1,6 +1,6 @@
# Get (your very own) .gov
# Infrastructure as a (public) service
Welcome to the repo for a WIP brand new registrar for .gov domains. Get.gov intends to serve all government entities in the United States looking for a .gov domain to use publicly (for a website, for an email address, etc.). Here you can find the code for the registrar and other artifacts about our product strategy and research.
The .gov domain helps U.S.-based government organizations gain public trust by being easily recognized online. This repo contains the code for the new .gov registrar where governments request and manage domains and other artifacts about our product strategy and research.
## Onboarding

View file

@ -20,8 +20,8 @@ applications:
DJANGO_BASE_URL: https://getgov-ab.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-ab.app.cloud.gov
services:

View file

@ -20,8 +20,8 @@ applications:
DJANGO_BASE_URL: https://getgov-bl.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-bl.app.cloud.gov
services:

View file

@ -20,8 +20,8 @@ applications:
DJANGO_BASE_URL: https://getgov-dk.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-dk.app.cloud.gov
services:

View file

@ -20,8 +20,8 @@ applications:
DJANGO_BASE_URL: https://getgov-rjm.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-rjm.app.cloud.gov
services:

View file

@ -20,8 +20,8 @@ applications:
DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-stable.app.cloud.gov
services:

View file

@ -0,0 +1,128 @@
# This script sets up a completely new Cloud.gov CF Space with all the corresponding
# infrastructure needed to run get.gov. It can serve for documentation for running
# NOTE: This script was written for MacOS and to be run at the root directory
# of the repository. It uses `docker compose` to compile assets, so it is
# safest to `docker compose down` before using this script.
if [ -z "$1" ]; then
echo 'Please specify a name on the command line for the new space (i.e. lmm)' >&2
exit 1
fi
if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then
echo "jq, cf, and gh packages must be installed. Please install via your preferred manager."
exit 1
fi
upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]')
read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
git checkout -b new-environment-$1
fi
cf target -o cisa-dotgov
read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
cf login -a https://api.fr.cloud.gov --sso
fi
gh auth status
read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
gh auth login
fi
echo "Creating manifest for $1..."
cp ops/scripts/manifest-sandbox-template-migrate.yaml ops/manifests/manifest-$1.yaml
sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml"
echo "Creating new cloud.gov space for $1..."
cf create-space $1
cf target -o "cisa-dotgov" -s $1
cf bind-security-group public_networks_egress cisa-dotgov --space $1
cf bind-security-group trusted_local_networks_egress cisa-dotgov --space $1
echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..."
cf create-service aws-rds micro-psql getgov-$1-database
until cf service getgov-$1-database | grep -q 'The service instance status is succeeded'
do
echo "Database not up yet, waiting..."
sleep 30
done
echo "Creating new cloud.gov credentials for $1..."
django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())')
openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt
login_key=$(base64 -i private-$1.pem)
jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json
cf cups getgov-credentials -p credentials-$1.json
echo "Now you will need to update some things for Login. Please sign-in to https://dashboard.int.identitysandbox.gov/."
echo "Navigate to our application config: https://dashboard.int.identitysandbox.gov/service_providers/2640/edit?"
echo "There are two things to update."
echo "1. You need to upload the public-$1.crt file generated as part of the previous command."
echo "2. You need to add two redirect URIs: https://getgov-$1.app.cloud.gov/openid/callback/login/ and
https://getgov-$1.app.cloud.gov/openid/callback/logout/ to the list of URIs."
read -p "Please confirm when this is done (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
exit 1
fi
echo "Database create succeeded and credentials created. Deploying the get.gov application to the new space $1..."
echo "Building assets..."
open -a Docker
cd src/
./build.sh
cd ..
cf push getgov-$1 -f ops/manifests/manifest-$1.yaml
read -p "Please provide the email of the space developer: " -r
cf set-space-role $REPLY cisa-dotgov $1 SpaceDeveloper
read -p "Should we run migrations? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
cf run-task getgov-$1 --command 'python manage.py migrate' --name migrate
fi
echo "Alright, your app is up and running at https://getgov-$1.app.cloud.gov!"
echo
echo "Moving on to setup Github automation..."
echo "Creating space deployer for Github deploys..."
cf create-service cloud-gov-service-account space-deployer github-cd-account
cf create-service-key github-cd-account github-cd-key
cf service-key github-cd-account github-cd-key
read -p "Please confirm we should set the above username and key to Github secrets. (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
exit 1
fi
cf service-key github-cd-account github-cd-key | sed 1,2d | jq -r '[.credentials.username, .credentials.password]|@tsv' |
while read -r username password; do
gh secret --repo cisagov/getgov set CF_${upcase_name}_USERNAME --body $username
gh secret --repo cisagov/getgov set CF_${upcase_name}_PASSWORD --body $password
done
read -p "All done! Should we open a PR with these changes? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
git add ops/manifests/manifest-$1.yaml .github/workflows/ src/registrar/config/settings.py
git commit -m "Add new developer sandbox '"$1"' infrastructure"
gh pr create
fi

View file

@ -0,0 +1,30 @@
---
applications:
- name: getgov-ENVIRONMENT
buildpacks:
- python_buildpack
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http
health-check-http-endpoint: /health
env:
# Send stdout and stderr straight to the terminal without buffering
PYTHONUNBUFFERED: yup
# Tell Django where to find its configuration
DJANGO_SETTINGS_MODULE: registrar.config.settings
# Tell Django where it is being hosted
DJANGO_BASE_URL: https://getgov-ENVIRONMENT.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
# use a non-default route to avoid conflicts
routes:
- route: getgov-ENVIRONMENT-migrate.app.cloud.gov
services:
- getgov-credentials
- getgov-ENVIRONMENT-database

36
ops/scripts/migrate_new_old.sh Executable file
View file

@ -0,0 +1,36 @@
# This script sets up a completely new Cloud.gov CF Space with all the corresponding
# infrastructure needed to run get.gov. It can serve for documentation for running
# NOTE: This script was written for MacOS and to be run at the root directory
# of the repository.
if [ -z "$1" ]; then
echo 'Please specify a name on the command line for the new space (i.e. lmm)' >&2
exit 1
fi
# The user running this script has to be a SpaceDeveloper on both the
# new and old org/spaces.
OLD_ORG="cisa-getgov-prototyping"
NEW_ORG="cisa-dotgov"
#
# delete old route
cf target -o $OLD_ORG -s $1
cf delete-route app.cloud.gov -n getgov-$1
# re-claim the route on new orf
cf target -o $NEW_ORG -s $1
cf map-route getgov-$1 app.cloud.gov -n getgov-$1
cf delete-route app.cloud.gov -n getgov-$1-migrate
# delete old app and services
cf target -o $OLD_ORG -s $1
cf delete getgov-$1
cf delete-service getgov-$1-database
cf delete-service getgov-credentials
cf delete-service getgov-cd-account
cf delete-space $1
printf "Remove -migrate from ops/manifests/manifest-$1.yaml"

View file

@ -238,6 +238,9 @@ class Client(oic.Client):
"client_secret": self.client_secret,
},
authn_method=self.registration_response["token_endpoint_auth_method"],
# There is a time desync issue between login.gov and cloud
# this addresses that by adding a clock skew.
skew=10,
)
except Exception as err:
logger.error(err)

View file

@ -54,6 +54,7 @@ services:
# command: "python"
command: >
bash -c " python manage.py migrate &&
python manage.py load &&
python manage.py runserver 0.0.0.0:8080"
db:

View file

@ -104,6 +104,36 @@ class MyUserAdmin(BaseUserAdmin):
inlines = [UserContactInline]
list_display = (
"email",
"first_name",
"last_name",
"is_staff",
"is_superuser",
"status",
)
fieldsets = (
(
None,
{"fields": ("username", "password", "status")},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
def get_list_display(self, request):
if not request.user.is_superuser:
# Customize the list display for staff users
@ -146,7 +176,7 @@ class DomainAdmin(ListHeaderAdmin):
readonly_fields = ["state"]
def response_change(self, request, obj):
ACTION_BUTTON = "_place_client_hold"
GET_SECURITY_EMAIL = "_get_security_email"
SET_SECURITY_CONTACT = "_set_security_contact"
MAKE_DOMAIN = "_make_domain_in_registry"
@ -157,7 +187,10 @@ class DomainAdmin(ListHeaderAdmin):
REMOVE_CLIENT_HOLD = "_rem_client_hold"
DELETE_DOMAIN = "_delete_domain"
if ACTION_BUTTON in request.POST:
PLACE_HOLD = "_place_client_hold"
EDIT_DOMAIN = "_edit_domain"
if PLACE_HOLD in request.POST:
try:
obj.place_client_hold()
except Exception as err:
@ -195,7 +228,7 @@ class DomainAdmin(ListHeaderAdmin):
)
return HttpResponseRedirect(".")
if SET_SECURITY_CONTACT in request.POST:
elif SET_SECURITY_CONTACT in request.POST:
try:
fake_email = "manuallyEnteredEmail@test.gov"
if PublicContact.objects.filter(
@ -218,7 +251,7 @@ class DomainAdmin(ListHeaderAdmin):
("The security email is %" ". Thanks!") % fake_email,
)
if MAKE_DOMAIN in request.POST:
elif MAKE_DOMAIN in request.POST:
try:
obj._get_or_create_domain()
except Exception as err:
@ -230,9 +263,8 @@ class DomainAdmin(ListHeaderAdmin):
)
return HttpResponseRedirect(".")
# make nameservers here
if MAKE_NAMESERVERS in request.POST:
elif MAKE_NAMESERVERS in request.POST:
try:
hosts = [("ns1.example.com", None), ("ns2.example.com", None)]
obj.nameservers = hosts
@ -244,7 +276,7 @@ class DomainAdmin(ListHeaderAdmin):
("Hosts set to be %s" ". Thanks!") % hosts,
)
return HttpResponseRedirect(".")
if GET_NAMESERVERS in request.POST:
elif GET_NAMESERVERS in request.POST:
try:
nameservers = obj.nameservers
except Exception as err:
@ -256,7 +288,7 @@ class DomainAdmin(ListHeaderAdmin):
)
return HttpResponseRedirect(".")
if GET_STATUS in request.POST:
elif GET_STATUS in request.POST:
try:
statuses = obj.statuses
except Exception as err:
@ -268,7 +300,7 @@ class DomainAdmin(ListHeaderAdmin):
)
return HttpResponseRedirect(".")
if SET_CLIENT_HOLD in request.POST:
elif SET_CLIENT_HOLD in request.POST:
try:
obj.clientHold()
obj.save()
@ -281,7 +313,7 @@ class DomainAdmin(ListHeaderAdmin):
)
return HttpResponseRedirect(".")
if REMOVE_CLIENT_HOLD in request.POST:
elif REMOVE_CLIENT_HOLD in request.POST:
try:
obj.revertClientHold()
obj.save()
@ -293,7 +325,8 @@ class DomainAdmin(ListHeaderAdmin):
("Domain %s will now have client hold removed") % obj.name,
)
return HttpResponseRedirect(".")
if DELETE_DOMAIN in request.POST:
elif DELETE_DOMAIN in request.POST:
try:
obj.deleted()
obj.save()
@ -305,8 +338,30 @@ class DomainAdmin(ListHeaderAdmin):
("Domain %s Should now be deleted " ". Thanks!") % obj.name,
)
return HttpResponseRedirect(".")
elif EDIT_DOMAIN in request.POST:
# We want to know, globally, when an edit action occurs
request.session["analyst_action"] = "edit"
# Restricts this action to this domain (pk) only
request.session["analyst_action_location"] = obj.id
return HttpResponseRedirect(reverse("domain", args=(obj.id,)))
return super().response_change(request, obj)
def change_view(self, request, object_id):
# If the analyst was recently editing a domain page,
# delete any associated session values
if "analyst_action" in request.session:
del request.session["analyst_action"]
del request.session["analyst_action_location"]
return super().change_view(request, object_id)
def has_change_permission(self, request, obj=None):
# Fixes a bug wherein users which are only is_staff
# can access 'change' when GET,
# but cannot access this page when it is a request of type POST.
if request.user.is_staff:
return True
return super().has_change_permission(request, obj)
class ContactAdmin(ListHeaderAdmin):
"""Custom contact admin class to add search."""
@ -319,6 +374,10 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"""Customize the applications listing view."""
# Set multi-selects 'read-only' (hide selects and show data)
# based on user perms and application creator's status
# form = DomainApplicationForm
# Columns
list_display = [
"requested_domain",
@ -392,7 +451,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
]
# Read only that we'll leverage for CISA Analysts
readonly_fields = [
analyst_readonly_fields = [
"creator",
"type_of_work",
"more_organization_information",
@ -410,49 +469,81 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):
if change: # Check if the application is being edited
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if obj and obj.creator.status != models.User.RESTRICTED:
if change: # Check if the application is being edited
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if obj.status != original_obj.status:
if obj.status == models.DomainApplication.STARTED:
# No conditions
pass
elif obj.status == models.DomainApplication.SUBMITTED:
# This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the
# status to what it was before the admin user changed it and
# let the fsm method set it. Same comment applies to
# transition method calls below.
obj.status = original_obj.status
obj.submit()
elif obj.status == models.DomainApplication.IN_REVIEW:
obj.status = original_obj.status
obj.in_review()
elif obj.status == models.DomainApplication.ACTION_NEEDED:
obj.status = original_obj.status
obj.action_needed()
elif obj.status == models.DomainApplication.APPROVED:
obj.status = original_obj.status
obj.approve()
elif obj.status == models.DomainApplication.WITHDRAWN:
obj.status = original_obj.status
obj.withdraw()
elif obj.status == models.DomainApplication.REJECTED:
obj.status = original_obj.status
obj.reject()
else:
logger.warning("Unknown status selected in django admin")
if obj.status != original_obj.status:
status_method_mapping = {
models.DomainApplication.STARTED: None,
models.DomainApplication.SUBMITTED: obj.submit,
models.DomainApplication.IN_REVIEW: obj.in_review,
models.DomainApplication.ACTION_NEEDED: obj.action_needed,
models.DomainApplication.APPROVED: obj.approve,
models.DomainApplication.WITHDRAWN: obj.withdraw,
models.DomainApplication.REJECTED: obj.reject,
models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice,
}
selected_method = status_method_mapping.get(obj.status)
if selected_method is None:
logger.warning("Unknown status selected in django admin")
else:
# This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the
# status to what it was before the admin user changed it and
# let the fsm method set it.
obj.status = original_obj.status
selected_method()
super().save_model(request, obj, form, change)
super().save_model(request, obj, form, change)
else:
# Clear the success message
messages.set_level(request, messages.ERROR)
messages.error(
request,
"This action is not permitted for applications "
+ "with a restricted creator.",
)
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
We have 2 conditions that determine which fields are read-only:
admin user permissions and the application creator's status, so
we'll use the baseline readonly_fields and extend it as needed.
"""
readonly_fields = list(self.readonly_fields)
# Check if the creator is restricted
if obj and obj.creator.status == models.User.RESTRICTED:
# For fields like CharField, IntegerField, etc., the widget used is
# straightforward and the readonly_fields list can control their behavior
readonly_fields.extend([field.name for field in self.model._meta.fields])
# Add the multi-select fields to readonly_fields:
# Complex fields like ManyToManyField require special handling
readonly_fields.extend(
["current_websites", "other_contacts", "alternative_domains"]
)
if request.user.is_superuser:
# Superusers have full access, no fields are read-only
return []
return readonly_fields
else:
# Regular users can only view the specified fields
return self.readonly_fields
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
def display_restricted_warning(self, request, obj):
if obj and obj.creator.status == models.User.RESTRICTED:
messages.warning(
request,
"Cannot edit an application with a restricted creator.",
)
def change_view(self, request, object_id, form_url="", extra_context=None):
obj = self.get_object(request, object_id)
self.display_restricted_warning(request, obj)
return super().change_view(request, object_id, form_url, extra_context)
admin.site.register(models.User, MyUserAdmin)

View file

@ -0,0 +1,50 @@
/**
* @file get-gov-admin.js includes custom code for the .gov registrar admin portal.
*
* Constants and helper functions are at the top.
* Event handlers are in the middle.
* Initialization (run-on-load) stuff goes at the bottom.
*/
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Helper functions.
/** Either sets attribute target="_blank" to a given element, or removes it */
function openInNewTab(el, removeAttribute = false){
if(removeAttribute){
el.setAttribute("target", "_blank");
}else{
el.removeAttribute("target", "_blank");
}
};
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Event handlers.
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
// Initialization code.
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
* Currently only appends target="_blank" to the domain_form object,
* but this can be expanded.
*/
(function (){
/*
On mouseover, appends target="_blank" on domain_form under the Domain page.
The reason for this is that the template has a form that contains multiple buttons.
The structure of that template complicates seperating those buttons
out of the form (while maintaining the same position on the page).
However, if we want to open one of those submit actions to a new tab -
such as the manage domain button - we need to dynamically append target.
As there is no built-in django method which handles this, we do it here.
*/
function prepareDjangoAdmin() {
let domainFormElement = document.getElementById("domain_form");
let domainSubmitButton = document.getElementById("manageDomainSubmitButton");
if(domainSubmitButton && domainFormElement){
domainSubmitButton.addEventListener("mouseover", () => openInNewTab(domainFormElement, true));
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
}
}
prepareDjangoAdmin();
})();

View file

@ -432,3 +432,21 @@ abbr[title] {
height: units('mobile');
}
}
// Fixes some font size disparities with the Figma
// for usa-alert alert elements
.usa-alert {
.usa-alert__heading.larger-font-sizing {
font-size: units(3);
}
}
// The icon was off center for some reason
// Fixes that issue
@media (min-width: 64em){
.usa-alert--warning{
.usa-alert__body::before {
left: 1rem !important;
}
}
}

View file

@ -251,8 +251,7 @@ AWS_MAX_ATTEMPTS = 3
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})
# email address to use for various automated correspondence
# TODO: pick something sensible here
DEFAULT_FROM_EMAIL = "registrar@get.gov"
DEFAULT_FROM_EMAIL = "help@get.gov <help@get.gov>"
# connect to an (external) SMTP server for sending email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"

View file

@ -63,7 +63,7 @@ class UserFixture:
"last_name": "Adkinson",
},
{
"username": "bb21f687-c773-4df3-9243-111cfd4c0be4",
"username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
"first_name": "Paul",
"last_name": "Kuykendall",
},
@ -72,6 +72,16 @@ class UserFixture:
"first_name": "Rebecca",
"last_name": "Hsieh",
},
{
"username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52",
"first_name": "David",
"last_name": "Kennedy",
},
{
"username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
"first_name": "Nicolle",
"last_name": "LeClair",
},
]
STAFF = [
@ -86,6 +96,12 @@ class UserFixture:
"first_name": "Alysia-Analyst",
"last_name": "Alysia-Analyst",
},
{
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
"first_name": "Katherine-Analyst",
"last_name": "Osos-Analyst",
"email": "kosos@truss.works",
},
{
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
"first_name": "Zander-Analyst",
@ -101,6 +117,23 @@ class UserFixture:
"first_name": "Rebecca-Analyst",
"last_name": "Hsieh-Analyst",
},
{
"username": "5dc6c9a6-61d9-42b4-ba54-4beff28bac3c",
"first_name": "David-Analyst",
"last_name": "Kennedy-Analyst",
},
{
"username": "0eb6f326-a3d4-410f-a521-aa4c1fad4e47",
"first_name": "Gaby-Analyst",
"last_name": "DiSarli-Analyst",
"email": "gaby@truss.works",
},
{
"username": "cfe7c2fc-e24a-480e-8b78-28645a1459b3",
"first_name": "Nicolle-Analyst",
"last_name": "LeClair",
"email": "nicolle.leclair@ecstech.com",
},
]
STAFF_PERMISSIONS = [
@ -248,6 +281,14 @@ class DomainApplicationFixture:
"status": "withdrawn",
"organization_name": "Example - Withdrawn",
},
{
"status": "action needed",
"organization_name": "Example - Action Needed",
},
{
"status": "rejected",
"organization_name": "Example - Rejected",
},
]
@classmethod

View file

@ -2,6 +2,7 @@ import logging
from django.core.management.base import BaseCommand
from auditlog.context import disable_auditlog # type: ignore
from django.conf import settings
from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture
@ -12,8 +13,11 @@ class Command(BaseCommand):
def handle(self, *args, **options):
# django-auditlog has some bugs with fixtures
# https://github.com/jazzband/django-auditlog/issues/17
with disable_auditlog():
UserFixture.load()
DomainApplicationFixture.load()
DomainFixture.load()
logger.info("All fixtures loaded.")
if settings.DEBUG:
with disable_auditlog():
UserFixture.load()
DomainApplicationFixture.load()
DomainFixture.load()
logger.info("All fixtures loaded.")
else:
logger.warn("Refusing to load fixture data in a non DEBUG env")

View file

@ -0,0 +1,42 @@
# Generated by Django 4.2.1 on 2023-08-18 16:59
from django.db import migrations, models
import django_fsm
class Migration(migrations.Migration):
dependencies = [
("registrar", "0028_alter_domainapplication_status"),
]
operations = [
migrations.AddField(
model_name="user",
name="status",
field=models.CharField(
blank=True,
choices=[("ineligible", "ineligible")],
default=None,
max_length=10,
null=True,
),
),
migrations.AlterField(
model_name="domainapplication",
name="status",
field=django_fsm.FSMField(
choices=[
("started", "started"),
("submitted", "submitted"),
("in review", "in review"),
("action needed", "action needed"),
("approved", "approved"),
("withdrawn", "withdrawn"),
("rejected", "rejected"),
("ineligible", "ineligible"),
],
default="started",
max_length=50,
),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.1 on 2023-08-29 17:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0029_user_status_alter_domainapplication_status"),
]
operations = [
migrations.AlterField(
model_name="user",
name="status",
field=models.CharField(
blank=True,
choices=[("restricted", "restricted")],
default=None,
max_length=10,
null=True,
),
),
]

View file

@ -721,6 +721,9 @@ class Domain(TimeStampedModel, DomainHelper):
help_text="Very basic info about the lifecycle of this domain object",
)
def isActive(self):
return self.state == Domain.State.CREATED
# ForeignKey on UserDomainRole creates a "permissions" member for
# all of the user-roles that are in place for this domain

View file

@ -26,6 +26,7 @@ class DomainApplication(TimeStampedModel):
APPROVED = "approved"
WITHDRAWN = "withdrawn"
REJECTED = "rejected"
INELIGIBLE = "ineligible"
STATUS_CHOICES = [
(STARTED, STARTED),
(SUBMITTED, SUBMITTED),
@ -34,6 +35,7 @@ class DomainApplication(TimeStampedModel):
(APPROVED, APPROVED),
(WITHDRAWN, WITHDRAWN),
(REJECTED, REJECTED),
(INELIGIBLE, INELIGIBLE),
]
class StateTerritoryChoices(models.TextChoices):
@ -554,7 +556,9 @@ class DomainApplication(TimeStampedModel):
)
@transition(
field="status", source=[SUBMITTED, IN_REVIEW, REJECTED], target=APPROVED
field="status",
source=[SUBMITTED, IN_REVIEW, REJECTED, INELIGIBLE],
target=APPROVED,
)
def approve(self):
"""Approve an application that has been submitted.
@ -590,6 +594,11 @@ class DomainApplication(TimeStampedModel):
@transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN)
def withdraw(self):
"""Withdraw an application that has been submitted."""
self._send_status_update_email(
"withdraw",
"emails/domain_request_withdrawn.txt",
"emails/domain_request_withdrawn_subject.txt",
)
@transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED)
def reject(self):
@ -603,6 +612,17 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_rejected_subject.txt",
)
@transition(field="status", source=[IN_REVIEW, APPROVED], target=INELIGIBLE)
def reject_with_prejudice(self):
"""The applicant is a bad actor, reject with prejudice.
No email As a side effect, but we block the applicant from editing
any existing domains/applications and from submitting new aplications.
We do this by setting an ineligible status on the user, which the
permissions classes test against"""
self.creator.restrict_user()
# ## Form policies ###
#
# These methods control what questions need to be answered by applicants

View file

@ -17,6 +17,18 @@ class User(AbstractUser):
but can be customized later.
"""
# #### Constants for choice fields ####
RESTRICTED = "restricted"
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default=None, # Set the default value to None
null=True, # Allow the field to be null
blank=True, # Allow the field to be blank
)
domains = models.ManyToManyField(
"registrar.Domain",
through="registrar.UserDomainRole",
@ -39,6 +51,17 @@ class User(AbstractUser):
else:
return self.username
def restrict_user(self):
self.status = self.RESTRICTED
self.save()
def unrestrict_user(self):
self.status = None
self.save()
def is_restricted(self):
return self.status == self.RESTRICTED
def first_login(self):
"""Callback when the user is authenticated for the very first time.

View file

@ -1,8 +1,6 @@
import logging
from django.conf import settings
from django.core.management import call_command
from django.db.models.signals import post_save, post_migrate
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User, Contact
@ -55,13 +53,3 @@ def handle_profile(sender, instance, **kwargs):
"There are multiple Contacts with the same email address."
f" Picking #{contacts[0].id} for User #{instance.id}."
)
@receiver(post_migrate)
def handle_loaddata(**kwargs):
"""Attempt to load test fixtures when in DEBUG mode."""
if settings.DEBUG:
try:
call_command("load")
except Exception as e:
logger.warning(e)

View file

@ -0,0 +1,12 @@
{% extends "admin/change_form.html" %}
{% comment %} Replace the Django ul markup with a div. We'll edit the child markup accordingly in change_form_object_tools {% endcomment %}
{% block object-tools %}
{% if change and not is_popup %}
<div class="object-tools">
{% block object-tools-items %}
{{ block.super }}
{% endblock %}
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,20 @@
{% load i18n admin_urls %}
{% comment %} Replace li with p for more semantic HTML if we have a single child {% endcomment %}
{% block object-tools-items %}
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
{% if has_absolute_url %}
<ul>
<li>
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
</li>
<li>
<a href="{{ absolute_url }}" class="viewsitelink">{% translate "View on site" %}</a>
</li>
</ul>
{% else %}
<p class="margin-0 padding-0">
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
</p>
{% endif %}
{% endblock %}

View file

@ -24,3 +24,12 @@
{% endif %}
</h2>
{% endblock %}
{% comment %} Replace the Django ul markup with a div. We'll replace the li with a p in change_list_object_tools {% endcomment %}
{% block object-tools %}
<div class="object-tools">
{% block object-tools-items %}
{{ block.super }}
{% endblock %}
</div>
{% endblock %}

View file

@ -0,0 +1,13 @@
{% load i18n admin_urls %}
{% comment %} Replace li with p for more semantic HTML {% endcomment %}
{% block object-tools-items %}
{% if has_add_permission %}
<p class="margin-0 padding-0">
{% url cl.opts|admin_urlname:'add' as add_url %}
<a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
{% blocktranslate with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktranslate %}
</a>
</p>
{% endif %}
{% endblock %}

View file

@ -17,7 +17,7 @@ Load our custom filters to extract info from the django generated markup.
<thead>
<tr>
{% if results.0.form %}
{% if results.0|contains_checkbox %}
{# .gov - hardcode the select all checkbox #}
<th scope="col" class="action-checkbox-column" title="Toggle all">
<div class="text">
@ -60,12 +60,11 @@ Load our custom filters to extract info from the django generated markup.
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
{% endif %}
<tr>
{% with result_value=result.0|extract_value %}
{% with result_label=result.1|extract_a_text %}
<td>
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_label|default:result_value }}" class="action-select">
<label class="usa-sr-only" for="{{ result_label|default:result_value }}">{{ result_label|default:'label' }}</label>
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}" class="action-select">
<label class="usa-sr-only" for="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}">{{ result_label|default:'label' }}</label>
</td>
{% endwith %}
{% endwith %}

View file

@ -51,7 +51,7 @@
{% with attr_aria_describedby="domain_instructions domain_instructions2" %}
{# attr_validate / validate="domain" invokes code in get-gov.js #}
{% with www_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.requested_domain %}
{% endwith %}
{% endwith %}
@ -75,7 +75,7 @@
{% with attr_aria_describedby="alt_domain_instructions" %}
{# attr_validate / validate="domain" invokes code in get-gov.js #}
{# attr_auto_validate likewise triggers behavior in get-gov.js #}
{% with www_gov=True attr_validate="domain" attr_auto_validate=True %}
{% with append_gov=True attr_validate="domain" attr_auto_validate=True %}
{% for form in forms.1 %}
{% input_with_errors form.alternative_domain %}
{% endfor %}

View file

@ -23,6 +23,7 @@
{% elif domainapplication.status == 'in review' %} In Review
{% elif domainapplication.status == 'rejected' %} Rejected
{% elif domainapplication.status == 'submitted' %} Submitted
{% elif domainapplication.status == 'ineligible' %} Ineligible
{% else %}ERROR Please contact technical support/dev
{% endif %}
</p>
@ -83,6 +84,14 @@
{% include "includes/summary_item.html" with title='Current website for your organization' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
{% endif %}
{% if domainapplication.requested_domain %}
{% include "includes/summary_item.html" with title='.gov domain' value=domainapplication.requested_domain heading_level=heading_level %}
{% endif %}
{% if domainapplication.alternative_domains.all %}
{% include "includes/summary_item.html" with title='Alternative domains' value=domainapplication.alternative_domains.all list='true' heading_level=heading_level %}
{% endif %}
{% if domainapplication.purpose %}
{% include "includes/summary_item.html" with title='Purpose of your domain' value=domainapplication.purpose heading_level=heading_level %}
{% endif %}

View file

@ -149,12 +149,12 @@
<button type="button" class="usa-nav__close">
<img src="/public/img/usa-icons/close.svg" role="img" alt="Close" />
</button>
<ul class="usa-nav__primary usa-accordion">
<ul class="usa-nav__primary usa-accordion display-flex flex-align-center">
<li class="usa-nav__primary-item">
{% if user.is_authenticated %}
<span>{{ user.email }}</span>
</li>
<li class="usa-nav__primary-item display-flex flex-align-center">
<li class="usa-nav__primary-item display-flex flex-align-center margin-left-2">
<span class="text-base"> | </span>
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
</li>

View file

@ -1,7 +1,14 @@
{% extends 'admin/change_form.html' %}
{% load i18n static %}
{% block extrahead %}
{{ block.super }}
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
{% endblock %}
{% block field_sets %}
<div class="submit-row">
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
<input type="submit" value="Place hold" name="_place_client_hold">
<input type="submit" value="Get the email" name="_get_security_email">
<input type="submit" value="Set New Security Contact" name="_set_security_contact">

View file

@ -10,8 +10,7 @@
<h1>Authorizing official</h1>
<p>Your authorizing official is the person within your organization who can
authorize domain requests. This is generally the highest-ranking or
highest-elected official in your organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
{% include "includes/required_fields.html" %}

View file

@ -5,12 +5,14 @@
{% block content %}
<div class="grid-container">
<div class="grid-row">
<p class="font-body-md margin-top-0 margin-bottom-2
<div class="grid-row">
{% if not is_analyst_or_superuser or not analyst_action or analyst_action_location != domain.pk %}
<p class="font-body-md margin-top-0 margin-bottom-2
text-primary-darker text-semibold"
>
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
</p>
{% endif %}
</div>
<div class="grid-row grid-gap">
<div class="tablet:grid-col-3">
@ -20,15 +22,26 @@
<div class="tablet:grid-col-9">
<main id="main-content" class="grid-container">
<a href="{% url 'home' %}" class="breadcrumb__back">
{% if is_analyst_or_superuser and analyst_action == 'edit' and analyst_action_location == domain.pk %}
<div class="usa-alert usa-alert--warning margin-bottom-2">
<div class="usa-alert__body">
<h4 class="usa-alert__heading larger-font-sizing">Attention!</h4>
<p class="usa-alert__text ">
You are making changes to a registrants domain. When finished making changes, close this tab and inform the registrant of your updates.
</p>
</div>
</div>
{% else %}
<a href="{% url 'home' %}" class="breadcrumb__back">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
</svg>
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
Back to manage your domains
Back to manage your domains
</p>
</a>
{% endif %}
{# messages block is under the back breadcrumb link #}
{% if messages %}
{% for message in messages %}

View file

@ -7,7 +7,8 @@
<h1>Domain contact information</h1>
<p>If youd like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here wont affect your Login.gov account information.</p>
<p>If youd like us to use a different name, email, or phone number you can make those changes below. <strong>Updating your contact information here will update the contact information for all domains in your account.</strong> However, it wont affect your Login.gov account information.
</p>
{% include "includes/required_fields.html" %}

View file

@ -0,0 +1,26 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Your .gov domain request has been withdrawn.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST #: {{ application.id }}
STATUS: Withdrawn
YOU CAN EDIT YOUR WITHDRAWN REQUEST
The details of your withdrawn request are included below. You can edit and resubmit this application by logging into the registrar. <https://manage.get.gov/>.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -0,0 +1 @@
Your .gov domain request has been withdrawn

View file

@ -19,7 +19,7 @@
</p>
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
<h2>Registered domains</h2>
<h2>Domains</h2>
{% if domains %}
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your registered domains</caption>
@ -43,13 +43,13 @@
<td data-label="Status">{{ domain.application_status|title }}</td>
<td>
<a href="{% url "domain" pk=domain.pk %}">
<svg
class="usa-icon"
aria-hidden="true"
focusable="false"
role="img"
<svg
class="usa-icon"
aria-hidden="true"
focusable="false"
role="img"
width="24"
>
>
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
</svg>
Manage <span class="usa-sr-only">{{ domain.name }}</span>
@ -69,7 +69,7 @@
</section>
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
<h2>Active domain requests</h2>
<h2>Domain requests</h2>
{% if domain_applications %}
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your domain applications</caption>
@ -115,15 +115,18 @@
aria-live="polite"
></div>
{% else %}
<p>You don't have any active domain requests right now</p>
{% endif %}
<p>You don't have any active domain requests right now</p>
<p><a href="{% url 'application:' %}" class="usa-button">Start a new domain request</a></p>
{% endif %}
</section>
{# Note: Reimplement this after MVP.. #}
<!--
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
<h2>Archived domains</h2>
<p>You don't have any archived domains</p>
<p>You don't have any archived domains</p>
</section>
-->
<!-- Note: Uncomment below when this is being implemented post-MVP -->
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
@ -132,8 +135,8 @@
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
Export domains as csv
</a>
</section> -->
</section>
-->
</div>
{% else %} {# not user.is_authenticated #}

View file

@ -55,15 +55,13 @@ error messages, if necessary.
</div>
{% endif %}
{% if www_gov %}
{% if append_gov %}
<div class="display-flex flex-align-center">
<span class="padding-top-05 padding-right-2px">www.</span>
{% endif %}
{# this is the input field, itself #}
{% include widget.template_name %}
{% if www_gov %}
{% if append_gov %}
<span class="padding-top-05 padding-left-2px">.gov </span>
</div>
{% endif %}

View file

@ -40,3 +40,11 @@ def slice_after(value, substring):
result = value[index + len(substring) :]
return result
return value
@register.filter
def contains_checkbox(html_list):
for html_string in html_list:
if re.search(r'<input[^>]*type="checkbox"', html_string):
return True
return False

View file

@ -518,3 +518,11 @@ def multiple_unalphabetical_domain_objects(
application = mock.create_full_dummy_domain_object(domain_type, object_name)
applications.append(application)
return applications
def generic_domain_object(domain_type, object_name):
"""Returns a generic domain object of
domain_type 'application', 'information', or 'invitation'"""
mock = AuditedAdminMockData()
application = mock.create_full_dummy_domain_object(domain_type, object_name)
return application

View file

@ -1,7 +1,9 @@
from django.test import TestCase, RequestFactory, Client
from django.contrib.admin.sites import AdminSite
from django.urls import reverse
from registrar.admin import (
DomainAdmin,
DomainApplicationAdmin,
ListHeaderAdmin,
MyUserAdmin,
@ -12,16 +14,19 @@ from registrar.models import (
DomainInformation,
User,
DomainInvitation,
Domain,
)
from .common import (
completed_application,
generic_domain_object,
mock_user,
create_superuser,
create_user,
multiple_unalphabetical_domain_objects,
)
from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model
from unittest.mock import patch
from django.conf import settings
from unittest.mock import MagicMock
@ -35,6 +40,11 @@ class TestDomainApplicationAdmin(TestCase):
def setUp(self):
self.site = AdminSite()
self.factory = RequestFactory()
self.admin = DomainApplicationAdmin(
model=DomainApplication, admin_site=self.site
)
self.superuser = create_superuser()
self.staffuser = create_user()
@boto3_mocking.patching
def test_save_model_sends_submitted_email(self):
@ -54,14 +64,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.SUBMITTED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -100,14 +107,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.IN_REVIEW
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -146,14 +150,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.APPROVED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -187,14 +188,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.APPROVED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Test that approved domain exists and equals requested domain
self.assertEqual(
@ -219,14 +217,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.ACTION_NEEDED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -268,14 +263,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.REJECTED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -296,6 +288,155 @@ class TestDomainApplicationAdmin(TestCase):
# Perform assertions on the mock call itself
mock_client_instance.send_email.assert_called_once()
def test_save_model_sets_restricted_status_on_user(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.IN_REVIEW)
# Create a mock request
request = self.factory.post(
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Modify the application's property
application.status = DomainApplication.INELIGIBLE
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Test that approved domain exists and equals requested domain
self.assertEqual(application.creator.status, "restricted")
def test_readonly_when_restricted_creator(self):
application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
request = self.factory.get("/")
request.user = self.superuser
readonly_fields = self.admin.get_readonly_fields(request, application)
expected_fields = [
"id",
"created_at",
"updated_at",
"status",
"creator",
"investigator",
"organization_type",
"federally_recognized_tribe",
"state_recognized_tribe",
"tribe_name",
"federal_agency",
"federal_type",
"is_election_board",
"organization_name",
"address_line1",
"address_line2",
"city",
"state_territory",
"zipcode",
"urbanization",
"type_of_work",
"more_organization_information",
"authorizing_official",
"approved_domain",
"requested_domain",
"submitter",
"purpose",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
"current_websites",
"other_contacts",
"alternative_domains",
]
self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_analyst(self):
request = self.factory.get("/") # Use the correct method and path
request.user = self.staffuser
readonly_fields = self.admin.get_readonly_fields(request)
expected_fields = [
"creator",
"type_of_work",
"more_organization_information",
"address_line1",
"address_line2",
"zipcode",
"requested_domain",
"alternative_domains",
"purpose",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
]
self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_superuser(self):
request = self.factory.get("/") # Use the correct method and path
request.user = self.superuser
readonly_fields = self.admin.get_readonly_fields(request)
expected_fields = []
self.assertEqual(readonly_fields, expected_fields)
def test_saving_when_restricted_creator(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
# Create a request object with a superuser
request = self.factory.get("/")
request.user = self.superuser
with patch("django.contrib.messages.error") as mock_error:
# Simulate saving the model
self.admin.save_model(request, application, None, False)
# Assert that the error message was called with the correct argument
mock_error.assert_called_once_with(
request,
"This action is not permitted for applications "
+ "with a restricted creator.",
)
# Assert that the status has not changed
self.assertEqual(application.status, DomainApplication.IN_REVIEW)
def test_change_view_with_restricted_creator(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
with patch("django.contrib.messages.warning") as mock_warning:
# Create a request object with a superuser
request = self.factory.get(
"/admin/your_app/domainapplication/{}/change/".format(application.pk)
)
request.user = self.superuser
self.admin.display_restricted_warning(request, application)
# Assert that the error message was called with the correct argument
mock_warning.assert_called_once_with(
request,
"Cannot edit an application with a restricted creator.",
)
def tearDown(self):
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
@ -375,7 +516,6 @@ class ListHeaderAdminTest(TestCase):
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
User.objects.all().delete()
self.superuser.delete()
class MyUserAdminTest(TestCase):
@ -636,3 +776,129 @@ class AuditedAdminTest(TestCase):
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
DomainInvitation.objects.all().delete()
class DomainSessionVariableTest(TestCase):
"""Test cases for session variables in Django Admin"""
def setUp(self):
self.factory = RequestFactory()
self.admin = DomainAdmin(Domain, None)
self.client = Client(HTTP_HOST="localhost:8080")
def test_session_vars_set_correctly(self):
"""Checks if session variables are being set correctly"""
p = "adminpass"
self.client.login(username="superuser", password=p)
dummy_domain_information = generic_domain_object("information", "session")
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
self.populate_session_values(request, dummy_domain_information.domain)
self.assertEqual(request.session["analyst_action"], "edit")
self.assertEqual(
request.session["analyst_action_location"],
dummy_domain_information.domain.pk,
)
def test_session_vars_set_correctly_hardcoded_domain(self):
"""Checks if session variables are being set correctly"""
p = "adminpass"
self.client.login(username="superuser", password=p)
dummy_domain_information: Domain = generic_domain_object(
"information", "session"
)
dummy_domain_information.domain.pk = 1
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
self.populate_session_values(request, dummy_domain_information.domain)
self.assertEqual(request.session["analyst_action"], "edit")
self.assertEqual(request.session["analyst_action_location"], 1)
def test_session_variables_reset_correctly(self):
"""Checks if incorrect session variables get overridden"""
p = "adminpass"
self.client.login(username="superuser", password=p)
dummy_domain_information = generic_domain_object("information", "session")
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
self.populate_session_values(
request, dummy_domain_information.domain, preload_bad_data=True
)
self.assertEqual(request.session["analyst_action"], "edit")
self.assertEqual(
request.session["analyst_action_location"],
dummy_domain_information.domain.pk,
)
def test_session_variables_retain_information(self):
"""Checks to see if session variables retain old information"""
p = "adminpass"
self.client.login(username="superuser", password=p)
dummy_domain_information_list = multiple_unalphabetical_domain_objects(
"information"
)
for item in dummy_domain_information_list:
request = self.get_factory_post_edit_domain(item.domain.pk)
self.populate_session_values(request, item.domain)
self.assertEqual(request.session["analyst_action"], "edit")
self.assertEqual(request.session["analyst_action_location"], item.domain.pk)
def test_session_variables_concurrent_requests(self):
"""Simulates two requests at once"""
p = "adminpass"
self.client.login(username="superuser", password=p)
info_first = generic_domain_object("information", "session")
info_second = generic_domain_object("information", "session2")
request_first = self.get_factory_post_edit_domain(info_first.domain.pk)
request_second = self.get_factory_post_edit_domain(info_second.domain.pk)
self.populate_session_values(request_first, info_first.domain, True)
self.populate_session_values(request_second, info_second.domain, True)
# Check if anything got nulled out
self.assertNotEqual(request_first.session["analyst_action"], None)
self.assertNotEqual(request_second.session["analyst_action"], None)
self.assertNotEqual(request_first.session["analyst_action_location"], None)
self.assertNotEqual(request_second.session["analyst_action_location"], None)
# Check if they are both the same action 'type'
self.assertEqual(request_first.session["analyst_action"], "edit")
self.assertEqual(request_second.session["analyst_action"], "edit")
# Check their locations, and ensure they aren't the same across both
self.assertNotEqual(
request_first.session["analyst_action_location"],
request_second.session["analyst_action_location"],
)
def populate_session_values(self, request, domain_object, preload_bad_data=False):
"""Boilerplate for creating mock sessions"""
request.user = self.client
request.session = SessionStore()
request.session.create()
if preload_bad_data:
request.session["analyst_action"] = "invalid"
request.session["analyst_action_location"] = "bad location"
self.admin.response_change(request, domain_object)
def get_factory_post_edit_domain(self, primary_key):
"""Posts to registrar domain change
with the edit domain button 'clicked',
then returns the factory object"""
return self.factory.post(
reverse("admin:registrar_domain_change", args=(primary_key,)),
{"_edit_domain": "true"},
follow=True,
)

View file

@ -171,6 +171,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_ineligible_submitted(self):
"""Create an application with status ineligible and call submit
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_started_in_review(self):
"""Create an application with status started and call in_review
against transition rules"""
@ -225,6 +234,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_ineligible_in_review(self):
"""Create an application with status ineligible and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_started_action_needed(self):
"""Create an application with status started and call action_needed
against transition rules"""
@ -270,6 +288,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_ineligible_action_needed(self):
"""Create an application with status ineligible and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_started_approved(self):
"""Create an application with status started and call approve
against transition rules"""
@ -351,6 +378,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_ineligible_withdrawn(self):
"""Create an application with status ineligible and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_started_rejected(self):
"""Create an application with status started and call reject
against transition rules"""
@ -396,6 +432,69 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_ineligible_rejected(self):
"""Create an application with status ineligible and call reject
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_started_ineligible(self):
"""Create an application with status started and call reject
against transition rules"""
application = completed_application(status=DomainApplication.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_submitted_ineligible(self):
"""Create an application with status submitted and call reject
against transition rules"""
application = completed_application(status=DomainApplication.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_action_needed_ineligible(self):
"""Create an application with status action needed and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_withdrawn_ineligible(self):
"""Create an application with status withdrawn and call reject
against transition rules"""
application = completed_application(status=DomainApplication.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_rejected_ineligible(self):
"""Create an application with status rejected and call reject
against transition rules"""
application = completed_application(status=DomainApplication.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_ineligible_ineligible(self):
"""Create an application with status ineligible and call reject
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
class TestPermissions(TestCase):

View file

@ -8,6 +8,7 @@ from registrar.templatetags.custom_filters import (
extract_a_text,
find_index,
slice_after,
contains_checkbox,
)
@ -83,3 +84,21 @@ class CustomFiltersTestCase(TestCase):
self.assertEqual(
result, value
) # Should return the original value if substring not found
def test_contains_checkbox_with_checkbox(self):
# Test the filter when HTML list contains a checkbox
html_list = [
'<input type="checkbox" name="_selected_action">',
"<div>Some other HTML content</div>",
]
result = contains_checkbox(html_list)
self.assertTrue(result) # Expecting True
def test_contains_checkbox_without_checkbox(self):
# Test the filter when HTML list does not contain a checkbox
html_list = [
"<div>Some HTML content without checkbox</div>",
"<p>More HTML content</p>",
]
result = contains_checkbox(html_list)
self.assertFalse(result) # Expecting False

View file

@ -101,6 +101,18 @@ class LoggedInTests(TestWithUser):
"What kind of U.S.-based government organization do you represent?",
)
def test_domain_application_form_with_ineligible_user(self):
"""Application form not accessible for an ineligible user.
This test should be solid enough since all application wizard
views share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
with less_console_noise():
response = self.client.get("/register/", follow=True)
print(response.status_code)
self.assertEqual(response.status_code, 403)
class DomainApplicationTests(TestWithUser, WebTest):
@ -1423,6 +1435,18 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
success_page, "The security email for this domain have been updated"
)
def test_domain_overview_blocked_for_ineligible_user(self):
"""We could easily duplicate this test for all domain management
views, but a single url test should be solid enough since all domain
management pages share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov")
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
class TestApplicationStatus(TestWithUser, WebTest):
def setUp(self):
@ -1437,6 +1461,28 @@ class TestApplicationStatus(TestWithUser, WebTest):
)
application.save()
home_page = self.app.get("/")
self.assertContains(home_page, "city.gov")
# click the "Manage" link
detail_page = home_page.click("Manage")
self.assertContains(detail_page, "city.gov")
self.assertContains(detail_page, "city1.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
def test_application_status_with_ineligible_user(self):
"""Checking application status page whith a blocked user.
The user should still have access to view."""
self.user.status = "ineligible"
self.user.save()
application = completed_application(
status=DomainApplication.SUBMITTED, user=self.user
)
application.save()
home_page = self.app.get("/")
self.assertContains(home_page, "city.gov")
# click the "Manage" link
@ -1459,6 +1505,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
# click the "Manage" link
detail_page = home_page.click("Manage")
self.assertContains(detail_page, "city.gov")
self.assertContains(detail_page, "city1.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
@ -1500,3 +1547,18 @@ class TestApplicationStatus(TestWithUser, WebTest):
reverse(url_name, kwargs={"pk": application.pk})
)
self.assertEqual(page.status_code, 403)
def test_approved_application_not_in_active_requests(self):
"""An approved application is not shown in the Active
Requests table on home.html."""
application = completed_application(
status=DomainApplication.APPROVED, user=self.user
)
application.save()
home_page = self.app.get("/")
# This works in our test environment because creating
# an approved application here does not generate a
# domain object, so we do not expect to see 'city.gov'
# in either the Domains or Requests tables.
self.assertNotContains(home_page, "city.gov")

View file

@ -12,7 +12,7 @@ from registrar.models import DomainApplication
from registrar.utility import StrEnum
from registrar.views.utility import StepsHelper
from .utility import DomainApplicationPermissionView
from .utility import DomainApplicationPermissionView, ApplicationWizardPermissionView
logger = logging.getLogger(__name__)
@ -43,7 +43,7 @@ class Step(StrEnum):
REVIEW = "review"
class ApplicationWizard(TemplateView):
class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
"""
A common set of methods and configuration.
@ -60,6 +60,8 @@ class ApplicationWizard(TemplateView):
although not without consulting the base implementation, first.
"""
template_name = ""
# uniquely namespace the wizard in urls.py
# (this is not seen _in_ urls, only for Django's internal naming)
# NB: this is included here for reference. Do not change it without

View file

@ -79,6 +79,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
messages.success(
self.request, "The organization name and mailing address has been updated."
)
# superclass has the redirect
return super().form_valid(form)
@ -121,6 +122,7 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
messages.success(
self.request, "The authorizing official for this domain has been updated."
)
# superclass has the redirect
return super().form_valid(form)
@ -191,6 +193,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
messages.success(
self.request, "The name servers for this domain have been updated."
)
# superclass has the redirect
return super().form_valid(formset)
@ -231,6 +234,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
messages.success(
self.request, "Your contact information for this domain has been updated."
)
# superclass has the redirect
return super().form_valid(form)
@ -278,6 +282,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
messages.success(
self.request, "The security email for this domain have been updated."
)
# superclass has the redirect
return redirect(self.get_success_url())
@ -353,6 +358,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
messages.success(
self.request, f"Invited {email_address} to this domain."
)
return redirect(self.get_success_url())
def form_valid(self, form):
@ -374,6 +380,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
pass
messages.success(self.request, f"Added user {requested_email}.")
return redirect(self.get_success_url())

View file

@ -9,7 +9,10 @@ def index(request):
context = {}
if request.user.is_authenticated:
applications = DomainApplication.objects.filter(creator=request.user)
context["domain_applications"] = applications
# Let's exclude the approved applications since our
# domain_applications context will be used to populate
# the active applications table
context["domain_applications"] = applications.exclude(status="approved")
domains = request.user.permissions.values(
"role",

View file

@ -5,4 +5,5 @@ from .permission_views import (
DomainPermissionView,
DomainApplicationPermissionView,
DomainInvitationPermissionDeleteView,
ApplicationWizardPermissionView,
)

View file

@ -2,7 +2,16 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from registrar.models import UserDomainRole, DomainApplication, DomainInvitation
from registrar.models import (
DomainApplication,
DomainInvitation,
DomainInformation,
UserDomainRole,
)
import logging
logger = logging.getLogger(__name__)
class PermissionsLoginMixin(PermissionRequiredMixin):
@ -25,27 +34,80 @@ class DomainPermission(PermissionsLoginMixin):
up from the domain's primary key in self.kwargs["pk"]
"""
# ticket 806
# if self.request.user is staff or admin and
# domain.application__status = 'approved' or 'rejected' or 'action needed'
# return True
if not self.request.user.is_authenticated:
return False
if self.request.user.is_restricted():
return False
pk = self.kwargs["pk"]
# If pk is none then something went very wrong...
if pk is None:
raise ValueError("Primary key is None")
if self.can_access_other_user_domains(pk):
return True
# user needs to have a role on the domain
if not UserDomainRole.objects.filter(
user=self.request.user, domain__id=self.kwargs["pk"]
user=self.request.user, domain__id=pk
).exists():
return False
# ticket 796
# if domain.application__status != 'approved'
# return false
# if we need to check more about the nature of role, do it here.
return True
def can_access_other_user_domains(self, pk):
"""Checks to see if an authorized user (staff or superuser)
can access a domain that they did not create or was invited to.
"""
# Check if the user is permissioned...
user_is_analyst_or_superuser = (
self.request.user.is_staff or self.request.user.is_superuser
)
if not user_is_analyst_or_superuser:
return False
# Check if the user is attempting a valid edit action.
# In other words, if the analyst/admin did not click
# the 'Manage Domain' button in /admin,
# then they cannot access this page.
session = self.request.session
can_do_action = (
"analyst_action" in session
and "analyst_action_location" in session
and session["analyst_action_location"] == pk
)
if not can_do_action:
return False
# Analysts may manage domains, when they are in these statuses:
valid_domain_statuses = [
DomainApplication.APPROVED,
DomainApplication.IN_REVIEW,
DomainApplication.REJECTED,
DomainApplication.ACTION_NEEDED,
# Edge case - some domains do not have
# a status or DomainInformation... aka a status of 'None'.
# It is necessary to access those to correct errors.
None,
]
requested_domain = None
if DomainInformation.objects.filter(id=pk).exists():
requested_domain = DomainInformation.objects.get(id=pk)
if requested_domain.domain_application.status not in valid_domain_statuses:
return False
# Valid session keys exist,
# the user is permissioned,
# and it is in a valid status
return True
class DomainApplicationPermission(PermissionsLoginMixin):
@ -71,6 +133,23 @@ class DomainApplicationPermission(PermissionsLoginMixin):
return True
class ApplicationWizardPermission(PermissionsLoginMixin):
"""Does the logged-in user have permission to start or edit an application?"""
def has_permission(self):
"""Check if this user has permission to start or edit an application.
The user is in self.request.user
"""
# The user has an ineligible flag
if self.request.user.is_restricted():
return False
return True
class DomainInvitationPermission(PermissionsLoginMixin):
"""Does the logged-in user have access to this domain invitation?

View file

@ -2,15 +2,18 @@
import abc # abstract base class
from django.views.generic import DetailView, DeleteView
from django.views.generic import DetailView, DeleteView, TemplateView
from registrar.models import Domain, DomainApplication, DomainInvitation
from .mixins import (
DomainPermission,
DomainApplicationPermission,
DomainInvitationPermission,
ApplicationWizardPermission,
)
import logging
logger = logging.getLogger(__name__)
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
@ -26,6 +29,22 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
# variable name in template context for the model object
context_object_name = "domain"
# Adds context information for user permissions
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
context["is_analyst_or_superuser"] = user.is_staff or user.is_superuser
# Stored in a variable for the linter
action = "analyst_action"
action_location = "analyst_action_location"
# Flag to see if an analyst is attempting to make edits
if action in self.request.session:
context[action] = self.request.session[action]
if action_location in self.request.session:
context[action_location] = self.request.session[action_location]
return context
# Abstract property enforces NotImplementedError on an attribute.
@property
@abc.abstractmethod
@ -53,6 +72,23 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
raise NotImplementedError
class ApplicationWizardPermissionView(
ApplicationWizardPermission, TemplateView, abc.ABC
):
"""Abstract base view for the application form that enforces permissions
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
# Abstract property enforces NotImplementedError on an attribute.
@property
@abc.abstractmethod
def template_name(self):
raise NotImplementedError
class DomainInvitationPermissionDeleteView(
DomainInvitationPermission, DeleteView, abc.ABC
):

View file

@ -31,7 +31,7 @@
10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js
# get-gov.js contains suspicious word "from" as in `Array.from()`
10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js
# Ignore wording of "TODO"
# Ignores suspicious word "TODO"
10027 OUTOFSCOPE http://app:8080.*$
10028 FAIL (Open Redirect - Passive/beta)
10029 FAIL (Cookie Poisoning - Passive/beta)