Skip to content

EHR security: demographics elevated-access docs + genetic-calc container authz#1152

Open
labkey-martyp wants to merge 2 commits into
release25.7-SNAPSHOTfrom
25.7_fb_demographics_elevated_access_doc
Open

EHR security: demographics elevated-access docs + genetic-calc container authz#1152
labkey-martyp wants to merge 2 commits into
release25.7-SNAPSHOTfrom
25.7_fb_demographics_elevated_access_doc

Conversation

@labkey-martyp

Copy link
Copy Markdown
Contributor

Rationale

Two related EHR security items surfaced during a security review. (1) EHRDemographicsService serves animal records built under the elevated EHR service user (EHRService.getEHRUser), so callers receive aggregated demographic/derived fields regardless of their per-dataset / QCState row permissions — and many DemographicsProviders set _supportsQCState=false, applying no QCState filter at all. This is intended behavior (within an EHR study folder, Read access is the trust boundary for the demographics summary, which backs the shared EHR.DemographicsCache UIs), so it is now documented in code to prevent a future caller from re-exposing these records at a lower trust boundary. (2) SetGeneticCalculationTaskSettingsAction enforced AdminPermission only on the request container while resolving and acting on a caller-supplied containerPath for the server-global genetic-calculation schedule, so a user who administers the request container could point the schedule at a container they do not administer. That gap is closed and covered by an integration test.

Related Pull Requests

  • None

Changes

  • Document elevated-access-by-design on EHRDemographicsServiceImpl.getAnimals and EHRController.GetDemographicsAction, warning callers not to forward these records to a lower trust boundary without re-securing them against the consuming user.
  • Re-verify AdminPermission on the resolved target container in SetGeneticCalculationTaskSettingsAction and throw UnauthorizedException when the caller does not administer it.
  • Add EHRController.TestCase, an integration test verifying that a user who administers the request container but not the resolved target is rejected while a user who administers the target succeeds; registered in EHRModule.getIntegrationTests().

EHRDemographicsService builds and caches AnimalRecords under the configured EHR service user (EHRService.getEHRUser), shared across all users in a container, so the aggregated demographic/derived fields reflect the service user's access and bypass the caller's per-dataset and per-QCState row permissions; several DemographicsProviders set _supportsQCState=false so no QCState filter is applied at all. Add Javadoc on EHRDemographicsServiceImpl.getAnimals and on the EHRController.GetDemographicsAction endpoint marking this elevated access as by design, with folder Read as the intended trust boundary for the demographics summary, and warning callers not to forward these records to a lower trust boundary without re-securing. Documentation only; no functional change.
SetGeneticCalculationTaskSettingsAction is annotated @RequiresPermission(AdminPermission.class), but that check only covers the request container while the action resolves and acts on a caller-supplied containerPath to configure the server-global genetic calculation schedule. A user who administers the request container could therefore point the schedule at a container they do not administer. Re-verify AdminPermission on the resolved target container and throw UnauthorizedException when the caller lacks it.

Add EHRController.TestCase, an integration test confirming that a user who administers the request container but not the resolved target is rejected while a user who administers the target succeeds, and register it in EHRModule.getIntegrationTests().

@labkey-bpatel labkey-bpatel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests largely covered the security fix. However, Claude noted a couple of small gaps in testing:
Gap 1 — Null containerPath fallback is uncovered. When the form omits containerPath, c falls back to getContainer() (the request container), and the new check then
re-runs against that. The two existing tests both send a non-null containerPath. A regression that, for example, inverted the condition (if (c.hasPermission(...)) →
throw) would still pass the foreign-container test but would break the null-path / same-container case in production. A third test with form.setContainerPath(null)
and the same admin user would lock this in.

Gap 2 — Only the container is asserted on the success path. testCanConfigureAdministeredContainer verifies GeneticCalculationsJob.getContainer().getId() matches, but
not the other fields the action persists (hourOfDay, dayOfWeek, kinshipValidation, allowImportDuringBusinessHours). The security goal is fully covered, but a refactor
that wrote the container correctly while silently dropping the other fields wouldn't be caught here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants