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 thedeclarative_policygem. - 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 flagorbit_redaction_decision_reuse(gitlab_com_derisk, default off).
The MR has four parts.
- Gem 2.2.0 and less code.
declarative_policy2.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. - Shared preloaders fix their own gaps.
ProjectRootAncestorPreloadernow handles Arrays and sets the root onproject.group.NamespaceRootAncestorPreloadershares one root object per root and keeps every input object. The EEProjectPolicyPreloadergets opt-in custom roles and anassociations: falsemode, and preloadsinvited_groupsonly 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 sharedCi::Preloaders::RunnerPolicyPreloader, which now also preloads member roles in EE. - Small app-wide wins. The work item type provider computes
tasks_on_boardsonly when a board reads it.Issue#has_widget?checks only the requested widget.WidgetDefinition#widget_classis memoized.Authz::Rolelookup and the member access key prefix are memoized.GroupPolicyandProjectPolicycheck namespace bans only for users who have one.ProjectPolicyskips job token, security policy bot and ban rules for users that cannot match them. Alib/gitlab/patchprepend memoizesDeclarativePolicy::Runner#scoreuntil the policy cache or scope changes. This cuts the re-scoring of long merged step lists for delegating policies (issue, work item, note, pipeline). - Decision reuse for project
read_code.Authz::ProjectReadCodeDecisionsgroups 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 reachesread_code. Rollout needs a sampled shadow mode that compares reused and full decisions in production.
Base: master at 1ee957c5.
References
- Decision reuse
- Runner score memo
- Root ancestor preloader
- EE project policy preloader
- EE redaction service
- Feature flag
- Gem release: declarative-policy 2.2.0 with !85, !86, !87.
- Issue: #630828.
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.
- 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.
- 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. - 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.