"Fossies" - the Fresh Open Source Software Archive

Member "LinOTP-release-2.10.5.2/linotpd/src/linotp/lib/policy/evaluate.py" (13 May 2019, 22308 Bytes) of package /linux/misc/LinOTP-release-2.10.5.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "evaluate.py" see the Fossies "Dox" file reference documentation.

    1 # -*- coding: utf-8 -*-
    2 #
    3 #    LinOTP - the open source solution for two factor authentication
    4 #    Copyright (C) 2010 - 2019 KeyIdentity GmbH
    5 #
    6 #    This file is part of LinOTP server.
    7 #
    8 #    This program is free software: you can redistribute it and/or
    9 #    modify it under the terms of the GNU Affero General Public
   10 #    License, version 3, as published by the Free Software Foundation.
   11 #
   12 #    This program is distributed in the hope that it will be useful,
   13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
   14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   15 #    GNU Affero General Public License for more details.
   16 #
   17 #    You should have received a copy of the
   18 #               GNU Affero General Public License
   19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
   20 #
   21 #
   22 #    E-mail: linotp@keyidentity.com
   23 #    Contact: www.linotp.org
   24 #    Support: www.keyidentity.com
   25 #
   26 
   27 """ policy evaluation """
   28 
   29 from datetime import datetime
   30 
   31 from netaddr import IPAddress
   32 from netaddr import IPNetwork
   33 
   34 from linotp.lib.policy.filter import UserDomainCompare
   35 from linotp.lib.policy.filter import AttributeCompare
   36 from linotp.lib.user import User
   37 from linotp.lib.realm import getRealms
   38 
   39 
   40 class PolicyEvaluator(object):
   41     """
   42     policy evaluation engine
   43 
   44     the policy evaluation is defined by an access request like:
   45         {'scope': 'admin', 'user': 'Hugo@realm'}
   46     which is checked against all policies. As result the list of all
   47     matching policies is returned.
   48 
   49     for refactoring the current policy evaluation
   50 
   51       getPolicy()
   52 
   53     could be replaced by three simple steps: by starting the policy class
   54     and adding the filters
   55 
   56         pe = PolicyEvaluator(Context.policies)
   57         pe.set_filters(param)
   58 
   59     followed by the evaluation:
   60 
   61         matching_policies = pe.evaluate()
   62 
   63     For post post processing more filters could be added - be aware filters are
   64     named and could be overwritten - and the evaluation could be made on an
   65     policy set:
   66 
   67         pe.set_filters({'client': '192.168.178.1'})
   68         pe.evaluate(previous_policies)
   69 
   70     [
   71      Currently the filter only return a boolean value, but this could be
   72      extendend to be a tuple of (match, exact or wildcard) which will help
   73      to determin the most precise policy
   74     ]
   75 
   76     [
   77      In addition to the categorization exact match/ wildcard match the initial
   78      set of policies for a request should be made. The request specific policy
   79      set will be determined at request start match for the primary access
   80      vector, which should be the:
   81 
   82        user, client, time and in some cases the realm
   83     ]
   84 
   85     """
   86 
   87     def __init__(self, all_policies):
   88         """
   89         policy evaluation constructor
   90 
   91         :param all_policies: the base for the policy evaluation set
   92         """
   93 
   94         self.all_policies = all_policies
   95         self.filters = []
   96 
   97     def has_policy(self, param):
   98         """
   99         check if a policy for example 'scope:admin' exists
  100 
  101         :param: dict with filter conditions
  102         :return: list of matching policies
  103         """
  104 
  105         try:
  106 
  107             # preserve the old filters
  108             sec_filters = self.filters
  109 
  110             self.set_filters(param)
  111             policies = self.evaluate(multiple=True)
  112 
  113         finally:
  114             # and restore the preserved ones
  115             self.filters = sec_filters
  116 
  117         return policies
  118 
  119     def evaluate(self, policy_set=None, multiple=True):
  120         """
  121         evaluate - compare all policies against the access request
  122 
  123         implementation detail:
  124         - The evaluate iterates over all given policies.
  125         - During the iteration all filter comparisons are made against
  126           the one policy. This allows an early exit, thus if one filter does
  127           not match, all further comparison of the given policy could be
  128           skipped.
  129         - during the filter definition the comparison function is defined, thus
  130           all filter evaluation steps could be treated equal by just calling
  131           the comparison function with the actual value.
  132 
  133         :param policy_set: optional, base policies against which all filter
  134                            are evaluated
  135         :param multiple: define if the policies should be post processed to
  136                          return the best matching ones. Default is to do no
  137                          post proessing
  138         :return: the set of matching policies
  139         """
  140 
  141         matching_policies = {}
  142 
  143         #
  144         # provide information about the policy evaluation - for debugging :)
  145 
  146         matching_details = {}
  147 
  148         all_policies = self.all_policies
  149 
  150         if policy_set:
  151             all_policies = policy_set
  152 
  153         if not self.filters:
  154             return all_policies
  155 
  156         for p_name, p_dict in all_policies.items():
  157 
  158             #
  159             # special case: for filtering of policies by name:
  160             # we add the name of the policy to the policy description
  161             # so we can use the same machine for the name compare
  162 
  163             if 'name' not in p_dict:
  164                 p_dict['name'] = p_name
  165 
  166             #
  167             # evaluate each filter against the policy. if one filter fails
  168             # we can skip the evaluation the given policy
  169 
  170             for (f_key, f_value, f_compare) in self.filters:
  171 
  172                 policy_condition = p_dict.get(f_key)
  173                 matching = f_compare(policy_condition, f_value)
  174 
  175                 if not matching:
  176                     break
  177 
  178             if matching:
  179                 matching_policies[p_name] = p_dict
  180 
  181         # if we have multiple policies and post processing should be made:
  182         if not multiple and len(matching_policies):
  183 
  184             #
  185             # so we do some post selection but dont care for the result, as
  186             # this is done in the upper level
  187 
  188             matching_policies = self._most_precise_policy(matching_policies)
  189             return matching_policies
  190 
  191         return matching_policies
  192 
  193     def _most_precise_policy(self, matching_policies):
  194 
  195         no_wild_card_match = {}
  196 
  197         for key in ['user', 'client', 'realm']:
  198             entry = []
  199             for name, policy in matching_policies.items():
  200                 conditions = [x.strip() for x in policy[key].split(',')]
  201                 if '*' not in conditions:
  202                     entry.append(name)
  203 
  204             if len(entry) > 0:
  205                 no_wild_card_match[key] = entry
  206 
  207         res = None
  208 
  209         if ('realm' in no_wild_card_match and
  210            len(no_wild_card_match['realm']) == 1):
  211 
  212             res = no_wild_card_match['realm']
  213 
  214         elif ('client' in no_wild_card_match and
  215               len(no_wild_card_match['client']) == 1):
  216 
  217             res = no_wild_card_match['client']
  218 
  219         elif ('user' in no_wild_card_match and
  220               len(no_wild_card_match['user']) == 1):
  221 
  222             res = no_wild_card_match['user']
  223 
  224         if res:
  225             policy_name = res[0]
  226             return {policy_name: matching_policies[policy_name]}
  227 
  228         return matching_policies
  229 
  230     def set_filters(self, params):
  231         """
  232         set up a set of filters from a dictionary
  233 
  234         interface to ease the migration
  235         """
  236 
  237         for key, value in params.items():
  238             if key == 'active':
  239                 self.filter_for_active(state=value)
  240             elif key == 'scope':
  241                 self.filter_for_scope(scope=value)
  242             elif key == 'user':
  243                 self.filter_for_user(user=value)
  244             elif key == 'realm':
  245                 self.filter_for_realm(realm=value)
  246             elif key == 'action':
  247                 self.filter_for_action(action=value)
  248             elif key == 'name':
  249                 self.filter_for_name(name=value)
  250             elif key == 'time':
  251                 self.filter_for_time(time=value)
  252             elif key == 'client':
  253                 self.filter_for_client(client=value)
  254 
  255         return self
  256 
  257     def reset_filters(self):
  258         """
  259         remove all filters
  260         """
  261         del self.filters[:]
  262 
  263     def add_filter(self, key, value, value_compare):
  264         """
  265         low level filter interface which adds a tuple of
  266             key, value and comparering_method
  267         like
  268            ('user , 'hugo', user_list_compare)
  269         """
  270         self.filters.append((key, value, value_compare))
  271 
  272     def filter_for_active(self, state=True):
  273         """
  274         usability wrapper for adding state filter for filtering active policies
  275 
  276         :param state: policy state - boolean
  277         :return: - nothing -
  278         """
  279         if state is not None:
  280             self.add_filter('active', state, bool_compare)
  281 
  282     def filter_for_scope(self, scope):
  283         """
  284         usability wrapper for the policy scope
  285 
  286         :param state: policy state - boolean
  287         :return: - nothing -
  288         """
  289         if scope is not None:
  290             self.add_filter('scope', scope, string_compare)
  291 
  292     def filter_for_user(self, user):
  293         """
  294         usability wrapper for adding a user filter
  295 
  296         :param user: the user, either of type User or string
  297         :return: - nothing -
  298         """
  299         if user is not None:
  300             self.add_filter('user', user, user_list_compare)
  301 
  302     def filter_for_action(self, action):
  303         """
  304         usability wrapper for adding a filter for actions
  305 
  306         :param user: the action
  307         :return: - nothing -
  308         """
  309 
  310         if action is not None:
  311             self.add_filter('action', action, value_list_compare)
  312 
  313     def filter_for_name(self, name):
  314         """
  315         usability wrapper for adding a filter for the policy name
  316 
  317         :param name: policy name - string
  318         :return: - nothing -
  319         """
  320         if name is not None:
  321             self.add_filter('name', name, string_compare)
  322 
  323     def filter_for_realm(self, realm):
  324         """
  325         usability wrapper for adding realm value for realm filtering
  326 
  327         :param realm: realm string
  328         :return: - nothing -
  329         """
  330         if realm is not None:
  331             self.add_filter('realm', realm, wildcard_icase_list_compare)
  332 
  333     def filter_for_client(self, client):
  334         """
  335         usability wrapper for adding client value for client filtering
  336 
  337         :param client: client ip as string
  338         :return: - nothing -
  339         """
  340 
  341         if client is not None:
  342             self.add_filter('client', client, ip_list_compare)
  343 
  344     def filter_for_time(self, time=None):
  345         """
  346         usability wrapper for adding time value for time filtering
  347 
  348         :param time: datetime object or None, which referes to now()
  349         :return: - nothing -
  350         """
  351         if time is None:
  352             time = datetime.now()
  353         self.add_filter('time', time, time_list_compare)
  354 
  355 #
  356 # below: the comparing functions
  357 #
  358 # unit tests in tests/unit/policy/test_condition_comparison.py
  359 #
  360 
  361 
  362 def value_list_compare(policy_conditions, action_name):
  363     """
  364     check if given action_name matches the conditions
  365 
  366     :param policy_condition: the condition described in the policy
  367     :param action_name: the name of the action, which could be a key=val
  368     :return: booleans
  369     """
  370 
  371     conditions = [x.strip() for x in policy_conditions.split(',')]
  372 
  373     if '*' in conditions:
  374         return True
  375 
  376     # exact action match
  377     if action_name in conditions:
  378         return True
  379 
  380     # extract action name from action_name=value
  381     for condition in conditions:
  382 
  383         cond_name, _sep, _cond_value = condition.partition('=')
  384         if cond_name.strip() == action_name:
  385             return True
  386 
  387     return False
  388 
  389 def wildcard_list_compare(policy_conditions, value):
  390     """
  391     check if given string value matches the conditions
  392 
  393     :param policy_condition: the condition described in the policy
  394     :param value: the string value
  395     :return: booleans
  396     """
  397 
  398     matched = wildcard_icase_list_compare(policy_conditions,
  399                                           value, ignore_case=False)
  400 
  401     return matched
  402 
  403 
  404 def wildcard_icase_list_compare(policy_conditions, value, ignore_case=True):
  405     """
  406     check if given string value matches the conditions
  407 
  408     :param policy_condition: the condition described in the policy
  409     :param value: the string value
  410     :return: booleans
  411     """
  412 
  413     conditions = [x.strip() for x in policy_conditions.split(',')]
  414 
  415     if '*' in conditions:
  416         return True
  417 
  418     matched = False
  419 
  420     for condition in conditions:
  421 
  422         if not condition:
  423             continue
  424 
  425         its_a_not_condition = False
  426 
  427         if condition[0] in ['-', '!']:
  428             its_a_not_condition = True
  429             condition = condition[1:]
  430 
  431         #
  432         # support for case sensitive comparison
  433 
  434         if ignore_case:
  435             cmp_value = value.lower()
  436             cmp_condition = condition.lower()
  437         else:
  438             cmp_value = value
  439             cmp_condition = condition
  440 
  441         if cmp_value == cmp_condition:
  442             if its_a_not_condition:
  443                 return False
  444             else:
  445                 matched = True
  446 
  447     return matched
  448 
  449 def string_compare(policy_condition, value):
  450     """
  451     check if given string value matches the conditions
  452 
  453     :param policy_condition: the condition described in the policy
  454     :param value: the string value
  455     :return: booleans
  456     """
  457     if policy_condition == value:
  458         return True
  459 
  460     return False
  461 
  462 
  463 def bool_compare(policy_condition, value):
  464     """
  465     check if given value is boolean and matches of policy conditions
  466 
  467     :param policy_condition: the condition described in the policy
  468     :param value: the string representation of a boolean value
  469     :return: booleans
  470     """
  471 
  472     boolean_condition = str(policy_condition).lower() == 'true'
  473 
  474     if boolean_condition == value:
  475         return True
  476 
  477     return False
  478 
  479 
  480 def ip_list_compare(policy_conditions, client):
  481     """
  482     check if client ip matches list of policy conditions
  483 
  484     :param policy_condition: the condition described in the policy
  485     :param client: the to be compared client ip
  486     :return: booleans
  487     """
  488 
  489     conditions = [x.strip() for x in policy_conditions.split(',')]
  490 
  491     if '*' in conditions:
  492         return True
  493 
  494     allowed = False
  495 
  496     for condition in conditions:
  497         identified = False
  498         its_a_not_condition = False
  499 
  500         if not condition:
  501             continue
  502 
  503         if condition[0] in ['-', '!']:
  504             condition = condition[1:]
  505             its_a_not_condition = True
  506 
  507         if condition == '*':
  508             identified = True
  509 
  510         elif IPAddress(client) in IPNetwork(condition):
  511             identified = True
  512 
  513         if identified:
  514             if its_a_not_condition:
  515                 return False
  516             allowed = True
  517 
  518     return allowed
  519 
  520 
  521 def user_list_compare(policy_conditions, login):
  522     """
  523     check if login name matches list of user policy conditions
  524 
  525     :param policy_condition: the condition described in the policy
  526     :param login: the to be compared user - either User obj or string
  527     :return: booleans
  528     """
  529     conditions = [x.strip() for x in policy_conditions.split(',')]
  530 
  531     if isinstance(login, User):
  532         user = login
  533     elif isinstance(login, str) or isinstance(login, unicode):
  534         if '@' in login:
  535             usr, _sep, realm = login.rpartition('@')
  536             user = User(usr, realm)
  537         else:
  538             user = User(login)
  539     else:
  540         raise Exception("unsupported type of login")
  541 
  542     matched = False
  543 
  544     domain_comp = UserDomainCompare()
  545     attr_comp = AttributeCompare()
  546 
  547     for condition in conditions:
  548 
  549         if not condition:
  550             continue
  551 
  552         its_a_not_condition = False
  553 
  554         # we preserve the kind of match:
  555         # in case of a 'non condition' match, we must return immeaditly
  556         # and return a False to break out of the loop of conditions
  557 
  558         if condition[0] in ['-', '!']:
  559             condition = condition[1:]
  560             its_a_not_condition = True
  561 
  562         if '#' in condition:
  563 
  564             if ((isinstance(login, str) or isinstance(login, unicode)) and
  565                '@' in login):
  566 
  567                 usr, _sep, realm = login.rpartition('@')
  568 
  569                 if realm in getRealms():
  570                     c_user = User(usr, realm)
  571                 else:
  572                     c_user = User(login)
  573 
  574             else:
  575                 c_user = user
  576 
  577             identified = attr_comp.compare(c_user, condition)
  578 
  579         elif '@' in condition:  # domain condition requires a domain compare
  580 
  581             #
  582             # we support fake users, where login is of type string
  583             # and who have an '@' in it - we rely on that real users
  584             # are identified up front and then login will of type User
  585 
  586             if ((isinstance(login, str) or isinstance(login, unicode)) and
  587                '@' in login):
  588                 u_login, _, r_login = login.rpartition('@')
  589                 c_user = User(u_login, r_login)
  590             else:
  591                 c_user = user
  592             identified = domain_comp.compare(c_user, condition)
  593 
  594         elif ':' in condition:  # resolver condition - by user exists check
  595 
  596             #
  597             # special treatment of literal user definition with an @ in login:
  598             # we can split last part and check if it is an existing realm. If
  599             # not we treat the user login as literal only
  600 
  601             if ((isinstance(login, str) or isinstance(login, unicode)) and
  602                '@' in login):
  603 
  604                 usr, _sep, realm = login.rpartition('@')
  605 
  606                 if realm in getRealms():
  607                     c_user = User(usr, realm)
  608                 else:
  609                     c_user = User(login)
  610 
  611             else:
  612                 c_user = user
  613 
  614             identified = domain_comp.exists(c_user, condition)
  615 
  616         else:  # simple user condition with string compare and wild cards
  617 
  618             identified = domain_comp.compare(user, condition)
  619 
  620         if identified:
  621             matched = True
  622 
  623             if its_a_not_condition:  # early exit on a not condition
  624                 return False
  625 
  626     return matched
  627 
  628 
  629 def _compare_cron_value(value, target):
  630     """
  631      cron value comparison - compare the target, if it matches the cron value
  632 
  633     a cron values could be like
  634 
  635         */15 */6 1,15,31 * 1-5 *
  636     or
  637         0 12 * * 1-5 * (0 12 * * Mon-Fri *)
  638 
  639      (c) code copied from pycron
  640 
  641         https://github.com/kipe/pycron
  642 
  643     with MIT Licence
  644 
  645         https://github.com/kipe/pycron/blob/master/LICENSE
  646 
  647     :param value: one cron entry
  648     :param target: the matching value
  649     :return: boolean - if target matches the cron entry
  650     """
  651 
  652     value = value.strip()
  653 
  654     if value == '*':
  655         return True
  656 
  657     values = [x.strip() for x in value.split(',')]
  658 
  659     for value in values:
  660         try:
  661             # First, try a direct comparison
  662             if int(value) == target:
  663                 return True
  664         except ValueError:
  665             pass
  666 
  667         if '/' in value:
  668             val, interval = [x.strip() for x in value.split('/')]
  669 
  670             #
  671             # Not sure if applicable for every situation, but
  672             # just to make sure...
  673 
  674             if val != '*':
  675                 continue
  676 
  677             # If the remainder is zero, this matches
  678 
  679             if target % int(interval) == 0:
  680                 return True
  681 
  682         if '-' in value:
  683             try:
  684                 start, end = [int(x.strip()) for x in value.split('-')]
  685             except ValueError:
  686                 continue
  687             # If target value is in the range, it matches
  688             if target in range(start, end + 1):
  689                 return True
  690 
  691     return False
  692 
  693 
  694 def cron_compare(condition, now):
  695     """
  696     compare a cron condition with a given datetime
  697 
  698     :param condition: a cron condition
  699     :param now: the datetime to compare with
  700 
  701     :return: boolean - is allowed or not
  702     """
  703 
  704     condition_parts = []
  705     parts = condition.split(' ')
  706     for part in parts:
  707         if part.strip():
  708             condition_parts.append(part)
  709 
  710     if len(condition_parts) != 6:
  711         raise Exception("Error in Time Condition format")
  712 
  713     #
  714     # extract the members of the cron condition
  715 
  716     minute = condition_parts[0]
  717     hour = condition_parts[1]
  718     dom = condition_parts[2]
  719     month = condition_parts[3]
  720     dow = condition_parts[4]
  721     year = condition_parts[5]
  722 
  723     weekday = now.isoweekday()
  724 
  725     return (_compare_cron_value(minute, now.minute) and
  726             _compare_cron_value(hour, now.hour) and
  727             _compare_cron_value(dom, now.day) and
  728             _compare_cron_value(month, now.month) and
  729             _compare_cron_value(dow, 0 if weekday == 7 else weekday) and
  730             _compare_cron_value(year, now.year))
  731 
  732 
  733 def time_list_compare(policy_conditions, now):
  734     """
  735     compare a given time with a time description in the policy
  736 
  737     for the time description we use the cron format, which allows to
  738     define time frames like access from Mo-Fr and from 6:00 to 18:00:
  739 
  740     * 6-18 * * 1-5 *
  741 
  742     * * * * * *
  743     | | | | | |
  744     | | | | | +-- Year              (range: 1900-3000)
  745     | | | | +---- Day of the Week   (range: 1-7, 1 standing for Monday)
  746     | | | +------ Month of the Year (range: 1-12)
  747     | | +-------- Day of the Month  (range: 1-31)
  748     | +---------- Hour              (range: 0-23)
  749     +------------ Minute            (range: 0-59)
  750 
  751     Remark: time conditions are separated by ';' as the ',' is part of
  752             the cron expression
  753 
  754     """
  755     conditions = [x.strip() for x in policy_conditions.split(';')]
  756 
  757     matched = False
  758 
  759     if now is None:
  760         now = datetime.now()
  761 
  762     for condition in conditions:
  763 
  764         #
  765         # skip for empty conditions
  766 
  767         if not condition:
  768             continue
  769 
  770         # if in the conditions one is with wildcard we grant access
  771 
  772         if condition == '*':
  773             return True
  774 
  775         #
  776         # support excluding conditions which start with [-,!]
  777 
  778         its_a_not_condition = False
  779 
  780         if condition[0] in ['-', '!']:
  781             its_a_not_condition = True
  782             condition = condition[1:]
  783 
  784         #
  785         # compare the cron condition
  786 
  787         if cron_compare(condition, now):
  788             if its_a_not_condition:
  789                 return False
  790             else:
  791                 matched = True
  792 
  793     return matched
  794 
  795 # eof