From db560145bacfec402575b0615b48b786356391e1 Mon Sep 17 00:00:00 2001 From: Renier Morales Date: Sun, 15 Nov 2015 19:47:58 -0400 Subject: [PATCH 1/4] Add token/password authentication to ruby client --- CHANGELOG.textile | 3 ++ lib/softlayer/Client.rb | 65 ++++++++++++++++++++++++++++++++++++----- lib/softlayer/base.rb | 2 +- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.textile b/CHANGELOG.textile index b33cc14..8e4f540 100644 --- a/CHANGELOG.textile +++ b/CHANGELOG.textile @@ -1,3 +1,6 @@ +*3.2* +* Add password-based authentication with `SoftLayer::Client.with_password(username: '...', password: '...', ...)`. + *3.0* * Substantially rewrote the ObjectFilter class. ObjectFilters used to be hashes which made it easy to manipulate their content incorrectly. The new implementation has a strict interface that makes it harder to manipulate filters incorrectly. * Added a model for Virtual Server Image Templates (SoftLayer::ImageTemplate) - VirtualServerOrder now requires an instance of this class rather than allowing you to provide the global_id of an image diff --git a/lib/softlayer/Client.rb b/lib/softlayer/Client.rb index e846b75..017c453 100644 --- a/lib/softlayer/Client.rb +++ b/lib/softlayer/Client.rb @@ -58,6 +58,32 @@ def self.default_client=(new_default) @@default_client = new_default end + ## + # This will be using your username and password to get a portal + # token with which to authenticate client calls. + # This is a wrapper around Client.new. You can pass it the same + # parameters as with Client.new, with the exception that this will + # be expecting a password in the options hash. + def self.with_password(options = {}) + if options[:username].nil? || options[:username].empty? + raise 'A username is required to create this client' + end + + if options[:password].nil? || options[:password].empty? + raise 'A password is required to create this client' + end + + service = SoftLayer::Service.new('SoftLayer_User_Customer') + token = service.getPortalLoginToken( + options[:username], options[:password] + ) + + options[:userId] = token['userId'] + options[:authToken] = token['hash'] + + SoftLayer::Client.new(options) + end + ## # # Clients are built with a number of settings: @@ -81,6 +107,10 @@ def initialize(options = {}) # do a similar thing for the api key @api_key = settings[:api_key] || "" + # grab token pair + @userId = settings[:userId] + @authToken = settings[:authToken] + # and the endpoint url @endpoint_url = settings[:endpoint_url] || API_PUBLIC_ENDPOINT @@ -90,19 +120,40 @@ def initialize(options = {}) # and assign a time out if the settings offer one @network_timeout = settings[:timeout] if settings.has_key?(:timeout) - raise "A SoftLayer Client requires a username" if !@username || @username.empty? - raise "A SoftLayer Client requires an api_key" if !@api_key || @api_key.empty? + if token_based? + raise "A SoftLayer Client requires a userId" if !@userId + raise "A SoftLayer Client requires an authToken" if !@authToken || @api_key.empty? + else + raise "A SoftLayer Client requires a username" if !@username || @username.empty? + raise "A SoftLayer Client requires an api_key" if !@api_key || @api_key.empty? + end + raise "A SoftLayer Client requires an endpoint URL" if !@endpoint_url || @endpoint_url.empty? end + # return whether this client is using token-based authentication + def token_based? + @userId && @authToken && !@authToken.empty? + end + # return a hash of the authentication headers for the client def authentication_headers - { - "authenticate" => { - "username" => @username, - "apiKey" => @api_key + if token_based? + { + 'authenticate' => { + 'complexType' => 'PortalLoginToken', + 'userId' => @userId, + 'authToken' => @authToken + } } - } + else + { + 'authenticate' => { + 'username' => @username, + 'apiKey' => @api_key + } + } + end end # Returns a service with the given name. diff --git a/lib/softlayer/base.rb b/lib/softlayer/base.rb index ada6f8e..c5779db 100644 --- a/lib/softlayer/base.rb +++ b/lib/softlayer/base.rb @@ -12,7 +12,7 @@ module SoftLayer # The version number (including major, minor, and bugfix numbers) # This should change in accordance with the concept of Semantic Versioning - VERSION = "3.1.1" # version history in the CHANGELOG.textile file at the root of the source + VERSION = "3.2.0" # version history in the CHANGELOG.textile file at the root of the source # The base URL of the SoftLayer API available to the public internet. API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/' From 921e6fdabcf5be6611fc2c0c1b0ebde720f40d64 Mon Sep 17 00:00:00 2001 From: Renier Morales Date: Sun, 15 Nov 2015 20:13:25 -0400 Subject: [PATCH 2/4] Remove unnecessary check on token pair --- lib/softlayer/Client.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/softlayer/Client.rb b/lib/softlayer/Client.rb index 017c453..dee1d6f 100644 --- a/lib/softlayer/Client.rb +++ b/lib/softlayer/Client.rb @@ -120,10 +120,7 @@ def initialize(options = {}) # and assign a time out if the settings offer one @network_timeout = settings[:timeout] if settings.has_key?(:timeout) - if token_based? - raise "A SoftLayer Client requires a userId" if !@userId - raise "A SoftLayer Client requires an authToken" if !@authToken || @api_key.empty? - else + unless token_based? raise "A SoftLayer Client requires a username" if !@username || @username.empty? raise "A SoftLayer Client requires an api_key" if !@api_key || @api_key.empty? end From 440c1d9b303d3d499b625b6985b6112674353ce7 Mon Sep 17 00:00:00 2001 From: Renier Morales Date: Sun, 15 Nov 2015 22:34:43 -0400 Subject: [PATCH 3/4] Fix error when creating service without a valid client --- lib/softlayer/Client.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/softlayer/Client.rb b/lib/softlayer/Client.rb index dee1d6f..d22ba32 100644 --- a/lib/softlayer/Client.rb +++ b/lib/softlayer/Client.rb @@ -102,10 +102,10 @@ def initialize(options = {}) settings = Config.client_settings(options) # pick up the username from the options, the global, or assume no username - @username = settings[:username] || "" + @username = settings[:username] # do a similar thing for the api key - @api_key = settings[:api_key] || "" + @api_key = settings[:api_key] # grab token pair @userId = settings[:userId] @@ -120,11 +120,6 @@ def initialize(options = {}) # and assign a time out if the settings offer one @network_timeout = settings[:timeout] if settings.has_key?(:timeout) - unless token_based? - raise "A SoftLayer Client requires a username" if !@username || @username.empty? - raise "A SoftLayer Client requires an api_key" if !@api_key || @api_key.empty? - end - raise "A SoftLayer Client requires an endpoint URL" if !@endpoint_url || @endpoint_url.empty? end @@ -133,6 +128,11 @@ def token_based? @userId && @authToken && !@authToken.empty? end + # return whether this client is using api_key-based authentication + def key_based? + @username && !@username.empty? && @api_key && !@api_key.empty? + end + # return a hash of the authentication headers for the client def authentication_headers if token_based? @@ -143,13 +143,15 @@ def authentication_headers 'authToken' => @authToken } } - else + elsif key_based? { 'authenticate' => { 'username' => @username, 'apiKey' => @api_key } } + else + {} end end From fc8fce029b0ed1cdfd804d18878429472cb3f9f3 Mon Sep 17 00:00:00 2001 From: Renier Morales Date: Sun, 15 Nov 2015 22:51:51 -0400 Subject: [PATCH 4/4] Adjust client tests to new auth flow --- spec/Client_spec.rb | 73 +++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/spec/Client_spec.rb b/spec/Client_spec.rb index 927488d..22dbf45 100644 --- a/spec/Client_spec.rb +++ b/spec/Client_spec.rb @@ -41,42 +41,43 @@ expect(client.api_key).to eq 'fake_key' end - it 'raises an error if passed an empty user name' do - expect do - $SL_API_USERNAME = '' - client = SoftLayer::Client.new(:api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error - - expect do - $SL_API_USERNAME = 'good_username' - $SL_API_KEY = 'sample' - client = SoftLayer::Client.new(:username => '', :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error - end - - it 'fails if the user name is nil' do - expect do - $SL_API_USERNAME = nil - client = SoftLayer::Client.new(:username => nil, :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error - end - - it 'fails if the api_key is empty' do - expect do - $SL_API_KEY = '' - client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error - - expect do - client = SoftLayer::Client.new(:username => 'fake_user', :api_key => '', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error - end - - it 'fails if the api_key is nil' do - expect do - $SL_API_KEY = nil - client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/', :api_key => nil) - end.to raise_error + it 'produces empty auth headers if the username is empty' do + + $SL_API_USERNAME = '' + client = SoftLayer::Client.new(:api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + + $SL_API_USERNAME = 'good_username' + $SL_API_KEY = 'sample' + client = SoftLayer::Client.new(:username => '', :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + end + + it 'produces empty auth headers if the username is nil' do + $SL_API_USERNAME = nil + client = SoftLayer::Client.new(:username => nil, :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + end + + it 'produces empty auth headers if the api_key is empty' do + $SL_API_KEY = '' + client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + + client = SoftLayer::Client.new(:username => 'fake_user', :api_key => '', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + end + + it 'produces empty auth headers if the api_key is nil' do + $SL_API_KEY = nil + client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/', :api_key => nil) + + expect(client.authentication_headers.empty?).to be true end it 'initializes by default with nil as the timeout' do