This pull request has been designated for reprocessing. Please check back in a few minutes.

ES|QL|DS: hide storage location without privilege #159736

Open
luigidellaquila opened 2:40pm on September 18, 2026 wants to merge 1425 Ξ” into elastic/elasticsearch main from
esql/ds/hide_dataset_location

Pull Request Overview

  • Opened on September 18, 2026
  • Status Open
  • Commit count 2 with first commit September 18, 2026

Total Delta

1425 Total Diff Delta

Open Days

Open 0 weekdays

Test Delta

223 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Fraction of total time Business days Phase
 
0.0 days Authoring 1 commit before pull request opened for review
 
0.0 days Awaiting first review

Total time for pull request still awaiting merge: 0.1 business days

Author avatar

ES|QL|DS: hide storage location without privilege

A user who holds only read on a dataset (which authorises
indices:data/read/esql/resolve_datasets) was able to discover the dataset's
full storage location β€” S3 bucket, GCS bucket, Azure container, object key,
prefix β€” through two independent routes:


  1. Failed-query error messages. Every storage layer (S3, GCS, Azure), every
    resolver stage (ExternalSourceResolver, FormatNameResolver), and every
    format reader (ParquetFormatReader) embedded the path directly into
    exception messages. Those messages crossed the node boundary and reached the
    user verbatim in the reason / caused_by chain.


  2. Profile plan strings on success. With profile: true, every entry in
    profile.plans contained the full storage URI via ExternalSourceExec's
    node string.


The right that separates these two callers already exists:
indices:admin/esql/dataset/get (granted by read_dataset_metadata,
manage_dataset, manage, or all). This PR makes both paths consult it.


What changed

Error-message path


  • Strip path at every composing site. All IOException /
    IllegalArgumentException constructions across S3, GCS, Azure storage
    objects and providers, FormatNameResolver, ExternalSourceResolver,
    ParquetFormatReader, FileSourceFactory, and TableCatalog now name the
    operation and condition without embedding the path.
    ExternalFailures.resolutionFailureMessage was deleted; its call sites use
    ExternalFailures.rootDetail(e) directly (path-free, wrapper-transparent).


  • Carry location in a structured field. ExternalFailures.LocatedException
    wraps every resolution-path failure as a pair: an unlocated form (no path)
    and a located form (path reinstated). The coordinator selects between them
    based on privilege.


  • One privilege check on the coordinator. EsqlSession checks
    indices:admin/esql/dataset/get via HasPrivilegesRequest after the
    caller's thread context is restored. The result (canSeeDatasetLocation) is
    stored on Configuration behind a new TransportVersion
    (esql_dataset_location_visible) so data nodes receive it too. Security-
    disabled clusters short-circuit to true.


  • Reinstatement. reinstateLocationIfAuthorized in EsqlSession resolves
    every LocatedException to its located or unlocated form before the failure
    reaches the user.


Profile/EXPLAIN plan path



  • NodeStringMapper gains an abstract location(String) method.
    IDENTITY.location() returns verbatim; new REDACT_LOCATION.location()
    returns "[redacted]".


  • ExternalSourceExec.nodeString() and ExternalRelation.nodeString() now
    call mapper.location(sourcePath) instead of mapper.opaque(sourcePath).


  • ComputeService and all three EXPLAIN rendering call sites in EsqlSession
    select the mapper from configuration.canSeeDatasetLocation().

Closes: elastic/esql-planning#1981

Comments Threads Pending Resolution

Resolved Comment Threads

No resolved comments have been left on this PR.