Adds a __repr__ to graph.
[pyutils.git] / src / pyutils / security / acl.py
1 #!/usr/bin/env python3
2
3 # © Copyright 2021-2023, 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         Raises:
120             ValueError: Invalid Order argument
121
122         .. note::
123
124             By using `order_to_check_allow_deny` and `default_answer` you
125             can create both *allow lists* and *deny lists*.  The former
126             uses `Order.ALLOW_DENY` with a default anwser of False whereas
127             the latter uses `Order.DENY_ALLOW` with a default answer of
128             True.
129         """
130         if order_to_check_allow_deny not in (
131             Order.ALLOW_DENY,
132             Order.DENY_ALLOW,
133         ):
134             raise ValueError(
135                 'order_to_check_allow_deny must be Order.ALLOW_DENY or '
136                 + 'Order.DENY_ALLOW'
137             )
138         self.order_to_check_allow_deny = order_to_check_allow_deny
139         self.default_answer = default_answer
140
141     def __call__(self, x: Any) -> bool:
142         """
143         Returns:
144             True if x is allowed, False otherwise.
145         """
146         logger.debug('SimpleACL checking %s', x)
147         if self.order_to_check_allow_deny == Order.ALLOW_DENY:
148             logger.debug('Checking allowed first...')
149             if self.check_allowed(x):
150                 logger.debug('%s was allowed explicitly.', x)
151                 return True
152             logger.debug('Checking denied next...')
153             if self.check_denied(x):
154                 logger.debug('%s was denied explicitly.', x)
155                 return False
156         elif self.order_to_check_allow_deny == Order.DENY_ALLOW:
157             logger.debug('Checking denied first...')
158             if self.check_denied(x):
159                 logger.debug('%s was denied explicitly.', x)
160                 return False
161             if self.check_allowed(x):
162                 logger.debug('%s was allowed explicitly.', x)
163                 return True
164
165         logger.debug(
166             f'{x} was not explicitly allowed or denied; '
167             + f'using default answer ({self.default_answer})'
168         )
169         return self.default_answer
170
171     @abstractmethod
172     def check_allowed(self, x: Any) -> bool:
173         """
174         Args:
175             x: the object being tested.
176
177         Returns:
178             True if x is explicitly allowed, False otherwise.
179         """
180         pass
181
182     @abstractmethod
183     def check_denied(self, x: Any) -> bool:
184         """
185         Args:
186             x: the object being tested.
187
188         Returns:
189             True if x is explicitly denied, False otherwise."""
190         pass
191
192
193 class SetBasedACL(SimpleACL):
194     """An ACL that allows or denies based on membership in a set."""
195
196     def __init__(
197         self,
198         *,
199         allow_set: Optional[Set[Any]] = None,
200         deny_set: Optional[Set[Any]] = None,
201         order_to_check_allow_deny: Order,
202         default_answer: bool,
203     ) -> None:
204         """
205         Args:
206             allow_set: the set of items that are allowed.
207             deny_set: the set of items that are denied.
208             order_to_check_allow_deny: set this argument to indicate what
209                 order to check items for allow and deny.  Pass either
210                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
211                 to check deny first.
212             default_answer: pass this argument to provide the ACL with a
213                 default answer.
214
215         .. note::
216
217             By using `order_to_check_allow_deny` and `default_answer` you
218             can create both *allow lists* and *deny lists*.  The former
219             uses `Order.ALLOW_DENY` with a default anwser of False whereas
220             the latter uses `Order.DENY_ALLOW` with a default answer of
221             True.
222         """
223         super().__init__(
224             order_to_check_allow_deny=order_to_check_allow_deny,
225             default_answer=default_answer,
226         )
227         self.allow_set = allow_set
228         self.deny_set = deny_set
229
230     @overrides
231     def check_allowed(self, x: Any) -> bool:
232         if self.allow_set is None:
233             return False
234         return x in self.allow_set
235
236     @overrides
237     def check_denied(self, x: Any) -> bool:
238         if self.deny_set is None:
239             return False
240         return x in self.deny_set
241
242
243 class AllowListACL(SetBasedACL):
244     """Convenience subclass for a list that only allows known items.
245     i.e. an 'allowlist'
246     """
247
248     def __init__(self, *, allow_set: Optional[Set[Any]]) -> None:
249         """
250         Args:
251             allow_set: a set containing the items that are allowed.
252         """
253         super().__init__(
254             allow_set=allow_set,
255             order_to_check_allow_deny=Order.ALLOW_DENY,
256             default_answer=False,
257         )
258
259
260 class DenyListACL(SetBasedACL):
261     """Convenience subclass for a list that only disallows known items.
262     i.e. a 'blocklist'
263     """
264
265     def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
266         """
267         Args:
268             deny_set: a set containing the items that are denied.
269         """
270         super().__init__(
271             deny_set=deny_set,
272             order_to_check_allow_deny=Order.DENY_ALLOW,
273             default_answer=True,
274         )
275
276
277 class BlockListACL(SetBasedACL):
278     """Convenience subclass for a list that only disallows known items.
279     i.e. a 'blocklist'
280     """
281
282     def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
283         """
284         Args:
285             deny_set: a set containing the items that are denied.
286         """
287         super().__init__(
288             deny_set=deny_set,
289             order_to_check_allow_deny=Order.DENY_ALLOW,
290             default_answer=True,
291         )
292
293
294 class PredicateListBasedACL(SimpleACL):
295     """An ACL that allows or denies by applying predicates."""
296
297     def __init__(
298         self,
299         *,
300         allow_predicate_list: Sequence[Callable[[Any], bool]] = None,
301         deny_predicate_list: Sequence[Callable[[Any], bool]] = None,
302         order_to_check_allow_deny: Order,
303         default_answer: bool,
304     ) -> None:
305         """
306         Args:
307             allow_predicate_list: a list of callables that indicate that
308                 an item should be allowed if they return True.
309             deny_predicate_list: a list of callables that indicate that an
310                 item should be denied if they return True.
311             order_to_check_allow_deny: set this argument to indicate what
312                 order to check items for allow and deny.  Pass either
313                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
314                 to check deny first.
315             default_answer: pass this argument to provide the ACL with a
316                 default answer.
317
318         .. note::
319
320             By using `order_to_check_allow_deny` and `default_answer` you
321             can create both *allow lists* and *deny lists*.  The former
322             uses `Order.ALLOW_DENY` with a default anwser of False whereas
323             the latter uses `Order.DENY_ALLOW` with a default answer of
324             True.
325         """
326         super().__init__(
327             order_to_check_allow_deny=order_to_check_allow_deny,
328             default_answer=default_answer,
329         )
330         self.allow_predicate_list = allow_predicate_list
331         self.deny_predicate_list = deny_predicate_list
332
333     @overrides
334     def check_allowed(self, x: Any) -> bool:
335         if self.allow_predicate_list is None:
336             return False
337         return any(predicate(x) for predicate in self.allow_predicate_list)
338
339     @overrides
340     def check_denied(self, x: Any) -> bool:
341         if self.deny_predicate_list is None:
342             return False
343         return any(predicate(x) for predicate in self.deny_predicate_list)
344
345
346 class StringWildcardBasedACL(PredicateListBasedACL):
347     """An ACL that allows or denies based on string glob :code:`(*, ?)`
348     patterns.
349     """
350
351     def __init__(
352         self,
353         *,
354         allowed_patterns: Optional[List[str]] = None,
355         denied_patterns: Optional[List[str]] = None,
356         order_to_check_allow_deny: Order,
357         default_answer: bool,
358     ) -> None:
359         """
360         Args:
361             allowed_patterns: a list of string, optionally containing glob-style
362                 wildcards, that, if they match an item, indicate it should be
363                 allowed.
364             denied_patterns: a list of string, optionally containing glob-style
365                 wildcards, that, if they match an item, indicate it should be
366                 denied.
367             order_to_check_allow_deny: set this argument to indicate what
368                 order to check items for allow and deny.  Pass either
369                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
370                 to check deny first.
371             default_answer: pass this argument to provide the ACL with a
372                 default answer.
373
374         .. note::
375
376             By using `order_to_check_allow_deny` and `default_answer` you
377             can create both *allow lists* and *deny lists*.  The former
378             uses `Order.ALLOW_DENY` with a default anwser of False whereas
379             the latter uses `Order.DENY_ALLOW` with a default answer of
380             True.
381         """
382         allow_predicates = []
383         if allowed_patterns is not None:
384             for pattern in allowed_patterns:
385                 allow_predicates.append(
386                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
387                 )
388         deny_predicates = None
389         if denied_patterns is not None:
390             deny_predicates = []
391             for pattern in denied_patterns:
392                 deny_predicates.append(
393                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
394                 )
395
396         super().__init__(
397             allow_predicate_list=allow_predicates,
398             deny_predicate_list=deny_predicates,
399             order_to_check_allow_deny=order_to_check_allow_deny,
400             default_answer=default_answer,
401         )
402
403
404 class StringREBasedACL(PredicateListBasedACL):
405     """An ACL that allows or denies by applying regexps."""
406
407     def __init__(
408         self,
409         *,
410         allowed_regexs: Optional[List[re.Pattern]] = None,
411         denied_regexs: Optional[List[re.Pattern]] = None,
412         order_to_check_allow_deny: Order,
413         default_answer: bool,
414     ) -> None:
415         """
416         Args:
417             allowed_regexs: a list of regular expressions that, if they match an
418                 item, indicate that the item should be allowed.
419             denied_regexs: a list of regular expressions that, if they match an
420                 item, indicate that the item should be denied.
421             order_to_check_allow_deny: set this argument to indicate what
422                 order to check items for allow and deny.  Pass either
423                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
424                 to check deny first.
425             default_answer: pass this argument to provide the ACL with a
426                 default answer.
427
428         .. note::
429
430             By using `order_to_check_allow_deny` and `default_answer` you
431             can create both *allow lists* and *deny lists*.  The former
432             uses `Order.ALLOW_DENY` with a default anwser of False whereas
433             the latter uses `Order.DENY_ALLOW` with a default answer of
434             True.
435         """
436         allow_predicates = None
437         if allowed_regexs is not None:
438             allow_predicates = []
439             for pattern in allowed_regexs:
440                 allow_predicates.append(
441                     lambda x, pattern=pattern: pattern.match(x) is not None
442                 )
443         deny_predicates = None
444         if denied_regexs is not None:
445             deny_predicates = []
446             for pattern in denied_regexs:
447                 deny_predicates.append(
448                     lambda x, pattern=pattern: pattern.match(x) is not None
449                 )
450         super().__init__(
451             allow_predicate_list=allow_predicates,
452             deny_predicate_list=deny_predicates,
453             order_to_check_allow_deny=order_to_check_allow_deny,
454             default_answer=default_answer,
455         )
456
457
458 class AnyCompoundACL(SimpleACL):
459     """An ACL that allows if any of its subacls allow."""
460
461     def __init__(
462         self,
463         *,
464         subacls: Optional[List[SimpleACL]] = None,
465         order_to_check_allow_deny: Order,
466         default_answer: bool,
467     ) -> None:
468         """
469         Args:
470             subacls: a list of sub-ACLs we will consult for each item.  If
471                 *any* of these sub-ACLs allow the item we will also allow it.
472             order_to_check_allow_deny: set this argument to indicate what
473                 order to check items for allow and deny.  Pass either
474                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
475                 to check deny first.
476             default_answer: pass this argument to provide the ACL with a
477                 default answer.
478
479         .. note::
480
481             By using `order_to_check_allow_deny` and `default_answer` you
482             can create both *allow lists* and *deny lists*.  The former
483             uses `Order.ALLOW_DENY` with a default anwser of False whereas
484             the latter uses `Order.DENY_ALLOW` with a default answer of
485             True.
486         """
487         super().__init__(
488             order_to_check_allow_deny=order_to_check_allow_deny,
489             default_answer=default_answer,
490         )
491         self.subacls = subacls
492
493     @overrides
494     def check_allowed(self, x: Any) -> bool:
495         if self.subacls is None:
496             return False
497         return any(acl(x) for acl in self.subacls)
498
499     @overrides
500     def check_denied(self, x: Any) -> bool:
501         if self.subacls is None:
502             return False
503         return any(not acl(x) for acl in self.subacls)
504
505
506 class AllCompoundACL(SimpleACL):
507     """An ACL that allows if all of its subacls allow."""
508
509     def __init__(
510         self,
511         *,
512         subacls: Optional[List[SimpleACL]] = None,
513         order_to_check_allow_deny: Order,
514         default_answer: bool,
515     ) -> None:
516         """
517         Args:
518             subacls: a list of sub-ACLs that we will consult for each item.  *All*
519                 sub-ACLs must allow an item for us to also allow that item.
520             order_to_check_allow_deny: set this argument to indicate what
521                 order to check items for allow and deny.  Pass either
522                 `Order.ALLOW_DENY` to check allow first or `Order.DENY_ALLOW`
523                 to check deny first.
524             default_answer: pass this argument to provide the ACL with a
525                 default answer.
526
527         .. note::
528
529             By using `order_to_check_allow_deny` and `default_answer` you
530             can create both *allow lists* and *deny lists*.  The former
531             uses `Order.ALLOW_DENY` with a default anwser of False whereas
532             the latter uses `Order.DENY_ALLOW` with a default answer of
533             True.
534         """
535         super().__init__(
536             order_to_check_allow_deny=order_to_check_allow_deny,
537             default_answer=default_answer,
538         )
539         self.subacls = subacls
540
541     @overrides
542     def check_allowed(self, x: Any) -> bool:
543         if self.subacls is None:
544             return False
545         return all(acl(x) for acl in self.subacls)
546
547     @overrides
548     def check_denied(self, x: Any) -> bool:
549         if self.subacls is None:
550             return False
551         return any(not acl(x) for acl in self.subacls)