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