[DT-3826] Formal access contract - #3003
Conversation
…quirements in the running ES cluster.
…y removing refs to it, improve coverage.
…t ES cluster information.
|
There was a problem hiding this comment.
Pull request overview
This PR formalizes and records the Elasticsearch dataset-search access contract (mechanism-neutral across native DLS/FLS vs app-layer fallback), and adjusts the Elasticsearch capability report to reliably resolve the REST client version in deployed (shaded) builds.
Changes:
- Add a detailed access-contract document defining document visibility + field exposure rules and how downstream tickets must align.
- Fix capability reporting by falling back to
mvn.propertiesforelasticsearch-rest-clientversion, and addtrack_total_hits=trueto the DLS enforcement probe for accurate evidence strings. - Update/build-wire tests and docs to reflect the contract decisions and corrected local-config expectations.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/test/resources/mvn.properties | Adds the REST client version property to the test-classpath mvn.properties shadow file. |
| src/test/java/org/broadinstitute/consent/http/service/ElasticSearchCapabilityServiceTest.java | Expands/updates capability-service tests for new probe query, build-property fallback, and additional response-shape cases. |
| src/main/java/org/broadinstitute/consent/http/service/ElasticSearchCapabilityService.java | Implements REST client version fallback via mvn.properties and uses track_total_hits=true in enforcement probe. |
| src/main/java/org/broadinstitute/consent/http/ConsentModule.java | Adds provider-level Javadoc clarifying Dropwizard lifecycle ordering expectations for RestClient shutdown hook registration. |
| pom.xml | Promotes the REST client version to a Maven property so it’s emitted into mvn.properties by properties-maven-plugin. |
| docs/plans/es-security-capability-record.md | Records production measurement details and clarifies local config realities (rendered/ignored config/). |
| docs/plans/es-access-contract.md | Adds the formal access contract document and enumerates decided rules + OPEN policy items. |
| docs/plans/elasticsearch-service-duos-ui-usage.md | Updates the plan to align with the access contract decisions (e.g., cancel per-document FLS marker, single allowlist). |
| DEVNOTES.md | Clarifies that config/ isn’t version-controlled and updates local security enablement guidance accordingly. |
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // License readings the cluster answers only partly |
kevinmarete
left a comment
There was a problem hiding this comment.
Here are some comments:
-
[P1] Native DLS does not guarantee the stated count/aggregation invisibility.
Decision 1 says restricted documents cannot contribute to counts, facets, or aggregations and that DLS filters before scoring/counting. Elastic explicitly documents that DLS still permits search requests that infer terms and counts from inaccessible documents through aggregate information. The native path therefore needs query mediation/restrictions too, or the contract must weaken this guarantee. See Elastic’s DLS limitations. -
[P1] The fallback allowlist only filters
_source, leaving other extraction channels open.
E-3 filters onlyhits.hits[*]._source, while E-1 preserves caller-supplied aggregations, sorting, and highlighting. A caller could aggregateaccessPolicy.custodianEmails, highlight a dynamic-map field, or receive an INTERNAL field through hitsortvalues. The design should validate every field reference against an allowed query-field list and filter all response channels—not just_source. -
[P1] The supposedly exhaustive field audit marks several live UI dependencies INTERNAL.
The classification says these have no consumers, but currentduos-ui/developuses:requestLocationin dataset details and the library’s “Request Path”deletablefor the submissions delete actioncreateUserIdandstudy.dataSubmitterIdto construct “My Data Submissions”submitter.displayNamein the DAC datasets table
Generating FLS/E-3 allowlists from this table would break those flows. The audit should cover all search callers and distinguish response-visible fields from fields that must remain queryable.
rushtong
left a comment
There was a problem hiding this comment.
Looks good, thank you 👍🏽
| **If a future requirement does need a privileged field in search results**, this decision has to be | ||
| reopened, not worked around: the answer is application projection (Epic E's mechanism, applied to | ||
| the native path as well), and the cost is that Epic D stops being sufficient on its own. |
There was a problem hiding this comment.
I can see this happening. For example, we removed approvedUserIds which had the potential for exposure of partially controlled info. The need for something similar could resurface. I think the risk is low, but non-zero and there are likely other workarounds we can come up with.
There was a problem hiding this comment.
Agreed on both fronts. The bigger issue I'm coming back to is aggregation across hidden documents from @kevinmarete's comments. I had forgotten about that leaky behavior in ES and need to think more about the consequences for this.
fboulnois
left a comment
There was a problem hiding this comment.
Thanks for verifying the behavior 👍



Addresses
DT-3826 — Ticket A-2, formal Elasticsearch
access contract.
Security risk: low — no change to authorization behavior. The contract is a written decision
record; the only production-path code change is how the capability report resolves the REST client's
version string. The admin-only capability endpoint itself lands in the base PR
(DT-3825).
Stacked on
otchet-dt-3825-confirm-es-in-env— review that PR first, or read this one's diff againstthat branch rather than
develop.Summary
Writes the formal access contract for dataset search, records the production A-1 measurement, and
adjusts the downstream plan to match what both turned up.
The contract —
docs/plans/es-access-contract.md— is the document B-1, C-1, D-3, and E-2/E-3 getimplemented against. It enumerates every access dimension, its source of truth, what it does today,
and what the contract commits to, in mechanism-neutral terms so it holds identically under native
DLS/FLS (Epic D) and the application-layer fallback (Epic E). It is stated as Complete and
unblocked: no OPEN item stands between it and B-1/B-3/C-1.
Three decisions the contract settles
no document and no hit-count contribution. Redaction leaks by counting, can't be expressed in
native FLS, and would loosen today's
DatasetService.verifyPublicVisibilityAccessbehavior byaccident.
grant to the index privilege, not the document, so the planned per-document
fieldAccessProfilemarker has nothing to bind to. The obvious repair (a per-caller "privileged"bundle) is also unsound: DLS is a disjunction, so a custodian of one restricted dataset would get
privileged fields projected out of every unrelated public document in the result set.
is PRESERVE. DAC membership, DAC chair,
dacApproval, institution, policy tags, and principalallowlists grant no read access today and are DEFERred, so a security refactor can't become a
silent authorization change.
Findings that change other tickets
fieldAccessProfileand hold back the four speculative dimensions rather thanshipping unpopulated fields that read as enforcement.
need indexing, and the filter compares the caller's one ID against both.
Alice@x.orgfailsagainst a stored
alice@x.org(OPEN-6).dataCustodianEmailis corrected — it lives in a JSONproperty bag with no referential integrity, no normalization, and no index. Workable for search
once B-3 denormalizes it; still a gap for the non-search endpoints, and it makes
reindex-on-custodian-change a correctness requirement.
publicVisibility = NULLwas recorded as an open policy question and is not one — the columnis
NOT NULL; the null in summary rows is the LEFT JOIN. Resolved by the schema, filter failsclosed.
Seven OPEN items remain in §E, each a proposed change (expand, tighten, or fix a defect) with a
preserve-today default, so none blocks Phase 1.
A-1: production measured
es-security-capability-record.mdrecords the production run (2026-08-05, write probes): ElasticCloud, enterprise license, all five capabilities observed — DLS and FLS enforced, not merely
accepted; keys created, used, and torn down cleanly. Four findings carried forward: Epic D's key
minting is not privilege-blocked in production (contradicting the prior assumption, in the
permissive direction — the breadth of that credential is worth its own review); the
run_asevidence is self-impersonation only; cluster-side audit logging must not be assumed; and Elastic
Cloud reserves some settings to Elastic's operators, making any future infra ask a support request.
Environment specifics stay on the ticket rather than in this public repo; the record keeps only what
the Epic D / Epic E decision rule consumes. The A-1 decision stays pending on dev and staging —
production being the environment most likely to carry the strongest license is exactly why the other
two can't be assumed to match it.
Code changes
rest_client_compatibilitycame back indeterminate in production and would have in everydeployed environment: the shade plugin strips dependency manifests, so
RestClient.class.getPackage().getImplementationVersion()is null in the uber jar while workingfine locally. Fixed by falling back to
elasticsearch.rest.client.versionin the build-generatedmvn.properties(the mechanismSwaggerResourcealready uses), with the dependency versionpromoted to a pom property so it lands there. The package lookup is still tried first. Verified
against the built
consent.jar.track_total_hits=trueon the DLS enforcement probe — the default capshits.total.valueat10 000, so on a larger index the evidence string would report "10000 of N" rather than the real
count. The verdict was unaffected; the quoted evidence wasn't.
ConsentModulejavadoc documents the ordering dependency in the sharedRestClientprovider:the shutdown hook is registered at provision time, which is safe only while every consumer is
reached from
ConsentApplication.run.Docs correction
config/is git-ignored and rendered per developer byfirecloud-develop, so nothing in thisrepository sets any Elasticsearch default. DEVNOTES and the capability record previously implied a
committed compose file; both now say the durable change belongs in the
firecloud-developtemplate(outside this repo, still needs an owner) and that each developer applies the settings to their own
rendered copy meanwhile.
Testing
Three unit tests added to
ElasticSearchCapabilityServiceTest: one guarding the pom wiring itself(inlining the dependency version again would silently return the report to "Could not determine" in
exactly the environments it's used to decide in, and nothing else in the suite would notice), one
for an absent build property, and one confirming a matching major version names the resolved client
version. Existing search-probe assertions updated for the new query string.
No UI or workflow change, so no PO sign-off needed. Post-deploy verification is a
GET /api/elasticSearch/capabilitiesagainst dev, which should now name the client version rather thandeclining to.
Have you read CONTRIBUTING.md lately? If not, do that first.