Skip to main content

Module audience_guard

Module audience_guard 

Source
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§

AudienceGuard
The whole guard: pure decision (is_readable) plus async resolution (AudienceIndex), fail-closed throughout. One instance is built per request (in query.rs’s register_lakehouse_functions) and shared, via Arc, across every arg-addressed UDTF/UDF the request’s session registers.
AudienceIndex
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 calling CallerContext::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 Uuid can legitimately be a process_id in one audience and a stream_id/block_id in 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.
OwnerAudience
What a resolution attempt found. Every existing process resolves to an audience – its own if it was stamped, MICROMEGAS_DEFAULT_AUDIENCE if it was not – so Unknown is 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 – see super::lakehouse_context. One Uuid + a short audience string is roughly 100 bytes, so 100k entries is ~10 MB: a fixed shape with no operational knob, mirroring analytics-web-srv/src/data_source_cache.rs’s hardcoded .max_capacity(1000) rather than MICROMEGAS_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 same process_id (deterministic UUIDv5 for OTLP) under fresh properties, 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::All is the only branch that passes Unknown – every other combination denies it unconditionally.