Skip to content

Commit

Permalink
Feat: process multiple claims on a flow (#2463)
Browse files Browse the repository at this point in the history
  • Loading branch information
lalver1 authored Oct 28, 2024
2 parents 79a2234 + c0a2516 commit 9ed721c
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 56 deletions.
30 changes: 30 additions & 0 deletions benefits/core/migrations/0029_add_extra_claims.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.1.1 on 2024-10-18 19:38

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0028_remove_transitagency_enrollment_flows_and_more"),
]

operations = [
migrations.RenameField(
model_name="enrollmentflow",
old_name="claims_claim",
new_name="claims_eligibility_claim",
),
migrations.AddField(
model_name="enrollmentflow",
name="claims_extra_claims",
field=models.TextField(blank=True, help_text="A space-separated list of any additional claims", null=True),
),
migrations.AlterField(
model_name="enrollmentflow",
name="claims_eligibility_claim",
field=models.TextField(
blank=True, help_text="The name of the claim that is used to verify eligibility", null=True
),
),
]
8 changes: 4 additions & 4 deletions benefits/core/migrations/local_fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"selection_label_template": "eligibility/includes/selection-label--senior.html",
"eligibility_start_template": "eligibility/start--senior.html",
"claims_scope": "verify:senior",
"claims_claim": "senior",
"claims_eligibility_claim": "senior",
"supported_enrollment_methods": ["digital", "in_person"],
"transit_agency": 1
}
Expand All @@ -103,7 +103,7 @@
"selection_label_template": "eligibility/includes/selection-label--veteran.html",
"eligibility_start_template": "eligibility/start--veteran.html",
"claims_scope": "verify:veteran",
"claims_claim": "veteran",
"claims_eligibility_claim": "veteran",
"supported_enrollment_methods": ["digital", "in_person"],
"transit_agency": 1
}
Expand Down Expand Up @@ -152,7 +152,7 @@
"eligibility_start_template": "eligibility/start--calfresh.html",
"help_template": "core/includes/help--calfresh.html",
"claims_scope": "verify:calfresh",
"claims_claim": "calfresh",
"claims_eligibility_claim": "calfresh",
"supported_enrollment_methods": ["digital", "in_person"],
"transit_agency": 1
}
Expand All @@ -171,7 +171,7 @@
"eligibility_start_template": "eligibility/start--medicare.html",
"help_template": "core/includes/help--medicare.html",
"claims_scope": "verify:medicare",
"claims_claim": "medicare",
"claims_eligibility_claim": "medicare",
"supported_enrollment_methods": ["digital", "in_person"],
"transit_agency": 1
}
Expand Down
14 changes: 11 additions & 3 deletions benefits/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,10 @@ class EnrollmentFlow(models.Model):
blank=True,
help_text="A space-separated list of identifiers used to specify what access privileges are being requested",
)
claims_claim = models.TextField(
null=True, blank=True, help_text="The name of the claim (name/value pair) that is used to verify eligibility"
claims_eligibility_claim = models.TextField(
null=True, blank=True, help_text="The name of the claim that is used to verify eligibility"
)
claims_extra_claims = models.TextField(null=True, blank=True, help_text="A space-separated list of any additional claims")
claims_scheme_override = models.TextField(
help_text="The authentication scheme to use (Optional). If blank, defaults to the value in Claims providers",
default=None,
Expand Down Expand Up @@ -404,7 +405,7 @@ def eligibility_api_public_key_data(self):
@property
def uses_claims_verification(self):
"""True if this flow verifies via the claims provider and has a scope and claim. False otherwise."""
return self.claims_provider is not None and bool(self.claims_scope) and bool(self.claims_claim)
return self.claims_provider is not None and bool(self.claims_scope) and bool(self.claims_eligibility_claim)

@property
def eligibility_verifier(self):
Expand Down Expand Up @@ -459,6 +460,13 @@ def claims_scheme(self):
return self.claims_provider.scheme
return self.claims_scheme_override

@property
def claims_all_claims(self):
claims = [self.claims_eligibility_claim]
if self.claims_extra_claims is not None:
claims.extend(self.claims_extra_claims.split())
return claims


class EnrollmentEvent(models.Model):
"""A record of a successful enrollment."""
Expand Down
18 changes: 9 additions & 9 deletions benefits/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
_ENROLLMENT_EXP = "enrollment_expiry"
_FLOW = "flow"
_LANG = "lang"
_OAUTH_CLAIM = "oauth_claim"
_OAUTH_CLAIMS = "oauth_claims"
_OAUTH_TOKEN = "oauth_token"
_ORIGIN = "origin"
_START = "start"
Expand Down Expand Up @@ -60,7 +60,7 @@ def context_dict(request):
_ENROLLMENT_TOKEN_EXP: enrollment_token_expiry(request),
_LANG: language(request),
_OAUTH_TOKEN: oauth_token(request),
_OAUTH_CLAIM: oauth_claim(request),
_OAUTH_CLAIMS: oauth_claims(request),
_ORIGIN: origin(request),
_START: start(request),
_UID: uid(request),
Expand Down Expand Up @@ -148,17 +148,17 @@ def logged_in(request):

def logout(request):
"""Reset the session claims and tokens."""
update(request, oauth_claim=False, oauth_token=False, enrollment_token=False)
update(request, oauth_claims=[], oauth_token=False, enrollment_token=False)


def oauth_token(request):
"""Get the oauth token from the request's session, or None"""
return request.session.get(_OAUTH_TOKEN)


def oauth_claim(request):
def oauth_claims(request):
"""Get the oauth claim from the request's session, or None"""
return request.session.get(_OAUTH_CLAIM)
return request.session.get(_OAUTH_CLAIMS)


def origin(request):
Expand All @@ -177,7 +177,7 @@ def reset(request):
request.session[_ENROLLMENT_TOKEN] = None
request.session[_ENROLLMENT_TOKEN_EXP] = None
request.session[_OAUTH_TOKEN] = None
request.session[_OAUTH_CLAIM] = None
request.session[_OAUTH_CLAIMS] = None

if _UID not in request.session or not request.session[_UID]:
logger.debug("Reset session time and uid")
Expand Down Expand Up @@ -236,7 +236,7 @@ def update(
enrollment_token=None,
enrollment_token_exp=None,
oauth_token=None,
oauth_claim=None,
oauth_claims=None,
origin=None,
):
"""Update the request's session with non-null values."""
Expand All @@ -260,8 +260,8 @@ def update(
request.session[_ENROLLMENT_TOKEN_EXP] = enrollment_token_exp
if oauth_token is not None:
request.session[_OAUTH_TOKEN] = oauth_token
if oauth_claim is not None:
request.session[_OAUTH_CLAIM] = oauth_claim
if oauth_claims is not None:
request.session[_OAUTH_CLAIMS] = oauth_claims
if origin is not None:
request.session[_ORIGIN] = origin
if flow is not None and isinstance(flow, models.EnrollmentFlow):
Expand Down
4 changes: 2 additions & 2 deletions benefits/eligibility/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def eligibility_from_api(flow: models.EnrollmentFlow, form, agency: models.Trans
return False


def eligibility_from_oauth(flow: models.EnrollmentFlow, oauth_claim, agency: models.TransitAgency):
if flow.uses_claims_verification and flow.claims_claim == oauth_claim:
def eligibility_from_oauth(flow: models.EnrollmentFlow, oauth_claims, agency: models.TransitAgency):
if flow.uses_claims_verification and flow.claims_eligibility_claim in oauth_claims:
return True
else:
return False
2 changes: 1 addition & 1 deletion benefits/eligibility/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def confirm(request):
if request.method == "GET" and flow.uses_claims_verification:
analytics.started_eligibility(request, flow)

is_verified = verify.eligibility_from_oauth(flow, session.oauth_claim(request), agency)
is_verified = verify.eligibility_from_oauth(flow, session.oauth_claims(request), agency)

if is_verified:
return verified(request)
Expand Down
32 changes: 17 additions & 15 deletions benefits/oauth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,27 +123,29 @@ def authorize(request):
id_token = token["id_token"]

# We store the returned claim in case it can be used later in eligibility verification.
flow_claim = flow.claims_claim
stored_claim = None
flow_claims = flow.claims_all_claims
stored_claims = []

error_claim = None

if flow_claim:
if flow_claims:
userinfo = token.get("userinfo")

if userinfo:
claim_value = userinfo.get(flow_claim)
# the claim comes back in userinfo like { "claim": "1" | "0" }
claim_value = int(claim_value) if claim_value else None
if claim_value is None:
logger.warning(f"userinfo did not contain: {flow_claim}")
elif claim_value == 1:
# if userinfo contains our claim and the flag is 1 (true), store the *claim*
stored_claim = flow_claim
elif claim_value >= 10:
error_claim = claim_value

session.update(request, oauth_token=id_token, oauth_claim=stored_claim)
for claim in flow_claims:
claim_value = userinfo.get(claim)
# the claim comes back in userinfo like { "claim": "1" | "0" }
claim_value = int(claim_value) if claim_value else None
if claim_value is None:
logger.warning(f"userinfo did not contain: {claim}")
elif claim_value == 1:
# if userinfo contains our claim and the flag is 1 (true), store the *claim*
stored_claims.append(claim)
elif claim_value >= 10 and claim == flow.claims_eligibility_claim:
# error_claim is only set if claim is the eligibility claim
error_claim = claim_value

session.update(request, oauth_token=id_token, oauth_claims=stored_claims)
analytics.finished_sign_in(request, error=error_claim)

return redirect(routes.ELIGIBILITY_CONFIRM)
Expand Down
2 changes: 1 addition & 1 deletion tests/pytest/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def model_EnrollmentFlow_with_eligibility_api(model_EnrollmentFlow, model_PemDat
def model_EnrollmentFlow_with_scope_and_claim(model_EnrollmentFlow, model_ClaimsProvider):
model_EnrollmentFlow.claims_provider = model_ClaimsProvider
model_EnrollmentFlow.claims_scope = "scope"
model_EnrollmentFlow.claims_claim = "claim"
model_EnrollmentFlow.claims_eligibility_claim = "claim"
model_EnrollmentFlow.save()

return model_EnrollmentFlow
Expand Down
15 changes: 15 additions & 0 deletions tests/pytest/core/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,21 @@ def test_EnrollmentFlow_supported_enrollment_methods(model_TransitAgency):
assert new_flow.supported_enrollment_methods == ["digital", "in_person"]


@pytest.mark.django_db
@pytest.mark.parametrize(
"extra_claims,all_claims",
[
(None, ["claim"]),
("extra_claim", ["claim", "extra_claim"]),
("extra_claim_1 extra_claim_2", ["claim", "extra_claim_1", "extra_claim_2"]),
],
)
def test_EnrollmentFlow_claims_all_claims(model_EnrollmentFlow_with_scope_and_claim, extra_claims, all_claims):
model_EnrollmentFlow_with_scope_and_claim.claims_extra_claims = extra_claims
model_EnrollmentFlow_with_scope_and_claim.save()
assert model_EnrollmentFlow_with_scope_and_claim.claims_all_claims == all_claims


class SampleFormClass:
"""A class for testing EligibilityVerificationForm references."""

Expand Down
10 changes: 5 additions & 5 deletions tests/pytest/core/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,16 @@ def test_logged_in_True(app_request):

@pytest.mark.django_db
def test_logout(app_request):
session.update(app_request, oauth_claim="oauth_claim", oauth_token="oauth_token", enrollment_token="enrollment_token")
session.update(app_request, oauth_claims=["oauth_claim"], oauth_token="oauth_token", enrollment_token="enrollment_token")
assert session.logged_in(app_request)
assert session.oauth_claim(app_request)
assert session.oauth_claims(app_request)

session.logout(app_request)

assert not session.logged_in(app_request)
assert not session.enrollment_token(app_request)
assert not session.oauth_token(app_request)
assert not session.oauth_claim(app_request)
assert not session.oauth_claims(app_request)


@pytest.mark.django_db
Expand Down Expand Up @@ -269,12 +269,12 @@ def test_reset_enrollment(app_request):
@pytest.mark.django_db
def test_reset_oauth(app_request):
app_request.session[session._OAUTH_TOKEN] = "oauthtoken456"
app_request.session[session._OAUTH_CLAIM] = "claim"
app_request.session[session._OAUTH_CLAIMS] = ["claim"]

session.reset(app_request)

assert session.oauth_token(app_request) is None
assert session.oauth_claim(app_request) is None
assert session.oauth_claims(app_request) is None


@pytest.mark.django_db
Expand Down
10 changes: 5 additions & 5 deletions tests/pytest/eligibility/test_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_eligibility_from_oauth_does_not_use_claims_verification(
# mocked_session_flow_does_not_use_claims_verification is Mocked version of the session.flow() function
flow = mocked_session_flow_does_not_use_claims_verification.return_value

response = eligibility_from_oauth(flow, "claim", model_TransitAgency)
response = eligibility_from_oauth(flow, ["claim"], model_TransitAgency)

assert response is False

Expand All @@ -69,9 +69,9 @@ def test_eligibility_from_oauth_does_not_use_claims_verification(
def test_eligibility_from_oauth_claim_mismatch(mocked_session_flow_uses_claims_verification, model_TransitAgency):
# mocked_session_flow_uses_claims_verification is Mocked version of the session.flow() function
flow = mocked_session_flow_uses_claims_verification.return_value
flow.claims_claim = "claim"
flow.claims_eligibility_claim = "claim"

response = eligibility_from_oauth(flow, "some_other_claim", model_TransitAgency)
response = eligibility_from_oauth(flow, ["some_other_claim"], model_TransitAgency)

assert response is False

Expand All @@ -80,8 +80,8 @@ def test_eligibility_from_oauth_claim_mismatch(mocked_session_flow_uses_claims_v
def test_eligibility_from_oauth_claim_match(mocked_session_flow_uses_claims_verification, model_TransitAgency):
# mocked_session_flow_uses_claims_verification is Mocked version of the session.flow() function
flow = mocked_session_flow_uses_claims_verification.return_value
flow.claims_claim = "claim"
flow.claims_eligibility_claim = "claim"

response = eligibility_from_oauth(flow, "claim", model_TransitAgency)
response = eligibility_from_oauth(flow, ["claim"], model_TransitAgency)

assert response is True
Loading

0 comments on commit 9ed721c

Please sign in to comment.