Normalise DAP origin URL to drop the CI job token

What this fixes

With dap_git_credential_helper enabled, an agent's git push fails with 403 You are not allowed to push code to this project.

Reproduced on gitlab.com in gitlab-org/duo-workflow/test-project-playground/aherczeg: job 16397592800 (convert_to_gl_ci/v1, private group runner) and job 16533271763 (Developer flow, shared SaaS runner).

Root cause

Both GitLab.com runner fleets set post_get_sources_script to run, inside get_sources:

git remote set-url origin "${CI_REPOSITORY_URL}" || echo 'Not a git repository; skipping'

CI_REPOSITORY_URL always embeds the job token — Ci::Build#repo_url hardcodes "#{::Gitlab::Auth::CI_JOB_USER}:#{token}@". A credential in the remote URL takes precedence over every credential helper, so the DAP OAuth helper installed by !243872 (merged) was never consulted. The push authenticated as gitlab-ci-token, which is denied :build_push_code by rule { ~push_repository_for_job_token_allowed } — hence the 403.

Why FF_GIT_URLS_WITHOUT_TOKENS doesn't cover this

The flag works as intended. helpers/url/gitauth.go — "When authenticated is true, the token from config is injected into URLs; when false, URLs are produced without credentials" — and common/build.go passes !IsFeatureFlagOn(GitURLsWithoutTokens). So every URL the runner constructs is credential-free.

CI_REPOSITORY_URL is not one of those URLs: it is a Rails-generated job variable, and no runner code references it at all (no hits in common/build.go, shells/abstract.go, functions/concrete/run/stages/get_sources.go). The flag cannot scrub it.

Why this only appeared after !243872 (merged)

oauth_remote_commands returns [] when the flag is on. With the flag off it runs in step_script — after get_sources, so after the hook — and overwrote the job-token origin as a side effect. That accidental ordering is why the sed path worked for months.

This wasn't caught pre-merge because GDK carries no post_get_sources_script: the verification recorded on #593467 confirmed a credential-free origin on GDK, where the hook does not exist.

The change

remote_url_commands replaces oauth_remote_commands at the call site. With the flag on it rewrites origin to a credential-free URL and lets the helper supply the OAuth token.

Restoring oauth_remote_commands was rejected deliberately: it puts the token back into .git/config, which is the vulnerability #2778 exists to remove. git -C pins the repo directory because setup_script runs in the same shell and may cd away.

Flag-off behaviour is unchanged.

Testing

  • New real-git example seeds a repo whose origin carries a job token — as the hook leaves it — runs the actual emitted command, and asserts the URL comes back credential-free. Verified failing without the fix (the job script does not rewrite origin) and passing with it.
  • A variables-hash assertion could not catch this class of bug: the defect was that no rewrite ran at all. The pre-existing expect(script).not_to include('git remote set-url origin') was itself the assertion encoding the wrong assumption; it now asserts a credential-free rewrite plus the absence of oauth: and of the token.
  • The 'a workload with a full coding environment' shared example also asserts the credential-free rewrite now. Its old not_to include only passed because -C split the substring.
  • start_workflow_service_spec.rb and resume_workflow_service_spec.rb on current master: 188 examples, 0 failures. RuboCop clean.

How to set up and validate locally

<🤖>

GDK runners carry no post_get_sources_script, so the bug only reproduces once you add the hook the GitLab.com runner fleets use.

  1. Use a GDK where Duo Agent Platform flows run on a local runner, and enable the flag in gdk rails console:
    Feature.enable(:dap_git_credential_helper)
  2. Add the GitLab.com hook to the runner's [[runners]] section (<gdk-root>/gitlab-runner-config.toml for the GDK-managed runner), then gdk restart runner:
    post_get_sources_script = "git remote set-url origin \"${CI_REPOSITORY_URL}\" || echo 'Not a git repository; skipping'"
  3. On master, open an issue in any project and start the Developer flow with Generate MR with Duo. In the flow's CI job log, the first git push -u origin ... fails with 403 You are not allowed to push code to this project. The agent may retry against an explicit URL; the failing origin push is the bug.
  4. Check out this branch, run gdk restart rails-web rails-background-jobs, and start the flow again on a new issue. The job log now runs git -C "$CI_PROJECT_DIR" remote set-url origin ... before the flow starts, git push -u origin ... succeeds on the first try, and the flow opens an MR.
  5. Optional: Feature.disable(:dap_git_credential_helper) and repeat. The job log shows the unchanged oauth: origin rewrite and the push still succeeds.

</🤖>

Rollout note

This affects every DAP job on GitLab.com once dap_git_credential_helper rolls out, not just one project — relevant to #612645. Worth confirming the hook's provenance with the Runner/infra owners so nothing else depends on "origin is credential-free when FF_GIT_URLS_WITHOUT_TOKENS=true".

No merge-order constraint with ai-assist: ai-assist!6713 was closed in favour of ai-assist!6962, which keeps git_setup and sets a credential-free origin itself, so either can land first.

Related to #593467 · follows up !243872 (merged)

Edited by Andras Herczeg

Merge request reports

Loading
Loading