Rename simple_acl -> acl
[python_utils.git] / acl.py
1 #!/usr/bin/env python3
2
3 from abc import ABC, abstractmethod
4 import fnmatch
5 import logging
6 import re
7 from typing import Any, Callable, List, Optional, Set
8
9
10 logger = logging.getLogger(__name__)
11
12
13 ACL_ORDER_ALLOW_DENY = 1
14 ACL_ORDER_DENY_ALLOW = 2
15
16
17 class SimpleACL(ABC):
18     """A simple Access Control List interface."""
19
20     def __init__(
21         self,
22         *,
23         order_to_check_allow_deny: int,
24         default_answer: bool
25     ):
26         if order_to_check_allow_deny not in (
27                 ACL_ORDER_ALLOW_DENY, ACL_ORDER_DENY_ALLOW
28         ):
29             raise Exception(
30                 'order_to_check_allow_deny must be ACL_ORDER_ALLOW_DENY or ' +
31                 'ACL_ORDER_DENY_ALLOW')
32         self.order_to_check_allow_deny = order_to_check_allow_deny
33         self.default_answer = default_answer
34
35     def __call__(self, x: Any) -> bool:
36         """Returns True if x is allowed, False otherwise."""
37         logger.debug(f'SimpleACL checking {x}')
38         if self.order_to_check_allow_deny == ACL_ORDER_ALLOW_DENY:
39             logger.debug('Checking allowed first...')
40             if self.check_allowed(x):
41                 logger.debug(f'{x} was allowed explicitly.')
42                 return True
43             logger.debug('Checking denied next...')
44             if self.check_denied(x):
45                 logger.debug(f'{x} was denied explicitly.')
46                 return False
47         elif self.order_to_check_allow_deny == ACL_ORDER_DENY_ALLOW:
48             logger.debug('Checking denied first...')
49             if self.check_denied(x):
50                 logger.debug(f'{x} was denied explicitly.')
51                 return False
52             if self.check_allowed(x):
53                 logger.debug(f'{x} was allowed explicitly.')
54                 return True
55
56         logger.debug(
57             f'{x} was not explicitly allowed or denied; ' +
58             f'using default answer ({self.default_answer})'
59         )
60         return self.default_answer
61
62     @abstractmethod
63     def check_allowed(self, x: Any) -> bool:
64         """Return True if x is explicitly allowed, False otherwise."""
65         pass
66
67     @abstractmethod
68     def check_denied(self, x: Any) -> bool:
69         """Return True if x is explicitly denied, False otherwise."""
70         pass
71
72
73 class SetBasedACL(SimpleACL):
74     """An ACL that allows or denies based on membership in a set."""
75     def __init__(self,
76                  *,
77                  allow_set: Optional[Set[Any]] = None,
78                  deny_set: Optional[Set[Any]] = None,
79                  order_to_check_allow_deny: int,
80                  default_answer: bool) -> None:
81         super().__init__(
82             order_to_check_allow_deny=order_to_check_allow_deny,
83             default_answer=default_answer
84         )
85         self.allow_set = allow_set
86         self.deny_set = deny_set
87
88     def check_allowed(self, x: Any) -> bool:
89         if self.allow_set is None:
90             return False
91         return x in self.allow_set
92
93     def check_denied(self, x: Any) -> bool:
94         if self.deny_set is None:
95             return False
96         return x in self.deny_set
97
98
99 class PredicateListBasedACL(SimpleACL):
100     """An ACL that allows or denies by applying predicates."""
101     def __init__(self,
102                  *,
103                  allow_predicate_list: List[Callable[[Any], bool]] = None,
104                  deny_predicate_list: List[Callable[[Any], bool]] = None,
105                  order_to_check_allow_deny: int,
106                  default_answer: bool) -> None:
107         super().__init__(
108             order_to_check_allow_deny=order_to_check_allow_deny,
109             default_answer=default_answer
110         )
111         self.allow_predicate_list = allow_predicate_list
112         self.deny_predicate_list = deny_predicate_list
113
114     def check_allowed(self, x: Any) -> bool:
115         if self.allow_predicate_list is None:
116             return False
117         return any(predicate(x) for predicate in self.allow_predicate_list)
118
119     def check_denied(self, x: Any) -> bool:
120         if self.deny_predicate_list is None:
121             return False
122         return any(predicate(x) for predicate in self.deny_predicate_list)
123
124
125 class StringWildcardBasedACL(PredicateListBasedACL):
126     """An ACL that allows or denies based on string glob (*, ?) patterns."""
127     def __init__(self,
128                  *,
129                  allowed_patterns: Optional[List[str]] = None,
130                  denied_patterns: Optional[List[str]] = None,
131                  order_to_check_allow_deny: int,
132                  default_answer: bool) -> None:
133         allow_predicates = []
134         if allowed_patterns is not None:
135             for pattern in allowed_patterns:
136                 allow_predicates.append(
137                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
138                 )
139         deny_predicates = None
140         if denied_patterns is not None:
141             deny_predicates = []
142             for pattern in denied_patterns:
143                 deny_predicates.append(
144                     lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern)
145                 )
146
147         super().__init__(
148             allow_predicate_list=allow_predicates,
149             deny_predicate_list=deny_predicates,
150             order_to_check_allow_deny=order_to_check_allow_deny,
151             default_answer=default_answer,
152         )
153
154
155 class StringREBasedACL(PredicateListBasedACL):
156     """An ACL that allows or denies by applying regexps."""
157     def __init__(self,
158                  *,
159                  allowed_regexs: Optional[List[re.Pattern]] = None,
160                  denied_regexs: Optional[List[re.Pattern]] = None,
161                  order_to_check_allow_deny: int,
162                  default_answer: bool) -> None:
163         allow_predicates = None
164         if allowed_regexs is not None:
165             allow_predicates = []
166             for pattern in allowed_regexs:
167                 allow_predicates.append(
168                     lambda x, pattern=pattern: pattern.match(x) is not None
169                 )
170         deny_predicates = None
171         if denied_regexs is not None:
172             deny_predicates = []
173             for pattern in denied_regexs:
174                 deny_predicates.append(
175                     lambda x, pattern=pattern: pattern.match(x) is not None
176                 )
177         super().__init__(
178             allow_predicate_list=allow_predicates,
179             deny_predicate_list=deny_predicates,
180             order_to_check_allow_deny=order_to_check_allow_deny,
181             default_answer=default_answer,
182         )
183
184
185 class AnyCompoundACL(object):
186     """An ACL that allows if any of its subacls allow."""
187     def __init__(self, subacls: List[SimpleACL]):
188         assert subacls is not None
189         self.subacls = subacls
190
191     def __call__(self, x: Any):
192         return any(acl(x) for acl in self.subacls)
193
194
195 class AllCompoundACL(object):
196     """An ACL that allows if all of its subacls allow."""
197     def __init__(self, subacls: List[SimpleACL]):
198         assert subacls is not None
199         self.subacls = subacls
200
201     def __call__(self, x: Any):
202         return all(acl(x) for acl in self.subacls)