BB2-4964/4965: Audit Event Endpoint/Scope#1647
Conversation
| ) | ||
|
|
||
|
|
||
| def test_failure_on_authorize_non_v3_with_audit_event_scope(create_application): |
There was a problem hiding this comment.
Note this uses a new fixture and is not part of the TestCase based classes.
There was a problem hiding this comment.
Pull request overview
Adds support for retrieving BFD AuditEvent data via the BlueButton API (v3) and introduces a new protected capability/scope (patient/AuditEvent.rs) that is created via create_blue_button_scopes, along with new pytest fixtures and tests.
Changes:
- Adds a new v3 FHIR endpoint/view for
AuditEventand allowsAuditEventas a supported resource type. - Introduces
patient/AuditEvent.rsscope/capability creation and updates auth/token behavior to control/enable AuditEvent access. - Adds pytest fixtures and unit/integration tests (plus sample response fixture) for the new endpoint and scope rules.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements/requirements.txt | Bumps fhir-core dependency version. |
| requirements/requirements.dev.txt | Bumps fhir-core and greenlet dev dependency versions. |
| conftest.py | Adds pytest factory fixtures for users, tokens, capabilities, and applications. |
| apps/fhir/constants.py | Adds AuditEvent to allowed FHIR resource types. |
| apps/fhir/bluebutton/views/audit_event.py | New AuditEventView for v3 AuditEvent requests to BFD. |
| apps/fhir/bluebutton/v3/urls.py | Routes /v3/fhir/AuditEvent to the new view behind v3_endpoints. |
| apps/fhir/bluebutton/tests/test_audit_event.py | Adds tests for AuditEvent permissions/behavior (mock + integration). |
| apps/fhir/bluebutton/tests/sample_responses/audit_event_response.json | Adds a sample BFD AuditEvent Bundle response fixture. |
| apps/fhir/bluebutton/exceptions.py | Import reordering/typing import placement. |
| apps/dot_ext/views/authorization.py | Blocks AuditEvent scope on non-v3 authorize; auto-adds AuditEvent scope to client_credentials token scopes. |
| apps/dot_ext/tests/test_views.py | Extends registration test to account for the new AuditEvent scope. |
| apps/dot_ext/tests/test_authorization.py | Adds a test asserting non-v3 authorize fails when AuditEvent scope is requested. |
| apps/dot_ext/tests/test_authorization_token.py | Asserts AuditEvent scope is auto-added to client_credentials tokens. |
| apps/constants.py | Adds AUDIT_EVENT_SCOPE constant. |
| apps/capabilities/management/commands/create_blue_button_scopes.py | Adds creation of AuditEvent protected capability/scope. |
| apps/authorization/permissions.py | Adds AuditEvent patient-matching logic in is_resource_for_patient. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| elif obj['resourceType'] == 'AuditEvent': | ||
| entity = obj.get('entity', [{}]) | ||
| patient_info = entity[0].get('what', {}) | ||
| reference = patient_info.get('reference', '') | ||
| reference_id = reference.split('/')[1] | ||
| if reference_id != patient_id: | ||
| raise exceptions.NotFound() |
There was a problem hiding this comment.
Leaning towards leaving this, as it will retrieve the patient_id from the BFD response if it is in the correct path. If it is not, and IndexError may be thrown, which would be caught by the except Exception block, and False would be returned (ultimately resulting in a 403). Otherwise, we can make this change, and it would be a 404 returned instead of a 403 if there was a problem parsing the AuditEvent.
… if AuditEvent in scope
…pes. Modify scope param handling for CAN requests
jimmyfagan
left a comment
There was a problem hiding this comment.
Looks good, I have a handful of comments and questions. On the testing front, mostly looks good, but I haven't been able to get a read request to work, I keep getting "detail": "You do not have permission to perform this action.". I might need to just update the protected capability, maybe I have a stale one. I'll reach out if I have trouble. Also, I want to test a bit more with the feature flag disabled to make sure that case is tested pretty thoroughly. But we should be on track to wrap before EOD today on this probably.
|
|
||
| def create_audit_event_read_search_capability(group): | ||
| c = None | ||
| description = 'Allow CAN patients and 3rd party apps to retrieve audit event data that shows what apps have had a successful patient match for network calls via the Blue Button API (CAN flow).' |
There was a problem hiding this comment.
(optional) Might be nice to differentiate in these descriptions too, though the titles are the main thing that matters, and I like the concise differentiation there.
| description = 'Allow CAN patients and 3rd party apps to retrieve audit event data that shows what apps have had a successful patient match for network calls via the Blue Button API (CAN flow).' | |
| description = 'Allow CAN patients and 3rd party apps to read and search audit event data that shows what apps have had a successful patient match for network calls via the Blue Button API (CAN flow).' |
There was a problem hiding this comment.
Love the coverage here with very little actual test code, nice job bringing in these pytest features!
| if AUDIT_EVENT_SCOPE in scope: | ||
| scope = scope.replace(AUDIT_EVENT_SCOPE, '') |
There was a problem hiding this comment.
My understanding is that this is removing patient/AuditEvent.rs so the replacement of patient/AuditEvent.r doesn't leave behind random s chars? I think this warrants a comment, because without thinking it through, it's a bit confusing that we're removing AUDIT_EVENT_SCOPE here. But also, you might be able to use re.sub(pattern, replacement, string) instead of replace here to make sure you only replace the regex matches. Your unit tests should verify that things continue to work correctly if you go down that path.
There was a problem hiding this comment.
Good call, will try re.sub(pattern, replacement, string) instead!
| re_path( | ||
| r'AuditEvent[/]?', | ||
| waffle_switch('enable_auditevents')(waffle_switch('v3_endpoints')(AuditEventView.as_view(version=3))), | ||
| name='bb_oauth_fhir_audit_event', |
There was a problem hiding this comment.
| name='bb_oauth_fhir_audit_event', | |
| name='bb_oauth_fhir_audit_event_search', |
| if self.version == Versions.V3 and getattr(fhir_settings, 'fhir_url_v3', None): | ||
| fhir_url = fhir_settings.fhir_url_v3 | ||
| else: | ||
| fhir_url = fhir_settings.fhir_url | ||
|
|
||
| return f'{fhir_url}/v{self.version}/fhir/AuditEvent' |
There was a problem hiding this comment.
Can this be simplified given that it's only for v3 at the moment? Or is this still necessary to properly fail on v2?
There was a problem hiding this comment.
I think so. In my testing, I still get a 404 on a v2 when this is hardcoded to v3.
There was a problem hiding this comment.
This is awesome, probably warrants an eng sync topic to share with the group.
jimmyfagan
left a comment
There was a problem hiding this comment.
Just saw one quirk in testing, but otherwise looks good!
| if switch_is_active('enable_auditevents') and ( | ||
| AUDIT_EVENT_SCOPE in request.GET.get('scope', '') | ||
| or AUDIT_EVENT_SCOPE in request.POST.get('scope', '') | ||
| or AUDIT_EVENT_READ_SCOPE in request.GET.get('scope', '') | ||
| or AUDIT_EVENT_READ_SCOPE in request.POST.get('scope', '') | ||
| or AUDIT_EVENT_SEARCH_SCOPE in request.GET.get('scope', '') | ||
| or AUDIT_EVENT_SEARCH_SCOPE in request.POST.get('scope', '') | ||
| ): | ||
| return JsonResponse( | ||
| {'status_code': HTTPStatus.BAD_REQUEST, 'message': AUDIT_EVENT_SCOPE_ERROR_MESSAGE}, | ||
| status=HTTPStatus.BAD_REQUEST, | ||
| ) | ||
|
|
There was a problem hiding this comment.
When the flag is disabled, token requests look like they are getting AuditEvent scopes back when they're requested. We might just want to always catch this and then conditionally return the custom message when the flag is enabled and otherwise return the general "invalid scopes" message.
…bility records having default = False
jimmyfagan
left a comment
There was a problem hiding this comment.
Looks good, just two minor comments that should hopefully be quick fixes.
| return JsonResponse( | ||
| {'status_code': HTTPStatus.BAD_REQUEST, 'message': AUDIT_EVENT_SCOPE_ERROR_MESSAGE}, | ||
| status=HTTPStatus.BAD_REQUEST, |
There was a problem hiding this comment.
Definitely a nit, but if the flag is not enabled, a generic "invalid_scopes" message might be better here. Also, we might not need to do too much with that case given that the flag will mostly be enabled, so consider this optional.
| # scopes in the request, the conditional for scopes_with_audit_event_filtered_out will not evaluate | ||
| # and we will need to return based on if there are any AuditEvent scopes. We want to allow | ||
| # client_credentials calls through, even if they only have AuditEvent scopes, | ||
| audit_event_scopes_in_request = set(scopes) & AUDIT_EVENT_SCOPE_SET |
There was a problem hiding this comment.
I think if you move this line down after the if scopes_with_audit_event_filtered_out block, this client credentials block becomes more clear, and you could probably rewrite this comment in a way that is more clear. We don't need to evalutate audit_event_scopes_in_request unless scopes_with_audit_event_filtered_out is empty.
JIRA Tickets:
BB2-4964
BB2-4965
What Does This PR Do?
This PR adds the ability to make BFD AuditEvent calls through the BlueButton API. It also introduces a new capabilities_protectedcapability DB record, added via the create_blue_button_scopes django management command.
For testing, some fixtures were created. We recently began to run unit/integration tests with pytest, so these fixtures extend the usage of pytest. More on those below.
What Should Reviewers Watch For?
If you're reviewing this PR, please check for these things in particular:
Validation
@jimmyfagan Some additional validation steps with the change to default = False:
What Security Implications Does This PR Have?
Please indicate if this PR does any of the following:
security engineer's approval.
Any Migrations?
To add this capabilities_protectedcapability record, we will manually create it via the Django Admin UI. My thought is that should be done as part of the deployment this PR will go in, and the capabilities_protectedcapability record should be created in an environment before the code is deployed there.
To test if creating the capabilities_protectedcapability record pre-deploy would work, I ensured that the new capabilities_protectedcapability record was present, and built the app off of the master branch. I then did the following:
To me, this indicates we will be fine in the window that the new capabilities_protectedcapability record has been created, but before the new code is live. There will be failures around patient/AuditEvent.rs in that meantime, but those are expected, and we have not yet publicized this new endpoint.
I also tested what would happen if we deployed the code, and then ran the command:
Given the above findings, I don't think it really matters when we add the capabilities_protectedcapability record, but I would recommend running it before the code is deployed.
etc)