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