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)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_admin_has_all_permissions() {
154        let all_permissions = [
155            Permission::GenerateData,
156            Permission::ManageJobs,
157            Permission::ViewJobs,
158            Permission::ManageConfig,
159            Permission::ViewConfig,
160            Permission::ViewMetrics,
161            Permission::ManageApiKeys,
162        ];
163        for perm in &all_permissions {
164            assert!(
165                RolePermissions::has_permission(&Role::Admin, perm),
166                "Admin should have permission: {}",
167                perm
168            );
169        }
170    }
171
172    #[test]
173    fn test_viewer_denied_generate() {
174        assert!(!RolePermissions::has_permission(
175            &Role::Viewer,
176            &Permission::GenerateData
177        ));
178        assert!(!RolePermissions::has_permission(
179            &Role::Viewer,
180            &Permission::ManageJobs
181        ));
182        assert!(!RolePermissions::has_permission(
183            &Role::Viewer,
184            &Permission::ManageConfig
185        ));
186        assert!(!RolePermissions::has_permission(
187            &Role::Viewer,
188            &Permission::ManageApiKeys
189        ));
190    }
191
192    #[test]
193    fn test_viewer_allowed_read_only() {
194        assert!(RolePermissions::has_permission(
195            &Role::Viewer,
196            &Permission::ViewJobs
197        ));
198        assert!(RolePermissions::has_permission(
199            &Role::Viewer,
200            &Permission::ViewConfig
201        ));
202        assert!(RolePermissions::has_permission(
203            &Role::Viewer,
204            &Permission::ViewMetrics
205        ));
206    }
207
208    #[test]
209    fn test_operator_permissions() {
210        // Allowed
211        assert!(RolePermissions::has_permission(
212            &Role::Operator,
213            &Permission::GenerateData
214        ));
215        assert!(RolePermissions::has_permission(
216            &Role::Operator,
217            &Permission::ManageJobs
218        ));
219        assert!(RolePermissions::has_permission(
220            &Role::Operator,
221            &Permission::ViewJobs
222        ));
223        assert!(RolePermissions::has_permission(
224            &Role::Operator,
225            &Permission::ViewConfig
226        ));
227        assert!(RolePermissions::has_permission(
228            &Role::Operator,
229            &Permission::ViewMetrics
230        ));
231
232        // Denied
233        assert!(!RolePermissions::has_permission(
234            &Role::Operator,
235            &Permission::ManageConfig
236        ));
237        assert!(!RolePermissions::has_permission(
238            &Role::Operator,
239            &Permission::ManageApiKeys
240        ));
241    }
242
243    #[test]
244    fn test_default_role_is_operator() {
245        let role = Role::default();
246        assert_eq!(role, Role::Operator);
247    }
248
249    #[test]
250    fn test_rbac_config_default_disabled() {
251        let config = RbacConfig::default();
252        assert!(!config.enabled);
253    }
254
255    #[test]
256    fn test_role_serialization_roundtrip() {
257        let role = Role::Admin;
258        let json = serde_json::to_string(&role).unwrap();
259        assert_eq!(json, "\"admin\"");
260        let deserialized: Role = serde_json::from_str(&json).unwrap();
261        assert_eq!(deserialized, Role::Admin);
262    }
263
264    #[test]
265    fn test_permissions_for_role() {
266        let admin_perms = RolePermissions::permissions_for(&Role::Admin);
267        assert_eq!(admin_perms.len(), 7);
268
269        let operator_perms = RolePermissions::permissions_for(&Role::Operator);
270        assert_eq!(operator_perms.len(), 5);
271
272        let viewer_perms = RolePermissions::permissions_for(&Role::Viewer);
273        assert_eq!(viewer_perms.len(), 3);
274    }
275}