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 :code:`(*, ?)`
193 allowed_patterns: Optional[List[str]] = None,
194 denied_patterns: Optional[List[str]] = None,
195 order_to_check_allow_deny: Order,
196 default_answer: bool,
198 allow_predicates = []
199 if allowed_patterns is not None:
200 for pattern in allowed_patterns:
201 allow_predicates.append(lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern))
202 deny_predicates = None
203 if denied_patterns is not None:
205 for pattern in denied_patterns:
206 deny_predicates.append(lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern))
209 allow_predicate_list=allow_predicates,
210 deny_predicate_list=deny_predicates,
211 order_to_check_allow_deny=order_to_check_allow_deny,
212 default_answer=default_answer,
216 class StringREBasedACL(PredicateListBasedACL):
217 """An ACL that allows or denies by applying regexps."""
222 allowed_regexs: Optional[List[re.Pattern]] = None,
223 denied_regexs: Optional[List[re.Pattern]] = None,
224 order_to_check_allow_deny: Order,
225 default_answer: bool,
227 allow_predicates = None
228 if allowed_regexs is not None:
229 allow_predicates = []
230 for pattern in allowed_regexs:
231 allow_predicates.append(lambda x, pattern=pattern: pattern.match(x) is not None)
232 deny_predicates = None
233 if denied_regexs is not None:
235 for pattern in denied_regexs:
236 deny_predicates.append(lambda x, pattern=pattern: pattern.match(x) is not None)
238 allow_predicate_list=allow_predicates,
239 deny_predicate_list=deny_predicates,
240 order_to_check_allow_deny=order_to_check_allow_deny,
241 default_answer=default_answer,
245 class AnyCompoundACL(SimpleACL):
246 """An ACL that allows if any of its subacls allow."""
251 subacls: Optional[List[SimpleACL]] = None,
252 order_to_check_allow_deny: Order,
253 default_answer: bool,
256 order_to_check_allow_deny=order_to_check_allow_deny,
257 default_answer=default_answer,
259 self.subacls = subacls
262 def check_allowed(self, x: Any) -> bool:
263 if self.subacls is None:
265 return any(acl(x) for acl in self.subacls)
268 def check_denied(self, x: Any) -> bool:
269 if self.subacls is None:
271 return any(not acl(x) for acl in self.subacls)
274 class AllCompoundACL(SimpleACL):
275 """An ACL that allows if all of its subacls allow."""
280 subacls: Optional[List[SimpleACL]] = None,
281 order_to_check_allow_deny: Order,
282 default_answer: bool,
285 order_to_check_allow_deny=order_to_check_allow_deny,
286 default_answer=default_answer,
288 self.subacls = subacls
291 def check_allowed(self, x: Any) -> bool:
292 if self.subacls is None:
294 return all(acl(x) for acl in self.subacls)
297 def check_denied(self, x: Any) -> bool:
298 if self.subacls is None:
300 return any(not acl(x) for acl in self.subacls)