Skip to content

Commit d8b898d

Browse files
BB2-4233: Ensure _id param is actually parsed for search Patient call… (#1425)
* BB2-4233: Ensure _id param is actually parsed for search Patient calls, throwing a 404 if it does not match current session. Remove _has:Coverage from swagger docs * Remove prints * Address PR feedback: use parse_qs, remove parse_string function/tests * Add additional test case that previously caused an erroneous 404
1 parent 24d4590 commit d8b898d

File tree

9 files changed

+117
-241
lines changed

9 files changed

+117
-241
lines changed

apps/fhir/bluebutton/tests/fhir_resources/fhir_meta_v1.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,6 @@
128128
"type": "string",
129129
"documentation": "The offset used for result pagination"
130130
},
131-
{
132-
"name": "_has:Coverage",
133-
"type": "token",
134-
"documentation": "Part D coverage type"
135-
},
136131
{
137132
"name": "cursor",
138133
"type": "string",

apps/fhir/bluebutton/tests/fhir_resources/fhir_meta_v2.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,6 @@
136136
"type": "string",
137137
"documentation": "The offset used for result pagination"
138138
},
139-
{
140-
"name": "_has:Coverage",
141-
"type": "token",
142-
"documentation": "Part D coverage type"
143-
},
144139
{
145140
"name": "cursor",
146141
"type": "string",

apps/fhir/bluebutton/tests/test_read_and_search.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
def get_expected_read_request(version: int):
2727
return {
2828
'method': 'GET',
29-
'url': f'{FHIR_SERVER["FHIR_URL"]}/v{version}/fhir/Patient/{FHIR_ID_V2}/?_format=json',
29+
'url': f'{FHIR_SERVER["FHIR_URL"]}/v{version}/fhir/Patient/{FHIR_ID_V2}/?_format=json&_id={FHIR_ID_V2}',
3030
'headers': {
3131
# 'User-Agent': 'python-requests/2.20.0',
3232
'Accept-Encoding': 'gzip, deflate',

apps/fhir/bluebutton/tests/test_utils.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
crosswalk_patient_id,
1919
get_resourcerouter,
2020
build_oauth_resource,
21-
valid_caller_for_patient_read,
21+
valid_patient_read_or_search_call,
2222
)
2323

2424
ENCODED = settings.ENCODING
@@ -70,16 +70,48 @@ def test_notNone(self):
7070
response = notNone(listing, "number")
7171
self.assertEqual(response, listing)
7272

73-
def test_valid_caller_for_patient_read(self):
74-
result = valid_caller_for_patient_read('PatientId:-20140000008326', '-20140000008326')
73+
def test_valid_patient_read_or_search_call_valid_read_calls(self):
74+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', '-20140000008329', '')
7575
assert result is True
7676

77-
result = valid_caller_for_patient_read('PatientId:-20140000008326', '-20140000008329')
77+
result = valid_patient_read_or_search_call('PatientId:-99140000008329', '-99140000008329', '')
78+
assert result is True
79+
80+
def test_valid_patient_read_or_search_call_invalid_read_calls(self):
81+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', '-99140000008329', '')
82+
assert result is False
83+
84+
result = valid_patient_read_or_search_call('PatientId:-99140000008329', '-20140000008329', '')
7885
assert result is False
7986

80-
# call with no colon in beneficiary_id to make sure
81-
invalid_call = valid_caller_for_patient_read('PatientId-20140000008326', '-20140000008329')
82-
assert invalid_call is False
87+
def test_valid_patient_read_or_search_call_valid_search_calls(self):
88+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', None, '_id=-20140000008329')
89+
assert result is True
90+
91+
result = valid_patient_read_or_search_call(
92+
'PatientId:-99140000008329',
93+
None,
94+
'_lastUpdated=lt2024-06-15&startIndex=0&cursor=0&_id=-99140000008329'
95+
)
96+
assert result is True
97+
98+
result = valid_patient_read_or_search_call(
99+
'PatientId:-99140000008329',
100+
None,
101+
'_id=-99140000008329&_lastUpdated=lt2024-06-15&startIndex=0&cursor=0'
102+
)
103+
assert result is True
104+
105+
def test_valid_patient_read_or_search_call_invalid_search_calls(self):
106+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', None, '_id=-99140000008329')
107+
assert result is False
108+
109+
result = valid_patient_read_or_search_call(
110+
'PatientId:-99140000008329',
111+
None,
112+
'_lastUpdated=lt2024-06-15&startIndex=0&cursor=0&_id=-20140000008329'
113+
)
114+
assert result is False
83115

84116

85117
class BlueButtonUtilSupportedResourceTypeControlTestCase(TestCase):

apps/fhir/bluebutton/utils.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from collections import OrderedDict
1111
from datetime import datetime
1212
from pytz import timezone
13+
from typing import Optional
14+
from urllib.parse import parse_qs
1315

1416
from django.conf import settings
1517
from django.contrib import messages
@@ -745,18 +747,31 @@ def get_patient_by_mbi_hash(mbi_hash, request):
745747
return response.json()
746748

747749

748-
def valid_caller_for_patient_read(beneficiary_id: str, patient_id: str) -> bool:
749-
"""When making a read patient call, we only want to ping BFD if the patient_id
750-
being passed matches the fhir_id (currently v2) associated with the current session
750+
def valid_patient_read_or_search_call(beneficiary_id: str, resource_id: Optional[str], query_param: str) -> bool:
751+
"""Determine if a read or search Patient call is valid, based on what was passed for the resource_id (read call)
752+
or the query_parameter (search call)
751753
752754
Args:
753-
beneficiary_id (str): beneficiary_id that is associated with the current session/
754-
Should have format 'patientId:00000000000000 coming in
755-
patient_id (str): patient_id that is will be passed to BFD
755+
beneficiary_id (str): This comes from the BlueButton-OriginalQuery attribute of the headers for the API call.
756+
Has the format, patientId:{{patientId}}, where patientId comes from the bluebutton_crosswalk table
757+
resource_id (Optional[str]): This will only be populated for read calls, and it is the id being passed to BFD
758+
query_param (str): String for the query parameter being passed for search calls. For read calls this is a blank string
756759
757760
Returns:
758-
bool: If the patient_id matches the beneficiary_id, then return True, else False
761+
bool: Whether or not the call is valid
759762
"""
760-
parts = beneficiary_id.split(':', 1)
761-
beneficiary_comparison_id = parts[1] if len(parts) == 2 else None
762-
return beneficiary_comparison_id == patient_id
763+
bene_split = beneficiary_id.split(':', 1)
764+
beneficiary_id = bene_split[1] if len(bene_split) > 1 else None
765+
# Handles the case where it is a read call, but what is passed does not match the beneficiary_id
766+
# which is constructed using the patient id for the current session in generate_info_headers.
767+
if resource_id and beneficiary_id and resource_id != beneficiary_id:
768+
return False
769+
770+
# Handles the case where it is a search call, but what is passed does not match the beneficiary_id
771+
# so a 404 Not found will be thrown before reaching out to BFD
772+
query_dict = parse_qs(query_param)
773+
passed_identifier = query_dict.get('_id', [None])
774+
if passed_identifier[0] and passed_identifier[0] != beneficiary_id:
775+
return False
776+
777+
return True

apps/fhir/bluebutton/views/generic.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from ..utils import (build_fhir_response,
3434
FhirServerVerify,
3535
get_resourcerouter,
36-
valid_caller_for_patient_read)
36+
valid_patient_read_or_search_call)
3737

3838
logger = logging.getLogger(bb2logging.HHS_SERVER_LOGNAME_FMT.format(__name__))
3939

@@ -151,16 +151,26 @@ def fetch_data(self, request, resource_type, *args, **kwargs):
151151

152152
prepped = s.prepare_request(req)
153153

154-
resource_id = kwargs.get('resource_id')
155-
beneficiary_id = prepped.headers.get('BlueButton-BeneficiaryId')
154+
if resource_type == 'Patient':
155+
query_param = prepped.headers.get('BlueButton-OriginalQuery')
156+
resource_id = kwargs.get('resource_id')
157+
beneficiary_id = prepped.headers.get('BlueButton-BeneficiaryId')
156158

157-
if resource_type == 'Patient' and resource_id and beneficiary_id:
158-
# If it is a patient read request, confirm it is valid for the current user
159-
# If not, throw a 404 before pinging BFD
160-
if not valid_caller_for_patient_read(beneficiary_id, resource_id):
159+
# For patient read and search calls, we need to ensure that what is being passed, either in
160+
# query parameters for search calls, or in the resource_id for read calls, is valid for the
161+
# current session (matching the beneficiary_id). If not, raise a 404 Not found before calling BFD.
162+
if not valid_patient_read_or_search_call(beneficiary_id, resource_id, query_param):
161163
error = NotFound('Not found.')
162164
raise error
163165

166+
# Handle the case where it is a patient search call, but neither _id or identifier were passed
167+
if '_id' not in get_parameters.keys() and 'identifier' not in get_parameters.keys():
168+
# depending on if 4166 is merged first, this will need to be updated
169+
get_parameters['_id'] = request.crosswalk.fhir_id(2)
170+
# Reset the request parameters and the prepped request after adding the missing, but required, _id param
171+
req.params = get_parameters
172+
prepped = s.prepare_request(req)
173+
164174
match self.version:
165175
case Versions.V1:
166176
api_ver_str = 'v1'

apps/fhir/bluebutton/views/search.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ def build_url(self, resource_router, resource_type, *args, **kwargs):
8383
class SearchViewPatient(SearchView):
8484
# Class used for Patient resource search view
8585
required_scopes = ['patient/Patient.read', 'patient/Patient.rs', 'patient/Patient.s']
86+
QUERY_SCHEMA = {
87+
**SearchView.QUERY_SCHEMA,
88+
'_id': str,
89+
'identifier': str
90+
}
8691

8792
def __init__(self, version=1):
8893
super().__init__(version)
@@ -91,8 +96,6 @@ def __init__(self, version=1):
9196
def build_parameters(self, request, *args, **kwargs):
9297
return {
9398
'_format': 'application/json+fhir',
94-
# BB2-4166-TODO : this needs to use self.version to determine fhir_id
95-
'_id': request.crosswalk.fhir_id(2)
9699
}
97100

98101

0 commit comments

Comments
 (0)