Skip to main content

datasynth_server/rest/
rbac.rs

1//! Role-Based Access Control (RBAC) for the REST API.
2//!
3//! Defines roles, permissions, and authorization logic. Roles are hierarchical:
4//! - **Admin**: full access to all operations
5//! - **Operator**: can generate data and manage jobs, but cannot manage API keys
6//! - **Viewer**: read-only access to jobs, config, and metrics
7
8use serde::{Deserialize, Serialize};
9
10// ===========================================================================
11// Roles
12// ===========================================================================
13
14/// User roles for access control.
15#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Role {
18    /// Full access to all operations including API key management.
19    Admin,
20    /// Can generate data, manage and view jobs, and view config/metrics.
21    #[default]
22    Operator,
23    /// Read-only access: view jobs, config, and metrics.
24    Viewer,
25}
26
27impl std::fmt::Display for Role {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Role::Admin => write!(f, "admin"),
31            Role::Operator => write!(f, "operator"),
32            Role::Viewer => write!(f, "viewer"),
33        }
34    }
35}
36
37// ===========================================================================
38// Permissions
39// ===========================================================================
40
41/// Fine-grained permissions that can be checked against a role.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum Permission {
45    /// Start a data generation job.
46    GenerateData,
47    /// Create, cancel, or modify generation jobs.
48    ManageJobs,
49    /// View job status and history.
50    ViewJobs,
51    /// Create or update server/generation configuration.
52    ManageConfig,
53    /// View current configuration.
54    ViewConfig,
55    /// View server metrics and health data.
56    ViewMetrics,
57    /// Create, revoke, or rotate API keys.
58    ManageApiKeys,
59}
60
61impl std::fmt::Display for Permission {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Permission::GenerateData => write!(f, "generate_data"),
65            Permission::ManageJobs => write!(f, "manage_jobs"),
66            Permission::ViewJobs => write!(f, "view_jobs"),
67            Permission::ManageConfig => write!(f, "manage_config"),
68            Permission::ViewConfig => write!(f, "view_config"),
69            Permission::ViewMetrics => write!(f, "view_metrics"),
70            Permission::ManageApiKeys => write!(f, "manage_api_keys"),
71        }
72    }
73}
74
75// ===========================================================================
76// Role → Permission mapping
77// ===========================================================================
78
79/// Resolves whether a given role has a specific permission.
80pub struct RolePermissions;
81
82impl RolePermissions {
83    /// Check if `role` is granted `permission`.
84    ///
85    /// Permission matrix:
86    ///
87    /// | Permission      | Admin | Operator | Viewer |
88    /// |-----------------|-------|----------|--------|
89    /// | GenerateData    |   Y   |    Y     |   N    |
90    /// | ManageJobs      |   Y   |    Y     |   N    |
91    /// | ViewJobs        |   Y   |    Y     |   Y    |
92    /// | ManageConfig    |   Y   |    N     |   N    |
93    /// | ViewConfig      |   Y   |    Y     |   Y    |
94    /// | ViewMetrics     |   Y   |    Y     |   Y    |
95    /// | ManageApiKeys   |   Y   |    N     |   N    |
96    pub fn has_permission(role: &Role, permission: &Permission) -> bool {
97        match role {
98            Role::Admin => true,
99            Role::Operator => matches!(
100                permission,
101                Permission::GenerateData
102                    | Permission::ManageJobs
103                    | Permission::ViewJobs
104                    | Permission::ViewConfig
105                    | Permission::ViewMetrics
106            ),
107            Role::Viewer => matches!(
108                permission,
109                Permission::ViewJobs | Permission::ViewConfig | Permission::ViewMetrics
110            ),
111        }
112    }
113
114    /// Return all permissions granted to a role.
115    pub fn permissions_for(role: &Role) -> Vec<Permission> {
116        let all = [
117            Permission::GenerateData,
118            Permission::ManageJobs,
119            Permission::ViewJobs,
120            Permission::ManageConfig,
121            Permission::ViewConfig,
122            Permission::ViewMetrics,
123            Permission::ManageApiKeys,
124        ];
125        all.into_iter()
126            .filter(|p| Self::has_permission(role, p))
127            .collect()
128    }
129}
130
131// ===========================================================================
132// Configuration
133// ===========================================================================
134
135/// Configuration for the RBAC subsystem.
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137pub struct RbacConfig {
138    /// Whether RBAC enforcement is enabled.
139    /// When `false`, all authenticated requests are treated as Admin.
140    #[serde(default)]
141    pub enabled: bool,
142}
143
144// ===========================================================================
145// Tests
146// ===========================================================================
147
148#[cfg(test)]
149#[allow(clippy::unwrap_used)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_admin_has_all_permissions() {
155        let all_permissions = [
156            Permission::GenerateData,
157            Permission::ManageJobs,
158            Permission::ViewJobs,
159            Permission::ManageConfig,
160            Permission::ViewConfig,
161            Permission::ViewMetrics,
162            Permission::ManageApiKeys,
163        ];
164        for perm in &all_permissions {
165            assert!(
166                RolePermissions::has_permission(&Role::Admin, perm),
167                "Admin should have permission: {}",
168                perm
169            );
170        }
171    }
172
173    #[test]
174    fn test_viewer_denied_generate() {
175        assert!(!RolePermissions::has_permission(
176            &Role::Viewer,
177            &Permission::GenerateData
178        ));
179        assert!(!RolePermissions::has_permission(
180            &Role::Viewer,
181            &Permission::ManageJobs
182        ));
183        assert!(!RolePermissions::has_permission(
184            &Role::Viewer,
185            &Permission::ManageConfig
186        ));
187        assert!(!RolePermissions::has_permission(
188            &Role::Viewer,
189            &Permission::ManageApiKeys
190        ));
191    }
192
193    #[test]
194    fn test_viewer_allowed_read_only() {
195        assert!(RolePermissions::has_permission(
196            &Role::Viewer,
197            &Permission::ViewJobs
198        ));
199        assert!(RolePermissions::has_permission(
200            &Role::Viewer,
201            &Permission::ViewConfig
202        ));
203        assert!(RolePermissions::has_permission(
204            &Role::Viewer,
205            &Permission::ViewMetrics
206        ));
207    }
208
209    #[test]
210    fn test_operator_permissions() {
211        // Allowed
212        assert!(RolePermissions::has_permission(
213            &Role::Operator,
214            &Permission::GenerateData
215        ));
216        assert!(RolePermissions::has_permission(
217            &Role::Operator,
218            &Permission::ManageJobs
219        ));
220        assert!(RolePermissions::has_permission(
221            &Role::Operator,
222            &Permission::ViewJobs
223        ));
224        assert!(RolePermissions::has_permission(
225            &Role::Operator,
226            &Permission::ViewConfig
227        ));
228        assert!(RolePermissions::has_permission(
229            &Role::Operator,
230            &Permission::ViewMetrics
231        ));
232
233        // Denied
234        assert!(!RolePermissions::has_permission(
235            &Role::Operator,
236            &Permission::ManageConfig
237        ));
238        assert!(!RolePermissions::has_permission(
239            &Role::Operator,
240            &Permission::ManageApiKeys
241        ));
242    }
243
244    #[test]
245    fn test_default_role_is_operator() {
246        let role = Role::default();
247        assert_eq!(role, Role::Operator);
248    }
249
250    #[test]
251    fn test_rbac_config_default_disabled() {
252        let config = RbacConfig::default();
253        assert!(!config.enabled);
254    }
255
256    #[test]
257    fn test_role_serialization_roundtrip() {
258        let role = Role::Admin;
259        let json = serde_json::to_string(&role).unwrap();
260        assert_eq!(json, "\"admin\"");
261        let deserialized: Role = serde_json::from_str(&json).unwrap();
262        assert_eq!(deserialized, Role::Admin);
263    }
264
265    #[test]
266    fn test_permissions_for_role() {
267        let admin_perms = RolePermissions::permissions_for(&Role::Admin);
268        assert_eq!(admin_perms.len(), 7);
269
270        let operator_perms = RolePermissions::permissions_for(&Role::Operator);
271        assert_eq!(operator_perms.len(), 5);
272
273        let viewer_perms = RolePermissions::permissions_for(&Role::Viewer);
274        assert_eq!(viewer_perms.len(), 3);
275    }
276}