Skip to content
Open
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: 14 additions & 2 deletions lib/flipper/cloud/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "logger"
require "socket"
require "uri"
require "flipper/adapters/http"
require "flipper/adapters/poll"
require "flipper/poller"
Expand All @@ -25,8 +26,9 @@ class Configuration

# Public: The url for http adapter. Really should only be customized for
# development work if you are me and you are not me. Feel free to
# forget you ever saw this.
attr_accessor :url
# forget you ever saw this. Must be https so the cloud token is
# never transmitted in cleartext (local development uses https too).
attr_reader :url

# Public: net/http read timeout for all http requests (default: 5).
attr_accessor :read_timeout
Expand Down Expand Up @@ -139,6 +141,16 @@ def instrument(name, payload = {}, &block)
instrumenter.instrument(name, payload, &block)
end

def url=(value)
unless URI(value.to_s).scheme == "https"
raise ArgumentError,
"Flipper::Cloud url must use https but was #{value.inspect}. " \
"https is required so your token is never sent in cleartext."
end

@url = value
end

private

def app_adapter
Expand Down
14 changes: 13 additions & 1 deletion lib/flipper/cloud/migrate.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "uri"
require "flipper/adapters/http/client"
require "flipper/typecast"

Expand Down Expand Up @@ -55,7 +56,7 @@ def self.push(token, flipper = Flipper)

# Private: Build an HTTP client for Cloud API requests.
def self.build_client(path, headers: {})
base_url = ENV.fetch("FLIPPER_CLOUD_URL", DEFAULT_CLOUD_URL)
base_url = https_cloud_url(ENV.fetch("FLIPPER_CLOUD_URL", DEFAULT_CLOUD_URL))

Flipper::Adapters::Http::Client.new(
url: "#{base_url}#{path}",
Expand All @@ -67,5 +68,16 @@ def self.build_client(path, headers: {})
)
end
private_class_method :build_client

def self.https_cloud_url(value)
unless URI(value.to_s).scheme == "https"
raise ArgumentError,
"Flipper::Cloud url must use https but was #{value.inspect}. " \
"https is required so your token is never sent in cleartext."
end

value
end
private_class_method :https_cloud_url
end
end
16 changes: 12 additions & 4 deletions spec/flipper/cloud/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,21 @@
end

it "can override url using options" do
options = required_options.merge(url: "http://localhost:5000/adapter")
options = required_options.merge(url: "https://localhost:5000/adapter")
instance = described_class.new(options)
expect(instance.url).to eq("http://localhost:5000/adapter")
expect(instance.url).to eq("https://localhost:5000/adapter")

instance = described_class.new(required_options)
instance.url = "https://localhost:5000/adapter"
expect(instance.url).to eq("https://localhost:5000/adapter")
end

it "requires https url" do
options = required_options.merge(url: "http://localhost:5000/adapter")
expect { described_class.new(options) }.to raise_error(ArgumentError, /must use https/)

instance = described_class.new(required_options)
instance.url = "http://localhost:5000/adapter"
expect(instance.url).to eq("http://localhost:5000/adapter")
expect { instance.url = "http://localhost:5000/adapter" }.to raise_error(ArgumentError, /must use https/)
end

it "can override URL using ENV var" do
Expand Down
44 changes: 25 additions & 19 deletions spec/flipper/cloud/migrate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

around do |example|
original = ENV["FLIPPER_CLOUD_URL"]
ENV["FLIPPER_CLOUD_URL"] = "http://localhost:5555"
ENV["FLIPPER_CLOUD_URL"] = "https://localhost:5555"
example.run
ensure
ENV["FLIPPER_CLOUD_URL"] = original
Expand All @@ -26,19 +26,19 @@ def decompress_request_body

describe ".migrate" do
it "returns a MigrateResult with code and url on success" do
stub_request(:post, "http://localhost:5555/api/migrate")
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc123"}', headers: {"Content-Type" => "application/json"})
stub_request(:post, "https://localhost:5555/api/migrate")
.to_return(status: 200, body: '{"url":"https://localhost:5555/cloud/setup/abc123"}', headers: {"Content-Type" => "application/json"})

result = Flipper::Cloud.migrate(flipper)

expect(result).to be_a(Flipper::Cloud::MigrateResult)
expect(result.code).to eq(200)
expect(result.url).to eq("http://localhost:5555/cloud/setup/abc123")
expect(result.url).to eq("https://localhost:5555/cloud/setup/abc123")
end

it "sends export data and metadata in the request body" do
stub = stub_request(:post, "http://localhost:5555/api/migrate")
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc123"}')
stub = stub_request(:post, "https://localhost:5555/api/migrate")
.to_return(status: 200, body: '{"url":"https://localhost:5555/cloud/setup/abc123"}')

Flipper::Cloud.migrate(flipper, app_name: "MyApp")

Expand All @@ -50,9 +50,9 @@ def decompress_request_body
end

it "sends gzip-compressed request body" do
stub = stub_request(:post, "http://localhost:5555/api/migrate")
stub = stub_request(:post, "https://localhost:5555/api/migrate")
.with(headers: {"content-encoding" => "gzip"})
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc"}')
.to_return(status: 200, body: '{"url":"https://localhost:5555/cloud/setup/abc"}')

Flipper::Cloud.migrate(flipper)

Expand All @@ -62,7 +62,7 @@ def decompress_request_body
end

it "handles error responses" do
stub_request(:post, "http://localhost:5555/api/migrate")
stub_request(:post, "https://localhost:5555/api/migrate")
.to_return(status: 500, body: '{"error":"Internal Server Error"}')

result = Flipper::Cloud.migrate(flipper)
Expand All @@ -72,7 +72,7 @@ def decompress_request_body
end

it "includes error message from response body" do
stub_request(:post, "http://localhost:5555/api/migrate")
stub_request(:post, "https://localhost:5555/api/migrate")
.to_return(status: 422, body: '{"error":"Invalid export format"}')

result = Flipper::Cloud.migrate(flipper)
Expand All @@ -82,21 +82,21 @@ def decompress_request_body
end

it "uses FLIPPER_CLOUD_URL environment variable" do
stub = stub_request(:post, "http://localhost:5555/api/migrate")
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc"}')
stub = stub_request(:post, "https://localhost:5555/api/migrate")
.to_return(status: 200, body: '{"url":"https://localhost:5555/cloud/setup/abc"}')

Flipper::Cloud.migrate(flipper)

expect(stub).to have_been_requested
end

it "sends content-type and accept headers" do
stub = stub_request(:post, "http://localhost:5555/api/migrate")
stub = stub_request(:post, "https://localhost:5555/api/migrate")
.with(headers: {
"content-type" => "application/json",
"accept" => "application/json",
})
.to_return(status: 200, body: '{"url":"http://localhost:5555/cloud/setup/abc"}')
.to_return(status: 200, body: '{"url":"https://localhost:5555/cloud/setup/abc"}')

Flipper::Cloud.migrate(flipper)

Expand All @@ -106,7 +106,7 @@ def decompress_request_body

describe ".push" do
it "returns a MigrateResult with code on success" do
stub_request(:post, "http://localhost:5555/adapter/import")
stub_request(:post, "https://localhost:5555/adapter/import")
.to_return(status: 204, body: "")

result = Flipper::Cloud.push("test-token", flipper)
Expand All @@ -116,7 +116,7 @@ def decompress_request_body
end

it "sends the token as a header" do
stub = stub_request(:post, "http://localhost:5555/adapter/import")
stub = stub_request(:post, "https://localhost:5555/adapter/import")
.with(headers: {"flipper-cloud-token" => "test-token"})
.to_return(status: 204, body: "")

Expand All @@ -125,8 +125,14 @@ def decompress_request_body
expect(stub).to have_been_requested
end

it "requires HTTPS when sending the token" do
ENV["FLIPPER_CLOUD_URL"] = "http://localhost:5555"

expect { Flipper::Cloud.push("test-token", flipper) }.to raise_error(ArgumentError, /must use https/)
end

it "sends gzip-compressed export contents as the body" do
stub = stub_request(:post, "http://localhost:5555/adapter/import")
stub = stub_request(:post, "https://localhost:5555/adapter/import")
.with(headers: {"content-encoding" => "gzip"})
.to_return(status: 204, body: "")

Expand All @@ -139,7 +145,7 @@ def decompress_request_body
end

it "handles error responses" do
stub_request(:post, "http://localhost:5555/adapter/import")
stub_request(:post, "https://localhost:5555/adapter/import")
.to_return(status: 401, body: '{"error":"Unauthorized"}')

result = Flipper::Cloud.push("bad-token", flipper)
Expand All @@ -148,7 +154,7 @@ def decompress_request_body
end

it "includes error message from response body" do
stub_request(:post, "http://localhost:5555/adapter/import")
stub_request(:post, "https://localhost:5555/adapter/import")
.to_return(status: 401, body: '{"error":"Invalid token"}')

result = Flipper::Cloud.push("bad-token", flipper)
Expand Down
Loading