arckinteractive / roles
Elgg 的角色管理
Requires (Dev)
- composer/installers: ~1.0
- phpunit/phpunit: ~4.1
README
本项目为 Elgg 实现用户角色,比 Elgg 默认的用户区分提供更细粒度的控制。
Elgg 基本上识别两种类型的用户:成员(普通用户)和管理员(可以在网站上做任何事的用户)。通常情况下,你可能需要额外的用户类型和相关的权限,以便每个 用户类型 可以对你的网站进行操作。通过使用此插件,你可以轻松引入自定义角色,将用户组限制在特定的权限内。
请理解,在当前状态下,Roles 插件 更像是框架而不是一个完整的独立插件。 你需要进一步调整或使用扩展插件来使自定义角色工作。
安装
# from project root composer require -g composer/installers composer require arckinteractive/roles:~2.0 # run tests phpunit
简介
Roles 插件为你的 Elgg 网站提供比 Elgg 默认的用户区分更细粒度的控制。Elgg 基本上识别两种类型的用户:成员(普通用户)和管理员(可以在网站上做任何事的用户)。还有一个第三种类型的伪用户,即 未登录用户,我将其称为访客。
通常情况下,你可能需要额外的用户类型和相关的权限,以便每个 用户类型 可以对你的网站进行操作。你可能希望将群组创建权限限制在某些用户,或者你可能需要特殊的行政用户(如管理员)来处理报告的内容,但不能访问其他管理功能(如安装新插件)。
这仅是我能想到的许多用例中的两个,但使用 Roles 插件可以实现这些功能——以及更多。在开始使用插件之前,请理解 Roles 插件在当前状态下 更像是框架而不是完整的独立插件。你需要进行额外的调整,以便使上述特殊角色的功能正常工作。
目标受众
尽管此插件的未来版本可能针对没有 PHP 技能的行政用户,但在当前状态下,此插件是为 Elgg 开发者设计的,以非侵入方式实现用户角色。 到目前为止,没有用户友好的方式使用此插件在 Elgg 安装中创建或配置角色。你将需要亲自动手,创建一个 PHP 配置数组,以充分利用插件的功能。
话虽如此,如果你有足够的耐心阅读此文档,你将能够为你的 Elgg 网站设置特定的角色——即使你的编程技能非常有限。
此外,我预计此插件将衍生出许多子插件,定义自定义角色。因此,即使你不想深入了解细节,此插件与未来的扩展结合可能正好为你提供你一直梦寐以求的细粒度访问权限。
从开发者的角度看角色
角色表示为用户和角色对象之间的称为 has_role
的关系。在任意时刻,一个用户要么没有角色关系,要么有一个角色关系(不支持每个用户有多个角色)。如果用户没有角色关系,他将自动与默认角色之一关联——更多内容请参考《默认角色》章节。
在当前(1.0.0)版本中,角色对象会自动从角色配置数组中创建。每当配置数组发生变更时,角色对象会相应地更新,无需手动运行升级脚本。然而,角色变更将在您作为站点管理员保存角色变更后,重新加载任何页面后才生效。新的页面请求将启动对所有角色配置的更新,但**这仅适用于您以管理员身份登录时**。
从配置数组中删除角色不会删除相应的角色对象。只有现有角色的权限变更将会更新,新角色将自动创建。要完全删除并替换所有角色对象以新的配置集,请先禁用,然后重新启用角色插件。请注意,这将销毁任何用户 <-> 角色
关系,即所有用户将失去他们现有的角色。警告:这意味着开发者在现有角色环境中修改角色插件代码时需要格外小心,因为Elgg可能会自动禁用插件,如果它无法启动。如果您对此行为不满意,只需从activate.php中删除相关部分即可——它只是几行代码。
role
对象
角色对象是ElggRole
类(扩展ElggObject
类)的实例,具有以下属性
name
:给定角色的唯一字符串标识符(员工、超级用户等)title
:角色的显示名称extends
:包含此角色扩展的其他角色名称的扁平数组。在创建或更新角色对象时,将按数组的自然顺序处理这些角色。permissions
:描述给定角色所有权限的序列化数组。关于权限的更多解释请参见配置章节。
请注意,角色扩展不会以动态、即时的方式工作。扩展是一个简化规则集定义的工具:通过将role2
扩展为role1
,您可以说role2
非常类似于role1
,除了以下规则:…——您只需要定义需要覆盖的rule1
中的规则。所有角色扩展在从配置数组创建(或更新)角色对象时都会被解析。
每个角色对象都附有一个有用的方法,称为public function getUsers($options)
,它将返回当前角色对象的用户列表。数组$options
接受与elgg_get_entities*
函数相同的键 => 值对。
默认角色
该插件附带3个默认(内置)角色:VISITOR_ROLE
、DEFAULT_ROLE
和ADMIN_ROLE
。这些角色将自动与不具有特定角色的用户(或未登录访客)关联。
默认角色是基于以下假设创建的:对于大多数用户来说,简单的member
角色(Elgg默认提供的)就足够了。由于我们只需要存储具有非默认角色的用户的角色关系,因此数据库中与角色相关的关系的数量显著减少。同时,我们还可以为这些默认的内置规则定义特定的规则。
从代码中获取和设置用户角色
该模块的目的是让插件作者能够在不需要明确知道用户特定角色的前提下开发大部分功能。大多数基于角色的功能——即用户可以看到和与之交互的内容——可以移动到配置数组中;因此无需在代码中处理基于角色的条件。
在极少数情况下,如果您确实需要知道用户具有什么角色,可以通过调用function roles_get_role($user = null)
来获取,它将返回对应于$user
参数的角色对象。如果未传递$user
,它将返回当前登录用户的角色对象。请注意,此函数保证返回一个ElggRole
对象 – 如果用户没有特定的角色,将返回一个默认角色对象。
明确设置用户角色的可能性更小,但仍然可以通过function roles_set_role($role, $user = null)
来实现,该函数期望一个角色对象,以及可选地设置角色的用户。如果未传递$user
,它将为当前登录用户设置角色。
要获取所有角色对象或特定一个,请使用以下两个函数:function roles_get_all_roles()
function roles_get_role_by_name($role_name)
通过用户设置页面设置用户角色
存在一个特定的视图roles/settings/account/role
,它实现了一个角色选择控件。这基本上是一个包含系统中所有定义的自定义角色的下拉框,以及一个名为“无特定角色”的附加项。
默认情况下,只有当当前用户具有默认的ADMIN_ROLE
时,此视图才会添加到用户设置页面。当然,这个插件的美妙之处在于您可以定义其他角色,这些角色可能具有更改用户角色的相同权限。
配置
配置数组
截至本版本,没有花哨的行政界面来设置角色权限;这必须通过关联配置数组来完成。默认角色的数组位于mod/roles/lib/config.php
文件中,具有以下结构(示例未显示所有配置细节)
$roles = array( VISITOR_ROLE => array( 'title' => 'roles:role:VISITOR_ROLE', 'extends' => array(), 'permissions' => array( // permissions ) ), DEFAULT_ROLE => array( 'title' => 'roles:role:DEFAULT_ROLE', 'extends' => array(), 'permissions' => array( // permissions ) ), ADMIN_ROLE => array( 'title' => 'roles:role:ADMIN_ROLE', 'extends' => array(), 'permissions' => array( // permissions ) ), );
您可以覆盖任何默认角色的权限,或向配置中添加新角色。推荐的做法是创建一个名为yoursitename_roles
的轻量级插件,并使用优先级大于500的'role:config', 'role'
插件钩子注册。您的钩子将由角色插件触发,并期望将角色定义合并到原始角色配置数组中。
以下是如何实现'role:config'
钩子的示例
function myroles_config($hook_name, $entity_type, $return_value, $params) { $roles = array( 'limited_users' => array( 'title' => 'yoursitename_roles:limited_users', 'permissions' => array( 'actions' => array( 'groups/save' => array('rule' => 'deny') ), ), ) ); if (!is_array($return_value)) { return $roles; } else { return array_merge($return_value, $roles); } }
对于简单的规则,如allow
和deny
,可以使用简写语法
$permissions['actions'] = array( 'groups/save' => 'deny', 'blogs/save' => 'allow', ), );
可以使用简单的正则表达式模式设置页面和动作路径
$permissions['pages'] = array( 'groups/(view|edit)' => 'deny', 'blogs/owner/\d+' => 'allow', 'admin/.*' => 'deny', ), );
上述代码向系统中添加了一个名为limited_users
的新角色。该角色的成员将无法创建组,如配置数组中的权限部分所定义。权限将在稍后进行更详细的解释。
上述$roles
关联数组可以包含任何数量的新角色和默认角色的覆盖定义(VISITOR_ROLE
、DEFAULT_ROLE
和ADMIN_ROLE
)。上述示例中的第一级键(上述示例中的单个角色键limited_users
)应在Elgg安装中是唯一的 – 此键是角色的主要标识符。当其他插件使用不同的规则集定义相同的角色时,后来的定义将覆盖早期的定义。
注意上述钩子函数如何使用优雅的角色定义:它不是仅仅返回新的角色定义数组,而是将其与先前存在的数组合并。这是您的角色扩展通常应该如何表现,允许其他插件定义其他一组角色。
permissions
部分包含确定用户可以在网站上看到和与之交互的个别权限规则。权限可以包含与菜单项、视图、页面、动作、插件钩子和事件相关的规则部分的规则。对于这些权限部分的绝大部分,基本的权限规则如下
deny
:拒绝访问特定项目。在某些情况下,这会产生错误消息并导致重定向(对于动作和页面),在其他情况下,给定的项目将简单地不会渲染(对于视图和菜单)。对于钩子,指定的钩子处理程序不会被触发。对于事件,您可以注销事件处理程序。allow
: 允许访问特定项目。此规则在扩展规则时最为有用——默认角色可以拒绝创建新组,而默认角色的扩展可以特别重新允许创建组。extend
: 在当前页面上动态添加新项目——这适用于钩子、事件、菜单和视图。例如,您可以通过使用正确的配置值来添加特定角色的菜单项、扩展现有视图、创建新的插件钩子或事件监听器。replace
: 替换现有项目。适用于视图、菜单、钩子和事件。redirect
: 重定向到另一个页面。适用于页面。
菜单权限
可以从任何Elgg菜单动态删除或附加菜单项。要禁用默认角色的 成员 主菜单项,配置将如下所示
$roles[DEFAULT_ROLE] = array( 'title' => 'roles:role:DEFAULT_ROLE', 'extends' => array(), 'permissions' => array( 'menus' => array( 'site::members' => array('rule' => 'deny') ), ) );
menus
配置部分可以包含任意数量的菜单规则,每个规则都使用 menu-name::item-name
结构作为键,用于标识要操作菜单项。一个更复杂的示例显示了扩展默认角色的角色配置中的菜单部分
$permissions['menus'] = array( 'site::members' => array( // 1st menu rule 'rule' => 'allow' ), 'site' => array( // 2nd menu rule 'rule' => 'extend', 'menu_item' => array( 'name' => 'books', 'text' => 'Books', 'href' => 'books/all', ) ), 'site::groups' => array( // 3rd menu rule 'rule' => 'replace', 'menu_item' => array( 'name' => 'mygroups', 'text' => 'My Groups', 'href' => 'groups/member/{$self_username}', ) ), 'filter::friends' => array( // 4th menu rule 'rule' => 'deny', 'context' => array('bookmarks', 'files') ), );
第一个规则将覆盖默认角色对“成员”菜单项的规则——对于具有上述角色的用户,它将再次可见。
第二个部分演示了如何为角色动态地向任何给定菜单添加新菜单项。在这里,菜单通过简单的 menu-name
而不是 menuname::item-name
结构进行标识。规则表示要扩展所选菜单并添加新项目,新项目本身在规则之后定义,在 menu_item
下。menu_item 关联数组期望与 ElggMenu::factory()
方法相同的键值对。简而言之,此规则将添加一个名为 书籍 的新菜单项到主菜单,并将链接到书籍的“所有”页面。当然,您仍然需要实际实现该页面的内容。
第三条规则显示了如何在菜单中替换现有菜单项。对于此角色的成员,标准 组 链接将在网站菜单中被替换为 我的组,指向当前用户是成员的组。注意 href 部分的动态变量:{$self_username}
。此引用将被替换为当前登录用户的用户名。有关动态页面路径的更多信息,请参阅 使用动态路径 章节。
第四条规则显示了如何创建上下文相关的规则。该规则将从 所有、我的、朋友 内容过滤菜单中删除“朋友”项,仅留下 所有 和 我的。然而,由于该部分已定义上下文,因此只有在当前上下文是预定义上下文之一时,该规则才会生效。换句话说,从筛选菜单中删除的“朋友”项将仅在书签和文件页面上。
页面权限
可以按角色分别拒绝或允许访问页面;页面也可以通过规则集静默重定向。
$permissions['pages'] => array( 'groups/add/{$self_guid}' => array( 'rule' => 'deny', 'forward' => 'groups/all', ) );
上述规则将拒绝访问 创建新组 页面,将错误消息注册以通知用户,并将用户重定向到 组 首页。如果没有指定重定向地址,用户将被重定向到引用页面。请注意,您可以使用与菜单权限部分中描述的相同的动态替换。
forward
规则的行为与 deny
规则相同,但它不会注册错误消息——它将简单地静默地将用户重定向到另一个页面。
allow
规则将覆盖给定页面路径之前角色的 deny
或 redirect
规则。
视图权限
视图可以被抑制、扩展或替换以适用于任何角色。以下是一些示例。
$permissions['views'] = array( 'input/password' => array('rule' => 'deny'), // 1st view rule 'forms/account/settings' => array( // 2nd view rule 'rule' => 'extend', 'view_extension' => array( 'view' => 'roles/settings/account/role', 'priority' => 150 ) ), 'object/blog' => array( // 3rd view rule 'rule' => 'replace', 'view_replacement' => array( 'location' => 'mod/modified_blog/extended/views/', ) ), );
第一条规则简单地抑制了 input/password
视图。这是一种禁用某些角色更改密码的简单方法。视图将不会显示,而不会出现任何警告或错误信息。
第二条规则展示了基于角色的视图管理的一个更有用的示例:它扩展了 forms/account/settings
视图,添加了一个新的视图 roles/settings/account/role
- 这恰好是角色选择框。换句话说,这个规则将允许成员更改他们(和其他用户的)角色。extend
规则的行为与 elgg_extend_view()
调用相同,但现在视图扩展已从源代码移动到角色配置。
第三条规则展示了如何为特定角色替换现有视图。视图替换的工作方式与调用 elgg_set_view_location()
函数相同,位置相对于 Elgg 的安装路径。在示例中,我们用不同的视图替换了这个角色的博客视图。
操作权限
操作有相当简单的规则选项:允许和拒绝。要禁用对特定操作的访问,请使用
$permissions['actions'] = array( 'registered/action/name' => array('rule' => 'deny') );
如果尝试访问不允许的操作,将记录错误消息,并将用户转发到引用页面。
钩子权限
可以为任何角色注册、注销或替换插件钩子处理器。
$permissions['hooks'] = array( 'usersettings:save::user' => array( 'rule' => 'extend', 'hook' => array( 'handler' => 'roles_user_settings_save', 'priority' => 500, ) ), );
上面的示例将注册一个新处理器,该处理器将由 'usersettings:save', 'user'
钩子触发。显然,你仍然需要实现处理器函数。上面的示例,如所示,由角色插件用于默认的 ADMIN_ROLE
。处理器实现负责在用户设置页面上更改用户角色时保存用户角色。
钩子应始终通过钩子名称或通过 ::
(双冒号)分隔的钩子名称和钩子类型来识别。如果未定义钩子类型,则假定类型为 all
。当你 deny
一个钩子时,角色插件将基本上注销给定的钩子处理器,或注销指定钩子的所有处理器。
$permissions['hooks'] = array( 'register::menu:extras' => 'deny', 'usersettings:save::user' => array( 'rule' => 'deny', 'hook' => array( 'handler' => 'user_settings_save', ) ), );
这将注销默认的 Elgg 保存用户设置的处理器,这意味着该角色的成员将无法保存对设置页面的任何更改。诚然,这不是一个很好的例子,但你应该了解其基本原理。一个更好的例子可能是替换保存用户设置的默认钩子,如下所示
$permissions['hooks'] = array( 'usersettings:save::user' => array( 'rule' => 'replace', 'hook' => array( 'old_handler' => 'user_settings_save', 'new_handler' => 'custom_user_settings_save', ) ), );
事件权限
事件权限与钩子的工作方式非常相似;事件处理器可以注册、注销或替换以适用于任何角色。以下是通过配置数组注册事件监听器的示例
$permissions['events'] = array( join::group' => array( 'rule' => 'extend', 'event' => array( 'handler' => 'custom_join_group_handler', ) ), );
当上述定义的角色成员加入一个群组时,将调用 custom_join_group_handler
函数,你可以在其中执行所需的任何操作——例如通知所有群组成员这一事实。
使用动态路径
对于操作路径和页面请求 URI,可能需要比声明静态字符串更复杂的路径定义功能。你有两种方法来实现更复杂的路径匹配:使用内置变量和正则表达式。
你可以在任何 URL 或路径样式的参数中使用以下变量
{$self_username}
当前登录用户的用户名{$self_rolename}
当前登录用户的角色名称{$self_guid}
当前登录用户的 GUID{$pageowner_username}
当前页面所有者的用户名{$pageowner_rolename}
当前页面所有者的角色名称{$pageowner_guid}
当前页面所有者的 GUID
在解析页面、菜单和动作路径时,上述变量将被替换。您可以在《菜单权限》章节中看到这个例子。您还可以在路径定义中使用正则表达式,如下所示。
$permissions['actions'] => array( 'regexp(/^admin\/((?!user\/ban|user\/unban).)*$/)' => array( 'rule' => 'deny' ) );
上述动作规则将匹配所有admin/*
动作,除了admin/user/ban
和admin/user/unban
,并且拒绝所有匹配动作的访问。换句话说,我们创建了一个具有非常有限功能的admin用户,该用户将能够禁止和取消禁止其他用户,但无法执行任何其他管理动作。
在使用正则表达式时,您需要将模式字符串放在一个regexp( )
伪调用中。这将告知Roles规则解析器将表达式内部的解释为正则表达式。
当结合上述两种技术时,即同时使用内置变量和正则表达式,请记住,首先将解析变量,然后将被替换的模式传递给正则表达式匹配器。
角色实现的指南 使用现实世界的例子
正如您从所有配置选项中可以看到的,您有一套相当灵活的工具来为您Elgg用户实现不同的角色。对于大多数目的,您需要使用规则的组合以安全的方式实现您的目标。
实现组管理员角色
考虑以下通常请求的角色:只有特权成员应该能够创建和编辑组。为了实现这一点,您需要采取以下步骤。
- 覆盖默认角色。
- 在默认角色定义中,拒绝访问组页面上的创建新组菜单项。
- 还拒绝访问侧边栏中的我拥有的组菜单项(这样您就不会用死链接混淆普通成员)。
- 拒绝访问
groups/add/{$self_guid}
页面。 - 出于安全原因,拒绝访问在创建新组或编辑现有组时都会调用的
groups/edit
动作。
有了上述规则,您实际上切断了默认成员对组创建的所有访问。现在您只需要为组管理员(可以创建和编辑组)定义另一个角色。这个角色的规则集可以完全为空——因为这个角色是新的,上面定义的限制不适用于该角色的成员。他们将能够像任何成员一样访问所有这些页面、动作和菜单。要使某人成为组管理员,只需以管理员用户身份访问目标用户的设置页面,并将其角色设置为新建的组管理员角色。
此外,如果您想使这个功能更加方便,您可以在成员图标角落打开的悬停上下文中添加新菜单项。这稍微复杂一些,以下是您如何做到这一点的方法。
- 覆盖默认管理员角色。
- 通过扩展
'register', 'menu:userhover'
钩子向配置数组中的钩子部分添加一个新处理程序。 - 实现处理程序函数。
- 在您的处理程序中,检查用户的当前角色(用户作为参数从钩子触发传递)。
- 如果当前角色是组管理员,则在悬停菜单中添加一个名为撤销组管理员权限的新菜单项。如果当前角色是默认成员角色,则在悬停菜单中添加一个名为授予组管理员权限的新菜单项。将这些菜单项链接到两个不同的动作。
- 在您的动作实现中,根据需要显式设置用户的角色为组管理员或默认成员。
Elgg社区中有上述角色配置的完整工作示例。查找插件roles_group_admins。
实现版主角色
在社区中,我经常看到的一个请求是让某些成员升级为管理员。管理员可以编辑其他用户创建的内容,并封禁/解封他们。然而,他们无法执行其他管理操作,例如安装插件或更改网站设置。
从角色的角度来看,自然的做法是扩展默认的ADMIN_ROLE
,并通过deny
规则逐步撤销大部分管理权限。
- 创建一个新的角色,称为
moderator
,覆盖默认管理员角色。 - 在这个新角色中,除了报告内容页面外,拒绝访问所有管理页面。这可以通过使用正则表达式来实现页面匹配模式。
- 通过拒绝访问
toolbar
菜单中的administration
菜单项,移除顶部栏的通用管理链接。 - 向顶部栏菜单或网站菜单添加一个新的菜单项,直接链接到报告的内容页面。
- 拒绝访问所有管理操作,除了封禁和解封用户。这同样可以通过使用正则表达式来实现动作匹配。
- 最后,抑制显示整个管理侧边栏的视图。这是为了在显示报告内容页面时移除所有管理快捷方式。就这样。现在你可以通过首先将用户提升为管理员,然后将他们的角色设置为管理员,从任何用户中创建一个管理员。Elgg社区中有一个完整的上述角色配置示例。查找插件
roles_moderators
。
自定义角色开发人员的约定
如果你正在实现一个特定的角色,并且认为它也可能对其他人有用,请务必在社区网站上发布它。以下是一些关于创建角色扩展插件的技巧
- 以
roles_
开头命名你的插件,例如上一个示例中的roles_group_admins
。 - 使用类似于
My specific role for Roles
的模式为你的插件命名,这样角色相关的搜索就能找到你的扩展 - 在你的清单文件中,使用
<requires>
标签来表示你的插件依赖于角色插件。 - 在你的角色配置钩子中,使用优雅的角色定义:除了返回新的角色定义数组外,还可以将其与之前存在的数组合并。
- 请记住安全性。仅从给定菜单中删除一项通常是不够的——你还将想要拒绝访问相应的页面和操作。
升级
从1.x
- 缓存不再写入全局
$PERMISSIONS_CACHE
。它不再是\Elgg\Roles\Api
类的私有属性 extends
和permissions
元数据不再可以直接访问ElggRole
对象。使用ElggRole::setExtends()
、ElggRole::getExtends()
、ElggRole::setPermissions()
和ElggRole::getPermissions()
ElggRole
对象的title
属性现在以原始字符串的形式存储。使用ElggRole::getDisplayName()
来显示i18n标题- 现在将自动删除链接到拒绝的页面和动作的所有菜单项。取消注册
roles_menus_cleanup
处理程序以恢复旧行为。