3 # © Copyright 2021-2023, Scott Gasch
5 """Right now this package only contains an implementation that allows you to
6 define and evaluate Access Control Lists (ACLs) easily. For example::
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,
14 self.assertTrue(even(2))
15 self.assertFalse(even(3))
16 self.assertFalse(even(-4))
18 ACLs can also be defined based on other criteria, for example::
20 a_or_b = acl.StringWildcardBasedACL(
21 allowed_patterns=['a*', 'b*'],
22 order_to_check_allow_deny=acl.Order.ALLOW_DENY,
25 self.assertTrue(a_or_b('aardvark'))
26 self.assertTrue(a_or_b('baboon'))
27 self.assertFalse(a_or_b('cheetah'))
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,
36 self.assertTrue(weird('aardvark'))
37 self.assertFalse(weird('anaconda'))
38 self.assertFalse(weird('blackneb'))
39 self.assertTrue(weird('crow'))
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.
47 Once a :class:`SimpleACL` is defined, it can be used within a
48 :class:`CompoundACL`::
50 a_b_c = acl.StringWildcardBasedACL(
51 allowed_patterns=['a*', 'b*', 'c*'],
52 order_to_check_allow_deny=acl.Order.ALLOW_DENY,
55 c_d_e = acl.StringWildcardBasedACL(
56 allowed_patterns=['c*', 'd*', 'e*'],
57 order_to_check_allow_deny=acl.Order.ALLOW_DENY,
60 conjunction = acl.AllCompoundACL(
61 subacls=[a_b_c, c_d_e],
62 order_to_check_allow_deny=acl.Order.ALLOW_DENY,
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'))
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
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.:
85 from abc import ABC, abstractmethod
86 from typing import Any, Callable, List, Optional, Sequence, Set
88 from overrides import overrides
90 # This module is commonly used by others in here and should avoid
91 # taking any unnecessary dependencies back on them.
93 logger = logging.getLogger(__name__)
96 class Order(enum.Enum):
97 """A helper to express the order of evaluation for allows/denies
98 in an Access Control List.
106 class SimpleACL(ABC):
107 """A simple Access Control List interface."""
109 def __init__(self, *, order_to_check_allow_deny: Order, default_answer: bool):
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`
116 default_answer: pass this argument to provide the ACL with a
120 ValueError: Invalid Order argument
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
130 if order_to_check_allow_deny not in (
135 'order_to_check_allow_deny must be Order.ALLOW_DENY or '
138 self.order_to_check_allow_deny = order_to_check_allow_deny
139 self.default_answer = default_answer
141 def __call__(self, x: Any) -> bool:
144 True if x is allowed, False otherwise.
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)
152 logger.debug('Checking denied next...')
153 if self.check_denied(x):
154 logger.debug('%s was denied explicitly.', x)
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)
161 if self.check_allowed(x):
162 logger.debug('%s was allowed explicitly.', x)
166 f'{x} was not explicitly allowed or denied; '
167 + f'using default answer ({self.default_answer})'
169 return self.default_answer
172 def check_allowed(self, x: Any) -> bool:
175 x: the object being tested.
178 True if x is explicitly allowed, False otherwise.
183 def check_denied(self, x: Any) -> bool:
186 x: the object being tested.
189 True if x is explicitly denied, False otherwise."""
193 class SetBasedACL(SimpleACL):
194 """An ACL that allows or denies based on membership in a set."""
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,
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`
212 default_answer: pass this argument to provide the ACL with a
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
224 order_to_check_allow_deny=order_to_check_allow_deny,
225 default_answer=default_answer,
227 self.allow_set = allow_set
228 self.deny_set = deny_set
231 def check_allowed(self, x: Any) -> bool:
232 if self.allow_set is None:
234 return x in self.allow_set
237 def check_denied(self, x: Any) -> bool:
238 if self.deny_set is None:
240 return x in self.deny_set
243 class AllowListACL(SetBasedACL):
244 """Convenience subclass for a list that only allows known items.
248 def __init__(self, *, allow_set: Optional[Set[Any]]) -> None:
251 allow_set: a set containing the items that are allowed.
255 order_to_check_allow_deny=Order.ALLOW_DENY,
256 default_answer=False,
260 class DenyListACL(SetBasedACL):
261 """Convenience subclass for a list that only disallows known items.
265 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
268 deny_set: a set containing the items that are denied.
272 order_to_check_allow_deny=Order.DENY_ALLOW,
277 class BlockListACL(SetBasedACL):
278 """Convenience subclass for a list that only disallows known items.
282 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
285 deny_set: a set containing the items that are denied.
289 order_to_check_allow_deny=Order.DENY_ALLOW,
294 class PredicateListBasedACL(SimpleACL):
295 """An ACL that allows or denies by applying predicates."""
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,
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`
315 default_answer: pass this argument to provide the ACL with a
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
327 order_to_check_allow_deny=order_to_check_allow_deny,
328 default_answer=default_answer,
330 self.allow_predicate_list = allow_predicate_list
331 self.deny_predicate_list = deny_predicate_list
334 def check_allowed(self, x: Any) -> bool:
335 if self.allow_predicate_list is None:
337 return any(predicate(x) for predicate in self.allow_predicate_list)
340 def check_denied(self, x: Any) -> bool:
341 if self.deny_predicate_list is None:
343 return any(predicate(x) for predicate in self.deny_predicate_list)
346 class StringWildcardBasedACL(PredicateListBasedACL):
347 """An ACL that allows or denies based on string glob :code:`(*, ?)`
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,
361 allowed_patterns: a list of string, optionally containing glob-style
362 wildcards, that, if they match an item, indicate it should be
364 denied_patterns: a list of string, optionally containing glob-style
365 wildcards, that, if they match an item, indicate it should be
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`
371 default_answer: pass this argument to provide the ACL with a
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
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)
388 deny_predicates = None
389 if denied_patterns is not None:
391 for pattern in denied_patterns:
392 deny_predicates.append(
393 lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
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,
404 class StringREBasedACL(PredicateListBasedACL):
405 """An ACL that allows or denies by applying regexps."""
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,
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`
425 default_answer: pass this argument to provide the ACL with a
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
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
443 deny_predicates = None
444 if denied_regexs is not None:
446 for pattern in denied_regexs:
447 deny_predicates.append(
448 lambda x, pattern=pattern: pattern.match(x) is not None
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,
458 class AnyCompoundACL(SimpleACL):
459 """An ACL that allows if any of its subacls allow."""
464 subacls: Optional[List[SimpleACL]] = None,
465 order_to_check_allow_deny: Order,
466 default_answer: bool,
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`
476 default_answer: pass this argument to provide the ACL with a
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
488 order_to_check_allow_deny=order_to_check_allow_deny,
489 default_answer=default_answer,
491 self.subacls = subacls
494 def check_allowed(self, x: Any) -> bool:
495 if self.subacls is None:
497 return any(acl(x) for acl in self.subacls)
500 def check_denied(self, x: Any) -> bool:
501 if self.subacls is None:
503 return any(not acl(x) for acl in self.subacls)
506 class AllCompoundACL(SimpleACL):
507 """An ACL that allows if all of its subacls allow."""
512 subacls: Optional[List[SimpleACL]] = None,
513 order_to_check_allow_deny: Order,
514 default_answer: bool,
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`
524 default_answer: pass this argument to provide the ACL with a
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
536 order_to_check_allow_deny=order_to_check_allow_deny,
537 default_answer=default_answer,
539 self.subacls = subacls
542 def check_allowed(self, x: Any) -> bool:
543 if self.subacls is None:
545 return all(acl(x) for acl in self.subacls)
548 def check_denied(self, x: Any) -> bool:
549 if self.subacls is None:
551 return any(not acl(x) for acl in self.subacls)