Skip to main content

cdx_core/security/
access_control.rs

1//! Access control and permissions for Codex documents.
2//!
3//! This module provides a permission system for controlling access to document
4//! operations like viewing, printing, copying, editing, and annotating.
5
6use serde::{Deserialize, Serialize};
7
8/// Access control settings for a document.
9///
10/// Defines default permissions and per-principal permission grants.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct AccessControl {
14    /// Default permissions for unauthenticated/unknown principals.
15    pub default: Permissions,
16
17    /// Per-principal permission grants.
18    #[serde(default, skip_serializing_if = "Vec::is_empty")]
19    pub grants: Vec<PermissionGrant>,
20}
21
22impl AccessControl {
23    /// Create access control with default permissions.
24    #[must_use]
25    pub fn new(default: Permissions) -> Self {
26        Self {
27            default,
28            grants: Vec::new(),
29        }
30    }
31
32    /// Create access control with all permissions denied by default.
33    #[must_use]
34    pub fn deny_all() -> Self {
35        Self::new(Permissions::none())
36    }
37
38    /// Create access control with read-only permissions by default.
39    #[must_use]
40    pub fn read_only() -> Self {
41        Self::new(Permissions::read_only())
42    }
43
44    /// Create access control with all permissions granted by default.
45    #[must_use]
46    pub fn full_access() -> Self {
47        Self::new(Permissions::all())
48    }
49
50    /// Add a permission grant for a principal.
51    #[must_use]
52    pub fn with_grant(mut self, grant: PermissionGrant) -> Self {
53        self.grants.push(grant);
54        self
55    }
56
57    /// Get effective permissions for a principal.
58    ///
59    /// Checks specific grants (User/Group/Role) first, then falls back to
60    /// `Everyone` grants, then the default permissions.
61    #[must_use]
62    pub fn permissions_for(&self, principal: &Principal) -> &Permissions {
63        // Specific matches first (skip Everyone)
64        for grant in &self.grants {
65            if !matches!(grant.principal, Principal::Everyone) && grant.principal.matches(principal)
66            {
67                return &grant.permissions;
68            }
69        }
70        // Then wildcard
71        for grant in &self.grants {
72            if matches!(grant.principal, Principal::Everyone) {
73                return &grant.permissions;
74            }
75        }
76        &self.default
77    }
78
79    /// Check if a principal can perform an operation.
80    #[must_use]
81    pub fn can(&self, principal: &Principal, operation: Operation) -> bool {
82        let perms = self.permissions_for(principal);
83        match operation {
84            Operation::View => perms.view,
85            Operation::Print => perms.print,
86            Operation::Copy => perms.copy,
87            Operation::Annotate => perms.annotate,
88            Operation::Edit => perms.edit,
89            Operation::Sign => perms.sign,
90            Operation::Decrypt => perms.decrypt,
91        }
92    }
93}
94
95impl Default for AccessControl {
96    fn default() -> Self {
97        Self::read_only()
98    }
99}
100
101/// Permission settings.
102///
103/// This struct uses boolean flags for each permission type as specified by the
104/// Codex Document Format specification. Each flag represents an independent
105/// permission that can be granted or denied.
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108#[allow(clippy::struct_excessive_bools)]
109pub struct Permissions {
110    /// Permission to view/read the document.
111    #[serde(default = "default_true")]
112    pub view: bool,
113
114    /// Permission to print the document.
115    #[serde(default)]
116    pub print: bool,
117
118    /// Permission to copy content from the document.
119    #[serde(default)]
120    pub copy: bool,
121
122    /// Permission to add annotations/comments.
123    #[serde(default)]
124    pub annotate: bool,
125
126    /// Permission to edit the document.
127    #[serde(default)]
128    pub edit: bool,
129
130    /// Permission to sign the document.
131    #[serde(default)]
132    pub sign: bool,
133
134    /// Permission to decrypt (if encrypted).
135    #[serde(default)]
136    pub decrypt: bool,
137}
138
139fn default_true() -> bool {
140    true
141}
142
143impl Permissions {
144    /// Create permissions with all operations denied.
145    #[must_use]
146    pub const fn none() -> Self {
147        Self {
148            view: false,
149            print: false,
150            copy: false,
151            annotate: false,
152            edit: false,
153            sign: false,
154            decrypt: false,
155        }
156    }
157
158    /// Create permissions with all operations allowed.
159    #[must_use]
160    pub const fn all() -> Self {
161        Self {
162            view: true,
163            print: true,
164            copy: true,
165            annotate: true,
166            edit: true,
167            sign: true,
168            decrypt: true,
169        }
170    }
171
172    /// Create read-only permissions (view only).
173    #[must_use]
174    pub const fn read_only() -> Self {
175        Self {
176            view: true,
177            print: false,
178            copy: false,
179            annotate: false,
180            edit: false,
181            sign: false,
182            decrypt: false,
183        }
184    }
185
186    /// Create view and print permissions.
187    #[must_use]
188    pub const fn view_and_print() -> Self {
189        Self {
190            view: true,
191            print: true,
192            copy: false,
193            annotate: false,
194            edit: false,
195            sign: false,
196            decrypt: false,
197        }
198    }
199
200    /// Create reviewer permissions (view, annotate, sign).
201    #[must_use]
202    pub const fn reviewer() -> Self {
203        Self {
204            view: true,
205            print: true,
206            copy: true,
207            annotate: true,
208            edit: false,
209            sign: true,
210            decrypt: true,
211        }
212    }
213
214    /// Create editor permissions (all except sign).
215    #[must_use]
216    pub const fn editor() -> Self {
217        Self {
218            view: true,
219            print: true,
220            copy: true,
221            annotate: true,
222            edit: true,
223            sign: false,
224            decrypt: true,
225        }
226    }
227}
228
229impl Default for Permissions {
230    fn default() -> Self {
231        Self::read_only()
232    }
233}
234
235/// A permission grant for a specific principal.
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct PermissionGrant {
239    /// The principal receiving the permissions.
240    pub principal: Principal,
241
242    /// The granted permissions.
243    pub permissions: Permissions,
244}
245
246impl PermissionGrant {
247    /// Create a new permission grant.
248    #[must_use]
249    pub fn new(principal: Principal, permissions: Permissions) -> Self {
250        Self {
251            principal,
252            permissions,
253        }
254    }
255
256    /// Grant full access to a user.
257    #[must_use]
258    pub fn full_access_for_user(email: impl Into<String>) -> Self {
259        Self::new(Principal::User(email.into()), Permissions::all())
260    }
261
262    /// Grant reviewer access to a user.
263    #[must_use]
264    pub fn reviewer_for_user(email: impl Into<String>) -> Self {
265        Self::new(Principal::User(email.into()), Permissions::reviewer())
266    }
267
268    /// Grant editor access to a user.
269    #[must_use]
270    pub fn editor_for_user(email: impl Into<String>) -> Self {
271        Self::new(Principal::User(email.into()), Permissions::editor())
272    }
273}
274
275/// A security principal (user, group, or role).
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(tag = "type", content = "id", rename_all = "lowercase")]
278pub enum Principal {
279    /// A specific user identified by email or ID.
280    User(String),
281
282    /// A group of users.
283    Group(String),
284
285    /// A role (e.g., "admin", "reviewer").
286    Role(String),
287
288    /// Everyone (wildcard).
289    Everyone,
290}
291
292impl Principal {
293    /// Create a user principal.
294    #[must_use]
295    pub fn user(id: impl Into<String>) -> Self {
296        Self::User(id.into())
297    }
298
299    /// Create a group principal.
300    #[must_use]
301    pub fn group(id: impl Into<String>) -> Self {
302        Self::Group(id.into())
303    }
304
305    /// Create a role principal.
306    #[must_use]
307    pub fn role(id: impl Into<String>) -> Self {
308        Self::Role(id.into())
309    }
310
311    /// Check if this principal matches another.
312    ///
313    /// `Everyone` matches all principals. Otherwise, exact match is required.
314    #[must_use]
315    pub fn matches(&self, other: &Principal) -> bool {
316        match self {
317            Self::Everyone => true,
318            Self::User(id) => matches!(other, Self::User(other_id) if id == other_id),
319            Self::Group(id) => matches!(other, Self::Group(other_id) if id == other_id),
320            Self::Role(id) => matches!(other, Self::Role(other_id) if id == other_id),
321        }
322    }
323}
324
325impl std::fmt::Display for Principal {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        match self {
328            Self::User(id) => write!(f, "user:{id}"),
329            Self::Group(id) => write!(f, "group:{id}"),
330            Self::Role(id) => write!(f, "role:{id}"),
331            Self::Everyone => write!(f, "*"),
332        }
333    }
334}
335
336/// Operations that can be controlled by permissions.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display)]
338#[strum(serialize_all = "lowercase")]
339pub enum Operation {
340    /// View/read the document.
341    View,
342    /// Print the document.
343    Print,
344    /// Copy content from the document.
345    Copy,
346    /// Add annotations or comments.
347    Annotate,
348    /// Edit the document content.
349    Edit,
350    /// Add a signature.
351    Sign,
352    /// Decrypt the document (if encrypted).
353    Decrypt,
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_permissions_none() {
362        let perms = Permissions::none();
363        assert!(!perms.view);
364        assert!(!perms.print);
365        assert!(!perms.edit);
366    }
367
368    #[test]
369    fn test_permissions_all() {
370        let perms = Permissions::all();
371        assert!(perms.view);
372        assert!(perms.print);
373        assert!(perms.copy);
374        assert!(perms.annotate);
375        assert!(perms.edit);
376        assert!(perms.sign);
377        assert!(perms.decrypt);
378    }
379
380    #[test]
381    fn test_permissions_read_only() {
382        let perms = Permissions::read_only();
383        assert!(perms.view);
384        assert!(!perms.print);
385        assert!(!perms.edit);
386    }
387
388    #[test]
389    fn test_access_control_default() {
390        let ac = AccessControl::read_only();
391        let user = Principal::user("test@example.com");
392
393        assert!(ac.can(&user, Operation::View));
394        assert!(!ac.can(&user, Operation::Print));
395        assert!(!ac.can(&user, Operation::Edit));
396    }
397
398    #[test]
399    fn test_access_control_with_grant() {
400        let ac = AccessControl::deny_all()
401            .with_grant(PermissionGrant::full_access_for_user("admin@example.com"));
402
403        let admin = Principal::user("admin@example.com");
404        let user = Principal::user("user@example.com");
405
406        assert!(ac.can(&admin, Operation::View));
407        assert!(ac.can(&admin, Operation::Edit));
408        assert!(!ac.can(&user, Operation::View));
409    }
410
411    #[test]
412    fn test_principal_matches() {
413        let everyone = Principal::Everyone;
414        let user = Principal::user("test@example.com");
415
416        assert!(everyone.matches(&user));
417        assert!(user.matches(&user));
418        assert!(!user.matches(&Principal::user("other@example.com")));
419    }
420
421    #[test]
422    fn test_principal_display() {
423        assert_eq!(
424            Principal::user("test@example.com").to_string(),
425            "user:test@example.com"
426        );
427        assert_eq!(Principal::group("admins").to_string(), "group:admins");
428        assert_eq!(Principal::role("reviewer").to_string(), "role:reviewer");
429        assert_eq!(Principal::Everyone.to_string(), "*");
430    }
431
432    #[test]
433    fn test_serialization() {
434        let ac = AccessControl::read_only().with_grant(PermissionGrant::new(
435            Principal::user("admin@example.com"),
436            Permissions::all(),
437        ));
438
439        let json = serde_json::to_string_pretty(&ac).unwrap();
440        assert!(json.contains("\"view\": true"));
441        assert!(json.contains("admin@example.com"));
442
443        let deserialized: AccessControl = serde_json::from_str(&json).unwrap();
444        assert_eq!(deserialized.grants.len(), 1);
445    }
446
447    #[test]
448    fn test_reviewer_permissions() {
449        let perms = Permissions::reviewer();
450        assert!(perms.view);
451        assert!(perms.annotate);
452        assert!(perms.sign);
453        assert!(!perms.edit);
454    }
455
456    #[test]
457    fn test_editor_permissions() {
458        let perms = Permissions::editor();
459        assert!(perms.view);
460        assert!(perms.edit);
461        assert!(!perms.sign);
462    }
463
464    #[test]
465    fn test_permissions_for_specificity() {
466        // Everyone → read_only, but admin user → full_access
467        // Admin should get full_access even though Everyone appears first
468        let ac = AccessControl::deny_all()
469            .with_grant(PermissionGrant::new(
470                Principal::Everyone,
471                Permissions::read_only(),
472            ))
473            .with_grant(PermissionGrant::full_access_for_user("admin@example.com"));
474
475        let admin = Principal::user("admin@example.com");
476        let perms = ac.permissions_for(&admin);
477        assert!(
478            perms.edit,
479            "admin should get full_access, not read_only from Everyone"
480        );
481        assert!(perms.sign);
482
483        // Regular user should still get Everyone's read_only
484        let user = Principal::user("user@example.com");
485        let perms = ac.permissions_for(&user);
486        assert!(perms.view);
487        assert!(!perms.edit);
488    }
489}