EHR security: demographics elevated-access docs + genetic-calc container authz#1152
EHR security: demographics elevated-access docs + genetic-calc container authz#1152labkey-martyp wants to merge 2 commits into
Conversation
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
left a comment
There was a problem hiding this comment.
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.
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
Changes