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 部分研究一下~