第九章 权限
Odoo的权限管理, 从粗到细可以大概分为四个等级, 对象级 视图级 字段级 记录级, 什么意思呢, 总结起来大概如下面的描述:
- 对象级: 以对象模型为基准的权限划分, 可以理解为数据库中表级的访问权限控制
- 视图级: 以视图为基准的权限划分, 不同用户组的用户可以看到不同的视图
- 字段级: 以字段为基准的权限划分, 对字段的访问权限控制
- 记录级: 以规则为基础的权限划分, 不同的记录适用于不同的规则体系下的用户权限访问控制
在了解这4个层级的权限控制之前, 我们首先要认知Odoo权限控制系统中的两大主角: 用户和组。
用户
用户的概念就是我们平时其他系统中的用户概念, 它代表了可以合法访问系统资源的对象。
用户的类型
在odoo中,用户可以分为三种类型:
- 普通用户: 对应于系统中的用户,通常是企业内部用户
- 门户用户:可以通过注册而来的用户,此类用户不能登录内部系统,只能拥有一个有限权限的门户账号,通常对应于企业的客户或供应商群体。
- 公开用户:即无需注册就可以查看的用户,有限的访问权限,一般对应于游客权限。
用户的管理
用户在odoo中对应的对象模型是res.users,12.0内置了两个用户,id为1的是一个名叫Odoobot的机器人,它会记录用户的操作。id为2的是超级管理员账号。
创建用户的同时,系统会自动创建一个partner对象,并关联到用户上。
未设置密码的用户不允许登录
密码管理
odoo密码加密采用的是passlib库, passlib是一个跨平台的哈希算法库,支持30多种加密算法的实现。
DEFAULT_CRYPT_CONTEXT = passlib.context.CryptContext(
# kdf which can be verified by the context. The default encryption kdf is
# the first of the list
['pbkdf2_sha512', 'plaintext'],
# deprecated algorithms are still verified as usual, but ``needs_update``
# will indicate that the stored hash should be replaced by a more recent
# algorithm. Passlib 1.6 supports an `auto` value which deprecates any
# algorithm but the default, but Ubuntu LTS only provides 1.5 so far.
deprecated=['plaintext'],
)
由odoo的源代码,可以得知,默认采用的加密算法是pbkdf2_sha512,简单说就是通过哈希算法加盐迭代数次得出的结果作为密码的算法。
pdkdf2(Password-Based Key Derivation Function),它的基本原理是通过一个伪随机函数(例如HMAC函数),把明文和一个盐值作为输入参数,然后重复进行运算,并最终产生密钥。如果重复的次数足够大,破解的成本就会变得很高。而盐值的添加也会增加“彩虹表”攻击的难度。
我们看一个经过这种加密算法得出的密码结构:
$pbkdf2-sha512$25000$tJbyHoNwLuX8f2.N8b43Bg$ViWUeJEtLMCm.NSmtMkZzG5M8.e312gzuee93VXqgGL.5izRTBL/Eza/7HMs.51SdpRjnRV2dVvCziQ3uTy07Q
密码通过$符号被分隔成4部分,第一个部分是计算算法名称,第二个是迭代次数,第三部分是盐值,第四部分是hash值。
12.0之前用户加密后的密码存在password_crypt字段中,12.0取消了这个字段,不再以明文存储。
密码的验证
odoo的密码验证使用passlib提供的verify_and_update方法,这个方法会根据当前密码使用的策略是否过时返回新策略的哈希值或空值。
odoo中验证用户密码使用低阶方法_check_credentials即可。
用户对象
用户对象(res.users)常用的几个方法有:
has_group
has_group方法用于确认用户是否属于某个用户组,接受一个参数xml_id。这里的xml_id指的是用户组的xmlid,例如:
self.env.user.has_group('base.group_system')
base.group_system 指代的是系统中的管理权限,意思就是判断当前用户是否是管理员组的成员。
组
用户组在odoo中的对象模型是res.groups,一个组里包含多个用户与用户是一对多的关系。
忘记密码
如果管理员忘记了密码,可以通过修改数据库的加密密码字段进行重置。方便起见,这里提供一个小工具,用于生成odoo要求的加密格式的密码。
#!/usr/bin/python3
# @Time : 2019-08-07
# @Author : Kevin Kong (kfx2007@163.com)
"""密码生成器"""
import passlib.context
DEFAULT_CRYPT_CONTEXT = passlib.context.CryptContext(
# kdf which can be verified by the context. The default encryption kdf is
# the first of the list
['pbkdf2_sha512', 'plaintext'],
# deprecated algorithms are still verified as usual, but ``needs_update``
# will indicate that the stored hash should be replaced by a more recent
# algorithm. Passlib 1.6 supports an `auto` value which deprecates any
# algorithm but the default, but Ubuntu LTS only provides 1.5 so far.
deprecated=['plaintext'],
)
def generate_password(password):
print(DEFAULT_CRYPT_CONTEXT.encrypt(password))
generate_password("admin")v
把这段代码保存为python文件运行即可。
这里提供一个原文为admin的加密示例,忘记密码的同学可以直接在数据库中将password字段修改为该字符串即可登录。
$pbkdf2-sha512$25000$X2vNmTMGAABAyDknJCTE2A$50FI91rr9B0JO8eAHKPqSPw0IPjYLfSSSTWeVFA5eK4yU8v79OwTZ3QxpEFXbGFivqMu6pohc5UltojXgfLlbg
或者直接执行下面的sql:
update res_users set password = '$pbkdf2-sha512$25000$X2vNmTMGAABAyDknJCTE2A$50FI91rr9B0JO8eAHKPqSPw0IPjYLfSSSTWeVFA5eK4yU8v79OwTZ3QxpEFXbGFivqMu6pohc5UltojXgfLlbg' where id = 2;
12.0以前的版本执行:
update res_users set password_crypt = '$pbkdf2-sha512$25000$X2vNmTMGAABAyDknJCTE2A$50FI91rr9B0JO8eAHKPqSPw0IPjYLfSSSTWeVFA5eK4yU8v79OwTZ3QxpEFXbGFivqMu6pohc5UltojXgfLlbg' where id = 1;
创建组
一般通过xml来定义组,跟定义视图类似,不过一般与用户安全相关的文件都放在security文件夹下。下面就是一个典型的组的定义:
<record id="group_sale_order_dates" model="res.groups">
<field name="name">Manage delivery dates from sales orders.</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="comment">This option introduces extra fields in the sales order to easily schedule product deliveries on your own: expected date, commitment date, effective date.</field>
</record>
- category_id 是组的分类,在odoo的设置界面中一般被称作应用程序
- comment: 组的说明
如果希望模块在安装的过程中就将系统预置的用户加入到我们定义的组中,那么可以在xml中像下面这样定义:
<record model="res.users" id="base.user_root">
<field eval="[(4,ref('base.group_partner_manager'))]" name="groups_id"/>
</record>
组的继承
组也是可以被继承的,方法是在组的定义中使用implied_ids字段来指明要继承的组。
<record id="group_shop_user" model="res.groups">
<field name="name">门店店员</field>
<field name="category_id" ref="portal_category_healthelement"/>
<field name="implied_ids" eval="[(4,ref('point_of_sale.group_pos_user'))]"/>
<field name="comment">门店店员,只能查看所属门店的使用权限</field>
</record>
给西西弗书店添加专属应用组
接下来,我们给我们的书店应用创建两个组,店员和书店管理员。在这之前,我们要先创建一个应用分类。所谓应用分类,就是你在用户管理列表中看到分类,本质上是一个ir.module.category对象:
添加分类的方式和添加组类似:
<record model="ir.module.category" id="categ_book_store">
<field name="name">西西弗书店</field>
</record>
然后我们添加两个组,一个店员一个管理员:
<record id="group_shop_user" model="res.groups">
<field name="name">门店店员</field>
<field name="category_id" ref="categ_book_store"/>
<field name="implied_ids" eval="[(4,ref('base.group_user'))]"/>
<field name="comment">门店店员,只能查看所属门店的使用权限</field>
</record>
<record id="group_shop_manager" model="res.groups">
<field name="name">门店店长</field>
<field name="category_id" ref="categ_book_store"/>
<field name="implied_ids" eval="[(4,ref('group_shop_user'))]"/>
<field name="comment">门店店长,只能管理自己的门店</field>
</record>
base.group_user是odoo中内置的内部用户的xml_id,因为我们的模块是内部应用,因此继承该组就不用再为了登录再重新分配权限。
升级西西弗斯书店模块,我们就能看到自己的组出现在了用户的权限列表中:
访问权限
用户和组有了,那么怎么控制用户的访问权限呢?
odoo对于用户的访问权限的设置有多级设计,大致可以分为如下几个层级:
- 菜单级
- 对象级
- 视图级
- 字段级
严格层度由大到小,菜单级仅仅是控制菜单的可见与否,并不能阻止用户通过URL直接访问对象界面。对象级才是真正访问控制的一级,渲染页面时,odoo会验证该页面所有引用到的对象与当前访问用户的权限关系,只有拥有访问权限的用户才能访问到页面。视图级是控制当前页面的某些部分只能由某些有权限的组访问,比如,产品的成本信息等。字段级,精确到某个字段应该由哪些组可以访问。
本节只介绍对象级的访问控制。其余将在第二部分介绍。
对象的访问权限主要控制的内容有读取(read)、创建(create)、修改(write)和删除(delete),也就是你在odoo的组的权限tab页中看到的内容:
即一个组对哪些对象,拥有哪些权限。通常这些由系统管理员进行配置。不过通常编写的模块都会将权限预置,在安装模块的时候就已经写入到数据库中了。预置的方法就是在security文件夹下新建一个csv文件,并配置在manifest文件的data中。csv文件的格式如下:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_book_store_book,book_store.book,model_book_store_book,base.group_user,1,1,1,1
access_book_store_author,book_store.author,model_book_store_author,base.group_user,1,1,1,1
access_book_store_publisher,book_store.publisher,model_book_store_publisher,base.group_user,1,1,1,1
access_book_store_ebook,book_store.ebook,model_book_store_ebook,base.group_user,1,1,1,1
access_book_store_sbook,book_store.sbook,model_book_store_sbook,base.group_user,1,1,1,1
- id: 记录的id,保持唯一
- name: 记录的名称
- modelid: 模型的名称,对应odoo的对象,不同于odoo系统中显示的类似sale.order的对象,csv文件中的对象,需要以model\开头,并将对象中的点号用下划线取代,即sale.order在csv文件中表示为model_sale_order
- group_id: 组的xmlid,通常由模块儿名和组的xmlid组成
- perm_read:读权限 1 表示拥有 0 表示没有
- perm_write:写权限 1 表示拥有 0 表示没有
- perm_create:创建权限 1 表示拥有 0 表示没有
- perm_unlink:删除权限 1 表示拥有 0 表示没有
继承的组会自动继承父组中的访问权限,因此,在父组中拥有的访问权限不必在子组中再次声明。
对于13.0及之前的版本, 瞬时模块(TransientModel)可以不用声明访问权限,默认为公开访问, 但是对于14.0+版本, 都需要预先设置访问权限, 否则系统将会提示你对象不可访问.
规则
访问权限只能限制用户组对该对象的访问权限,并不能限制特定用户对某些记录的访问。举例来说,销售人员A拥有对销售单sale.order的访问权限,他能够看到全公司的销售单。很显然,这是实际应用中不可以接受的情况。通常的规则是,销售人员只能看到自己创建的销售单据。在odoo中,这种需求的解决方案就是规则(ir.rule)。
规则的管理在系统设置-技术设置-安全-记录规则中:
规则的设置主要有一下几点:
- 名称: 规则的名称
- 对象: 规则应用的对象
- 访问权限: 规则应用的操作,读、写、创建和删除
- 域(domain): 规则的主要内容,过虑的条件
- 组:规则生效的用户组,缺省是全局生效。
前面的例子中,销售人员只能看到自己的销售单,应用的规则domain是:
['|',('user_id','=',user.id),('user_id','=',False)]
"|" 代表的是或关系,"&"代表与关系,一般可以省略。user_id是销售单上的销售人员字段,该domain翻译过来的过滤条件即:过滤出销售单中销售人员是我,或者没有销售人员的单据。
我们在使用权限的过程中,可能会碰到由于安全原因提示有关记录不能被访问的提示。通常是由读取当前对象的字段时关联到了其他对象,而其他对象由于记录规则的原因导致当前用户没有权限访问而提示出错。 如果关联字段时计算字段,那么可以使用sudo提权来规避报错,如果是非计算字段,那么可以在视图中的字段上添加权限组,用来规避当前用户没有权限,odoo视图中没有出现的字段是不会被读取到的,因此也就不存在访问错误。
全局记录规则与组之间的记录规则
在记录规则设置界面有这么一段提示,写的比较抽象:
Global rules (non group-specific) are restrictions, and cannot be bypassed. Group-specific rules grant additional permissions, but are constrained within the bounds of global ones. The first group rules restrict further the global rules, but can be relaxed by additional group rules.
Detailed algorithm:
Global rules are combined together with a logical AND operator, and with the result of the following steps
Group-specific rules are combined together with a logical OR operator
If user belongs to several groups, the results from step 2 are combined with logical OR operator
Example: GLOBAL_RULE_1 AND GLOBAL_RULE_2 AND ( (GROUP_A_RULE_1 OR GROUP_A_RULE_2) OR (GROUP_B_RULE_1 OR GROUP_B_RULE_2) )
什么意思呢,我们这里来翻译一下:
在记录规则中有一类全局规则(即没有指定用户组的规则),全局规则不能被跳过,全局规则之前是且的关系。而组规则(设置了用户组的规则)之间则是或的关系。
这里举个简单的例子,假设我们的销售订单模型sale.order,设置了全局规则
[('id','in',company_ids)]
即所有用户都只能看到自己拥有权限的公司的下的订单,此设置属于全局设置,此后添加的每个组规则都会在该限制范围内。
我们给用户组查看自己的订单,设置组规则:
[('user_id','=',user.id)]
给用户组销售管理员,设置了组规则:
[(1,'=',1)]
根据规则,苹果公司销售经理厨子先生拥有了销售管理员权限,他就能看到苹果公司下所有的订单,但是由于全局规则的限制,厨子先生不能看到集团的另外一家公司锤子公司的订单。·
总结
本章介绍了用户和用户组,以及用户组中的访问权限设置和用户规则,读者可以试着自己创建一个组并设置权限,练习一下吧。