安全、测试和迁移¶
本部分将涵盖各种主题:
配置安全性
正在迁移现有实例
编写一些单元测试
这里是 read
我想要的安全模型:
文件夹、文件、图像和注释应具有以下可见性之一:
public
,每个人都能看到authenticated
,只有经过身份验证的用户才能看到它restricted
,只有经过身份验证的用户的子集才能看到它
经理(例如我)可以看到一切
只有经过身份验证的用户才能看到人
每个人都可以看到分类器实体,比如tag
也:
除非明确指定,否则图像的可见性应与
它的父文件夹 注释的可见性应与被注释实体的可见性相同。 如果没有父实体,则默认可见性为 authenticated
.
关于写安全性,这要容易得多:
匿名不能写任何东西
经过身份验证的用户只能添加注释
经理将添加剩余的内容
现在,让我们实现它!
在CubicWeb中进行适当的安全保护 在架构级别 ,所以您不必在视图中费心:用户只会自动看到他们可以看到的内容。
步骤1:在架构中配置安全性¶
在模式中,您可以根据以下条件授予访问权限:
组
对于某些RQL表达式:如果表达式返回某些结果,则用户可以访问
要实现前面定义的读取安全性,组是不够的,我们需要一些RQL表达式。这是一个想法:
添加一个 visibility 属性对 Folder , File 和 Comment ,可能是上面解释的值之一
添加一个 may_be_read_by 关系来自 Folder , File 和 Comment 到 users ,它将定义谁可以查看实体
将在挂钩中进行安全传播
注解
是什么造成的 visibility 属性而不是关系是其对象是基元类型,这里 String .
其他内置原语包括string、int、bigint、float、decimal、boolean、date、date time、time、interval、byte和password,有关详细信息,请阅读 实体类型
所以首先要做的是修改我的多维数据集 schema.py
定义这些关系:
from yams.constraints import StaticVocabularyConstraint
class visibility(RelationDefinition):
subject = ('Folder', 'File', 'Comment')
object = 'String'
constraints = [StaticVocabularyConstraint(('public', 'authenticated',
'restricted', 'parent'))]
default = 'parent'
cardinality = '11' # required
class may_be_read_by(RelationDefinition):
__permissions__ = {
'read': ('managers', 'users'),
'add': ('managers',),
'delete': ('managers',),
}
subject = ('Folder', 'File', 'Comment',)
object = 'CWUser'
我们可以注意到以下几点:
我们添加了一个新的 visibility 属性到 Folder , File , Image 和 Comment 使用A RelationDefinition
cardinality = '11' 表示此属性是必需的。这通常隐藏在 required 给出给 String 构造器,但是我们可以在这里依赖它(对于StaticVocabularyConstraint也是一样的,它通常被 vocabulary 参数)
这个 parent 可能的值将用于可见性传播
考虑确保 may_be_read_by 权限,否则任何用户都可以在默认情况下添加/删除它,这在一定程度上破坏了我们的安全模型…
现在,我们应该能够根据这些新的属性和关系在模式中定义安全规则。这是要添加的代码 schema.py
:
from cubicweb.schema import ERQLExpression
VISIBILITY_PERMISSIONS = {
'read': ('managers',
ERQLExpression('X visibility "public"'),
ERQLExpression('X may_be_read_by U')),
'add': ('managers',),
'update': ('managers', 'owners',),
'delete': ('managers', 'owners'),
}
AUTH_ONLY_PERMISSIONS = {
'read': ('managers', 'users'),
'add': ('managers',),
'update': ('managers', 'owners',),
'delete': ('managers', 'owners'),
}
CLASSIFIERS_PERMISSIONS = {
'read': ('managers', 'users', 'guests'),
'add': ('managers',),
'update': ('managers', 'owners',),
'delete': ('managers', 'owners'),
}
from cubicweb_folder.schema import Folder
from cubicweb_file.schema import File
from cubicweb_comment.schema import Comment
from cubicweb_person.schema import Person
from cubicweb_tag.schema import Tag
Folder.__permissions__ = VISIBILITY_PERMISSIONS
File.__permissions__ = VISIBILITY_PERMISSIONS
Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
Comment.__permissions__['add'] = ('managers', 'users',)
Person.__permissions__ = AUTH_ONLY_PERMISSIONS
Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
重要的是:
VISIBILITY_PERMISSIONS 提供对管理器组的读取权限,如果 visibility 属性的值为“public”,或者如果用户(由表达式中的“u”变量设计)通过 may_be_read_by 许可
我们通过导入和修改实体类型来修改它们的权限。 __permissions__ 属性
注意 .copy() :我们只想修改的“添加”权限 Comment ,不是所有实体类型都使用 VISIBILITY_PERMISSIONS 你说什么?
安全模型的其余部分是使用常规组完成的:
users 是所有已验证用户将属于的组
guests 是匿名用户组
步骤2:钩子中的安全传播¶
为了满足前面定义的要求,我们必须实施:
此外,除非明确指定,否则图像的可见性应与其父文件夹相同,注释的可见性也应与注释的实体相同。
这种 active 规则将使用CubicWeb的挂钩系统完成。钩子在数据库事件上触发,例如添加新的实体或关系。
这项要求的棘手之处在于 除非明确规定 特别是因为当添加实体时,我们还不知道它的“父”实体(例如文件的文件夹,由注释注释的文件)。为了处理这些事情,CubicWeb提供了 Operation 允许在提交时安排要做的事情。
在我们的案例中,我们将:
在创建实体时,计划将设置默认可见性的操作
当A parent 添加关系,传播父级的可见性,除非子级已经具有可见性集
这是多维数据集中的代码 hooks.py
:
from cubicweb.predicates import is_instance
from cubicweb.server import hook
class SetVisibilityOp(hook.DataOperationMixIn, hook.Operation):
def precommit_event(self):
for eid in self.get_data():
entity = self.cnx.entity_from_eid(eid)
if entity.visibility == 'parent':
entity.cw_set(visibility=u'authenticated')
class SetVisibilityHook(hook.Hook):
__regid__ = 'sytweb.setvisibility'
__select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Comment')
events = ('after_add_entity',)
def __call__(self):
SetVisibilityOp.get_instance(self._cw).add_data(self.entity.eid)
class SetParentVisibilityHook(hook.Hook):
__regid__ = 'sytweb.setparentvisibility'
__select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
events = ('after_add_relation',)
def __call__(self):
parent = self._cw.entity_from_eid(self.eidto)
child = self._cw.entity_from_eid(self.eidfrom)
if child.visibility == 'parent':
child.cw_set(visibility=parent.visibility)
注意事项:
钩子是应用程序对象,因此具有应与钩子应用的实体或关系类型匹配的选择器。为了匹配关系类型,我们使用钩子特定的 match_rtype 选择器。
使用 DataOperationMixIn :而不是为每个添加的实体添加操作, DataOperationMixIn 允许创建单个实体,并将要处理的实体的EID存储在事务数据中。这是一个很好的实践,避免了在同一个事务中创建大量实体时操作成本过大。
这个 precommit_event 该操作的方法将在事务的提交时间调用。
在一个钩子里, self._cw 是存储库会话,而不是通常在视图中的Web请求
根据hook的事件,您可以访问hook实例上的不同属性。在这里:
self.entity 是“after-add-u-entity”事件上新添加的实体吗?
self.eidfrom / self.eidto 是“添加关系”事件后主题/对象实体的EID(您也可以使用 self.rtype )
这个 parent 可见性值用于告诉“使用父安全性传播”,因为我们希望该属性是必需的,所以不能使用 None 值,否则我们会在有机会传播之前得到一个错误…
现在,我们还想传播 may_be_read_by 关系。幸运的是,CubicWeb为这些东西提供了一些基本的钩子类,因此我们只需要将以下代码添加到 hooks.py
:
# relations where the "parent" entity is the subject
S_RELS = set()
# relations where the "parent" entity is the object
O_RELS = set(('filed_under', 'comments',))
class AddEntitySecurityPropagationHook(hook.PropagateRelationHook):
"""propagate permissions when new entity are added"""
__regid__ = 'sytweb.addentity_security_propagation'
__select__ = (hook.PropagateRelationHook.__select__
& hook.match_rtype_sets(S_RELS, O_RELS))
main_rtype = 'may_be_read_by'
subject_relations = S_RELS
object_relations = O_RELS
class AddPermissionSecurityPropagationHook(hook.PropagateRelationAddHook):
"""propagate permissions when new entity are added"""
__regid__ = 'sytweb.addperm_security_propagation'
__select__ = (hook.PropagateRelationAddHook.__select__
& hook.match_rtype('may_be_read_by',))
subject_relations = S_RELS
object_relations = O_RELS
class DelPermissionSecurityPropagationHook(hook.PropagateRelationDelHook):
__regid__ = 'sytweb.delperm_security_propagation'
__select__ = (hook.PropagateRelationDelHook.__select__
& hook.match_rtype('may_be_read_by',))
subject_relations = S_RELS
object_relations = O_RELS
这个 AddEntitySecurityPropagationHook 将在以下时间传播关系 filed_under 或 comments 添加关系
这个 S_RELS 和 O_RELS 设置以及 match_rtype_sets 这里使用选择器,这样如果我的多维数据集被另一个使用,它就能够通过简单地向其中一个集添加关系来配置安全传播。
其他两个将父实体上的权限更改传播到子实体
步骤3:测试我们的安全性¶
安全问题很棘手。为它编写一些测试是一个很好的主意。您甚至应该首先编写它们,正如测试驱动开发推荐的那样!
下面是一个小测试用例,它将检查我们的安全模型的基础,在 test/test_sytweb.py
:
from cubicweb.devtools import testlib
from cubicweb import Binary
class SecurityTC(testlib.CubicWebTC):
def test_visibility_propagation(self):
with self.admin_access.repo_cnx() as cnx:
# create a user for later security checks
toto = self.create_user(cnx, 'toto')
cnx.commit()
# init some data using the default manager connection
folder = cnx.create_entity('Folder',
name=u'restricted',
visibility=u'restricted')
photo1 = cnx.create_entity('File',
data_name=u'photo1.jpg',
data=Binary(b'xxx'),
filed_under=folder)
cnx.commit()
# visibility propagation
self.assertEquals(photo1.visibility, 'restricted')
# unless explicitly specified
photo2 = cnx.create_entity('File',
data_name=u'photo2.jpg',
data=Binary(b'xxx'),
visibility=u'public',
filed_under=folder)
cnx.commit()
self.assertEquals(photo2.visibility, 'public')
with self.new_access('toto').repo_cnx() as cnx:
# test security
self.assertEqual(1, len(cnx.execute('File X'))) # only the public one
self.assertEqual(0, len(cnx.execute('Folder X'))) # restricted...
with self.admin_access.repo_cnx() as cnx:
# may_be_read_by propagation
folder = cnx.entity_from_eid(folder.eid)
folder.cw_set(may_be_read_by=toto)
cnx.commit()
with self.new_access('toto').repo_cnx() as cnx:
photo1 = cnx.entity_from_eid(photo1.eid)
self.failUnless(photo1.may_be_read_by)
# test security with permissions
self.assertEquals(2, len(cnx.execute('File X'))) # now toto has access to photo2
self.assertEquals(1, len(cnx.execute('Folder X'))) # and to restricted folder
if __name__ == '__main__':
from unittest import main
main()
它并不完整,但显示了您在测试中要做的大多数事情:添加一些内容、创建用户以及像测试中那样连接用户等等。
要运行它,请键入:
$ python3 test/test_sytweb.py
======================================================================
-> creating tables [====================]
-> inserting default user and default groups.
-> storing the schema in the database [====================]
-> database for instance data initialized.
.
----------------------------------------------------------------------
Ran 1 test in 22.547s
OK
第一次执行需要时间,因为它为测试实例创建了一个sqlite数据库。第二个会更快:
$ python3 test/test_sytweb.py
======================================================================
.
----------------------------------------------------------------------
Ran 1 test in 2.662s
OK
如果您在模式中做了一些更改,则必须强制重新生成该数据库。您可以在运行测试之前删除tmpdb文件:::
$ rm data/database/tmpdb*
步骤4:编写迁移脚本并迁移实例¶
在进行这些更改之前,我创建了一个实例,向它提供了一些数据,所以我不想创建新的实例,而是要迁移现有的实例。让我们看看怎么做。
迁移命令应该放在多维数据集中 migration
目录,在名为 <X.Y.Z>_Any.py
('any'主要出于历史原因而存在,'x.y.z>是我们将要发布的多维数据集的版本号。)
在这里,我将创建一个 migration/0.2.0_Any.py
包含以下说明的文件:
add_relation_type('may_be_read_by')
add_relation_type('visibility')
sync_schema_props_perms()
然后我更新多维数据集中的版本号 __pkginfo__.py
至0.2.0。就这样!这些说明将:
通过添加我们的两个新关系来更新实例的模式,并相应地更新基础数据库表(前两条指令)
更新架构的权限定义(最后一条指令)
要迁移我的实例,我只需键入:
cubicweb-ctl upgrade sytweb_instance
然后会有人问您一些问题来逐步进行迁移。当它询问是否应该备份数据库时,您应该说“是”,这样,如果出现任何问题,您可以返回到初始状态…