除了使用自定义代码手动管理访问之外,Odoo还提供了两种主要的数据驱动机制来管理或限制对数据的访问。
这两种机制都通过组链接到特定的用户:用户属于任意数量的组,安全机制与组相关联,从而将安全机制应用于用户
class res.groups
- name
作为用户可读的组标识(阐明组的角色/目的) - category_id
模块类别用于将组与Odoo应用程序(~一组相关的业务模型)关联,并将它们转换为用户表单中的独家选择 - implied_ids
与此用户一起设置的其他组。这是一种方便的伪继承关系:可以显式地从用户中删除隐含组,而不删除隐含组。 - comment
关于小组的补充说明,例如
Access Rights(访问权限)
授予对给定操作集的整个模型的访问权限。如果没有访问权限与用户(通过其组)在模型上的操作匹配,则该用户没有访问权限。
访问权限是附加的,用户的访问权限是他们在所有组中访问权限的并集,例如,给定一个用户属于授予读和创建访问权限的组a,而属于授予更新访问权限的组B,则该用户将拥有创建、读和更新这三种权限。
class ir.model.access
- name
团体的目的或角色 - model_id
ACL控制其访问权限的模型 - group_id
授予访问权限的res.groups,空group_id表示将ACL授予每个用户(非雇员,例如门户或公共用户)
perm_method属性在设置时授予相应的CRUD访问权限,默认情况下它们都是不设置的
- perm_create
- perm_read
- perm_write
- perm_unlink
Record Rules(记录规则)
记录规则是为了允许某项操作而必须满足的条件。记录规则按照访问权限逐个记录进行评估。
记录规则为default-allow:如果访问权限允许访问,且该用户的操作和型号没有任何规则,则授予访问权限
class ir.rule
-
name
规则的描述 -
model_id
规则适用的模型 -
groups
授予(或不授予)访问权限的res.group。可以指定多个组。如果没有指定组,则该规则是全局的,其处理方式与“组”规则不同(见下文)。 -
global
以组为基础计算,提供对规则的全局状态(或非全局状态)的方便访问 -
domain_force
作为指定为域的谓词,如果域与记录匹配,则允许所选操作,否则禁止所选操作域是一个python表达式,可以使用以下变量
- time
Python的时间模块 - user
当前用户,作为单例记录集 - company_id
当前用户当前选择的公司作为单个公司id(不是记录集) - company_ids
当前用户可以访问的所有公司作为公司id列表(而不是记录集),请参阅安全性规则了解更多详细信息
perm_method具有与ir.model.access完全不同的语义:对于规则,它们指定规则适用于哪个操作。如果未选择某个操作,则不会检查该规则,就像该规则不存在一样
默认选中所有操作
- time
-
perm_create
-
perm_read
-
perm_write
-
perm_unlink
Global rules versus group rules(全局规则与组规则)
全局规则和群体规则在组成和组合方式上有很大的不同
- 全局规则相交,如果应用两个全局规则,则必须同时满足两个规则才能授予访问,这意味着添加全局规则总是进一步限制访问
- 组规则统一,如果应用两个组规则,则可以满足任何一个来授予访问。这意味着添加组规则可以扩展访问,但不能超出全局规则定义的范围
- 全局规则集和组规则集相交,这意味着添加到给定全局规则集的第一个组规则将限制访问
危险
创建多个全局规则是有风险的,因为可能会创建不重叠的规则集,这将删除所有访问权限
Field Access(字段访问 /域访问)
ORM字段可以具有groups属性,提供组列表(作为逗号分隔的外部标识符字符串)。
如果当前用户不在列出的组中,他将无权访问该字段:
- 受限制的字段将自动从请求的视图中删除
- 限制性字段将从fields_get()响应中删除
- 尝试(显式地)读取或写入受限制的字段将导致访问错误
Security Pitfalls(安全隐患)
作为开发人员,了解安全机制并避免导致不安全代码的常见错误非常重要
Unsafe Public Methods(不安全的公共方法)
任何公共方法都可以通过带有所选参数的RPC调用来执行。以_开头的方法不能从动作按钮或外部API调用。
在公共方法上,不能信任执行方法的记录和参数,ACL仅在CRUD操作期间进行验证
# this method is public and its arguments can not be trusted
def action_done(self):
if self.state == "draft" and self.user_has_groups('base.manager'):
self._set_state("done")
# this method is private and can only be called from other python methods
def _set_state(self, new_state):
self.sudo().write({"state": new_state})
将方法设为私有显然是不够的,必须注意正确地使用它。
Bypassing the ORM(绕过ORM)
当ORM可以做同样的事情时,永远不要直接使用数据库游标!通过这样做,您可以绕过所有ORM功能,可能是自动行为,如翻译、字段无效、活动、访问权限等。
而且,您还可能使代码更难阅读,并且可能不那么安全。
# very very wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + ','.join(map(str, ids))+') AND state=%s AND obj_price > 0', ('draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# no injection, but still wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s '\
'AND state=%s AND obj_price > 0', (tuple(ids), 'draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# better
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
SQL injections
在使用手动SQL查询时,必须注意不要引入SQL注入漏洞。当用户输入被错误地过滤或引用不当,允许攻击者在SQL查询中引入不受欢迎的子句(例如绕过过滤器或执行UPDATE或DELETE命令)时,就会出现该漏洞。
确保安全的最佳方法是永远不要使用Python字符串连接(+)或字符串参数插值(%)将变量传递给SQL查询字符串。
第二个几乎同样重要的原因是,决定如何格式化查询参数是数据库抽象层(psycopg2)的工作,而不是您的工作!例如,psycopg2知道,当您传递一个值列表时,它需要将它们格式化为逗号分隔的列表,并将其括在括号中!
# the following is very bad:
# - it's a SQL injection vulnerability
# - it's unreadable
# - it's not your job to format the list of ids
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
'WHERE parent_id IN ('+','.join(map(str, ids))+')')
# better
self.env.cr.execute('SELECT DISTINCT child_id '\
'FROM account_account_consol_rel '\
'WHERE parent_id IN %s',
(tuple(ids),))
这一点非常重要,所以在重构时也要小心,最重要的是不要复制这些模式!
这里有一个令人难忘的例子,可以帮助您记住问题是关于什么的(但不要复制代码)。在继续之前,请务必阅读pyscopg2的在线文档,以了解如何正确使用它:
- 查询参数的问题
- 如何使用psycopg2传递参数
- 高级参数类型
- Psycopg文档
Unescaped field content(未转义字段内容)
在使用JavaScript和XML呈现内容时,可能会尝试使用t-raw来显示富文本内容。应该避免将其作为频繁使用的XSS向量。
要控制从计算到最终集成到浏览器DOM中的数据的完整性是非常困难的。在引入时正确转义的t-raw在下一次错误修复或重构时可能不再安全
QWeb.render('insecure_template', {
info_message: "You have an <strong>important</strong> notification",
})
<div t-name="insecure_template">
<div id="information-bar"><t t-raw="info_message" /></div>
</div>
由于消息内容受到控制,上述代码可能感觉安全,但这是一种不好的做法,一旦该代码在将来发展,可能会导致意外的安全漏洞
// XSS possible with unescaped user provided content !
QWeb.render('insecure_template', {
info_message: "You have an <strong>important</strong> notification on " \
+ "the product <strong>" + product.name + "</strong>",
})
而以不同的方式格式化模板可以防止此类漏洞。
QWeb.render('secure_template', {
message: "You have an important notification on the product:",
subject: product.name
})
<div t-name="secure_template">
<div id="information-bar">
<div class="info"><t t-esc="message" /></div>
<div class="subject"><t t-esc="subject" /></div>
</div>
</div>
.subject {
font-weight: bold;
}
Escaping vs Sanitizing(逃跑vs消毒)
重要的
当您混合数据和代码时,无论数据有多安全,转义总是100%强制性的
转义将文本转换为代码。每次将DATA/TEXT与CODE混合时都必须这样做(例如,生成要在safe_eval中求值的HTML或python代码),因为CODE总是需要对TEXT进行编码。这对安全性至关重要,但它也是一个正确性问题。即使没有安全风险(因为文本是100%保证安全或可信的),它仍然是必需的(例如,避免破坏生成的HTML的布局)。
转义永远不会破坏任何特性,只要开发人员确定哪个变量包含TEXT,哪个包含CO
from odoo.tools import html_escape, html_sanitize
data = "<R&D>" # `data` is some TEXT coming from somewhere
# Escaping turns it into CODE, good!
code = html_escape(data)
code
'<R&D>'
# Now you can mix it with other code...
self.message_post(body="<strong>%s</strong>" % code)
消毒将代码转换为更安全的代码(但不是必要的安全代码)。它不适用于TEXT。只有在CODE不受信任时才需要进行清理,因为它全部或部分来自某些用户提供的数据。如果用户提供的数据是TEXT形式的(例如,由用户填写的表单的内容),并且如果该数据在放入CODE之前被正确转义,那么清理是无用的(但仍然可以完成)。但是,如果用户提供的数据没有转义,那么清理将无法按预期工作
# Sanitizing without escaping is BROKEN: data is corrupted!
>>> html_sanitize(data)
''
# Sanitizing *after* escaping is OK!
>>> html_sanitize(code)
'<p><R&D></p>'
清理可能会破坏特性,这取决于代码是否包含不安全的模式。这就是为什么fields.Html和tools.html_sanitize()有选项来微调样式等的清理级别。必须根据数据的来源和所需的功能仔细考虑这些选项。卫生处理的安全性与卫生处理的破坏是平衡的:卫生处理越安全,破坏东西的可能性越大
>>code = "<p class='text-warning'>Important Information</p>"
# this will remove the style, which may break features
# but is necessary if the source is untrusted
>> html_sanitize(code, strip_classes=True)
'<p>Important Information</p>'
Evaluating content(评价内容)
有些人可能希望通过eval来解析用户提供的内容。无论如何都应该避免使用eval。可以使用更安全的沙盒方法safe_eval,但它仍然为运行它的用户提供了巨大的功能,并且必须仅为受信任的特权用户保留,因为它打破了代码和数据之间的障碍
# very bad
domain = eval(self.filter_domain)
return self.search(domain)
# better but still not recommended
from odoo.tools import safe_eval
domain = safe_eval(self.filter_domain)
return self.search(domain)
# good
from ast import literal_eval
domain = literal_eval(self.filter_domain)
return self.search(domain)
解析内容不需要eval
Accessing object attributes(访问对象属性)
如果需要动态检索或修改记录的值,则可能需要使用getattr和setattr方法
# unsafe retrieval of a field value
def _get_state_value(self, res_id, state_field):
record = self.sudo().browse(res_id)
return getattr(record, state_field, False)
然而,这段代码并不安全,因为它允许访问记录的任何属性,包括私有属性或方法。
已经定义了记录集的__getitem__,并且可以轻松安全地访问动态字段值:
# better retrieval of a field value
def _get_state_value(self, res_id, state_field):
record = self.sudo().browse(res_id)
return record[state_field]
上面的方法显然仍然过于乐观,必须对记录id和字段值进行额外的验证