安全、测试和迁移

本部分将涵盖各种主题:

  • 配置安全性

  • 正在迁移现有实例

  • 编写一些单元测试

这里是 read 我想要的安全模型:

  • 文件夹、文件、图像和注释应具有以下可见性之一:

    • public ,每个人都能看到

    • authenticated ,只有经过身份验证的用户才能看到它

    • restricted ,只有经过身份验证的用户的子集才能看到它

  • 经理(例如我)可以看到一切

  • 只有经过身份验证的用户才能看到人

  • 每个人都可以看到分类器实体,比如tag

也:

  • 除非明确指定,否则图像的可见性应与

它的父文件夹 注释的可见性应与被注释实体的可见性相同。 如果没有父实体,则默认可见性为 authenticated .

关于写安全性,这要容易得多:

  • 匿名不能写任何东西

  • 经过身份验证的用户只能添加注释

  • 经理将添加剩余的内容

现在,让我们实现它!

在CubicWeb中进行适当的安全保护 在架构级别 ,所以您不必在视图中费心:用户只会自动看到他们可以看到的内容。

步骤1:在架构中配置安全性

在模式中,您可以根据以下条件授予访问权限:

  • 对于某些RQL表达式:如果表达式返回某些结果,则用户可以访问

要实现前面定义的读取安全性,组是不够的,我们需要一些RQL表达式。这是一个想法:

  • 添加一个 visibility 属性对 FolderFileComment ,可能是上面解释的值之一

  • 添加一个 may_be_read_by 关系来自 FolderFileCommentusers ,它将定义谁可以查看实体

  • 将在挂钩中进行安全传播

注解

是什么造成的 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 属性到 FolderFileImageComment 使用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_undercomments 添加关系

    • 这个 S_RELSO_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

然后会有人问您一些问题来逐步进行迁移。当它询问是否应该备份数据库时,您应该说“是”,这样,如果出现任何问题,您可以返回到初始状态…