Skip to main content

micromegas_ingestion/
write_audience.rs

1//! The authenticated write audience a request ingests under.
2//!
3//! Always a real audience: a credential that carries none resolves to the deployment default at
4//! the HTTP edge (`micromegas::servers::write_audience::resolve_write_audience`), the same
5//! resolution every other surface that touches an audience already performs. See
6//! `WriteAudience` itself for the charset contract this validates against.
7
8use std::sync::Arc;
9
10/// The authenticated write audience a request ingests under.
11/// Always a real audience -- a credential that carries none resolves to the deployment default
12/// at the HTTP edge rather than staying a distinct, unstamped third state.
13///
14/// Per `rust/CLAUDE.md`'s Rust-API stance, every process-insert call site must state its
15/// audience explicitly -- there is no `Default` impl, so a call site with nothing of its own to
16/// pass must pass the resolved deployment default, and the compiler enumerates every site that
17/// needs updating when this type's callers change.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct WriteAudience(Arc<str>);
20
21impl WriteAudience {
22    /// Builds a `WriteAudience` from an audience label.
23    ///
24    /// Rejects a malformed label: `ingestion_api_keys.audience` is already `CHECK`-constrained
25    /// to `[A-Za-z0-9_-]{1,255}`, so this is defence in depth against a future producer of
26    /// `bound_audience` that doesn't go through that column. The HTTP-edge caller
27    /// (`micromegas::servers::write_audience::resolve_write_audience`) does not treat this
28    /// `Err` as a rejection -- it warns and degrades to the deployment default instead, since
29    /// that caller has no `Result` to propagate the failure through. This is also the validating
30    /// constructor for the deployment default itself, called once at startup. The charset check
31    /// duplicates `micromegas_auth::policy::is_valid_audience` (`policy.rs`) rather than
32    /// depending on `micromegas-auth` from `micromegas-ingestion` -- the same crate-boundary
33    /// trade-off `read_scope.rs`'s `is_well_formed_audience` already makes for
34    /// `micromegas-analytics`. Keep the three copies in step if the charset ever changes.
35    pub fn new(audience: &str) -> anyhow::Result<Self> {
36        if is_valid_audience(audience) {
37            Ok(Self(Arc::from(audience)))
38        } else {
39            Err(anyhow::anyhow!(
40                "invalid write audience {audience:?}: must match [A-Za-z0-9_-]{{1,255}}"
41            ))
42        }
43    }
44
45    /// The audience label. Never absent.
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49
50    /// The OTLP id-derivation namespace `self` occupies, relative to `default`. `default` is the
51    /// deployment's resolved default audience: it keeps the un-salted `NS_OTEL_PROCESS_V1`
52    /// namespace, so a resolved-to-default caller and one explicitly bound to a label equal to
53    /// the default derive the *same* ids. Every other audience gets its own salted namespace.
54    ///
55    /// Returns `None` (the un-salted default namespace) when `self == default`, `Some(self)`
56    /// (a per-audience salted namespace) otherwise. Named and pulled out here, rather than
57    /// inlined at each of the five `IdentityContext` construction sites, so the rule has exactly
58    /// one home.
59    pub fn id_namespace<'a>(&'a self, default: &WriteAudience) -> Option<&'a str> {
60        (self.as_str() != default.as_str()).then_some(self.as_str())
61    }
62}
63
64/// `true` if `aud` is a valid audience name: `[A-Za-z0-9_-]{1,255}`, checked in bytes. See
65/// `WriteAudience::new`'s doc comment for why this is a local copy rather than a dependency on
66/// `micromegas-auth`.
67fn is_valid_audience(aud: &str) -> bool {
68    !aud.is_empty()
69        && aud.len() <= 255
70        && aud
71            .bytes()
72            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
73}