Skip to main content

alibi_plugins/organization/
extensions.rs

1//! Configurable teams and persisted organization access control.
2use alibi_core::store::TeamStore;
3use alibi_core::types::{
4    CreateTeam, Organization, OrganizationPermissions, Team, TeamMember, UpdateTeam,
5};
6use alibi_core::wire::{SessionView, UserView};
7use alibi_core::{AuthRequest, AuthResult};
8use async_trait::async_trait;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Default)]
12pub struct TeamLimitContext {
13    pub organization_id: String,
14    pub team_id: Option<String>,
15    pub session: Option<SessionView>,
16    pub user: Option<UserView>,
17    /// The endpoint request supplied to the upstream maximum-teams callback.
18    pub request: Option<AuthRequest>,
19}
20
21/// Application quota callbacks use raw ECMAScript Number policies.
22///
23/// Migration from integer callbacks: return `AuthResult<Option<f64>>` and
24/// `Some(3.0)` instead of `AuthResult<Option<usize>>` and `Some(3)`.
25/// `None` means no quota; it does not model a JavaScript callback returning
26/// non-numeric values. Callbacks keep the actual existing contexts and follow each Source endpoint
27/// ordering contract.
28#[async_trait]
29pub trait OrganizationLimitResolver: std::fmt::Debug + Send + Sync {
30    async fn maximum_teams(&self, _context: &TeamLimitContext) -> AuthResult<Option<f64>> {
31        Ok(None)
32    }
33    async fn maximum_team_members(&self, _context: &TeamLimitContext) -> AuthResult<Option<f64>> {
34        Ok(None)
35    }
36    async fn maximum_roles(&self, _organization_id: &str) -> AuthResult<Option<f64>> {
37        Ok(None)
38    }
39}
40
41#[derive(Debug, Clone)]
42pub struct TeamHookContext {
43    pub organization: Organization,
44    pub user: Option<UserView>,
45}
46
47#[async_trait]
48pub trait OrganizationTeamHooks: std::fmt::Debug + Send + Sync {
49    async fn before_create(
50        &self,
51        _data: &mut CreateTeam,
52        _context: &TeamHookContext,
53    ) -> AuthResult<()> {
54        Ok(())
55    }
56    async fn after_create(&self, _team: &Team, _context: &TeamHookContext) -> AuthResult<()> {
57        Ok(())
58    }
59    async fn before_update(
60        &self,
61        _team: &Team,
62        _updates: &mut UpdateTeam,
63        _context: &TeamHookContext,
64    ) -> AuthResult<()> {
65        Ok(())
66    }
67    async fn after_update(&self, _team: &Team, _context: &TeamHookContext) -> AuthResult<()> {
68        Ok(())
69    }
70    async fn before_delete(&self, _team: &Team, _context: &TeamHookContext) -> AuthResult<()> {
71        Ok(())
72    }
73    async fn after_delete(&self, _team: &Team, _context: &TeamHookContext) -> AuthResult<()> {
74        Ok(())
75    }
76    async fn before_add_member(
77        &self,
78        _team: &Team,
79        _user: &UserView,
80        _context: &TeamHookContext,
81    ) -> AuthResult<()> {
82        Ok(())
83    }
84    async fn after_add_member(
85        &self,
86        _member: &TeamMember,
87        _team: &Team,
88        _user: &UserView,
89        _context: &TeamHookContext,
90    ) -> AuthResult<()> {
91        Ok(())
92    }
93    async fn before_remove_member(
94        &self,
95        _member: &TeamMember,
96        _team: &Team,
97        _user: &UserView,
98        _context: &TeamHookContext,
99    ) -> AuthResult<()> {
100        Ok(())
101    }
102    async fn after_remove_member(
103        &self,
104        _member: &TeamMember,
105        _team: &Team,
106        _user: &UserView,
107        _context: &TeamHookContext,
108    ) -> AuthResult<()> {
109        Ok(())
110    }
111}
112
113#[derive(Clone)]
114pub struct DefaultTeamContext {
115    pub request: Option<AuthRequest>,
116    pub user: UserView,
117    pub session: Option<SessionView>,
118    pub config: Arc<alibi_core::AuthConfig>,
119}
120
121impl std::fmt::Debug for DefaultTeamContext {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.debug_struct("DefaultTeamContext").finish_non_exhaustive()
124    }
125}
126
127#[async_trait]
128pub trait DefaultTeamFactory: std::fmt::Debug + Send + Sync {
129    async fn create(
130        &self,
131        organization: &Organization,
132        context: &DefaultTeamContext,
133        store: &dyn TeamStore,
134    ) -> AuthResult<Option<Team>>;
135}
136
137#[derive(Debug, Clone)]
138pub struct TeamsConfig {
139    pub enabled: bool,
140    pub create_default_team: bool,
141    pub allow_removing_all_teams: bool,
142    /// Raw team quota. Zero and NaN disable this quota; negative values deny
143    /// creation. Fractional values are compared to the Source adapter's list
144    /// length without rounding. `None` leaves the quota unset.
145    /// Integer configuration migrates from `Some(3)` to `Some(3.0)`.
146    pub maximum_teams: Option<f64>,
147    /// Raw durable seat quota, bound to the adapter's `member_count < maximum`
148    /// predicate. In SQLite zero, negative values, NaN and negative infinity
149    /// deny new seats; positive infinity admits them. Existing seats are retries.
150    /// Integer configuration migrates from `Some(3)` to `Some(3.0)`.
151    pub maximum_members_per_team: Option<f64>,
152    pub limit_resolver: Option<Arc<dyn OrganizationLimitResolver>>,
153    pub hooks: Option<Arc<dyn OrganizationTeamHooks>>,
154    pub default_team_factory: Option<Arc<dyn DefaultTeamFactory>>,
155}
156impl Default for TeamsConfig {
157    fn default() -> Self {
158        Self {
159            enabled: false,
160            create_default_team: true,
161            allow_removing_all_teams: false,
162            maximum_teams: None,
163            maximum_members_per_team: None,
164            limit_resolver: None,
165            hooks: None,
166            default_team_factory: None,
167        }
168    }
169}
170
171#[derive(Debug, Clone, Default)]
172pub struct DynamicAccessControlConfig {
173    pub enabled: bool,
174    /// Raw role quota, compared to the full persisted count. `None` is the
175    /// Source nullish default of positive infinity; zero/negative values deny,
176    /// NaN/positive infinity admit, and fractions are never rounded.
177    /// Integer configuration migrates from `Some(3)` to `Some(3.0)`.
178    pub maximum_roles_per_organization: Option<f64>,
179    pub limit_resolver: Option<Arc<dyn OrganizationLimitResolver>>,
180}
181
182#[must_use]
183pub fn default_organization_statements() -> OrganizationPermissions {
184    [
185        ("organization", vec!["update", "delete"]),
186        ("member", vec!["create", "update", "delete"]),
187        ("invitation", vec!["create", "cancel"]),
188        ("team", vec!["create", "update", "delete"]),
189        ("ac", vec!["create", "read", "update", "delete"]),
190    ]
191    .into_iter()
192    .map(|(resource, actions)| {
193        (
194            resource.to_owned(),
195            actions.into_iter().map(str::to_owned).collect(),
196        )
197    })
198    .collect()
199}
200
201/// Preserve explicit application API errors; ordinary team callback failures
202/// use the pinned empty HTTP 500 contract without exposing their private cause.
203pub(super) fn team_callback_error(error: alibi_core::AuthError) -> alibi_core::AuthError {
204    match error {
205        error @ (alibi_core::AuthError::Api { .. }
206        | alibi_core::AuthError::Upstream { .. }
207        | alibi_core::AuthError::CallbackFailure(_)) => error,
208        error if error.status_code() != 500 => error,
209        error => alibi_core::AuthError::CallbackFailure(Box::new(error)),
210    }
211}