diff --git a/benefits/core/admin.py b/benefits/core/admin.py index 6ad65f919..144abffac 100644 --- a/benefits/core/admin.py +++ b/benefits/core/admin.py @@ -65,13 +65,13 @@ class SortableEligibilityVerifierAdmin(SortableAdminMixin, admin.ModelAdmin): # def get_exclude(self, request, obj=None): if not request.user.is_superuser: return [ - "api_auth_header", - "api_auth_key_secret_name", - "public_key", - "jwe_cek_enc", - "jwe_encryption_alg", - "jws_signing_alg", - "form_class", + "eligibility_api_auth_header", + "eligibility_api_auth_key_secret_name", + "eligibility_api_public_key", + "eligibility_api_jwe_cek_enc", + "eligibility_api_jwe_encryption_alg", + "eligibility_api_jws_signing_alg", + "eligibility_form_class", ] else: return super().get_exclude(request, obj) @@ -79,12 +79,12 @@ def get_exclude(self, request, obj=None): def get_readonly_fields(self, request, obj=None): if not request.user.is_superuser: return [ - "api_url", "claims_provider", - "selection_label_template", - "start_template", - "unverified_template", + "eligibility_api_url", + "eligibility_start_template", + "eligibility_unverified_template", "help_template", + "selection_label_template", ] else: return super().get_readonly_fields(request, obj) diff --git a/benefits/core/migrations/0018_rename_eligibility_api_fields.py b/benefits/core/migrations/0018_rename_eligibility_api_fields.py new file mode 100644 index 000000000..6c8c41235 --- /dev/null +++ b/benefits/core/migrations/0018_rename_eligibility_api_fields.py @@ -0,0 +1,68 @@ +# Generated by Django 5.0.7 on 2024-08-01 21:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0017_refactor_authprovider_claimsprovider"), + ] + + operations = [ + migrations.RenameField( + model_name="eligibilityverifier", + old_name="api_auth_header", + new_name="eligibility_api_auth_header", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="api_auth_key_secret_name", + new_name="eligibility_api_auth_key_secret_name", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="jwe_cek_enc", + new_name="eligibility_api_jwe_cek_enc", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="jwe_encryption_alg", + new_name="eligibility_api_jwe_encryption_alg", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="jws_signing_alg", + new_name="eligibility_api_jws_signing_alg", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="public_key", + new_name="eligibility_api_public_key", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="api_url", + new_name="eligibility_api_url", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="form_class", + new_name="eligibility_form_class", + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="start_template", + new_name="eligibility_start_template", + ), + migrations.AlterField( + model_name="eligibilityverifier", + name="eligibility_start_template", + field=models.TextField(default="eligibility/start.html"), + ), + migrations.RenameField( + model_name="eligibilityverifier", + old_name="unverified_template", + new_name="eligibility_unverified_template", + ), + ] diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 526a19f22..1d6a8f32e 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -120,18 +120,10 @@ "name": "(CST) oauth claims via Login.gov", "display_order": 1, "active": true, - "api_url": null, - "api_auth_header": null, - "api_auth_key_secret_name": null, "eligibility_type": 1, - "public_key": null, - "jwe_cek_enc": null, - "jwe_encryption_alg": null, - "jws_signing_alg": null, "claims_provider": 1, "selection_label_template": "eligibility/includes/selection-label--senior.html", - "start_template": "eligibility/start--senior.html", - "form_class": null + "eligibility_start_template": "eligibility/start--senior.html" } }, { @@ -141,18 +133,10 @@ "name": "(CST) VA.gov - veteran", "display_order": 3, "active": true, - "api_url": null, - "api_auth_header": null, - "api_auth_key_secret_name": null, "eligibility_type": 2, - "public_key": null, - "jwe_cek_enc": null, - "jwe_encryption_alg": null, - "jws_signing_alg": null, "claims_provider": 2, "selection_label_template": "eligibility/includes/selection-label--veteran.html", - "start_template": "eligibility/start--veteran.html", - "form_class": null + "eligibility_start_template": "eligibility/start--veteran.html" } }, { @@ -162,19 +146,18 @@ "name": "(CST) eligibility server verifier", "display_order": 4, "active": true, - "api_url": "http://server:8000/verify", - "api_auth_header": "X-Server-API-Key", - "api_auth_key_secret_name": "agency-card-verifier-api-auth-key", + "eligibility_api_url": "http://server:8000/verify", + "eligibility_api_auth_header": "X-Server-API-Key", + "eligibility_api_auth_key_secret_name": "agency-card-verifier-api-auth-key", "eligibility_type": 3, - "public_key": 1, - "jwe_cek_enc": "A256CBC-HS512", - "jwe_encryption_alg": "RSA-OAEP", - "jws_signing_alg": "RS256", - "claims_provider": null, + "eligibility_api_public_key": 1, + "eligibility_api_jwe_cek_enc": "A256CBC-HS512", + "eligibility_api_jwe_encryption_alg": "RSA-OAEP", + "eligibility_api_jws_signing_alg": "RS256", "selection_label_template": "eligibility/includes/selection-label--cst-agency-card.html", - "start_template": "eligibility/start--cst-agency-card.html", - "form_class": "benefits.eligibility.forms.CSTAgencyCard", - "unverified_template": "eligibility/unverified--cst-agency-card.html", + "eligibility_start_template": "eligibility/start--cst-agency-card.html", + "eligibility_form_class": "benefits.eligibility.forms.CSTAgencyCard", + "eligibility_unverified_template": "eligibility/unverified--cst-agency-card.html", "help_template": "core/includes/help--cst-agency-card.html" } }, @@ -185,18 +168,10 @@ "name": "(CST) CalFresh oauth claims via Login.gov", "display_order": 2, "active": true, - "api_url": null, - "api_auth_header": null, - "api_auth_key_secret_name": null, "eligibility_type": 4, - "public_key": null, - "jwe_cek_enc": null, - "jwe_encryption_alg": null, - "jws_signing_alg": null, "claims_provider": 3, "selection_label_template": "eligibility/includes/selection-label--calfresh.html", - "start_template": "eligibility/start--calfresh.html", - "form_class": null, + "eligibility_start_template": "eligibility/start--calfresh.html", "help_template": "core/includes/help--calfresh.html" } }, diff --git a/benefits/core/models.py b/benefits/core/models.py index 397a265b1..5c343dd56 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -175,24 +175,24 @@ class EligibilityVerifier(models.Model): name = models.TextField() display_order = models.PositiveSmallIntegerField(default=0, blank=False, null=False) active = models.BooleanField(default=False) - api_url = models.TextField(null=True, blank=True) - api_auth_header = models.TextField(null=True, blank=True) - api_auth_key_secret_name = SecretNameField(null=True, blank=True) + eligibility_api_url = models.TextField(null=True, blank=True) + eligibility_api_auth_header = models.TextField(null=True, blank=True) + eligibility_api_auth_key_secret_name = SecretNameField(null=True, blank=True) eligibility_type = models.ForeignKey(EligibilityType, on_delete=models.PROTECT) # public key is used to encrypt requests targeted at this Verifier and to verify signed responses from this verifier - public_key = models.ForeignKey(PemData, related_name="+", on_delete=models.PROTECT, null=True, blank=True) + eligibility_api_public_key = models.ForeignKey(PemData, related_name="+", on_delete=models.PROTECT, null=True, blank=True) # The JWE-compatible Content Encryption Key (CEK) key-length and mode - jwe_cek_enc = models.TextField(null=True, blank=True) + eligibility_api_jwe_cek_enc = models.TextField(null=True, blank=True) # The JWE-compatible encryption algorithm - jwe_encryption_alg = models.TextField(null=True, blank=True) + eligibility_api_jwe_encryption_alg = models.TextField(null=True, blank=True) # The JWS-compatible signing algorithm - jws_signing_alg = models.TextField(null=True, blank=True) + eligibility_api_jws_signing_alg = models.TextField(null=True, blank=True) claims_provider = models.ForeignKey(ClaimsProvider, on_delete=models.PROTECT, null=True, blank=True) selection_label_template = models.TextField() - start_template = models.TextField(null=True, blank=True) + eligibility_start_template = models.TextField(default="eligibility/start.html") # reference to a form class used by this Verifier, e.g. benefits.app.forms.FormClass - form_class = models.TextField(null=True, blank=True) - unverified_template = models.TextField(default="eligibility/unverified.html") + eligibility_form_class = models.TextField(null=True, blank=True) + eligibility_unverified_template = models.TextField(default="eligibility/unverified.html") help_template = models.TextField(null=True, blank=True) class Meta: @@ -202,29 +202,29 @@ def __str__(self): return self.name @property - def api_auth_key(self): - if self.api_auth_key_secret_name is not None: - return get_secret_by_name(self.api_auth_key_secret_name) + def eligibility_api_auth_key(self): + if self.eligibility_api_auth_key_secret_name is not None: + return get_secret_by_name(self.eligibility_api_auth_key_secret_name) else: return None @property - def public_key_data(self): + def eligibility_api_public_key_data(self): """This Verifier's public key as a string.""" - return self.public_key.data + return self.eligibility_api_public_key.data @property def uses_claims_verification(self): """True if this Verifier verifies via the claims provider. False otherwise.""" return self.claims_provider is not None and self.claims_provider.supports_claims_verification - def form_instance(self, *args, **kwargs): + def eligibility_form_instance(self, *args, **kwargs): """Return an instance of this verifier's form, or None.""" - if not bool(self.form_class): + if not bool(self.eligibility_form_class): return None # inspired by https://stackoverflow.com/a/30941292 - module_name, class_name = self.form_class.rsplit(".", 1) + module_name, class_name = self.eligibility_form_class.rsplit(".", 1) FormClass = getattr(importlib.import_module(module_name), class_name) return FormClass(*args, **kwargs) diff --git a/benefits/eligibility/verify.py b/benefits/eligibility/verify.py index d70674bdd..9a4986d34 100644 --- a/benefits/eligibility/verify.py +++ b/benefits/eligibility/verify.py @@ -7,15 +7,15 @@ def eligibility_from_api(verifier, form, agency): sub, name = form.cleaned_data.get("sub"), form.cleaned_data.get("name") client = Client( - verify_url=verifier.api_url, - headers={verifier.api_auth_header: verifier.api_auth_key}, + verify_url=verifier.eligibility_api_url, + headers={verifier.eligibility_api_auth_header: verifier.eligibility_api_auth_key}, issuer=settings.ALLOWED_HOSTS[0], agency=agency.agency_id, jws_signing_alg=agency.jws_signing_alg, client_private_key=agency.private_key_data, - jwe_encryption_alg=verifier.jwe_encryption_alg, - jwe_cek_enc=verifier.jwe_cek_enc, - server_public_key=verifier.public_key_data, + jwe_encryption_alg=verifier.eligibility_api_jwe_encryption_alg, + jwe_cek_enc=verifier.eligibility_api_jwe_cek_enc, + server_public_key=verifier.eligibility_api_public_key_data, timeout=settings.REQUESTS_TIMEOUT, ) diff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py index 221312143..e094e1edf 100644 --- a/benefits/eligibility/views.py +++ b/benefits/eligibility/views.py @@ -22,7 +22,6 @@ ROUTE_UNVERIFIED = "eligibility:unverified" ROUTE_ENROLLMENT = "enrollment:index" -TEMPLATE_START = "eligibility/start.html" TEMPLATE_CONFIRM = "eligibility/confirm.html" @@ -78,9 +77,8 @@ def start(request): session.update(request, eligibility_types=[], origin=reverse(ROUTE_START)) verifier = session.verifier(request) - template = verifier.start_template or TEMPLATE_START - return TemplateResponse(request, template) + return TemplateResponse(request, verifier.eligibility_start_template) @decorator_from_middleware(AgencySessionRequired) @@ -111,7 +109,7 @@ def confirm(request): else: return redirect(unverified_view) - form = verifier.form_instance() + form = verifier.eligibility_form_instance() # GET/POST for Eligibility API verification context = {"form": form} @@ -124,7 +122,7 @@ def confirm(request): elif request.method == "POST": analytics.started_eligibility(request, types_to_verify) - form = verifier.form_instance(data=request.POST) + form = verifier.eligibility_form_instance(data=request.POST) # form was not valid, allow for correction/resubmission if not form.is_valid(): if recaptcha.has_error(form): @@ -171,4 +169,4 @@ def unverified(request): analytics.returned_fail(request, types_to_verify) - return TemplateResponse(request, verifier.unverified_template) + return TemplateResponse(request, verifier.eligibility_unverified_template) diff --git a/benefits/oauth/middleware.py b/benefits/oauth/middleware.py index 25478ce3d..69dae16b2 100644 --- a/benefits/oauth/middleware.py +++ b/benefits/oauth/middleware.py @@ -27,7 +27,7 @@ def process_request(self, request): if verifier.uses_claims_verification: # all good, the chosen verifier is configured correctly return None - elif not (verifier.api_url or verifier.form_class): + elif not (verifier.eligibility_api_url or verifier.eligibility_form_class): # the chosen verifier doesn't have Eligibility API config OR claims provider config # this is likely a misconfiguration on the backend, not a user error message = f"Verifier with no API or IDP config: {verifier.name} (id={verifier.id})" diff --git a/docs/configuration/transit-agency.md b/docs/configuration/transit-agency.md index 1a5f9471b..3fbd34289 100644 --- a/docs/configuration/transit-agency.md +++ b/docs/configuration/transit-agency.md @@ -14,10 +14,10 @@ Note that a `TransitAgency` model requires: - HTML templates for various buttons, text and other user interface elements of the flow, including: - `index_template`: _Required for agencies_ - Text for agency direct entry page - `eligibility_index_template`: _Required for agencies_ - Text for Eligibility Index page + - `selection_label_template`: _Required for verifiers_ - Text and optional modals for the radio button form on the Eligibility Index page + - `eligibility_start_template`: _Required for verifiers_ - Text and optional custom styles for call to action button on the Eligibility Start page - `enrollment_success_template`: _Required for agencies_ - Text for Enrollment Success page - `help_template`: _Required for agencies_ - Agency-specific help questions and answers - - `selection_label_template`: _Required for verifiers_ - Text and optional modals for the radio button form on the Eligibility Index page - - `start_template`: _Required for verifiers_ - Text and optional custom styles for call to action button on the Eligibility Start page - `sign_out_button_template`: _Required for claims providers_ - Sign out link button, used on any page after sign in - `sign_out_link_template`: _Required for claims providers_ - Sign out link text, used on any page after sign in diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 4e05c48c4..1cc036cc1 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -156,11 +156,11 @@ def model_EligibilityVerifier(model_PemData, model_EligibilityType): verifier = EligibilityVerifier.objects.create( name="Test Verifier", active=True, - api_url="https://example.com/verify", - api_auth_header="X-API-AUTH", - api_auth_key_secret_name="secret-key", + eligibility_api_url="https://example.com/verify", + eligibility_api_auth_header="X-API-AUTH", + eligibility_api_auth_key_secret_name="secret-key", eligibility_type=model_EligibilityType, - public_key=model_PemData, + eligibility_api_public_key=model_PemData, selection_label_template="eligibility/includes/selection-label.html", ) diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 3d6104338..f45649d5f 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -265,13 +265,18 @@ def __init__(self, *args, **kwargs): @pytest.mark.django_db -def test_EligibilityVerifier_form_instance(model_EligibilityVerifier): - model_EligibilityVerifier.form_class = f"{__name__}.SampleFormClass" +def test_EligibilityVerifier_eligibility_start_template(model_EligibilityVerifier): + assert model_EligibilityVerifier.eligibility_start_template == "eligibility/start.html" + + +@pytest.mark.django_db +def test_EligibilityVerifier_eligibility_form_instance(model_EligibilityVerifier): + model_EligibilityVerifier.eligibility_form_class = f"{__name__}.SampleFormClass" model_EligibilityVerifier.save() args = (1, "2") kwargs = {"one": 1, "two": "2"} - form_instance = model_EligibilityVerifier.form_instance(*args, **kwargs) + form_instance = model_EligibilityVerifier.eligibility_form_instance(*args, **kwargs) assert isinstance(form_instance, SampleFormClass) assert form_instance.args == args @@ -335,10 +340,10 @@ def test_EligibilityVerifier_no_ClaimsProvider(model_EligibilityVerifier): @pytest.mark.django_db -def test_EligiblityVerifier_api_auth_key(model_EligibilityVerifier, mock_models_get_secret_by_name): - secret_value = model_EligibilityVerifier.api_auth_key +def test_EligiblityVerifier_eligibility_api_auth_key(model_EligibilityVerifier, mock_models_get_secret_by_name): + secret_value = model_EligibilityVerifier.eligibility_api_auth_key - mock_models_get_secret_by_name.assert_called_once_with(model_EligibilityVerifier.api_auth_key_secret_name) + mock_models_get_secret_by_name.assert_called_once_with(model_EligibilityVerifier.eligibility_api_auth_key_secret_name) assert secret_value == mock_models_get_secret_by_name.return_value diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py index 904ee80fe..b2a6c82a4 100644 --- a/tests/pytest/eligibility/test_views.py +++ b/tests/pytest/eligibility/test_views.py @@ -71,7 +71,7 @@ def __init__(self, *args, **kwargs): @pytest.fixture def model_EligibilityVerifier_with_form_class(mocker, model_EligibilityVerifier): - model_EligibilityVerifier.form_class = f"{__name__}.SampleVerificationForm" + model_EligibilityVerifier.eligibility_form_class = f"{__name__}.SampleVerificationForm" model_EligibilityVerifier.save() mocker.patch("benefits.eligibility.views.session.verifier", return_value=model_EligibilityVerifier) return model_EligibilityVerifier diff --git a/tests/pytest/oauth/test_middleware_authverifier_required.py b/tests/pytest/oauth/test_middleware_authverifier_required.py index ec4106911..ae78f4ba5 100644 --- a/tests/pytest/oauth/test_middleware_authverifier_required.py +++ b/tests/pytest/oauth/test_middleware_authverifier_required.py @@ -56,8 +56,8 @@ def test_authverifier_required_misconfigured_verifier( form_class, ): # fake a misconfigured verifier - mocked_session_verifier_does_not_use_claims_verification.return_value.api_url = api_url - mocked_session_verifier_does_not_use_claims_verification.return_value.form_class = form_class + mocked_session_verifier_does_not_use_claims_verification.return_value.eligibility_api_url = api_url + mocked_session_verifier_does_not_use_claims_verification.return_value.eligibility_form_class = form_class response = decorated_view(app_request)