From 7b837757e45494221d6a518b2455e1b211c42164 Mon Sep 17 00:00:00 2001 From: serknight Date: Mon, 19 Apr 2021 14:20:15 -0600 Subject: [PATCH 1/2] Fix threadsafe issue for all sensitive configuration options --- lib/xero-ruby/api/accounting_api.rb | 6 +++ lib/xero-ruby/api_client.rb | 28 +++++++---- lib/xero-ruby/configuration.rb | 11 +++++ spec/api_client_spec.rb | 77 +++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 9 deletions(-) diff --git a/lib/xero-ruby/api/accounting_api.rb b/lib/xero-ruby/api/accounting_api.rb index 96ac23d3..66f73545 100644 --- a/lib/xero-ruby/api/accounting_api.rb +++ b/lib/xero-ruby/api/accounting_api.rb @@ -7730,6 +7730,9 @@ def get_contact_history_with_http_info(xero_tenant_id, contact_id, options = {}) # @option opts [Boolean] :include_archived e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included in the response # @return [Contacts] def get_contacts(xero_tenant_id, opts = {}) + puts "\n\n\n" + puts "*****************************" + puts "opts #{opts}" data, _status_code, _headers = get_contacts_with_http_info(xero_tenant_id, opts) data end @@ -7746,6 +7749,9 @@ def get_contacts(xero_tenant_id, opts = {}) # @return [Array<(Contacts, Integer, Hash)>] Contacts data, response status code and response headers def get_contacts_with_http_info(xero_tenant_id, options = {}) opts = options.dup + + puts "opts == options ?" + puts "#{opts} == #{options} ?" if @api_client.config.debugging @api_client.config.logger.debug 'Calling API: AccountingApi.get_contacts ...' end diff --git a/lib/xero-ruby/api_client.rb b/lib/xero-ruby/api_client.rb index a80285ce..09fcf1a0 100644 --- a/lib/xero-ruby/api_client.rb +++ b/lib/xero-ruby/api_client.rb @@ -38,7 +38,8 @@ def initialize(config: {}, credentials: {}) @redirect_uri = credentials[:redirect_uri] @scopes = credentials[:scopes] @state = credentials[:state] - @config = append_to_default_config(config) + default_config = Configuration.default.clone + @config = append_to_default_config(default_config, config) @user_agent = "xero-ruby-#{VERSION}" @default_headers = { @@ -47,9 +48,9 @@ def initialize(config: {}, credentials: {}) } end - def append_to_default_config(user_config) - config = Configuration.default - user_config.each{|k,v| config.send("#{k}=", v) } if user_config + def append_to_default_config(default_config, user_config) + config = default_config + user_config.each{|k,v| config.send("#{k}=", v)} config end @@ -96,22 +97,30 @@ def payroll_uk_api # Token Helpers def token_set - XeroRuby.configure.token_set + @config.token_set end def access_token - XeroRuby.configure.access_token + @config.access_token + end + + def id_token + @config.id_token end def set_token_set(token_set) # helper to set the token_set on a client once the user # has a valid token set ( access_token & refresh_token ) - XeroRuby.configure.token_set = token_set + @config.token_set = token_set set_access_token(token_set['access_token']) end def set_access_token(access_token) - XeroRuby.configure.access_token = access_token + @config.access_token = access_token + end + + def set_id_token(id_token) + @config.id_token = id_token end def get_token_set_from_callback(params) @@ -265,7 +274,8 @@ def build_request(http_method, path, request, opts = {}) end end request.headers = header_params - request.options.timeout = @config.timeout + timeout = @config.timeout + request.options.timeout = timeout if timeout > 0 request.body = req_body request.url url request.params = query_params diff --git a/lib/xero-ruby/configuration.rb b/lib/xero-ruby/configuration.rb index ebf4163c..4e29b797 100644 --- a/lib/xero-ruby/configuration.rb +++ b/lib/xero-ruby/configuration.rb @@ -62,6 +62,7 @@ class Configuration # Defines the access token (Bearer) used with OAuth2. attr_accessor :access_token + attr_accessor :id_token # Defines the token set used with OAuth2. May include id/access/refresh token & other meta info. attr_accessor :token_set @@ -146,6 +147,8 @@ def initialize @payroll_au_url = 'https://api.xero.com/payroll.xro/1.0/' @payroll_nz_url = 'https://api.xero.com/payroll.xro/2.0/' @payroll_uk_url = 'https://api.xero.com/payroll.xro/2.0/' + @access_token = nil + @id_token = nil @api_key = {} @api_key_prefix = {} @timeout = 0 @@ -191,6 +194,14 @@ def base_path=(base_path) def base_url=(api_url) @base_url = api_url end + + def access_token=(access_token) + @access_token = access_token + end + + def id_token=(id_token) + @id_token = id_token + end # Gets API key (with prefix if set). # @param [String] param_name the parameter name of API key auth diff --git a/spec/api_client_spec.rb b/spec/api_client_spec.rb index 56a7c260..206aaf47 100644 --- a/spec/api_client_spec.rb +++ b/spec/api_client_spec.rb @@ -371,4 +371,81 @@ expect(api_client.sanitize_filename('.\sun.gif')).to eq('sun.gif') end end + + describe 'thread safety in the XeroClient' do + let(:creds) {{ + client_id: 'abc', + client_secret: '123', + redirect_uri: 'https://mydomain.com/callback', + scopes: 'openid profile email accounting.transactions' + }} + let(:api_client_1) {XeroRuby::ApiClient.new(credentials: creds)} + let(:api_client_2) {XeroRuby::ApiClient.new(credentials: creds)} + let(:api_client_3) {XeroRuby::ApiClient.new(credentials: creds)} + + let(:tkn_set_1){{id_token: "abc.123.1", access_token: "xxx.yyy.zzz.111"}} + let(:tkn_set_2){{id_token: "efg.456.2", access_token: "xxx.yyy.zzz.222"}} + + describe 'when configuration is changed, other instantiations of the client are not affected' do + it 'applies to #set_access_token' do + expect(api_client_1.access_token).to eq(nil) + expect(api_client_2.access_token).to eq(nil) + expect(api_client_3.access_token).to eq(nil) + + api_client_1.set_access_token("ACCESS_TOKEN_1") + expect(api_client_1.access_token).to eq("ACCESS_TOKEN_1") + expect(api_client_2.access_token).to eq(nil) + expect(api_client_3.access_token).to eq(nil) + + api_client_2.set_access_token("ACCESS_TOKEN_2") + expect(api_client_1.access_token).to eq("ACCESS_TOKEN_1") + expect(api_client_2.access_token).to eq("ACCESS_TOKEN_2") + expect(api_client_3.access_token).to eq(nil) + + api_client_3.set_access_token("ACCESS_TOKEN_3") + expect(api_client_1.access_token).to eq("ACCESS_TOKEN_1") + expect(api_client_2.access_token).to eq("ACCESS_TOKEN_2") + expect(api_client_3.access_token).to eq("ACCESS_TOKEN_3") + end + + it 'applies to #set_id_token' do + expect(api_client_1.id_token).to eq(nil) + expect(api_client_2.id_token).to eq(nil) + + api_client_1.set_id_token("ACCESS_TOKEN_1") + expect(api_client_1.id_token).to eq("ACCESS_TOKEN_1") + expect(api_client_2.id_token).to eq(nil) + + api_client_2.set_id_token("ACCESS_TOKEN_2") + expect(api_client_1.id_token).to eq("ACCESS_TOKEN_1") + expect(api_client_2.id_token).to eq("ACCESS_TOKEN_2") + end + + it 'applies to #set_token_set' do + expect(api_client_1.token_set).to eq(nil) + expect(api_client_2.token_set).to eq(nil) + + api_client_1.set_token_set(tkn_set_1) + expect(api_client_1.token_set).to eq(tkn_set_1) + expect(api_client_2.token_set).to eq(nil) + + api_client_2.set_token_set(tkn_set_2) + expect(api_client_1.token_set).to eq(tkn_set_1) + expect(api_client_2.token_set).to eq(tkn_set_2) + end + + it 'applies to #base_url' do + expect(api_client_1.config.base_url).to eq(nil) + expect(api_client_2.config.base_url).to eq(nil) + + api_client_1.accounting_api + expect(api_client_1.config.base_url).to eq(api_client_1.config.accounting_url) + expect(api_client_2.config.base_url).to eq(nil) + + api_client_2.files_api + expect(api_client_1.config.base_url).to eq(api_client_1.config.accounting_url) + expect(api_client_2.config.base_url).to eq(api_client_1.config.files_url) + end + end + end end From 9f8f356a73ca9f4fd9880912ce5823702a345a91 Mon Sep 17 00:00:00 2001 From: serknight Date: Mon, 19 Apr 2021 16:15:05 -0600 Subject: [PATCH 2/2] fix the thread safe issues. seemingly no breaking changes --- Gemfile.lock | 10 ++++++--- docs/accounting/index.html | 2 +- docs/assets/index.html | 2 +- docs/files/index.html | 2 +- docs/payroll_au/DeductionLine.md | 2 +- docs/payroll_au/index.html | 4 ++-- docs/payroll_nz/index.html | 2 +- docs/payroll_uk/index.html | 2 +- docs/projects/index.html | 2 +- lib/xero-ruby/api/accounting_api.rb | 6 ------ lib/xero-ruby/api_client.rb | 21 ++++++++++++++++++- lib/xero-ruby/configuration.rb | 4 +++- .../models/payroll_au/deduction_line.rb | 5 ----- lib/xero-ruby/version.rb | 4 ++-- spec/api_client_spec.rb | 14 ++++++++----- 15 files changed, 50 insertions(+), 32 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 78e7649e..b53a0ab5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - xero-ruby (2.9.0) + xero-ruby (2.9.1) faraday (~> 1.0, >= 1.0.1) json (~> 2.1, >= 2.1.0) @@ -12,11 +12,15 @@ GEM byebug (11.1.3) coderay (1.1.3) diff-lcs (1.4.4) - faraday (1.3.0) + faraday (1.4.1) + faraday-excon (~> 1.1) faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) multipart-post (>= 1.2, < 3) - ruby2_keywords + ruby2_keywords (>= 0.0.4) + faraday-excon (1.1.0) faraday-net_http (1.0.1) + faraday-net_http_persistent (1.1.0) jaro_winkler (1.5.4) json (2.5.1) method_source (1.0.0) diff --git a/docs/accounting/index.html b/docs/accounting/index.html index 75a6f4ed..a0c7533e 100644 --- a/docs/accounting/index.html +++ b/docs/accounting/index.html @@ -5795,7 +5795,7 @@ } else if (location.includes('php')){ sdkLang = 'php' sdkName = 'xero-php-oauth2' - } else if (location.includes('dotnet')){ + } else if (location.includes('xero-netstandard') || location.includes('csharp')){ sdkLang = 'dotnet' sdkName = 'Xero-NetStandard' } else if (location.includes('java')){ diff --git a/docs/assets/index.html b/docs/assets/index.html index 0edddd7c..6c223e2e 100644 --- a/docs/assets/index.html +++ b/docs/assets/index.html @@ -1365,7 +1365,7 @@ } else if (location.includes('php')){ sdkLang = 'php' sdkName = 'xero-php-oauth2' - } else if (location.includes('dotnet')){ + } else if (location.includes('xero-netstandard') || location.includes('csharp')){ sdkLang = 'dotnet' sdkName = 'Xero-NetStandard' } else if (location.includes('java')){ diff --git a/docs/files/index.html b/docs/files/index.html index a4f14598..555e26a9 100644 --- a/docs/files/index.html +++ b/docs/files/index.html @@ -1129,7 +1129,7 @@ } else if (location.includes('php')){ sdkLang = 'php' sdkName = 'xero-php-oauth2' - } else if (location.includes('dotnet')){ + } else if (location.includes('xero-netstandard') || location.includes('csharp')){ sdkLang = 'dotnet' sdkName = 'Xero-NetStandard' } else if (location.includes('java')){ diff --git a/docs/payroll_au/DeductionLine.md b/docs/payroll_au/DeductionLine.md index 01f60081..d2e7c8f0 100644 --- a/docs/payroll_au/DeductionLine.md +++ b/docs/payroll_au/DeductionLine.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **deduction_type_id** | **String** | Xero deduction type identifier | -**calculation_type** | [**DeductionTypeCalculationType**](DeductionTypeCalculationType.md) | | +**calculation_type** | [**DeductionTypeCalculationType**](DeductionTypeCalculationType.md) | | [optional] **amount** | **BigDecimal** | Deduction type amount | [optional] **percentage** | **BigDecimal** | The Percentage of the Deduction | [optional] **number_of_units** | **BigDecimal** | Deduction number of units | [optional] diff --git a/docs/payroll_au/index.html b/docs/payroll_au/index.html index 3bfbcc35..1664703f 100644 --- a/docs/payroll_au/index.html +++ b/docs/payroll_au/index.html @@ -970,7 +970,7 @@ }; defs["DeductionLine"] = { "title" : "", - "required" : [ "CalculationType", "DeductionTypeID" ], + "required" : [ "DeductionTypeID" ], "type" : "object", "properties" : { "DeductionTypeID" : { @@ -3207,7 +3207,7 @@ } else if (location.includes('php')){ sdkLang = 'php' sdkName = 'xero-php-oauth2' - } else if (location.includes('dotnet')){ + } else if (location.includes('xero-netstandard') || location.includes('csharp')){ sdkLang = 'dotnet' sdkName = 'Xero-NetStandard' } else if (location.includes('java')){ diff --git a/docs/payroll_nz/index.html b/docs/payroll_nz/index.html index de729b5f..f4347a68 100644 --- a/docs/payroll_nz/index.html +++ b/docs/payroll_nz/index.html @@ -3812,7 +3812,7 @@ } else if (location.includes('php')){ sdkLang = 'php' sdkName = 'xero-php-oauth2' - } else if (location.includes('dotnet')){ + } else if (location.includes('xero-netstandard') || location.includes('csharp')){ sdkLang = 'dotnet' sdkName = 'Xero-NetStandard' } else if (location.includes('java')){ diff --git a/docs/payroll_uk/index.html b/docs/payroll_uk/index.html index 9655516d..8edeb2ae 100644 --- a/docs/payroll_uk/index.html +++ b/docs/payroll_uk/index.html @@ -3480,7 +3480,7 @@ } else if (location.includes('php')){ sdkLang = 'php' sdkName = 'xero-php-oauth2' - } else if (location.includes('dotnet')){ + } else if (location.includes('xero-netstandard') || location.includes('csharp')){ sdkLang = 'dotnet' sdkName = 'Xero-NetStandard' } else if (location.includes('java')){ diff --git a/docs/projects/index.html b/docs/projects/index.html index 5a4c7785..884b7877 100644 --- a/docs/projects/index.html +++ b/docs/projects/index.html @@ -1442,7 +1442,7 @@ } else if (location.includes('php')){ sdkLang = 'php' sdkName = 'xero-php-oauth2' - } else if (location.includes('dotnet')){ + } else if (location.includes('xero-netstandard') || location.includes('csharp')){ sdkLang = 'dotnet' sdkName = 'Xero-NetStandard' } else if (location.includes('java')){ diff --git a/lib/xero-ruby/api/accounting_api.rb b/lib/xero-ruby/api/accounting_api.rb index 66f73545..96ac23d3 100644 --- a/lib/xero-ruby/api/accounting_api.rb +++ b/lib/xero-ruby/api/accounting_api.rb @@ -7730,9 +7730,6 @@ def get_contact_history_with_http_info(xero_tenant_id, contact_id, options = {}) # @option opts [Boolean] :include_archived e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included in the response # @return [Contacts] def get_contacts(xero_tenant_id, opts = {}) - puts "\n\n\n" - puts "*****************************" - puts "opts #{opts}" data, _status_code, _headers = get_contacts_with_http_info(xero_tenant_id, opts) data end @@ -7749,9 +7746,6 @@ def get_contacts(xero_tenant_id, opts = {}) # @return [Array<(Contacts, Integer, Hash)>] Contacts data, response status code and response headers def get_contacts_with_http_info(xero_tenant_id, options = {}) opts = options.dup - - puts "opts == options ?" - puts "#{opts} == #{options} ?" if @api_client.config.debugging @api_client.config.logger.debug 'Calling API: AccountingApi.get_contacts ...' end diff --git a/lib/xero-ruby/api_client.rb b/lib/xero-ruby/api_client.rb index 09fcf1a0..260c432c 100644 --- a/lib/xero-ruby/api_client.rb +++ b/lib/xero-ruby/api_client.rb @@ -191,7 +191,26 @@ def call_api(http_method, path, api_client, opts = {}) :client_key => @config.ssl_client_key } - connection = Faraday.new(:url => config.base_url, :ssl => ssl_options) do |conn| + case api_client + when "AccountingApi" + method_base_url = @config.accounting_url + when "AssetApi" + method_base_url = @config.asset_url + when "FilesApi" + method_base_url = @config.files_url + when "PayrollAuApi" + method_base_url = @config.payroll_au_url + when "PayrollNzApi" + method_base_url = @config.payroll_nz_url + when "PayrollUkApi" + method_base_url = @config.payroll_uk_url + when "ProjectApi" + method_base_url = @config.project_url + else + method_base_url = @config.accounting_url + end + + connection = Faraday.new(:url => method_base_url, :ssl => ssl_options) do |conn| conn.basic_auth(config.username, config.password) if opts[:header_params]["Content-Type"] == "multipart/form-data" conn.request :multipart diff --git a/lib/xero-ruby/configuration.rb b/lib/xero-ruby/configuration.rb index 4e29b797..78dbc44a 100644 --- a/lib/xero-ruby/configuration.rb +++ b/lib/xero-ruby/configuration.rb @@ -60,8 +60,10 @@ class Configuration # @return [String] attr_accessor :password - # Defines the access token (Bearer) used with OAuth2. + # Defines the access token (Bearer) used with OAuth2 authorization attr_accessor :access_token + + # Defines OpenID Connect id_token containing Xero user authentication detail attr_accessor :id_token # Defines the token set used with OAuth2. May include id/access/refresh token & other meta info. diff --git a/lib/xero-ruby/models/payroll_au/deduction_line.rb b/lib/xero-ruby/models/payroll_au/deduction_line.rb index 5f7b8c73..11f77d5d 100644 --- a/lib/xero-ruby/models/payroll_au/deduction_line.rb +++ b/lib/xero-ruby/models/payroll_au/deduction_line.rb @@ -97,10 +97,6 @@ def list_invalid_properties invalid_properties.push('invalid value for "deduction_type_id", deduction_type_id cannot be nil.') end - if @calculation_type.nil? - invalid_properties.push('invalid value for "calculation_type", calculation_type cannot be nil.') - end - invalid_properties end @@ -108,7 +104,6 @@ def list_invalid_properties # @return true if the model is valid def valid? return false if @deduction_type_id.nil? - return false if @calculation_type.nil? true end diff --git a/lib/xero-ruby/version.rb b/lib/xero-ruby/version.rb index 4a2d3681..1a013880 100644 --- a/lib/xero-ruby/version.rb +++ b/lib/xero-ruby/version.rb @@ -7,9 +7,9 @@ Generated by: https://openapi-generator.tech OpenAPI Generator version: 4.3.1 -The version of the XeroOpenAPI document: 2.10.4 +The version of the XeroOpenAPI document: 2.10.5 =end module XeroRuby - VERSION = '2.9.0' + VERSION = '2.9.1' end diff --git a/spec/api_client_spec.rb b/spec/api_client_spec.rb index 206aaf47..a38481b2 100644 --- a/spec/api_client_spec.rb +++ b/spec/api_client_spec.rb @@ -412,13 +412,13 @@ expect(api_client_1.id_token).to eq(nil) expect(api_client_2.id_token).to eq(nil) - api_client_1.set_id_token("ACCESS_TOKEN_1") - expect(api_client_1.id_token).to eq("ACCESS_TOKEN_1") + api_client_1.set_id_token("id_token_1") + expect(api_client_1.id_token).to eq("id_token_1") expect(api_client_2.id_token).to eq(nil) - api_client_2.set_id_token("ACCESS_TOKEN_2") - expect(api_client_1.id_token).to eq("ACCESS_TOKEN_1") - expect(api_client_2.id_token).to eq("ACCESS_TOKEN_2") + api_client_2.set_id_token("id_token_2") + expect(api_client_1.id_token).to eq("id_token_1") + expect(api_client_2.id_token).to eq("id_token_2") end it 'applies to #set_token_set' do @@ -445,6 +445,10 @@ api_client_2.files_api expect(api_client_1.config.base_url).to eq(api_client_1.config.accounting_url) expect(api_client_2.config.base_url).to eq(api_client_1.config.files_url) + + api_client_2.project_api + expect(api_client_1.config.base_url).to eq(api_client_1.config.accounting_url) + expect(api_client_2.config.base_url).to eq(api_client_1.config.project_url) end end end