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}