OpenStack Policy鉴权大解密!

2020-06-05 11:00:59 浏览数 (1)

lottewong

程序员小黄

云上移民,编程后浪

前 言

我们知道,OpenStack的认证 (authentication) 统一在 keystone 组件中完成,而鉴权 (authorization) 则由具体组件自己实现。

本文将以 cinder 为例主要聊一聊鉴权的那些事儿,一起来探索分两步走的 Policy 机制吧~

预备知识

在dive into cinder 之前,我们先来简单回顾一下 keystone 的几个基本概念:

- Identity:表示用户身份,主要包括 user 和 group

- Resource:表示资源集合,主要包括 project 和 domain

- Assignment:表示角色分配,主要包括 role 和 role assignment

- Catalog:表示具体某个资源内提供的服务和入口,主要包括 service 和 endpoint

- Token:表示访问凭据,主要用于认证(authentication)

- Policy:表示权限规则,主要用于鉴权(authorization)

举个栗子解释一下:

某天,小明想住连锁酒店集团(domain)旗下的某家具体酒店(project),酒店提供很多服务(service)比如住宿、餐饮和娱乐等等,每个服务都有可以进去的入口(endpoint)。当他来到酒店入住,需要登记他的相关证件,他可以以个人的身份(user)入住,也可以以所属公司员工的身份(group)入住。在核实身份后,小明拿到了房卡(token),前台小姐姐还为他贴心地办理了白金会员(role),根据酒店条例(policy),他只要出示会员身份就可以享受到更多规定的服务。

我们现在关心的是小明拿到房卡后的流程,对应到 OpenStack 中来说,对具体组件的每个 API 入口,OpenStack 都会根据 Policy 来判定某个 Identity 所属的 Role 是否对某个 Resource(也可能具体到 Catalog) 拥有访问权限,这就实现了 Role-based Access Control (RBAC) 的鉴权。

进入正题

再来看看 cinder,当我们访问 cinder 的 api 时,主要涉及两个过程:

1、首先会触发 @wrap_check_policy 中的 check_policy

2、紧接着 cinder 通过 oslo 来完成 policy.json 的解析和注册

关于 /etc/cinder/policy.json

policy 的本质是一套约定,约定谁可以做什么不可以做什么(操作限制),以及谁可以访问什么不可以访问什么(数据限制)。如果我们能够比较来访者的信息和 policy 的规则,那么我们就在鉴权的过程中。

简单介绍下其中的语法规则:"rule"、 "condition"

- "rule":指出这条规则适用的范围和操作,一般包括 "scope:action"

- "condition":指出这条规则在什么条件下生效,且条件支持嵌套已有的规则

- 如下所示:以 /etc/cinder/policy.json为示例

代码语言:javascript复制
"admin_or_owner":  "is_admin:True or (role:admin and is_admin_project:True) or  project_id:%(project_id)s", // 定义如何才算admin或owner的规则

"volume:delete": "rule:admin_or_owner", // 如果是admin或owner的角色,那么允许删除卷的操作

关于 @wrap_check_policy

@wrap_check_policy包装了紧跟其后的 check_policy 方法,具体组件可以根据自身需要,在该方法中自定义自己如何来检验策略。

代码语言:javascript复制
def wrap_check_policy(func):
    """Check policy corresponding to the wrapped methods prior to execution

    This decorator requires the first 3 args of the wrapped function
    to be (self, context, volume)
    """
    @functools.wraps(func)
    def wrapped(self, context, target_obj, *args, **kwargs):
        # wrap_check_policy 正如其名为了包装 check_policy
        check_policy(context, func.__name__, target_obj)
        return func(self, context, target_obj, *args, **kwargs)
    return wrapped


def check_policy(context, action, target_obj=None):
  # ...
    
    # 传入request的上下文、对哪个资源执行哪个操作以及目标资源,进行检验
    cinder.policy.enforce(context, _action, target)

关于 oslo_policy/policy.py

看到这里,还是有点疑惑:policy.json的policy到底怎么跑到 @wrap_check_policy 里面去了?答案就是,我们还需要一个中间商没有差价,来帮我们完成转换的工作,这时候公共组件库 olso 闪亮登场了。

从 @wrap_check_policy起穿越层层调用,我们就会发现具体组件 cinder 的 enforce方法会调用公共组件 oslo.policy 的 enforce 方法:

代码语言:javascript复制
# cinder的enforce方法
# filepath: cinderpolicy.py
def enforce(context, action, target):
  # ...
    
    init()
  
    # 这里 _ENFORCER 属于 olso
    return _ENFORCER.enforce(action,
                             target,
                             context.to_policy_values(),
                             do_raise=True,
                             exc=exception.PolicyNotAuthorized,
                             action=action)

# olso.policy的enforce方法
# filepath: oslo_policypolicy.py
def enforce(self, rule, target, creds, do_raise=False,
                exc=None, *args, **kwargs):
    # ...
  
    # 最关键的一步:对 policy.json 做读取
  self.load_rules()

    # Allow the rule to be a Check tree
    # ...

    # If it is False, raise the exception if requested
    # ...

    return result

load_rules() 会读入 policy.json 的内容,并将 json解析为dict,其中:

- key:表示范围和操作限定的"scope: action"

- value:代表某种条件的检查类 XxxCheck

代码语言:javascript复制
def load_rules(self, force_reload=False):
    """Loads policy_path's rules.

  Policy file is cached and will be reloaded if modified.

    :param force_reload: Whether to reload rules from config file.
    """
  # ...

      if self.use_conf:
            # ...
            
      if self.policy_path:
                # 解析policy,key为"scope: action",value为Check类
                self._load_policy_file(self.policy_path, force_reload,
                                       overwrite=self.overwrite)
                
       for default in self.registered_rules.values():
           if default.name not in self.rules:
              # 注册policy,key为"scope: action",value为Check类
              self.rules[default.name] = default.check
  
  # ...
    
def _load_policy_file(self, path, force_reload, overwrite=True):
  # 如果未经修改则从缓存中读取;否则重新加载
    reloaded, data = _cache_handler.read_cached_file(
            self._file_cache, path, force_reload=force_reload)
    if reloaded or not self.rules:
            rules = Rules.load(data, self.default_rule)
  
    # ...
    
class Rules(dict):
    """A store for rules. Handles the default_rule setting directly."""
    
    def load(cls, data, default_rule=None):
        """Allow loading of YAML/JSON rule data.

        .. versionadded:: 1.5.0

        """
        parsed_file = parse_file_contents(data)

        # Parse the rules
        rules = {k: _parser.parse_rule(v) for k, v in parsed_file.items()}

        return cls(rules, default_rule)

总 结

通过对 cinder 和 oslo.policy 源码的阅读,我们不难发现,关于鉴权主要做了两件事情(a.k.a 分两步走):

Step1: 在 cinder/etc/cinder/policy.json中按语法规则来填写策略

Step2: 对cinder/xxx/api.py中每个需要暴露的接口加上装饰器 @wrap_check_policy 来检验策略

以上内容是对 cinder 鉴权过程的一些见解,对于 OpenStack 而言其它组件的鉴权设计都是大同小异的(keystone可能更复杂一些),如果感兴趣还可以继续深入 parse_rule 部分研究一下~

0 人点赞