#!/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, 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__) 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): """A simple Access Control List interface.""" def __init__( self, *, order_to_check_allow_deny: Order, default_answer: bool ): if order_to_check_allow_deny not in ( Order.ALLOW_DENY, Order.DENY_ALLOW ): raise Exception( '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 == Order.ALLOW_DENY: logger.debug('Checking allowed first...') if self.check_allowed(x): logger.debug(f'{x} was allowed explicitly.') return True logger.debug('Checking denied next...') if self.check_denied(x): logger.debug(f'{x} was denied explicitly.') return False 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.') return False if self.check_allowed(x): logger.debug(f'{x} was allowed explicitly.') return True logger.debug( f'{x} was not explicitly allowed or denied; ' + f'using default answer ({self.default_answer})' ) return self.default_answer @abstractmethod def check_allowed(self, x: Any) -> bool: """Return True if x is explicitly allowed, False otherwise.""" pass @abstractmethod def check_denied(self, x: Any) -> bool: """Return True if x is explicitly denied, False otherwise.""" pass class SetBasedACL(SimpleACL): """An ACL that allows or denies based on membership in a set.""" def __init__(self, *, allow_set: Optional[Set[Any]] = None, deny_set: Optional[Set[Any]] = 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.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: 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, default_answer=default_answer ) 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 return any(predicate(x) for predicate in self.deny_predicate_list) class StringWildcardBasedACL(PredicateListBasedACL): """An ACL that allows or denies based on string glob (*, ?) patterns.""" def __init__(self, *, allowed_patterns: Optional[List[str]] = None, denied_patterns: Optional[List[str]] = None, order_to_check_allow_deny: Order, default_answer: bool) -> None: allow_predicates = [] if allowed_patterns is not None: for pattern in allowed_patterns: allow_predicates.append( lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern) ) deny_predicates = None if denied_patterns is not None: deny_predicates = [] for pattern in denied_patterns: deny_predicates.append( lambda x, pattern=pattern: fnmatch.fnmatch(x, pattern) ) super().__init__( allow_predicate_list=allow_predicates, deny_predicate_list=deny_predicates, order_to_check_allow_deny=order_to_check_allow_deny, default_answer=default_answer, ) class StringREBasedACL(PredicateListBasedACL): """An ACL that allows or denies by applying regexps.""" def __init__(self, *, allowed_regexs: Optional[List[re.Pattern]] = None, denied_regexs: Optional[List[re.Pattern]] = None, order_to_check_allow_deny: Order, default_answer: bool) -> None: allow_predicates = None if allowed_regexs is not None: allow_predicates = [] for pattern in allowed_regexs: allow_predicates.append( lambda x, pattern=pattern: pattern.match(x) is not None ) deny_predicates = None if denied_regexs is not None: deny_predicates = [] for pattern in denied_regexs: deny_predicates.append( lambda x, pattern=pattern: pattern.match(x) is not None ) super().__init__( allow_predicate_list=allow_predicates, deny_predicate_list=deny_predicates, order_to_check_allow_deny=order_to_check_allow_deny, default_answer=default_answer, ) class AnyCompoundACL(SimpleACL): """An ACL that allows if any of its subacls allow.""" 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 @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(SimpleACL): """An ACL that allows if all of its subacls allow.""" 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 @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)