Draft: [skip ci] Cut Orbit redaction authz cost for every resource type

What does this MR do and why?

Orbit sends POST /api/v4/internal/orbit/redaction with up to 1,000 resource ids per type. Authz::RedactionService runs one policy check per id and answers allowed or denied. This MR cuts the CPU and SQL of that batch for every resource type Orbit sends. Most changes fix shared code, so other callers of the same preloaders and policies gain too.

Terms:

  • Policy check: one Ability.allowed? call through the declarative_policy gem.
  • Preseed: loading the rows a batch of policy checks will read, once, before the checks run.
  • Decision reuse: for project read_code, one policy check per group of projects that share the same authorization facts. Behind the flag orbit_redaction_decision_reuse (gitlab_com_derisk, default off).

The MR has four parts.

  1. Gem 2.2.0 and less code. declarative_policy 2.2.0 caches each policy class's combined declarations in the process. The class-map memo from !257946 (merged) is redundant and is deleted with its initializer prepend and specs.
  2. Shared preloaders fix their own gaps. ProjectRootAncestorPreloader now handles Arrays and sets the root on project.group. NamespaceRootAncestorPreloader shares one root object per root and keeps every input object. The EE ProjectPolicyPreloader gets opt-in custom roles and an associations: false mode, and preloads invited_groups only for users with namespace bans. The redaction service drops its own copies of root, SAML, plan, IP restriction, invited group and custom role preloads and calls the shared preloaders instead. Runners go through the shared Ci::Preloaders::RunnerPolicyPreloader, which now also preloads member roles in EE.
  3. Small app-wide wins. The work item type provider computes tasks_on_boards only when a board reads it. Issue#has_widget? checks only the requested widget. WidgetDefinition#widget_class is memoized. Authz::Role lookup and the member access key prefix are memoized. GroupPolicy and ProjectPolicy check namespace bans only for users who have one. ProjectPolicy skips job token, security policy bot and ban rules for users that cannot match them. A lib/gitlab/patch prepend memoizes DeclarativePolicy::Runner#score until the policy cache or scope changes. This cuts the re-scoring of long merged step lists for delegating policies (issue, work item, note, pipeline).
  4. Decision reuse for project read_code. Authz::ProjectReadCodeDecisions groups projects by their authorization facts in one SQL query and runs the full check on one project per group. IP-restricted projects, non-human users, admins, auditors and banned users always take the full path. A rule-tree pin spec fails when a new condition reaches read_code. Rollout needs a sampled shadow mode that compares reused and full decisions in production.

Base: master at 1ee957c5.

References

Results

Licensed GDK bench, GITLAB_SIMULATE_SAAS=1, Ultimate on all bench roots. Thread CPU per id, allocated KB per id and SQL per batch, medians. User 133 is a human bench user. User 105 is the Duo service account, which decision reuse never admits.

Project read_code, 1,000 ids. Before is master with gem 2.1.0 and the !257946 (merged) memo.

Scenario User Before ms/id Flag off ms/id Flag on ms/id KB/id before / off / on SQL before / off / on
2 roots 133 0.889 0.356 0.033 79.3 / 32.8 / 3.0 122 / 15 / 17
2 roots 105 2.423 0.383 0.378 161.5 / 37.5 / 37.5 1064 / 15 / 15
300 roots 133 1.796 0.510 0.032 154.1 / 56.8 / 2.7 770 / 15 / 17
300 roots 105 3.104 0.533 0.536 232.4 / 61.0 / 61.0 1783 / 15 / 15

Other types Orbit sends, user 133, 500 ids (250 runners). Before is the previous draft head 1a0af474. That tree runs gem 2.1.0 with the memo and the two micro memos. Weighted by production ids per week, CPU per id drops 2.9x and allocated memory per id drops 2.8x.

Type Prod ids per week Before ms/id After ms/id KB/id before / after SQL before / after CPU
merge_request 36.8k 1.041 0.700 85.5 / 54.0 25 / 22 1.5x
work_item 33.7k 8.205 2.283 395.9 / 104.2 561 / 28 3.6x
group 17.6k 0.760 0.602 76.1 / 64.7 64 / 63 1.3x
vulnerability 17.6k 2.942 0.952 203.8 / 60.8 530 / 19 3.1x
note 15.6k 9.181 3.089 522.0 / 143.4 594 / 34 3.0x
user 8.5k 0.105 0.094 16.5 / 14.7 2 / 2 1.1x
label 8.5k 0.801 0.544 75.9 / 53.5 30 / 25 1.5x
milestone 8.5k 0.757 0.504 74.0 / 51.4 30 / 22 1.5x
ci_build 3.4k 0.967 0.627 80.1 / 47.6 24 / 19 1.5x
ci_pipeline 2.4k 1.841 0.950 115.0 / 59.7 24 / 19 1.9x
ci_runner 0.8k 11.356 2.435 652.9 / 170.5 1540 / 112 4.7x

Group stays at 1.3x. Its remaining cost is GroupPolicy#has_projects, one query per denied group. A batched version must reproduce the GroupProjectsFinder filters and is out of scope. Merge request, label, milestone, build and pipeline had no N+1. Their remaining cost is the policy runner itself.

App-wide. On a dev VM with gem 2.2.0, the score memo and the work item type fix were profiled on nine endpoints. GraphQL group projects with permissions drop about 15% CPU. GraphQL issues and MRs drop about 8%, and issue discussions about 5%. Project show, MR show and the REST issue list move within noise.

How to set up and validate locally

Every equivalence check reports 0 differing decisions. The proof compares this branch with its own tree before each change, and with master.

  1. Ability dump for five users: the bench user, an admin, anonymous, a guest with a custom role and a banned user. Subjects are projects, groups, issues, work items, MRs, runners, notes, labels, milestones, vulnerabilities and global: 107,240 decisions.
  2. Service against a naive per-record Ability.allowed? for 13 types and 4 users, plain and licensed. Fixtures add an IP restriction, SAML, a namespace ban and a custom role: 52,236 project decisions and 29,368 other-type decisions.
  3. Decision reuse flag off against flag on, with mutations and five token contexts: 68,484 and 44,740 decisions.

Specs, each file run alone:

Spec files Examples Failures
CE and EE ProjectPolicy 6,048 0
CE and EE GroupPolicy 6,150 0
Issue, note, work item and runner policies 706 0
Redaction service (CE, EE) and decision reuse 159 0
Root ancestor, project policy, runner policy preloaders 58 0
Work item type framework (CE, EE) 313 0
Runner score memo, Ability, SSO enforcer 305 0

RuboCop reports no offenses on the changed files.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Michael Angelo Rivera

Merge request reports

Loading
Loading