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