Skip to content
Closed
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
16 changes: 15 additions & 1 deletion artifactory/commands/ruby/native_ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ func (rc *RubyCommand) injectAuth(serverDetails *coreConfig.ServerDetails, sourc
var extraEnv []string
switch rc.nativeTool {
case toolBundle:
cred := fmt.Sprintf("%s:%s", user, pass)
cred := BundleCredential(user, pass)
// Bundler's credential lookup varies by version, and falls back from the
// host:port form to the bare hostname, so inject every candidate spelling.
candidates := BundleCredentialKeys(host)
Expand Down Expand Up @@ -679,6 +679,20 @@ func rubyHasCredentials(pass string) bool {
return pass != ""
}

// BundleCredential formats a user/password pair as a Bundler per-host credential (the
// value of BUNDLE_<HOST>, as an env var or in ~/.bundle/config). Bundler assigns that
// value to the source URI's userinfo, which rejects raw reserved characters such as '@'
// (e.g. an email username) or a ':' in the username, and later decodes each part with
// CGI.unescape. Each part is therefore percent-encoded.
// Spaces are written as %20 rather than '+' so the value also round-trips through
// decoders that do not treat '+' as a space.
func BundleCredential(user, pass string) string {
escape := func(s string) string {
return strings.ReplaceAll(url.QueryEscape(s), "+", "%20")
}
return escape(user) + ":" + escape(pass)
}

// BundleEnvKeyForHost converts a host into Bundler's per-host credential env var name,
// following Bundler's key normalization: uppercase, "." → "__", "-" → "___", and any
// remaining non-alphanumeric character → "_", prefixed with "BUNDLE_".
Expand Down
32 changes: 32 additions & 0 deletions artifactory/commands/ruby/native_ruby_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,35 @@ func TestUserHomeDir_PrefersHome(t *testing.T) {
assert.Equal(t, expected, fallback)
}
}

func TestBundleCredential(t *testing.T) {
cases := []struct {
user, pass, want string
}{
{"myuser", "mypass", "myuser:mypass"},
{"jane@example.com", "mypass", "jane%40example.com:mypass"},
{"user:name", "p@ss:w/rd", "user%3Aname:p%40ss%3Aw%2Frd"},
{"user", "a+b c", "user:a%2Bb%20c"},
{"", "token", ":token"},
}
for _, c := range cases {
assert.Equal(t, c.want, BundleCredential(c.user, c.pass), "user %q pass %q", c.user, c.pass)
}
}

func TestRubyInjectAuth_BundleEscapesEmailUsername(t *testing.T) {
server := &coreConfig.ServerDetails{
User: "jane@example.com",
Password: "p@ss",
ArtifactoryUrl: "https://my.jfrog.io/artifactory/",
}
key := BundleEnvKeyForHost("my.jfrog.io")
t.Setenv(key, "")

rc := NewRubyCommand().SetNativeTool(toolBundle)
env := rc.injectAuth(server, "https://my.jfrog.io/artifactory/api/gems/gems-virtual/")
assert.Contains(t, env, key+"=jane%40example.com:p%40ss")
for _, e := range env {
assert.NotContains(t, e, "jane@example.com", "raw '@' must not reach Bundler's userinfo")
}
}
2 changes: 1 addition & 1 deletion artifactory/commands/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@ func (sc *SetupCommand) configureRuby() error {

// Per-host credentials, for Gemfiles that name the Artifactory source explicitly.
if password != "" {
credential := username + ":" + password
credential := ruby.BundleCredential(username, password)
for _, key := range ruby.BundleCredentialKeys(repoUrl.Hostname()) {
settings[key] = credential
}
Expand Down
17 changes: 17 additions & 0 deletions artifactory/commands/setup/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,23 @@ func TestWriteBundleSettings_BundlerReadsMirrorAndCredentials(t *testing.T) {
assert.Equal(t, "admin:secret", parsed["BUNDLE_MY___CO__JFROG__IO"])
}

// TestConfigureRuby_EscapesEmailUsername guards against an email-style username being
// written raw into the per-host credential: Bundler rejects a bare '@' in a URI userinfo,
// so the credential must be percent-encoded.
func TestConfigureRuby_EscapesEmailUsername(t *testing.T) {
tmpHome := withTempHome(t)

cmd := createTestSetupCommand(project.Ruby)
cmd.serverDetails.User = "jane@example.com"
cmd.serverDetails.Password = "p@ss" + "word"

require.NoError(t, cmd.configureRuby())

parsed := readBundleConfig(t, tmpHome)
assert.Equal(t, "jane%40example.com:p%40ssword", parsed["BUNDLE_ACME__JFROG__IO"])
assert.NotContains(t, parsed["BUNDLE_ACME__JFROG__IO"], "@", "raw '@' must not reach Bundler's userinfo")
}

func TestAddGemrcSource_EmptyFile(t *testing.T) {
tmpHome := withTempHome(t)

Expand Down
Loading