diff --git a/lib/swift-storage.rb b/lib/swift-storage.rb index 8159fc4..41643ac 100644 --- a/lib/swift-storage.rb +++ b/lib/swift-storage.rb @@ -1,5 +1,6 @@ require "net/http" require "swift_storage/version" +require "swift_storage/configuration" require "swift_storage/errors" require "swift_storage/utils" require "swift_storage/headers" @@ -12,4 +13,15 @@ require "swift_storage/service" module SwiftStorage + class << self + attr_accessor :configuration + end + + def self.configuration + @configuration ||= Configuration.new + end + + def self.configure + yield configuration + end end diff --git a/lib/swift_storage/auth/v1_0.rb b/lib/swift_storage/auth/v1_0.rb new file mode 100644 index 0000000..63a0e20 --- /dev/null +++ b/lib/swift_storage/auth/v1_0.rb @@ -0,0 +1,31 @@ +module SwiftStorage + module Auth + module V1_0 + attr_accessor :auth_path + + def authenticate! + headers = { + Headers::AUTH_USER => "#{tenant}:#{username}", + Headers::AUTH_KEY => password + } + res = request(auth_url, headers: headers) + + h = res.header + self.storage_url = h[Headers::STORAGE_URL] + @auth_token = h[Headers::AUTH_TOKEN] + @storage_token = h[Headers::STORAGE_TOKEN] + @auth_at = Time.now + end + + def authenticated? + !!(self.storage_url && auth_token) + end + + private + + def auth_url + File.join(endpoint, @auth_path || 'auth/v1.0') + end + end + end +end diff --git a/lib/swift_storage/auth/v2_0.rb b/lib/swift_storage/auth/v2_0.rb new file mode 100644 index 0000000..b25d3cb --- /dev/null +++ b/lib/swift_storage/auth/v2_0.rb @@ -0,0 +1,76 @@ +module SwiftStorage + module Auth + module V2_0 + extend Forwardable + def_delegators SwiftStorage, :configuration + + attr_accessor :auth_path + + def authenticate! + res = request("#{auth_url}/tokens", method: :post, json_data: auth_data) + + JSON.parse(res.body).tap do |body| + @auth_token = body['access']['token']['id'] + storage_endpoint(body['access']['serviceCatalog']) do |endpoint| + self.storage_url = endpoint['publicURL'] + @storage_token = endpoint['id'] + @auth_at = Time.now + end + end + end + + def authenticated? + !!(self.storage_url && auth_token) + end + + private + + def auth_url + File.join(endpoint, @auth_path || 'v2.0').chomp('/') + end + + def auth_data + case configuration.auth_method + when :password + { + auth: { + passwordCredentials: { + username: username, + password: password + }, + configuration.authtenant_type => tenant || username + } + } + when :rax_kskey + { + auth: { + 'RAX-KSKEY:apiKeyCredentials' => { + username: username, + apiKey: password + } + } + } + when :key + { + auth: { + apiAccessKeyCredentials: { + accessKey: username, + secretKey: password + }, + configuration.authtenant_type => tenant || username + } + } + else + fail "Unsupported authentication method #{configuration.auth_method}" + end + end + + def storage_endpoint(service_catalog) + unless (swift = service_catalog.find { |service| service['type'] == 'object-store' }) + fail SwiftStorage::Errors::NotFoundError.new 'No object-store service found' + end + yield swift['endpoints'].sample + end + end + end +end diff --git a/lib/swift_storage/configuration.rb b/lib/swift_storage/configuration.rb new file mode 100644 index 0000000..321c554 --- /dev/null +++ b/lib/swift_storage/configuration.rb @@ -0,0 +1,18 @@ +module SwiftStorage + class Configuration + attr_accessor :auth_version, :tenant, :username, :password, :endpoint, :temp_url_key, + :auth_method, :authtenant_type + + def initialize + @auth_version = ENV['SWIFT_STORAGE_AUTH_VERSION'] || '1.0' + @tenant = ENV['SWIFT_STORAGE_TENANT'] + @username = ENV['SWIFT_STORAGE_USERNAME'] + @password = ENV['SWIFT_STORAGE_PASSWORD'] + @endpoint = ENV['SWIFT_STORAGE_ENDPOINT'] + @temp_url_key = ENV['SWIFT_STORAGE_TEMP_URL_KEY'] + + @auth_method = :password + @authtenant_type = 'tenantName' # `tenantName` or `tenantId` + end + end +end diff --git a/lib/swift_storage/service.rb b/lib/swift_storage/service.rb index d9b21a0..a61c784 100644 --- a/lib/swift_storage/service.rb +++ b/lib/swift_storage/service.rb @@ -1,9 +1,13 @@ require 'uri' +require 'json' +require 'swift_storage/auth/v1_0' +require 'swift_storage/auth/v2_0' class SwiftStorage::Service - include SwiftStorage::Utils include SwiftStorage + extend Forwardable + def_delegators SwiftStorage, :configuration attr_reader :tenant, :endpoint, @@ -17,41 +21,31 @@ class SwiftStorage::Service :storage_path, :temp_url_key - def initialize(tenant: ENV['SWIFT_STORAGE_TENANT'], - username: ENV['SWIFT_STORAGE_USERNAME'], - password: ENV['SWIFT_STORAGE_PASSWORD'], - endpoint: ENV['SWIFT_STORAGE_ENDPOINT'], - temp_url_key: ENV['SWIFT_STORAGE_TEMP_URL_KEY']) + def initialize(tenant: configuration.tenant, + username: configuration.username, + password: configuration.password, + endpoint: configuration.endpoint, + temp_url_key: configuration.temp_url_key) @temp_url_key = temp_url_key %w(tenant username password endpoint).each do |n| eval("#{n} or raise ArgumentError, '#{n} is required'") eval("@#{n} = #{n}") end - self.storage_url = File.join(endpoint, 'v1', "AUTH_#{tenant}") + setup_auth @sessions = {} end - def authenticate! - @auth_token = nil - @storage_token = nil - @auth_at = nil - headers = { - Headers::AUTH_USER => "#{tenant}:#{username}", - Headers::AUTH_KEY => password - } - res = request(auth_url, :headers => headers) - - h = res.header - self.storage_url = h[Headers::STORAGE_URL] - @auth_token = h[Headers::AUTH_TOKEN] - @storage_token = h[Headers::STORAGE_TOKEN] - @auth_at = Time.new - end - - def authenticated? - !!(storage_url && auth_token) + def setup_auth + case configuration.auth_version + when '1.0' + extend SwiftStorage::Auth::V1_0 + when '2.0' + extend SwiftStorage::Auth::V2_0 + else + fail "Unsupported auth version #{configuration.auth_version}" + end end def containers @@ -77,10 +71,10 @@ def create_temp_url(container, object, expires, method, options = {}) method = method.to_s.upcase # Limit methods - %w{GET PUT HEAD}.include?(method) or raise ArgumentError, "Only GET, PUT, HEAD supported" + %w{GET PUT HEAD}.include?(method) or raise ArgumentError, 'Only GET, PUT, HEAD supported' expires = expires.to_i - object_path_escaped = File.join(storage_path, escape(container), escape(object,"/")) + object_path_escaped = File.join(storage_path, escape(container), escape(object, '/')) object_path_unescaped = File.join(storage_path, escape(container), object) string_to_sign = "#{method}\n#{expires}\n#{object_path_unescaped}" @@ -90,13 +84,13 @@ def create_temp_url(container, object, expires, method, options = {}) klass = scheme == 'http' ? URI::HTTP : URI::HTTPS temp_url_options = { - :scheme => scheme, - :host => storage_host, - :port => storage_port, - :path => object_path_escaped, - :query => URI.encode_www_form( - :temp_url_sig => sig, - :temp_url_expires => expires + scheme: scheme, + host: storage_host, + port: storage_port, + path: object_path_escaped, + query: URI.encode_www_form( + temp_url_sig: sig, + temp_url_expires: expires ) } klass.build(temp_url_options).to_s @@ -115,13 +109,15 @@ def escape(*args) end def request(path_or_url, - method: :get, - headers: nil, - params: nil, - input_stream: nil, - output_stream: nil) + method: :get, + headers: nil, + params: nil, + json_data: nil, + input_stream: nil, + output_stream: nil) headers ||= {} headers.merge!(Headers::AUTH_TOKEN => auth_token) if authenticated? + headers.merge!(Headers::CONTENT_TYPE => 'application/json') if json_data headers.merge!(Headers::CONNECTION => 'keep-alive', Headers::PROXY_CONNECTION => 'keep-alive') if !(path_or_url =~ /^http/) @@ -163,6 +159,10 @@ def request(path_or_url, raise ArgumentError, "Method #{method} not supported" end + if json_data + req.body = JSON.generate(json_data) + end + if input_stream if String === input_stream input_stream = StringIO.new(input_stream) @@ -200,10 +200,6 @@ def request(path_or_url, :username, :password - def auth_url - File.join(endpoint, 'auth/v1.0') - end - def check_response!(response) case response.code when /^2/ diff --git a/lib/swift_storage/utils.rb b/lib/swift_storage/utils.rb index 781d45f..36ca408 100644 --- a/lib/swift_storage/utils.rb +++ b/lib/swift_storage/utils.rb @@ -1,5 +1,6 @@ -module SwiftStorage::Utils +require 'openssl' +module SwiftStorage::Utils include SwiftStorage::Errors def hmac(type, key, data) @@ -8,16 +9,11 @@ def hmac(type, key, data) end def sig_to_hex(str) - str.unpack("C*").map { |c| - c.to_s(16) - }.map { |h| - h.size == 1 ? "0#{h}" : h - }.join + Digest.hexencode(str) end def struct(h) - return nil if h.empty? + return if h.empty? Struct.new(*h.keys.map(&:to_sym)).new(*h.values) end - end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ad00433..371e08f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,6 @@ require_relative "support/local_server" module TestServerMixin - def h SwiftStorage::Headers end @@ -30,7 +29,7 @@ def swift_service :password => 'testpassword', :endpoint => server.base_url, :temp_url_key => 'A1234' - ) + ).tap { |s| s.storage_url = test_storage_url } end def test_storage_url diff --git a/spec/swift/auth_spec.rb b/spec/swift/auth_spec.rb index bbb1354..9dfbe9d 100644 --- a/spec/swift/auth_spec.rb +++ b/spec/swift/auth_spec.rb @@ -1,25 +1,123 @@ -require "spec_helper" +require 'spec_helper' RSpec.describe 'Auth' do subject { swift_service } - it "success" do - headers( - h::STORAGE_URL => test_storage_url, - h::AUTH_TOKEN => 'auth_token', - h::STORAGE_TOKEN => 'storage_token' - ) - - expect{ subject.authenticate! }.to send_request(:get, '/auth/v1.0', :headers => { - h::AUTH_USER => 'test:testuser', - h::AUTH_KEY => 'testpassword' - }) - - expect(subject.auth_token).to eq('auth_token') - expect(subject.storage_url).to eq(test_storage_url) - expect(subject.storage_token).to eq('storage_token') + + context 'when v1 is used' do + before do + headers( + h::STORAGE_URL => test_storage_url, + h::AUTH_TOKEN => 'auth_token', + h::STORAGE_TOKEN => 'storage_token' + ) + end + + it 'authenticates' do + expect { subject.authenticate! }.to send_request(:get, '/auth/v1.0', + headers: { h::AUTH_USER => 'test:testuser', h::AUTH_KEY => 'testpassword' }) + end + + it 'sets the authentication token' do + subject.authenticate! + expect(subject.auth_token).to eq('auth_token') + end + + it 'sets the storage url' do + subject.authenticate! + expect(subject.storage_url).to eq(test_storage_url) + end + + it 'sets the storage token' do + subject.authenticate! + expect(subject.storage_token).to eq('storage_token') + end + end + + context 'when v2 is used' do + let(:credentials) do + { + auth: { + passwordCredentials: { + username: 'testuser', + password: 'testpassword' + }, + subject.configuration.authtenant_type => 'test' + } + } + end + let(:body) do + { + 'access' => { + 'token' => { + 'id' => 'auth_token' + }, + 'serviceCatalog' => [ + { + 'endpoints' => [ + { + 'id' => 'storage_token', + 'publicURL' => test_storage_url + } + ], + 'type' => 'object-store' + } + ] + } + } + end + + before do + SwiftStorage.configuration.auth_version = '2.0' + allow(JSON).to receive(:parse).and_return(body) + end + after { SwiftStorage.configuration.auth_version = '1.0' } + + it 'authenticates' do + expect { subject.authenticate! }.to send_request(:post, '/v2.0/tokens', + json_data: credentials.to_json) + end + + it 'sets the authentication token' do + subject.authenticate! + expect(subject.auth_token).to eq('auth_token') + end + + context 'when there is an object-store' do + it 'sets the storage url' do + subject.authenticate! + expect(subject.storage_url).to eq(test_storage_url) + end + + it 'sets the storage token' do + subject.authenticate! + expect(subject.storage_token).to eq('storage_token') + end + end + + context 'when there is no object-store' do + let(:bad_body) do + { + 'access' => { + 'token' => { + 'id' => 'auth_token' + }, + 'serviceCatalog' => [] + } + } + end + + before do + allow(JSON).to receive(:parse).and_return(bad_body) + end + + it 'raises an error' do + expect{ subject.authenticate! } + .to raise_error(SwiftStorage::Service::NotFoundError, 'No object-store service found') + end + end end - it "failure" do + it 'failure' do status(401) expect{ subject.authenticate! }.to raise_error(SwiftStorage::Service::AuthError) diff --git a/spec/swift/container_spec.rb b/spec/swift/container_spec.rb index f9fcae9..3387712 100644 --- a/spec/swift/container_spec.rb +++ b/spec/swift/container_spec.rb @@ -9,4 +9,3 @@ ) end end - diff --git a/swift-ruby.gemspec b/swift-ruby.gemspec index eac4c14..3dfd49a 100644 --- a/swift-ruby.gemspec +++ b/swift-ruby.gemspec @@ -18,8 +18,6 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - - spec.add_development_dependency "bundler", "~> 1.7" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.1.0"