Fix merge conflicts

This commit is contained in:
Rebecca Hsieh 2023-10-11 09:19:14 -07:00
commit 7fec0462f5
No known key found for this signature in database
GPG key ID: 644527A2F375A379
32 changed files with 826 additions and 377 deletions

View file

@ -1,34 +1,35 @@
name: Issue
description: Capture uncategorized work or content
description: Describe an idea, feature, content, or non-bug finding
body:
- type: markdown
id: help
id: title-help
attributes:
value: |
> **Note**
> GitHub Issues use [GitHub Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting.
> Titles should be short, descriptive, and compelling.
- type: textarea
id: issue
id: issue-description
attributes:
label: Issue Description
label: Issue description and context
description: |
Describe the issue you are adding or content you are suggesting.
Share any next steps that should be taken our outcomes that would be beneficial.
Describe the issue so that someone who wasn't present for its discovery can understand the problem and why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Share desired outcomes or potential next steps. Images or links to other content/context (like documents or Slack discussions) are welcome.
validations:
required: true
- type: textarea
id: additional-context
id: acceptance-criteria
attributes:
label: Additional Context (optional)
description: "Include additional references (screenshots, design links, documentation, etc.) that are relevant"
label: Acceptance criteria
description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate."
placeholder: "- [ ] The button does the thing."
- type: textarea
id: issue-links
id: links-to-other-issues
attributes:
label: Issue Links
label: Links to other issues
description: |
What other issues does this story relate to and how?
Example:
- 🚧 Blocked by: #123
- 🔄 Relates to: #234
Add the issue #number of other issues this relates to and how (e.g., 🚧 Blocks, ⛔️ Is blocked by, 🔄 Relates to).
placeholder: 🔄 Relates to...
- type: markdown
id: note
attributes:
value: |
> We may edit this issue's text to document our understanding and clarify the product work.

View file

@ -80,7 +80,7 @@ The endpoint /admin can be used to view and manage site content, including but n
1. Login via login.gov
2. Go to the home page and make sure you can see the part where you can submit an application
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4
4. in src/registrar/fixtures.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below:
4. in src/registrar/fixtures_users.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below:
```
ADMINS = [
@ -102,7 +102,7 @@ Analysts are a variant of the admin role with limited permissions. The process f
1. Login via login.gov (if you already exist as an admin, you will need to create a separate login.gov account for this: i.e. first.last+1@email.com)
2. Go to the home page and make sure you can see the part where you can submit an application
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 (this will be a different UUID than the one obtained from creating an admin)
4. in src/registrar/fixtures.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below:
4. in src/registrar/fixtures_users.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below:
```
STAFF = [
@ -145,7 +145,7 @@ You can change the logging verbosity, if needed. Do a web search for "django log
## Mock data
There is a `post_migrate` signal in [signals.py](../../src/registrar/signals.py) that will load the fixtures from [fixtures.py](../../src/registrar/fixtures.py), giving you some test data to play with while developing.
There is a `post_migrate` signal in [signals.py](../../src/registrar/signals.py) that will load the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_applications.py](../../src/registrar/fixtures_applications.py), giving you some test data to play with while developing.
See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures.

View file

@ -48,3 +48,7 @@ future, as we add additional roles that our product vision calls for
(read-only? editing only some information?), we need to add conditional
behavior in the permission mixin, or additional mixins that more clearly
express what is allowed for those new roles.
# Admin User Permissions
Refer to [Django Admin Roles](../django-admin/roles.md)

View file

@ -1,21 +1,19 @@
# Django admin user roles
Roles other than superuser should be defined in authentication and authorization groups in django admin
For our MVP, we create and maintain 2 admin roles:
Full access and CISA analyst. Both have the role `staff`.
Permissions on these roles are set through groups:
`full_access_group` and `cisa_analysts_group`. These
groups and the methods to create them are defined in
our `user_group` model and run in a migration.
## Superuser
For more details, refer to the [user group model](../../src/registrar/models/user_group.py).
Full access
## Editing group permissions through code
## CISA analyst
We can edit and deploy new group permissions by:
### Basic permission level
Staff
### Additional group permissions
auditlog | log entry | can view log entry
registrar | contact | can view contact
registrar | domain application | can change domain application
registrar | domain | can view domain
registrar | user | can view user
1. editing `user_group` then:
2. Duplicating migration `0036_create_groups_01`
and running migrations (append the name with a version number
to help django detect the migration eg 0037_create_groups_02)

View file

@ -89,7 +89,8 @@ command in the running Cloud.gov container. For example, to run our Django
admin command that loads test fixture data:
```
cf run-task getgov-{environment} --command "./manage.py load" --name fixtures
cf run-task getgov-{environment} --command "./manage.py load" --name fixtures--users
cf run-task getgov-{environment} --command "./manage.py load" --name fixtures--applications
```
However, this task runs asynchronously in the background without any command

View file

@ -3,6 +3,7 @@ from django import forms
from django_fsm import get_available_FIELD_transitions
from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.http.response import HttpResponseRedirect
from django.urls import reverse
@ -137,8 +138,9 @@ class MyUserAdmin(BaseUserAdmin):
"email",
"first_name",
"last_name",
"is_staff",
"is_superuser",
# Group is a custom property defined within this file,
# rather than in a model like the other properties
"group",
"status",
)
@ -163,6 +165,9 @@ class MyUserAdmin(BaseUserAdmin):
("Important dates", {"fields": ("last_login", "date_joined")}),
)
# Hide Username (uuid), Groups and Permissions
# Q: Now that we're using Groups and Permissions,
# do we expose those to analysts to view?
analyst_fieldsets = (
(
None,
@ -174,14 +179,23 @@ class MyUserAdmin(BaseUserAdmin):
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
analyst_list_display = [
"email",
"first_name",
"last_name",
"group",
"status",
]
# NOT all fields are readonly for admin, otherwise we would have
# set this at the permissions level. The exception is 'status'
analyst_readonly_fields = [
"password",
"Personal Info",
@ -190,43 +204,56 @@ class MyUserAdmin(BaseUserAdmin):
"email",
"Permissions",
"is_active",
"is_staff",
"is_superuser",
"groups",
"Important dates",
"last_login",
"date_joined",
]
def get_list_display(self, request):
if not request.user.is_superuser:
# Customize the list display for staff users
return (
"email",
"first_name",
"last_name",
"is_staff",
"is_superuser",
"status",
list_filter = (
"is_active",
"groups",
)
# Use the default list display for non-staff users
# Let's define First group
# (which should in theory be the ONLY group)
def group(self, obj):
if obj.groups.filter(name="full_access_group").exists():
return "Full access"
elif obj.groups.filter(name="cisa_analysts_group").exists():
return "Analyst"
return ""
def get_list_display(self, request):
# The full_access_permission perm will load onto the full_access_group
# which is equivalent to superuser. The other group we use to manage
# perms is cisa_analysts_group. cisa_analysts_group will never contain
# full_access_permission
if request.user.has_perm("registrar.full_access_permission"):
# Use the default list display for all access users
return super().get_list_display(request)
def get_fieldsets(self, request, obj=None):
if not request.user.is_superuser:
# If the user doesn't have permission to change the model,
# show a read-only fieldset
return self.analyst_fieldsets
# Customize the list display for analysts
return self.analyst_list_display
# If the user has permission to change the model, show all fields
def get_fieldsets(self, request, obj=None):
if request.user.has_perm("registrar.full_access_permission"):
# Show all fields for all access users
return super().get_fieldsets(request, obj)
elif request.user.has_perm("registrar.analyst_access_permission"):
# show analyst_fieldsets for analysts
return self.analyst_fieldsets
else:
# any admin user should belong to either full_access_group
# or cisa_analyst_group
return []
def get_readonly_fields(self, request, obj=None):
if request.user.is_superuser:
return () # No read-only fields for superusers
elif request.user.is_staff:
return self.analyst_readonly_fields # Read-only fields for staff
return () # No read-only fields for other users
if request.user.has_perm("registrar.full_access_permission"):
return () # No read-only fields for all access users
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
return self.analyst_readonly_fields
class HostIPInline(admin.StackedInline):
@ -405,11 +432,12 @@ class DomainInformationAdmin(ListHeaderAdmin):
readonly_fields = list(self.readonly_fields)
if request.user.is_superuser:
if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields
else:
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
return readonly_fields # Read-only fields for analysts
class DomainApplicationAdminForm(forms.ModelForm):
@ -623,9 +651,10 @@ class DomainApplicationAdmin(ListHeaderAdmin):
["current_websites", "other_contacts", "alternative_domains"]
)
if request.user.is_superuser:
if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields
else:
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
@ -784,7 +813,8 @@ class DomainAdmin(ListHeaderAdmin):
else:
self.message_user(
request,
("Domain statuses are %s" ". Thanks!") % statuses,
f"The registry statuses are {statuses}. "
"These statuses are from the provider of the .gov registry.",
)
return HttpResponseRedirect(".")
@ -870,7 +900,9 @@ class DomainAdmin(ListHeaderAdmin):
# 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:
if request.user.has_perm(
"registrar.full_access_permission"
) or request.user.has_perm("registrar.analyst_access_permission"):
return True
return super().has_change_permission(request, obj)
@ -885,6 +917,10 @@ class DraftDomainAdmin(ListHeaderAdmin):
admin.site.unregister(LogEntry) # Unregister the default registration
admin.site.register(LogEntry, CustomLogEntryAdmin)
admin.site.register(models.User, MyUserAdmin)
# Unregister the built-in Group model
admin.site.unregister(Group)
# Register UserGroup
admin.site.register(models.UserGroup)
admin.site.register(models.UserDomainRole, UserDomainRoleAdmin)
admin.site.register(models.Contact, ContactAdmin)
admin.site.register(models.DomainInvitation, DomainInvitationAdmin)

View file

@ -10,255 +10,10 @@ from registrar.models import (
Website,
)
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
fake = Faker()
logger = logging.getLogger(__name__)
class UserFixture:
"""
Load users into the database.
Make sure this class' `load` method is called from `handle`
in management/commands/load.py, then use `./manage.py load`
to run this code.
"""
ADMINS = [
{
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"first_name": "Rachid",
"last_name": "Mrad",
},
{
"username": "eb2214cd-fc0c-48c0-9dbd-bc4cd6820c74",
"first_name": "Alysia",
"last_name": "Broddrick",
},
{
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
"first_name": "Katherine",
"last_name": "Osos",
},
{
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
"first_name": "Gaby",
"last_name": "DiSarli",
},
{
"username": "83c2b6dd-20a2-4cac-bb40-e22a72d2955c",
"first_name": "Cameron",
"last_name": "Dixon",
},
{
"username": "0353607a-cbba-47d2-98d7-e83dcd5b90ea",
"first_name": "Ryan",
"last_name": "Brooks",
},
{
"username": "30001ee7-0467-4df2-8db2-786e79606060",
"first_name": "Zander",
"last_name": "Adkinson",
},
{
"username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
"first_name": "Paul",
"last_name": "Kuykendall",
},
{
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
"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",
},
{
"username": "24840450-bf47-4d89-8aa9-c612fe68f9da",
"first_name": "Erin",
"last_name": "Song",
},
{
"username": "e0ea8b94-6e53-4430-814a-849a7ca45f21",
"first_name": "Kristina",
"last_name": "Yin",
},
]
STAFF = [
{
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
"first_name": "Rachid-Analyst",
"last_name": "Mrad-Analyst",
"email": "rachid.mrad@gmail.com",
},
{
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
"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",
"last_name": "Adkinson-Analyst",
},
{
"username": "57ab5847-7789-49fe-a2f9-21d38076d699",
"first_name": "Paul-Analyst",
"last_name": "Kuykendall-Analyst",
},
{
"username": "e474e7a9-71ca-449d-833c-8a6e094dd117",
"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-Analyst",
"email": "nicolle.leclair@ecstech.com",
},
{
"username": "378d0bc4-d5a7-461b-bd84-3ae6f6864af9",
"first_name": "Erin-Analyst",
"last_name": "Song-Analyst",
"email": "erin.song+1@gsa.gov",
},
{
"username": "9a98e4c9-9409-479d-964e-4aec7799107f",
"first_name": "Kristina-Analyst",
"last_name": "Yin-Analyst",
"email": "kristina.yin+1@gsa.gov",
},
]
STAFF_PERMISSIONS = [
{
"app_label": "auditlog",
"model": "logentry",
"permissions": ["view_logentry"],
},
{"app_label": "registrar", "model": "contact", "permissions": ["view_contact"]},
{
"app_label": "registrar",
"model": "domaininformation",
"permissions": ["change_domaininformation"],
},
{
"app_label": "registrar",
"model": "domainapplication",
"permissions": ["change_domainapplication"],
},
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]},
{
"app_label": "registrar",
"model": "draftdomain",
"permissions": ["change_draftdomain"],
},
{"app_label": "registrar", "model": "user", "permissions": ["change_user"]},
]
@classmethod
def load(cls):
logger.info("Going to load %s superusers" % str(len(cls.ADMINS)))
for admin in cls.ADMINS:
try:
user, _ = User.objects.get_or_create(
username=admin["username"],
)
user.is_superuser = True
user.first_name = admin["first_name"]
user.last_name = admin["last_name"]
if "email" in admin.keys():
user.email = admin["email"]
user.is_staff = True
user.is_active = True
user.save()
logger.debug("User object created for %s" % admin["first_name"])
except Exception as e:
logger.warning(e)
logger.info("All superusers loaded.")
logger.info("Going to load %s CISA analysts (staff)" % str(len(cls.STAFF)))
for staff in cls.STAFF:
try:
user, _ = User.objects.get_or_create(
username=staff["username"],
)
user.is_superuser = False
user.first_name = staff["first_name"]
user.last_name = staff["last_name"]
if "email" in admin.keys():
user.email = admin["email"]
user.is_staff = True
user.is_active = True
for permission in cls.STAFF_PERMISSIONS:
app_label = permission["app_label"]
model_name = permission["model"]
permissions = permission["permissions"]
# Retrieve the content type for the app and model
content_type = ContentType.objects.get(
app_label=app_label, model=model_name
)
# Retrieve the permissions based on their codenames
permissions = Permission.objects.filter(
content_type=content_type, codename__in=permissions
)
# Assign the permissions to the user
user.user_permissions.add(*permissions)
# Convert the permissions QuerySet to a list of codenames
permission_list = list(
permissions.values_list("codename", flat=True)
)
logger.debug(
app_label
+ " | "
+ model_name
+ " | "
+ ", ".join(permission_list)
+ " added for user "
+ staff["first_name"]
)
user.save()
logger.debug("User object created for %s" % staff["first_name"])
except Exception as e:
logger.warning(e)
logger.info("All CISA analysts (staff) loaded.")
class DomainApplicationFixture:
"""
Load domain applications into the database.

View file

@ -0,0 +1,177 @@
import logging
from faker import Faker
from registrar.models import (
User,
UserGroup,
)
fake = Faker()
logger = logging.getLogger(__name__)
class UserFixture:
"""
Load users into the database.
Make sure this class' `load` method is called from `handle`
in management/commands/load.py, then use `./manage.py load`
to run this code.
"""
ADMINS = [
{
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"first_name": "Rachid",
"last_name": "Mrad",
},
{
"username": "eb2214cd-fc0c-48c0-9dbd-bc4cd6820c74",
"first_name": "Alysia",
"last_name": "Broddrick",
},
{
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
"first_name": "Katherine",
"last_name": "Osos",
},
{
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
"first_name": "Gaby",
"last_name": "DiSarli",
},
{
"username": "83c2b6dd-20a2-4cac-bb40-e22a72d2955c",
"first_name": "Cameron",
"last_name": "Dixon",
},
{
"username": "0353607a-cbba-47d2-98d7-e83dcd5b90ea",
"first_name": "Ryan",
"last_name": "Brooks",
},
{
"username": "30001ee7-0467-4df2-8db2-786e79606060",
"first_name": "Zander",
"last_name": "Adkinson",
},
{
"username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
"first_name": "Paul",
"last_name": "Kuykendall",
},
{
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
"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",
},
{
"username": "24840450-bf47-4d89-8aa9-c612fe68f9da",
"first_name": "Erin",
"last_name": "Song",
},
{
"username": "e0ea8b94-6e53-4430-814a-849a7ca45f21",
"first_name": "Kristina",
"last_name": "Yin",
},
]
STAFF = [
{
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
"first_name": "Rachid-Analyst",
"last_name": "Mrad-Analyst",
"email": "rachid.mrad@gmail.com",
},
{
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
"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",
"last_name": "Adkinson-Analyst",
},
{
"username": "57ab5847-7789-49fe-a2f9-21d38076d699",
"first_name": "Paul-Analyst",
"last_name": "Kuykendall-Analyst",
},
{
"username": "e474e7a9-71ca-449d-833c-8a6e094dd117",
"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-Analyst",
"email": "nicolle.leclair@ecstech.com",
},
{
"username": "378d0bc4-d5a7-461b-bd84-3ae6f6864af9",
"first_name": "Erin-Analyst",
"last_name": "Song-Analyst",
"email": "erin.song+1@gsa.gov",
},
{
"username": "9a98e4c9-9409-479d-964e-4aec7799107f",
"first_name": "Kristina-Analyst",
"last_name": "Yin-Analyst",
"email": "kristina.yin+1@gsa.gov",
},
]
def load_users(cls, users, group_name):
logger.info(f"Going to load {len(users)} users in group {group_name}")
for user_data in users:
try:
user, _ = User.objects.get_or_create(username=user_data["username"])
user.is_superuser = False
user.first_name = user_data["first_name"]
user.last_name = user_data["last_name"]
if "email" in user_data:
user.email = user_data["email"]
user.is_staff = True
user.is_active = True
group = UserGroup.objects.get(name=group_name)
user.groups.add(group)
user.save()
logger.debug(f"User object created for {user_data['first_name']}")
except Exception as e:
logger.warning(e)
logger.info(f"All users in group {group_name} loaded.")
@classmethod
def load(cls):
cls.load_users(cls, cls.ADMINS, "full_access_group")
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")

View file

@ -65,7 +65,7 @@ class DomainSecurityEmailForm(forms.Form):
"""Form for adding or editing a security email to a domain."""
security_email = forms.EmailField(label="Security email")
security_email = forms.EmailField(label="Security email", required=False)
class DomainOrgNameAddressForm(forms.ModelForm):

View file

@ -4,7 +4,8 @@ from django.core.management.base import BaseCommand
from auditlog.context import disable_auditlog # type: ignore
from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture
from registrar.fixtures_users import UserFixture
from registrar.fixtures_applications import DomainApplicationFixture, DomainFixture
logger = logging.getLogger(__name__)

View file

@ -0,0 +1,39 @@
# Generated by Django 4.2.1 on 2023-09-20 19:04
import django.contrib.auth.models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("registrar", "0033_alter_userdomainrole_role"),
]
operations = [
migrations.CreateModel(
name="UserGroup",
fields=[
(
"group_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="auth.group",
),
),
],
options={
"verbose_name": "User group",
"verbose_name_plural": "User groups",
},
bases=("auth.group",),
managers=[
("objects", django.contrib.auth.models.GroupManager()),
],
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 4.2.1 on 2023-09-27 18:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("registrar", "0034_usergroup"),
]
operations = [
migrations.AlterModelOptions(
name="user",
options={
"permissions": [
("analyst_access_permission", "Analyst Access Permission"),
("full_access_permission", "Full Access Permission"),
]
},
),
]

View file

@ -0,0 +1,43 @@
# From mithuntnt's answer on:
# https://stackoverflow.com/questions/26464838/getting-model-contenttype-in-migration-django-1-7
# The problem is that ContentType and Permission objects are not already created
# while we're still running migrations, so we'll go ahead and speed up that process
# a bit before we attempt to create groups which require Permissions and ContentType.
from django.conf import settings
from django.db import migrations
def create_all_contenttypes(**kwargs):
from django.apps import apps
from django.contrib.contenttypes.management import create_contenttypes
for app_config in apps.get_app_configs():
create_contenttypes(app_config, **kwargs)
def create_all_permissions(**kwargs):
from django.contrib.auth.management import create_permissions
from django.apps import apps
for app_config in apps.get_app_configs():
create_permissions(app_config, **kwargs)
def forward(apps, schema_editor):
create_all_contenttypes()
create_all_permissions()
def backward(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("contenttypes", "0002_remove_content_type_name"),
("registrar", "0035_alter_user_options"),
]
operations = [migrations.RunPython(forward, backward)]

View file

@ -0,0 +1,34 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
# It is dependent on 0035 (which populates ContentType and Permissions)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
# step 3: fake run the latest migration in the migrations list
# Alternatively:
# Only step: duplicate the migtation that loads data and run: docker-compose exec app ./manage.py migrate
from django.db import migrations
from registrar.models import UserGroup
from typing import Any
# For linting: RunPython expects a function reference,
# so let's give it one
def create_groups(apps, schema_editor) -> Any:
UserGroup.create_cisa_analyst_group(apps, schema_editor)
UserGroup.create_full_access_group(apps, schema_editor)
class Migration(migrations.Migration):
dependencies = [
("registrar", "0036_contenttypes_permissions"),
]
operations = [
migrations.RunPython(
create_groups,
reverse_code=migrations.RunPython.noop,
atomic=True,
),
]

View file

@ -12,6 +12,7 @@ from .nameserver import Nameserver
from .user_domain_role import UserDomainRole
from .public_contact import PublicContact
from .user import User
from .user_group import UserGroup
from .website import Website
from .transition_domain import TransitionDomain
@ -28,6 +29,7 @@ __all__ = [
"UserDomainRole",
"PublicContact",
"User",
"UserGroup",
"Website",
"TransitionDomain",
]
@ -42,6 +44,7 @@ auditlog.register(Host)
auditlog.register(Nameserver)
auditlog.register(UserDomainRole)
auditlog.register(PublicContact)
auditlog.register(User)
auditlog.register(User, m2m_fields=["user_permissions", "groups"])
auditlog.register(UserGroup, m2m_fields=["permissions"])
auditlog.register(Website)
auditlog.register(TransitionDomain)

View file

@ -16,12 +16,15 @@ from epplibwrapper import (
RegistryError,
ErrorCode,
)
from registrar.utility.errors import (
ActionNotAllowed,
NameserverError,
NameserverErrorCodes as nsErrorCodes,
)
from registrar.models.utility.contact_error import ContactError
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
from .utility.domain_field import DomainField
from .utility.domain_helper import DomainHelper
@ -799,7 +802,10 @@ class Domain(TimeStampedModel, DomainHelper):
def get_security_email(self):
logger.info("get_security_email-> getting the contact ")
secContact = self.security_contact
if secContact is not None:
return secContact.email
else:
return None
def clientHoldStatus(self):
return epp.Status(state=self.Status.CLIENT_HOLD, description="", lang="en")
@ -872,10 +878,10 @@ class Domain(TimeStampedModel, DomainHelper):
return None
if contact_type is None:
raise ContactError("contact_type is None")
raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE)
if contact_id is None:
raise ContactError("contact_id is None")
raise ContactError(code=ContactErrorCodes.CONTACT_ID_NONE)
# Since contact_id is registry_id,
# check that its the right length
@ -884,14 +890,10 @@ class Domain(TimeStampedModel, DomainHelper):
contact_id_length > PublicContact.get_max_id_length()
or contact_id_length < 1
):
raise ContactError(
"contact_id is of invalid length. "
"Cannot exceed 16 characters, "
f"got {contact_id} with a length of {contact_id_length}"
)
raise ContactError(code=ContactErrorCodes.CONTACT_ID_INVALID_LENGTH)
if not isinstance(contact, eppInfo.InfoContactResultData):
raise ContactError("Contact must be of type InfoContactResultData")
raise ContactError(code=ContactErrorCodes.CONTACT_INVALID_TYPE)
auth_info = contact.auth_info
postal_info = contact.postal_info
@ -1055,8 +1057,8 @@ class Domain(TimeStampedModel, DomainHelper):
return self._handle_registrant_contact(desired_contact)
_registry_id: str
if contact_type in contacts:
_registry_id: str = ""
if contacts is not None and contact_type in contacts:
_registry_id = contacts.get(contact_type)
desired = PublicContact.objects.filter(

View file

@ -81,3 +81,9 @@ class User(AbstractUser):
logger.warn(
"Failed to retrieve invitation %s", invitation, exc_info=True
)
class Meta:
permissions = [
("analyst_access_permission", "Analyst Access Permission"),
("full_access_permission", "Full Access Permission"),
]

View file

@ -0,0 +1,132 @@
from django.contrib.auth.models import Group
import logging
logger = logging.getLogger(__name__)
class UserGroup(Group):
class Meta:
verbose_name = "User group"
verbose_name_plural = "User groups"
def create_cisa_analyst_group(apps, schema_editor):
"""This method gets run from a data migration."""
# Hard to pass self to these methods as the calls from migrations
# are only expecting apps and schema_editor, so we'll just define
# apps, schema_editor in the local scope instead
CISA_ANALYST_GROUP_PERMISSIONS = [
{
"app_label": "auditlog",
"model": "logentry",
"permissions": ["view_logentry"],
},
{
"app_label": "registrar",
"model": "contact",
"permissions": ["view_contact"],
},
{
"app_label": "registrar",
"model": "domaininformation",
"permissions": ["change_domaininformation"],
},
{
"app_label": "registrar",
"model": "domainapplication",
"permissions": ["change_domainapplication"],
},
{
"app_label": "registrar",
"model": "domain",
"permissions": ["view_domain"],
},
{
"app_label": "registrar",
"model": "draftdomain",
"permissions": ["change_draftdomain"],
},
{
"app_label": "registrar",
"model": "user",
"permissions": ["analyst_access_permission", "change_user"],
},
]
# Avoid error: You can't execute queries until the end
# of the 'atomic' block.
# From django docs:
# https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations
# We cant import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
ContentType = apps.get_model("contenttypes", "ContentType")
Permission = apps.get_model("auth", "Permission")
UserGroup = apps.get_model("registrar", "UserGroup")
logger.info("Going to create the Analyst Group")
try:
cisa_analysts_group, _ = UserGroup.objects.get_or_create(
name="cisa_analysts_group",
)
cisa_analysts_group.permissions.clear()
for permission in CISA_ANALYST_GROUP_PERMISSIONS:
app_label = permission["app_label"]
model_name = permission["model"]
permissions = permission["permissions"]
# Retrieve the content type for the app and model
content_type = ContentType.objects.get(
app_label=app_label, model=model_name
)
# Retrieve the permissions based on their codenames
permissions = Permission.objects.filter(
content_type=content_type, codename__in=permissions
)
# Assign the permissions to the group
cisa_analysts_group.permissions.add(*permissions)
# Convert the permissions QuerySet to a list of codenames
permission_list = list(permissions.values_list("codename", flat=True))
logger.debug(
app_label
+ " | "
+ model_name
+ " | "
+ ", ".join(permission_list)
+ " added to group "
+ cisa_analysts_group.name
)
cisa_analysts_group.save()
logger.debug(
"CISA Analyt permissions added to group " + cisa_analysts_group.name
)
except Exception as e:
logger.error(f"Error creating analyst permissions group: {e}")
def create_full_access_group(apps, schema_editor):
"""This method gets run from a data migration."""
Permission = apps.get_model("auth", "Permission")
UserGroup = apps.get_model("registrar", "UserGroup")
logger.info("Going to create the Full Access Group")
try:
full_access_group, _ = UserGroup.objects.get_or_create(
name="full_access_group",
)
# Get all available permissions
all_permissions = Permission.objects.all()
# Assign all permissions to the group
full_access_group.permissions.add(*all_permissions)
full_access_group.save()
logger.debug("All permissions added to group " + full_access_group.name)
except Exception as e:
logger.error(f"Error creating full access group: {e}")

View file

@ -1,2 +1,51 @@
from enum import IntEnum
class ContactErrorCodes(IntEnum):
"""Used in the ContactError class for
error mapping.
Overview of contact error codes:
- 2000 CONTACT_TYPE_NONE
- 2001 CONTACT_ID_NONE
- 2002 CONTACT_ID_INVALID_LENGTH
- 2003 CONTACT_INVALID_TYPE
"""
CONTACT_TYPE_NONE = 2000
CONTACT_ID_NONE = 2001
CONTACT_ID_INVALID_LENGTH = 2002
CONTACT_INVALID_TYPE = 2003
CONTACT_NOT_FOUND = 2004
class ContactError(Exception):
...
"""
Overview of contact error codes:
- 2000 CONTACT_TYPE_NONE
- 2001 CONTACT_ID_NONE
- 2002 CONTACT_ID_INVALID_LENGTH
- 2003 CONTACT_INVALID_TYPE
- 2004 CONTACT_NOT_FOUND
"""
# For linter
_contact_id_error = "contact_id has an invalid length. Cannot exceed 16 characters."
_contact_invalid_error = "Contact must be of type InfoContactResultData"
_contact_not_found_error = "No contact was found in cache or the registry"
_error_mapping = {
ContactErrorCodes.CONTACT_TYPE_NONE: "contact_type is None",
ContactErrorCodes.CONTACT_ID_NONE: "contact_id is None",
ContactErrorCodes.CONTACT_ID_INVALID_LENGTH: _contact_id_error,
ContactErrorCodes.CONTACT_INVALID_TYPE: _contact_invalid_error,
ContactErrorCodes.CONTACT_NOT_FOUND: _contact_not_found_error,
}
def __init__(self, *args, code=None, **kwargs):
super().__init__(*args, **kwargs)
self.code = code
if self.code in self._error_mapping:
self.message = self._error_mapping.get(self.code)
def __str__(self):
return f"{self.message}"

View file

@ -13,10 +13,10 @@
{% elif original.state == original.State.ON_HOLD %}
<input type="submit" value="Remove hold" name="_remove_client_hold">
{% endif %}
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
<input type="submit" value="get status" name="_get_status">
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
<input type="submit" value="Get registry status" name="_get_status">
{% if original.state != original.State.DELETED %}
<input type="submit" value="Delete Domain in Registry" name="_delete_domain">
<input type="submit" value="Delete domain in registry" name="_delete_domain">
{% endif %}
</div>
{{ block.super }}

View file

@ -46,8 +46,11 @@
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url %}
{% url 'domain-security-email' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Security email' value=domain.security_email edit_link=url %}
{% if security_email is not None and security_email != default_security_email%}
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url %}
{% else %}
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %}
{% endif %}
{% url 'domain-users' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %}

View file

@ -1,7 +1,7 @@
{% extends "domain_base.html" %}
{% load static field_helpers%}
{% block title %}Domain name servers | {{ domain.name }} | {% endblock %}
{% block title %}DNS name servers | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{# this is right after the messages block in the parent template #}
@ -9,7 +9,7 @@
{% include "includes/form_errors.html" with form=form %}
{% endfor %}
<h1>Domain name servers</h1>
<h1>DNS name servers</h1>
<p>Before your domain can be used we'll need information about your domain
name servers.</p>

View file

@ -1,18 +1,16 @@
{% extends "domain_base.html" %}
{% load static field_helpers url_helpers %}
{% block title %}Domain security email | {{ domain.name }} | {% endblock %}
{% block title %}Security email | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>Domain security email</h1>
<h1>Security email</h1>
<p>We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the <a href="{% public_site_url 'about/data/' %}">.gov domain data</a> we provide.</p>
<p>A security contact should be capable of evaluating or triaging security reports for your entire domain. Use a team email address, not an individuals email. We recommend using an alias, like security@domain.gov.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate>
{% csrf_token %}

View file

@ -1,11 +1,11 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% block title %}Domain contact information | {{ domain.name }} | {% endblock %}
{% block title %}Your contact information | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>Domain contact information</h1>
<h1>Your contact information</h1>
<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>

View file

@ -19,6 +19,7 @@ from registrar.models import (
DomainApplication,
DomainInvitation,
User,
UserGroup,
DomainInformation,
PublicContact,
Domain,
@ -95,7 +96,10 @@ class MockUserLogin:
}
user, _ = UserModel.objects.get_or_create(**args)
user.is_staff = True
user.is_superuser = True
# Create or retrieve the group
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
# Add the user to the group
user.groups.set([group])
user.save()
backend = settings.AUTHENTICATION_BACKENDS[-1]
login(request, user, backend=backend)
@ -427,22 +431,33 @@ def mock_user():
def create_superuser():
User = get_user_model()
p = "adminpass"
return User.objects.create_superuser(
user = User.objects.create_user(
username="superuser",
email="admin@example.com",
is_staff=True,
password=p,
)
# Retrieve the group or create it if it doesn't exist
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
# Add the user to the group
user.groups.set([group])
return user
def create_user():
User = get_user_model()
p = "userpass"
return User.objects.create_user(
user = User.objects.create_user(
username="staffuser",
email="user@example.com",
is_staff=True,
password=p,
)
# Retrieve the group or create it if it doesn't exist
group, _ = UserGroup.objects.get_or_create(name="cisa_analysts_group")
# Add the user to the group
user.groups.set([group])
return user
def create_ready_domain():

View file

@ -52,6 +52,7 @@ class TestDomainAdmin(MockEppLib):
self.factory = RequestFactory()
super().setUp()
@skip("Why did this test stop working, and is is a good test")
def test_place_and_remove_hold(self):
domain = create_ready_domain()
# get admin page and assert Place Hold button
@ -108,12 +109,12 @@ class TestDomainAdmin(MockEppLib):
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Delete Domain in Registry")
self.assertContains(response, "Delete domain in registry")
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Delete Domain in Registry", "name": domain.name},
{"_delete_domain": "Delete domain in registry", "name": domain.name},
follow=True,
)
request.user = self.client
@ -148,12 +149,12 @@ class TestDomainAdmin(MockEppLib):
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Delete Domain in Registry")
self.assertContains(response, "Delete domain in registry")
# Test the error
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Delete Domain in Registry", "name": domain.name},
{"_delete_domain": "Delete domain in registry", "name": domain.name},
follow=True,
)
request.user = self.client
@ -193,12 +194,12 @@ class TestDomainAdmin(MockEppLib):
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Delete Domain in Registry")
self.assertContains(response, "Delete domain in registry")
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Delete Domain in Registry", "name": domain.name},
{"_delete_domain": "Delete domain in registry", "name": domain.name},
follow=True,
)
request.user = self.client
@ -220,7 +221,7 @@ class TestDomainAdmin(MockEppLib):
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Delete Domain in Registry", "name": domain.name},
{"_delete_domain": "Delete domain in registry", "name": domain.name},
follow=True,
)
request.user = self.client
@ -933,14 +934,13 @@ class MyUserAdminTest(TestCase):
request.user = create_user()
list_display = self.admin.get_list_display(request)
expected_list_display = (
expected_list_display = [
"email",
"first_name",
"last_name",
"is_staff",
"is_superuser",
"group",
"status",
)
]
self.assertEqual(list_display, expected_list_display)
self.assertNotIn("username", list_display)
@ -952,14 +952,14 @@ class MyUserAdminTest(TestCase):
expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request)
self.assertEqual(fieldsets, expected_fieldsets)
def test_get_fieldsets_non_superuser(self):
def test_get_fieldsets_cisa_analyst(self):
request = self.client.request().wsgi_request
request.user = create_user()
fieldsets = self.admin.get_fieldsets(request)
expected_fieldsets = (
(None, {"fields": ("password", "status")}),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
("Permissions", {"fields": ("is_active", "is_staff", "is_superuser")}),
("Permissions", {"fields": ("is_active", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
self.assertEqual(fieldsets, expected_fieldsets)

View file

@ -0,0 +1,51 @@
from django.test import TestCase
from registrar.models import (
UserGroup,
)
import logging
logger = logging.getLogger(__name__)
class TestGroups(TestCase):
def test_groups_created(self):
"""The test enviroment contains data that was created in migration,
so we are able to test groups and permissions.
- Test cisa_analysts_group and full_access_group created
- Test permissions on full_access_group
"""
# Get the UserGroup objects
cisa_analysts_group = UserGroup.objects.get(name="cisa_analysts_group")
full_access_group = UserGroup.objects.get(name="full_access_group")
# Assert that the cisa_analysts_group exists in the database
self.assertQuerysetEqual(
UserGroup.objects.filter(name="cisa_analysts_group"), [cisa_analysts_group]
)
# Assert that the full_access_group exists in the database
self.assertQuerysetEqual(
UserGroup.objects.filter(name="full_access_group"), [full_access_group]
)
# Test permissions for cisa_analysts_group
# Define the expected permission codenames
expected_permissions = [
"view_logentry",
"view_contact",
"view_domain",
"change_domainapplication",
"change_domaininformation",
"change_draftdomain",
"analyst_access_permission",
"change_user",
]
# Get the codenames of actual permissions associated with the group
actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()]
# Assert that the actual permissions match the expected permissions
self.assertListEqual(actual_permissions, expected_permissions)

View file

@ -17,6 +17,9 @@ from registrar.models.draft_domain import DraftDomain
from registrar.models.public_contact import PublicContact
from registrar.models.user import User
from registrar.utility.errors import ActionNotAllowed, NameserverError
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
from .common import MockEppLib
from django_fsm import TransitionNotAllowed # type: ignore
from epplibwrapper import (
@ -195,6 +198,56 @@ class TestDomainCache(MockEppLib):
self.assertEqual(cached_contact, in_db.registry_id)
self.assertEqual(domain.security_contact.email, "123test@mail.gov")
def test_errors_map_epp_contact_to_public_contact(self):
"""
Scenario: Registrant gets invalid data from EPPLib
When the `map_epp_contact_to_public_contact` function
gets invalid data from EPPLib
Then the function throws the expected ContactErrors
"""
domain, _ = Domain.objects.get_or_create(name="registry.gov")
fakedEpp = self.fakedEppObject()
invalid_length = fakedEpp.dummyInfoContactResultData(
"Cymaticsisasubsetofmodalvibrationalphenomena", "lengthInvalid@mail.gov"
)
valid_object = fakedEpp.dummyInfoContactResultData("valid", "valid@mail.gov")
desired_error = ContactErrorCodes.CONTACT_ID_INVALID_LENGTH
with self.assertRaises(ContactError) as context:
domain.map_epp_contact_to_public_contact(
invalid_length,
invalid_length.id,
PublicContact.ContactTypeChoices.SECURITY,
)
self.assertEqual(context.exception.code, desired_error)
desired_error = ContactErrorCodes.CONTACT_ID_NONE
with self.assertRaises(ContactError) as context:
domain.map_epp_contact_to_public_contact(
valid_object,
None,
PublicContact.ContactTypeChoices.SECURITY,
)
self.assertEqual(context.exception.code, desired_error)
desired_error = ContactErrorCodes.CONTACT_INVALID_TYPE
with self.assertRaises(ContactError) as context:
domain.map_epp_contact_to_public_contact(
"bad_object",
valid_object.id,
PublicContact.ContactTypeChoices.SECURITY,
)
self.assertEqual(context.exception.code, desired_error)
desired_error = ContactErrorCodes.CONTACT_TYPE_NONE
with self.assertRaises(ContactError) as context:
domain.map_epp_contact_to_public_contact(
valid_object,
valid_object.id,
None,
)
self.assertEqual(context.exception.code, desired_error)
class TestDomainCreation(MockEppLib):
"""Rule: An approved domain application must result in a domain"""

View file

@ -1309,7 +1309,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
page = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain name servers")
self.assertContains(page, "DNS name servers")
@skip("Broken by adding registry connection fix in ticket 848")
def test_domain_nameservers_form(self):
@ -1414,7 +1414,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
page = self.client.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain contact information")
self.assertContains(page, "Your contact information")
def test_domain_your_contact_information_content(self):
"""Logged-in user's contact information appears on the page."""
@ -1439,7 +1439,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
)
# Loads correctly
self.assertContains(page, "Domain security email")
self.assertContains(page, "Security email")
self.assertContains(page, "security@mail.gov")
self.mockSendPatch.stop()
@ -1455,7 +1455,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
)
# Loads correctly
self.assertContains(page, "Domain security email")
self.assertContains(page, "Security email")
self.assertNotContains(page, "dotgov@cisa.dhs.gov")
self.mockSendPatch.stop()
@ -1464,7 +1464,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
page = self.client.get(
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain security email")
self.assertContains(page, "Security email")
def test_domain_security_email_form(self):
"""Adding a security email works.

View file

@ -21,6 +21,7 @@ from registrar.models import (
User,
UserDomainRole,
)
from registrar.models.public_contact import PublicContact
from ..forms import (
ContactForm,
@ -42,6 +43,19 @@ class DomainView(DomainPermissionView):
template_name = "domain_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
default_email = Domain().get_default_security_contact().email
context["default_security_email"] = default_email
security_email = self.get_object().get_security_email()
if security_email is None or security_email == default_email:
context["security_email"] = None
return context
context["security_email"] = security_email
return context
class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
"""Organization name and mailing address view"""
@ -284,10 +298,21 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
"""The form is valid, call setter in model."""
# Set the security email from the form
new_email = form.cleaned_data.get("security_email", "")
new_email: str = form.cleaned_data.get("security_email", "")
# If we pass nothing for the sec email, set to the default
if new_email is None or new_email.strip() == "":
new_email = PublicContact.get_default_security().email
domain = self.get_object()
contact = domain.security_contact
# If no default is created for security_contact,
# then we cannot connect to the registry.
if contact is None:
messages.error(self.request, "Update failed. Cannot contact the registry.")
return redirect(self.get_success_url())
contact.email = new_email
contact.save()

View file

@ -63,9 +63,9 @@ class DomainPermission(PermissionsLoginMixin):
"""
# Check if the user is permissioned...
user_is_analyst_or_superuser = (
self.request.user.is_staff or self.request.user.is_superuser
)
user_is_analyst_or_superuser = self.request.user.has_perm(
"registrar.analyst_access_permission"
) or self.request.user.has_perm("registrar.full_access_permission")
if not user_is_analyst_or_superuser:
return False

View file

@ -33,7 +33,9 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
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
context["is_analyst_or_superuser"] = user.has_perm(
"registrar.analyst_access_permission"
) or user.has_perm("registrar.full_access_permission")
# Stored in a variable for the linter
action = "analyst_action"
action_location = "analyst_action_location"