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 '
44 self.order_to_check_allow_deny = order_to_check_allow_deny
45 self.default_answer = default_answer
47 def __call__(self, x: Any) -> bool:
48 """Returns True if x is allowed, False otherwise."""
49 logger.debug('SimpleACL checking %s', x)
50 if self.order_to_check_allow_deny == Order.ALLOW_DENY:
51 logger.debug('Checking allowed first...')
52 if self.check_allowed(x):
53 logger.debug('%s was allowed explicitly.', x)
55 logger.debug('Checking denied next...')
56 if self.check_denied(x):
57 logger.debug('%s was denied explicitly.', x)
59 elif self.order_to_check_allow_deny == Order.DENY_ALLOW:
60 logger.debug('Checking denied first...')
61 if self.check_denied(x):
62 logger.debug('%s was denied explicitly.', x)
64 if self.check_allowed(x):
65 logger.debug('%s was allowed explicitly.', x)
69 f'{x} was not explicitly allowed or denied; '
70 + f'using default answer ({self.default_answer})'
72 return self.default_answer
75 def check_allowed(self, x: Any) -> bool:
76 """Return True if x is explicitly allowed, False otherwise."""
80 def check_denied(self, x: Any) -> bool:
81 """Return True if x is explicitly denied, False otherwise."""
85 class SetBasedACL(SimpleACL):
86 """An ACL that allows or denies based on membership in a set."""
91 allow_set: Optional[Set[Any]] = None,
92 deny_set: Optional[Set[Any]] = None,
93 order_to_check_allow_deny: Order,
97 order_to_check_allow_deny=order_to_check_allow_deny,
98 default_answer=default_answer,
100 self.allow_set = allow_set
101 self.deny_set = deny_set
104 def check_allowed(self, x: Any) -> bool:
105 if self.allow_set is None:
107 return x in self.allow_set
110 def check_denied(self, x: Any) -> bool:
111 if self.deny_set is None:
113 return x in self.deny_set
116 class AllowListACL(SetBasedACL):
117 """Convenience subclass for a list that only allows known items.
121 def __init__(self, *, allow_set: Optional[Set[Any]]) -> None:
124 order_to_check_allow_deny=Order.ALLOW_DENY,
125 default_answer=False,
129 class DenyListACL(SetBasedACL):
130 """Convenience subclass for a list that only disallows known items.
134 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
137 order_to_check_allow_deny=Order.ALLOW_DENY,
142 class BlockListACL(SetBasedACL):
143 """Convenience subclass for a list that only disallows known items.
147 def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
150 order_to_check_allow_deny=Order.ALLOW_DENY,
155 class PredicateListBasedACL(SimpleACL):
156 """An ACL that allows or denies by applying predicates."""
161 allow_predicate_list: Sequence[Callable[[Any], bool]] = None,
162 deny_predicate_list: Sequence[Callable[[Any], bool]] = None,
163 order_to_check_allow_deny: Order,
164 default_answer: bool,
167 order_to_check_allow_deny=order_to_check_allow_deny,
168 default_answer=default_answer,
170 self.allow_predicate_list = allow_predicate_list
171 self.deny_predicate_list = deny_predicate_list
174 def check_allowed(self, x: Any) -> bool:
175 if self.allow_predicate_list is None:
177 return any(predicate(x) for predicate in self.allow_predicate_list)
180 def check_denied(self, x: Any) -> bool:
181 if self.deny_predicate_list is None:
183 return any(predicate(x) for predicate in self.deny_predicate_list)
186 class StringWildcardBasedACL(PredicateListBasedACL):
187 """An ACL that allows or denies based on string glob :code:`(*, ?)`
194 allowed_patterns: Optional[List[str]] = None,
195 denied_patterns: Optional[List[str]] = None,
196 order_to_check_allow_deny: Order,
197 default_answer: bool,
199 allow_predicates = []
200 if allowed_patterns is not None:
201 for pattern in allowed_patterns:
202 allow_predicates.append(
203 lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
205 deny_predicates = None
206 if denied_patterns is not None:
208 for pattern in denied_patterns:
209 deny_predicates.append(
210 lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
214 allow_predicate_list=allow_predicates,
215 deny_predicate_list=deny_predicates,
216 order_to_check_allow_deny=order_to_check_allow_deny,
217 default_answer=default_answer,
221 class StringREBasedACL(PredicateListBasedACL):
222 """An ACL that allows or denies by applying regexps."""
227 allowed_regexs: Optional[List[re.Pattern]] = None,
228 denied_regexs: Optional[List[re.Pattern]] = None,
229 order_to_check_allow_deny: Order,
230 default_answer: bool,
232 allow_predicates = None
233 if allowed_regexs is not None:
234 allow_predicates = []
235 for pattern in allowed_regexs:
236 allow_predicates.append(
237 lambda x, pattern=pattern: pattern.match(x) is not None
239 deny_predicates = None
240 if denied_regexs is not None:
242 for pattern in denied_regexs:
243 deny_predicates.append(
244 lambda x, pattern=pattern: pattern.match(x) is not None
247 allow_predicate_list=allow_predicates,
248 deny_predicate_list=deny_predicates,
249 order_to_check_allow_deny=order_to_check_allow_deny,
250 default_answer=default_answer,
254 class AnyCompoundACL(SimpleACL):
255 """An ACL that allows if any of its subacls allow."""
260 subacls: Optional[List[SimpleACL]] = None,
261 order_to_check_allow_deny: Order,
262 default_answer: bool,
265 order_to_check_allow_deny=order_to_check_allow_deny,
266 default_answer=default_answer,
268 self.subacls = subacls
271 def check_allowed(self, x: Any) -> bool:
272 if self.subacls is None:
274 return any(acl(x) for acl in self.subacls)
277 def check_denied(self, x: Any) -> bool:
278 if self.subacls is None:
280 return any(not acl(x) for acl in self.subacls)
283 class AllCompoundACL(SimpleACL):
284 """An ACL that allows if all of its subacls allow."""
289 subacls: Optional[List[SimpleACL]] = None,
290 order_to_check_allow_deny: Order,
291 default_answer: bool,
294 order_to_check_allow_deny=order_to_check_allow_deny,
295 default_answer=default_answer,
297 self.subacls = subacls
300 def check_allowed(self, x: Any) -> bool:
301 if self.subacls is None:
303 return all(acl(x) for acl in self.subacls)
306 def check_denied(self, x: Any) -> bool:
307 if self.subacls is None:
309 return any(not acl(x) for acl in self.subacls)