Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: TransitAgency Eligibility API fields #2280

Merged
merged 4 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading