前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OpenStack Policy鉴权大解密!

OpenStack Policy鉴权大解密!

作者头像
腾讯云TStack
发布2020-06-05 11:00:59
1.4K0
发布2020-06-05 11:00:59
举报

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: cinder\policy.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_policy\policy.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 部分研究一下~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云TStack 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档