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

Feat: process multiple claims on a flow #2463

Merged
merged 3 commits into from
Oct 28, 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
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
thekaveman marked this conversation as resolved.
Show resolved Hide resolved

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
Loading