Skip to main content

webgates_core/permissions/
application_validator.rs

1use crate::errors_core::Result;
2use crate::permissions::collision_checker::PermissionCollisionChecker;
3use crate::permissions::errors::PermissionsError;
4use crate::permissions::validation_report::ValidationReport;
5use tracing::info;
6
7/// High-level builder for validating your application's permission list.
8///
9/// `ApplicationValidator` collects permission strings from one or more sources,
10/// validates them for duplicates and hash collisions, and returns a
11/// [`ValidationReport`].
12///
13/// This is the easiest API to use at startup or in tests when you want to say:
14/// "here is the full permission surface of my application; make sure it is safe."
15///
16/// If you need reusable post-validation inspection helpers, use
17/// [`PermissionCollisionChecker`] directly.
18///
19/// # Examples
20///
21/// ```
22/// use webgates_core::permissions::application_validator::ApplicationValidator;///
23/// # fn load_config_permissions() -> Vec<String> { vec!["user:read".to_string()] }
24/// # async fn load_db_permissions() -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> { Ok(vec!["admin:write".to_string()]) }
25/// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
26/// let config_permissions = load_config_permissions();
27/// let db_permissions = load_db_permissions().await?;
28///
29/// let report = ApplicationValidator::new()
30///     .add_permissions(config_permissions)
31///     .add_permissions(db_permissions)
32///     .add_permission("system:health")
33///     .validate()?;
34///
35/// assert!(report.is_valid());
36/// # Ok(())
37/// # }
38/// ```
39///
40/// ```
41/// use webgates_core::permissions::application_validator::ApplicationValidator;
42/// use webgates_core::permissions::collision_checker::PermissionCollisionChecker;
43///
44/// let permissions = vec!["user:read".to_string(), "user:write".to_string()];
45///
46/// let report = ApplicationValidator::new()
47///     .add_permissions(permissions.clone())
48///     .validate()
49///     .map_err(|error| error.to_string())?;
50///
51/// let mut checker = PermissionCollisionChecker::new(permissions);
52/// let inspected = checker.validate().map_err(|error| error.to_string())?;
53///
54/// assert_eq!(report.is_valid(), inspected.is_valid());
55/// # Ok::<(), String>(())
56/// ```
57pub struct ApplicationValidator {
58    permissions: Vec<String>,
59}
60
61impl ApplicationValidator {
62    /// Creates a new empty validator.
63    pub fn new() -> Self {
64        Self {
65            permissions: Vec::new(),
66        }
67    }
68
69    /// Adds permissions from an iterator of string-like values.
70    ///
71    /// # Arguments
72    ///
73    /// * `permissions` - Iterator of items that can be converted to String
74    pub fn add_permissions<I, S>(mut self, permissions: I) -> Self
75    where
76        I: IntoIterator<Item = S>,
77        S: Into<String>,
78    {
79        self.permissions
80            .extend(permissions.into_iter().map(|s| s.into()));
81        self
82    }
83
84    /// Adds permissions from a vector of owned strings.
85    ///
86    /// This is a convenience method for callers that already have `Vec<String>`.
87    ///
88    /// # Arguments
89    ///
90    /// * `permissions` - Vector of permission strings
91    pub fn add_permission_strings(mut self, permissions: Vec<String>) -> Self {
92        self.permissions.extend(permissions);
93        self
94    }
95
96    /// Adds one permission string.
97    ///
98    /// # Arguments
99    ///
100    /// * `permission` - A single permission string to add
101    pub fn add_permission<S: Into<String>>(mut self, permission: S) -> Self {
102        self.permissions.push(permission.into());
103        self
104    }
105
106    /// Validates all collected permissions and returns a detailed report.
107    ///
108    /// This method performs validation and logs results automatically. It
109    /// returns a [`ValidationReport`] whether validation succeeds or fails,
110    /// unless the validation process itself encounters an unexpected error.
111    ///
112    /// # Returns
113    ///
114    /// * `Ok(ValidationReport)` - Complete validation report
115    /// * `Err(webgates_core::errors::Error)` - Validation process failed
116    pub fn validate(self) -> Result<ValidationReport, PermissionsError> {
117        let mut checker = PermissionCollisionChecker::new(self.permissions);
118        let report = checker.validate().map_err(|e| {
119            PermissionsError::collision(
120                0,
121                vec![format!("Permission validation process failed: {}", e)],
122            )
123        })?;
124
125        report.log_results();
126
127        if report.is_valid() {
128            info!("✓ Permission validation completed successfully");
129        }
130
131        Ok(report)
132    }
133
134    /// Returns how many permission strings are currently queued for validation.
135    pub fn permission_count(&self) -> usize {
136        self.permissions.len()
137    }
138}
139
140impl Default for ApplicationValidator {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn application_validator_basic() {
152        let result = ApplicationValidator::new()
153            .add_permissions(["user:read", "user:write"])
154            .add_permission("admin:delete")
155            .validate();
156
157        match result {
158            Ok(report) => assert!(report.is_valid()),
159            Err(error) => panic!("validation unexpectedly failed: {error}"),
160        }
161    }
162
163    #[test]
164    fn application_validator_with_duplicates() {
165        let result = ApplicationValidator::new()
166            .add_permissions(["user:read", "user:read"])
167            .validate();
168
169        match result {
170            Ok(report) => {
171                assert!(!report.is_valid());
172                assert!(!report.duplicates().is_empty());
173            }
174            Err(error) => panic!("validation unexpectedly failed: {error}"),
175        }
176    }
177}