Help us learn about your current experience with the documentation. Take the survey.

用户贡献映射开发者文档

用户贡献映射是一项功能,它允许将导入的记录归属于某个用户,而无需源用户提前配置公开邮箱。相反,系统会创建虚拟的 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

如何在导入器中实现用户映射

  1. 在导入器中为用户映射实现一个功能开关。

  2. 确保在导入开始时存储功能开关状态,这样在导入过程中更改开关状态不会影响正在进行的导入。

  3. 在将用户贡献保存到数据库之前,使用 Gitlab::Import::SourceUserMapper#find_or_create_source_user 来查找正确的 User 以将贡献归属于该用户。

  4. 将映射后的用户保存贡献到数据库。

  5. 记录持久化后,初始化 Import::PlaceholderReferences::PushService 并执行它,将初始化的 Import::SourceUserPlaceholderReference 推送到 Redis。使用 from_record 初始化服务通常最方便。

  6. 使用 LoadPlaceholderReferencesWorker 异步持久化缓存的 Import::SourceUserPlaceholderReference。该工作器使用 Import::PlaceholderReferences::LoadService 来持久化占位引用。最好在导入过程中定期调用此工作器,例如在阶段结束时以及导入结束时。

    • 重要: 占位用户引用在加载前被缓存,以避免对 import_source_user_placeholder_references 表进行过多并发写入。如果数据库记录引用了占位用户的 ID 但由于某种原因占位引用未被持久化,则贡献无法被重新分配,占位用户可能不会被删除
  7. 延迟完成导入,直到所有缓存的占位引用都已加载。

    • 并行第三方导入器通过在项目中仍有占位引用留在 Redis 时重新排队 FinishImportWorker 来实现这一点。参见 Gitlab::GithubImport::Stage::FinishImportWorker 作为示例。
    • 同步第三方导入器必须以其他方式等待占位引用完成加载。例如,Gitea 导入器使用 Kernel.sleep来延迟导入,直到占位用户引用被加载。
    • 直接传输的功能类似于并行导入器,使用 BulkImport::ProcessService 重新排队 BulkImportWorker,如果占位用户未完成加载,则延迟迁移完成。

导入后的占位用户重新分配

占位用户重新分配在导入完成后进行。无论创建占位的导入类型如何,所有占位用户的重新分配过程都是相同的。

重新分配流程

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_confirmationImport::UserMappingEnterpriseBypassAuthorizer 模块确定是否满足所有条件。

用户贡献重新分配

当真实用户接受重新分配时,开始将所有对占位用户 ID 的外键引用替换为重新分配用户 ID 的过程:

  1. Import::SourceUsers::AcceptReassignmentService 异步调用 Import::ReassignPlaceholderUserRecordsWorker

  2. 该工作器执行 Import::ReassignPlaceholderUserRecordsService,通过查询属于源用户的占位引用和占位成员资格,将对占位用户 ID 的外键引用替换为重新分配用户的 ID。

  3. 服务在成功重新分配后删除占位引用和占位成员资格。

  4. 服务将源用户的状态设置为 complete

    • 注意: 存在有效场景,占位引用可能无法重新分配。例如,如果用户被添加为合并请求的审查者,而该合并请求已有占位用户审查者,然后用户接受了对该占位用户的重新分配。这会在贡献重新分配期间引发 ActiveRecord::RecordNotUnique 错误,但这是有效场景。
    • 注意: 由于未处理的错误,重新分配可能失败。我们需要调查问题,因为重新分配应该总是成功的。
  5. 服务完成后,工作器异步调用 Import::DeletePlaceholderUserWorker 删除占位用户。如果占位用户 ID 仍被任何导入表引用,则不会被删除。检查 AliasResolver 中的 columns_ignored_on_deletion 以了解例外情况。

  6. 如果占位用户在未获得重新分配用户确认的情况下被重新分配,会发送邮件给重新分配的用户,通知他们已被重新分配。

占位引用别名化

将模型名称和列名称保存到 import_source_user_placeholder_references 表是脆弱的。实际的模型和列名称可能会更改,但没有东西可以更新当前存储旧名称的占位记录。与其将占位引用的 modelnumeric_keycomposite_key 视为真实名称,不如将它们视为别名。Import::PlaceholderReferences::AliasResolver 用于将这些属性中存储的值映射到真实的模型和列名称。

何时需要更新 Import::PlaceholderReferences::AliasResolver

如果因为 MissingAlias 错误被引导至此,则必须使用缺少的别名更新 Import::PlaceholderReferences::AliasResolver

如果您主动进行此更改,Import::PlaceholderReferences::AliasResolver 只需要在以下情况下更新:

  • 您正在处理的模型至少被一个实现用户贡献映射的导入器导入。
  • 您模型上的列引用了 User ID。即该列是引用 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 中的新模型,只需要被至少一个实现用户贡献映射的导入器导入。

示例

直接传输更新为导入 TodoTodo 有两个用户引用列,user_idauthor_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 中以避免性能问题。