More work to improve documentation generated by sphinx. Also fixes
[pyutils.git] / src / pyutils / security / acl.py
1 #!/usr/bin/env python3
2
3 # © Copyright 2021-2022, Scott Gasch
4
5 """Right now this package only contains an implementation that allows you to
6 define and evaluate Access Control Lists (ACLs) easily.  For example::
7
8         even = acl.SetBasedACL(
9             allow_set=set([2, 4, 6, 8, 10]),
10             deny_set=set([1, 3, 5, 7, 9]),
11             order_to_check_allow_deny=acl.Order.ALLOW_DENY,
12             default_answer=False,
13         )
14         self.assertTrue(even(2))
15         self.assertFalse(even(3))
16         self.assertFalse(even(-4))
17
18 ACLs can also be defined based on other criteria, for example::
19
20         a_or_b = acl.StringWildcardBasedACL(
21             allowed_patterns=['a*', 'b*'],
22             order_to_check_allow_deny=acl.Order.ALLOW_DENY,
23             default_answer=False,
24         )
25         self.assertTrue(a_or_b('aardvark'))
26         self.assertTrue(a_or_b('baboon'))
27         self.assertFalse(a_or_b('cheetah'))
28
29 Or::
30
31         weird = acl.StringREBasedACL(
32             denied_regexs=[re.compile('^a.*a$'), re.compile('^b.*b$')],
33             order_to_check_allow_deny=acl.Order.DENY_ALLOW,
34             default_answer=True,
35         )
36         self.assertTrue(weird('aardvark'))
37         self.assertFalse(weird('anaconda'))
38         self.assertFalse(weird('blackneb'))
39         self.assertTrue(weird('crow'))
40
41 There are implementations for wildcards, sets, regular expressions,
42 allow lists, deny lists, sequences of user defined predicates, etc...
43 You can also just subclass the base :class:`SimpleACL` interface to
44 define your own ACLs easily.  Its :meth:`__call__` simply needs to
45 decide whether an item is allowed or denied.
46
47 Once a :class:`SimpleACL` is defined, it can be used within a
48 :class:`CompoundACL`::
49
50         a_b_c = acl.StringWildcardBasedACL(
51             allowed_patterns=['a*', 'b*', 'c*'],
52             order_to_check_allow_deny=acl.Order.ALLOW_DENY,
53             default_answer=False,
54         )
55         c_d_e = acl.StringWildcardBasedACL(
56             allowed_patterns=['c*', 'd*', 'e*'],
57             order_to_check_allow_deny=acl.Order.ALLOW_DENY,
58             default_answer=False,
59         )
60         conjunction = acl.AllCompoundACL(
61             subacls=[a_b_c, c_d_e],
62             order_to_check_allow_deny=acl.Order.ALLOW_DENY,
63             default_answer=False,
64         )
65         self.assertFalse(conjunction('aardvark'))
66         self.assertTrue(conjunction('caribou'))
67         self.assertTrue(conjunction('condor'))
68         self.assertFalse(conjunction('eagle'))
69         self.assertFalse(conjunction('newt'))
70
71 A :class:`CompoundACL` can also be used inside another :class:`CompoundACL`
72 so this should be a flexible framework when defining complex access control
73 requirements:
74
75 There are two flavors of :class:`CompoundACL`:
76 :class:`AllCompoundACL` and :class:`AnyCompoundAcl`.  The former only
77 admits an item if all of its sub-acls admit it and the latter will
78 admit an item if any of its sub-acls admit it.:
79 """
80
81 import enum
82 import fnmatch
83 import logging
84 import re
85 from abc import ABC, abstractmethod
86 from typing import Any, Callable, List, Optional, Sequence, Set
87
88 from overrides import overrides
89
90 # This module is commonly used by others in here and should avoid
91 # taking any unnecessary dependencies back on them.
92
93 logger = logging.getLogger(__name__)
94
95
96 class Order(enum.Enum):
97     """A helper to express the order of evaluation for allows/denies
98     in an Access Control List.
99     """
100
101     UNDEFINED = 0
102     ALLOW_DENY = 1
103     DENY_ALLOW = 2
104
105
106 class SimpleACL(ABC):
107     """A simple Access Control List interface."""
108
109     def __init__(self, *, order_to_check_allow_deny: Order, default_answer: bool):
110         """
111         Args:
112             order_to_check_allow_deny: set this argument to indicate what
113                 order to check items for allow and deny.  Pass either
114                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
115                 to check deny first.
116             default_answer: pass this argument to provide the ACL with a
117                 default answer.
118
119         .. note::
120
121             By using `order_to_check_allow_deny` and `default_answer` you
122             can create both *allow lists* and *deny lists*.  The former
123             uses `Order.ALLOW_DENY` with a default anwser of False whereas
124             the latter uses `Order.DENY_ALLOW` with a default answer of
125             True.
126         """
127         if order_to_check_allow_deny not in (
128             Order.ALLOW_DENY,
129             Order.DENY_ALLOW,
130         ):
131             raise Exception(
132                 'order_to_check_allow_deny must be Order.ALLOW_DENY or '
133                 + 'Order.DENY_ALLOW'
134             )
135         self.order_to_check_allow_deny = order_to_check_allow_deny
136         self.default_answer = default_answer
137
138     def __call__(self, x: Any) -> bool:
139         """
140         Returns:
141             True if x is allowed, False otherwise.
142         """
143         logger.debug('SimpleACL checking %s', x)
144         if self.order_to_check_allow_deny == Order.ALLOW_DENY:
145             logger.debug('Checking allowed first...')
146             if self.check_allowed(x):
147                 logger.debug('%s was allowed explicitly.', x)
148                 return True
149             logger.debug('Checking denied next...')
150             if self.check_denied(x):
151                 logger.debug('%s was denied explicitly.', x)
152                 return False
153         elif self.order_to_check_allow_deny == Order.DENY_ALLOW:
154             logger.debug('Checking denied first...')
155             if self.check_denied(x):
156                 logger.debug('%s was denied explicitly.', x)
157                 return False
158             if self.check_allowed(x):
159                 logger.debug('%s was allowed explicitly.', x)
160                 return True
161
162         logger.debug(
163             f'{x} was not explicitly allowed or denied; '
164             + f'using default answer ({self.default_answer})'
165         )
166         return self.default_answer
167
168     @abstractmethod
169     def check_allowed(self, x: Any) -> bool:
170         """
171         Args:
172             x: the object being tested.
173
174         Returns:
175             True if x is explicitly allowed, False otherwise.
176         """
177         pass
178
179     @abstractmethod
180     def check_denied(self, x: Any) -> bool:
181         """
182         Args:
183             x: the object being tested.
184
185         Returns:
186             True if x is explicitly denied, False otherwise."""
187         pass
188
189
190 class SetBasedACL(SimpleACL):
191     """An ACL that allows or denies based on membership in a set."""
192
193     def __init__(
194         self,
195         *,
196         allow_set: Optional[Set[Any]] = None,
197         deny_set: Optional[Set[Any]] = None,
198         order_to_check_allow_deny: Order,
199         default_answer: bool,
200     ) -> None:
201         """
202         Args:
203             allow_set: the set of items that are allowed.
204             deny_set: the set of items that are denied.
205             order_to_check_allow_deny: set this argument to indicate what
206                 order to check items for allow and deny.  Pass either
207                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
208                 to check deny first.
209             default_answer: pass this argument to provide the ACL with a
210                 default answer.
211
212         .. note::
213
214             By using `order_to_check_allow_deny` and `default_answer` you
215             can create both *allow lists* and *deny lists*.  The former
216             uses `Order.ALLOW_DENY` with a default anwser of False whereas
217             the latter uses `Order.DENY_ALLOW` with a default answer of
218             True.
219         """
220         super().__init__(
221             order_to_check_allow_deny=order_to_check_allow_deny,
222             default_answer=default_answer,
223         )
224         self.allow_set = allow_set
225         self.deny_set = deny_set
226
227     @overrides
228     def check_allowed(self, x: Any) -> bool:
229         if self.allow_set is None:
230             return False
231         return x in self.allow_set
232
233     @overrides
234     def check_denied(self, x: Any) -> bool:
235         if self.deny_set is None:
236             return False
237         return x in self.deny_set
238
239
240 class AllowListACL(SetBasedACL):
241     """Convenience subclass for a list that only allows known items.
242     i.e. an 'allowlist'
243     """
244
245     def __init__(self, *, allow_set: Optional[Set[Any]]) -> None:
246         """
247         Args:
248             allow_set: a set containing the items that are allowed.
249         """
250         super().__init__(
251             allow_set=allow_set,
252             order_to_check_allow_deny=Order.ALLOW_DENY,
253             default_answer=False,
254         )
255
256
257 class DenyListACL(SetBasedACL):
258     """Convenience subclass for a list that only disallows known items.
259     i.e. a 'blocklist'
260     """
261
262     def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
263         """
264         Args:
265             deny_set: a set containing the items that are denied.
266         """
267         super().__init__(
268             deny_set=deny_set,
269             order_to_check_allow_deny=Order.DENY_ALLOW,
270             default_answer=True,
271         )
272
273
274 class BlockListACL(SetBasedACL):
275     """Convenience subclass for a list that only disallows known items.
276     i.e. a 'blocklist'
277     """
278
279     def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
280         """
281         Args:
282             deny_set: a set containing the items that are denied.
283         """
284         super().__init__(
285             deny_set=deny_set,
286             order_to_check_allow_deny=Order.DENY_ALLOW,
287             default_answer=True,
288         )
289
290
291 class PredicateListBasedACL(SimpleACL):
292     """An ACL that allows or denies by applying predicates."""
293
294     def __init__(
295         self,
296         *,
297         allow_predicate_list: Sequence[Callable[[Any], bool]] = None,
298         deny_predicate_list: Sequence[Callable[[Any], bool]] = None,
299         order_to_check_allow_deny: Order,
300         default_answer: bool,
301     ) -> None:
302         """
303         Args:
304             allow_predicate_list: a list of callables that indicate that
305                 an item should be allowed if they return True.
306             deny_predicate_list: a list of callables that indicate that an
307                 item should be denied if they return True.
308             order_to_check_allow_deny: set this argument to indicate what
309                 order to check items for allow and deny.  Pass either
310                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
311                 to check deny first.
312             default_answer: pass this argument to provide the ACL with a
313                 default answer.
314
315         .. note::
316
317             By using `order_to_check_allow_deny` and `default_answer` you
318             can create both *allow lists* and *deny lists*.  The former
319             uses `Order.ALLOW_DENY` with a default anwser of False whereas
320             the latter uses `Order.DENY_ALLOW` with a default answer of
321             True.
322         """
323         super().__init__(
324             order_to_check_allow_deny=order_to_check_allow_deny,
325             default_answer=default_answer,
326         )
327         self.allow_predicate_list = allow_predicate_list
328         self.deny_predicate_list = deny_predicate_list
329
330     @overrides
331     def check_allowed(self, x: Any) -> bool:
332         if self.allow_predicate_list is None:
333             return False
334         return any(predicate(x) for predicate in self.allow_predicate_list)
335
336     @overrides
337     def check_denied(self, x: Any) -> bool:
338         if self.deny_predicate_list is None:
339             return False
340         return any(predicate(x) for predicate in self.deny_predicate_list)
341
342
343 class StringWildcardBasedACL(PredicateListBasedACL):
344     """An ACL that allows or denies based on string glob :code:`(*, ?)`
345     patterns.
346     """
347
348     def __init__(
349         self,
350         *,
351         allowed_patterns: Optional[List[str]] = None,
352         denied_patterns: Optional[List[str]] = None,
353         order_to_check_allow_deny: Order,
354         default_answer: bool,
355     ) -> None:
356         """
357         Args:
358             allowed_patterns: a list of string, optionally containing glob-style
359                 wildcards, that, if they match an item, indicate it should be
360                 allowed.
361             denied_patterns: a list of string, optionally containing glob-style
362                 wildcards, that, if they match an item, indicate it should be
363                 denied.
364             order_to_check_allow_deny: set this argument to indicate what
365                 order to check items for allow and deny.  Pass either
366                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
367                 to check deny first.
368             default_answer: pass this argument to provide the ACL with a
369                 default answer.
370
371         .. note::
372
373             By using `order_to_check_allow_deny` and `default_answer` you
374             can create both *allow lists* and *deny lists*.  The former
375             uses `Order.ALLOW_DENY` with a default anwser of False whereas
376             the latter uses `Order.DENY_ALLOW` with a default answer of
377             True.
378         """
379         allow_predicates = []
380         if allowed_patterns is not None:
381             for pattern in allowed_patterns:
382                 allow_predicates.append(
383                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
384                 )
385         deny_predicates = None
386         if denied_patterns is not None:
387             deny_predicates = []
388             for pattern in denied_patterns:
389                 deny_predicates.append(
390                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
391                 )
392
393         super().__init__(
394             allow_predicate_list=allow_predicates,
395             deny_predicate_list=deny_predicates,
396             order_to_check_allow_deny=order_to_check_allow_deny,
397             default_answer=default_answer,
398         )
399
400
401 class StringREBasedACL(PredicateListBasedACL):
402     """An ACL that allows or denies by applying regexps."""
403
404     def __init__(
405         self,
406         *,
407         allowed_regexs: Optional[List[re.Pattern]] = None,
408         denied_regexs: Optional[List[re.Pattern]] = None,
409         order_to_check_allow_deny: Order,
410         default_answer: bool,
411     ) -> None:
412         """
413         Args:
414             allowed_regexs: a list of regular expressions that, if they match an
415                 item, indicate that the item should be allowed.
416             denied_regexs: a list of regular expressions that, if they match an
417                 item, indicate that the item should be denied.
418             order_to_check_allow_deny: set this argument to indicate what
419                 order to check items for allow and deny.  Pass either
420                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
421                 to check deny first.
422             default_answer: pass this argument to provide the ACL with a
423                 default answer.
424
425         .. note::
426
427             By using `order_to_check_allow_deny` and `default_answer` you
428             can create both *allow lists* and *deny lists*.  The former
429             uses `Order.ALLOW_DENY` with a default anwser of False whereas
430             the latter uses `Order.DENY_ALLOW` with a default answer of
431             True.
432         """
433         allow_predicates = None
434         if allowed_regexs is not None:
435             allow_predicates = []
436             for pattern in allowed_regexs:
437                 allow_predicates.append(
438                     lambda x, pattern=pattern: pattern.match(x) is not None
439                 )
440         deny_predicates = None
441         if denied_regexs is not None:
442             deny_predicates = []
443             for pattern in denied_regexs:
444                 deny_predicates.append(
445                     lambda x, pattern=pattern: pattern.match(x) is not None
446                 )
447         super().__init__(
448             allow_predicate_list=allow_predicates,
449             deny_predicate_list=deny_predicates,
450             order_to_check_allow_deny=order_to_check_allow_deny,
451             default_answer=default_answer,
452         )
453
454
455 class AnyCompoundACL(SimpleACL):
456     """An ACL that allows if any of its subacls allow."""
457
458     def __init__(
459         self,
460         *,
461         subacls: Optional[List[SimpleACL]] = None,
462         order_to_check_allow_deny: Order,
463         default_answer: bool,
464     ) -> None:
465         """
466         Args:
467             subacls: a list of sub-ACLs we will consult for each item.  If
468                 *any* of these sub-ACLs allow the item we will also allow it.
469             order_to_check_allow_deny: set this argument to indicate what
470                 order to check items for allow and deny.  Pass either
471                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
472                 to check deny first.
473             default_answer: pass this argument to provide the ACL with a
474                 default answer.
475
476         .. note::
477
478             By using `order_to_check_allow_deny` and `default_answer` you
479             can create both *allow lists* and *deny lists*.  The former
480             uses `Order.ALLOW_DENY` with a default anwser of False whereas
481             the latter uses `Order.DENY_ALLOW` with a default answer of
482             True.
483         """
484         super().__init__(
485             order_to_check_allow_deny=order_to_check_allow_deny,
486             default_answer=default_answer,
487         )
488         self.subacls = subacls
489
490     @overrides
491     def check_allowed(self, x: Any) -> bool:
492         if self.subacls is None:
493             return False
494         return any(acl(x) for acl in self.subacls)
495
496     @overrides
497     def check_denied(self, x: Any) -> bool:
498         if self.subacls is None:
499             return False
500         return any(not acl(x) for acl in self.subacls)
501
502
503 class AllCompoundACL(SimpleACL):
504     """An ACL that allows if all of its subacls allow."""
505
506     def __init__(
507         self,
508         *,
509         subacls: Optional[List[SimpleACL]] = None,
510         order_to_check_allow_deny: Order,
511         default_answer: bool,
512     ) -> None:
513         """
514         Args:
515             subacls: a list of sub-ACLs that we will consult for each item.  *All*
516                 sub-ACLs must allow an item for us to also allow that item.
517             order_to_check_allow_deny: set this argument to indicate what
518                 order to check items for allow and deny.  Pass either
519                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
520                 to check deny first.
521             default_answer: pass this argument to provide the ACL with a
522                 default answer.
523
524         .. note::
525
526             By using `order_to_check_allow_deny` and `default_answer` you
527             can create both *allow lists* and *deny lists*.  The former
528             uses `Order.ALLOW_DENY` with a default anwser of False whereas
529             the latter uses `Order.DENY_ALLOW` with a default answer of
530             True.
531         """
532         super().__init__(
533             order_to_check_allow_deny=order_to_check_allow_deny,
534             default_answer=default_answer,
535         )
536         self.subacls = subacls
537
538     @overrides
539     def check_allowed(self, x: Any) -> bool:
540         if self.subacls is None:
541             return False
542         return all(acl(x) for acl in self.subacls)
543
544     @overrides
545     def check_denied(self, x: Any) -> bool:
546         if self.subacls is None:
547             return False
548         return any(not acl(x) for acl in self.subacls)