micromegas_auth/db_audience_grants.rs
1//! DB-backed audience grant store: a whole-table snapshot cache over the `audience_grants` table
2//! (migration v7, `rust/ingestion/src/sql_migration.rs`), checked alongside the static grant map
3//! by [`crate::policy::AudienceReadPolicy`]/[`crate::policy::AudienceMintPolicy`] -- a selector
4//! present in either source grants access, without either side being deep-cloned or merged into
5//! a combined map (`current()` hands back the cached grants behind an `Arc`). This is what makes
6//! a grant creatable without a service restart -- the static map stays the bootstrap/test-only
7//! layer.
8//!
9//! The cache mechanics (cold-start throttling, last-good serving, `ProviderUnavailable`
10//! wrapping) live in [`crate::db_snapshot`], shared with [`crate::groups::DbGroupsSource`].
11
12use crate::db_api_key::resolve_u64;
13use crate::db_snapshot::{SnapshotLoader, SnapshotSource};
14use crate::policy::{AudienceGrants, GrantAxis};
15use anyhow::{Context, Result, anyhow};
16use async_trait::async_trait;
17use sqlx::{PgPool, Row};
18
19/// Cache-TTL knob for [`DbAudienceGrantsSource`], read from env with a default.
20#[derive(Clone, Copy, Debug)]
21pub struct DbAudienceGrantsConfig {
22 /// `MICROMEGAS_AUTH_CACHE_TTL_SECONDS`, default 60 -- a single flat, unprefixed knob (no
23 /// `{prefix}_` role variant) shared with `DbApiKeyConfig`/`DbGroupsConfig`'s own
24 /// positive-cache TTL: one value governs the API-key, audience-grant, and group snapshot
25 /// caches process-wide, across every role.
26 pub cache_ttl_secs: u64,
27}
28
29impl DbAudienceGrantsConfig {
30 /// Resolves the flat, unprefixed `MICROMEGAS_AUTH_CACHE_TTL_SECONDS` knob directly -- there
31 /// is exactly one value for this knob process-wide, so `prefix` is accepted (for call-site
32 /// symmetry with every other `from_env_with_prefix`) but not consulted here.
33 pub fn from_env_with_prefix(_prefix: &str) -> Self {
34 Self {
35 cache_ttl_secs: resolve_u64("", "AUTH_CACHE_TTL_SECONDS", 60),
36 }
37 }
38}
39
40/// [`SnapshotLoader`] for the `audience_grants` table.
41#[derive(Debug)]
42pub struct AudienceGrantsLoader;
43
44#[async_trait]
45impl SnapshotLoader for AudienceGrantsLoader {
46 type Snapshot = AudienceGrants;
47 const NAME: &'static str = "audience grant store";
48
49 /// Queries the whole table and builds an `AudienceGrants` via
50 /// [`AudienceGrants::from_rows`]. The one place a malformed row (one that slipped past the
51 /// table's own `CHECK` constraints, e.g. via a direct `psql` session) surfaces as a load
52 /// failure rather than a silently-inert or silently-unreadable grant.
53 async fn fetch(pool: &PgPool) -> Result<AudienceGrants> {
54 let rows = sqlx::query("SELECT audience, axis, selector FROM audience_grants")
55 .fetch_all(pool)
56 .await
57 .context("querying audience_grants")?;
58 let mut triples = Vec::with_capacity(rows.len());
59 for row in rows {
60 let audience: String = row.try_get("audience").context("reading audience")?;
61 let axis: String = row.try_get("axis").context("reading axis")?;
62 let selector: String = row.try_get("selector").context("reading selector")?;
63 let axis = match axis.as_str() {
64 "read" => GrantAxis::Read,
65 "mint" => GrantAxis::Mint,
66 other => {
67 return Err(anyhow!(
68 "audience_grants row for {audience:?}/{selector:?} has unrecognized \
69 axis {other:?}"
70 ));
71 }
72 };
73 triples.push((audience, axis, selector));
74 }
75 AudienceGrants::from_rows(triples)
76 }
77
78 fn count_refresh_error() {
79 micromegas_tracing::imetric!("audience_grant_refresh_error_count", "count", 1_u64);
80 }
81}
82
83/// The whole-table snapshot cache described in the module doc comment.
84pub type DbAudienceGrantsSource = SnapshotSource<AudienceGrantsLoader>;