Make smart futures avoid polling.
[python_utils.git] / acl.py
diff --git a/acl.py b/acl.py
index 5040304a7913fe4e452ceb2dc5ef5f2a76b36491..2b347673af1b14dbb144ca9179d115a5eba642b0 100644 (file)
--- a/acl.py
+++ b/acl.py
@@ -1,17 +1,27 @@
 #!/usr/bin/env python3
 
 from abc import ABC, abstractmethod
+import enum
 import fnmatch
 import logging
 import re
-from typing import Any, Callable, List, Optional, Set
+from typing import Any, Callable, List, Optional, Set, Sequence
 
+from overrides import overrides
+
+# This module is commonly used by others in here and should avoid
+# taking any unnecessary dependencies back on them.
 
 logger = logging.getLogger(__name__)
 
 
-ACL_ORDER_ALLOW_DENY = 1
-ACL_ORDER_DENY_ALLOW = 2
+class Order(enum.Enum):
+    """A helper to express the order of evaluation for allows/denies
+    in an Access Control List.
+    """
+    UNDEFINED = 0
+    ALLOW_DENY = 1
+    DENY_ALLOW = 2
 
 
 class SimpleACL(ABC):
@@ -20,22 +30,22 @@ class SimpleACL(ABC):
     def __init__(
         self,
         *,
-        order_to_check_allow_deny: int,
+        order_to_check_allow_deny: Order,
         default_answer: bool
     ):
         if order_to_check_allow_deny not in (
-                ACL_ORDER_ALLOW_DENY, ACL_ORDER_DENY_ALLOW
+                Order.ALLOW_DENY, Order.DENY_ALLOW
         ):
             raise Exception(
-                'order_to_check_allow_deny must be ACL_ORDER_ALLOW_DENY or ' +
-                'ACL_ORDER_DENY_ALLOW')
+                'order_to_check_allow_deny must be Order.ALLOW_DENY or ' +
+                'Order.DENY_ALLOW')
         self.order_to_check_allow_deny = order_to_check_allow_deny
         self.default_answer = default_answer
 
     def __call__(self, x: Any) -> bool:
         """Returns True if x is allowed, False otherwise."""
         logger.debug(f'SimpleACL checking {x}')
-        if self.order_to_check_allow_deny == ACL_ORDER_ALLOW_DENY:
+        if self.order_to_check_allow_deny == Order.ALLOW_DENY:
             logger.debug('Checking allowed first...')
             if self.check_allowed(x):
                 logger.debug(f'{x} was allowed explicitly.')
@@ -44,7 +54,7 @@ class SimpleACL(ABC):
             if self.check_denied(x):
                 logger.debug(f'{x} was denied explicitly.')
                 return False
-        elif self.order_to_check_allow_deny == ACL_ORDER_DENY_ALLOW:
+        elif self.order_to_check_allow_deny == Order.DENY_ALLOW:
             logger.debug('Checking denied first...')
             if self.check_denied(x):
                 logger.debug(f'{x} was denied explicitly.')
@@ -76,7 +86,7 @@ class SetBasedACL(SimpleACL):
                  *,
                  allow_set: Optional[Set[Any]] = None,
                  deny_set: Optional[Set[Any]] = None,
-                 order_to_check_allow_deny: int,
+                 order_to_check_allow_deny: Order,
                  default_answer: bool) -> None:
         super().__init__(
             order_to_check_allow_deny=order_to_check_allow_deny,
@@ -85,24 +95,65 @@ class SetBasedACL(SimpleACL):
         self.allow_set = allow_set
         self.deny_set = deny_set
 
+    @overrides
     def check_allowed(self, x: Any) -> bool:
         if self.allow_set is None:
             return False
         return x in self.allow_set
 
+    @overrides
     def check_denied(self, x: Any) -> bool:
         if self.deny_set is None:
             return False
         return x in self.deny_set
 
 
+class AllowListACL(SetBasedACL):
+    """Convenience subclass for a list that only allows known items.
+    i.e. a 'allowlist'
+    """
+    def __init__(self,
+                 *,
+                 allow_set: Optional[Set[Any]]) -> None:
+        super().__init__(
+            allow_set = allow_set,
+            order_to_check_allow_deny = Order.ALLOW_DENY,
+            default_answer = False)
+
+
+class DenyListACL(SetBasedACL):
+    """Convenience subclass for a list that only disallows known items.
+    i.e. a 'blocklist'
+    """
+    def __init__(self,
+                 *,
+                 deny_set: Optional[Set[Any]]) -> None:
+        super().__init__(
+            deny_set = deny_set,
+            order_to_check_allow_deny = Order.ALLOW_DENY,
+            default_answer = True)
+
+
+class BlockListACL(SetBasedACL):
+    """Convenience subclass for a list that only disallows known items.
+    i.e. a 'blocklist'
+    """
+    def __init__(self,
+                 *,
+                 deny_set: Optional[Set[Any]]) -> None:
+        super().__init__(
+            deny_set = deny_set,
+            order_to_check_allow_deny = Order.ALLOW_DENY,
+            default_answer = True)
+
+
 class PredicateListBasedACL(SimpleACL):
     """An ACL that allows or denies by applying predicates."""
     def __init__(self,
                  *,
-                 allow_predicate_list: List[Callable[[Any], bool]] = None,
-                 deny_predicate_list: List[Callable[[Any], bool]] = None,
-                 order_to_check_allow_deny: int,
+                 allow_predicate_list: Sequence[Callable[[Any], bool]] = None,
+                 deny_predicate_list: Sequence[Callable[[Any], bool]] = None,
+                 order_to_check_allow_deny: Order,
                  default_answer: bool) -> None:
         super().__init__(
             order_to_check_allow_deny=order_to_check_allow_deny,
@@ -111,11 +162,13 @@ class PredicateListBasedACL(SimpleACL):
         self.allow_predicate_list = allow_predicate_list
         self.deny_predicate_list = deny_predicate_list
 
+    @overrides
     def check_allowed(self, x: Any) -> bool:
         if self.allow_predicate_list is None:
             return False
         return any(predicate(x) for predicate in self.allow_predicate_list)
 
+    @overrides
     def check_denied(self, x: Any) -> bool:
         if self.deny_predicate_list is None:
             return False
@@ -128,7 +181,7 @@ class StringWildcardBasedACL(PredicateListBasedACL):
                  *,
                  allowed_patterns: Optional[List[str]] = None,
                  denied_patterns: Optional[List[str]] = None,
-                 order_to_check_allow_deny: int,
+                 order_to_check_allow_deny: Order,
                  default_answer: bool) -> None:
         allow_predicates = []
         if allowed_patterns is not None:
@@ -158,7 +211,7 @@ class StringREBasedACL(PredicateListBasedACL):
                  *,
                  allowed_regexs: Optional[List[re.Pattern]] = None,
                  denied_regexs: Optional[List[re.Pattern]] = None,
-                 order_to_check_allow_deny: int,
+                 order_to_check_allow_deny: Order,
                  default_answer: bool) -> None:
         allow_predicates = None
         if allowed_regexs is not None:
@@ -182,21 +235,53 @@ class StringREBasedACL(PredicateListBasedACL):
         )
 
 
-class AnyCompoundACL(object):
+class AnyCompoundACL(SimpleACL):
     """An ACL that allows if any of its subacls allow."""
-    def __init__(self, subacls: List[SimpleACL]):
-        assert subacls is not None
+    def __init__(self,
+                 *,
+                 subacls: Optional[List[SimpleACL]] = None,
+                 order_to_check_allow_deny: Order,
+                 default_answer: bool) -> None:
+        super().__init__(
+            order_to_check_allow_deny = order_to_check_allow_deny,
+            default_answer = default_answer
+        )
         self.subacls = subacls
 
-    def __call__(self, x: Any):
+    @overrides
+    def check_allowed(self, x: Any) -> bool:
+        if self.subacls is None:
+            return False
         return any(acl(x) for acl in self.subacls)
 
+    @overrides
+    def check_denied(self, x: Any) -> bool:
+        if self.subacls is None:
+            return False
+        return any(not acl(x) for acl in self.subacls)
 
-class AllCompoundACL(object):
+
+class AllCompoundACL(SimpleACL):
     """An ACL that allows if all of its subacls allow."""
-    def __init__(self, subacls: List[SimpleACL]):
-        assert subacls is not None
+    def __init__(self,
+                 *,
+                 subacls: Optional[List[SimpleACL]] = None,
+                 order_to_check_allow_deny: Order,
+                 default_answer: bool) -> None:
+        super().__init__(
+            order_to_check_allow_deny = order_to_check_allow_deny,
+            default_answer = default_answer
+        )
         self.subacls = subacls
 
-    def __call__(self, x: Any):
+    @overrides
+    def check_allowed(self, x: Any) -> bool:
+        if self.subacls is None:
+            return False
         return all(acl(x) for acl in self.subacls)
+
+    @overrides
+    def check_denied(self, x: Any) -> bool:
+        if self.subacls is None:
+            return False
+        return any(not acl(x) for acl in self.subacls)