Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
rachidatecs committed Sep 4, 2024
2 parents 79fbc5c + cc7f588 commit dea71ce
Show file tree
Hide file tree
Showing 21 changed files with 511 additions and 23 deletions.
16 changes: 16 additions & 0 deletions docs/developer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ While on production (the sandbox referred to as `stable`), an existing analyst o
"username": "<UUID here>",
"first_name": "",
"last_name": "",
"email": "",
},
...
]
Expand All @@ -121,6 +122,7 @@ Analysts are a variant of the admin role with limited permissions. The process f
"username": "<UUID here>",
"first_name": "",
"last_name": "",
"email": "",
},
...
]
Expand All @@ -131,6 +133,20 @@ Analysts are a variant of the admin role with limited permissions. The process f

Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name, or use a completely different first/last name to avoid confusion. Example: `Bob-Analyst`

## Adding an email address to the email whitelist (sandboxes only)
On all non-production environments, we use an email whitelist table (called `Allowed emails`). This whitelist is not case sensitive, and it provides an inclusion for +1 emails (like [email protected]). The content after the `+` can be any _digit_. The whitelist checks for the "base" email (example.person) so even if you only have the +1 email defined, an email will still be sent assuming that it follows those conventions.

To add yourself to this, you can go about it in three ways.

Permanent (all sandboxes):
1. In src/registrar/fixtures_users.py, add the "email" field to your user in either the ADMIN or STAFF table.
2. In src/registrar/fixtures_users.py, add the desired email address to the `ADDITIONAL_ALLOWED_EMAILS` list. This route is suggested for product.

Sandbox specific (wiped when the db is reset):
3. Create a new record on the `Allowed emails` table with your email address. This can be done through django admin.

More detailed instructions regarding #3 can be found [here](https://docs.google.com/document/d/1ebIz4PcUuoiT7LlVy83EAyHAk_nWPEc99neMp4QjzDs).

## Adding to CODEOWNERS (optional)

The CODEOWNERS file sets the tagged individuals as default reviewers on any Pull Request that changes files that they are marked as owners of.
Expand Down
38 changes: 38 additions & 0 deletions src/registrar/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.db.models import Value, CharField, Q
from django.db.models.functions import Concat, Coalesce
from django.http import HttpResponseRedirect
from django.conf import settings
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions, FSMField
from registrar.models.domain_information import DomainInformation
Expand Down Expand Up @@ -1966,6 +1967,9 @@ def save_model(self, request, obj, form, change):
else:
obj.action_needed_reason_email = default_email

if obj.status in DomainRequest.get_statuses_that_send_emails() and not settings.IS_PRODUCTION:
self._check_for_valid_email(request, obj)

# == Handle status == #
if obj.status == original_obj.status:
# If the status hasn't changed, let the base function take care of it
Expand All @@ -1978,6 +1982,29 @@ def save_model(self, request, obj, form, change):
if should_save:
return super().save_model(request, obj, form, change)

def _check_for_valid_email(self, request, obj):
"""Certain emails are whitelisted in non-production environments,
so we should display that information using this function.
"""

# TODO 2574: remove lines 1977-1978 (refactor as needed)
profile_flag = flag_is_active(request, "profile_feature")
if profile_flag and hasattr(obj, "creator"):
recipient = obj.creator
elif not profile_flag and hasattr(obj, "submitter"):
recipient = obj.submitter
else:
recipient = None

# Displays a warning in admin when an email cannot be sent
if recipient and recipient.email:
email = recipient.email
allowed = models.AllowedEmail.is_allowed_email(email)
error_message = f"Could not send email. The email '{email}' does not exist within the whitelist."
if not allowed:
messages.warning(request, error_message)

def _handle_status_change(self, request, obj, original_obj):
"""
Checks for various conditions when a status change is triggered.
Expand Down Expand Up @@ -3357,6 +3384,16 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
return super().change_view(request, object_id, form_url, extra_context)


class AllowedEmailAdmin(ListHeaderAdmin):
class Meta:
model = models.AllowedEmail

list_display = ["email"]
search_fields = ["email"]
search_help_text = "Search by email."
ordering = ["email"]


admin.site.unregister(LogEntry) # Unregister the default registration

admin.site.register(LogEntry, CustomLogEntryAdmin)
Expand Down Expand Up @@ -3385,6 +3422,7 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
admin.site.register(models.Suborganization, SuborganizationAdmin)
admin.site.register(models.SeniorOfficial, SeniorOfficialAdmin)
admin.site.register(models.UserPortfolioPermission, UserPortfolioPermissionAdmin)
admin.site.register(models.AllowedEmail, AllowedEmailAdmin)

# Register our custom waffle implementations
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
Expand Down
11 changes: 5 additions & 6 deletions src/registrar/assets/js/get-gov-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,6 @@ function initializeWidgetOnList(list, parentId) {
var actionNeededEmailReadonlyTextarea = document.querySelector("#action-needed-reason-email-readonly-textarea")

// Edit e-mail modal (and its confirmation button)
var actionNeededEmailAlreadySentModal = document.querySelector("#email-already-sent-modal")
var confirmEditEmailButton = document.querySelector("#email-already-sent-modal_continue-editing-button")

// Headers and footers (which change depending on if the e-mail was sent or not)
Expand Down Expand Up @@ -561,11 +560,11 @@ function initializeWidgetOnList(list, parentId) {
updateActionNeededEmailDisplay(reason)
});

editEmailButton.addEventListener("click", function() {
if (!checkEmailAlreadySent()) {
showEmail(canEdit=true)
}
});
// editEmailButton.addEventListener("click", function() {
// if (!checkEmailAlreadySent()) {
// showEmail(canEdit=true)
// }
// });

confirmEditEmailButton.addEventListener("click", function() {
// Show editable view
Expand Down
2 changes: 1 addition & 1 deletion src/registrar/assets/sass/_theme/_admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ button .usa-icon,
#submitRowToggle {
color: var(--body-fg);
}
.requested-domain-sticky {
.submit-row-sticky {
max-width: 325px;
overflow: hidden;
white-space: nowrap;
Expand Down
45 changes: 44 additions & 1 deletion src/registrar/fixtures_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
User,
UserGroup,
)
from registrar.models.allowed_email import AllowedEmail


fake = Faker()
Expand All @@ -32,6 +33,7 @@ class UserFixture:
"username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf",
"first_name": "Aditi",
"last_name": "Green",
"email": "[email protected]",
},
{
"username": "be17c826-e200-4999-9389-2ded48c43691",
Expand All @@ -42,11 +44,13 @@ class UserFixture:
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"first_name": "Rachid",
"last_name": "Mrad",
"email": "[email protected]",
},
{
"username": "eb2214cd-fc0c-48c0-9dbd-bc4cd6820c74",
"first_name": "Alysia",
"last_name": "Broddrick",
"email": "[email protected]",
},
{
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
Expand All @@ -63,6 +67,7 @@ class UserFixture:
"username": "83c2b6dd-20a2-4cac-bb40-e22a72d2955c",
"first_name": "Cameron",
"last_name": "Dixon",
"email": "[email protected]",
},
{
"username": "0353607a-cbba-47d2-98d7-e83dcd5b90ea",
Expand All @@ -83,16 +88,19 @@ class UserFixture:
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
"first_name": "Rebecca",
"last_name": "Hsieh",
"email": "[email protected]",
},
{
"username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52",
"first_name": "David",
"last_name": "Kennedy",
"email": "[email protected]",
},
{
"username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
"first_name": "Nicolle",
"last_name": "LeClair",
"email": "[email protected]",
},
{
"username": "24840450-bf47-4d89-8aa9-c612fe68f9da",
Expand Down Expand Up @@ -141,6 +149,7 @@ class UserFixture:
"username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54",
"first_name": "Aditi-Analyst",
"last_name": "Green-Analyst",
"email": "[email protected]",
},
{
"username": "d6bf296b-fac5-47ff-9c12-f88ccc5c1b99",
Expand Down Expand Up @@ -183,6 +192,7 @@ class UserFixture:
"username": "5dc6c9a6-61d9-42b4-ba54-4beff28bac3c",
"first_name": "David-Analyst",
"last_name": "Kennedy-Analyst",
"email": "[email protected]",
},
{
"username": "0eb6f326-a3d4-410f-a521-aa4c1fad4e47",
Expand All @@ -194,7 +204,7 @@ class UserFixture:
"username": "cfe7c2fc-e24a-480e-8b78-28645a1459b3",
"first_name": "Nicolle-Analyst",
"last_name": "LeClair-Analyst",
"email": "nicolle.leclair@ecstech.com",
"email": "nicolle.leclair@gmail.com",
},
{
"username": "378d0bc4-d5a7-461b-bd84-3ae6f6864af9",
Expand Down Expand Up @@ -240,6 +250,9 @@ class UserFixture:
},
]

# Additional emails to add to the AllowedEmail whitelist.
ADDITIONAL_ALLOWED_EMAILS: list[str] = ["[email protected]", "[email protected]"]

def load_users(cls, users, group_name, are_superusers=False):
logger.info(f"Going to load {len(users)} users in group {group_name}")
for user_data in users:
Expand All @@ -264,6 +277,32 @@ def load_users(cls, users, group_name, are_superusers=False):
logger.warning(e)
logger.info(f"All users in group {group_name} loaded.")

def load_allowed_emails(cls, users, additional_emails):
"""Populates a whitelist of allowed emails (as defined in this list)"""
logger.info(f"Going to load allowed emails for {len(users)} users")
if additional_emails:
logger.info(f"Going to load {len(additional_emails)} additional allowed emails")

# Load user emails
allowed_emails = []
for user_data in users:
user_email = user_data.get("email")
if user_email and user_email not in allowed_emails:
allowed_emails.append(AllowedEmail(email=user_email))
else:
first_name = user_data.get("first_name")
last_name = user_data.get("last_name")
logger.warning(f"Could not add email to whitelist for {first_name} {last_name}.")

# Load additional emails
allowed_emails.extend([AllowedEmail(email=email) for email in additional_emails])

if allowed_emails:
AllowedEmail.objects.bulk_create(allowed_emails)
logger.info(f"Loaded {len(allowed_emails)} allowed emails")
else:
logger.info("No allowed emails to load")

@classmethod
def load(cls):
# Lumped under .atomic to ensure we don't make redundant DB calls.
Expand All @@ -275,3 +314,7 @@ def load(cls):
with transaction.atomic():
cls.load_users(cls, cls.ADMINS, "full_access_group", are_superusers=True)
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")

# Combine ADMINS and STAFF lists
all_users = cls.ADMINS + cls.STAFF
cls.load_allowed_emails(cls, all_users, additional_emails=cls.ADDITIONAL_ALLOWED_EMAILS)
25 changes: 25 additions & 0 deletions src/registrar/migrations/0121_allowedemail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.10 on 2024-08-29 18:16

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("registrar", "0120_add_domainrequest_submission_dates"),
]

operations = [
migrations.CreateModel(
name="AllowedEmail",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("email", models.EmailField(max_length=320, unique=True)),
],
options={
"abstract": False,
},
),
]
37 changes: 37 additions & 0 deletions src/registrar/migrations/0122_create_groups_v16.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
# It is dependent on 0079 (which populates federal agencies)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
# 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
# [RECOMMENDED]
# Alternatively:
# step 1: duplicate the migration that loads data
# step 2: 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", "0121_allowedemail"),
]

operations = [
migrations.RunPython(
create_groups,
reverse_code=migrations.RunPython.noop,
atomic=True,
),
]
3 changes: 3 additions & 0 deletions src/registrar/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .suborganization import Suborganization
from .senior_official import SeniorOfficial
from .user_portfolio_permission import UserPortfolioPermission
from .allowed_email import AllowedEmail


__all__ = [
Expand All @@ -48,6 +49,7 @@
"Suborganization",
"SeniorOfficial",
"UserPortfolioPermission",
"AllowedEmail",
]

auditlog.register(Contact)
Expand All @@ -73,3 +75,4 @@
auditlog.register(Suborganization)
auditlog.register(SeniorOfficial)
auditlog.register(UserPortfolioPermission)
auditlog.register(AllowedEmail)
Loading

0 comments on commit dea71ce

Please sign in to comment.