Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,11 @@ def write_temporary_dependency_files

File.write(lockfile.name, lockfile.content)
File.write(T.must(toolchain).name, T.must(toolchain).content) if toolchain
return unless config
config_file = config
Copy link
Member Author

@jeffwidman jeffwidman Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Sorbet variable narrowing... so that we don't have to have another T.must() call... this is arguably more readable.

return unless config_file

FileUtils.mkdir_p(File.dirname(T.must(config).name))
File.write(T.must(config).name, T.must(config).content)
FileUtils.mkdir_p(File.dirname(config_file.name))
File.write(config_file.name, Helpers.sanitize_cargo_config(T.must(config_file.content)))
end

sig { void }
Expand Down
54 changes: 47 additions & 7 deletions cargo/lib/dependabot/cargo/helpers.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
# typed: strong
# typed: strict
# frozen_string_literal: true

require "toml-rb"

module Dependabot
module Cargo
module Helpers
extend T::Sig

# Disable Cargo's *global* credential providers so that Cargo does not attempt to look up registry tokens
# on its own. The dependabot proxy (https://github.com/dependabot/proxy/) handles all registry authentication
# transparently by intercepting HTTP requests and injecting the appropriate credentials.
#
# Note: this only affects the global/default credential provider. Per-registry `credential-provider` settings
# in .cargo/config.toml override this env var, so those are stripped separately by `sanitize_cargo_config`.
#
# Uses ||= so developers can override by setting CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS=cargo:token in their
# shell (along with the appropriate CARGO_REGISTRIES_{NAME}_TOKEN vars) for local development without the proxy.
sig { void }
def self.bypass_cargo_credential_providers
# Disable Cargo's built-in credential providers entirely so that Cargo does not attempt to look up registry
# tokens on its own. The dependabot proxy (https://github.com/dependabot/proxy/) handles all registry
# authentication transparently by intercepting HTTP requests and injecting the appropriate credentials.
#
# Uses ||= so developers can override by setting CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS=cargo:token in their
# shell (along with the appropriate CARGO_REGISTRIES_{NAME}_TOKEN vars) for local development without the proxy.
ENV["CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS"] ||= ""
end

# Strip per-registry `credential-provider` settings from .cargo/config.toml.
#
# Users may have entries like:
# [registries.my-registry]
# credential-provider = "cargo:token"
#
# These per-registry settings override the global CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS env var,
# causing Cargo to look up tokens locally. Since the dependabot proxy handles all registry authentication
# transparently, we remove these so Cargo makes plain unauthenticated requests that the proxy can intercept.
sig { params(config_content: String).returns(String) }
def self.sanitize_cargo_config(config_content)
parsed = TomlRB.parse(config_content)
return config_content unless parsed.is_a?(Hash)

registries = parsed["registries"]
if registries.is_a?(Hash)
registries.each_value do |registry_config|
registry_config.delete("credential-provider") if registry_config.is_a?(Hash)
end
end

# Also strip credential-provider from [registry] (crates.io default registry). Users who `cargo publish`
# from CI may have this set. It's a per-registry override that takes precedence over the global env var,
# so we need to remove it to prevent Cargo from trying to look up a token.
registry = parsed["registry"]
registry.delete("credential-provider") if registry.is_a?(Hash)

TomlRB.dump(parsed)
rescue TomlRB::Error => e
raise Dependabot::DependencyFileNotParseable.new(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a well-known error that we display to users so that if they hit problems, they can hopefully self-diagnose.

".cargo/config.toml",
"Failed to parse Cargo config file: #{e.message}"
)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,11 @@ def write_temporary_dependency_files(prepared: true)

File.write(T.must(lockfile).name, T.must(lockfile).content) if lockfile
File.write(T.must(toolchain).name, T.must(toolchain).content) if toolchain
return unless config
config_file = config
return unless config_file

FileUtils.mkdir_p(File.dirname(T.must(config).name))
File.write(T.must(config).name, T.must(config).content)
FileUtils.mkdir_p(File.dirname(config_file.name))
File.write(config_file.name, Helpers.sanitize_cargo_config(T.must(config_file.content)))
end

sig { void }
Expand Down
125 changes: 125 additions & 0 deletions cargo/spec/dependabot/cargo/helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,129 @@
end
end
end

describe ".sanitize_cargo_config" do
context "when config has no credential-provider settings" do
let(:config_content) do
<<~TOML
[registries.my-registry]
index = "sparse+https://example.com/index/"
token = "some-token"
TOML
end

it "returns equivalent config with non-credential-provider keys preserved" do
result = described_class.sanitize_cargo_config(config_content)
parsed = TomlRB.parse(result)
expect(parsed["registries"]["my-registry"]["index"]).to eq("sparse+https://example.com/index/")
expect(parsed["registries"]["my-registry"]["token"]).to eq("some-token")
end
end

context "when config has no registries section" do
let(:config_content) do
<<~TOML
[source.crates-io]
replace-with = "my-mirror"

[net]
git-fetch-with-cli = true
TOML
end

it "returns equivalent config unchanged" do
result = described_class.sanitize_cargo_config(config_content)
parsed = TomlRB.parse(result)
expect(parsed["source"]["crates-io"]["replace-with"]).to eq("my-mirror")
expect(parsed["net"]["git-fetch-with-cli"]).to be true
end
end

context "when config has per-registry credential-provider" do
let(:config_content) do
<<~TOML
[registries.artifactory]
index = "sparse+https://example.com/api/cargo/cargo-local/index/"
credential-provider = "cargo:token"

[registries.artifactory-remote]
index = "sparse+https://example.com/api/cargo/cargo-crates-remote/index/"
credential-provider = "cargo:token"
TOML
end

it "strips credential-provider from all registries" do
result = described_class.sanitize_cargo_config(config_content)
parsed = TomlRB.parse(result)

expect(parsed["registries"]["artifactory"]).not_to have_key("credential-provider")
expect(parsed["registries"]["artifactory-remote"]).not_to have_key("credential-provider")
end

it "preserves index URLs" do
result = described_class.sanitize_cargo_config(config_content)
parsed = TomlRB.parse(result)

expect(parsed["registries"]["artifactory"]["index"])
.to eq("sparse+https://example.com/api/cargo/cargo-local/index/")
expect(parsed["registries"]["artifactory-remote"]["index"])
.to eq("sparse+https://example.com/api/cargo/cargo-crates-remote/index/")
end
end

context "when config has [registry] credential-provider (e.g. for cargo publish)" do
let(:config_content) do
<<~TOML
[registry]
credential-provider = "cargo:token"
TOML
end

it "strips the credential-provider" do
result = described_class.sanitize_cargo_config(config_content)
parsed = TomlRB.parse(result)

expect(parsed.fetch("registry", {})).not_to have_key("credential-provider")
end
end

context "when config has mixed settings" do
let(:config_content) do
<<~TOML
[registries.with-cred]
index = "sparse+https://example.com/index/"
credential-provider = "cargo:token"

[registries.without-cred]
index = "sparse+https://other.example.com/index/"

[source.crates-io]
replace-with = "with-cred"

[net]
git-fetch-with-cli = true
TOML
end

it "strips only credential-provider, preserves everything else" do
result = described_class.sanitize_cargo_config(config_content)
parsed = TomlRB.parse(result)

expect(parsed["registries"]["with-cred"]).not_to have_key("credential-provider")
expect(parsed["registries"]["with-cred"]["index"]).to eq("sparse+https://example.com/index/")
expect(parsed["registries"]["without-cred"]["index"]).to eq("sparse+https://other.example.com/index/")
expect(parsed["source"]["crates-io"]["replace-with"]).to eq("with-cred")
expect(parsed["net"]["git-fetch-with-cli"]).to be true
end
end

context "when config is malformed TOML" do
let(:config_content) { "this is not valid toml {{{{" }

it "raises DependencyFileNotParseable" do
expect { described_class.sanitize_cargo_config(config_content) }
.to raise_error(Dependabot::DependencyFileNotParseable, /Failed to parse Cargo config file/)
end
end
end
end
Loading