Skip to main content

steel_core/permission/
manager.rs

1use std::{error::Error, fmt, sync::Arc};
2
3use futures::future::BoxFuture;
4use steel_utils::locks::{AsyncMutex, SyncRwLock};
5
6use super::{
7    PermissionConfigError, PermissionGroups, PermissionGroupsConfig, PermissionMetadataSet,
8    PermissionSet,
9};
10
11/// Persists permission group configuration owned outside `steel-core`.
12pub trait PermissionGroupStore: Send + Sync {
13    /// Saves the complete permission group configuration.
14    fn save_groups(
15        &self,
16        config: PermissionGroupsConfig,
17    ) -> BoxFuture<'static, Result<(), PermissionGroupStoreError>>;
18}
19
20/// Permission group persistence failure.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct PermissionGroupStoreError {
23    message: String,
24}
25
26impl PermissionGroupStoreError {
27    /// Creates a persistence error from a displayable message.
28    #[must_use]
29    pub fn new(message: impl Into<String>) -> Self {
30        Self {
31            message: message.into(),
32        }
33    }
34}
35
36impl fmt::Display for PermissionGroupStoreError {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        formatter.write_str(&self.message)
39    }
40}
41
42impl Error for PermissionGroupStoreError {}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
45struct PermissionGroupManagerState {
46    config: PermissionGroupsConfig,
47    groups: PermissionGroups,
48}
49
50/// Runtime permission groups with serialized, persistence-first updates.
51pub struct PermissionGroupManager {
52    updates: AsyncMutex<()>,
53    state: SyncRwLock<PermissionGroupManagerState>,
54    store: Option<Arc<dyn PermissionGroupStore>>,
55}
56
57impl fmt::Debug for PermissionGroupManager {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        formatter
60            .debug_struct("PermissionGroupManager")
61            .field("updates", &self.updates)
62            .field("state", &self.state)
63            .field("store", &self.store.as_ref().map(|_| "<group store>"))
64            .finish()
65    }
66}
67
68impl PermissionGroupManager {
69    /// Builds a manager from typed config and an optional persistence store.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error when the initial config does not resolve.
74    pub fn new(
75        config: PermissionGroupsConfig,
76        store: Option<Arc<dyn PermissionGroupStore>>,
77    ) -> Result<Self, PermissionConfigError> {
78        let groups = PermissionGroups::from_config(config.clone())?;
79        Ok(Self {
80            updates: AsyncMutex::new(()),
81            state: SyncRwLock::new(PermissionGroupManagerState { config, groups }),
82            store,
83        })
84    }
85
86    /// Builds a manager without persistence.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error when the initial config does not resolve.
91    pub fn transient(config: PermissionGroupsConfig) -> Result<Self, PermissionConfigError> {
92        Self::new(config, None)
93    }
94
95    /// Returns a typed snapshot of the current config.
96    #[must_use]
97    pub fn config_snapshot(&self) -> PermissionGroupsConfig {
98        self.state.read().config.clone()
99    }
100
101    /// Returns whether a configured group exists.
102    #[must_use]
103    pub fn contains_group(&self, group: &str) -> bool {
104        self.state.read().groups.contains_group(group)
105    }
106
107    /// Returns configured group names sorted by name.
108    #[must_use]
109    pub fn group_names(&self) -> Vec<String> {
110        self.state.read().groups.groups().keys().cloned().collect()
111    }
112
113    /// Builds an effective permission set from the current group snapshot.
114    #[must_use]
115    pub fn effective_permissions(
116        &self,
117        assigned_groups: &[String],
118        subject_permissions: &PermissionSet,
119    ) -> PermissionSet {
120        self.state
121            .read()
122            .groups
123            .effective_permissions(assigned_groups, subject_permissions)
124    }
125
126    /// Builds effective metadata from the current group snapshot.
127    #[must_use]
128    pub fn effective_metadata(
129        &self,
130        assigned_groups: &[String],
131        subject_metadata: &PermissionMetadataSet,
132    ) -> PermissionMetadataSet {
133        self.state
134            .read()
135            .groups
136            .effective_metadata(assigned_groups, subject_metadata)
137    }
138
139    /// Replaces the complete config after validation and optional persistence.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error when validation or persistence fails.
144    pub async fn replace_config(
145        &self,
146        config: PermissionGroupsConfig,
147    ) -> Result<(), PermissionGroupManagerError> {
148        let _guard = self.updates.lock().await;
149        self.replace_config_locked(config).await
150    }
151
152    /// Updates the latest config under the manager update lock.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error when the updated config is invalid or cannot be persisted.
157    pub async fn update_config(
158        &self,
159        update: impl FnOnce(&mut PermissionGroupsConfig) + Send,
160    ) -> Result<(), PermissionGroupManagerError> {
161        let _guard = self.updates.lock().await;
162        let mut config = self.state.read().config.clone();
163        update(&mut config);
164        self.replace_config_locked(config).await
165    }
166
167    /// Updates the latest config with a fallible caller-owned edit.
168    ///
169    /// # Errors
170    ///
171    /// Returns the edit error, or a validation or persistence error.
172    pub async fn try_update_config<T, E>(
173        &self,
174        update: impl FnOnce(&mut PermissionGroupsConfig) -> Result<T, E> + Send,
175    ) -> Result<T, PermissionGroupUpdateError<E>>
176    where
177        T: Send,
178        E: Send,
179    {
180        let _guard = self.updates.lock().await;
181        let current = self.state.read().config.clone();
182        let mut config = current.clone();
183        let result = update(&mut config).map_err(PermissionGroupUpdateError::Edit)?;
184        if config == current {
185            return Ok(result);
186        }
187
188        self.replace_config_locked(config)
189            .await
190            .map_err(PermissionGroupUpdateError::Manager)?;
191        Ok(result)
192    }
193
194    async fn replace_config_locked(
195        &self,
196        config: PermissionGroupsConfig,
197    ) -> Result<(), PermissionGroupManagerError> {
198        let groups = PermissionGroups::from_config(config.clone())?;
199        if let Some(store) = &self.store {
200            store.save_groups(config.clone()).await?;
201        }
202        *self.state.write() = PermissionGroupManagerState { config, groups };
203        Ok(())
204    }
205}
206
207/// Permission group manager update failure.
208#[derive(Clone, Debug, PartialEq, Eq)]
209pub enum PermissionGroupManagerError {
210    /// The candidate config is invalid.
211    Config(PermissionConfigError),
212    /// The candidate config could not be persisted.
213    Store(PermissionGroupStoreError),
214}
215
216impl fmt::Display for PermissionGroupManagerError {
217    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        match self {
219            Self::Config(error) => write!(formatter, "invalid permission groups config: {error}"),
220            Self::Store(error) => write!(formatter, "failed to store permission groups: {error}"),
221        }
222    }
223}
224
225impl Error for PermissionGroupManagerError {}
226
227impl From<PermissionConfigError> for PermissionGroupManagerError {
228    fn from(value: PermissionConfigError) -> Self {
229        Self::Config(value)
230    }
231}
232
233impl From<PermissionGroupStoreError> for PermissionGroupManagerError {
234    fn from(value: PermissionGroupStoreError) -> Self {
235        Self::Store(value)
236    }
237}
238
239/// Fallible edit failure from `PermissionGroupManager::try_update_config`.
240#[derive(Clone, Debug, PartialEq, Eq)]
241pub enum PermissionGroupUpdateError<E> {
242    /// The caller rejected its edit before persistence.
243    Edit(E),
244    /// The edited config failed validation or persistence.
245    Manager(PermissionGroupManagerError),
246}
247
248impl<E: fmt::Display> fmt::Display for PermissionGroupUpdateError<E> {
249    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            Self::Edit(error) => error.fmt(formatter),
252            Self::Manager(error) => error.fmt(formatter),
253        }
254    }
255}
256
257impl<E> Error for PermissionGroupUpdateError<E> where E: Error + 'static {}
258
259impl<E> From<PermissionGroupManagerError> for PermissionGroupUpdateError<E> {
260    fn from(value: PermissionGroupManagerError) -> Self {
261        Self::Manager(value)
262    }
263}