diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 8523af013..2033ee51c 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -26,7 +26,6 @@ on: - rb - ko - ab - - bl - rjm - dk diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 3848a33bd..f8730c865 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -26,7 +26,6 @@ on: - rb - ko - ab - - bl - rjm - dk diff --git a/ops/manifests/manifest-bl.yaml b/ops/manifests/manifest-bl.yaml deleted file mode 100644 index ea0617427..000000000 --- a/ops/manifests/manifest-bl.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -applications: -- name: getgov-bl - 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 - health-check-invocation-timeout: 40 - 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-bl.app.cloud.gov - # Tell Django how much stuff to log - DJANGO_LOG_LEVEL: INFO - # default public site location - GETGOV_PUBLIC_SITE_URL: https://get.gov - # Flag to disable/enable features in prod environments - IS_PRODUCTION: False - routes: - - route: getgov-bl.app.cloud.gov - services: - - getgov-credentials - - getgov-bl-database diff --git a/src/djangooidc/exceptions.py b/src/djangooidc/exceptions.py index 260750a4d..226337f54 100644 --- a/src/djangooidc/exceptions.py +++ b/src/djangooidc/exceptions.py @@ -33,6 +33,10 @@ class AuthenticationFailed(OIDCException): friendly_message = "This login attempt didn't work." +class NoStateDefined(OIDCException): + friendly_message = "The session state is None." + + class InternalError(OIDCException): status = status.INTERNAL_SERVER_ERROR friendly_message = "The system broke while trying to log you in." diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py index 91bfddc66..bff766bb4 100644 --- a/src/djangooidc/oidc.py +++ b/src/djangooidc/oidc.py @@ -183,6 +183,8 @@ class Client(oic.Client): if authn_response["state"] != session.get("state", None): # this most likely means the user's Django session vanished logger.error("Received state not the same as expected for %s" % state) + if session.get("state", None) is None: + raise o_e.NoStateDefined() raise o_e.AuthenticationFailed(locator=state) if self.behaviour.get("response_type") == "code": @@ -272,6 +274,11 @@ class Client(oic.Client): super(Client, self).store_response(resp, info) + def get_default_acr_value(self): + """returns the acr_value from settings + this helper function is called from djangooidc views""" + return self.behaviour.get("acr_value") + def get_step_up_acr_value(self): """returns the step_up_acr_value from settings this helper function is called from djangooidc views""" diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 282e91e1f..63b23df96 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -3,6 +3,8 @@ from unittest.mock import MagicMock, patch from django.http import HttpResponse from django.test import Client, TestCase, RequestFactory from django.urls import reverse + +from djangooidc.exceptions import NoStateDefined from ..views import login_callback from .common import less_console_noise @@ -17,6 +19,9 @@ class ViewsTest(TestCase): def say_hi(*args): return HttpResponse("Hi") + def create_acr(*args): + return "any string" + def user_info(*args): return { "sub": "TEST", @@ -34,6 +39,7 @@ class ViewsTest(TestCase): callback_url = reverse("openid_login_callback") # mock mock_client.create_authn_request.side_effect = self.say_hi + mock_client.get_default_acr_value.side_effect = self.create_acr # test response = self.client.get(reverse("login"), {"next": callback_url}) # assert @@ -53,6 +59,19 @@ class ViewsTest(TestCase): self.assertTemplateUsed(response, "500.html") self.assertIn("Server error", response.content.decode("utf-8")) + def test_callback_with_no_session_state(self, mock_client): + """If the local session is None (ie the server restarted while user was logged out), + we do not throw an exception. Rather, we attempt to login again.""" + # mock + mock_client.get_default_acr_value.side_effect = self.create_acr + mock_client.callback.side_effect = NoStateDefined() + # test + with less_console_noise(): + response = self.client.get(reverse("openid_login_callback")) + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/") + def test_login_callback_reads_next(self, mock_client): # setup session = self.client.session diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index b5905df48..2fc2a0363 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -55,6 +55,10 @@ def error_page(request, error): def openid(request): """Redirect the user to an authentication provider (OP).""" + + # If the session reset because of a server restart, attempt to login again + request.session["acr_value"] = CLIENT.get_default_acr_value() + request.session["next"] = request.GET.get("next", "/") try: @@ -78,9 +82,13 @@ def login_callback(request): if user: login(request, user) logger.info("Successfully logged in user %s" % user) + # Double login bug (1507)? return redirect(request.session.get("next", "/")) else: raise o_e.BannedUser() + except o_e.NoStateDefined as nsd_err: + logger.warning(f"No State Defined: {nsd_err}") + return redirect(request.session.get("next", "/")) except Exception as err: return error_page(request, err) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8d3b1d29f..bd5555805 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -610,7 +610,7 @@ class DomainInformationAdmin(ListHeaderAdmin): ), ("Anything else?", {"fields": ["anything_else"]}), ( - "Requirements for operating .gov domains", + "Requirements for operating a .gov domain", {"fields": ["is_policy_acknowledged"]}, ), ] @@ -779,7 +779,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): ), ("Anything else?", {"fields": ["anything_else"]}), ( - "Requirements for operating .gov domains", + "Requirements for operating a .gov domain", {"fields": ["is_policy_acknowledged"]}, ), ] @@ -1239,6 +1239,29 @@ class DraftDomainAdmin(ListHeaderAdmin): search_help_text = "Search by draft domain name." +class VeryImportantPersonAdmin(ListHeaderAdmin): + list_display = ("email", "requestor", "truncated_notes", "created_at") + search_fields = ["email"] + search_help_text = "Search by email." + list_filter = [ + "requestor", + ] + readonly_fields = [ + "requestor", + ] + + def truncated_notes(self, obj): + # Truncate the 'notes' field to 50 characters + return str(obj.notes)[:50] + + truncated_notes.short_description = "Notes (Truncated)" # type: ignore + + def save_model(self, request, obj, form, change): + # Set the user field to the current admin user + obj.requestor = request.user if request.user.is_authenticated else None + super().save_model(request, obj, form, change) + + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(models.User, MyUserAdmin) @@ -1259,3 +1282,4 @@ admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) +admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index efa512f22..372434887 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -660,7 +660,6 @@ ALLOWED_HOSTS = [ "getgov-rb.app.cloud.gov", "getgov-ko.app.cloud.gov", "getgov-ab.app.cloud.gov", - "getgov-bl.app.cloud.gov", "getgov-rjm.app.cloud.gov", "getgov-dk.app.cloud.gov", "manage.get.gov", diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 36ff408c2..85ce28bb6 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -838,8 +838,8 @@ class AnythingElseForm(RegistrarForm): class RequirementsForm(RegistrarForm): is_policy_acknowledged = forms.BooleanField( - label="I read and agree to the requirements for operating .gov domains.", + label="I read and agree to the requirements for operating a .gov domain.", error_messages={ - "required": ("Check the box if you read and agree to the requirements for operating .gov domains.") + "required": ("Check the box if you read and agree to the requirements for operating a .gov domain.") }, ) diff --git a/src/registrar/migrations/0063_veryimportantperson.py b/src/registrar/migrations/0063_veryimportantperson.py new file mode 100644 index 000000000..38cfe328e --- /dev/null +++ b/src/registrar/migrations/0063_veryimportantperson.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.7 on 2024-01-23 23:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0062_alter_host_name"), + ] + + operations = [ + migrations.CreateModel( + name="VeryImportantPerson", + 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(db_index=True, help_text="Email", max_length=254)), + ("notes", models.TextField(help_text="Notes")), + ( + "requestor", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="verifiedby_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/src/registrar/migrations/0064_alter_domainapplication_address_line1_and_more.py b/src/registrar/migrations/0064_alter_domainapplication_address_line1_and_more.py new file mode 100644 index 000000000..7241c7164 --- /dev/null +++ b/src/registrar/migrations/0064_alter_domainapplication_address_line1_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.7 on 2024-01-23 22:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0063_veryimportantperson"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="address_line1", + field=models.TextField(blank=True, help_text="Street address", null=True, verbose_name="Address line 1"), + ), + migrations.AlterField( + model_name="domainapplication", + name="address_line2", + field=models.TextField( + blank=True, help_text="Street address line 2 (optional)", null=True, verbose_name="Address line 2" + ), + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 6afad5a5c..90cb2e286 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -13,6 +13,7 @@ from .user import User from .user_group import UserGroup from .website import Website from .transition_domain import TransitionDomain +from .very_important_person import VeryImportantPerson __all__ = [ "Contact", @@ -29,6 +30,7 @@ __all__ = [ "UserGroup", "Website", "TransitionDomain", + "VeryImportantPerson", ] auditlog.register(Contact) @@ -45,3 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"]) auditlog.register(UserGroup, m2m_fields=["permissions"]) auditlog.register(Website) auditlog.register(TransitionDomain) +auditlog.register(VeryImportantPerson) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 196449bfa..30def9cfc 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -431,11 +431,13 @@ class DomainApplication(TimeStampedModel): null=True, blank=True, help_text="Street address", + verbose_name="Address line 1", ) address_line2 = models.TextField( null=True, blank=True, help_text="Street address line 2 (optional)", + verbose_name="Address line 2", ) city = models.TextField( null=True, diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index d79e4c9ee..269569bfe 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -7,6 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole from .domain_invitation import DomainInvitation from .transition_domain import TransitionDomain +from .very_important_person import VeryImportantPerson from .domain import Domain from phonenumber_field.modelfields import PhoneNumberField # type: ignore @@ -89,6 +90,10 @@ class User(AbstractUser): if TransitionDomain.objects.filter(username=email).exists(): return False + # New users flagged by Staff to bypass ial2 + if VeryImportantPerson.objects.filter(email=email).exists(): + return False + # A new incoming user who is being invited to be a domain manager (that is, # their email address is in DomainInvitation for an invitation that is not yet "retrieved"). invited = DomainInvitation.DomainInvitationStatus.INVITED diff --git a/src/registrar/models/very_important_person.py b/src/registrar/models/very_important_person.py new file mode 100644 index 000000000..9134cb893 --- /dev/null +++ b/src/registrar/models/very_important_person.py @@ -0,0 +1,32 @@ +from django.db import models + +from .utility.time_stamped_model import TimeStampedModel + + +class VeryImportantPerson(TimeStampedModel): + + """emails that get added to this table will bypass ial2 on login.""" + + email = models.EmailField( + null=False, + blank=False, + help_text="Email", + db_index=True, + ) + + requestor = models.ForeignKey( + "registrar.User", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="verifiedby_user", + ) + + notes = models.TextField( + null=False, + blank=False, + help_text="Notes", + ) + + def __str__(self): + return self.email diff --git a/src/registrar/templates/application_form.html b/src/registrar/templates/application_form.html index c34ddf5bc..524045fbe 100644 --- a/src/registrar/templates/application_form.html +++ b/src/registrar/templates/application_form.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load static form_helpers url_helpers %} -{% block title %}Apply for a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %} +{% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %} {% block content %}
Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.
+Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.
The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.
diff --git a/src/registrar/templates/django/forms/label.html b/src/registrar/templates/django/forms/label.html index 18d24a7bd..545ccf781 100644 --- a/src/registrar/templates/django/forms/label.html +++ b/src/registrar/templates/django/forms/label.html @@ -10,7 +10,7 @@ {% if widget.attrs.required %} - {% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating .gov domains." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %} + {% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %} {% else %} * {% endif %} diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index b9e4ba853..8896bd85f 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -1,7 +1,7 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi. -{{ requester_email }} has added you as a manager on {{ domain.name }}. +{{ requestor_email }} has added you as a manager on {{ domain.name }}. You can manage this domain on the .gov registrar