一、什么是policy
policy策略,是OpenStack用来约束不同等级用户的操作权限,用于自定义权限管理的一种机制。
二、为什么要用policy
1、基于keystone理解policy
Keystone中有User(用户)、Project(租户)、role(角色)三个概念,使用基本流程为:
1)创建租户 2)创建用户 3)创建角色 4)给用户和租户赋予角色
赋予角色之后,角色还只是一个空的概念定义,没有实际意义。真正限制用户角色执行具体action操作的,是policy.json文件。
Keystone使用基于角色的访问控制(RBAC)保护其API。用户根据他们具体的角色,才可以访问不同的API。
OpenStack各组件下都有对应的policy.json,每一个模块通过各自的policy.json文件对Role进行访问控制,为其定义API访问策略,来确定哪个用户可以以哪种方式访问哪些对象。
当对OpenStack服务进行API调用时,服务的策略引擎就会使用适当的策略定义来确定是否可以接受该调用,策略规则确定在什么情况下允许API调用。
因此,当定制需求需要新增api接口时,需要在组件对应的policy.json中,添加对应API接口的权限控制。
policy.json文件会立即生效,不需要重新启动服务。云管理员可以修改或更新这些策略,以控制对各种资源的访问。
用户角色和权限的关系,参考RBAC相关描述:http://www.woshipm.com/pd/1150093.html
2、举例理解policy
某公司通过RBAC做权限管理,场景如下
用户:员工张三、管理者李四、老板王五、人力赵六
租户:研发部、销售部、财务部
角色:程序员、技术总监、老板、人力
action:增、删、改、查部门信息; 增、删、改、查员工信息; 查看公告信息; 访问门禁系统等等
policy.json中可以定义以下内容:
普通员工只能查询自己信息,老板可以对所有员工信息增删改查。
老板可以新增一个部门、删除一个部门,员工就不可以。
所有人都可以访问门禁系统。
员工只能使用部门内部的资源,类似租户资源隔离。
policy.json文件可以理解为一份权利清单,里面描述了不同的角色分别可以做哪些事情。例如:
{
"is_boss": "role:boss",
"is_leader": "role:leader",
"is_employee": "role:employee",
"is_hr": "role:hr",
"company_all_members": "rule:is_boss or rule:is_leader or rule:is_hr or rule:is_employee",
"boss_or_leader_or_hr": "rule:is_boss or rule:is_leader or rule:is_hr",
"employee:list": "rule:boss or rule:is_hr or leader_id:%(leader_id)s",
"employee:show": "rule:boss_or_leader_or_hr or rule: employee_id:%(employee_id)s",
"employee:create": "rule:is_hr",
"employee:update": "rule:is_hr or rule: employee_id:%(employee_id)s",
"employee:delete": "rule:is_hr",
"department:list": "rule:company_all_members",
"department:show": "rule:company_all_members",
"department:create": "rule:is_boss",
"department:update": "rule:is_leader",
"department:delete": "rule:is_boss",
"access_control_card: get_token": "company_all_members"
"company_notice:show": ""
"no_released_message:show": "!"
}
三、怎么用policy约束权限
policy.json基于JSON文本格式,使用白名单机制进行过滤,每个策略由单行语句定义。
1、policy.json位置
位于每个OpenStack组件的配置文件中,/etc/$component_name/policy.json
例如:# ls /etc/nova/policy.json
2、policy.json文件整体格式
policy.json文件由target:rule或alias:definition形式的策略和别名组成,并用逗号分隔并用花括号括起来:
{
"alias 1" : "definition 1",
"alias 2" : "definition 2",
...
"target 1" : "rule 1",
"target 2" : "rule 2",
....
}
3、target格式
target即指定的API,可以写为"service:API"或简称为"API". 例如,"compute:create"或"add_image"
4、rule格式
rule决定是否允许API调用。
rule规则可以是:
1)始终允许该操作,可以写为""(空字符串),[]或"@",一般为空字符串即可
2)始终不允许该操作,写成"!"
3)两个值的比较,例如检查project_id是否匹配,若匹配才可以调用。
(a) 常量:字符串,数字,true,false
(b) API属性,可以是project_id,user_id或domain_id
(c) 目标对象属性,即数据库中对象描述中的字段. 例如,对于"compute:start" API,对象是要启动的实例. 启动实例的策略可以使用%(project_id)s属性,即拥有实例的项目。尾随s表示这是一个字符串。
(d) is_admin标志位,表示管理特权是通过admin令牌机制( keystone命令的--os-token选项)授予的. 管理员令牌允许在存在管理员角色之前初始化身份数据库.
4)基于简单规则的布尔表达式,例如,使用not限制指定限制范围
5)特殊检查
(a) role:<role name> 指定角色来定义规则,例如:"deny_stack_user": "not role:heat_stack_user",deny_stack_user这个rule规则就表示,所有不是heat_stack_user角色的用户
(b) rule:<rule name> 定义rule规则别名, 例如:"stacks:create": "rule:deny_stack_user",其中deny_stack_user即为上一步定义的别名
(c) http:<target URL> 将检查委托给远程服务器. 服务器返回True时,将授权该API
5、alias别名
别名构造是为了方便起见. 别名是复杂或难以理解的规则的简称. 它的定义与策略相同
格式如下:
alias name : alias definition
定义别名后,请使用rule关键字在策略规则中使用它.
6、常用规则举例
1)rule为"",表示允许all通过
"compute:get_all" : ""
目标是Compute服务的"列出所有实例" API "compute:get_all" . 规则是一个空字符串,表示"始终". 此策略允许任何人列出实例.
2)rule为"!",表示拒绝使用API的权限,用于禁用某个API调用
"compute:shelve": "!"
3)rule为"role:admin",表示此API只能由管理员调用
"identity:create_user" : "role:admin"
此策略确保只有管理员才能在Identity数据库中创建新用户
4)使用运算符not,and,or和括号构建更复杂的规则
"stacks:create": "not role:heat_stack_user"
具有heat_stack_user角色的任何人都不能使用heat创建stack
5)用别名定义角色role,规则rule指向role别名
定义别名:
"deny_stack_user": "not role:heat_stack_user"
policy解析时,会分析到"deny_stack_user"不是API,因此将其解释为别名.
此时,禁止heat_stack_user角色使用heat创建stack,也可指定如下:
"stacks:create": "rule:deny_stack_user"
6)将API属性与对象属性进行比较
"os_compute_api:servers:start" : "project_id:%(project_id)s"
指出只有实例的所有者才能启动它. 冒号之前的project_id字符串是API属性,即API用户的项目ID. 它将与对象的项目ID(在这种情况下为实例)进行比较. 更准确地说,将其与数据库中该对象的project_id字段进行比较. 如果两个值相等,则授予权限.
四、policy代码实现
以nova创建虚机为例说明。
1、nova\compute\api.py
def create()方法,调用 _check_create_policies
_check_create_policies() 调用 check_policy()
2、 nova\policy.py
def enforce()方法,首先 init() 初始化Enforcer类对象_ENFORCER,此时传入use_conf为True,表示初始化操作时,需要读取policy配置文件。
初始化之后,调用对应的enforce方法
3、 nova\openstack\common\policy.py
直接调用Enforcer类的def enforce(), 调用load_rules()
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 force_reload:
self.use_conf = force_reload
if self.use_conf:
if not self.policy_path:
self.policy_path = self._get_policy_path(self.policy_file)
self._load_policy_file(self.policy_path, force_reload,
overwrite=self.overwrite)
for path in CONF.policy_dirs:
try:
path = self._get_policy_path(path)
except cfg.ConfigFilesNotFoundError:
continue
self._walk_through_policy_directory(path,
self._load_policy_file,
force_reload, False)
def _load_policy_file(self, path, force_reload, overwrite=True):
reloaded, data = fileutils.read_cached_file(
path, force_reload=force_reload)
if reloaded or not self.rules or not overwrite:
rules = Rules.load_json(data, self.default_rule)
self.set_rules(rules, overwrite=overwrite, use_conf=True)
LOG.debug("Reloaded policy file: %(path)s",
{'path': path})
def set_rules(self, rules, overwrite=True, use_conf=False):
"""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.
:param use_conf: Whether to reload rules from cache or config file.
"""
if not isinstance(rules, dict):
raise TypeError(_("Rules must be an instance of dict or Rules, "
"got %s instead") % type(rules))
self.use_conf = use_conf
if overwrite:
self.rules = Rules(rules, self.default_rule)
else:
self.rules.update(rules)
4、 nova\openstack\common\fileutils.py
read_cached_file() 方法读取policy文件属性,如果修改时间变更,则认为有修改,则重新加载读取policy配置文件。
policy支持动态策略,无须重启服务,主要由此方法实现
def read_cached_file(filename, force_reload=False):
"""Read from a file if it has been modified.
:param force_reload: Whether to reload the file.
:returns: A tuple with a boolean specifying if the data is fresh
or not.
"""
global _FILE_CACHE
if force_reload:
delete_cached_file(filename)
reloaded = False
mtime = os.path.getmtime(filename)
cache_info = _FILE_CACHE.setdefault(filename, {})
if not cache_info or mtime > cache_info.get('mtime', 0):
LOG.debug("Reloading cached file %s" % filename)
with open(filename) as fap:
cache_info['data'] = fap.read()
cache_info['mtime'] = mtime
reloaded = True
return (reloaded, cache_info['data'])
参考调用图: