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