From c40ac49af44202d8c88dea7828704b492d292873 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Tue, 16 Apr 2024 14:42:22 +0200 Subject: [PATCH 01/23] WIP: Client can now fetch tokens --- lib/sharepoint/client.rb | 92 +++++++++++++++++++++++++++++++++++++--- lib/sharepoint/errors.rb | 10 +++++ 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 7d59887..46c9526 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -11,8 +11,80 @@ module Sharepoint class Client FILENAME_INVALID_CHARS = '~"#%&*:<>?/\{|}' + attr_accessor :token + + class InvalidTokenError < StandardError + end + + class Token + attr_accessor :expires_in + attr_accessor :access_token + attr_accessor :fetched_at + attr_reader :config + + def initialize(config) + @config = config + end + + def ensure + return access_token unless access_token.nil? || expired? + fetch + end + + def to_s + access_token + end + + def fetch + auth_request = { + client_id: config.client_id, + client_secret: config.client_secret, + tenant_id: config.tenant_id, + cert_name: config.cert_name, + auth_scope: config.auth_scope + }.to_json + + headers = headers = {'Content-Type' => 'application/json'} + + ethon = Ethon::Easy.new(followlocation: true) + ethon.http_request(config.token_url, :post, body: auth_request, headers: headers) + ethon.perform + + raise InvalidTokenError.new(ethon.response_body.to_s) unless ethon.response_code == 200 + + response = JSON.parse(ethon.response_body) + + details = response["Token"] + self.fetched_at = Time.now.utc.to_i + self.expires_in = details["expires_in"] + self.access_token = details["access_token"] + end + + private + + def expired? + return true unless fetched_at && expires_in + + (fetched_at + expires_in) < Time.now.utc.to_i + end + end # endof Token + + def authenticating(&block) + ensure_token + yield + end + + def ensure_token + token.ensure + end + + def bearer_auth + "Bearer #{token.to_s}" + end + # @return [OpenStruct] The current configuration. attr_reader :config + attr_reader :token # Initializes a new client with given options. # @@ -23,6 +95,7 @@ class Client # @return [Sharepoint::Client] client object def initialize(config = {}) @config = OpenStruct.new(config) + @token = Token.new(@config) validate_config! end @@ -503,9 +576,11 @@ def computed_web_api_url(site) end def ethon_easy_json_requester - easy = ethon_easy_requester - easy.headers = { 'accept' => 'application/json;odata=verbose' } - easy + authenticating do + easy = ethon_easy_requester + easy.headers = { 'accept'=> 'application/json;odata=verbose', 'authentication' => bearer_auth } + easy + end end def ethon_easy_options @@ -584,9 +659,16 @@ def extract_paths(url) } end + def validate_ouath_config + [:client_id, :client_secret, :tenant_id, :cert_name, :auth_scope].map do |opt| + next if config.send(opt).present? + opt + end.compact + end + def validate_config! - raise Errors::UsernameConfigurationError.new unless string_not_blank?(@config.username) - raise Errors::PasswordConfigurationError.new unless string_not_blank?(@config.password) + invalid_oauth_opts = validate_ouath_config + raise Errors::InvalidOauthConfigError.new(invalid_oauth_opts) unless invalid_oauth_opts.empty? raise Errors::UriConfigurationError.new unless valid_config_uri? raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash) end diff --git a/lib/sharepoint/errors.rb b/lib/sharepoint/errors.rb index d4fe870..f6a91a6 100644 --- a/lib/sharepoint/errors.rb +++ b/lib/sharepoint/errors.rb @@ -23,5 +23,15 @@ def initialize super('Invalid ethon easy options') end end + + class InvalidOauthConfigError < StandardError + def initialize(invalid_entries) + error_messages = invalid_entries.map do |e| + "Invalid #{e} in OAUTH configuration" + end + + super error_messages.join(',') + end + end end end From a3227c5a2d64e2c9caed2a039b13aede60aac75b Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Wed, 17 Apr 2024 14:18:19 +0200 Subject: [PATCH 02/23] WIP: add authentication Bearer header to the requests --- lib/sharepoint/client.rb | 65 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 46c9526..a761c21 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -44,7 +44,7 @@ def fetch auth_scope: config.auth_scope }.to_json - headers = headers = {'Content-Type' => 'application/json'} + headers = {'Content-Type' => 'application/json'} ethon = Ethon::Easy.new(followlocation: true) ethon.http_request(config.token_url, :post, body: auth_request, headers: headers) @@ -424,10 +424,8 @@ def upload(filename, content, path, site_path = nil) path = path[1..-1] if path[0].eql?('/') url = uri_escape "#{url}GetFolderByServerRelativeUrl('#{path}')/Files/Add(url='#{sanitized_filename}',overwrite=true)" easy = ethon_easy_json_requester - easy.headers = { - 'accept' => 'application/json;odata=verbose', - 'X-RequestDigest' => xrequest_digest(site_path) - } + easy.headers = with_authentication_header({ 'accept' => 'application/json;odata=verbose', + 'X-RequestDigest' => xrequest_digest(site_path) }) easy.http_request(url, :post, { body: content }) easy.perform check_and_raise_failure(easy) @@ -455,13 +453,11 @@ def update_metadata(filename, metadata, path, site_path = nil) prepared_metadata = prepare_metadata(metadata, __metadata['type']) easy = ethon_easy_json_requester - easy.headers = { - 'accept' => 'application/json;odata=verbose', - 'content-type' => 'application/json;odata=verbose', - 'X-RequestDigest' => xrequest_digest(site_path), - 'X-Http-Method' => 'PATCH', - 'If-Match' => '*' - } + easy.headers = with_authentication_header({ 'accept' => 'application/json;odata=verbose', + 'content-type' => 'application/json;odata=verbose', + 'X-RequestDigest' => xrequest_digest(site_path), + 'X-Http-Method' => 'PATCH', + 'If-Match' => "*" }) easy.http_request(update_metadata_url, :post, { body: prepared_metadata }) @@ -539,10 +535,10 @@ def process_url(url, fields) parsed_response_body = JSON.parse(easy.response_body) page_content = if fields - parsed_response_body['d']['results'].map { |v| v.fetch_values(*fields) } - else - parsed_response_body['d']['results'] - end + parsed_response_body['d']['results'].map{|v|v.fetch_values(*fields)} + else + parsed_response_body['d']['results'] + end if next_url = parsed_response_body['d']['__next'] page_content + process_url(next_url, fields) @@ -551,6 +547,14 @@ def process_url(url, fields) end end + def with_authentication_header(h) + h.merge(auth_header) + end + + def auth_header + {"Authorization" => bearer_auth } + end + def base_url config.uri end @@ -576,11 +580,9 @@ def computed_web_api_url(site) end def ethon_easy_json_requester - authenticating do - easy = ethon_easy_requester - easy.headers = { 'accept'=> 'application/json;odata=verbose', 'authentication' => bearer_auth } - easy - end + easy = ethon_easy_requester + easy.headers = with_authentication_header({ 'accept'=> 'application/json;odata=verbose'}) + easy end def ethon_easy_options @@ -588,10 +590,11 @@ def ethon_easy_options end def ethon_easy_requester - easy = Ethon::Easy.new({ httpauth: :ntlm, followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) - easy.username = config.username - easy.password = config.password - easy + authenticating do + easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy.headers = auth_header + easy + end end # When you send a POST request, the request must include the form digest @@ -838,13 +841,11 @@ def update_object_metadata(metadata, new_metadata, site_path = '') prepared_metadata = prepare_metadata(new_metadata, metadata['type']) easy = ethon_easy_json_requester - easy.headers = { - 'accept' => 'application/json;odata=verbose', - 'content-type' => 'application/json;odata=verbose', - 'X-RequestDigest' => xrequest_digest(site_path), - 'X-Http-Method' => 'PATCH', - 'If-Match' => '*' - } + easy.headers = with_authentication_header({ 'accept' => 'application/json;odata=verbose', + 'content-type' => 'application/json;odata=verbose', + 'X-RequestDigest' => xrequest_digest(site_path), + 'X-Http-Method' => 'PATCH', + 'If-Match' => "*" }) easy.http_request(update_metadata_url, :post, From 2a4d667f1a1107df3d698718685b64980327bbb7 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Wed, 17 Apr 2024 14:01:26 +0200 Subject: [PATCH 03/23] Proper naming of token's methods --- lib/sharepoint/client.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index a761c21..cfb55af 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -26,7 +26,7 @@ def initialize(config) @config = config end - def ensure + def get_or_fetch return access_token unless access_token.nil? || expired? fetch end @@ -70,12 +70,12 @@ def expired? end # endof Token def authenticating(&block) - ensure_token + get_token yield end - def ensure_token - token.ensure + def get_token + token.get_or_fetch end def bearer_auth From 9d76a6d9b9b625c9b6e7aa2f90105dd057df2331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Wed, 17 Apr 2024 16:38:09 +0200 Subject: [PATCH 04/23] Make tests pass --- .github/workflows/ruby.yml | 7 +- README.md | 17 ++- env-example | 7 +- lib/sharepoint/client.rb | 59 +++++---- lib/sharepoint/errors.rb | 12 -- lib/sharepoint/spec_helpers.rb | 17 +++ spec/lib/sharepoint/client_methods_spec.rb | 14 +-- spec/lib/sharepoint/client_spec.rb | 132 +++++++++++++++------ 8 files changed, 181 insertions(+), 84 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 013624d..3ab256e 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -26,9 +26,12 @@ jobs: continue-on-error: ${{ matrix.channel != 'stable' }} env: - SP_USERNAME: test - SP_PASSWORD: test SP_URL: http://localhost:1234/ + SP_CLIENT_ID: clientfoo + SP_CLIENT_SECRET: secretfoo + SP_TENANT_ID: tenantfoo + SP_CERT_NAME: certfoo + SP_AUTH_SCOPE: http://localhost:1234/ steps: - name: Install libmagic-dev diff --git a/README.md b/README.md index 32aafdb..795bf51 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,12 @@ You can instantiate a number of SharePoint clients in your application: ```rb client = Sharepoint::Client.new({ - username: 'username', - password: 'password', - uri: 'https://sharepoint_url' + client_id: "client_id", + client_secret: "client_secret", + tenant_id: "tenant_id", + cert_name: "cert_name", + auth_scope: "auth_scope", + uri: "http://sharepoint_url" }) ``` @@ -47,3 +50,11 @@ client.upload filename, content, path ```rb client.update_metadata filename, metadata, path ``` + +## Testing + +Create a .env file based on the `env-example` and run + +```bash +$ bundle exec rake +``` diff --git a/env-example b/env-example index 0eb4e30..60978c6 100644 --- a/env-example +++ b/env-example @@ -1,4 +1,7 @@ # SharePoint Access -SP_USERNAME= -SP_PASSWORD= +SP_CLIENT_ID= +SP_CLIENT_SECRET= +SP_TENANT_ID= +SP_CERT_NAME= +SP_AUTH_SCOPE= SP_URL= diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index cfb55af..20392c3 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -36,6 +36,23 @@ def to_s end def fetch + response = request_new_token + + details = response["Token"] + self.fetched_at = Time.now.utc.to_i + self.expires_in = details["expires_in"] + self.access_token = details["access_token"] + end + + private + + def expired? + return true unless fetched_at && expires_in + + (fetched_at + expires_in) < Time.now.utc.to_i + end + + def request_new_token auth_request = { client_id: config.client_id, client_secret: config.client_secret, @@ -52,20 +69,7 @@ def fetch raise InvalidTokenError.new(ethon.response_body.to_s) unless ethon.response_code == 200 - response = JSON.parse(ethon.response_body) - - details = response["Token"] - self.fetched_at = Time.now.utc.to_i - self.expires_in = details["expires_in"] - self.access_token = details["access_token"] - end - - private - - def expired? - return true unless fetched_at && expires_in - - (fetched_at + expires_in) < Time.now.utc.to_i + JSON.parse(ethon.response_body) end end # endof Token @@ -90,8 +94,11 @@ def bearer_auth # # @param [Hash] config The client options: # - `:uri` The SharePoint server's root url - # - `:username` self-explanatory - # - `:password` self-explanatory + # - `:client_id` self-explanatory + # - `:client_secret` self-explanatory + # - `:tenant_id` self-explanatory + # - `:cert_name` self-explanatory + # - `:auth_scope` self-explanatory # @return [Sharepoint::Client] client object def initialize(config = {}) @config = OpenStruct.new(config) @@ -664,26 +671,30 @@ def extract_paths(url) def validate_ouath_config [:client_id, :client_secret, :tenant_id, :cert_name, :auth_scope].map do |opt| - next if config.send(opt).present? + c = config.send(opt) + + next if c.present? && string_not_blank?(c) opt end.compact end def validate_config! invalid_oauth_opts = validate_ouath_config - raise Errors::InvalidOauthConfigError.new(invalid_oauth_opts) unless invalid_oauth_opts.empty? - raise Errors::UriConfigurationError.new unless valid_config_uri? - raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash) + + raise Errors::InvalidOauthConfigError.new(invalid_oauth_opts) unless invalid_oauth_opts.empty? + raise Errors::UriConfigurationError.new unless valid_uri?(config.auth_scope) + raise Errors::UriConfigurationError.new unless valid_uri?(config.uri) + raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash) end def string_not_blank?(object) !object.nil? && object != '' && object.is_a?(String) end - def valid_config_uri? - if @config.uri and @config.uri.is_a? String - uri = URI.parse(@config.uri) - uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) + def valid_uri?(which) + if which and which.is_a? String + uri = URI.parse(which) + uri.kind_of?(URI::HTTP) || uri.kind_of?(URI::HTTPS) else false end diff --git a/lib/sharepoint/errors.rb b/lib/sharepoint/errors.rb index f6a91a6..8b09449 100644 --- a/lib/sharepoint/errors.rb +++ b/lib/sharepoint/errors.rb @@ -1,17 +1,5 @@ module Sharepoint module Errors - class UsernameConfigurationError < StandardError - def initialize - super('Invalid Username Configuration') - end - end - - class PasswordConfigurationError < StandardError - def initialize - super('Invalid Password configuration') - end - end - class UriConfigurationError < StandardError def initialize super('Invalid Uri configuration') diff --git a/lib/sharepoint/spec_helpers.rb b/lib/sharepoint/spec_helpers.rb index ef073aa..28c4f49 100644 --- a/lib/sharepoint/spec_helpers.rb +++ b/lib/sharepoint/spec_helpers.rb @@ -11,12 +11,29 @@ def value_to_string(value) end end + def sp_config + { + uri: ENV['SP_URL'], + client_id: ENV['SP_CLIENT_ID'], + client_secret: ENV['SP_CLIENT_SECRET'], + tenant_id: ENV['SP_TENANT_ID'], + cert_name: ENV['SP_CERT_NAME'], + auth_scope: ENV['SP_AUTH_SCOPE'] + } + end + def mock_requests allow_any_instance_of(Ethon::Easy) .to receive(:perform) .and_return(nil) end + def mock_token_responses + allow_any_instance_of(Sharepoint::Client::Token) + .to receive(:request_new_token) + .and_return({"Token" => { "expires_in" => 3600, "access_token" => "access_token" }}) + end + def mock_responses(fixture_file) allow_any_instance_of(Ethon::Easy) .to receive(:response_code) diff --git a/spec/lib/sharepoint/client_methods_spec.rb b/spec/lib/sharepoint/client_methods_spec.rb index 1a4f8a9..451b156 100644 --- a/spec/lib/sharepoint/client_methods_spec.rb +++ b/spec/lib/sharepoint/client_methods_spec.rb @@ -3,15 +3,13 @@ require 'spec_helper' describe Sharepoint::Client do - before { mock_requests } - - let(:config) do - { - username: ENV.fetch('SP_USERNAME', nil), - password: ENV.fetch('SP_PASSWORD', nil), - uri: ENV.fetch('SP_URL', nil) - } + before do + mock_requests + mock_token_responses end + + let(:config) { sp_config } + let(:client) { described_class.new(config) } describe '#documents_for' do diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index 145348e..41ee879 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -3,11 +3,9 @@ require 'spec_helper' describe Sharepoint::Client do - let(:config) do - { username: ENV.fetch('SP_USERNAME', nil), - password: ENV.fetch('SP_PASSWORD', nil), - uri: ENV.fetch('SP_URL', nil) } - end + before { ENV['SP_URL'] = 'https://localhost:8888' } + + let(:config) { sp_config } describe '#initialize' do context 'with success' do @@ -20,7 +18,7 @@ it 'sets config object' do client_config = client.config expect(client_config).to be_a OpenStruct - %i[username password url].each do |key| + [:client_id, :client_secret, :tenant_id, :cert_name, :auth_scope, :url].each do |key| value = client_config.send(key) expect(value).to eq config[key] end @@ -30,12 +28,12 @@ expect(client.send(:base_url)).to eql(ENV.fetch('SP_URL', nil)) end - it 'sets base_api_url in the client' do - expect(client.send(:base_api_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/") + it "sets base_api_url in the client" do + expect(subject.send :base_api_url).to eql("#{ENV['SP_URL']}/_api/") end - it 'sets base_api_web_url in the client' do - expect(client.send(:base_api_web_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/web/") + it "sets base_api_web_url in the client" do + expect(subject.send :base_api_web_url).to eql("#{ENV['SP_URL']}/_api/web/") end end @@ -62,43 +60,111 @@ end end - context 'with failure' do - context 'with bad username' do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 }].each do |ocurrence| - it "raises username configuration error for #{ocurrence[:name]} username" do + context 'failure' do + + context "bad client_id" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| + + it "should raise client_id configuration error for #{ ocurrence[:name]} client_id" do wrong_config = config - wrong_config[:username] = ocurrence[:value] + wrong_config[:client_id] = ocurrence[:value] - expect do + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + end + end + end + + context "bad client_secret" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| + + it "should raise client_secret configuration error for #{ocurrence[:name]} client_secret" do + wrong_config = config + wrong_config[:client_secret] = ocurrence[:value] + + expect{ described_class.new(wrong_config) - end.to raise_error(Sharepoint::Errors::UsernameConfigurationError) + }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) end end end - context 'with bad password' do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 }].each do |ocurrence| - it "raises password configuration error for #{ocurrence[:name]} password" do + context "bad tenant_id" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| + + it "should raise tenant_id configuration error for #{ocurrence[:name]} tenant_id" do wrong_config = config - wrong_config[:password] = ocurrence[:value] + wrong_config[:tenant_id] = ocurrence[:value] - expect do + expect { described_class.new(wrong_config) - end.to raise_error(Sharepoint::Errors::PasswordConfigurationError) + }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) end end end - context 'with bad uri' do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 }, - { value: 'ftp://www.test.com', name: 'invalid uri' }].each do |ocurrence| - it "raises uri configuration error for #{ocurrence[:name]} uri" do + context "bad cert_name" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| + + it "should raise cert_name configuration error for #{ocurrence[:name]} cert_name" do + wrong_config = config + wrong_config[:cert_name] = ocurrence[:value] + + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + end + end + end + + context "bad auth_scope" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + + it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do + wrong_config = config + wrong_config[:auth_scope] = ocurrence[:value] + + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + end + end + + end + + context "bad auth_scope" do + [{ value: 'ftp://www.test.com', name: "invalid auth_scope" }].each do |ocurrence| + + it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do + wrong_config = config + wrong_config[:auth_scope] = ocurrence[:value] + + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::UriConfigurationError) + end + end + + end + + context "bad uri" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }, + { value: 'ftp://www.test.com', name: "invalid uri" }].each do |ocurrence| + + it "should raise uri configuration error for #{ocurrence[:name]} uri" do wrong_config = config wrong_config[:uri] = ocurrence[:value] From c48fe657c6feced15b1a945e831e1064ff4617c1 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Wed, 17 Apr 2024 17:23:08 +0200 Subject: [PATCH 05/23] Added sample values to the sample env --- env-example | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/env-example b/env-example index 60978c6..5484d5f 100644 --- a/env-example +++ b/env-example @@ -1,7 +1,8 @@ # SharePoint Access -SP_CLIENT_ID= -SP_CLIENT_SECRET= -SP_TENANT_ID= -SP_CERT_NAME= -SP_AUTH_SCOPE= -SP_URL= + +SP_CLIENT_ID= foo +SP_CLIENT_SECRET= foo +SP_TENANT_ID= foo +SP_CERT_NAME= foo +SP_AUTH_SCOPE= https://foobar.ifad.org +SP_URL= https://foobar.ifad.org From e4be23f30725299a00e58679f60e2fb96e7f318a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Thu, 18 Apr 2024 17:23:43 +0200 Subject: [PATCH 06/23] Allow NTLM and Token authentication --- .github/workflows/ruby.yml | 3 + env-example | 14 +- lib/sharepoint/client.rb | 115 ++++++---------- lib/sharepoint/client/token.rb | 64 +++++++++ lib/sharepoint/errors.rb | 20 ++- lib/sharepoint/spec_helpers.rb | 5 +- spec/lib/sharepoint/client_spec.rb | 212 +++++++++++++++++++---------- 7 files changed, 281 insertions(+), 152 deletions(-) create mode 100644 lib/sharepoint/client/token.rb diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 3ab256e..a7c2c45 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -27,11 +27,14 @@ jobs: env: SP_URL: http://localhost:1234/ + SP_AUTHENTICATION: token SP_CLIENT_ID: clientfoo SP_CLIENT_SECRET: secretfoo SP_TENANT_ID: tenantfoo SP_CERT_NAME: certfoo SP_AUTH_SCOPE: http://localhost:1234/ + SP_USERNAME: userfoo + SP_PASSWORD: passfoo steps: - name: Install libmagic-dev diff --git a/env-example b/env-example index 5484d5f..deaf01e 100644 --- a/env-example +++ b/env-example @@ -1,8 +1,8 @@ # SharePoint Access - -SP_CLIENT_ID= foo -SP_CLIENT_SECRET= foo -SP_TENANT_ID= foo -SP_CERT_NAME= foo -SP_AUTH_SCOPE= https://foobar.ifad.org -SP_URL= https://foobar.ifad.org +SP_AUTHENTICATION=token +SP_CLIENT_ID=foo +SP_CLIENT_SECRET=foo +SP_TENANT_ID=foo +SP_CERT_NAME=foo +SP_AUTH_SCOPE=https://foobar.ifad.org +SP_URL=https://foobar.ifad.org diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 20392c3..30a7e2c 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -6,72 +6,13 @@ require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' +require 'sharepoint/client/token' module Sharepoint class Client FILENAME_INVALID_CHARS = '~"#%&*:<>?/\{|}' - attr_accessor :token - - class InvalidTokenError < StandardError - end - - class Token - attr_accessor :expires_in - attr_accessor :access_token - attr_accessor :fetched_at - attr_reader :config - - def initialize(config) - @config = config - end - - def get_or_fetch - return access_token unless access_token.nil? || expired? - fetch - end - - def to_s - access_token - end - - def fetch - response = request_new_token - - details = response["Token"] - self.fetched_at = Time.now.utc.to_i - self.expires_in = details["expires_in"] - self.access_token = details["access_token"] - end - - private - - def expired? - return true unless fetched_at && expires_in - - (fetched_at + expires_in) < Time.now.utc.to_i - end - - def request_new_token - auth_request = { - client_id: config.client_id, - client_secret: config.client_secret, - tenant_id: config.tenant_id, - cert_name: config.cert_name, - auth_scope: config.auth_scope - }.to_json - - headers = {'Content-Type' => 'application/json'} - - ethon = Ethon::Easy.new(followlocation: true) - ethon.http_request(config.token_url, :post, body: auth_request, headers: headers) - ethon.perform - - raise InvalidTokenError.new(ethon.response_body.to_s) unless ethon.response_code == 200 - - JSON.parse(ethon.response_body) - end - end # endof Token + attr_accessor :token def authenticating(&block) get_token @@ -94,6 +35,9 @@ def bearer_auth # # @param [Hash] config The client options: # - `:uri` The SharePoint server's root url + # - `:authentication` The authentication method to use [:ntlm, :token] + # - `:username` self-explanatory + # - `:password` self-explanatory # - `:client_id` self-explanatory # - `:client_secret` self-explanatory # - `:tenant_id` self-explanatory @@ -597,10 +541,16 @@ def ethon_easy_options end def ethon_easy_requester - authenticating do - easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) - easy.headers = auth_header - easy + case config.authentication + when "token" + easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy.headers = auth_header + easy + when "ntlm" + easy = Ethon::Easy.new({ httpauth: :ntlm, followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy.username = config.username + easy.password = config.password + easy end end @@ -669,20 +619,39 @@ def extract_paths(url) } end - def validate_ouath_config - [:client_id, :client_secret, :tenant_id, :cert_name, :auth_scope].map do |opt| - c = config.send(opt) + def validate_token_config + valid_config_options( %i(client_id client_secret tenant_id cert_name auth_scope) ) + end + def validate_ntlm_config + valid_config_options( %i(username password) ) + end + + def valid_config_options(options = []) + options.map do |opt| + c = config.send(opt) + next if c.present? && string_not_blank?(c) opt end.compact end def validate_config! - invalid_oauth_opts = validate_ouath_config + raise Errors::InvalidAuthenticationError.new unless valid_authentication?(config.authentication) + + if config.authentication == "token" + invalid_token_opts = validate_token_config + + raise Errors::InvalidTokenConfigError.new(invalid_token_opts) unless invalid_token_opts.empty? + raise Errors::UriConfigurationError.new unless valid_uri?(config.auth_scope) + end - raise Errors::InvalidOauthConfigError.new(invalid_oauth_opts) unless invalid_oauth_opts.empty? - raise Errors::UriConfigurationError.new unless valid_uri?(config.auth_scope) + if config.authentication == "ntlm" + invalid_ntlm_opts = validate_ntlm_config + + raise Errors::InvalidNTLMConfigError.new(invalid_ntlm_opts) unless invalid_ntlm_opts.empty? + end + raise Errors::UriConfigurationError.new unless valid_uri?(config.uri) raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash) end @@ -700,6 +669,10 @@ def valid_uri?(which) end end + def valid_authentication?(which) + %w(ntlm token).include?(which) + end + # Waiting for RFC 3986 to be implemented, we need to escape square brackets def uri_escape(uri) URI::DEFAULT_PARSER.escape(uri).gsub('[', '%5B').gsub(']', '%5D') diff --git a/lib/sharepoint/client/token.rb b/lib/sharepoint/client/token.rb new file mode 100644 index 0000000..ae8cf26 --- /dev/null +++ b/lib/sharepoint/client/token.rb @@ -0,0 +1,64 @@ +module Sharepoint + class Client + class Token + class InvalidTokenError < StandardError + end + + attr_accessor :expires_in + attr_accessor :access_token + attr_accessor :fetched_at + attr_reader :config + + def initialize(config) + @config = config + end + + def get_or_fetch + return access_token unless access_token.nil? || expired? + fetch + end + + def to_s + access_token + end + + def fetch + response = request_new_token + + details = response["Token"] + self.fetched_at = Time.now.utc.to_i + self.expires_in = details["expires_in"] + self.access_token = details["access_token"] + end + + private + + def expired? + return true unless fetched_at && expires_in + + (fetched_at + expires_in) < Time.now.utc.to_i + end + + def + def request_new_token + auth_request = { + client_id: config.client_id, + client_secret: config.client_secret, + tenant_id: config.tenant_id, + cert_name: config.cert_name, + auth_scope: config.auth_scope + }.to_json + + headers = {'Content-Type' => 'application/json'} + + ethon = Ethon::Easy.new(followlocation: true) + ethon.http_request(config.token_url, :post, body: auth_request, headers: headers) + ethon.perform + + raise InvalidTokenError.new(ethon.response_body.to_s) unless ethon.response_code == 200 + + JSON.parse(ethon.response_body) + end + end + end +end diff --git a/lib/sharepoint/errors.rb b/lib/sharepoint/errors.rb index 8b09449..a56bba2 100644 --- a/lib/sharepoint/errors.rb +++ b/lib/sharepoint/errors.rb @@ -12,10 +12,26 @@ def initialize end end - class InvalidOauthConfigError < StandardError + class InvalidAuthenticationError < StandardError + def initialize + super "Invalid authentication mechanism" + end + end + + class InvalidTokenConfigError < StandardError + def initialize(invalid_entries) + error_messages = invalid_entries.map do |e| + "Invalid #{e} in Token configuration" + end + + super error_messages.join(',') + end + end + + class InvalidNTLMConfigError < StandardError def initialize(invalid_entries) error_messages = invalid_entries.map do |e| - "Invalid #{e} in OAUTH configuration" + "Invalid #{e} in NTLM configuration" end super error_messages.join(',') diff --git a/lib/sharepoint/spec_helpers.rb b/lib/sharepoint/spec_helpers.rb index 28c4f49..92b26a4 100644 --- a/lib/sharepoint/spec_helpers.rb +++ b/lib/sharepoint/spec_helpers.rb @@ -14,11 +14,14 @@ def value_to_string(value) def sp_config { uri: ENV['SP_URL'], + authentication: ENV['SP_AUTHENTICATION'], client_id: ENV['SP_CLIENT_ID'], client_secret: ENV['SP_CLIENT_SECRET'], tenant_id: ENV['SP_TENANT_ID'], cert_name: ENV['SP_CERT_NAME'], - auth_scope: ENV['SP_AUTH_SCOPE'] + auth_scope: ENV['SP_AUTH_SCOPE'], + username: ENV['SP_USERNAME'], + password: ENV['SP_PASSWORD'] } end diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index 41ee879..a276b1e 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -37,15 +37,28 @@ end end - context 'with ethon easy options' do - context 'with success' do - subject(:client) { described_class.new(config_ethon) } + context "correct authentication" do + [{ value: "ntlm", name: 'ntlm' }, + { value: "token", name: 'token' } ].each do |ocurrence| + it "should not raise authentication configuration error for #{ ocurrence[:name]} authentication" do + correct_config = config + correct_config[:authentication] = ocurrence[:value] + + expect { + described_class.new(correct_config) + }.not_to raise_error(Sharepoint::Errors::InvalidAuthenticationError) + end + end + end + + context 'ethon easy options' do + context 'success' do let(:config_ethon) { config.merge({ ethon_easy_options: ssl_verifypeer }) } let(:ssl_verifypeer) { { ssl_verifypeer: false } } it 'sets ethon easy options in the client' do - expect(client.send(:ethon_easy_options)).to eql(ssl_verifypeer) + expect(described_class.new(config_ethon).send(:ethon_easy_options)).to eql(ssl_verifypeer) end end @@ -62,115 +75,172 @@ context 'failure' do - context "bad client_id" do + context "bad authentication" do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 } ].each do |ocurrence| - it "should raise client_id configuration error for #{ ocurrence[:name]} client_id" do + it "should raise authentication configuration error for #{ ocurrence[:name]} authentication" do wrong_config = config - wrong_config[:client_id] = ocurrence[:value] + wrong_config[:authentication] = ocurrence[:value] expect { described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + }.to raise_error(Sharepoint::Errors::InvalidAuthenticationError) end end end + + context "token" do + before { ENV['SP_AUTHENTICATION'] = 'token' } + + context "bad client_id" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| + + it "should raise client_id configuration error for #{ ocurrence[:name]} client_id" do + wrong_config = config + wrong_config[:client_id] = ocurrence[:value] + + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end + end + end - context "bad client_secret" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| + context "bad client_secret" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| - it "should raise client_secret configuration error for #{ocurrence[:name]} client_secret" do - wrong_config = config - wrong_config[:client_secret] = ocurrence[:value] + it "should raise client_secret configuration error for #{ocurrence[:name]} client_secret" do + wrong_config = config + wrong_config[:client_secret] = ocurrence[:value] - expect{ - described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end end end - end - context "bad tenant_id" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| + context "bad tenant_id" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| - it "should raise tenant_id configuration error for #{ocurrence[:name]} tenant_id" do - wrong_config = config - wrong_config[:tenant_id] = ocurrence[:value] + it "should raise tenant_id configuration error for #{ocurrence[:name]} tenant_id" do + wrong_config = config + wrong_config[:tenant_id] = ocurrence[:value] - expect { - described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end end end - end - context "bad cert_name" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| + context "bad cert_name" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| - it "should raise cert_name configuration error for #{ocurrence[:name]} cert_name" do - wrong_config = config - wrong_config[:cert_name] = ocurrence[:value] + it "should raise cert_name configuration error for #{ocurrence[:name]} cert_name" do + wrong_config = config + wrong_config[:cert_name] = ocurrence[:value] - expect { - described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end end end - end - context "bad auth_scope" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 }].each do |ocurrence| + context "bad auth_scope" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| - it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do - wrong_config = config - wrong_config[:auth_scope] = ocurrence[:value] + it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do + wrong_config = config + wrong_config[:auth_scope] = ocurrence[:value] - expect { - described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidOauthConfigError) + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end end - end - end + end - context "bad auth_scope" do - [{ value: 'ftp://www.test.com', name: "invalid auth_scope" }].each do |ocurrence| + context "bad auth_scope" do + [{ value: 'ftp://www.test.com', name: "invalid auth_scope" }].each do |ocurrence| - it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do - wrong_config = config - wrong_config[:auth_scope] = ocurrence[:value] + it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do + wrong_config = config + wrong_config[:auth_scope] = ocurrence[:value] - expect { - described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::UriConfigurationError) + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::UriConfigurationError) + end end + end + context "bad uri" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }, + { value: 'ftp://www.test.com', name: "invalid uri" }].each do |ocurrence| + + it "should raise uri configuration error for #{ocurrence[:name]} uri" do + wrong_config = config + wrong_config[:uri] = ocurrence[:value] + + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::UriConfigurationError) + end + end + + end end - context "bad uri" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 }, - { value: 'ftp://www.test.com', name: "invalid uri" }].each do |ocurrence| + context "ntlm" do + before { ENV['SP_AUTHENTICATION'] = 'ntlm' } - it "should raise uri configuration error for #{ocurrence[:name]} uri" do - wrong_config = config - wrong_config[:uri] = ocurrence[:value] + context "bad username" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| - expect do + it "should raise username configuration error for #{ ocurrence[:name]} username" do + wrong_config = config + wrong_config[:username] = ocurrence[:value] + + expect { described_class.new(wrong_config) - end.to raise_error(Sharepoint::Errors::UriConfigurationError) + }.to raise_error(Sharepoint::Errors::InvalidNTLMConfigError) + end + end + end + + context "bad password" do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 } ].each do |ocurrence| + + it "should raise password configuration error for #{ocurrence[:name]} password" do + wrong_config = config + wrong_config[:password] = ocurrence[:value] + + expect { + described_class.new(wrong_config) + }.to raise_error(Sharepoint::Errors::InvalidNTLMConfigError) + end end end end From 377b0baf4335a3662bec8bc107cf7af97a993c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Thu, 18 Apr 2024 17:27:29 +0200 Subject: [PATCH 07/23] Documentation --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 795bf51..e935c42 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,11 @@ And then execute: You can instantiate a number of SharePoint clients in your application: +#### Token authentication + ```rb client = Sharepoint::Client.new({ + authentication: "token", client_id: "client_id", client_secret: "client_secret", tenant_id: "tenant_id", @@ -33,6 +36,17 @@ client = Sharepoint::Client.new({ }) ``` +#### NTLN authentication + +```rb +client = Sharepoint::Client.new({ + authentication: "mtlm", + username: "username", + password: "password", + uri: "http://sharepoint_url" +}) +``` + ### Get documents of a folder ```rb From ab1dfdba8a2271db3a96d770f743913f2c889f29 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Fri, 19 Apr 2024 16:30:34 +0200 Subject: [PATCH 08/23] Added new entry in the sample --- env-example | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/env-example b/env-example index deaf01e..996b2d0 100644 --- a/env-example +++ b/env-example @@ -4,5 +4,8 @@ SP_CLIENT_ID=foo SP_CLIENT_SECRET=foo SP_TENANT_ID=foo SP_CERT_NAME=foo -SP_AUTH_SCOPE=https://foobar.ifad.org -SP_URL=https://foobar.ifad.org +SP_AUTH_SCOPE=https://foobar.example.org +SP_URL=https://foobar.example.org +SP_USERNAME=foo +SP_PASSWORD=foo +SP_AUTHENTICATION= token | ntlm From 35a33c42078c448b1827ceb4cb23dca7661646a2 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Fri, 19 Apr 2024 16:44:26 +0200 Subject: [PATCH 09/23] Cleanup of authentication strategies --- lib/sharepoint/client.rb | 51 +++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 30a7e2c..5e64eea 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -12,7 +12,7 @@ module Sharepoint class Client FILENAME_INVALID_CHARS = '~"#%&*:<>?/\{|}' - attr_accessor :token + attr_accessor :token def authenticating(&block) get_token @@ -375,7 +375,7 @@ def upload(filename, content, path, site_path = nil) path = path[1..-1] if path[0].eql?('/') url = uri_escape "#{url}GetFolderByServerRelativeUrl('#{path}')/Files/Add(url='#{sanitized_filename}',overwrite=true)" easy = ethon_easy_json_requester - easy.headers = with_authentication_header({ 'accept' => 'application/json;odata=verbose', + easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', 'X-RequestDigest' => xrequest_digest(site_path) }) easy.http_request(url, :post, { body: content }) easy.perform @@ -404,7 +404,7 @@ def update_metadata(filename, metadata, path, site_path = nil) prepared_metadata = prepare_metadata(metadata, __metadata['type']) easy = ethon_easy_json_requester - easy.headers = with_authentication_header({ 'accept' => 'application/json;odata=verbose', + easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', 'content-type' => 'application/json;odata=verbose', 'X-RequestDigest' => xrequest_digest(site_path), 'X-Http-Method' => 'PATCH', @@ -498,11 +498,21 @@ def process_url(url, fields) end end - def with_authentication_header(h) - h.merge(auth_header) + def token_auth? + config.authentication == 'token' end - def auth_header + def ntlm_auth? + config.authentication == 'ntlm' + end + + def with_bearer_authentication_header(h) + return h if ntlm_auth? + + h.merge(bearer_auth_header) + end + + def bearer_auth_header {"Authorization" => bearer_auth } end @@ -532,7 +542,7 @@ def computed_web_api_url(site) def ethon_easy_json_requester easy = ethon_easy_requester - easy.headers = with_authentication_header({ 'accept'=> 'application/json;odata=verbose'}) + easy.headers = with_bearer_authentication_header({ 'accept'=> 'application/json;odata=verbose'}) easy end @@ -541,16 +551,15 @@ def ethon_easy_options end def ethon_easy_requester - case config.authentication - when "token" - easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) - easy.headers = auth_header - easy - when "ntlm" - easy = Ethon::Easy.new({ httpauth: :ntlm, followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) - easy.username = config.username - easy.password = config.password - easy + if token_auth? + easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy.headers = with_bearer_authentication_header({}) + easy + elsif ntlm_auth? + easy = Ethon::Easy.new({ httpauth: :ntlm, followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy.username = config.username + easy.password = config.password + easy end end @@ -626,11 +635,11 @@ def validate_token_config def validate_ntlm_config valid_config_options( %i(username password) ) end - + def valid_config_options(options = []) options.map do |opt| c = config.send(opt) - + next if c.present? && string_not_blank?(c) opt end.compact @@ -651,7 +660,7 @@ def validate_config! raise Errors::InvalidNTLMConfigError.new(invalid_ntlm_opts) unless invalid_ntlm_opts.empty? end - + raise Errors::UriConfigurationError.new unless valid_uri?(config.uri) raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash) end @@ -825,7 +834,7 @@ def update_object_metadata(metadata, new_metadata, site_path = '') prepared_metadata = prepare_metadata(new_metadata, metadata['type']) easy = ethon_easy_json_requester - easy.headers = with_authentication_header({ 'accept' => 'application/json;odata=verbose', + easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', 'content-type' => 'application/json;odata=verbose', 'X-RequestDigest' => xrequest_digest(site_path), 'X-Http-Method' => 'PATCH', From f36bedeebfb3baa188cdd66c0909cf329c6248d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Mon, 22 Apr 2024 10:05:41 +0200 Subject: [PATCH 10/23] Rubocop autofix --- lib/sharepoint/client.rb | 53 ++++---- lib/sharepoint/client/token.rb | 21 ++-- lib/sharepoint/errors.rb | 6 +- lib/sharepoint/spec_helpers.rb | 20 +-- spec/lib/sharepoint/client_spec.rb | 187 +++++++++++++---------------- 5 files changed, 136 insertions(+), 151 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 5e64eea..9f371fb 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -14,7 +14,7 @@ class Client attr_accessor :token - def authenticating(&block) + def authenticating get_token yield end @@ -24,7 +24,7 @@ def get_token end def bearer_auth - "Bearer #{token.to_s}" + "Bearer #{token}" end # @return [OpenStruct] The current configuration. @@ -376,7 +376,7 @@ def upload(filename, content, path, site_path = nil) url = uri_escape "#{url}GetFolderByServerRelativeUrl('#{path}')/Files/Add(url='#{sanitized_filename}',overwrite=true)" easy = ethon_easy_json_requester easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', - 'X-RequestDigest' => xrequest_digest(site_path) }) + 'X-RequestDigest' => xrequest_digest(site_path) }) easy.http_request(url, :post, { body: content }) easy.perform check_and_raise_failure(easy) @@ -404,11 +404,11 @@ def update_metadata(filename, metadata, path, site_path = nil) prepared_metadata = prepare_metadata(metadata, __metadata['type']) easy = ethon_easy_json_requester - easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', - 'content-type' => 'application/json;odata=verbose', - 'X-RequestDigest' => xrequest_digest(site_path), - 'X-Http-Method' => 'PATCH', - 'If-Match' => "*" }) + easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', + 'content-type' => 'application/json;odata=verbose', + 'X-RequestDigest' => xrequest_digest(site_path), + 'X-Http-Method' => 'PATCH', + 'If-Match' => '*' }) easy.http_request(update_metadata_url, :post, { body: prepared_metadata }) @@ -486,10 +486,10 @@ def process_url(url, fields) parsed_response_body = JSON.parse(easy.response_body) page_content = if fields - parsed_response_body['d']['results'].map{|v|v.fetch_values(*fields)} - else - parsed_response_body['d']['results'] - end + parsed_response_body['d']['results'].map { |v| v.fetch_values(*fields) } + else + parsed_response_body['d']['results'] + end if next_url = parsed_response_body['d']['__next'] page_content + process_url(next_url, fields) @@ -513,7 +513,7 @@ def with_bearer_authentication_header(h) end def bearer_auth_header - {"Authorization" => bearer_auth } + { 'Authorization' => bearer_auth } end def base_url @@ -542,7 +542,7 @@ def computed_web_api_url(site) def ethon_easy_json_requester easy = ethon_easy_requester - easy.headers = with_bearer_authentication_header({ 'accept'=> 'application/json;odata=verbose'}) + easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose' }) easy end @@ -629,11 +629,11 @@ def extract_paths(url) end def validate_token_config - valid_config_options( %i(client_id client_secret tenant_id cert_name auth_scope) ) + valid_config_options(%i[client_id client_secret tenant_id cert_name auth_scope]) end def validate_ntlm_config - valid_config_options( %i(username password) ) + valid_config_options(%i[username password]) end def valid_config_options(options = []) @@ -641,21 +641,22 @@ def valid_config_options(options = []) c = config.send(opt) next if c.present? && string_not_blank?(c) + opt end.compact end def validate_config! - raise Errors::InvalidAuthenticationError.new unless valid_authentication?(config.authentication) + raise Errors::InvalidAuthenticationError.new unless valid_authentication?(config.authentication) - if config.authentication == "token" + if config.authentication == 'token' invalid_token_opts = validate_token_config raise Errors::InvalidTokenConfigError.new(invalid_token_opts) unless invalid_token_opts.empty? raise Errors::UriConfigurationError.new unless valid_uri?(config.auth_scope) end - if config.authentication == "ntlm" + if config.authentication == 'ntlm' invalid_ntlm_opts = validate_ntlm_config raise Errors::InvalidNTLMConfigError.new(invalid_ntlm_opts) unless invalid_ntlm_opts.empty? @@ -672,14 +673,14 @@ def string_not_blank?(object) def valid_uri?(which) if which and which.is_a? String uri = URI.parse(which) - uri.kind_of?(URI::HTTP) || uri.kind_of?(URI::HTTPS) + uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) else false end end def valid_authentication?(which) - %w(ntlm token).include?(which) + %w[ntlm token].include?(which) end # Waiting for RFC 3986 to be implemented, we need to escape square brackets @@ -834,11 +835,11 @@ def update_object_metadata(metadata, new_metadata, site_path = '') prepared_metadata = prepare_metadata(new_metadata, metadata['type']) easy = ethon_easy_json_requester - easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', - 'content-type' => 'application/json;odata=verbose', - 'X-RequestDigest' => xrequest_digest(site_path), - 'X-Http-Method' => 'PATCH', - 'If-Match' => "*" }) + easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', + 'content-type' => 'application/json;odata=verbose', + 'X-RequestDigest' => xrequest_digest(site_path), + 'X-Http-Method' => 'PATCH', + 'If-Match' => '*' }) easy.http_request(update_metadata_url, :post, diff --git a/lib/sharepoint/client/token.rb b/lib/sharepoint/client/token.rb index ae8cf26..c230cf2 100644 --- a/lib/sharepoint/client/token.rb +++ b/lib/sharepoint/client/token.rb @@ -2,11 +2,9 @@ module Sharepoint class Client class Token class InvalidTokenError < StandardError - end - - attr_accessor :expires_in - attr_accessor :access_token - attr_accessor :fetched_at + end + + attr_accessor :expires_in, :access_token, :fetched_at attr_reader :config def initialize(config) @@ -15,6 +13,7 @@ def initialize(config) def get_or_fetch return access_token unless access_token.nil? || expired? + fetch end @@ -25,10 +24,10 @@ def to_s def fetch response = request_new_token - details = response["Token"] + details = response['Token'] self.fetched_at = Time.now.utc.to_i - self.expires_in = details["expires_in"] - self.access_token = details["access_token"] + self.expires_in = details['expires_in'] + self.access_token = details['access_token'] end private @@ -39,8 +38,8 @@ def expired? (fetched_at + expires_in) < Time.now.utc.to_i end - def - def request_new_token + def + def(_request_new_token) auth_request = { client_id: config.client_id, client_secret: config.client_secret, @@ -49,7 +48,7 @@ def request_new_token auth_scope: config.auth_scope }.to_json - headers = {'Content-Type' => 'application/json'} + headers = { 'Content-Type' => 'application/json' } ethon = Ethon::Easy.new(followlocation: true) ethon.http_request(config.token_url, :post, body: auth_request, headers: headers) diff --git a/lib/sharepoint/errors.rb b/lib/sharepoint/errors.rb index a56bba2..65ca127 100644 --- a/lib/sharepoint/errors.rb +++ b/lib/sharepoint/errors.rb @@ -14,7 +14,7 @@ def initialize class InvalidAuthenticationError < StandardError def initialize - super "Invalid authentication mechanism" + super('Invalid authentication mechanism') end end @@ -24,7 +24,7 @@ def initialize(invalid_entries) "Invalid #{e} in Token configuration" end - super error_messages.join(',') + super(error_messages.join(',')) end end @@ -34,7 +34,7 @@ def initialize(invalid_entries) "Invalid #{e} in NTLM configuration" end - super error_messages.join(',') + super(error_messages.join(',')) end end end diff --git a/lib/sharepoint/spec_helpers.rb b/lib/sharepoint/spec_helpers.rb index 92b26a4..7d05a0d 100644 --- a/lib/sharepoint/spec_helpers.rb +++ b/lib/sharepoint/spec_helpers.rb @@ -13,15 +13,15 @@ def value_to_string(value) def sp_config { - uri: ENV['SP_URL'], - authentication: ENV['SP_AUTHENTICATION'], - client_id: ENV['SP_CLIENT_ID'], - client_secret: ENV['SP_CLIENT_SECRET'], - tenant_id: ENV['SP_TENANT_ID'], - cert_name: ENV['SP_CERT_NAME'], - auth_scope: ENV['SP_AUTH_SCOPE'], - username: ENV['SP_USERNAME'], - password: ENV['SP_PASSWORD'] + uri: ENV.fetch('SP_URL', nil), + authentication: ENV.fetch('SP_AUTHENTICATION', nil), + client_id: ENV.fetch('SP_CLIENT_ID', nil), + client_secret: ENV.fetch('SP_CLIENT_SECRET', nil), + tenant_id: ENV.fetch('SP_TENANT_ID', nil), + cert_name: ENV.fetch('SP_CERT_NAME', nil), + auth_scope: ENV.fetch('SP_AUTH_SCOPE', nil), + username: ENV.fetch('SP_USERNAME', nil), + password: ENV.fetch('SP_PASSWORD', nil) } end @@ -34,7 +34,7 @@ def mock_requests def mock_token_responses allow_any_instance_of(Sharepoint::Client::Token) .to receive(:request_new_token) - .and_return({"Token" => { "expires_in" => 3600, "access_token" => "access_token" }}) + .and_return({ 'Token' => { 'expires_in' => 3600, 'access_token' => 'access_token' } }) end def mock_responses(fixture_file) diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index a276b1e..8f4a112 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -18,7 +18,7 @@ it 'sets config object' do client_config = client.config expect(client_config).to be_a OpenStruct - [:client_id, :client_secret, :tenant_id, :cert_name, :auth_scope, :url].each do |key| + %i[client_id client_secret tenant_id cert_name auth_scope url].each do |key| value = client_config.send(key) expect(value).to eq config[key] end @@ -28,26 +28,25 @@ expect(client.send(:base_url)).to eql(ENV.fetch('SP_URL', nil)) end - it "sets base_api_url in the client" do - expect(subject.send :base_api_url).to eql("#{ENV['SP_URL']}/_api/") + it 'sets base_api_url in the client' do + expect(subject.send(:base_api_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/") end - it "sets base_api_web_url in the client" do - expect(subject.send :base_api_web_url).to eql("#{ENV['SP_URL']}/_api/web/") + it 'sets base_api_web_url in the client' do + expect(subject.send(:base_api_web_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/web/") end end - context "correct authentication" do - [{ value: "ntlm", name: 'ntlm' }, - { value: "token", name: 'token' } ].each do |ocurrence| - - it "should not raise authentication configuration error for #{ ocurrence[:name]} authentication" do + context 'correct authentication' do + [{ value: 'ntlm', name: 'ntlm' }, + { value: 'token', name: 'token' }].each do |ocurrence| + it "does not raise authentication configuration error for #{ocurrence[:name]} authentication" do correct_config = config correct_config[:authentication] = ocurrence[:value] - expect { - described_class.new(correct_config) - }.not_to raise_error(Sharepoint::Errors::InvalidAuthenticationError) + expect do + described_class.new(correct_config) + end.not_to raise_error(Sharepoint::Errors::InvalidAuthenticationError) end end end @@ -74,172 +73,158 @@ end context 'failure' do - - context "bad authentication" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| - - it "should raise authentication configuration error for #{ ocurrence[:name]} authentication" do + context 'bad authentication' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises authentication configuration error for #{ocurrence[:name]} authentication" do wrong_config = config wrong_config[:authentication] = ocurrence[:value] - expect { - described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidAuthenticationError) + expect do + described_class.new(wrong_config) + end.to raise_error(Sharepoint::Errors::InvalidAuthenticationError) end end end - - context "token" do - before { ENV['SP_AUTHENTICATION'] = 'token' } - context "bad client_id" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| + context 'token' do + before { ENV['SP_AUTHENTICATION'] = 'token' } - it "should raise client_id configuration error for #{ ocurrence[:name]} client_id" do + context 'bad client_id' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises client_id configuration error for #{ocurrence[:name]} client_id" do wrong_config = config wrong_config[:client_id] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) end end end - context "bad client_secret" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| - - it "should raise client_secret configuration error for #{ocurrence[:name]} client_secret" do + context 'bad client_secret' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises client_secret configuration error for #{ocurrence[:name]} client_secret" do wrong_config = config wrong_config[:client_secret] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) end end end - context "bad tenant_id" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| - - it "should raise tenant_id configuration error for #{ocurrence[:name]} tenant_id" do + context 'bad tenant_id' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises tenant_id configuration error for #{ocurrence[:name]} tenant_id" do wrong_config = config wrong_config[:tenant_id] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) end end end - context "bad cert_name" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| - - it "should raise cert_name configuration error for #{ocurrence[:name]} cert_name" do + context 'bad cert_name' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises cert_name configuration error for #{ocurrence[:name]} cert_name" do wrong_config = config wrong_config[:cert_name] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) end end end - context "bad auth_scope" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 }].each do |ocurrence| - - it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do + context 'bad auth_scope' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises auth_scope configuration error for #{ocurrence[:name]} auth_scope" do wrong_config = config wrong_config[:auth_scope] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) + end.to raise_error(Sharepoint::Errors::InvalidTokenConfigError) end end - end - context "bad auth_scope" do - [{ value: 'ftp://www.test.com', name: "invalid auth_scope" }].each do |ocurrence| - - it "should raise auth_scope configuration error for #{ocurrence[:name]} auth_scope" do + context 'bad auth_scope' do + [{ value: 'ftp://www.test.com', name: 'invalid auth_scope' }].each do |ocurrence| + it "raises auth_scope configuration error for #{ocurrence[:name]} auth_scope" do wrong_config = config wrong_config[:auth_scope] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::UriConfigurationError) + end.to raise_error(Sharepoint::Errors::UriConfigurationError) end end - end - context "bad uri" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 }, - { value: 'ftp://www.test.com', name: "invalid uri" }].each do |ocurrence| - - it "should raise uri configuration error for #{ocurrence[:name]} uri" do + context 'bad uri' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }, + { value: 'ftp://www.test.com', name: 'invalid uri' }].each do |ocurrence| + it "raises uri configuration error for #{ocurrence[:name]} uri" do wrong_config = config wrong_config[:uri] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::UriConfigurationError) + end.to raise_error(Sharepoint::Errors::UriConfigurationError) end end - end end - context "ntlm" do + context 'ntlm' do before { ENV['SP_AUTHENTICATION'] = 'ntlm' } - context "bad username" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| - - it "should raise username configuration error for #{ ocurrence[:name]} username" do + context 'bad username' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises username configuration error for #{ocurrence[:name]} username" do wrong_config = config wrong_config[:username] = ocurrence[:value] - expect { - described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidNTLMConfigError) + expect do + described_class.new(wrong_config) + end.to raise_error(Sharepoint::Errors::InvalidNTLMConfigError) end end end - context "bad password" do - [{ value: nil, name: 'nil' }, - { value: '', name: 'blank' }, - { value: 344, name: 344 } ].each do |ocurrence| - - it "should raise password configuration error for #{ocurrence[:name]} password" do + context 'bad password' do + [{ value: nil, name: 'nil' }, + { value: '', name: 'blank' }, + { value: 344, name: 344 }].each do |ocurrence| + it "raises password configuration error for #{ocurrence[:name]} password" do wrong_config = config wrong_config[:password] = ocurrence[:value] - expect { + expect do described_class.new(wrong_config) - }.to raise_error(Sharepoint::Errors::InvalidNTLMConfigError) + end.to raise_error(Sharepoint::Errors::InvalidNTLMConfigError) end end end From 0afe025c7035249c1c24e54ce2de57862bc02669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Mon, 22 Apr 2024 10:34:51 +0200 Subject: [PATCH 11/23] Rubocop manual fixes --- lib/sharepoint/client.rb | 35 +++++++++++++++-------------- lib/sharepoint/client/token.rb | 6 +++-- spec/lib/sharepoint/client_spec.rb | 36 +++++++++++++++--------------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 9f371fb..c33e37c 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -9,18 +9,16 @@ require 'sharepoint/client/token' module Sharepoint - class Client + class Client # rubocop:disable Metrics/ClassLength FILENAME_INVALID_CHARS = '~"#%&*:<>?/\{|}' - attr_accessor :token - def authenticating - get_token + generate_new_token yield end - def get_token - token.get_or_fetch + def generate_new_token + token.retrieve end def bearer_auth @@ -649,21 +647,24 @@ def valid_config_options(options = []) def validate_config! raise Errors::InvalidAuthenticationError.new unless valid_authentication?(config.authentication) - if config.authentication == 'token' - invalid_token_opts = validate_token_config + validate_token_config! if config.authentication == 'token' + validate_ntlm_config! if config.authentication == 'ntlm' - raise Errors::InvalidTokenConfigError.new(invalid_token_opts) unless invalid_token_opts.empty? - raise Errors::UriConfigurationError.new unless valid_uri?(config.auth_scope) - end + raise Errors::UriConfigurationError.new unless valid_uri?(config.uri) + raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash) + end - if config.authentication == 'ntlm' - invalid_ntlm_opts = validate_ntlm_config + def validate_token_config! + invalid_token_opts = validate_token_config - raise Errors::InvalidNTLMConfigError.new(invalid_ntlm_opts) unless invalid_ntlm_opts.empty? - end + raise Errors::InvalidTokenConfigError.new(invalid_token_opts) unless invalid_token_opts.empty? + raise Errors::UriConfigurationError.new unless valid_uri?(config.auth_scope) + end - raise Errors::UriConfigurationError.new unless valid_uri?(config.uri) - raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash) + def validate_ntlm_config! + invalid_ntlm_opts = validate_ntlm_config + + raise Errors::InvalidNTLMConfigError.new(invalid_ntlm_opts) unless invalid_ntlm_opts.empty? end def string_not_blank?(object) diff --git a/lib/sharepoint/client/token.rb b/lib/sharepoint/client/token.rb index c230cf2..05c226b 100644 --- a/lib/sharepoint/client/token.rb +++ b/lib/sharepoint/client/token.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module Sharepoint class Client - class Token + class Token # rubocop:disable Style/Documentation class InvalidTokenError < StandardError end @@ -11,7 +13,7 @@ def initialize(config) @config = config end - def get_or_fetch + def retrieve return access_token unless access_token.nil? || expired? fetch diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index 8f4a112..36bce0d 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -29,15 +29,15 @@ end it 'sets base_api_url in the client' do - expect(subject.send(:base_api_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/") + expect(client.send(:base_api_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/") end it 'sets base_api_web_url in the client' do - expect(subject.send(:base_api_web_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/web/") + expect(client.send(:base_api_web_url)).to eql("#{ENV.fetch('SP_URL', nil)}/_api/web/") end end - context 'correct authentication' do + context 'with authentication' do [{ value: 'ntlm', name: 'ntlm' }, { value: 'token', name: 'token' }].each do |ocurrence| it "does not raise authentication configuration error for #{ocurrence[:name]} authentication" do @@ -51,8 +51,8 @@ end end - context 'ethon easy options' do - context 'success' do + context 'with ethon easy options' do + context 'with success' do let(:config_ethon) { config.merge({ ethon_easy_options: ssl_verifypeer }) } let(:ssl_verifypeer) { { ssl_verifypeer: false } } @@ -72,8 +72,8 @@ end end - context 'failure' do - context 'bad authentication' do + context 'with failure' do + context 'with bad authentication' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| @@ -88,10 +88,10 @@ end end - context 'token' do + context 'with token' do before { ENV['SP_AUTHENTICATION'] = 'token' } - context 'bad client_id' do + context 'with bad client_id' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| @@ -106,7 +106,7 @@ end end - context 'bad client_secret' do + context 'with bad client_secret' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| @@ -121,7 +121,7 @@ end end - context 'bad tenant_id' do + context 'with bad tenant_id' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| @@ -136,7 +136,7 @@ end end - context 'bad cert_name' do + context 'with bad cert_name' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| @@ -151,7 +151,7 @@ end end - context 'bad auth_scope' do + context 'with bad auth_scope' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| @@ -166,7 +166,7 @@ end end - context 'bad auth_scope' do + context 'with bad auth_scope uri format' do [{ value: 'ftp://www.test.com', name: 'invalid auth_scope' }].each do |ocurrence| it "raises auth_scope configuration error for #{ocurrence[:name]} auth_scope" do wrong_config = config @@ -179,7 +179,7 @@ end end - context 'bad uri' do + context 'with bad uri' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }, @@ -196,10 +196,10 @@ end end - context 'ntlm' do + context 'when ntlm' do before { ENV['SP_AUTHENTICATION'] = 'ntlm' } - context 'bad username' do + context 'with bad username' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| @@ -214,7 +214,7 @@ end end - context 'bad password' do + context 'with bad password' do [{ value: nil, name: 'nil' }, { value: '', name: 'blank' }, { value: 344, name: 344 }].each do |ocurrence| From 624f59756b44c86486b80f771d80732c27f3ab5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Mon, 22 Apr 2024 10:37:34 +0200 Subject: [PATCH 12/23] Fix indentation --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e935c42..83e5b48 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ You can instantiate a number of SharePoint clients in your application: ```rb client = Sharepoint::Client.new({ - authentication: "token", - client_id: "client_id", - client_secret: "client_secret", - tenant_id: "tenant_id", - cert_name: "cert_name", - auth_scope: "auth_scope", - uri: "http://sharepoint_url" + authentication: "token", + client_id: "client_id", + client_secret: "client_secret", + tenant_id: "tenant_id", + cert_name: "cert_name", + auth_scope: "auth_scope", + uri: "http://sharepoint_url" }) ``` @@ -40,10 +40,10 @@ client = Sharepoint::Client.new({ ```rb client = Sharepoint::Client.new({ - authentication: "mtlm", - username: "username", - password: "password", - uri: "http://sharepoint_url" + authentication: "mtlm", + username: "username", + password: "password", + uri: "http://sharepoint_url" }) ``` From c1befbca5c169f68ccdd68b5fc73c2708a206b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lle=C3=AFr=20Borr=C3=A0s=20Metje?= Date: Mon, 22 Apr 2024 10:46:21 +0200 Subject: [PATCH 13/23] Small rubocop fix --- lib/sharepoint/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index c33e37c..8c40b7d 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -9,7 +9,7 @@ require 'sharepoint/client/token' module Sharepoint - class Client # rubocop:disable Metrics/ClassLength + class Client FILENAME_INVALID_CHARS = '~"#%&*:<>?/\{|}' def authenticating From 07e7f458d20016d49fbcb224d83a112968a2b439 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Mon, 22 Apr 2024 15:58:00 +0200 Subject: [PATCH 14/23] Dont parse auth scope --- lib/sharepoint/client.rb | 1 - spec/lib/sharepoint/client_spec.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 8c40b7d..985f2b5 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -658,7 +658,6 @@ def validate_token_config! invalid_token_opts = validate_token_config raise Errors::InvalidTokenConfigError.new(invalid_token_opts) unless invalid_token_opts.empty? - raise Errors::UriConfigurationError.new unless valid_uri?(config.auth_scope) end def validate_ntlm_config! diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index 36bce0d..5a24af0 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -166,7 +166,7 @@ end end - context 'with bad auth_scope uri format' do + skip 'with bad auth_scope uri format' do [{ value: 'ftp://www.test.com', name: 'invalid auth_scope' }].each do |ocurrence| it "raises auth_scope configuration error for #{ocurrence[:name]} auth_scope" do wrong_config = config From 9b5759097699da498b4e2b480ea151d74425e033 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Mon, 22 Apr 2024 16:02:57 +0200 Subject: [PATCH 15/23] Use autentication header when strategy is token --- lib/sharepoint/client.rb | 15 +++++++++------ lib/sharepoint/client/token.rb | 3 +-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 985f2b5..9319386 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -12,7 +12,7 @@ module Sharepoint class Client FILENAME_INVALID_CHARS = '~"#%&*:<>?/\{|}' - def authenticating + def authenticating_with_token generate_new_token yield end @@ -326,11 +326,11 @@ def create_folder(name, path, site_path = nil) path = path[1..-1] if path[0].eql?('/') url = uri_escape "#{url}GetFolderByServerRelativeUrl('#{path}')/Folders" easy = ethon_easy_json_requester - easy.headers = { + easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose', 'content-type' => 'application/json;odata=verbose', 'X-RequestDigest' => xrequest_digest(site_path) - } + }) payload = { '__metadata' => { 'type' => 'SP.Folder' @@ -539,6 +539,7 @@ def computed_web_api_url(site) end def ethon_easy_json_requester + easy = ethon_easy_requester easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose' }) easy @@ -550,9 +551,11 @@ def ethon_easy_options def ethon_easy_requester if token_auth? - easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) - easy.headers = with_bearer_authentication_header({}) - easy + authenticating_with_token do + easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy.headers = with_bearer_authentication_header({}) + easy + end elsif ntlm_auth? easy = Ethon::Easy.new({ httpauth: :ntlm, followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) easy.username = config.username diff --git a/lib/sharepoint/client/token.rb b/lib/sharepoint/client/token.rb index 05c226b..6a0f060 100644 --- a/lib/sharepoint/client/token.rb +++ b/lib/sharepoint/client/token.rb @@ -40,8 +40,7 @@ def expired? (fetched_at + expires_in) < Time.now.utc.to_i end - def - def(_request_new_token) + def request_new_token auth_request = { client_id: config.client_id, client_secret: config.client_secret, From 97ce50cce754c613ec723c1e773d1e8f5d513207 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Mon, 22 Apr 2024 17:12:48 +0200 Subject: [PATCH 16/23] Wip: working on specs --- lib/sharepoint/client.rb | 9 ++++----- lib/sharepoint/spec_helpers.rb | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 9319386..7af86a5 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -327,10 +327,10 @@ def create_folder(name, path, site_path = nil) url = uri_escape "#{url}GetFolderByServerRelativeUrl('#{path}')/Folders" easy = ethon_easy_json_requester easy.headers = with_bearer_authentication_header({ - 'accept' => 'application/json;odata=verbose', - 'content-type' => 'application/json;odata=verbose', - 'X-RequestDigest' => xrequest_digest(site_path) - }) + 'accept' => 'application/json;odata=verbose', + 'content-type' => 'application/json;odata=verbose', + 'X-RequestDigest' => xrequest_digest(site_path) + }) payload = { '__metadata' => { 'type' => 'SP.Folder' @@ -539,7 +539,6 @@ def computed_web_api_url(site) end def ethon_easy_json_requester - easy = ethon_easy_requester easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose' }) easy diff --git a/lib/sharepoint/spec_helpers.rb b/lib/sharepoint/spec_helpers.rb index 7d05a0d..6300d34 100644 --- a/lib/sharepoint/spec_helpers.rb +++ b/lib/sharepoint/spec_helpers.rb @@ -11,10 +11,10 @@ def value_to_string(value) end end - def sp_config + def sp_config(authentication: nil) { uri: ENV.fetch('SP_URL', nil), - authentication: ENV.fetch('SP_AUTHENTICATION', nil), + authentication: authentication || ENV.fetch('SP_AUTHENTICATION', nil), client_id: ENV.fetch('SP_CLIENT_ID', nil), client_secret: ENV.fetch('SP_CLIENT_SECRET', nil), tenant_id: ENV.fetch('SP_TENANT_ID', nil), From 886d3add99e7e2dd7b3f2d779ca56aa4549fa63b Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Tue, 23 Apr 2024 18:43:13 +0200 Subject: [PATCH 17/23] Added public method for requester --- lib/sharepoint/client.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 7af86a5..22ba35c 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -474,6 +474,10 @@ def index_field(list_name, field_name, site_path = '') update_object_metadata parsed_response_body['d']['__metadata'], { 'Indexed' => true }, site_path end + def requester + ethon_easy_requester + end + private def process_url(url, fields) From 199319e0f25a60912f78c7b02c40227802bbe2d2 Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Wed, 24 Apr 2024 13:34:52 +0200 Subject: [PATCH 18/23] Added tests on ethon_requester --- spec/lib/sharepoint/client_spec.rb | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index 5a24af0..6835eab 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -232,6 +232,47 @@ end end + describe '#ethon_requester' do + subject { client.send(:ethon_easy_requester) } + + let(:client) { described_class.new(client_config) } + let(:token) { double('Token', access_token: 'footoken') } + + before do + mock_token_responses + allow(client).to receive(:authenticating_with_token).and_call_original + end + + context 'when has token authentication' do + let(:client_config) { sp_config(authentication: 'token') } + + it 'calls authenticating_with_token' do + subject + expect(client).to have_received(:authenticating_with_token) + end + + it 'client token is set' do + subject + expect(client.token.access_token).not_to be_nil + end + end + + context 'when has ntlm authentication' do + subject { client.send(:ethon_easy_requester) } + + let(:client_config) { sp_config(authentication: 'ntlm') } + + it 'does not call authenticating_with_token' do + subject + expect(client).not_to have_received(:authenticating_with_token) + end + + it 'token is null' do + expect(client.token.access_token).to be_nil + end + end + end + describe '#remove_double_slashes' do { 'foobar' => 'foobar', From cb84bdbd62c23d97f45b7d3f649ca84d5f76d655 Mon Sep 17 00:00:00 2001 From: Danilo Grieco <53854308+gridanjbf@users.noreply.github.com> Date: Wed, 29 May 2024 14:19:04 +0200 Subject: [PATCH 19/23] Update README.md Co-authored-by: Geremia Taglialatela --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83e5b48..f082101 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ client = Sharepoint::Client.new({ }) ``` -#### NTLN authentication +#### NTLM authentication ```rb client = Sharepoint::Client.new({ From 8928f4a57d2b8ec57298626ae599ce1172814921 Mon Sep 17 00:00:00 2001 From: Danilo Grieco <53854308+gridanjbf@users.noreply.github.com> Date: Wed, 29 May 2024 14:19:22 +0200 Subject: [PATCH 20/23] Update README.md Co-authored-by: Geremia Taglialatela --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f082101..b43623e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ client = Sharepoint::Client.new({ ```rb client = Sharepoint::Client.new({ - authentication: "mtlm", + authentication: "ntlm", username: "username", password: "password", uri: "http://sharepoint_url" From 08653d0a4a2e53657569a2571fd5e2a904bf8f2f Mon Sep 17 00:00:00 2001 From: Danilo Grieco Date: Wed, 29 May 2024 14:31:42 +0200 Subject: [PATCH 21/23] Fixed rubocop offenses --- spec/lib/sharepoint/client_spec.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index 6835eab..fc8d672 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -166,7 +166,8 @@ end end - skip 'with bad auth_scope uri format' do + it 'with bad auth_scope uri format' do + skip 'Uri is not formatted' [{ value: 'ftp://www.test.com', name: 'invalid auth_scope' }].each do |ocurrence| it "raises auth_scope configuration error for #{ocurrence[:name]} auth_scope" do wrong_config = config @@ -233,10 +234,10 @@ end describe '#ethon_requester' do - subject { client.send(:ethon_easy_requester) } + subject(:requester) { client.send(:ethon_easy_requester) } let(:client) { described_class.new(client_config) } - let(:token) { double('Token', access_token: 'footoken') } + let(:token) { instance_double(Token, access_token: 'footoken') } before do mock_token_responses @@ -247,12 +248,12 @@ let(:client_config) { sp_config(authentication: 'token') } it 'calls authenticating_with_token' do - subject + requester expect(client).to have_received(:authenticating_with_token) end it 'client token is set' do - subject + requester expect(client.token.access_token).not_to be_nil end end @@ -263,7 +264,7 @@ let(:client_config) { sp_config(authentication: 'ntlm') } it 'does not call authenticating_with_token' do - subject + requester expect(client).not_to have_received(:authenticating_with_token) end From 831f20346fb68e2dd898ad4d6778c6ca3fc513dc Mon Sep 17 00:00:00 2001 From: Geremia Taglialatela Date: Thu, 30 May 2024 10:03:55 +0200 Subject: [PATCH 22/23] Move options to constants --- lib/sharepoint/client.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/sharepoint/client.rb b/lib/sharepoint/client.rb index 22ba35c..207d015 100644 --- a/lib/sharepoint/client.rb +++ b/lib/sharepoint/client.rb @@ -12,6 +12,12 @@ module Sharepoint class Client FILENAME_INVALID_CHARS = '~"#%&*:<>?/\{|}' + DEFAULT_TOKEN_ETHON_OPTIONS = { followlocation: 1, maxredirs: 5 }.freeze + VALID_TOKEN_CONFIG_OPTIONS = %i[client_id client_secret tenant_id cert_name auth_scope].freeze + + DEFAULT_NTLM_ETHON_OPTIONS = { httpauth: :ntlm, followlocation: 1, maxredirs: 5 }.freeze + VALID_NTLM_CONFIG_OPTIONS = %i[username password].freeze + def authenticating_with_token generate_new_token yield @@ -555,12 +561,12 @@ def ethon_easy_options def ethon_easy_requester if token_auth? authenticating_with_token do - easy = Ethon::Easy.new({ followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy = Ethon::Easy.new(DEFAULT_TOKEN_ETHON_OPTIONS.merge(ethon_easy_options)) easy.headers = with_bearer_authentication_header({}) easy end elsif ntlm_auth? - easy = Ethon::Easy.new({ httpauth: :ntlm, followlocation: 1, maxredirs: 5 }.merge(ethon_easy_options)) + easy = Ethon::Easy.new(DEFAULT_NTLM_ETHON_OPTIONS.merge(ethon_easy_options)) easy.username = config.username easy.password = config.password easy @@ -633,11 +639,11 @@ def extract_paths(url) end def validate_token_config - valid_config_options(%i[client_id client_secret tenant_id cert_name auth_scope]) + valid_config_options(VALID_TOKEN_CONFIG_OPTIONS) end def validate_ntlm_config - valid_config_options(%i[username password]) + valid_config_options(VALID_NTLM_CONFIG_OPTIONS) end def valid_config_options(options = []) From 919d0b6c21213e45ba75a1824fef1c4f614082cc Mon Sep 17 00:00:00 2001 From: Danilo Grieco <53854308+gridanjbf@users.noreply.github.com> Date: Thu, 30 May 2024 10:53:07 +0200 Subject: [PATCH 23/23] Update spec/lib/sharepoint/client_spec.rb Co-authored-by: Geremia Taglialatela --- spec/lib/sharepoint/client_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/sharepoint/client_spec.rb b/spec/lib/sharepoint/client_spec.rb index fc8d672..97a3a1f 100644 --- a/spec/lib/sharepoint/client_spec.rb +++ b/spec/lib/sharepoint/client_spec.rb @@ -46,7 +46,7 @@ expect do described_class.new(correct_config) - end.not_to raise_error(Sharepoint::Errors::InvalidAuthenticationError) + end.not_to raise_error end end end