diff options
Diffstat (limited to 'acl.py')
| -rw-r--r-- | acl.py | 202 |
1 files changed, 202 insertions, 0 deletions
@@ -0,0 +1,202 @@ +#!/usr/bin/env python3 + +from abc import ABC, abstractmethod +import fnmatch +import logging +import re +from typing import Any, Callable, List, Optional, Set + + +logger = logging.getLogger(__name__) + + +ACL_ORDER_ALLOW_DENY = 1 +ACL_ORDER_DENY_ALLOW = 2 + + +class SimpleACL(ABC): + """A simple Access Control List interface.""" + + def __init__( + self, + *, + order_to_check_allow_deny: int, + default_answer: bool + ): + if order_to_check_allow_deny not in ( + ACL_ORDER_ALLOW_DENY, ACL_ORDER_DENY_ALLOW + ): + raise Exception( + 'order_to_check_allow_deny must be ACL_ORDER_ALLOW_DENY or ' + + 'ACL_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: + 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 == ACL_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: int, + 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 + + def check_allowed(self, x: Any) -> bool: + if self.allow_set is None: + return False + return x in self.allow_set + + def check_denied(self, x: Any) -> bool: + if self.deny_set is None: + return False + return x in self.deny_set + + +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, + 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 + + 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) + + 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: int, + 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: int, + 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(object): + """An ACL that allows if any of its subacls allow.""" + def __init__(self, subacls: List[SimpleACL]): + assert subacls is not None + self.subacls = subacls + + def __call__(self, x: Any): + return any(acl(x) for acl in self.subacls) + + +class AllCompoundACL(object): + """An ACL that allows if all of its subacls allow.""" + def __init__(self, subacls: List[SimpleACL]): + assert subacls is not None + self.subacls = subacls + + def __call__(self, x: Any): + return all(acl(x) for acl in self.subacls) |
