跳转到内容

ERP5 手册/魔法安全

来自维基教科书,开放世界开放书籍

玩转安全

[编辑 | 编辑源代码]

不用说,安全在大多数项目中至关重要。设计一个既严格又灵活的权限系统可能是开发人员的噩梦。

幸运的是,ERP5 带有极其强大的安全机制。不幸的是,它不能开箱即用,需要进行自定义。

5A 安全模型

[编辑 | 编辑源代码]

ERP5 的安全模型基于角色。角色是 Zope 的概念,在 Zope 文档(Zope 手册)中进行了说明。一般来说,角色描述了用户的身份(例如,会计,文员,官员),但不描述他们可以做什么。这是由权限定义的(例如,查看权限,修改权限)。对于每个文档,都存在一个角色到权限的映射。拥有会计角色的用户可以被允许访问某些文档,但不能被允许访问其他文档。

ERP5 扩展了 Zope 的概念,引入了 5A 安全模型,它为业务安全提供了简单一致的视图。5A 代表 ERP5 的作者、审计员、分配者、被分配者和关联者的角色。这些角色中的每一个都有一个特定的目的

作者 可以创建新文档。他不需要能够查看文档(他可能能够查看自己的文档,这得益于 Zope 的所有者角色)。

审计员 被允许查看文档。

分配者 可以查看、修改、删除和创建文档,以及将其他用户分配到指定的文档。

被分配者 只能处理模块中的某些文档。例如,他只能访问与特定区域相关的文档。

关联者 可以在某些条件下处理文档(例如,他可以查看组织,前提是它们与他负责的发票文档相关)。

角色在:http://wiki.erp5.org/HowToDesignSecurity 中有描述

如何成为用户?

[编辑 | 编辑源代码]

当用户尝试登录 ERP5 网站时,安全机制会尝试找到一个人(在 person_module 中),该人具有引用属性的值(在人员表单的详细信息选项卡上称为“用户名”)设置为用户的登录名。如果存在这样的人,拥有“内部”角色并且已通过验证(参见关于工作流的章节),并且用户提供的密码与该人的密码匹配,那么允许用户登录。

重要——从修订版 r15915 开始,人员不再需要拥有“内部”角色。相反,人员必须至少拥有一个打开的分配。)

所以,要让某人登录 ERP5 网站,你必须

  • 定义一个人;
  • 将引用属性设置为用户登录名;
  • 设置密码;
  • 将人员的角色设置为内部(在 r15915 之后的修订版中不再需要);
  • 验证一个人;
  • 为该人员打开一个分配(从修订版 r15915 开始)。

安全机制

[编辑 | 编辑源代码]

安全分为两部分——用户端安全和对象端安全。

用户端安全定义了用户成功登录后拥有的安全设置。

对象端安全定义了用户在给定对象上下文中获得角色时应该满足的条件。

在 ERP5 中,几乎所有东西都是对象——模块、首选项、类别等等。每次用户尝试访问对象时,ERP5 会执行以下操作

  • 检查用户安全设置和对象安全设置是否允许在该对象上下文中授予用户角色或多个角色;
  • 查找授予这些角色的权限;
  • 允许或拒绝访问对象。
它是如何工作的?
[编辑 | 编辑源代码]

当用户登录 ERP5 时,会为他计算安全类别。这些安全类别类似于一组键(“键”指的是一个小金属物体,而不是密码学中使用的键!)。回到编程,类别只是一列字符串,例如: ['MA_CS','MA_CS_WA']

每个 ERP5 对象也具有其安全类别。这些类别可以被认为是钥匙孔。每个类别都与角色相关联。例如:[('HR','Assignor'),('SD','Assignee'),('MA_CS_WA','Auditor')]

当用户拥有与给定钥匙孔匹配的钥匙时,他将获得钥匙孔后面的角色。在前面的示例中,用户获得了对象的审计员角色,因为他拥有(除其他外)MA_CS_WA 安全类别,并且对象与其关联了审计员角色。

由于每个对象可以拥有不同的安全类别,因此用户可以为不同的对象获得不同的角色。如果另一个对象具有以下类别:[('HR','Assignor'),('SD','Assignee'),('MA_CS_LD','Auditor')],则第一个示例中的用户将不会获得任何角色,因此将无法访问该对象。

现在主要的问题是——安全类别是如何计算的?好吧,这取决于我们是在谈论用户端安全设置还是对象端安全设置。

用户端安全设置

[编辑 | 编辑源代码]

用户端安全基于门户类别(如果您不熟悉类别工具,请参见类别工具的说明)。正如我们之前提到的(参见上面的“ 如何成为用户?”),每个登录的用户都拥有与之关联的人员对象,定义在人员模块中(这对像 Zope 这样的特殊用户来说并不适用,因此 ERP5 安全机制将无法为他们工作!)。这个人属于一些类别(直接通过从包含的职业或分配中获取,例如职位、组或站点)。

如果您查看门户类别,您会发现每个类别值都可能定义了一个编码,即一个简短的字符串,用于识别该值。假设类别职位可以具有(除其他外)“经理”的值,它具有“MA”编码。类别组可以具有“客户服务”的值,它具有“CS”编码。

如果用户的职能是"经理",所属组是"客户服务",那么该用户的安全类别可以是"MA""经理"的编码)和"CS""客户服务"的编码),用下划线连接:MA_CS。对于另一个职能是"职员"(编码"CL")且所属组是"客户服务"的用户,其安全类别将是CL_CS。

关键问题是:哪些类别用于安全计算,以及它是如何工作的?

所以让我们拿起螺丝刀,拆下 ERP5 安全机制的前面板,看看内部。

计算用户安全设置的中心点是名为 ERP5Type_getSecurityCategoryMapping 的 Python 脚本,它定义了哪些脚本和哪些基本类别将用于安全计算。此脚本应返回一个元组

((e1),(e2),...,(en))

该元组的每个元素也是一个元组:(script_name, cat_list),其中脚本是用于计算安全类别的脚本名称,而 cat_list 是将作为参数之一传递给该脚本的基本类别列表(其余参数将在后面讨论)。因此,如果 ERP5Type_getSecurityCategoryMapping 返回:(('script1', ['function']),('script2',['function','group'])),那么首先将使用 ['function'] 调用 script1,然后将使用 ['function','group'] 调用 script2。

这些脚本做什么?它们检查用户的 Person 对象,获取指定类别的值并将其作为字典列表返回。在前面的示例中,script1 应获取 function 类别的值 ID,而 script2 应获取 functiongroup 类别的值 ID。因此,对于在客户服务中的经理角色的 Person,script1 可能返回: [{'function':'manager'}] ,而 script2 将返回: [{'function':'manager'},{'group','customer_service'}].

有一件事应该解释清楚 - 为什么脚本返回字典列表而不是单个字典?这是因为某些类别可能在 Person 对象中包含的对象(例如分配)中定义,用户可能有多个开放的分配,这会导致给定类别有多个值。

这些脚本长什么样?它们应该接受以下参数

  • base_category_list:之前已经讨论过。
  • user_name:已登录用户的名称(登录名),
  • object:通常在此处不使用;
  • portal_type:对象的入口类型(在此也不使用)。

ERP5Type_getSecurityCategoryMapping 应该由开发人员提供,并放置在门户皮肤中的某个位置。如果没有此脚本,系统将按脚本存在并返回以下元组的方式运行

(('ERP5Type_getSecurityCategoryFromAssignment',
     self.getPortalAssignmentBaseCategoryList() ),)

这会导致使用从 getPortalAssignmentBaseCategoryList() 调用获得的 base_category_list 参数调用 ERP5Type_getSecurityCategoryFromAssignment,该参数目前是: ('function', 'group', 'site')

ERP5Type_getSecurityCategoryFromAssignment 从已登录用户 Person 对象的所有开放分配计算安全映射。

一个(几乎)真实的案例示例

[编辑 | 编辑源代码]

让我们假设有一家跨多个地区经营的能源销售公司。该公司有一个客户服务部门。客户服务的区域经理应该能够访问其各自区域的最终客户的连接文档,但不能访问其他区域的最终客户的文档。区域存储为 site 类别。

假设用户 jacek 是一位经理。他有两种开放的分配 - 华沙和普鲁什科夫地区。第一个分配有以下设置

function: manager
group: customer_service
site: warsaw

而第二个分配有以下设置

function: manager
group: customer_service
site: pruszkow

注意:对于这类公司,拥有一个在两个不同地区拥有两种分配的经理可能不太常见。但是,我想表明,即使是这种不常见的需求也能够在 ERP5 中轻松实现。

ERP5Type_getSecurityCategoryFromAssignment 将为用户 jacek 返回以下字典列表

[{'function': 'manager', 'group': 'customer_service', 'site': 'warsaw'},
 {'function': 'manager', 'group': 'customer_service', 'site': 'pruszkow'}]

用户 bartek 是塞德尔策地区的经理。他拥有以下分配

function: manager
group: customer_service
site: siedlce

因此,脚本为他返回

[{'function': 'manager', 'group': 'customer_service', 'site': 'siedlce'},]

但是,关于我在本章开头提到的安全类别的字符串表示呢?为此,使用了编码。让我们假设: function/manager 的编码是 MA

group/production 的编码是 CS

site/warsaw 的编码是 WA

site/pruszkow 的编码是 PRS

以及

site/siedlce 的编码是 SL

在获得字典列表后,每个字典的 ERP5 安全机制

  • 按字母顺序对键进行排序;
  • 用其编码替换类别值 ID;
  • 将所有编码连接到一个字符串中,并用下划线分隔;

因此,对于用户 jacek 和第一个字典,编码将是:MA_CS_WA,而第二个将是:MA_CS_PRS

最后,jacek 获取以下映射:['MA_CS_WA','MA_CS_PRS']

用户 bartek 获取:['MA_CS_SL']

注意:如果未为给定类别定义编码,则将使用该类别的 ID。但是,最好为安全中使用的类别定义编码。

然后,这些安全设置将被 ERP5 用于控制对对象的访问。这将在下一章中解释。

对象侧安全设置

[编辑 | 编辑源代码]

对象安全设置(角色设置)是在门户类型基础上定义的(如果您不熟悉门户类型,请阅读此处)。这些设置是“钥匙孔”,允许钥匙所有者访问指定的 roles。

角色可以是静态计算也可以是动态计算。不幸的是,静态和动态角色计算之间没有明显的区别,因为它们都定义在同一个表单中。稍后我们将讨论这种差异。

要为门户类型定义安全设置,您应该导航到其管理表单并点击“角色”选项卡。

<图片待定 - 角色定义表单>

Name 字段中,您可以输入设置的名称。(它不是角色名称)。在实践中,通常在此处输入简短的描述。这仅供参考。

Role 字段中,您可以设置要授予用户的角色名称,该用户的“钥匙”与这个“钥匙孔”匹配(也就是说,拥有正确安全类别的用户)。

Description 字段中,您可以设置设置的更详细描述。这仅供参考。

Condition 字段中,您可以设置一个条件,该条件应该满足才能应用设置(例如,它可以是一个 Python 脚本调用)。

Base Category 字段中,您可以设置用于角色计算的基本类别名称的空格分隔列表。

Base Category Script 字段中,您可以设置如果使用动态角色计算方法,将调用的 Python 脚本的名称。如果使用静态角色计算,则不会调用脚本,并且字段值没有意义。

Category 文本区域中,您可以设置用于角色计算的类别值 ID 列表。每个类别值 ID 应放在单独的行上。

静态与动态
[编辑 | 编辑源代码]

如果对于在 Base Category 字段中定义的每个基本类别名称,在 Category 文本区域中都有一个匹配的定义,则计算是静态的,也就是说,不会调用 Base Category Script。如果存在没有在 Category 文本区域中匹配定义的基本类别名称,则计算是动态的,也就是说,将调用脚本,并且其返回值将用于缺失的定义。

它是如何工作的?
[编辑 | 编辑源代码]

静态计算非常简单 - 基于类别值创建映射。如果 Base Category 字段包含 function group,而 Category 文本区域包含

function/sale_manager
group/customer_service

那么计算出的映射将是 MA_CS,前提是编码与前面的示例相同。任何拥有 MA_CS 安全类别的用户都将获得该对象的 role。

但是,动态计算更困难。如果 Base Category 字段中的条目多于 Category 文本区域中的类别值,则将为缺失的类别调用在 Base Category Script 字段中定义的脚本。

脚本获得以下参数

  • base_category_list:在 Base Category 字段中定义的类别列表。
  • user_name:已登录用户的名称(登录名),
  • object:计算角色设置的对象,
  • portal_type:对象的入口类型。

脚本应返回字典列表,就像用户侧安全计算脚本一样(事实上,在某些情况下,它们可能是相同的脚本)。每个字典都包含来自 base_category_list 的类别名称及其相应的 value ID。

这些脚本应该做什么?这取决于开发人员。

回到(几乎)真实的案例示例

[编辑 | 编辑源代码]

在我们的示例中,我们想要限制对客户连接参数的访问,只允许负责指定区域的经理访问(即客户居住的区域)。连接参数存储在门户类型为 Connection Parameters 的对象中。此对象通过 source 关系链接到 Person 对象(代表最终客户)。Person 包含 Address 对象,而 Address 对象又具有 city 属性。

让我们导航到 Connection Parameters 管理表单,选择 Role 选项卡并填写表单

  • 在 Name 字段中,输入简短的描述,例如 Customer Service Management People
  • 在 Role 字段中,输入要授予用户的角色名称,例如 Assignor
  • 在“描述”字段中输入设置的描述,例如客户管理人员应具有其区域内所有客户的连接参数的分配者访问权限
  • 将“条件”字段留空。
  • 在“基本类别”字段中输入以空格分隔的基本类别名称列表:“功能组站点”。
  • 在“基本类别脚本”字段中输入 Python 脚本的名称。我们将其命名为ConnectionParameters_getSecurityCategoryFromCustomerAddress,以遵循 ERP5 命名约定。
  • 在“类别”文本区域中输入类别值 ID 列表
function/manager
group/customer_service

(您应该将每个类别值 ID 放在单独的行中)可以很容易地看到,我们定义了三个基本类别,但只有两个类别值 ID。因此,对于缺少的类别(站点),将使用ConnectionParameters_getSecurityCategoryFromCustomerAddress脚本。

该脚本应检查连接参数对象,获取链接的人员对象,获取其默认地址城市,从城市名称计算区域(它可以在类别中定义城市到区域的映射;或调用外部 GIS 系统;或使用水晶球),然后返回相应的字典列表。

该脚本可能类似于以下脚本

if portal_type != 'Connection Parameters':
 raise RuntimeError, 'Error: Script called for invalid portal type'

customer = object.getSourceValue()
city = customer.getDefaultAddressCity()
region = context.ERP5Site_GetRegionFromCity()  #guess the region somehow
category_dict = {}

for base_category in base_category_list:
  if base_category == 'site':
    category_dict[base_category]=region

对于居住在普鲁什科夫地区的城市的客户,该脚本应返回:[{'site':'pruszkow'}]

其余的映射将静态计算 – 功能/经理映射到MA组/客户服务映射到CS,并且由于站点/普鲁什科夫映射到PRS,最终映射将为MA_CS_PRS。任何具有此安全类别的用户(即负责普鲁什科夫地区的客户服务经理)将被授予文档的分配者角色。

那么,与多个客户(可能居住在不同地区)相关的文档呢?没问题,这只需要稍作修改。该脚本应检查与文档相关的人员列表,并返回一个字典列表 – 一个客户一个字典。因此,如果文档与两个客户相关 – 一个在普鲁什科夫地区,一个在耶齐奥尔纳地区,返回值将如下:[{'site':'pruszkow'},{'site':'jeziorna'}],最终文档将获得以下映射'MA_CS_PRS','MA_CS_JEZ'


扩展默认值
[edit | edit source]

假设有一类文档(例如连接标准,门户类型连接标准)应提供给所有经理,无论他们负责哪个地区。如何实现这一点?第一个想法是为连接标准门户类型编写一个基本类别脚本,以便它返回所有地区的所有可能映射:MA_CS_PRSMA_CS_WAMA_CS_SL,……等等。嗯,如果公司有 20 个地区,就会有 20 种不同的映射。很多吧?更糟糕的是,如果公司发展壮大,出现了新的地区,所有文档的安全设置都必须重新定义。

更好的想法是定义没有地区代码的映射,即MA_CS。具有此映射的文档将可供所有客户服务经理访问……或者不?

嗯,不完全是,因为没有经理会获得MA_CS安全类别!因此,需要稍微修改一下默认的 ERP5 安全机制。

为此,您必须将ERP5Type_getSecurityCategoryMapping脚本放在皮肤中。该脚本应如下所示

return
(('ERP5Type_getSecurityCategoryFromAssignment', ['function','group','site']),
 ('ERP5Type_getSecurityCategoryFromAssignment', ['function','group']))

第一行看起来类似于系统默认值(我用显式基本类别列表替换了 getPortalAssignmentBaseCategoryList()),第二行是新的。现在,当用户登录时,ERP5Type_getSecurityCategoryFromAssignment 将被调用两次 – 一次使用['function','group','site'],另一次仅使用['function','group']。此第二次调用为任何“客户服务经理”(无论地区如何)产生MA_CS

对于连接标准门户类型,我们应该定义以下角色定义(仅显示相关字段)

  • 在“角色”字段中:分配者
  • 在“基本类别”字段中:功能组
  • 在“类别”文本区域中
function/manager
group/customer_service

这将产生映射MA_CS,因此所有具有MA_CS安全类别的用户都将被授予分配者角色。

角色不是权限

[edit | edit source]

好吧,在这一点上,您可能会认为所有需要做的事情都已完成,但事实并非如此。您尚未定义角色到权限的映射。

什么到什么的映射?——你可能会问。嗯,尽管有他们的名字,但角色并不能说明与他们相关的特权。在“审计员”或“分配者”这两个词中没有任何神奇之处,ERP5 本身并不知道这些角色被允许做什么。

(这甚至适用于 Zope 的经理角色!在许多系统中,经理(或者无论他被称为什么)都是一个强大的警长,可以做任何事情。但在 ERP5 中则不然。您决定每个角色拥有哪些权限。如果您愿意,甚至可以定义没有任何权限的经理,尽管这是一个很糟糕的想法。但是,您很容易错误地授予经理不足的权限,在这种情况下,您会在开发过程中遇到很大的麻烦。)

如何定义角色的权限?可以通过静态方式或动态方式完成。

静态权限定义
[edit | edit source]

这适用于在开发期间存在的对象。它们主要是模块。要为这样的对象定义权限,请导航到该对象的管理页面,然后转到“安全性”选项卡。阅读 Zope 文档以了解详细信息。

动态权限定义
[edit | edit source]

这是工作流进入场景的时候。如您所知(如果您不知道,请阅读有关工作流的部分),每个门户类型都可以分配给一个或多个工作流。因此,此门户类型的对象(实例)可以处于某些工作流状态。每个工作流状态定义单独的角色到权限映射。进入特定工作流状态的对象将获得为此工作流状态定义的角色到权限映射。

这允许非常灵活的特权操作。例如,新创建的连接参数文档应该只能由其创建者访问,因此草稿状态仅为所有者定义了查看和修改权限。文档完成后,可以提交。在这种状态下,分配者和审计员可以阅读,分配者可以读写。然后文档经过工作流的剩余部分,最终进入存档状态,在此状态下,任何人都无法更改它,但许多人可以阅读。

要启用基于工作流的安全性,请导航到工作流管理页面,单击“权限”选项卡并添加所需的权限。至少以下权限应由工作流管理:查看和访问内容信息(读取)、修改门户内容(写入)和添加门户内容(创建新对象)。

要为工作流状态定义角色到权限映射,请导航到工作流状态管理页面,然后单击“权限”选项卡。您将仅看到由此工作流管理的权限。


更新安全设置

[edit | edit source]

对象上的安全设置不会自动更新。这是开发人员在对象状态更改后更新安全设置的责任。

一种方法是使用单独的交互工作流,该工作流为参与安全计算的属性的设置器定义了交互(在我们“几乎真实的案例示例”中,人员的地址城市名称是此类属性的示例 – 城市名称的更改会导致相关连接参数文档的安全设置更改,如果新城市与旧城市位于不同的地区)。交互应触发一个检查所有相关对象并为其重新计算安全设置的脚本。

要重新计算对象上的安全设置,请在该对象的上下文中调用 updateLocalRolesOnSecurityGroups()。然后,重新索引该对象(这是必要的,因为对象权限存储在门户目录中 -> 请参阅目录工具的说明)。

调试安全性

[edit | edit source]

<待撰写>

华夏公益教科书