Expand description
The call-level guard: per-id audience
resolution (AudienceIndex) and the fail-closed guard (AudienceGuard) for the
span/metadata UDTFs and get_payload UDF OwnershipRewrite (the row-level filter) structurally cannot
reach, plus a scan-time guard on view_instance(...). For the six view sets carrying a
physical audience column, the row-level filter can and does reach a view_instance scan too,
row-filtering it the same as the named-table form – there view_instance joined the call-level guard to
close a cost/availability residual (stopping JIT materialization for a foreign-audience
instance), not because the row-level filter can’t filter it. For the other five view sets, reachable only
through a guarded view_instance(...), the row-level filter injects no predicate at all, so this guard is
their sole enforcement.
The call-level guard – arg-addressed guards for
five entry points: the span/metadata UDTFs and the get_payload UDF that
crate::lakehouse::ownership_rewrite::OwnershipRewrite (the row-level filter) structurally cannot reach (they bake
their target id into a provider at plan time, return schemas with no process_id column to
filter on, and some build their own inner session under ReadScope::All), plus
view_instance(...). For the six view sets carrying a physical audience column, the row-level filter
can and does reach a view_instance scan too – it row-filters it the same as the
named-table form – so this guard’s job there is only to stop a caller from triggering JIT
materialization (real compute and object-storage writes) for an instance outside their
audiences before the row-level filter ever gets to run, a cost/availability concern rather than a
confidentiality one. For the other five view sets (net_spans, otel_spans, images,
async_events, thread_spans – reachable only through a guarded view_instance(...), never
as a named table), the row-level filter injects no per-row predicate at all once this guard is in place –
see crate::lakehouse::materialized_view::MaterializedView::instance_is_audience_guarded and
OwnershipRewrite’s module doc for the other side of that coupling – so this guard is their
sole confidentiality enforcement, not a redundant belt-and-braces check.
§One cache, one question
crate::lakehouse::audience_guard::AudienceIndex answers exactly one question, for three id kinds (crate::lakehouse::audience_guard::IdKind): what
audience is this id’s own row stamped with – a process’s for crate::lakehouse::audience_guard::IdKind::Process, a
block’s own for crate::lakehouse::audience_guard::IdKind::Block, a process’s or a stream’s own for
crate::lakehouse::audience_guard::IdKind::ProcessOrStream. It resolves from Postgres, by a single-table, primary-key
point query per row kind – fresher and independent of materialization, unlike
crate::lakehouse::ownership_rewrite::OwnershipRewrite, which reads a daemon-materialized snapshot.
§Fail-closed
crate::lakehouse::audience_guard::is_readable is the whole authorization rule, pure and offline-testable: ReadScope::All
passes everything; ReadScope::Audiences denies crate::lakehouse::audience_guard::OwnerAudience::Unknown unconditionally
and matches crate::lakehouse::audience_guard::OwnerAudience::Audience byte-exactly. There is no unstamped state any more: a
process/stream/block registered through the HTTP ingestion path always carries a real,
non-NULL audience column (a credential with no bound audience is stamped with the resolved
deployment default explicitly); only a legacy, pre-v8 row keeps a NULL column, and
owner_query_sql’s COALESCE resolves that to MICROMEGAS_DEFAULT_AUDIENCE here, so every
existing id has a real owning audience either way. Unknown therefore means “no such row”
– deny, on the same fail-closed reasoning as before. One behaviour change: since each arm now
reads its own table directly rather than joining through processes, a block or stream whose
process_id no longer resolves (retention swept it, or it hasn’t arrived yet) resolves to
its own stamp instead of falling through to Unknown – see owner_query_sql’s doc
comment. An id ambiguous between a process_id and a stream_id
interpretation (crate::lakehouse::audience_guard::OwnerAudience::Ambiguous) is readable only when every interpretation is –
never by picking one arm over the other. A resolution error (Postgres unreachable) is a
denial too – crate::lakehouse::audience_guard::AudienceGuard::authorize/crate::lakehouse::audience_guard::AudienceGuard::readable_ids map it to a query
failure, never to a readable verdict.
§No existence oracle
Every guard denial and every “no such id” produce the same error text (e.g. process_spans: 'xxx' not found or not accessible) – a distinct “permission denied” would let a caller
enumerate which ids exist in other audiences. The server log (debug!) records the real
reason so an operator can tell the two apart; the client cannot.
Structs§
- Audience
Guard - The whole guard: pure decision (
is_readable) plus async resolution (AudienceIndex), fail-closed throughout. One instance is built per request (inquery.rs’sregister_lakehouse_functions) and shared, viaArc, across every arg-addressed UDTF/UDF the request’s session registers. - Audience
Index - Resolves any telemetry id -> its owning process’s audience, from Postgres, cached and TTL-bounded. See the module doc comment for the full rationale.
- Authorized
- Witness that some id’s owning process was resolved and found readable under the caller’s
scope. Constructible only by
AudienceGuard::authorize– a future call site that wants an inner, unscoped session must hold one of these first, which is what keeps a fourth such UDTF from silently reproducing the bypass this stage closes by callingCallerContext::internal()directly. Not airtight:internal()stays public for genuinely non-user-reachable callers.
Enums§
- IdKind
- Which table resolves the id to its owning process. Also the cache’s key discriminator: the
same
Uuidcan legitimately be aprocess_idin one audience and astream_id/block_idin another (all three are client-supplied at ingestion, with no cross-table uniqueness constraint), so caching by kind keeps those disjoint instead of relying on downstream emptiness to make a wrong-kind hit harmless. - Owner
Audience - What a resolution attempt found. Every existing process resolves to an audience – its own
if it was stamped,
MICROMEGAS_DEFAULT_AUDIENCEif it was not – soUnknownis the only “not a real, readable audience” state left, and it means “no such row” (not yet ingested, or retention already deleted it). Deny, fail-closed.
Constants§
- DEFAULT_
AUDIENCE_ CACHE_ ENTRIES - Default entry-count bound for
AudienceIndex’s cache – seesuper::lakehouse_context. OneUuid+ a short audience string is roughly 100 bytes, so 100k entries is ~10 MB: a fixed shape with no operational knob, mirroringanalytics-web-srv/src/data_source_cache.rs’s hardcoded.max_capacity(1000)rather thanMICROMEGAS_METADATA_CACHE_MB’s per-entry-weight variability. - DEFAULT_
AUDIENCE_ CACHE_ TTL - Default freshness bound for
AudienceIndex’s cache entries. The only invalidation mechanism: a process’s Postgres row is written once and never updated in place, but a retention-then-re-export cycle can recreate the sameprocess_id(deterministic UUIDv5 for OTLP) under freshproperties, and nothing else ever expires a stale cached answer. Bounds how long a re-derived process’s audience can serve a stale answer.
Functions§
- is_
readable - Pure, offline-testable: the whole authorization rule, with no I/O in it.
ReadScope::Allis the only branch that passesUnknown– every other combination denies it unconditionally.