HAVANA KEYSTONE中policy.json的解析过程

之前写过几篇关于keystone中api的实现的流程分析,在里边可以看到有对policy.json分析的代码。这篇文章小秦我会仔细分析一下这段代码,毕竟官方的文档里对这个json的语法格式写的也不是很清楚。

1.代码定位
具体的代码是这里:
[keystone/policy/backends/rules.py]

def enforce(credentials, action, target, do_raise=True):
    """Verifies that the action is valid on the target in this context.

       :param credentials: user credentials
       :param action: string representing the action to be checked, which
                      should be colon separated for clarity.
       :param target: dictionary representing the object of the action
                      for object creation this should be a dictionary
                      representing the location of the object e.g.
                      {'project_id': object.project_id}
       :raises: `exception.Forbidden` if verification fails.

       Actions should be colon separated for clarity. For example:

        * identity:list_users

    """
    init()

    # Add the exception arguments if asked to do a raise
    extra = {}
    if do_raise:
        extra.update(exc=exception.ForbiddenAction, action=action,
                     do_raise=do_raise)

    return _ENFORCER.enforce(action, target, credentials, **extra)

根据注释,这里主要就是提供用户的认证信息和要做的操作已经操作的target,然后返回或抛出是否授权的决定。另外根据这篇文章,我们可以知道每次调用一个API去做操作的时候,都会的调用这个init方法,因此小秦我在这里猜测一下,policy.json的修改可以立马生效。

2.代码分析
看下这个init方法:
[keystone/policy/backends/rules.py]

def init():
    global _POLICY_PATH
    global _POLICY_CACHE
    global _ENFORCER
    if not _POLICY_PATH:
        _POLICY_PATH = CONF.policy_file
        if not os.path.exists(_POLICY_PATH):
            _POLICY_PATH = CONF.find_file(_POLICY_PATH)
    if not _ENFORCER:
        _ENFORCER = common_policy.Enforcer(policy_file=_POLICY_PATH)
    utils.read_cached_file(_POLICY_PATH,
                           _POLICY_CACHE,
                           reload_func=_set_rules)

这里的CONF.policy_file就是:

policy_file = policy.json

common_policy组件是放在openstack这个文件夹下的,所以应该是openstack所有组件都公用的。对应的初始化代码是:
[keystone/openstack/common/policy.py]

class Enforcer(object):
    """Responsible for loading and enforcing rules.

    :param policy_file: Custom policy file to use, if none is
                        specified, `CONF.policy_file` will be
                        used.
    :param rules: Default dictionary / Rules to use. It will be
                  considered just in the first instantiation. If
                  `load_rules(True)`, `clear()` or `set_rules(True)`
                  is called this will be overwritten.
    :param default_rule: Default rule to use, CONF.default_rule will
                         be used if none is specified.
    """

所以这里我们的_ENFORCER则是:

<keystone.openstack.common.policy.Enforcer object at 0x2e93850>

至于read_cached_file的功能则是看这个文件有没有被修改,如果修改了则重新载入。看来我们的policy.json真的是动态更新的:

def read_cached_file(filename, cache_info, reload_func=None):
    """Read from a file if it has been modified.

    :param cache_info: dictionary to hold opaque cache.
    :param reload_func: optional function to be called with data when
                        file is reloaded due to a modification.

    :returns: data from file.

    """
    mtime = os.path.getmtime(filename)
    if not cache_info or mtime != cache_info.get('mtime'):
        with open(filename) as fap:
            cache_info['data'] = fap.read()
        cache_info['mtime'] = mtime
        if reload_func:
            reload_func(cache_info['data'])
    return cache_info['data']

然后看一个比较关键的东西,这个东西把json文件解析成Rules对象(其实也就是个字典,这个方法是由read_cached_file调用的):

    def set_rules(self, rules, overwrite=True):
        """Create a new Rules object based on the provided dict of rules.

        :param rules: New rules to use. It should be an instance of dict.
        :param overwrite: Whether to overwrite current rules or update them
                          with the new rules.
        """

        if not isinstance(rules, dict):
            raise TypeError(_("Rules must be an instance of dict or Rules, "
                            "got %s instead") % type(rules))

        if overwrite:
            self.rules = Rules(rules, self.default_rule)
        else:
            self.rules.update(rules)

我们打印下这个self.rules看看,其实就是把json文件转换成了字典。:

{
    "identity:delete_project": "rule:admin_required",
    "identity:list_access_tokens": "rule:admin_required",
    "identity:list_trusts": "",
    "identity:update_service": "rule:admin_required",
    "identity:delete_service": "rule:admin_required",
    "identity:list_domains": "rule:admin_required",
    "identity:list_policies": "rule:admin_required",
    "service_role": "role:service",
    "identity:list_groups_for_user": "rule:admin_or_owner",
    "identity:add_user_to_group": "rule:admin_required",
    "identity:list_users": "rule:admin_required",
    "identity:check_user_in_group": "rule:admin_required",
    "identity:create_user": "rule:admin_required",
    "identity:create_grant": "rule:admin_required",
    "identity:list_groups": "rule:admin_required",
    "identity:validate_token": "rule:service_or_admin",
    "identity:create_endpoint": "rule:admin_required",
    "identity:check_token": "rule:admin_required",
    "identity:create_credential": "rule:admin_required",
    "identity:revocation_list": "rule:service_or_admin",
    "identity:update_policy": "rule:admin_required",
    "identity:get_user": "rule:admin_required",
    "identity:check_grant": "rule:admin_required",
    "identity:update_domain": "rule:admin_required",
    "identity:get_trust": "rule:admin_or_owner",
    "identity:get_endpoint": "rule:admin_required",
    "identity:get_access_token": "rule:admin_required",
    "identity:authorize_request_token": "rule:admin_required",
    "identity:list_endpoints_for_project": "rule:admin_required",
    "identity:delete_endpoint": "rule:admin_required",
    "identity:create_project": "rule:admin_required",
    "identity:update_endpoint": "rule:admin_required",
    "identity:create_trust": "user_id:%(trust.trustor_user_id)s",
    "identity:delete_trust": "",
    "identity:list_projects": "rule:admin_required",
    "identity:update_role": "rule:admin_required",
    "identity:delete_policy": "rule:admin_required",
    "identity:delete_role": "rule:admin_required",
    "identity:create_role": "rule:admin_required",
    "identity:list_consumers": "rule:admin_required",
    "identity:get_role_for_trust": "",
    "identity:create_group": "rule:admin_required",
    "owner": "user_id:%(user_id)s",
    "identity:get_project": "rule:admin_required",
    "identity:get_service": "rule:admin_required",
    "identity:get_policy": "rule:admin_required",
    "service_or_admin": "(rule:admin_required or rule:service_role)",
    "identity:get_access_token_role": "rule:admin_required",
    "identity:delete_consumer": "rule:admin_required",
    "identity:remove_user_from_group": "rule:admin_required",
    "identity:list_projects_for_endpoint": "rule:admin_required",
    "identity:list_role_assignments": "rule:admin_required",
    "identity:get_credential": "rule:admin_required",
    "identity:create_service": "rule:admin_required",
    "admin_or_owner": "(rule:admin_required or rule:owner)",
    "identity:create_policy": "rule:admin_required",
    "identity:delete_user": "rule:admin_required",
    "identity:get_group": "rule:admin_required",
    "identity:create_consumer": "rule:admin_required",
    "identity:list_roles_for_trust": "",
    "default": "rule:admin_required",
    "identity:get_consumer": "rule:admin_required",
    "identity:revoke_grant": "rule:admin_required",
    "identity:list_user_projects": "rule:admin_or_owner",
    "identity:list_services": "rule:admin_required",
    "identity:list_endpoints": "rule:admin_required",
    "identity:update_project": "rule:admin_required",
    "identity:delete_group": "rule:admin_required",
    "identity:delete_domain": "rule:admin_required",
    "identity:add_endpoint_to_project": "rule:admin_required",
    "identity:delete_access_token": "rule:admin_required",
    "identity:update_credential": "rule:admin_required",
    "identity:revoke_token": "rule:admin_or_owner",
    "identity:create_domain": "rule:admin_required",
    "identity:list_grants": "rule:admin_required",
    "identity:list_credentials": "rule:admin_required",
    "admin_required": "(role:admin or is_admin:1)",
    "identity:check_role_for_trust": "",
    "identity:get_role": "rule:admin_required",
    "identity:update_group": "rule:admin_required",
    "identity:remove_endpoint_from_project": "rule:admin_required",
    "identity:delete_credential": "rule:admin_required",
    "identity:list_users_in_group": "rule:admin_required",
    "identity:check_endpoint_in_project": "rule:admin_required",
    "identity:list_roles": "rule:admin_required",
    "identity:update_user": "rule:admin_required",
    "identity:validate_token_head": "rule:service_or_admin",
    "identity:update_consumer": "rule:admin_required",
    "identity:list_access_token_roles": "rule:admin_required",
    "identity:get_domain": "rule:admin_required"
}

现在我们有了这个字典,我们看看具体的过滤过程吧:
[keystone/openstack/common/policy.py]

    def enforce(self, rule, target, creds, do_raise=False,
                exc=None, *args, **kwargs):
        """Checks authorization of a rule against the target and credentials.

        :param rule: A string or BaseCheck instance specifying the rule
                    to evaluate.
        :param target: As much information about the object being operated
                    on as possible, as a dictionary.
        :param creds: As much information about the user performing the
                    action as possible, as a dictionary.
        :param do_raise: Whether to raise an exception or not if check
                        fails.
        :param exc: Class of the exception to raise if the check fails.
                    Any remaining arguments passed to check() (both
                    positional and keyword arguments) will be passed to
                    the exception class. If not specified, PolicyNotAuthorized
                    will be used.

        :return: Returns False if the policy does not allow the action and
                exc is not provided; otherwise, returns a value that
                evaluates to True.  Note: for rules using the "case"
                expression, this True value will be the specified string
                from the expression.
        """

        # NOTE(flaper87): Not logging target or creds to avoid
        # potential security issues.
        LOG.debug(_("Rule %s will be now enforced") % rule)

        self.load_rules()

        # Allow the rule to be a Check tree
        if isinstance(rule, BaseCheck):
            result = rule(target, creds, self)
        elif not self.rules:
            # No rules to reference means we're going to fail closed
            result = False
        else:
            try:
                # Evaluate the rule
                result = self.rules[rule](target, creds, self)
            except KeyError:
                LOG.debug(_("Rule [%s] doesn't exist") % rule)
                # If the rule doesn't exist, fail closed
                result = False

        # If it is False, raise the exception if requested
        if do_raise and not result:
            if exc:
                raise exc(*args, **kwargs)

            raise PolicyNotAuthorized(rule)

        return result

我这里的参数是这样的:

rule admin_required
target {}
creds {'tenant_id': u'bd2a1cc49364499da4d4b89c7f557eee', 'user_id': u'eebe9f317ee247b49be425068777105e', u'roles': [u'admin']}

这行代码比较关键:

result = self.rules[rule](target, creds, self)

这里的self.rules[rule]是这样的:

(role:admin or is_admin:1)

这里的几个参数是:

target {}
creds {'tenant_id': u'bd2a1cc49364499da4d4b89c7f557eee', 'user_id': u'eebe9f317ee247b49be425068777105e', u'roles': [u'admin']}
self <keystone.openstack.common.policy.Enforcer object at 0x2249b50>

这里的self.rules[rule]的type是:

<class 'keystone.openstack.common.policy.OrCheck'>

在这个方法之后,我们的result就差不多知道了,然后就能会给caller了。

现在有两个地方不清楚:
1.为什么self.rules[rule]会变成<class ‘keystone.openstack.common.policy.OrCheck’>类型?self.rules不是只是一个policy.json转成的字典吗?
2.OrCheck的实现

看来还是有地方漏了,再排查一遍吧。根据跟踪,我发现在init()这里的时候就已经是把self.rules[rule]的类型设置好了:

    def set_rules(self, rules, overwrite=True):
		print type(rules["admin_required"])
        """Create a new Rules object based on the provided dict of rules.

        :param rules: New rules to use. It should be an instance of dict.
        :param overwrite: Whether to overwrite current rules or update them
                          with the new rules.
        """

        if not isinstance(rules, dict):
            raise TypeError(_("Rules must be an instance of dict or Rules, "
                            "got %s instead") % type(rules))

        if overwrite:
            self.rules = Rules(rules, self.default_rule)
        else:
            self.rules.update(rules)

输出是:

<class 'keystone.openstack.common.policy.OrCheck'>

看来传递给set_rules的时候,rules这个字典已经做了很多操作。而调用我们的set_rules方法的是_set_rules,我这里加了些print,我们来看下:

def _set_rules(data):
    global _ENFORCER
    default_rule = CONF.policy_default_rule
    print "$" * 20
    print type(common_policy.Rules.load_json(data, default_rule)["admin_required"])
    #print common_policy.Rules.load_json(data, default_rule)
    _ENFORCER.set_rules(common_policy.Rules.load_json(
        data, default_rule))

输出是:

$$$$$$$$$$$$$$$$$$$$
<class 'keystone.openstack.common.policy.OrCheck'>

可以看到,common_policy.Rules.load_json确实把我们的文本转成了对应的method。小样的终于被揪出来了。我们来看看:
[keystone/openstack/common/policy.py]

    @classmethod
    def load_json(cls, data, default_rule=None):
        """Allow loading of JSON rule data."""

        # Suck in the JSON data and parse the rules
        rules = dict((k, parse_rule(v)) for k, v in
                     jsonutils.loads(data).items())

        return cls(rules, default_rule)

由于是classmethod,所以cls就是我们的Rule。这里的data就是我们从policy.json中读取到的文本。jsonutils.loads(data).items()应该就是遍历json文件,应该也不会涉及到类型的转换,所以关键的代码应该是parse_rule(v)这里。因此小秦我来看下这个东西:
[keystone/openstack/common/policy.py]

def parse_rule(rule):
    """Parses a policy rule into a tree of Check objects."""

    # If the rule is a string, it's in the policy language
    if isinstance(rule, six.string_types):
        return _parse_text_rule(rule)
    return _parse_list_rule(rule)

[keystone/openstack/common/policy.py]

def _parse_list_rule(rule):
    """Translates the old list-of-lists syntax into a tree of Check objects.

    Provided for backwards compatibility.
    """

    # Empty rule defaults to True
    if not rule:
        return TrueCheck()

    # Outer list is joined by "or"; inner list by "and"
    or_list = []
    for inner_rule in rule:
        # Elide empty inner lists
        if not inner_rule:
            continue

        # Handle bare strings
        if isinstance(inner_rule, six.string_types):
            inner_rule = [inner_rule]

        # Parse the inner rules into Check objects
        and_list = [_parse_check(r) for r in inner_rule]

        # Append the appropriate check to the or_list
        if len(and_list) == 1:
            or_list.append(and_list[0])
        else:
            or_list.append(AndCheck(and_list))

    # If we have only one check, omit the "or"
    if not or_list:
        return FalseCheck()
    elif len(or_list) == 1:
        return or_list[0]

    return OrCheck(or_list)

首先可以看到,这里的格式是老的格式。如果rule是空的,那么就是true。这里的rule是下面这样的格式,就是policy.json右边部分。另外对于每条rule,如果都在最外面的list,那么就是or的关系,如果是在里边的一个list,那么就是and的关系:

rule: [[u'rule:admin_or_owner']]

对每个rule,都会进行这样的解析:

def _parse_check(rule):
    """Parse a single base check rule into an appropriate Check object."""

    # Handle the special checks
    if rule == '!':
        return FalseCheck()
    elif rule == '@':
        return TrueCheck()

    try:
        kind, match = rule.split(':', 1)
    except Exception:
        LOG.exception(_("Failed to understand rule %s") % rule)
        # If the rule is invalid, we'll fail closed
        return FalseCheck()

    # Find what implements the check
    if kind in _checks:
        return _checks[kind](kind, match)
    elif None in _checks:
        return _checks[None](kind, match)
    else:
        LOG.error(_("No handler for matches of kind %s") % kind)
        return FalseCheck()

可以到,如果是!,则不允许任何人操作。如果是@,则允许所有人操作。
然后就是检查_checks,这里的_checks有下面几种类型,通过@register(“role”)装饰器注册:

_checks: {None: <class 'keystone.openstack.common.policy.GenericCheck'>, 'http': <class 'keystone.openstack.common.policy.HttpCheck'>, 'role': <class 'keystone.openstack.common.policy.RoleCheck'>, 'rule': <class 'keystone.openstack.common.policy.RuleCheck'>}

如果是Rule的,这里我们就会得到一个Check的对象。最后会的把这个对象返回给and_list。那么这个and_list的为什么最终会通过一个OrCheck返回呢?猜测语法应该是这样子的:

[[A:xxx,B:xxxx],[N:XXX]]

这里的[A:xxx,B:xxxx]内部的A和B是个inner list,所以是and的关系。但[A:xxx,B:xxxx],[N:XXX]这两个list之间是or的关系。所以最终返回的是个OrCheck。

在得到我们的OrCheck后,就返回了一个Rules。这就是为什么self.rules[rule]会变成<class ‘keystone.openstack.common.policy.OrCheck’>类型的原因了。

另外有个地方也要说明一下,这个一开始也让我感到奇怪,后来想到了就简单了,主要就是check的格式,比如当我把checks从policy.json生成字典后,我看了下生成的字典的内容,发现其value不是想象中的class实例名字,而是下面这种东西:

"identity:get_domain": "rule:admin_required"

后来去看了下Check这个类,发现其重载了__str__方法。所以直接用print的时候看到的是比较好看的格式(虽然让我一开始觉得比较奇怪)。

然后再看我们剩下的那个问题:OrCheck的实现:
我们来看OrCheck的实现,因为这个简单。另外在policy.py中还有NotCheck,AndCheck等,原理都差不多。

    def __call__(self, target, cred, enforcer):
        """Check the policy.

        Requires that at least one rule accept in order to return True.
        """

        for rule in self.rules:
            if rule(target, cred, enforcer):
                return True
        return False

这里的for循环就是or的意思,有任何一个是成功的,那么就返回True。如果都不满足,那么返回False。由于我们上面的例子最终的的调用RuleCheck。这个方法是在result = self.rules[rule](target, creds, self)这行代码中被调用的,这里的self是:keystone.openstack.common.policy.py。

来看一个check吧,在我们这里会的调用到role的这个check:

@register("role")
class RoleCheck(Check):
    def __call__(self, target, creds, enforcer):
        """Check that there is a matching role in the cred dict."""

        return self.match.lower() in [x.lower() for x in creds['roles']]

可以看到,check的返回值应该是个Bool值。这里就是判断creds[‘roles’]中有没有这个rule的role。

3.总结
每次有请求的时候,读取policy.json文件(当然会根据这个文件的最近修改时间判断读不读)。通过policy.json会生成一个rules的字典,字典的key是policy.json的左边的那个字符串,右边的value(Check类型)则是policy.json中那一行右边生成的Check,这个Check(一般是OrCheck)则会的包含policy.json中的一行右边的内容(rule)。然后keystone会遍历这个Check自己含有的Rules集合,比如规则是Rule:XXX的,那么就用RuleCheck去检查。如果是Role:XXX的,那么就用RoleCheck去检查。RoleCheck比较简单,就是看下policy.json中的role和request中的role的关系,匹配就ok了。

另外policy.json是由API负责去调用并校验的。用户请求API后,API通过token会的获取用户的user,role,tenant等信息,根据这些信息去做上面的这些匹配步骤。

4 Responses

  1. MatheMatrix 2014年9月16日 / 上午10:58

    policy 这里有个坑,就是如果 methmod 没有 default 则不会应用 policy,当初调了好久……

    • Gorden 2017年7月17日 / 下午3:35

      你的意思是不是policy.json中没有”default”: “rule:admin_required”,则不应用policy.json的设置

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*