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