3 # © Copyright 2021-2022, Scott Gasch
5 """This module defines various flavors of Access Control Lists."""
11 from abc import ABC, abstractmethod
12 from typing import Any, Callable, List, Optional, Sequence, Set
14 from overrides import overrides
16 # This module is commonly used by others in here and should avoid
17 # taking any unnecessary dependencies back on them.
19 logger = logging.getLogger(__name__)
22 class Order(enum.Enum):
23 """A helper to express the order of evaluation for allows/denies
24 in an Access Control List.
33 """A simple Access Control List interface."""
35 def __init__(self, *, order_to_check_allow_deny: Order, default_answer: bool):
36 if order_to_check_allow_deny not in (
41 'order_to_check_allow_deny must be Order.ALLOW_DENY or ' + 'Order.DENY_ALLOW'
43 self.order_to_check_allow_deny = order_to_check_allow_deny
44 self.default_answer = default_answer
46 def __call__(self, x: Any) -> bool:
47 """Returns True if x is allowed, False otherwise."""
48 logger.debug('SimpleACL checking %s', x)
49 if self.order_to_check_allow_deny == Order.ALLOW_DENY:
50 logger.debug('Checking allowed first...')
51 if self.check_allowed(x):
52 logger.debug('%s was allowed explicitly.', x)
54 logger.debug('Checking denied next...')
55 if self.check_denied(x):
56 logger.debug('%s was denied explicitly.', x)
58 elif self.order_to_check_allow_deny == Order.DENY_ALLOW:
59 logger.debug('Checking denied first...')
60 if self.check_denied(x):
61 logger.debug('%s was denied explicitly.', x)
63 if self.check_allowed(x):
64 logger.debug('%s was allowed explicitly.', x)
68 f'{x} was not explicitly allowed or denied; '
69 + f'using default answer ({self.default_answer})'
71 return self.default_answer
74 def check_allowed(self, x: Any) -> bool:
75 """Return True if x is explicitly allowed, False otherwise."""
79 def check_denied(self, x: Any) -> bool:
80 """Return True if x is explicitly denied, False otherwise."""
84 class SetBasedACL(SimpleACL):
85 """An ACL that allows or denies based on membership in a set."""
90 allow_set: Optional[Set[Any]] = None,
91 deny_set: Optional[Set[Any]] = None,
92 order_to_check_allow_deny: Order,
96 order_to_check_allow_deny=order_to_check_allow_deny,
97 default_answer=default_answer,
99 self.allow_set = allow_set
100 self.deny_set = deny_set
103 def check_allowed(self, x: Any) -> bool:
104 if self.allow_set is None:
106 return x in self.allow_set
109 def check_denied(self, x: Any) -> bool:
110 if self.deny_set is None:
112 return x in self.deny_set
115 class AllowListACL(SetBasedACL):
116 """Convenience subclass for a list that only allows known items.
120 def __init__(self, *, allow_set: Optional[Set[Any]]) -> None:
123 order_to_check_allow_deny=Order.ALLOW_DENY,
124 default_answer=False,
128 class DenyListACL(SetBasedACL):
129 """Convenience subclass for a list that only disallows known items.
133 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
136 order_to_check_allow_deny=Order.ALLOW_DENY,
141 class BlockListACL(SetBasedACL):
142 """Convenience subclass for a list that only disallows known items.
146 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
149 order_to_check_allow_deny=Order.ALLOW_DENY,
154 class PredicateListBasedACL(SimpleACL):
155 """An ACL that allows or denies by applying predicates."""
160 allow_predicate_list: Sequence[Callable[[Any], bool]] = None,
161 deny_predicate_list: Sequence[Callable[[Any], bool]] = None,
162 order_to_check_allow_deny: Order,
163 default_answer: bool,
166 order_to_check_allow_deny=order_to_check_allow_deny,
167 default_answer=default_answer,
169 self.allow_predicate_list = allow_predicate_list
170 self.deny_predicate_list = deny_predicate_list
173 def check_allowed(self, x: Any) -> bool:
174 if self.allow_predicate_list is None:
176 return any(predicate(x) for predicate in self.allow_predicate_list)
179 def check_denied(self, x: Any) -> bool:
180 if self.deny_predicate_list is None:
182 return any(predicate(x) for predicate in self.deny_predicate_list)
185 class StringWildcardBasedACL(PredicateListBasedACL):
186 """An ACL that allows or denies based on string glob (*, ?) patterns."""
191 allowed_patterns: Optional[List[str]] = None,
192 denied_patterns: Optional[List[str]] = None,
193 order_to_check_allow_deny: Order,
194 default_answer: bool,
196 allow_predicates = []
197 if allowed_patterns is not None:
198 for pattern in allowed_patterns:
199 allow_predicates.append(lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern))
200 deny_predicates = None
201 if denied_patterns is not None:
203 for pattern in denied_patterns:
204 deny_predicates.append(lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern))
207 allow_predicate_list=allow_predicates,
208 deny_predicate_list=deny_predicates,
209 order_to_check_allow_deny=order_to_check_allow_deny,
210 default_answer=default_answer,
214 class StringREBasedACL(PredicateListBasedACL):
215 """An ACL that allows or denies by applying regexps."""
220 allowed_regexs: Optional[List[re.Pattern]] = None,
221 denied_regexs: Optional[List[re.Pattern]] = None,
222 order_to_check_allow_deny: Order,
223 default_answer: bool,
225 allow_predicates = None
226 if allowed_regexs is not None:
227 allow_predicates = []
228 for pattern in allowed_regexs:
229 allow_predicates.append(lambda x, pattern=pattern: pattern.match(x) is not None)
230 deny_predicates = None
231 if denied_regexs is not None:
233 for pattern in denied_regexs:
234 deny_predicates.append(lambda x, pattern=pattern: pattern.match(x) is not None)
236 allow_predicate_list=allow_predicates,
237 deny_predicate_list=deny_predicates,
238 order_to_check_allow_deny=order_to_check_allow_deny,
239 default_answer=default_answer,
243 class AnyCompoundACL(SimpleACL):
244 """An ACL that allows if any of its subacls allow."""
249 subacls: Optional[List[SimpleACL]] = None,
250 order_to_check_allow_deny: Order,
251 default_answer: bool,
254 order_to_check_allow_deny=order_to_check_allow_deny,
255 default_answer=default_answer,
257 self.subacls = subacls
260 def check_allowed(self, x: Any) -> bool:
261 if self.subacls is None:
263 return any(acl(x) for acl in self.subacls)
266 def check_denied(self, x: Any) -> bool:
267 if self.subacls is None:
269 return any(not acl(x) for acl in self.subacls)
272 class AllCompoundACL(SimpleACL):
273 """An ACL that allows if all of its subacls allow."""
278 subacls: Optional[List[SimpleACL]] = None,
279 order_to_check_allow_deny: Order,
280 default_answer: bool,
283 order_to_check_allow_deny=order_to_check_allow_deny,
284 default_answer=default_answer,
286 self.subacls = subacls
289 def check_allowed(self, x: Any) -> bool:
290 if self.subacls is None:
292 return all(acl(x) for acl in self.subacls)
295 def check_denied(self, x: Any) -> bool:
296 if self.subacls is None:
298 return any(not acl(x) for acl in self.subacls)