micromegas_auth/membership.rs
1//! Wraps an inner [`crate::types::AuthProvider`] and resolves the caller's transitive local-group membership
2//! once per request -- the one resolution site upstream of every consumer (`is_admin()`,
3//! `caller_selectors`/`selector_matches`'s `group:` arm).
4
5use crate::groups::{DbGroupsSource, GroupGraph};
6use crate::types::{AuthContext, AuthProvider, RequestParts};
7use anyhow::Result;
8use async_trait::async_trait;
9use std::sync::Arc;
10
11/// The group-snapshot seam [`MembershipProvider`] resolves through -- implemented by
12/// [`DbGroupsSource`] in production, and by a canned/error-returning test double in
13/// `rust/auth/tests/membership_tests.rs`, so this crate's no-DB test policy can exercise the
14/// wrapper's success and failure paths without a live Postgres.
15#[async_trait]
16pub trait GroupSnapshot: Send + Sync {
17 /// Returns the current `GroupGraph` snapshot, or `Err` (typically a
18 /// [`crate::types::ProviderUnavailable`]) on a store outage with no prior successful load.
19 async fn current(&self) -> Result<Arc<GroupGraph>>;
20}
21
22#[async_trait]
23impl GroupSnapshot for DbGroupsSource {
24 async fn current(&self) -> Result<Arc<GroupGraph>> {
25 DbGroupsSource::current(self).await
26 }
27}
28
29/// Wraps `inner`, filling `AuthContext.memberships` with `groups.current()`'s closure over
30/// `ctx.email` after `inner` authenticates the request.
31pub struct MembershipProvider {
32 inner: Arc<dyn AuthProvider>,
33 groups: Arc<dyn GroupSnapshot>,
34}
35
36impl MembershipProvider {
37 /// Wraps `inner`, resolving membership from `groups` on every `validate_request` call.
38 pub fn new(inner: Arc<dyn AuthProvider>, groups: Arc<dyn GroupSnapshot>) -> Self {
39 Self { inner, groups }
40 }
41}
42
43#[async_trait]
44impl AuthProvider for MembershipProvider {
45 async fn validate_request(&self, parts: &dyn RequestParts) -> Result<AuthContext> {
46 let mut ctx = self.inner.validate_request(parts).await?;
47 // `ProviderUnavailable` propagates unchanged: a group-store outage denies the request
48 // the same way an inner-provider store outage does.
49 let graph = self.groups.current().await?;
50 ctx.memberships = graph.closure(ctx.email.as_deref()).into();
51 Ok(ctx)
52 }
53}