Skip to main content

ldap_acis/
operation.rs

1//! LDAP operations and permissions.
2
3use acls_rs::algebra::{BoundedJoinSemilattice, BoundedMeetSemilattice};
4use acls_rs::permission::AtomicPermission;
5use acls_rs::prelude::*;
6
7use std::cmp::Ordering;
8use std::fmt;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// LDAP operation types.
14///
15/// Partially ordered by subsumption: `All` is greatest (grants everything),
16/// `SelfWrite > Modify`, all other pairs are incomparable.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19#[non_exhaustive]
20pub enum OperationType {
21    /// Bind (authenticate) operation
22    Bind,
23    /// Search operation
24    Search,
25    /// Compare attribute values
26    Compare,
27    /// Read entry/attributes
28    Read,
29    /// Add new entry
30    Add,
31    /// Delete entry
32    Delete,
33    /// Modify entry attributes
34    Modify,
35    /// Modify DN (rename/move)
36    ModifyDn,
37    /// All operations (389-ds shorthand)
38    All,
39    /// Self-write: user can add/remove own DN as attribute value (SLAPI_ACL_SELF | SLAPI_ACL_WRITE)
40    SelfWrite,
41}
42
43impl OperationType {
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            Self::Bind => "bind",
47            Self::Search => "search",
48            Self::Compare => "compare",
49            Self::Read => "read",
50            Self::Add => "add",
51            Self::Delete => "delete",
52            Self::Modify => "modify",
53            Self::ModifyDn => "modifydn",
54            Self::All => "all",
55            Self::SelfWrite => "selfwrite",
56        }
57    }
58
59    /// Whether `self` grants `other`.
60    /// `All` grants every operation; otherwise exact equality is required.
61    pub fn grants(&self, other: &OperationType) -> bool {
62        *self == OperationType::All
63            || *self == *other
64            || (*self == OperationType::SelfWrite && *other == OperationType::Modify)
65    }
66
67    /// Whether this operation is a write-class operation
68    /// (modify, add, delete, modifydn, or all).
69    pub fn is_write(&self) -> bool {
70        matches!(
71            self,
72            Self::Modify | Self::Add | Self::Delete | Self::ModifyDn | Self::All | Self::SelfWrite
73        )
74    }
75
76    /// Whether this operation is a read-class operation
77    /// (read, search, compare, or all).
78    pub fn is_read(&self) -> bool {
79        matches!(self, Self::Read | Self::Search | Self::Compare | Self::All)
80    }
81
82    fn bit(self) -> u16 {
83        match self {
84            Self::Bind => 1 << 0,
85            Self::Search => 1 << 1,
86            Self::Compare => 1 << 2,
87            Self::Read => 1 << 3,
88            Self::Add => 1 << 4,
89            Self::Delete => 1 << 5,
90            Self::Modify => 1 << 6,
91            Self::ModifyDn => 1 << 7,
92            Self::SelfWrite => 1 << 8,
93            Self::All => OperationSet::ALL_CONCRETE,
94        }
95    }
96
97    /// Returns the bit position for this operation type (0-8).
98    /// Returns `None` for `OperationType::All` since it doesn't map to a single bit.
99    pub fn bit_index(self) -> Option<usize> {
100        match self {
101            Self::Bind => Some(0),
102            Self::Search => Some(1),
103            Self::Compare => Some(2),
104            Self::Read => Some(3),
105            Self::Add => Some(4),
106            Self::Delete => Some(5),
107            Self::Modify => Some(6),
108            Self::ModifyDn => Some(7),
109            Self::SelfWrite => Some(8),
110            Self::All => None,
111        }
112    }
113}
114
115impl PartialOrd for OperationType {
116    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
117        if self == other {
118            return Some(Ordering::Equal);
119        }
120        if self.grants(other) {
121            return Some(Ordering::Greater);
122        }
123        if other.grants(self) {
124            return Some(Ordering::Less);
125        }
126        None
127    }
128}
129
130/// Extension trait for permission slices to handle `OperationType::All`.
131pub trait PermissionSlice {
132    /// Like `contains()`, but `All` in the slice matches any operation.
133    fn grants(&self, op: &OperationType) -> bool;
134    /// Whether any element is a write-class operation.
135    fn grants_write(&self) -> bool;
136    /// Whether any element is a read-class operation.
137    fn grants_read(&self) -> bool;
138}
139
140impl PermissionSlice for [OperationType] {
141    fn grants(&self, op: &OperationType) -> bool {
142        self.iter().any(|p| p.grants(op))
143    }
144
145    fn grants_write(&self) -> bool {
146        self.iter().any(|p| p.is_write())
147    }
148
149    fn grants_read(&self) -> bool {
150        self.iter().any(|p| p.is_read())
151    }
152}
153
154/// LDAP permissions mapped to acls-rs AtomicPermissions.
155#[derive(Debug, Clone, PartialEq, Eq, Hash)]
156pub struct LdapPermission {
157    operation: OperationType,
158    target: String,
159}
160
161impl LdapPermission {
162    pub fn new(operation: OperationType, target: impl Into<String>) -> Self {
163        Self {
164            operation,
165            target: target.into(),
166        }
167    }
168
169    /// Create read permission for a DN
170    pub fn read(dn: impl Into<String>) -> Self {
171        Self::new(OperationType::Read, dn)
172    }
173
174    /// Create write/modify permission
175    pub fn write(dn: impl Into<String>) -> Self {
176        Self::new(OperationType::Modify, dn)
177    }
178
179    /// Create search permission
180    pub fn search(base_dn: impl Into<String>) -> Self {
181        Self::new(OperationType::Search, base_dn)
182    }
183
184    /// Create add permission
185    pub fn add(parent_dn: impl Into<String>) -> Self {
186        Self::new(OperationType::Add, parent_dn)
187    }
188
189    /// Create delete permission
190    pub fn delete(dn: impl Into<String>) -> Self {
191        Self::new(OperationType::Delete, dn)
192    }
193
194    /// Convert to acls-rs AtomicPermission
195    pub fn to_atomic(&self) -> AtomicPermission {
196        AtomicPermission::new(&self.target, self.operation.as_str())
197    }
198}
199
200/// An LDAP operation to be authorized.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct LdapOperation {
203    pub operation_type: OperationType,
204    pub target_dn: String,
205    pub attributes: Vec<String>,
206}
207
208impl LdapOperation {
209    pub fn new(op: OperationType, dn: impl Into<String>) -> Self {
210        Self {
211            operation_type: op,
212            target_dn: dn.into(),
213            attributes: Vec::new(),
214        }
215    }
216
217    pub fn with_attributes(mut self, attrs: Vec<String>) -> Self {
218        self.attributes = attrs.into_iter().map(|a| a.to_lowercase()).collect();
219        self
220    }
221
222    /// Create a new operation with a different target DN.
223    pub fn with_target(&self, target_dn: impl Into<String>) -> Self {
224        Self {
225            operation_type: self.operation_type,
226            target_dn: target_dn.into(),
227            attributes: self.attributes.clone(),
228        }
229    }
230
231    /// Convert to permission for authorization
232    pub fn to_permission(&self) -> LdapPermission {
233        LdapPermission::new(self.operation_type, &self.target_dn)
234    }
235}
236
237/// Normalized set of LDAP operations, stored as a bitflag.
238///
239/// `All` and `SelfWrite` are expanded at construction: `All` sets every concrete
240/// operation bit, `SelfWrite` also sets `Modify`. This makes set operations
241/// (union, intersection, containment) correct without special-casing.
242///
243/// Forms a bounded lattice under subset inclusion, with bitwise OR as join
244/// (union) and bitwise AND as meet (intersection).
245#[derive(Clone, Copy, PartialEq, Eq, Hash)]
246#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
247pub struct OperationSet(u16);
248
249pub const NUM_CONCRETE_OPS: usize = 9;
250
251impl OperationSet {
252    const ALL_CONCRETE: u16 = (1 << NUM_CONCRETE_OPS) - 1;
253
254    pub(crate) fn raw_bits(self) -> u16 {
255        self.0
256    }
257
258    pub fn empty() -> Self {
259        Self(0)
260    }
261
262    pub fn all() -> Self {
263        Self(Self::ALL_CONCRETE)
264    }
265
266    pub fn contains(self, op: OperationType) -> bool {
267        if self.0 & op.bit() == op.bit() {
268            return true;
269        }
270        // SelfWrite subsumes Modify per 389-ds semantics
271        op == OperationType::Modify && self.0 & OperationType::SelfWrite.bit() != 0
272    }
273
274    pub fn is_empty(self) -> bool {
275        self.0 == 0
276    }
277
278    pub fn len(self) -> usize {
279        self.0.count_ones() as usize
280    }
281
282    pub fn insert(&mut self, op: OperationType) {
283        self.0 |= op.bit();
284    }
285
286    pub fn union(self, other: Self) -> Self {
287        Self(self.0 | other.0)
288    }
289
290    pub fn intersection(self, other: Self) -> Self {
291        Self(self.0 & other.0)
292    }
293
294    pub fn difference(self, other: Self) -> Self {
295        Self(self.0 & !other.0)
296    }
297
298    pub fn is_subset_of(self, other: Self) -> bool {
299        self.0 & other.0 == self.0
300    }
301
302    pub fn is_superset_of(self, other: Self) -> bool {
303        other.is_subset_of(self)
304    }
305
306    pub fn grants_write(self) -> bool {
307        let write_bits = OperationType::Modify.bit()
308            | OperationType::Add.bit()
309            | OperationType::Delete.bit()
310            | OperationType::ModifyDn.bit()
311            | OperationType::SelfWrite.bit();
312        self.0 & write_bits != 0
313    }
314
315    pub fn grants_read(self) -> bool {
316        let read_bits =
317            OperationType::Read.bit() | OperationType::Search.bit() | OperationType::Compare.bit();
318        self.0 & read_bits != 0
319    }
320
321    pub fn iter(self) -> OperationSetIter {
322        OperationSetIter {
323            bits: self.0,
324            pos: 0,
325        }
326    }
327
328    pub fn to_vec(self) -> Vec<OperationType> {
329        self.iter().collect()
330    }
331}
332
333impl From<OperationType> for OperationSet {
334    fn from(op: OperationType) -> Self {
335        Self(op.bit())
336    }
337}
338
339impl From<&[OperationType]> for OperationSet {
340    fn from(ops: &[OperationType]) -> Self {
341        let mut bits = 0u16;
342        for op in ops {
343            bits |= op.bit();
344        }
345        Self(bits)
346    }
347}
348
349impl FromIterator<OperationType> for OperationSet {
350    fn from_iter<I: IntoIterator<Item = OperationType>>(iter: I) -> Self {
351        let mut bits = 0u16;
352        for op in iter {
353            bits |= op.bit();
354        }
355        Self(bits)
356    }
357}
358
359impl fmt::Debug for OperationSet {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        f.debug_set().entries(self.iter()).finish()
362    }
363}
364
365impl fmt::Display for OperationSet {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        let ops: Vec<&str> = self.iter().map(|op| op.as_str()).collect();
368        write!(f, "{}", ops.join(","))
369    }
370}
371
372impl PartialOrd for OperationSet {
373    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
374        let a_sub_b = self.is_subset_of(*other);
375        let b_sub_a = other.is_subset_of(*self);
376        match (a_sub_b, b_sub_a) {
377            (true, true) => Some(Ordering::Equal),
378            (true, false) => Some(Ordering::Less),
379            (false, true) => Some(Ordering::Greater),
380            (false, false) => None,
381        }
382    }
383}
384
385impl Semigroup for OperationSet {
386    fn combine(self, other: Self) -> Self {
387        self.union(other)
388    }
389}
390
391impl Monoid for OperationSet {
392    fn identity() -> Self {
393        Self::empty()
394    }
395}
396
397impl MeetSemilattice for OperationSet {
398    fn meet(self, other: Self) -> Self {
399        self.intersection(other)
400    }
401}
402
403impl JoinSemilattice for OperationSet {
404    fn join(self, other: Self) -> Self {
405        self.union(other)
406    }
407}
408
409impl BoundedMeetSemilattice for OperationSet {
410    fn top() -> Self {
411        Self::all()
412    }
413}
414
415impl BoundedJoinSemilattice for OperationSet {
416    fn bottom() -> Self {
417        Self::empty()
418    }
419}
420
421/// Iterator over operations in an `OperationSet`.
422pub struct OperationSetIter {
423    bits: u16,
424    pos: u8,
425}
426
427const OPERATION_TABLE: [OperationType; 9] = [
428    OperationType::Bind,
429    OperationType::Search,
430    OperationType::Compare,
431    OperationType::Read,
432    OperationType::Add,
433    OperationType::Delete,
434    OperationType::Modify,
435    OperationType::ModifyDn,
436    OperationType::SelfWrite,
437];
438
439impl Iterator for OperationSetIter {
440    type Item = OperationType;
441
442    fn next(&mut self) -> Option<Self::Item> {
443        while (self.pos as usize) < OPERATION_TABLE.len() {
444            let bit = 1u16 << self.pos;
445            self.pos += 1;
446            if self.bits & bit != 0 {
447                return Some(OPERATION_TABLE[(self.pos - 1) as usize]);
448            }
449        }
450        None
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    mod operation_type_ordering {
459        use super::*;
460
461        #[test]
462        fn all_subsumes_everything() {
463            for op in OPERATION_TABLE {
464                assert!(OperationType::All >= op, "All should subsume {:?}", op);
465            }
466        }
467
468        #[test]
469        fn selfwrite_subsumes_modify() {
470            assert!(OperationType::SelfWrite > OperationType::Modify);
471        }
472
473        #[test]
474        fn incomparable_leaves() {
475            assert_eq!(
476                OperationType::Read.partial_cmp(&OperationType::Search),
477                None
478            );
479            assert_eq!(OperationType::Add.partial_cmp(&OperationType::Delete), None);
480            assert_eq!(
481                OperationType::Bind.partial_cmp(&OperationType::Modify),
482                None
483            );
484        }
485
486        #[test]
487        fn reflexive() {
488            for op in OPERATION_TABLE {
489                assert_eq!(op.partial_cmp(&op), Some(Ordering::Equal));
490            }
491            assert_eq!(
492                OperationType::All.partial_cmp(&OperationType::All),
493                Some(Ordering::Equal)
494            );
495        }
496    }
497
498    mod operation_set {
499        use super::*;
500
501        #[test]
502        fn from_slice() {
503            let ops = [OperationType::Read, OperationType::Search];
504            let set = OperationSet::from(ops.as_slice());
505            assert!(set.contains(OperationType::Read));
506            assert!(set.contains(OperationType::Search));
507            assert!(!set.contains(OperationType::Modify));
508            assert_eq!(set.len(), 2);
509        }
510
511        #[test]
512        fn all_expands() {
513            let set = OperationSet::from(OperationType::All);
514            assert_eq!(set.len(), 9);
515            for op in OPERATION_TABLE {
516                assert!(set.contains(op), "All should contain {:?}", op);
517            }
518        }
519
520        #[test]
521        fn selfwrite_subsumes_modify_in_set() {
522            let set = OperationSet::from(OperationType::SelfWrite);
523            assert!(set.contains(OperationType::SelfWrite));
524            assert!(set.contains(OperationType::Modify));
525            assert_eq!(set.len(), 1);
526        }
527
528        #[test]
529        fn subset_ordering() {
530            let small = OperationSet::from([OperationType::Read].as_slice());
531            let big = OperationSet::from([OperationType::Read, OperationType::Search].as_slice());
532            assert!(small < big);
533            assert!(small <= big);
534            assert!(big > small);
535        }
536
537        #[test]
538        fn incomparable_sets() {
539            let a = OperationSet::from([OperationType::Read].as_slice());
540            let b = OperationSet::from([OperationType::Modify].as_slice());
541            assert_eq!(a.partial_cmp(&b), None);
542        }
543
544        #[test]
545        fn lattice_meet() {
546            let a = OperationSet::from([OperationType::Read, OperationType::Search].as_slice());
547            let b = OperationSet::from([OperationType::Read, OperationType::Modify].as_slice());
548            let meet = a.meet(b);
549            assert_eq!(meet.len(), 1);
550            assert!(meet.contains(OperationType::Read));
551        }
552
553        #[test]
554        fn lattice_join() {
555            let a = OperationSet::from([OperationType::Read, OperationType::Search].as_slice());
556            let b = OperationSet::from([OperationType::Read, OperationType::Modify].as_slice());
557            let join = a.join(b);
558            assert_eq!(join.len(), 3);
559            assert!(join.contains(OperationType::Read));
560            assert!(join.contains(OperationType::Search));
561            assert!(join.contains(OperationType::Modify));
562        }
563
564        #[test]
565        fn monoid_identity() {
566            let set = OperationSet::from([OperationType::Read, OperationType::Search].as_slice());
567            assert_eq!(set.combine(OperationSet::identity()), set);
568            assert_eq!(OperationSet::identity().combine(set), set);
569        }
570
571        #[test]
572        fn bounded_top_bottom() {
573            let set = OperationSet::from([OperationType::Read, OperationType::Search].as_slice());
574            assert_eq!(set.meet(OperationSet::top()), set);
575            assert_eq!(set.join(OperationSet::bottom()), set);
576        }
577
578        #[test]
579        fn idempotence() {
580            let set = OperationSet::from([OperationType::Read, OperationType::Modify].as_slice());
581            assert_eq!(set.meet(set), set);
582            assert_eq!(set.join(set), set);
583        }
584
585        #[test]
586        fn iter_round_trip() {
587            let ops = [
588                OperationType::Read,
589                OperationType::Search,
590                OperationType::Add,
591            ];
592            let set = OperationSet::from(ops.as_slice());
593            let vec = set.to_vec();
594            assert_eq!(vec.len(), 3);
595            assert!(vec.contains(&OperationType::Read));
596            assert!(vec.contains(&OperationType::Search));
597            assert!(vec.contains(&OperationType::Add));
598        }
599    }
600}