Skip to content

Commit

Permalink
Refactor: TransitAgency Eligibility API fields (#2280)
Browse files Browse the repository at this point in the history
  • Loading branch information
thekaveman authored Aug 7, 2024
2 parents 35d8fce + 290c208 commit 95d915b
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 52 deletions.
8 changes: 4 additions & 4 deletions benefits/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ class TransitAgencyAdmin(admin.ModelAdmin): # pragma: no cover
def get_exclude(self, request, obj=None):
if not request.user.is_superuser:
return [
"private_key",
"public_key",
"jws_signing_alg",
"eligibility_api_private_key",
"eligibility_api_public_key",
"eligibility_api_jws_signing_alg",
"transit_processor_client_id",
"transit_processor_client_secret_name",
"transit_processor_audience",
Expand All @@ -127,7 +127,7 @@ def get_exclude(self, request, obj=None):
def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return [
"agency_id",
"eligibility_api_id",
"transit_processor",
"index_template",
"eligibility_index_template",
Expand Down
94 changes: 94 additions & 0 deletions benefits/core/migrations/0019_refactor_transitagency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Generated by Django 5.0.7 on 2024-08-06 19:14

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0018_rename_eligibility_api_fields"),
]

operations = [
migrations.RenameField(model_name="transitagency", old_name="agency_id", new_name="eligibility_api_id"),
migrations.RenameField(
model_name="transitagency", old_name="jws_signing_alg", new_name="eligibility_api_jws_signing_alg"
),
migrations.RenameField(model_name="transitagency", old_name="private_key", new_name="eligibility_api_private_key"),
migrations.RenameField(model_name="transitagency", old_name="public_key", new_name="eligibility_api_public_key"),
migrations.AlterField(
model_name="transitagency",
name="active",
field=models.BooleanField(default=False, help_text="Determines if this Agency is enabled for users"),
),
migrations.AlterField(
model_name="transitagency",
name="eligibility_api_id",
field=models.TextField(help_text="The identifier for this agency used in Eligibility API calls."),
),
migrations.AlterField(
model_name="transitagency",
name="eligibility_api_jws_signing_alg",
field=models.TextField(help_text="The JWS-compatible signing algorithm used in Eligibility API calls."),
),
migrations.AlterField(
model_name="transitagency",
name="eligibility_api_private_key",
field=models.ForeignKey(
help_text="Private key used to sign Eligibility API tokens created on behalf of this Agency.",
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="core.pemdata",
),
),
migrations.AlterField(
model_name="transitagency",
name="eligibility_api_public_key",
field=models.ForeignKey(
help_text="Public key corresponding to the agency's private key, used by Eligibility Verification servers to encrypt responses.", # noqa: E501
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="core.pemdata",
),
),
migrations.AlterField(
model_name="transitagency",
name="eligibility_index_template",
field=models.TextField(help_text="The template used for this agency's eligibility landing page"),
),
migrations.AlterField(
model_name="transitagency",
name="index_template",
field=models.TextField(help_text="The template used for this agency's landing page"),
),
migrations.AlterField(
model_name="transitagency",
name="info_url",
field=models.URLField(help_text="URL of a website/page with more information about the agency's discounts"),
),
migrations.AlterField(
model_name="transitagency",
name="long_name",
field=models.TextField(
help_text="The user-facing long name for this agency. Often the short_name acronym, spelled out."
),
),
migrations.AlterField(
model_name="transitagency",
name="phone",
field=models.TextField(help_text="Agency customer support phone number"),
),
migrations.AlterField(
model_name="transitagency",
name="short_name",
field=models.TextField(help_text="The user-facing short name for this agency. Often an uppercase acronym."),
),
migrations.AlterField(
model_name="transitagency",
name="slug",
field=models.TextField(
help_text="Used for URL navigation for this agency, e.g. the agency homepage url is /{slug}"
),
),
]
22 changes: 11 additions & 11 deletions benefits/core/migrations/local_fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,24 +190,24 @@
"model": "core.transitagency",
"pk": 1,
"fields": {
"active": true,
"eligibility_types": [1, 2, 3, 4],
"eligibility_verifiers": [1, 2, 3, 4],
"slug": "cst",
"short_name": "CST (local)",
"long_name": "California State Transit (local)",
"agency_id": "cst",
"info_url": "https://www.agency-website.com",
"phone": "1-800-555-5555",
"active": true,
"transit_processor": 1,
"transit_processor_client_id": "",
"transit_processor_client_secret_name": "cst-transit-processor-client-secret",
"transit_processor_audience": "",
"private_key": 2,
"public_key": 3,
"jws_signing_alg": "RS256",
"index_template": "core/index--cst.html",
"eligibility_index_template": "eligibility/index--cst.html",
"eligibility_types": [1, 2, 3, 4],
"eligibility_verifiers": [1, 2, 3, 4]
"eligibility_api_id": "cst",
"eligibility_api_private_key": 2,
"eligibility_api_public_key": 3,
"eligibility_api_jws_signing_alg": "RS256",
"transit_processor": 1,
"transit_processor_audience": "",
"transit_processor_client_id": "",
"transit_processor_client_secret_name": "cst-transit-processor-client-secret"
}
}
]
57 changes: 34 additions & 23 deletions benefits/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,34 +258,45 @@ class TransitAgency(models.Model):
"""An agency offering transit service."""

id = models.AutoField(primary_key=True)
slug = models.TextField()
short_name = models.TextField()
long_name = models.TextField()
agency_id = models.TextField()
info_url = models.URLField()
phone = models.TextField()
active = models.BooleanField(default=False)
active = models.BooleanField(default=False, help_text="Determines if this Agency is enabled for users")
eligibility_types = models.ManyToManyField(EligibilityType)
eligibility_verifiers = models.ManyToManyField(EligibilityVerifier)
slug = models.TextField(help_text="Used for URL navigation for this agency, e.g. the agency homepage url is /{slug}")
short_name = models.TextField(help_text="The user-facing short name for this agency. Often an uppercase acronym.")
long_name = models.TextField(
help_text="The user-facing long name for this agency. Often the short_name acronym, spelled out."
)
info_url = models.URLField(help_text="URL of a website/page with more information about the agency's discounts")
phone = models.TextField(help_text="Agency customer support phone number")
index_template = models.TextField(help_text="The template used for this agency's landing page")
eligibility_index_template = models.TextField(help_text="The template used for this agency's eligibility landing page")
eligibility_api_id = models.TextField(help_text="The identifier for this agency used in Eligibility API calls.")
eligibility_api_private_key = models.ForeignKey(
PemData,
related_name="+",
on_delete=models.PROTECT,
help_text="Private key used to sign Eligibility API tokens created on behalf of this Agency.",
)
eligibility_api_public_key = models.ForeignKey(
PemData,
related_name="+",
on_delete=models.PROTECT,
help_text="Public key corresponding to the agency's private key, used by Eligibility Verification servers to encrypt responses.", # noqa: E501
)
eligibility_api_jws_signing_alg = models.TextField(
help_text="The JWS-compatible signing algorithm used in Eligibility API calls."
)
transit_processor = models.ForeignKey(TransitProcessor, on_delete=models.PROTECT)
transit_processor_audience = models.TextField(
help_text="This agency's audience value used to access the TransitProcessor's API.", default=""
)
transit_processor_client_id = models.TextField(
help_text="This agency's client_id value used to access the TransitProcessor's API.", default=""
)
transit_processor_client_secret_name = SecretNameField(
help_text="The name of the secret containing this agency's client_secret value used to access the TransitProcessor's API.", # noqa: E501
default="",
)
transit_processor_audience = models.TextField(
help_text="This agency's audience value used to access the TransitProcessor's API.", default=""
)
# The Agency's private key, used to sign tokens created on behalf of this Agency
private_key = models.ForeignKey(PemData, related_name="+", on_delete=models.PROTECT)
# The public key corresponding to the Agency's private key, used by Eligibility Verification servers to encrypt responses
public_key = models.ForeignKey(PemData, related_name="+", on_delete=models.PROTECT)
# The JWS-compatible signing algorithm
jws_signing_alg = models.TextField()
index_template = models.TextField()
eligibility_index_template = models.TextField()

def __str__(self):
return self.long_name
Expand Down Expand Up @@ -325,19 +336,19 @@ def eligibility_index_url(self):
return reverse("eligibility:agency_index", args=[self.slug])

@property
def public_key_url(self):
def eligibility_api_public_key_url(self):
"""Public-facing URL to the TransitAgency's public key."""
return reverse("core:agency_public_key", args=[self.slug])

@property
def private_key_data(self):
def eligibility_api_private_key_data(self):
"""This Agency's private key as a string."""
return self.private_key.data
return self.eligibility_api_private_key.data

@property
def public_key_data(self):
def eligibility_api_public_key_data(self):
"""This Agency's public key as a string."""
return self.public_key.data
return self.eligibility_api_public_key.data

@property
def active_verifiers(self):
Expand Down
8 changes: 4 additions & 4 deletions benefits/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.template import loader
from django.template.response import TemplateResponse

from . import session
from . import models, session
from .middleware import pageview_decorator, index_or_agencyindex_origin_decorator

ROUTE_ELIGIBILITY = "eligibility:index"
Expand All @@ -33,7 +33,7 @@ def index(request):


@pageview_decorator
def agency_index(request, agency):
def agency_index(request, agency: models.TransitAgency):
"""View handler for an agency entry page."""
session.reset(request)
session.update(request, agency=agency, origin=agency.index_url)
Expand All @@ -42,9 +42,9 @@ def agency_index(request, agency):


@pageview_decorator
def agency_public_key(request, agency):
def agency_public_key(request, agency: models.TransitAgency):
"""View handler returns an agency's public key as plain text."""
return HttpResponse(agency.public_key_data, content_type="text/plain")
return HttpResponse(agency.eligibility_api_public_key_data, content_type="text/plain")


@pageview_decorator
Expand Down
10 changes: 6 additions & 4 deletions benefits/eligibility/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

from eligibility_api.client import Client

from benefits.core import models

def eligibility_from_api(verifier, form, agency):

def eligibility_from_api(verifier: models.EligibilityVerifier, form, agency: models.TransitAgency):
sub, name = form.cleaned_data.get("sub"), form.cleaned_data.get("name")

client = Client(
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,
agency=agency.eligibility_api_id,
jws_signing_alg=agency.eligibility_api_jws_signing_alg,
client_private_key=agency.eligibility_api_private_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,
Expand Down
8 changes: 4 additions & 4 deletions tests/pytest/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,17 @@ def model_TransitAgency(model_PemData, model_EligibilityType, model_EligibilityV
slug="test",
short_name="TEST",
long_name="Test Transit Agency",
agency_id="test123",
info_url="https://example.com/test-agency",
phone="800-555-5555",
active=True,
transit_processor=model_TransitProcessor,
transit_processor_client_id="client_id",
transit_processor_client_secret_name="client_secret_name",
transit_processor_audience="audience",
private_key=model_PemData,
public_key=model_PemData,
jws_signing_alg="alg",
eligibility_api_id="test123",
eligibility_api_private_key=model_PemData,
eligibility_api_public_key=model_PemData,
eligibility_api_jws_signing_alg="alg",
index_template="core/agency-index.html",
eligibility_index_template="eligibility/index.html",
)
Expand Down
4 changes: 2 additions & 2 deletions tests/pytest/core/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ def test_agency_index_multiple_verifier(

@pytest.mark.django_db
def test_agency_public_key(client, model_TransitAgency):
response = client.get(model_TransitAgency.public_key_url)
response = client.get(model_TransitAgency.eligibility_api_public_key_url)

assert response.status_code == 200
assert response.headers["Content-Type"] == "text/plain"
assert response.content.decode("utf-8") == model_TransitAgency.public_key_data
assert response.content.decode("utf-8") == model_TransitAgency.eligibility_api_public_key_data


@pytest.mark.django_db
Expand Down

0 comments on commit 95d915b

Please sign in to comment.