3 # © Copyright 2021-2022, 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
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
127 if order_to_check_allow_deny not in (
132 'order_to_check_allow_deny must be Order.ALLOW_DENY or '
135 self.order_to_check_allow_deny = order_to_check_allow_deny
136 self.default_answer = default_answer
138 def __call__(self, x: Any) -> bool:
141 True if x is allowed, False otherwise.
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)
149 logger.debug('Checking denied next...')
150 if self.check_denied(x):
151 logger.debug('%s was denied explicitly.', x)
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)
158 if self.check_allowed(x):
159 logger.debug('%s was allowed explicitly.', x)
163 f'{x} was not explicitly allowed or denied; '
164 + f'using default answer ({self.default_answer})'
166 return self.default_answer
169 def check_allowed(self, x: Any) -> bool:
172 x: the object being tested.
175 True if x is explicitly allowed, False otherwise.
180 def check_denied(self, x: Any) -> bool:
183 x: the object being tested.
186 True if x is explicitly denied, False otherwise."""
190 class SetBasedACL(SimpleACL):
191 """An ACL that allows or denies based on membership in a set."""
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,
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`
209 default_answer: pass this argument to provide the ACL with a
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
221 order_to_check_allow_deny=order_to_check_allow_deny,
222 default_answer=default_answer,
224 self.allow_set = allow_set
225 self.deny_set = deny_set
228 def check_allowed(self, x: Any) -> bool:
229 if self.allow_set is None:
231 return x in self.allow_set
234 def check_denied(self, x: Any) -> bool:
235 if self.deny_set is None:
237 return x in self.deny_set
240 class AllowListACL(SetBasedACL):
241 """Convenience subclass for a list that only allows known items.
245 def __init__(self, *, allow_set: Optional[Set[Any]]) -> None:
248 allow_set: a set containing the items that are allowed.
252 order_to_check_allow_deny=Order.ALLOW_DENY,
253 default_answer=False,
257 class DenyListACL(SetBasedACL):
258 """Convenience subclass for a list that only disallows known items.
262 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
265 deny_set: a set containing the items that are denied.
269 order_to_check_allow_deny=Order.DENY_ALLOW,
274 class BlockListACL(SetBasedACL):
275 """Convenience subclass for a list that only disallows known items.
279 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
282 deny_set: a set containing the items that are denied.
286 order_to_check_allow_deny=Order.DENY_ALLOW,
291 class PredicateListBasedACL(SimpleACL):
292 """An ACL that allows or denies by applying predicates."""
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,
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`
312 default_answer: pass this argument to provide the ACL with a
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
324 order_to_check_allow_deny=order_to_check_allow_deny,
325 default_answer=default_answer,
327 self.allow_predicate_list = allow_predicate_list
328 self.deny_predicate_list = deny_predicate_list
331 def check_allowed(self, x: Any) -> bool:
332 if self.allow_predicate_list is None:
334 return any(predicate(x) for predicate in self.allow_predicate_list)
337 def check_denied(self, x: Any) -> bool:
338 if self.deny_predicate_list is None:
340 return any(predicate(x) for predicate in self.deny_predicate_list)
343 class StringWildcardBasedACL(PredicateListBasedACL):
344 """An ACL that allows or denies based on string glob :code:`(*, ?)`
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,
358 allowed_patterns: a list of string, optionally containing glob-style
359 wildcards, that, if they match an item, indicate it should be
361 denied_patterns: a list of string, optionally containing glob-style
362 wildcards, that, if they match an item, indicate it should be
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`
368 default_answer: pass this argument to provide the ACL with a
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
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)
385 deny_predicates = None
386 if denied_patterns is not None:
388 for pattern in denied_patterns:
389 deny_predicates.append(
390 lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
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,
401 class StringREBasedACL(PredicateListBasedACL):
402 """An ACL that allows or denies by applying regexps."""
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,
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`
422 default_answer: pass this argument to provide the ACL with a
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
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
440 deny_predicates = None
441 if denied_regexs is not None:
443 for pattern in denied_regexs:
444 deny_predicates.append(
445 lambda x, pattern=pattern: pattern.match(x) is not None
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,
455 class AnyCompoundACL(SimpleACL):
456 """An ACL that allows if any of its subacls allow."""
461 subacls: Optional[List[SimpleACL]] = None,
462 order_to_check_allow_deny: Order,
463 default_answer: bool,
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`
473 default_answer: pass this argument to provide the ACL with a
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
485 order_to_check_allow_deny=order_to_check_allow_deny,
486 default_answer=default_answer,
488 self.subacls = subacls
491 def check_allowed(self, x: Any) -> bool:
492 if self.subacls is None:
494 return any(acl(x) for acl in self.subacls)
497 def check_denied(self, x: Any) -> bool:
498 if self.subacls is None:
500 return any(not acl(x) for acl in self.subacls)
503 class AllCompoundACL(SimpleACL):
504 """An ACL that allows if all of its subacls allow."""
509 subacls: Optional[List[SimpleACL]] = None,
510 order_to_check_allow_deny: Order,
511 default_answer: bool,
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`
521 default_answer: pass this argument to provide the ACL with a
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
533 order_to_check_allow_deny=order_to_check_allow_deny,
534 default_answer=default_answer,
536 self.subacls = subacls
539 def check_allowed(self, x: Any) -> bool:
540 if self.subacls is None:
542 return all(acl(x) for acl in self.subacls)
545 def check_denied(self, x: Any) -> bool:
546 if self.subacls is None:
548 return any(not acl(x) for acl in self.subacls)