Skip to content

Commit 0ad5f69

Browse files
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
1 parent 15c1c3a commit 0ad5f69

File tree

9 files changed

+133
-241
lines changed

9 files changed

+133
-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: 3 additions & 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',
@@ -660,6 +660,8 @@ def _read_request(self, version: int = 1):
660660

661661
@all_requests
662662
def catchall(url, req):
663+
print("EXPECTED: ", expected_request['url'])
664+
print("RETURNED: ", unquote(req.url))
663665
self.assertEqual(expected_request['url'], unquote(req.url))
664666
self.assertEqual(expected_request['method'], req.method)
665667
self.assertDictContainsSubset(expected_request['headers'], req.headers)

apps/fhir/bluebutton/tests/test_utils.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
crosswalk_patient_id,
1919
get_resourcerouter,
2020
build_oauth_resource,
21-
valid_caller_for_patient_read,
21+
parse_string,
22+
valid_patient_read_or_search_call,
2223
)
2324

2425
ENCODED = settings.ENCODING
@@ -70,16 +71,51 @@ def test_notNone(self):
7071
response = notNone(listing, "number")
7172
self.assertEqual(response, listing)
7273

73-
def test_valid_caller_for_patient_read(self):
74-
result = valid_caller_for_patient_read('PatientId:-20140000008326', '-20140000008326')
74+
def test_parse_string(self):
75+
result = parse_string('PatientId:-20140000008326', ':')
76+
assert result == '-20140000008326'
77+
78+
result = parse_string('?_lastUpdated=lt2024-06-15&startIndex=0&cursor=0&_id=-99000010256546', '_id=')
79+
assert result == '-99000010256546'
80+
81+
result = parse_string('?_lastUpdated=lt2024-06-15&startIndex=0&cursor=0&_id=-99000010256546', '_id==')
82+
assert result is None
83+
84+
def test_valid_patient_read_or_search_call_valid_read_calls(self):
85+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', '-20140000008329', '')
86+
assert result is True
87+
88+
result = valid_patient_read_or_search_call('PatientId:-99140000008329', '-99140000008329', '')
89+
assert result is True
90+
91+
def test_valid_patient_read_or_search_call_invalid_read_calls(self):
92+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', '-99140000008329', '')
93+
assert result is False
94+
95+
result = valid_patient_read_or_search_call('PatientId:-99140000008329', '-20140000008329', '')
96+
assert result is False
97+
98+
def test_valid_patient_read_or_search_call_valid_search_calls(self):
99+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', None, '_id=-20140000008329')
75100
assert result is True
76101

77-
result = valid_caller_for_patient_read('PatientId:-20140000008326', '-20140000008329')
102+
result = valid_patient_read_or_search_call(
103+
'PatientId:-99140000008329',
104+
None,
105+
'_lastUpdated=lt2024-06-15&startIndex=0&cursor=0&_id=-99140000008329'
106+
)
107+
assert result is True
108+
109+
def test_valid_patient_read_or_search_call_invalid_search_calls(self):
110+
result = valid_patient_read_or_search_call('PatientId:-20140000008329', None, '_id=-99140000008329')
78111
assert result is False
79112

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
113+
result = valid_patient_read_or_search_call(
114+
'PatientId:-99140000008329',
115+
None,
116+
'_lastUpdated=lt2024-06-15&startIndex=0&cursor=0&_id=-20140000008329'
117+
)
118+
assert result is False
83119

84120

85121
class BlueButtonUtilSupportedResourceTypeControlTestCase(TestCase):

apps/fhir/bluebutton/utils.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from collections import OrderedDict
1111
from datetime import datetime
1212
from pytz import timezone
13+
from typing import Optional
1314

1415
from django.conf import settings
1516
from django.contrib import messages
@@ -745,18 +746,42 @@ def get_patient_by_mbi_hash(mbi_hash, request):
745746
return response.json()
746747

747748

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
749+
def parse_string(string_to_parse: str, split_char: str) -> str:
750+
"""_summary_
751+
Args:
752+
string_to_parse (str): _description_
753+
split_char (str): _description_
754+
Returns:
755+
str: _description_
756+
"""
757+
parts = string_to_parse.split(split_char, 1)
758+
parsed_string = parts[1] if len(parts) > 1 else None
759+
return parsed_string
760+
761+
762+
def valid_patient_read_or_search_call(beneficiary_id: str, resource_id: Optional[str], query_param: str) -> bool:
763+
"""Determine if a read or search Patient call is valid, based on what was passed for the resource_id (read call)
764+
or the query_parameter (search call)
751765
752766
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
767+
beneficiary_id (str): This comes from the BlueButton-OriginalQuery attribute of the headers for the API call.
768+
Has the format, patientId:{{patientId}}, where patientId comes from the bluebutton_crosswalk table
769+
resource_id (Optional[str]): This will only be populated for read calls, and it is the id being passed to BFD
770+
query_param (str): String for the query parameter being passed for search calls. For read calls this is a blank string
756771
757772
Returns:
758-
bool: If the patient_id matches the beneficiary_id, then return True, else False
773+
bool: Whether or not the call is valid
759774
"""
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
775+
beneficiary_id = parse_string(beneficiary_id, ':')
776+
# Handles the case where it is a read call, but what is passed does not match the beneficiary_id
777+
# which is constructed using the patient id for the current session in generate_info_headers.
778+
if resource_id and resource_id != beneficiary_id:
779+
return False
780+
781+
# Handles the case where it is a search call, but what is passed does not match the beneficiary_id
782+
# so a 404 Not found will be thrown before reaching out to BFD
783+
patient_id = parse_string(query_param, '_id=')
784+
if patient_id and patient_id != beneficiary_id:
785+
return False
786+
787+
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)