Easier and more self documenting patterns for loading/saving Persistent
[python_utils.git] / acl.py
1 #!/usr/bin/env python3
2
3 # © Copyright 2021-2022, Scott Gasch
4
5 """This module defines various flavors of Access Control Lists."""
6
7 import enum
8 import fnmatch
9 import logging
10 import re
11 from abc import ABC, abstractmethod
12 from typing import Any, Callable, List, Optional, Sequence, Set
13
14 from overrides import overrides
15
16 # This module is commonly used by others in here and should avoid
17 # taking any unnecessary dependencies back on them.
18
19 logger = logging.getLogger(__name__)
20
21
22 class Order(enum.Enum):
23     """A helper to express the order of evaluation for allows/denies
24     in an Access Control List.
25     """
26
27     UNDEFINED = 0
28     ALLOW_DENY = 1
29     DENY_ALLOW = 2
30
31
32 class SimpleACL(ABC):
33     """A simple Access Control List interface."""
34
35     def __init__(self, *, order_to_check_allow_deny: Order, default_answer: bool):
36         if order_to_check_allow_deny not in (
37             Order.ALLOW_DENY,
38             Order.DENY_ALLOW,
39         ):
40             raise Exception(
41                 'order_to_check_allow_deny must be Order.ALLOW_DENY or ' + 'Order.DENY_ALLOW'
42             )
43         self.order_to_check_allow_deny = order_to_check_allow_deny
44         self.default_answer = default_answer
45
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)
53                 return True
54             logger.debug('Checking denied next...')
55             if self.check_denied(x):
56                 logger.debug('%s was denied explicitly.', x)
57                 return False
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)
62                 return False
63             if self.check_allowed(x):
64                 logger.debug('%s was allowed explicitly.', x)
65                 return True
66
67         logger.debug(
68             f'{x} was not explicitly allowed or denied; '
69             + f'using default answer ({self.default_answer})'
70         )
71         return self.default_answer
72
73     @abstractmethod
74     def check_allowed(self, x: Any) -> bool:
75         """Return True if x is explicitly allowed, False otherwise."""
76         pass
77
78     @abstractmethod
79     def check_denied(self, x: Any) -> bool:
80         """Return True if x is explicitly denied, False otherwise."""
81         pass
82
83
84 class SetBasedACL(SimpleACL):
85     """An ACL that allows or denies based on membership in a set."""
86
87     def __init__(
88         self,
89         *,
90         allow_set: Optional[Set[Any]] = None,
91         deny_set: Optional[Set[Any]] = None,
92         order_to_check_allow_deny: Order,
93         default_answer: bool,
94     ) -> None:
95         super().__init__(
96             order_to_check_allow_deny=order_to_check_allow_deny,
97             default_answer=default_answer,
98         )
99         self.allow_set = allow_set
100         self.deny_set = deny_set
101
102     @overrides
103     def check_allowed(self, x: Any) -> bool:
104         if self.allow_set is None:
105             return False
106         return x in self.allow_set
107
108     @overrides
109     def check_denied(self, x: Any) -> bool:
110         if self.deny_set is None:
111             return False
112         return x in self.deny_set
113
114
115 class AllowListACL(SetBasedACL):
116     """Convenience subclass for a list that only allows known items.
117     i.e. a 'allowlist'
118     """
119
120     def __init__(self, *, allow_set: Optional[Set[Any]]) -> None:
121         super().__init__(
122             allow_set=allow_set,
123             order_to_check_allow_deny=Order.ALLOW_DENY,
124             default_answer=False,
125         )
126
127
128 class DenyListACL(SetBasedACL):
129     """Convenience subclass for a list that only disallows known items.
130     i.e. a 'blocklist'
131     """
132
133     def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
134         super().__init__(
135             deny_set=deny_set,
136             order_to_check_allow_deny=Order.ALLOW_DENY,
137             default_answer=True,
138         )
139
140
141 class BlockListACL(SetBasedACL):
142     """Convenience subclass for a list that only disallows known items.
143     i.e. a 'blocklist'
144     """
145
146     def __init__(self, *, deny_set: Optional[Set[Any]]) -> None:
147         super().__init__(
148             deny_set=deny_set,
149             order_to_check_allow_deny=Order.ALLOW_DENY,
150             default_answer=True,
151         )
152
153
154 class PredicateListBasedACL(SimpleACL):
155     """An ACL that allows or denies by applying predicates."""
156
157     def __init__(
158         self,
159         *,
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,
164     ) -> None:
165         super().__init__(
166             order_to_check_allow_deny=order_to_check_allow_deny,
167             default_answer=default_answer,
168         )
169         self.allow_predicate_list = allow_predicate_list
170         self.deny_predicate_list = deny_predicate_list
171
172     @overrides
173     def check_allowed(self, x: Any) -> bool:
174         if self.allow_predicate_list is None:
175             return False
176         return any(predicate(x) for predicate in self.allow_predicate_list)
177
178     @overrides
179     def check_denied(self, x: Any) -> bool:
180         if self.deny_predicate_list is None:
181             return False
182         return any(predicate(x) for predicate in self.deny_predicate_list)
183
184
185 class StringWildcardBasedACL(PredicateListBasedACL):
186     """An ACL that allows or denies based on string glob :code:`(*, ?)`
187     patterns.
188     """
189
190     def __init__(
191         self,
192         *,
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,
197     ) -> None:
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:
204             deny_predicates = []
205             for pattern in denied_patterns:
206                 deny_predicates.append(lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern))
207
208         super().__init__(
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,
213         )
214
215
216 class StringREBasedACL(PredicateListBasedACL):
217     """An ACL that allows or denies by applying regexps."""
218
219     def __init__(
220         self,
221         *,
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,
226     ) -> None:
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:
234             deny_predicates = []
235             for pattern in denied_regexs:
236                 deny_predicates.append(lambda x, pattern=pattern: pattern.match(x) is not None)
237         super().__init__(
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,
242         )
243
244
245 class AnyCompoundACL(SimpleACL):
246     """An ACL that allows if any of its subacls allow."""
247
248     def __init__(
249         self,
250         *,
251         subacls: Optional[List[SimpleACL]] = None,
252         order_to_check_allow_deny: Order,
253         default_answer: bool,
254     ) -> None:
255         super().__init__(
256             order_to_check_allow_deny=order_to_check_allow_deny,
257             default_answer=default_answer,
258         )
259         self.subacls = subacls
260
261     @overrides
262     def check_allowed(self, x: Any) -> bool:
263         if self.subacls is None:
264             return False
265         return any(acl(x) for acl in self.subacls)
266
267     @overrides
268     def check_denied(self, x: Any) -> bool:
269         if self.subacls is None:
270             return False
271         return any(not acl(x) for acl in self.subacls)
272
273
274 class AllCompoundACL(SimpleACL):
275     """An ACL that allows if all of its subacls allow."""
276
277     def __init__(
278         self,
279         *,
280         subacls: Optional[List[SimpleACL]] = None,
281         order_to_check_allow_deny: Order,
282         default_answer: bool,
283     ) -> None:
284         super().__init__(
285             order_to_check_allow_deny=order_to_check_allow_deny,
286             default_answer=default_answer,
287         )
288         self.subacls = subacls
289
290     @overrides
291     def check_allowed(self, x: Any) -> bool:
292         if self.subacls is None:
293             return False
294         return all(acl(x) for acl in self.subacls)
295
296     @overrides
297     def check_denied(self, x: Any) -> bool:
298         if self.subacls is None:
299             return False
300         return any(not acl(x) for acl in self.subacls)