Get rid of blacklist/whitelist; use blocklist/allowlist instead.
[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 # This module is commonly used by others in here and should avoid
11 # taking any unnecessary dependencies back on them.
12
13 logger = logging.getLogger(__name__)
14
15
16 class Order(enum.Enum):
17     """A helper to express the order of evaluation for allows/denies
18     in an Access Control List.
19     """
20     UNDEFINED = 0
21     ALLOW_DENY = 1
22     DENY_ALLOW = 2
23
24
25 class SimpleACL(ABC):
26     """A simple Access Control List interface."""
27
28     def __init__(
29         self,
30         *,
31         order_to_check_allow_deny: Order,
32         default_answer: bool
33     ):
34         if order_to_check_allow_deny not in (
35                 Order.ALLOW_DENY, Order.DENY_ALLOW
36         ):
37             raise Exception(
38                 'order_to_check_allow_deny must be Order.ALLOW_DENY or ' +
39                 'Order.DENY_ALLOW')
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     def __init__(self,
84                  *,
85                  allow_set: Optional[Set[Any]] = None,
86                  deny_set: Optional[Set[Any]] = None,
87                  order_to_check_allow_deny: Order,
88                  default_answer: bool) -> None:
89         super().__init__(
90             order_to_check_allow_deny=order_to_check_allow_deny,
91             default_answer=default_answer
92         )
93         self.allow_set = allow_set
94         self.deny_set = deny_set
95
96     def check_allowed(self, x: Any) -> bool:
97         if self.allow_set is None:
98             return False
99         return x in self.allow_set
100
101     def check_denied(self, x: Any) -> bool:
102         if self.deny_set is None:
103             return False
104         return x in self.deny_set
105
106
107 class AllowListACL(SetBasedACL):
108     """Convenience subclass for a list that only allows known items.
109     i.e. a 'allowlist'
110     """
111     def __init__(self,
112                  *,
113                  allow_set: Optional[Set[Any]]) -> None:
114         super().__init__(
115             allow_set = allow_set,
116             order_to_check_allow_deny = Order.ALLOW_DENY,
117             default_answer = False)
118
119
120 class DenyListACL(SetBasedACL):
121     """Convenience subclass for a list that only disallows known items.
122     i.e. a 'blocklist'
123     """
124     def __init__(self,
125                  *,
126                  deny_set: Optional[Set[Any]]) -> None:
127         super().__init__(
128             deny_set = deny_set,
129             order_to_check_allow_deny = Order.ALLOW_DENY,
130             default_answer = True)
131
132
133 class BlockListACL(SetBasedACL):
134     """Convenience subclass for a list that only disallows known items.
135     i.e. a 'blocklist'
136     """
137     def __init__(self,
138                  *,
139                  deny_set: Optional[Set[Any]]) -> None:
140         super().__init__(
141             deny_set = deny_set,
142             order_to_check_allow_deny = Order.ALLOW_DENY,
143             default_answer = True)
144
145
146 class PredicateListBasedACL(SimpleACL):
147     """An ACL that allows or denies by applying predicates."""
148     def __init__(self,
149                  *,
150                  allow_predicate_list: Sequence[Callable[[Any], bool]] = None,
151                  deny_predicate_list: Sequence[Callable[[Any], bool]] = None,
152                  order_to_check_allow_deny: Order,
153                  default_answer: bool) -> None:
154         super().__init__(
155             order_to_check_allow_deny=order_to_check_allow_deny,
156             default_answer=default_answer
157         )
158         self.allow_predicate_list = allow_predicate_list
159         self.deny_predicate_list = deny_predicate_list
160
161     def check_allowed(self, x: Any) -> bool:
162         if self.allow_predicate_list is None:
163             return False
164         return any(predicate(x) for predicate in self.allow_predicate_list)
165
166     def check_denied(self, x: Any) -> bool:
167         if self.deny_predicate_list is None:
168             return False
169         return any(predicate(x) for predicate in self.deny_predicate_list)
170
171
172 class StringWildcardBasedACL(PredicateListBasedACL):
173     """An ACL that allows or denies based on string glob (*, ?) patterns."""
174     def __init__(self,
175                  *,
176                  allowed_patterns: Optional[List[str]] = None,
177                  denied_patterns: Optional[List[str]] = None,
178                  order_to_check_allow_deny: Order,
179                  default_answer: bool) -> None:
180         allow_predicates = []
181         if allowed_patterns is not None:
182             for pattern in allowed_patterns:
183                 allow_predicates.append(
184                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
185                 )
186         deny_predicates = None
187         if denied_patterns is not None:
188             deny_predicates = []
189             for pattern in denied_patterns:
190                 deny_predicates.append(
191                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
192                 )
193
194         super().__init__(
195             allow_predicate_list=allow_predicates,
196             deny_predicate_list=deny_predicates,
197             order_to_check_allow_deny=order_to_check_allow_deny,
198             default_answer=default_answer,
199         )
200
201
202 class StringREBasedACL(PredicateListBasedACL):
203     """An ACL that allows or denies by applying regexps."""
204     def __init__(self,
205                  *,
206                  allowed_regexs: Optional[List[re.Pattern]] = None,
207                  denied_regexs: Optional[List[re.Pattern]] = None,
208                  order_to_check_allow_deny: Order,
209                  default_answer: bool) -> None:
210         allow_predicates = None
211         if allowed_regexs is not None:
212             allow_predicates = []
213             for pattern in allowed_regexs:
214                 allow_predicates.append(
215                     lambda x, pattern=pattern: pattern.match(x) is not None
216                 )
217         deny_predicates = None
218         if denied_regexs is not None:
219             deny_predicates = []
220             for pattern in denied_regexs:
221                 deny_predicates.append(
222                     lambda x, pattern=pattern: pattern.match(x) is not None
223                 )
224         super().__init__(
225             allow_predicate_list=allow_predicates,
226             deny_predicate_list=deny_predicates,
227             order_to_check_allow_deny=order_to_check_allow_deny,
228             default_answer=default_answer,
229         )
230
231
232 class AnyCompoundACL(SimpleACL):
233     """An ACL that allows if any of its subacls allow."""
234     def __init__(self,
235                  *,
236                  subacls: Optional[List[SimpleACL]] = None,
237                  order_to_check_allow_deny: Order,
238                  default_answer: bool) -> None:
239         super().__init__(
240             order_to_check_allow_deny = order_to_check_allow_deny,
241             default_answer = default_answer
242         )
243         self.subacls = subacls
244
245     def check_allowed(self, x: Any) -> bool:
246         if self.subacls is None:
247             return False
248         return any(acl(x) for acl in self.subacls)
249
250     def check_denied(self, x: Any) -> bool:
251         if self.subacls is None:
252             return False
253         return any(not acl(x) for acl in self.subacls)
254
255
256 class AllCompoundACL(SimpleACL):
257     """An ACL that allows if all of its subacls allow."""
258     def __init__(self,
259                  *,
260                  subacls: Optional[List[SimpleACL]] = None,
261                  order_to_check_allow_deny: Order,
262                  default_answer: bool) -> None:
263         super().__init__(
264             order_to_check_allow_deny = order_to_check_allow_deny,
265             default_answer = default_answer
266         )
267         self.subacls = subacls
268
269     def check_allowed(self, x: Any) -> bool:
270         if self.subacls is None:
271             return False
272         return all(acl(x) for acl in self.subacls)
273
274     def check_denied(self, x: Any) -> bool:
275         if self.subacls is None:
276             return False
277         return any(not acl(x) for acl in self.subacls)