Ugh, a bunch of things. @overrides. --lmodule. Chromecasts. etc...
[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     UNDEFINED = 0
23     ALLOW_DENY = 1
24     DENY_ALLOW = 2
25
26
27 class SimpleACL(ABC):
28     """A simple Access Control List interface."""
29
30     def __init__(
31         self,
32         *,
33         order_to_check_allow_deny: Order,
34         default_answer: bool
35     ):
36         if order_to_check_allow_deny not in (
37                 Order.ALLOW_DENY, Order.DENY_ALLOW
38         ):
39             raise Exception(
40                 'order_to_check_allow_deny must be Order.ALLOW_DENY or ' +
41                 'Order.DENY_ALLOW')
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     def __init__(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) -> 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     def __init__(self,
116                  *,
117                  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 class DenyListACL(SetBasedACL):
125     """Convenience subclass for a list that only disallows known items.
126     i.e. a 'blocklist'
127     """
128     def __init__(self,
129                  *,
130                  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 class BlockListACL(SetBasedACL):
138     """Convenience subclass for a list that only disallows known items.
139     i.e. a 'blocklist'
140     """
141     def __init__(self,
142                  *,
143                  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 class PredicateListBasedACL(SimpleACL):
151     """An ACL that allows or denies by applying predicates."""
152     def __init__(self,
153                  *,
154                  allow_predicate_list: Sequence[Callable[[Any], bool]] = None,
155                  deny_predicate_list: Sequence[Callable[[Any], bool]] = None,
156                  order_to_check_allow_deny: Order,
157                  default_answer: bool) -> None:
158         super().__init__(
159             order_to_check_allow_deny=order_to_check_allow_deny,
160             default_answer=default_answer
161         )
162         self.allow_predicate_list = allow_predicate_list
163         self.deny_predicate_list = deny_predicate_list
164
165     @overrides
166     def check_allowed(self, x: Any) -> bool:
167         if self.allow_predicate_list is None:
168             return False
169         return any(predicate(x) for predicate in self.allow_predicate_list)
170
171     @overrides
172     def check_denied(self, x: Any) -> bool:
173         if self.deny_predicate_list is None:
174             return False
175         return any(predicate(x) for predicate in self.deny_predicate_list)
176
177
178 class StringWildcardBasedACL(PredicateListBasedACL):
179     """An ACL that allows or denies based on string glob (*, ?) patterns."""
180     def __init__(self,
181                  *,
182                  allowed_patterns: Optional[List[str]] = None,
183                  denied_patterns: Optional[List[str]] = None,
184                  order_to_check_allow_deny: Order,
185                  default_answer: bool) -> None:
186         allow_predicates = []
187         if allowed_patterns is not None:
188             for pattern in allowed_patterns:
189                 allow_predicates.append(
190                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
191                 )
192         deny_predicates = None
193         if denied_patterns is not None:
194             deny_predicates = []
195             for pattern in denied_patterns:
196                 deny_predicates.append(
197                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
198                 )
199
200         super().__init__(
201             allow_predicate_list=allow_predicates,
202             deny_predicate_list=deny_predicates,
203             order_to_check_allow_deny=order_to_check_allow_deny,
204             default_answer=default_answer,
205         )
206
207
208 class StringREBasedACL(PredicateListBasedACL):
209     """An ACL that allows or denies by applying regexps."""
210     def __init__(self,
211                  *,
212                  allowed_regexs: Optional[List[re.Pattern]] = None,
213                  denied_regexs: Optional[List[re.Pattern]] = None,
214                  order_to_check_allow_deny: Order,
215                  default_answer: bool) -> None:
216         allow_predicates = None
217         if allowed_regexs is not None:
218             allow_predicates = []
219             for pattern in allowed_regexs:
220                 allow_predicates.append(
221                     lambda x, pattern=pattern: pattern.match(x) is not None
222                 )
223         deny_predicates = None
224         if denied_regexs is not None:
225             deny_predicates = []
226             for pattern in denied_regexs:
227                 deny_predicates.append(
228                     lambda x, pattern=pattern: pattern.match(x) is not None
229                 )
230         super().__init__(
231             allow_predicate_list=allow_predicates,
232             deny_predicate_list=deny_predicates,
233             order_to_check_allow_deny=order_to_check_allow_deny,
234             default_answer=default_answer,
235         )
236
237
238 class AnyCompoundACL(SimpleACL):
239     """An ACL that allows if any of its subacls allow."""
240     def __init__(self,
241                  *,
242                  subacls: Optional[List[SimpleACL]] = None,
243                  order_to_check_allow_deny: Order,
244                  default_answer: bool) -> None:
245         super().__init__(
246             order_to_check_allow_deny = order_to_check_allow_deny,
247             default_answer = default_answer
248         )
249         self.subacls = subacls
250
251     @overrides
252     def check_allowed(self, x: Any) -> bool:
253         if self.subacls is None:
254             return False
255         return any(acl(x) for acl in self.subacls)
256
257     @overrides
258     def check_denied(self, x: Any) -> bool:
259         if self.subacls is None:
260             return False
261         return any(not acl(x) for acl in self.subacls)
262
263
264 class AllCompoundACL(SimpleACL):
265     """An ACL that allows if all of its subacls allow."""
266     def __init__(self,
267                  *,
268                  subacls: Optional[List[SimpleACL]] = None,
269                  order_to_check_allow_deny: Order,
270                  default_answer: bool) -> None:
271         super().__init__(
272             order_to_check_allow_deny = order_to_check_allow_deny,
273             default_answer = default_answer
274         )
275         self.subacls = subacls
276
277     @overrides
278     def check_allowed(self, x: Any) -> bool:
279         if self.subacls is None:
280             return False
281         return all(acl(x) for acl in self.subacls)
282
283     @overrides
284     def check_denied(self, x: Any) -> bool:
285         if self.subacls is None:
286             return False
287         return any(not acl(x) for acl in self.subacls)