Skip to content

Commit

Permalink
Add support for upstream auth with ENV variables
Browse files Browse the repository at this point in the history
  • Loading branch information
CiTroNaK committed Sep 28, 2023
1 parent 6fb78ea commit 8c15ddc
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased

- Add support for upstream auth with ENV variables ([#339](https://github.com/rubygems/gemstash/pull/339), [@CiTroNaK](https://github.com/CiTroNaK))

## 2.4.0 (2023-09-27)

### Changes
Expand Down
52 changes: 52 additions & 0 deletions docs/gemstash-multiple-sources.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ gem "rubywarrior"
source "http://localhost:9292/upstream/#{CGI.escape("https://my.gem-source.local")}" do
gem "my-gem"
end

source "http://localhost:9292/upstream/my-other.gem-source.local" do
gem "my-other-gem"
end
```

Notice the `CGI.escape` call in the second source. This is important, as
Expand All @@ -57,6 +61,54 @@ you want. The `/upstream` prefix tells Gemstash to use a gem source
other than the default source. You can now bundle with the additional
source.

Notice that the third source doesn't need to be escaped.
This is because the `https://` is used by default when no scheme is set,
and the source URL does not contain any chars that need to be escaped.

## Authentication with Multiple Sources

You can use basic authentication or API keys on sources directly in Gemfile
or using ENV variables on the Gemstash instance.

Example `Gemfile`:
```ruby
# ./Gemfile
require "cgi"
source "http://localhost:9292"

source "http://localhost:9292/upstream/#{CGI.escape("user:password@my.gem-source.local")}" do
gem "my-gem"
end

source "http://localhost:9292/upstream/#{CGI.escape("api_key@my-other.gem-source.local")}" do
gem "my-other-gem"
end
```

If you set `GEMSTASH_<HOST>` ENV variable with your authentication information,
you can omit it from the `Gemfile`:

```ruby
# ./Gemfile
source "http://localhost:9292"

source "http://localhost:9292/upstream/my.gem-source.local" do
gem "my-gem"
end
```

And run the Gemstash with the credentials set in an ENV variable:

```bash
GEMSTASH_MY__GEM___SOURCE__LOCAL=user:password gemstash start --no-daemonize --config-file config.yml.erb
```

The name of the ENV variable is the uppercase version of the host name,
with all `.` characters replaced with `__`, all `-` with `___` and a `GEMSTASH_` prefix
(it uses the same syntax as [Bundler](https://bundler.io/v2.4/man/bundle-config.1.html#CREDENTIALS-FOR-GEM-SOURCES)).

Example: `my.gem-source.local` => `GEMSTASH_MY__GEM___SOURCE__LOCAL`

## Redirecting

Gemstash supports an alternate mode of specifying your gem sources. If
Expand Down
35 changes: 33 additions & 2 deletions lib/gemstash/upstream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ class Upstream

attr_reader :user_agent, :uri

def_delegators :@uri, :scheme, :host, :user, :password, :to_s
def_delegators :@uri, :scheme, :host, :to_s

def initialize(upstream, user_agent: nil)
@uri = URI(CGI.unescape(upstream.to_s))
url = CGI.unescape(upstream.to_s)
url = "https://#{url}" unless %r{^https?://}.match?(url)
@uri = URI(url)
@user_agent = user_agent
raise "URL '#{@uri}' is not valid!" unless @uri.to_s&.match?(URI::DEFAULT_PARSER.make_regexp)
end
Expand All @@ -40,12 +42,41 @@ def host_id
@host_id ||= "#{host}_#{hash}"
end

def user
env_auth_user || @uri.user
end

def password
env_auth_pass || @uri.password
end

private

def hash
Digest::MD5.hexdigest(to_s)
end

def env_auth_user
return unless env_auth

env_auth.split(":", 2).first
end

def env_auth_pass
return unless env_auth
return unless env_auth.include?(":")

env_auth.split(":", 2).last
end

def env_auth
@env_auth ||= ENV["GEMSTASH_#{host_for_env}"]
end

def host_for_env
host.upcase.gsub(".", "__").gsub("-", "___")
end

# :nodoc:
class GemName
def initialize(upstream, gem_name)
Expand Down
22 changes: 22 additions & 0 deletions spec/gemstash/http_client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,34 @@
it { is_expected.to include("Authorization" => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=") }
end

context "when user:pass auth is set by ENV variable" do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("GEMSTASH_LOCALHOST").and_return("username:password")
end

let(:upstream) { Gemstash::Upstream.new("https://localhost/") }
subject { Gemstash::HTTPClient.for(upstream).client.headers }
it { is_expected.to include("Authorization" => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=") }
end

context "when api_key auth is in the upstream url" do
let(:upstream) { Gemstash::Upstream.new("https://api_key@localhost/") }
subject { Gemstash::HTTPClient.for(upstream).client.headers }
it { is_expected.to include("Authorization" => "Basic YXBpX2tleTo=") }
end

context "when api_key auth is set by ENV variable" do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("GEMSTASH_LOCALHOST").and_return("api_key")
end

let(:upstream) { Gemstash::Upstream.new("https://localhost/") }
subject { Gemstash::HTTPClient.for(upstream).client.headers }
it { is_expected.to include("Authorization" => "Basic YXBpX2tleTo=") }
end

context "when no auth is included in the upstream url" do
let(:upstream) { Gemstash::Upstream.new("https://localhost/") }
subject { Gemstash::HTTPClient.for(upstream).client.headers }
Expand Down
48 changes: 41 additions & 7 deletions spec/gemstash/upstream_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
expect(upstream_uri.password).to be_nil
end

it "uses HTTPS schema by default" do
upstream_uri = Gemstash::Upstream.new("rubygems.org")
expect(upstream_uri.to_s).to eq("https://rubygems.org")
expect(upstream_uri.host).to eq("rubygems.org")
expect(upstream_uri.scheme).to eq("https")
expect(upstream_uri.url("gems")).to eq("https://rubygems.org/gems")
expect(upstream_uri.user).to be_nil
expect(upstream_uri.password).to be_nil
end

it "supports user:pass url auth in the uri" do
upstream_uri = Gemstash::Upstream.new("https://myuser:mypassword@rubygems.org/")
expect(upstream_uri.user).to eq("myuser")
Expand Down Expand Up @@ -55,19 +65,43 @@
expect(upstream_uri.url("gems", "key=value")).to eq("https://rubygems.org/gems?key=value")
end

it "fails if the uri is not valid" do
expect { Gemstash::Upstream.new("something_that_is_not_an_uri") }.to raise_error(
/URL 'something_that_is_not_an_uri' is not valid/
)
end

it "has a nil user agent if not provided" do
expect(Gemstash::Upstream.new("https://rubygems.org/").user_agent).to be_nil
end

it "supports getting user agent" do
expect(Gemstash::Upstream.new("https://rubygems.org/",
user_agent: "my_user_agent").user_agent).to eq("my_user_agent")
user_agent: "my_user_agent").user_agent).to eq("my_user_agent")
end

context "with ENV variables for upstream authentication" do
context "with user and password" do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("GEMSTASH_RUBYGEMS__ORG").and_return("myuser:mypassword")
end

it "users user:pass for auth" do
upstream_uri = Gemstash::Upstream.new("https://rubygems.org/")
expect(upstream_uri.user).to eq("myuser")
expect(upstream_uri.password).to eq("mypassword")
expect(upstream_uri.auth?).to be_truthy
end
end

context "with api key" do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("GEMSTASH_RUBYGEMS__ORG").and_return("api_key")
end

it "uses api_key for auth" do
upstream_uri = Gemstash::Upstream.new("https://rubygems.org/")
expect(upstream_uri.user).to eq("api_key")
expect(upstream_uri.password).to be_nil
expect(upstream_uri.auth?).to be_truthy
end
end
end

describe ".url" do
Expand Down

0 comments on commit 8c15ddc

Please sign in to comment.