自定义角色开发指南
最终用户可以创建自定义角色,并通过分配特定能力来定义这些角色。
例如,用户可以创建一个"工程师"角色,赋予其"读取代码"和"管理合并请求"的能力,但不包括"管理问题"这类能力。
在此上下文中,“权限"和"能力"这两个术语经常互换使用。
- “能力"是用户可以执行的操作。这些对应到声明式策略能力,并存在于
ee/app/policies/*目录下的策略类中。 - “权限"是我们在面向用户的文档中指代能力的术语。权限文档是手动生成的,因此文档中列出的权限与策略类中定义的能力之间不一定存在一一对应关系。
自定义角色与默认角色
在 GitLab 15.9 及更早版本中,GitLab 只有默认角色作为权限系统。在这个系统中,有几个预定义的角色被静态分配给特定能力。这些默认角色客户无法自定义。
通过自定义角色,客户可以决定将哪些能力分配给特定的用户组。例如:
- 在默认角色系统中,漏洞读取权限仅限于开发者角色。
- 在自定义角色系统中,客户可以将此能力分配给基于任何默认角色的新自定义角色。
与默认角色一样,自定义角色在组层次结构内是继承的。如果用户对某个组有自定义角色,那么该用户对该组内的任何项目或子组也将拥有相同的自定义角色。
技术概述
- 单个自定义角色存储在
member_roles表中(MemberRole模型)。 member_roles记录通过namespace_id外键与顶级组(非子组)关联。- 组或项目成员资格(
members记录)通过member_role_id外键与自定义角色关联。 - 组或项目成员资格可以与定义在该组或项目的根级组的任何自定义角色关联。
member_roles表包含单个权限和base_access_level值。base_access_level必须是有效的访问级别。base_access_level决定了自定义角色包含哪些能力。例如,如果base_access_level是10,则自定义角色将包含默认访客角色会获得的所有能力,以及通过设置属性(如read_code为 true)启用的任何额外能力。- 自定义角色可以为
base_access_level启用额外能力,但不能禁用权限。因此,自定义角色是"仅添加的”。选择这种理由的说明见此评论。 - 自定义角色能力在项目级别和组级别都受支持。
能力重构
查找现有能力检查
对于单个端点或 Web 请求,能力经常在多个位置进行检查。因此,很难找到为给定端点运行的授权检查列表。
为此,您可以在本地设置 GITLAB_DEBUG_POLICIES=true。
这将输出关于在您运行的任何规范中请求检查了哪些能力的信息。输出还包括进行授权检查的代码行。调用者信息在使用了元编程的情况下特别有用,因为通过搜索能力名称字符串很难找到这些情况。
例如:
# 示例规范运行
GITLAB_DEBUG_POLICIES=true bundle exec rspec spec/controllers/groups_controller_spec.rb:162
# 规范运行时的权限调试输出;如果运行了多个策略检查,它们都会出现在调试输出中。
POLICY CHECK DEBUG -> policy: GlobalPolicy, ability: create_group, called_from: ["/gitlab/app/controllers/application_controller.rb:245:in `can?'", "/gitlab/app/controllers/groups_controller.rb:255:in `authorize_create_group!'"]使用此设置来了解重构时的授权检查。您不应在默认分支的任何规范中保持此设置启用。
理解单个能力的逻辑
对能力的引用可能多次出现在 DeclarativePolicy 类中,并依赖于引用其他能力的条件和规则。因此,很难确切知道哪些条件适用于特定能力。
DeclarativePolicy 为每个策略类提供了一个 ability_map,它将能力的所有规则提取到一个数组中。
例如:
> GroupPolicy.ability_map.map.select { |k,v| k == :read_group_member }
=> {:read_group_member=>[[:enable, #<Rule can?(:read_group)>], [:prevent, #<Rule ~can_read_group_member>]]}
> GroupPolicy.ability_map.map.select { |k,v| k == :read_group }
=> {:read_group=>
[[:enable, #<Rule public_group>],
[:enable, #<Rule logged_in_viewable>],
[:enable, #<Rule guest>],
[:enable, #<Rule admin>],
[:enable, #<Rule has_projects>],
[:enable, #<Rule read_package_registry_deploy_token>],
[:enable, #<Rule write_package_registry_deploy_token>],
[:prevent, #<Rule all?(~public_group, ~admin, user_banned_from_group)>],
[:enable, #<Rule auditor>],
[:prevent, #<Rule needs_new_sso_session>],
[:prevent, #<Rule all?(ip_enforcement_prevents_access, ~owner, ~auditor)>]]}DeclarativePolicy 还提供了一个 debug 方法,可用于理解特定对象和参与者的逻辑树。输出类似于 ability_map 中的规则列表。但是,DeclarativePolicy 在您 prevent 某个能力后会停止评估规则,因此可能不会调用所有条件。
示例:
policy = GroupPolicy.new(User.last, Group.last)
policy.debug(:read_group)
- [0] enable when public_group ((@custom_guest_user1 : Group/139))
- [0] enable when logged_in_viewable ((@custom_guest_user1 : Group/139))
- [0] enable when admin ((@custom_guest_user1 : Group/139))
- [0] enable when auditor ((@custom_guest_user1 : Group/139))
- [14] prevent when all?(~public_group, ~admin, user_banned_from_group) ((@custom_guest_user1 : Group/139))
- [14] prevent when needs_new_sso_session ((@custom_guest_user1 : Group/139))
- [16] enable when guest ((@custom_guest_user1 : Group/139))
- [16] enable when has_projects ((@custom_guest_user1 : Group/139))
- [16] enable when read_package_registry_deploy_token ((@custom_guest_user1 : Group/139))
- [16] enable when write_package_registry_deploy_token ((@custom_guest_user1 : Group/139))
[21] prevent when all?(ip_enforcement_prevents_access, ~owner, ~auditor) ((@custom_guest_user1 : Group/139))
=> #<DeclarativePolicy::Runner::State:0x000000015c665050
@called_conditions=
#<Set: {
"/dp/condition/GroupPolicy/public_group/Group:139",
"/dp/condition/GroupPolicy/logged_in_viewable/User:83,Group:139",
"/dp/condition/BasePolicy/admin/User:83",
"/dp/condition/BasePolicy/auditor/User:83",
"/dp/condition/GroupPolicy/user_banned_from_group/User:83,Group:139",
"/dp/condition/GroupPolicy/needs_new_sso_session/User:83,Group:139",
"/dp/condition/GroupPolicy/guest/User:83,Group:139",
"/dp/condition/GroupPolicy/has_projects/User:83,Group:139",
"/dp/condition/GroupPolicy/read_package_registry_deploy_token/User:83,Group:139",
"/dp/condition/GroupPolicy/write_package_registry_deploy_token/User:83,Group:139"}>,
@enabled=false,
@prevented=true>能力整合
添加到自定义角色的每个功能都应具有最少的能力。对于大多数功能,拥有 read_* 和 admin_* 应该就足够了。您应该整合所有:
- 与查看相关的能力整合到
read_*下。例如,查看列表或详情。 - 对象更新整合到
admin_*下。例如,更新对象、添加指派人或关闭该对象。通常,启用admin_的角色也必须启用read_能力。这定义在MemberRole模型上ALL_CUSTOMIZABLE_PERMISSIONS哈希中的requirement选项中。
有些功能可能需要额外能力,但请尽量减少这些。您可以随时询问认证和授权小组成员的意见或寻求帮助。
这也应该是您工作的起点。获取您所处理功能的所有能力,并将这些能力整合到 read_、admin_ 或必要时添加额外能力。
GroupPolicy 和 ProjectPolicy 类中的许多能力都有许多冗余策略。有一个用于整合这些策略类的史诗。如果您在这些类中遇到类似的权限,请考虑重构,使它们具有相同的名称。
例如,您在 GroupPolicy 中看到一个名为 read_group_security_dashboard 的能力,在 ProjectPolicy 中有一个名为 read_project_security_dashboard 的能力。您希望使两者都可自定义。而不是为每个能力在 member_roles 表中添加一行,考虑将它们重命名为 read_security_dashboard,并将 read_security_dashboard 添加到 member_roles 表中。在父组上启用 read_security_dashboard 将允许自定义角色访问该组的安全仪表板以及该组中每个项目的安全仪表板。在特定项目上启用相同的权限将允许访问该项目的安全仪表板。
如何为自定义角色添加能力支持
如果添加现有能力,请在完成以下步骤之前,考虑在单独的合并请求中重构和整合功能的能力。
第 1 步:生成配置文件
- 运行
./ee/bin/custom-ability <ABILITY_NAME>为新能力生成配置文件。 - 这将在
ee/config/custom_abilities中生成一个遵循以下架构的 YAML 文件:
| 字段 | 必需 | 描述 |
|---|---|---|
name |
yes | 描述自定义能力的唯一、小写和下划线分隔的名称。必须与文件名匹配。 |
title |
yes | 自定义可读性标题。 |
description |
yes | 自定义可读性描述。 |
feature_category |
yes | 功能类别名称。例如,vulnerability_management。 |
introduced_by_issue |
yes | 提议添加此自定义能力的 Issue URL。 |
introduced_by_mr |
yes | 添加此自定义能力的 MR URL。 |
milestone |
yes | 添加此自定义能力的里程碑。 |
admin_ability |
no | 布尔值,指示此能力是否在管理员级别进行检查。 |
group_ability |
yes | 布尔值,指示此能力是否在组级别进行检查。 |
enabled_for_group_access_levels |
if group_ability = true |
已经在组中访问此自定义能力的访问级别数组。有关帮助确定能力的基本访问级别,请参阅理解单个能力逻辑部分。这仅供参考,对自定义角色的运行方式没有影响。 |
project_ability |
yes | 布尔值,指示此能力是否在项目级别进行检查。 |
enabled_for_project_access_levels |
if project_ability = true |
已经在项目中访问此自定义能力的访问级别数组。有关帮助确定能力的基本访问级别,请参阅理解单个能力逻辑部分。这仅供参考,对自定义角色的运行方式没有影响。 |
requirements |
no | 此能力依赖的自定义权限列表。例如 admin_vulnerability 依赖于 read_vulnerability。如果没有,则输入 [] |
available_from_access_level |
no | 此能力可用的预定义角色的访问级别(如果适用)。有关帮助确定能力的基本访问级别,请参阅理解单个能力逻辑部分。这仅供参考,对自定义角色的运行方式没有影响。 |
第 2 步:创建规范文件并更新验证架构
- 运行
bundle exec rails generate gitlab:custom_roles:code --ability <ABILITY_NAME>,这将更新权限验证架构文件并创建一个空的规范文件。
第 3 步:创建功能标志(可选)
- 如果您想使用功能标志切换自定义能力,请创建一个名称为
custom_ability_<name>的功能标志。例如,对于能力read_code,功能标志将是custom_ability_read_code。当此功能标志被禁用时,创建新自定义角色时将隐藏自定义能力,或者获取用户自定义能力时也会隐藏。
第 4 步:更新策略
- 如果能力在组级别进行检查,请向 GroupPolicy 添加规则以启用该能力。
- 例如:如果我们想要添加的能力是
read_dependency,那么对ee/app/policies/ee/group_policy.rb的更新将如下所示:
rule { custom_role_enables_read_dependency }.enable(:read_dependency)- 类似地,如果能力在项目级别进行检查,请向 ProjectPolicy 添加规则以启用该能力。
- 例如:如果我们想要添加的能力是
read_dependency,那么对ee/app/policies/ee/project_policy.rb的更新将如下所示:
rule { custom_role_enables_read_dependency }.enable(:read_dependency)- 并非所有能力都需要在两个级别上启用,例如
admin_terraform_state允许用户管理项目的 terraform 状态。它只需要在项目级别启用,而不需要在组级别启用,因此只需要在ee/app/policies/ee/project_policy.rb中配置。
第 5 步:验证角色访问
- 使用
GITLAB_SIMULATE_SAAS=1确保 SaaS 模式已启用。 - 转到您拥有者身份的任何组,然后转到
设置 -> 角色和权限。 - 选择
新角色并使用您刚刚创建的权限创建自定义角色。 - 转到该组的
管理 -> 成员页面,并将一个成员分配给这个新创建的自定义角色。 - 接下来,以该成员身份登录,并确保您能够访问自定义能力旨在访问的页面。
第 6 步:评估对高级搜索的影响
如果能力影响高级搜索索引的数据,自定义角色可能会影响高级搜索功能。
- 启用高级搜索并索引实例
- 以分配了自定义角色的成员身份登录任何组
- 通过导航到
搜索或前往...执行全局搜索。输入搜索词并选择在所有 GitLab中搜索。 - 验证用户可以搜索受自定义角色影响的数据
- 通过导航到组页面然后
搜索或前往...执行组搜索。输入搜索词并选择在组中搜索。 - 验证用户可以搜索受自定义角色影响的数据
- 如需要,更新搜索授权
第 7 步:添加规范
- 将该能力作为特征添加到
MemberRoles工厂中,ee/spec/factories/member_roles.rb。 - 将测试添加到
ee/spec/requests/custom_roles/<ABILITY_NAME>/request_spec.rb,以确保一旦用户被分配了自定义能力,他们就可以成功访问控制器、REST API 端点和 GraphQL API 端点。 - 以下是测试 Rails 控制器端点所需典型设置的示例。
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :in_group) }
let_it_be(:role) { create(:member_role, :guest, :custom_permission, namespace: project.group) }
let_it_be(:membership) { create(:project_member, :guest, member_role: role, user: user, project: project) }
before do
stub_licensed_features(custom_roles: true)
sign_in(user)
end
describe MyController do
describe '#show' do
it 'allows access' do
get my_controller_path(project)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
end
end- 以下是测试 GraphQL 变量所需典型设置的示例。
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :in_group) }
let_it_be(:role) { create(:member_role, :guest, :custom_permission, namespace: project.group) }
let_it_be(:membership) { create(:project_member, :guest, member_role: role, user: user, project: project) }
before do
stub_licensed_features(custom_roles: true)
sign_in(user)
end
describe MyMutation do
include GraphqlHelpers
describe '#show' do
let(:mutation) { graphql_mutation(:my_mutation) }
it_behaves_like 'a working graphql query'
end
end- 为
ProjectPolicy和/或GroupPolicy添加测试。以下是测试与ProjectPolicy相关更改的示例。
context 'for a member role with read_dependency true' do
let(:member_role_abilities) { { read_dependency: true } }
let(:allowed_abilities) { [:read_dependency] }
it_behaves_like 'custom roles abilities'
end- 如果需要,为受影响的范围添加高级搜索权限测试
第 8 步:更新文档
遵循为 GitLab 文档做贡献页面,对文档进行以下更改:
- 通过运行
bundle exec rake gitlab:custom_roles:compile_docs更新自定义能力列表 - 通过运行
bundle exec rake gitlab:graphql:compile_docs更新 GraphQL 文档
权限提升考虑
基础角色通常具有允许在与该工件交互时创建或管理对应于基础角色的工件的权限。例如,当 开发者 为项目创建访问令牌时,该令牌被创建时编码了 开发者 访问权限。重要的是要注意,随着新自定义权限的创建,在与 GitLab 工件交互时可能存在权限提升的风险,应添加适当的保护措施或基础角色检查。
消耗席位
如果将具有 访客 角色的新用户添加到包含启用不在 CUSTOMIZABLE_PERMISSIONS_EXEMPT_FROM_CONSUMING_SEAT 数组中的能力的成员角色中,则会消耗一个席位。我们只是想确保我们对具有"提升"能力的访客用户收取 Ultimate 客户费用。这仅适用于 SaaS 上的计费用户(计入命名空间订阅的计费用户)。有关此主题的更多详细信息,请参阅此问题。
模块化策略
为了支持GitLab 模块化单体设计文档,授权组正在与创建:IDE 组合作。一旦实施概念验证,将讨论这些发现,授权组将决定策略的模块化设计将如何进行。