用户贡献映射开发者文档
用户贡献映射是一项功能,它允许将导入的记录归属于某个用户,而无需源用户提前配置公开邮箱。相反,系统会创建虚拟的 User 记录作为占位符,在导入完成后,当真实用户被分配到这些贡献时,这些占位符才会被替换。
用户贡献映射在每个导入器中实现,用于在迁移过程中将贡献分配给占位用户,但所有使用此映射的导入器都遵循相同的流程。组所有者在迁移完成后重新分配真实用户到占位用户的过程,与迁移过程是分开的。
术语表和相关模型
| 术语 | 对应的 ActiveRecord 模型 |
定义 |
|---|---|---|
| 源用户 | Import::SourceUser |
将占位用户映射到真实用户,并跟踪重新分配详情、导入源和顶级组关联。 |
| 占位用户 | user_type: 'placeholder' 的 User |
在迁移过程中满足外键约束的 User 记录,计划在导入后重新分配给真实用户。占位用户无法登录,在 GitLab 中没有任何权限。 |
| 分配用户、真实用户 | user_type: 'human' 的 User |
被分配给占位用户的真实用户。 |
| 用户贡献 | 任何 GitLab ActiveRecord 模型 | 迁移过程中导入的任何属于 User 的 ActiveRecord 模型。例如合并请求分配人、备注、成员资格等。 |
| 占位引用 | Import::SourceUserPlaceholderReference |
一个单独的模型,用于跟踪数据库中所有占位用户的贡献,不包括成员资格。 |
| 占位成员资格 | Import::Placeholders::Membership |
一个单独的模型,用于跟踪属于占位用户的导入成员资格。迁移过程中不会为占位用户创建 Member 记录,以防止占位用户显示为成员。 |
| 导入用户 | Import::NamespaceImportUser |
当记录无法分配给常规占位用户时使用的占位用户。例如,当达到占位用户限制时。 |
| 占位详情 | Import::PlaceholderUserDetail |
一个记录,用于跟踪哪些命名空间有占位用户,以便在删除顶级组时删除占位用户。 |
| 占位用户表 | N/A | 占位用户表,组所有者可以在 UI 中选择真实用户分配给占位用户。位于顶级组的成员页面的占位符标签下。仅对组所有者可见。 |
导入过程中的占位用户创建
在占位用户可以被重新分配给真实用户之前,必须在导入过程中创建占位用户。
导入贡献的用户映射分配流程
flowchart
%%% nodes
Start{{
组或项目
迁移已开始
}}
FetchInfo[
获取贡献的
信息
]
FetchUserInfo[
获取与贡献关联的
用户信息
]
CheckSourceUser{
目标实例中是否已有用户
接受映射到源用户?
}
AssignToUser[
将贡献分配给用户
]
PlaceholderLimit{
命名空间已达到
占位用户限制?
}
CreatePlaceholderUser[
创建占位用户并
保存源用户详情
]
AssignContributionPlaceholder[
将贡献分配给
占位用户
]
AssignImportUser[
将贡献分配给
ImportUser 并保存源用户
详情
]
ImportContribution[
将贡献保存
到数据库
]
PushPlaceholderReference[
将占位引用实例
推送到 Redis
]
LoadPlaceholderReference(((
将占位引用加载
到数据库
)))
%%% connections
Start --> FetchInfo
FetchInfo --> FetchUserInfo
FetchUserInfo --> CheckSourceUser
CheckSourceUser -- Yes --> AssignToUser
CheckSourceUser -- No --> PlaceholderLimit
PlaceholderLimit -- No --> CreatePlaceholderUser
CreatePlaceholderUser --> AssignContributionPlaceholder
PlaceholderLimit -- Yes --> AssignImportUser
AssignToUser-->ImportContribution
AssignContributionPlaceholder-->ImportContribution
AssignImportUser-->ImportContribution
ImportContribution-->PushPlaceholderReference
PushPlaceholderReference-->LoadPlaceholderReference
如何在导入器中实现用户映射
-
在导入器中为用户映射实现一个功能开关。
-
确保在导入开始时存储功能开关状态,这样在导入过程中更改开关状态不会影响正在进行的导入。
- 第三方导入器通过在导入开始时设置
project.import_data[:data][:user_contribution_mapping_enabled]来实现这一点。参见Gitlab::GithubImport::Settings或Gitlab::BitbucketServerImport::ProjectCreator作为示例。 - 直接传输使用
BulkImports::CreateService中的EphemeralData来缓存功能开关状态。
- 第三方导入器通过在导入开始时设置
-
在将用户贡献保存到数据库之前,使用
Gitlab::Import::SourceUserMapper#find_or_create_source_user来查找正确的User以将贡献归属于该用户。 -
将映射后的用户保存贡献到数据库。
-
记录持久化后,初始化
Import::PlaceholderReferences::PushService并执行它,将初始化的Import::SourceUserPlaceholderReference推送到 Redis。使用from_record初始化服务通常最方便。 -
使用
LoadPlaceholderReferencesWorker异步持久化缓存的Import::SourceUserPlaceholderReference。该工作器使用Import::PlaceholderReferences::LoadService来持久化占位引用。最好在导入过程中定期调用此工作器,例如在阶段结束时以及导入结束时。- 重要: 占位用户引用在加载前被缓存,以避免对
import_source_user_placeholder_references表进行过多并发写入。如果数据库记录引用了占位用户的 ID 但由于某种原因占位引用未被持久化,则贡献无法被重新分配,占位用户可能不会被删除。
- 重要: 占位用户引用在加载前被缓存,以避免对
-
延迟完成导入,直到所有缓存的占位引用都已加载。
- 并行第三方导入器通过在项目中仍有占位引用留在 Redis 时重新排队
FinishImportWorker来实现这一点。参见Gitlab::GithubImport::Stage::FinishImportWorker作为示例。 - 同步第三方导入器必须以其他方式等待占位引用完成加载。例如,Gitea 导入器使用
Kernel.sleep来延迟导入,直到占位用户引用被加载。 - 直接传输的功能类似于并行导入器,使用
BulkImport::ProcessService重新排队BulkImportWorker,如果占位用户未完成加载,则延迟迁移完成。
- 并行第三方导入器通过在项目中仍有占位引用留在 Redis 时重新排队
导入后的占位用户重新分配
占位用户重新分配在导入完成后进行。无论创建占位的导入类型如何,所有占位用户的重新分配过程都是相同的。
重新分配流程
flowchart TD
%% Nodes
OwnerAssigns{{
组所有者请求将
源用户分配给
目标实例中的用户
}}
Notification[
通知已发送
给用户
]
ReceivesNotification[
用户收到通知并
点击按钮查看更多详情
]
ClickMoreDetails[
用户被重定向到
更多详情页面
]
CheckRequestStatus{
组所有者已取消
请求?
}
RequestCancelledPage(((
显示请求被
所有者取消的消息
)))
OwnerCancel(
组所有者选择取消
分配请求
)
ReassigmentOptions{
显示重新分配选项
}
ReassigmentStarts(((
开始重新分配
贡献
)))
ReassigmentRejected(((
显示请求被
用户拒绝的消息
)))
%% Edge connections between nodes
OwnerAssigns --> Notification --> ReceivesNotification --> ClickMoreDetails
OwnerAssigns --> OwnerCancel
ClickMoreDetails --> CheckRequestStatus
CheckRequestStatus -- Yes --> RequestCancelledPage
CheckRequestStatus -- No --> ReassigmentOptions
ReassigmentOptions -- 用户接受 --> ReassigmentStarts
ReassigmentOptions -- 用户拒绝 --> ReassigmentRejected
OwnerCancel-.->CheckRequestStatus
重新分配流程的每个步骤都对应一个源用户状态:
stateDiagram-v2 [*] --> pending_reassignment pending_reassignment --> reassignment_in_progress: 重新分配用户并绕过分配人确认 awaiting_approval --> reassignment_in_progress: 接受重新分配 reassignment_in_progress --> completed: 贡献重新分配成功完成 reassignment_in_progress --> failed: 重新分配贡献时出错 pending_reassignment --> awaiting_approval: 重新分配用户 awaiting_approval --> pending_reassignment: 取消重新分配 awaiting_approval --> rejected: 拒绝重新分配 rejected --> pending_reassignment: 取消重新分配 rejected --> keep_as_placeholder: 保持为占位符 pending_reassignment --> keep_as_placeholder: 保持为占位符
而不是直接在源用户上调用 state_machines-activerecord 方法,已经为每个状态转换实现了服务,以一致地处理验证和行为:
Import::SourceUsers::ReassignService: 启动重新分配给真实用户。如果启用了占位符确认绕过,它可能会开始用户贡献重新分配。Import::SourceUsers::AcceptReassignmentService: 处理用户接受重新分配并开始用户贡献重新分配。Import::SourceUsers::RejectReassignmentService: 处理用户拒绝重新分配。Import::SourceUsers::CancelReassignmentService: 在用户接受之前或拒绝之后取消待处理的重新分配请求。Import::SourceUsers::KeepAsPlaceholderService: 将单个用户标记为保持为占位符
还存在一些其他服务来处理批量请求和相关行为:
Import::SourceUsers::GenerateCsvService: 生成 CSV 文件以批量重新分配占位用户。Import::SourceUsers::BulkReassignFromCsvService: 处理来自 CSV 文件的批量重新分配,最终为 CSV 中的每个占位用户调用Import::SourceUsers::ReassignService。Import::SourceUsers::KeepAllAsPlaceholderService: 将所有未分配的占位用户标记为无限期保持为占位用户。Import::SourceUsers::ResendNotificationService: 重新发送重新分配通知邮件给重新分配的用户。
启用占位用户绕过时的重新分配流程
当占位符确认绕过启用时,用户贡献重新分配在重新分配时立即开始,无需真实用户确认。
flowchart LR
%% Nodes
OwnerAssigns{{
管理员或企业组所有者
请求将源用户分配给
目标实例中的用户
}}
ConfirmAssignmentWithBypass[
组所有者确认分配
无需分配人确认
]
ReassigmentStarts((
开始重新分配
贡献
))
NotifyAssignee(((
重新分配的真实用户
被通知贡献已重新分配
))
%% Edge connections between nodes
OwnerAssigns --> ConfirmAssignmentWithBypass --> ReassigmentStarts --> NotifyAssignee
只有在以下情况下才能绕过分配人的确认:
- 管理员在启用了应用程序设置
allow_bypass_placeholder_confirmation的 GitLab 自托管实例上重新分配占位用户。Import::UserMapping::AdminBypassAuthorizer模块确定是否满足所有条件。 - 企业组所有者在
.com上的企业内重新分配占位用户给真实用户,并启用了组设置allow_enterprise_bypass_placeholder_confirmation。Import::UserMappingEnterpriseBypassAuthorizer模块确定是否满足所有条件。
用户贡献重新分配
当真实用户接受重新分配时,开始将所有对占位用户 ID 的外键引用替换为重新分配用户 ID 的过程:
-
Import::SourceUsers::AcceptReassignmentService异步调用Import::ReassignPlaceholderUserRecordsWorker。 -
该工作器执行
Import::ReassignPlaceholderUserRecordsService,通过查询属于源用户的占位引用和占位成员资格,将对占位用户 ID 的外键引用替换为重新分配用户的 ID。 -
服务在成功重新分配后删除占位引用和占位成员资格。
-
服务将源用户的状态设置为
complete。- 注意: 存在有效场景,占位引用可能无法重新分配。例如,如果用户被添加为合并请求的审查者,而该合并请求已有占位用户审查者,然后用户接受了对该占位用户的重新分配。这会在贡献重新分配期间引发
ActiveRecord::RecordNotUnique错误,但这是有效场景。 - 注意: 由于未处理的错误,重新分配可能失败。我们需要调查问题,因为重新分配应该总是成功的。
- 注意: 存在有效场景,占位引用可能无法重新分配。例如,如果用户被添加为合并请求的审查者,而该合并请求已有占位用户审查者,然后用户接受了对该占位用户的重新分配。这会在贡献重新分配期间引发
-
服务完成后,工作器异步调用
Import::DeletePlaceholderUserWorker删除占位用户。如果占位用户 ID 仍被任何导入表引用,则不会被删除。检查 AliasResolver 中的columns_ignored_on_deletion以了解例外情况。 -
如果占位用户在未获得重新分配用户确认的情况下被重新分配,会发送邮件给重新分配的用户,通知他们已被重新分配。
占位引用别名化
将模型名称和列名称保存到 import_source_user_placeholder_references 表是脆弱的。实际的模型和列名称可能会更改,但没有东西可以更新当前存储旧名称的占位记录。与其将占位引用的 model、numeric_key 和 composite_key 视为真实名称,不如将它们视为别名。Import::PlaceholderReferences::AliasResolver 用于将这些属性中存储的值映射到真实的模型和列名称。
何时需要更新 Import::PlaceholderReferences::AliasResolver?
如果因为 MissingAlias 错误被引导至此,则必须使用缺少的别名更新 Import::PlaceholderReferences::AliasResolver。
如果您主动进行此更改,Import::PlaceholderReferences::AliasResolver 只需要在以下情况下更新:
- 您正在处理的模型至少被一个实现用户贡献映射的导入器导入。
- 您模型上的列引用了
UserID。即该列是引用users(id)的外键约束,或者该列建立了模型与User之间的关联。
如何在架构更改时更新 Import::PlaceholderReferences::AliasResolver
如果导入的模型发生更改,有几种情况必须更新 Import::PlaceholderReferences::AliasResolver。
添加了新的用户引用列并被导入
将新列键添加到模型别名的最新版本中。除非新列已填充了可能属于占位用户的用户 ID,否则不需要将新列添加到以前的版本中。
示例
在 snippets 表中添加了 last_updated_by_id,这是对 User ID 的引用。author_id 仍然存在且未更改。
"Snippet" => {
1 => {
model: Snippet,
- columns: { "author_id" => "author_id" }
+ columns: { "author_id" => "author_id", "last_updated_by_id" => "last_updated_by_id" }
}
},
用户引用列被重命名
为模型别名添加一个新版本,使用更新的列名。同时更新所有以前版本的列值,使用更新的列名,以确保尚未重新分配的占位引用将更新到正确的、最新的列名。
示例
snippets 表中的 author_id 被重命名为 user_id。注意 columns 中的键保持不变:
"Snippet" => {
1 => {
model: Snippet,
- columns: { "author_id" => "author_id" }
+ columns: { "author_id" => "user_id" }
+ },
+ 2 => {
+ model: Snippet,
+ columns: { "user_id" => "user_id" }
}
},
一段时间后,snippets 表中的 user_id 又更改为 created_by_id:
"Snippet" => {
1 => {
model: Snippet,
- columns: { "author_id" => "user_id" }
+ columns: { "author_id" => "created_by_id" }
},
2 => {
model: Snippet,
- columns: { "user_id" => "user_id" }
+ columns: { "user_id" => "created_by_id" }
+ },
+ 3 => {
+ model: Snippet,
+ columns: { "created_by_id" => "created_by_id" }
}
},
导入了具有用户贡献的新模型
为 ALIASES 哈希添加新模型的别名。模型不一定是 GitLab 中的新模型,只需要被至少一个实现用户贡献映射的导入器导入。
示例
直接传输更新为导入 Todo。Todo 有两个用户引用列,user_id 和 author_id:
},
+"Todo" => {
+ 1 => {
+ model: Todo,
+ columns: { "user_id" => "user_id", "author_id" => "author_id"}
+ }
+},
"Vulnerability" => {
具有用户贡献的模型被重命名
将模型视为新导入的模型,为 ALIASES 哈希添加别名。同时更新所有以前版本的模型值为新模型名称。不要删除旧别名,即使其下的旧模型名称不再存在。可能仍存在引用旧模型名称的未使用占位引用。
示例
Snippet 被重命名为 Sample:
+"Sample" => {
+ 1 => {
+ model: Sample,
+ columns: { "author_id" => "author_id" }
+ }
+},
"Snippet" => {
1 => {
- model: Snippet,
+ model: Sample,
columns: { "author_id" => "author_id" }
}
},
边缘情况示例
在 Snippet 重命名为 Sample 一段时间后,Snippet 模型被重新引入并导入,但用途完全不同。新的 Snippet 模型通过 user_id 属于一个 User。在这种情况下,不要更新 "Snippet" 别名的以前版本,因为任何带有 alias_version 1 的占位引用实际上是 Sample 的引用:
"Sample" => {
1 => {
model: Sample,
columns: { "author_id" => "author_id" }
}
},
"Snippet" => {
1 => {
model: Sample,
columns: { "author_id" => "author_id" }
},
+ 2 => {
+ model: Snippet,
+ columns: { "user_id" => "user_id" }
}
},
规范
Import::PlaceholderReferences::AliasResolver 中别名的更改通常不需要在其规范文件中单独测试。其规范设置用于验证每个别名版本指向现有列,并且未索引的列列在 columns_ignored_on_deletion 中以避免性能问题。