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::BuildPolicyprevents:read_job_artifactsforduo_workflowjobs without_read_agent_build. This covers REST and the job-level checks.Ci::JobArtifactPolicygets 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_idsneeds no key change, because child pipelines never have theduo_workflowsource.- 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_pipelinescope preloads it inCi::DownloadableArtifactEntityandMergeRequests::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 checksCi::BuildPolicyfirst, which can't see the file type. Jobs with private archives already behave the same way. RelaxingCi::BuildPolicyfor them isn't safe, because some routes only check the build.
References
- Issue: https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/work_items/3194
- Original issue: https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/work_items/3010
- Job log restriction: !259995 (merged)
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" = 2940460198Index 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=1Time: 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=1Time: 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=10Time: 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
-
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) -
Create a
duo_workflowjob 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" -
Open the URL in a private window (signed out). The download is refused.
-
Sign in as a Reporter of the project. The download is refused.
-
Sign in as a Developer. The SAST report downloads.
-
Control: a SAST artifact on a job from a normal
pushpipeline 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
Impersonating a reporter
Impersonating a developer
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.


