Skip to content

Commit 70f4826

Browse files
committed
Incremental, adding impl, tests.
1 parent 00a0be6 commit 70f4826

File tree

5 files changed

+139
-9
lines changed

5 files changed

+139
-9
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
2+
from django.conf import settings
3+
from django.test.client import Client
4+
from django.urls import reverse
5+
from httmock import all_requests, HTTMock
6+
from http import HTTPStatus
7+
from oauth2_provider.models import get_access_token_model
8+
9+
from apps.test import BaseApiTest
10+
11+
# Get the pre-defined Conformance statement
12+
from waffle.testutils import override_switch
13+
14+
AccessToken = get_access_token_model()
15+
16+
FHIR_ID_V2 = settings.DEFAULT_SAMPLE_FHIR_ID_V2
17+
18+
POSSIBLE_COVERAGE_SCOPES = ['patient/Coverage.read', 'patient/Coverage.rs', 'patient/Coverage.s']
19+
POSSIBLE_PATIENT_SCOPES = ['patient/Patient.read', 'patient/Patient.rs', 'patient/Patient.r']
20+
21+
22+
class InsuranceCardTest(BaseApiTest):
23+
TEST_TABLE = []
24+
# Add all of the passing combos. These should be 200s.
25+
for C in POSSIBLE_COVERAGE_SCOPES:
26+
for P in POSSIBLE_PATIENT_SCOPES:
27+
TEST_TABLE.append({'status': HTTPStatus.OK, 'scope': [C, P]})
28+
# If we only have one, those should fail with a 403.
29+
for C in POSSIBLE_COVERAGE_SCOPES:
30+
TEST_TABLE.append({'status': HTTPStatus.FORBIDDEN, 'scope': [C]})
31+
for P in POSSIBLE_PATIENT_SCOPES:
32+
TEST_TABLE.append({'status': HTTPStatus.FORBIDDEN, 'scope': [P]})
33+
34+
def setUp(self):
35+
# create read and write capabilities
36+
self.read_capability = self._create_capability('Read', [])
37+
self.write_capability = self._create_capability('Write', [])
38+
self.client = Client()
39+
40+
@override_switch('v3_endpoints', active=True)
41+
def test_scope_combinations(self):
42+
for tt in InsuranceCardTest.TEST_TABLE:
43+
with self.subTest(tt=tt):
44+
token = self.create_token('Annie', 'User', fhir_id_v2=FHIR_ID_V2)
45+
ac = AccessToken.objects.get(token=token)
46+
ac.scope = " ".join(tt['scope'])
47+
ac.save()
48+
49+
@all_requests
50+
def catchall(url, req):
51+
return {
52+
'status_code': 200,
53+
'content': {
54+
'doesnot': 'matter',
55+
}
56+
}
57+
with HTTMock(catchall):
58+
response = self.client.get(
59+
reverse('bb_oauth_fhir_dic_read'),
60+
Authorization='Bearer %s' % (token)
61+
)
62+
self.assertEqual(response.status_code, tt['status'])

apps/fhir/bluebutton/v3/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
SearchViewExplanationOfBenefit,
1212
)
1313
from apps.fhir.bluebutton.views.patient_viewset import PatientViewSet
14-
from apps.fhir.bluebutton.views.insurancecard_viewset import DigitalInsuranceCardViewSet
14+
from apps.fhir.bluebutton.views.insurancecard import DigitalInsuranceCardView
1515

1616
admin.autodiscover()
1717

@@ -59,7 +59,7 @@
5959
# application, which means we're kinda breaking REST principles here.
6060
re_path(
6161
r'DigitalInsuranceCard[/]?',
62-
waffle_switch('v3_endpoints')(DigitalInsuranceCardViewSet.as_view({'get': 'list'}, version=3)),
62+
waffle_switch('v3_endpoints')(DigitalInsuranceCardView.as_view(version=3)),
6363
name='bb_oauth_fhir_dic_read',
6464
),
6565
]

apps/fhir/bluebutton/views/insurancecard.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
from waffle import switch_is_active
12
from apps.fhir.bluebutton.views.generic import FhirDataView
23
from apps.fhir.bluebutton.permissions import (SearchCrosswalkPermission,
34
ResourcePermission,
45
ApplicationActivePermission)
56
from apps.authorization.permissions import DataAccessGrantPermission
67
from apps.capabilities.permissions import TokenHasProtectedCapability
8+
from django.http import JsonResponse
79

810
from rest_framework import permissions # pyright: ignore[reportMissingImports]
911

1012

11-
def _is_not_empty(s: set):
13+
def _is_not_empty(s: set) -> bool:
1214
if len(s) > 0:
1315
return True
1416
else:
@@ -21,23 +23,41 @@ class HasDigitalInsuranceCardScope(permissions.BasePermission):
2123
required_patient_read_scopes = ['patient/Patient.r', 'patient/Patient.rs', 'patient/Patient.read']
2224

2325
def has_permission(self, request, view) -> bool: # type: ignore
26+
print("HasDigitalInsuranceCardScope has_permission")
27+
2428
# Is this an authorized request? If not, exit.
25-
if request.get('auth', None) is None:
29+
if not hasattr(request, 'auth'):
30+
return False
31+
if request.auth is None:
2632
return False
2733

2834
# If we're authenticated, then we can check the scopes from the token.
29-
token_scopes = request.auth.scope
35+
token_scope_string = request.auth.scope
36+
# This will be a space-separated string.
37+
token_scopes = list(map(lambda s: s.strip(), token_scope_string.split(" ")))
38+
3039
# Two things need to be true:
3140
# 1. At least one of the scopes in the token needs to be one of the above coverage scopes.
3241
# 2. At leaset one of the scopes in the token needs to be one of the above read scopes.
3342
coverage_set = set(HasDigitalInsuranceCardScope.required_coverage_search_scopes)
3443
patient_set = set(HasDigitalInsuranceCardScope.required_patient_read_scopes)
3544
token_set = set(token_scopes)
45+
46+
# print()
47+
# print("CS", coverage_set)
48+
# print("PS", patient_set)
49+
# print("TS", token_set)
50+
3651
return (_is_not_empty(coverage_set.intersection(token_set))
3752
and _is_not_empty(patient_set.intersection(token_set)))
3853

3954

40-
class DigitalInsuranceCardSearchView(FhirDataView):
55+
class WaffleSwitchV3IsActive(permissions.BasePermission):
56+
def has_permission(self, request, view):
57+
return switch_is_active('v3_endpoints')
58+
59+
60+
class DigitalInsuranceCardView(FhirDataView):
4161
'''Digital Insurance Card view for handling BFD Endpoint'''
4262

4363
permission_classes = [
@@ -59,7 +79,20 @@ def __init__(self, version=1):
5979
super().__init__(version)
6080
self.resource_type = 'Bundle'
6181

82+
def initial(self, request, *args, **kwargs):
83+
return super().initial(request, self.resource_type, *args, **kwargs)
84+
85+
def get(self, request, *args, **kwargs):
86+
# return super().get(request, self.resource_type, *args, **kwargs)
87+
return JsonResponse(status=200, data={"ok": "go"})
88+
89+
# How do the has_permission herre and the has_permission in the permission classes
90+
# play together? If they pass, can this fail? Visa-versa?
91+
6292
def has_permission(self, request, view):
93+
# TODO: Why is this not being called?
94+
# A print statement where this comment is does not appear when unit tests are run.
95+
# But, the permission classes run. Where/when does has_permission get called?
6396
required_scopes = getattr(view, 'required_scopes', None)
6497
if required_scopes is None:
6598
return False

apps/fhir/bluebutton/views/insurancecard_viewset.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,36 @@
99
ApplicationActivePermission,
1010
)
1111

12+
from rest_framework.response import Response
13+
14+
15+
def _is_not_empty(s: set) -> bool:
16+
if len(s) > 0:
17+
return True
18+
else:
19+
return False
20+
1221

1322
class HasDigitalInsuranceCardScope(permissions.BasePermission):
23+
24+
required_coverage_search_scopes = ['patient/Coverage.rs', 'patient/Coverage.s', 'patient/Coverage.read']
25+
required_patient_read_scopes = ['patient/Patient.r', 'patient/Patient.rs', 'patient/Patient.read']
26+
1427
def has_permission(self, request, view) -> bool: # type: ignore
15-
# TODO - implement scope checking logic
16-
return True
28+
# Is this an authorized request? If not, exit.
29+
if request.GET.get('auth', None) is None:
30+
return False
31+
32+
# If we're authenticated, then we can check the scopes from the token.
33+
token_scopes = request.auth.scope
34+
# Two things need to be true:
35+
# 1. At least one of the scopes in the token needs to be one of the above coverage scopes.
36+
# 2. At leaset one of the scopes in the token needs to be one of the above read scopes.
37+
coverage_set = set(HasDigitalInsuranceCardScope.required_coverage_search_scopes)
38+
patient_set = set(HasDigitalInsuranceCardScope.required_patient_read_scopes)
39+
token_set = set(token_scopes)
40+
return (_is_not_empty(coverage_set.intersection(token_set))
41+
and _is_not_empty(patient_set.intersection(token_set)))
1742

1843

1944
class DigitalInsuranceCardViewSet(ResourceViewSet):
@@ -44,6 +69,9 @@ def __init__(self, version, **kwargs):
4469
def initial(self, request, *args, **kwargs):
4570
return super().initial(request, self.resource_type, *args, **kwargs)
4671

72+
def list(self, request, resource_id, *args, **kwargs):
73+
return Response({"ok": "go"})
74+
4775
def build_url(self, fhir_settings, resource_type, resource_id=None, *args, **kwargs):
4876
if fhir_settings.fhir_url.endswith('v1/fhir/'):
4977
# only if called by tests

apps/test.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ def _create_user(
7474
"""
7575
fhir_id_v2 = fhir_id_v2 or settings.DEFAULT_SAMPLE_FHIR_ID_V2
7676
fhir_id_v3 = fhir_id_v3 or settings.DEFAULT_SAMPLE_FHIR_ID_V3
77+
78+
# Some of our tests might create users more than once in the DB.
79+
# Just return them if they exist.
80+
if User.objects.filter(username=username).exists():
81+
return User.objects.get(username=username)
82+
7783
user = User.objects.create_user(username, password=password, **extra_fields)
7884
self._create_crosswalk(
7985
user=user,
@@ -490,6 +496,7 @@ def create_token(
490496
self, first_name, last_name, fhir_id_v2=None, fhir_id_v3=None, hicn_hash=None, mbi=None
491497
):
492498
passwd = "123456"
499+
493500
user = self._create_user(
494501
first_name,
495502
passwd,
@@ -499,7 +506,7 @@ def create_token(
499506
fhir_id_v3=fhir_id_v3,
500507
user_hicn_hash=hicn_hash if hicn_hash is not None else self.test_hicn_hash,
501508
user_mbi=mbi if mbi is not None else self.test_mbi,
502-
email="%s@%s.net" % (first_name, last_name),
509+
email="%s@%s.notanagency.gov" % (first_name, last_name),
503510
)
504511

505512
# create a oauth2 application and add capabilities

0 commit comments

Comments
 (0)