Add Ci::Preloaders::JobPolicyPreloader for pipeline job policy checks
What does this MR do and why?
Adds Ci::Preloaders::JobPolicyPreloader, a class that batch-loads everything Ci::BuildPolicy and Ci::DeployablePolicy read when checking access for a page of pipeline jobs. Nothing calls it yet; this MR only adds the class and its specs.
Ci::Preloaders::JobPolicyPreloader.new(jobs, user).execute takes a page mixing Ci::Build, Ci::Bridge, and generic commit statuses, plus the current user, and loads: job_definition; job_environment with its environment; deployment with its environment; the environments' last_deployment; and, for bridges, the downstream pipeline's project with the user's access level there. For playable manual bridges whose trigger path is a literal string rather than a variable reference, it also resolves the downstream project by that path and assigns it through the new Ci::Bridge#downstream_project=. It assigns persisted_environment on each job directly, so environment jobs skip resolving it by name.
Jobs reached through the pipeline's association already hold the pipeline and its project, so the preloader passes that project to Rails' preloader as an available_records entry: every job, and every environment the preloader assigns a project to, points at the instance the resolver already has, with no projects query. One environment row is one Ruby object shared by every job on the page that deploys to it, so the row is loaded once rather than once per job. Sharing environment instances this way is why the previous MR in this split had to stop approval summaries from writing deployment-specific state into shared approval rules.
The EE extension overrides a preload_protected_environments hook to load protected environments with their deploy access levels and approval rules, and each deployment's approvals, where the project's licence allows. This EE preload runs before bridges are checked for playability, because EE's Ci::Bridge#playable? reads deployment approvals to see whether a deployment is waiting on one. Running it later would leave that check issuing its own query per bridge instead of reusing the batch-loaded data.
Wiring this preloader into the GraphQL CiJob type, behind the feature flag batch_pipeline_job_policy_checks, is a separate follow-up MR. That follow-up is what remains of the original draft MR once this and the other pieces are split out of it.
Bundled fix: ProtectedEnvironmentPreloader dropped ancestor-group rules
A reviewer found a pre-existing bug in Preloaders::Environments::ProtectedEnvironmentPreloader, which this MR's EE extension now depends on. It built its group-level lookup of ProtectedEnvironment rows with index_by(&:name). ProtectedEnvironment.for_environments returns one row per ancestor group of the project, and the unique index is (group_id, name), not name alone, so a parent group and a subgroup can both protect the same tier. index_by kept only one of the two rows.
Environment#protected_from? is true when any associated protected environment denies the user, so dropping the parent group's row made the check more permissive than intended: a subgroup developer could pass a check that the parent group's maintainers-only rule should have blocked. That contradicts the documented behaviour that a subgroup cannot override a parent group's protected environment. The non-preloaded path, ProtectedEnvironment.for_environment, keeps all matching rows and does not have the bug.
The group-level lookup now uses group_by(&:name). The project-level one keeps index_by: (project_id, name) is unique and only one project is ever in scope. The call site already does << followed by .flatten.compact, so the array value needs no other change. Two spec examples cover a tier protected in both a root group and a subgroup; both fail on index_by and pass on group_by.
This is user-visible — the check becomes stricter in the parent+subgroup case — so the commit carries Changelog: fixed and EE: true. It is fixed here rather than in a follow-up because until now the helper only fed environment views, and this MR is what first makes pipeline job permission checks such as can?(:play_job) read the same data.
One nuance: with several ancestor-group rows, the order of associated_protected_environments follows the union query rather than being fixed. protected_from?, protected_by? and required_approval_count do not depend on order, and find_approval_rule_for returns the first match. The non-preloaded path has the same property, so this is parity with it rather than new behaviour.
Database review notes. New query shapes this preloader issues:
job_environments WHERE ci_job_id IN (...)deployments WHERE deployable_id IN (...) AND deployable_type = 'CommitStatus'deploymentsUNION of per-environmentLIMIT 1queries (existing preloader, unchanged shape)projects WHERE id IN (...)(downstream projects of bridges only; the pipeline's own project is reused from memory)projects JOIN project_authorizations ... WHERE projects.id IN (...)(UserMaxAccessLevelInProjectsPreloader, downstream projects of bridges)- EE:
protected_environmentsfor the page's environments,protected_environment_deploy_access_levels WHERE protected_environment_id IN (...), andprotected_environment_approval_rules WHERE protected_environment_id IN (...) - EE:
deployment_approvals WHERE deployment_id IN (...)for the page's deployments
These replace per-row LIMIT 1 lookups on the same indexed columns once the preloader is wired in. Query plans are not attached yet.
References
- Related to #629310
- Rollout issue: #629426
- Part of splitting !255978 (merged), which becomes the wiring MR
- Bundled fix reported in review: !256453 (comment 3887269525)
Screenshots or screen recordings
No UI change. Nothing calls this preloader yet.
How to set up and validate locally
Run the specs:
bin/rspec spec/models/ci/preloaders/job_policy_preloader_spec.rb ee/spec/models/ee/ci/preloaders/job_policy_preloader_spec.rb spec/models/ci/bridge_spec.rb ee/spec/models/preloaders/environments/protected_environment_preloader_spec.rbSince nothing calls the preloader yet, these specs exercise it directly rather than through a page load.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.