Restrict Duo Agent Platform job artifacts to Developer and above

What does this MR do and why?

Follow-up to !259995 (merged), which restricted Duo Agent Platform (DAP) job logs to Developer and above.

DAP flow jobs can also upload report artifacts. For example, the bl_security flow uploads a SAST report. GitLab generates the workload CI config and never sets artifacts:access, so these artifacts default to public. On public projects, anonymous users can download them.

This MR applies the same Developer+ rule to DAP job artifacts, using the same _read_agent_build permission that gates their logs:

  • Ci::BuildPolicy prevents :read_job_artifacts for duo_workflow jobs without _read_agent_build. This covers REST and the job-level checks.
  • Ci::JobArtifactPolicy gets the same rule, because the web download checks the artifact itself. The existing private-artifact rule never applied here: Ci::Build#artifacts_public? is true for jobs with no archive, and DAP jobs only upload reports.
  • The existing Security Manager exception for security reports is kept for artifact-level checks (see Impact).
  • The policy runs on every read, so artifacts of jobs that already ran are covered too.

Supporting changes:

  • Ci::Build#test_report_readable_by? memoizes the artifact check. Its cache key now includes the pipeline, because the new condition reads the pipeline source. Ci::Pipeline#accessible_test_report_build_ids needs no key change, because child pipelines never have the duo_workflow source.
  • The new condition reads each artifact's job pipeline, which caused one query per job on the pipeline and MR artifact lists. A new Ci::JobArtifact.preload_job_pipeline scope preloads it in Ci::DownloadableArtifactEntity and MergeRequests::PipelineEntity.
  • The test report path now also preloads the job pipeline for the artifacts it checks.

Impact

  • Anonymous users, Guests, Planners and Reporters can no longer download artifacts of DAP jobs. Jobs from other pipeline sources are unchanged.
  • Security Managers can download DAP security reports from the web UI, but not through GET /projects/:id/jobs/:job_id/artifacts. The REST endpoint checks Ci::BuildPolicy first, which can't see the file type. Jobs with private archives already behave the same way. Relaxing Ci::BuildPolicy for them isn't safe, because some routes only check the build.

References

Database queries

preload_job_pipeline adds one batched primary-key lookup per artifact list, replacing one pipeline query per job. Plans are from gitlab-production-ci (Database Lab) using two gitlab-org/gitlab pipelines.

Ci::DownloadableArtifactEntity (one pipeline):

SELECT "p_ci_pipelines".* FROM "p_ci_pipelines" WHERE "p_ci_pipelines"."partition_id" = 116 AND "p_ci_pipelines"."id" = 2940460198
Index Scan using ci_pipelines_116_pkey on gitlab_partitions_dynamic.ci_pipelines_116 p_ci_pipelines  (cost=0.56..3.58 rows=1 width=399) (actual time=2.298..2.301 rows=1 loops=1)
  Index Cond: ((p_ci_pipelines.id = '2940460198'::bigint) AND (p_ci_pipelines.partition_id = 116))
  Buffers: shared hit=4 read=1

Time: 2.385 ms execution, 5 buffers. Plan

MergeRequests::PipelineEntity (parent and child pipelines, same partition):

SELECT "p_ci_pipelines".* FROM "p_ci_pipelines" WHERE "p_ci_pipelines"."partition_id" = 116 AND "p_ci_pipelines"."id" IN (2940460198, 2940457167)
Index Scan using ci_pipelines_116_pkey on gitlab_partitions_dynamic.ci_pipelines_116 p_ci_pipelines  (cost=0.56..6.41 rows=2 width=399) (actual time=1.488..1.513 rows=2 loops=1)
  Index Cond: ((p_ci_pipelines.id = ANY ('{2940460198,2940457167}'::bigint[])) AND (p_ci_pipelines.partition_id = 116))
  Buffers: shared hit=9 read=1

Time: 1.593 ms execution, 10 buffers. Plan

When the pipelines are loaded per partition, Rails can generate the OR form instead:

SELECT "p_ci_pipelines".* FROM "p_ci_pipelines" WHERE ("p_ci_pipelines"."id" = 2940460198 AND "p_ci_pipelines"."partition_id" = 116 OR "p_ci_pipelines"."id" = 2940457167 AND "p_ci_pipelines"."partition_id" = 116)
Bitmap Heap Scan on gitlab_partitions_dynamic.ci_pipelines_116 p_ci_pipelines  (cost=4.15..7.19 rows=2 width=399) (actual time=0.034..0.037 rows=2 loops=1)
  ->  BitmapOr
        ->  Bitmap Index Scan on ci_pipelines_116_pkey  (actual rows=1)
        ->  Bitmap Index Scan on ci_pipelines_116_pkey  (actual rows=1)
  Buffers: shared hit=10

Time: 0.037 ms execution, 10 buffers. Plan

All three use the (id, partition_id) primary key and read a single partition.

The test report summary (Ci::Pipeline#accessible_test_report_build_ids) also preloads pipelines for its representative artifacts, using the same query shape.

Query count: before the preload, the artifact lists ran one pipeline query per job for users below Developer. With it, the number of pipeline queries stays constant as jobs are added (covered by the serializer specs).

How to set up and validate locally

  1. In a Rails console, make a project public with public pipelines:

    project = Project.find_by_full_path('<namespace>/<project>')
    project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC, public_builds: true)
  2. Create a duo_workflow job with a SAST report artifact (no runner needed):

    definition = Ci::Workloads::WorkloadDefinition.new do |d|
      d.image = 'alpine'
      d.commands = ['echo hello']
    end
    
    workload = Ci::Workloads::RunWorkloadService.new(
      project: project, current_user: User.find_by_username('root'),
      source: :duo_workflow, workload_definition: definition
    ).execute.payload
    
    build = workload.pipeline.builds.first
    build.success!
    report = Tempfile.new(['gl-sast-report', '.json']).tap { |f| f.write('{"version":"15.2.1","vulnerabilities":[]}'); f.rewind }
    Ci::JobArtifact.create!(job: build, project: project, file_type: :sast, file_format: :raw, file: report)
    
    puts "#{Gitlab::Routing.url_helpers.project_job_url(project, build)}/artifacts/download?file_type=sast"
  3. Open the URL in a private window (signed out). The download is refused.

  4. Sign in as a Reporter of the project. The download is refused.

  5. Sign in as a Developer. The SAST report downloads.

  6. Control: a SAST artifact on a job from a normal push pipeline is still downloadable while signed out.

Testing notes

As DAP download Push download
Signed out Refused Downloads
dawne (Reporter) Refused Downloads
hedwig (Developer) Downloads Downloads

Not signed in

Pasted Graphic.png

Impersonating a reporter

404 Page not found.png

Impersonating a developer

Pasted Graphic 3.png

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.

Edited by Charlie Linacre

Merge request reports

Loading
Loading