Skip to content

Commit

Permalink
UPGRADE: Feature/sharepoint online (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
gridanjbf authored Jun 3, 2024
1 parent aa66649 commit cb17ecb
Show file tree
Hide file tree
Showing 9 changed files with 480 additions and 94 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ jobs:
continue-on-error: ${{ matrix.channel != 'stable' }}

env:
SP_USERNAME: test
SP_PASSWORD: test
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
Expand Down
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,28 @@ And then execute:

You can instantiate a number of SharePoint clients in your application:

#### Token authentication

```rb
client = Sharepoint::Client.new({
username: 'username',
password: 'password',
uri: 'https://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"
})
```

#### NTLM authentication

```rb
client = Sharepoint::Client.new({
authentication: "ntlm",
username: "username",
password: "password",
uri: "http://sharepoint_url"
})
```

Expand All @@ -47,3 +64,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
```
13 changes: 10 additions & 3 deletions env-example
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# SharePoint Access
SP_USERNAME=
SP_PASSWORD=
SP_URL=
SP_AUTHENTICATION=token
SP_CLIENT_ID=foo
SP_CLIENT_SECRET=foo
SP_TENANT_ID=foo
SP_CERT_NAME=foo
SP_AUTH_SCOPE=https://foobar.example.org
SP_URL=https://foobar.example.org
SP_USERNAME=foo
SP_PASSWORD=foo
SP_AUTHENTICATION= token | ntlm
159 changes: 124 additions & 35 deletions lib/sharepoint/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,51 @@

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 = '~"#%&*:<>?/\{|}'

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
end

def generate_new_token
token.retrieve
end

def bearer_auth
"Bearer #{token}"
end

# @return [OpenStruct] The current configuration.
attr_reader :config
attr_reader :token

# Initializes a new client with given options.
#
# @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
# - `:cert_name` self-explanatory
# - `:auth_scope` self-explanatory
# @return [Sharepoint::Client] client object
def initialize(config = {})
@config = OpenStruct.new(config)
@token = Token.new(@config)
validate_config!
end

Expand Down Expand Up @@ -304,11 +332,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 = {
'accept' => 'application/json;odata=verbose',
'content-type' => 'application/json;odata=verbose',
'X-RequestDigest' => xrequest_digest(site_path)
}
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'
Expand Down Expand Up @@ -351,10 +379,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_bearer_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)
Expand Down Expand Up @@ -382,13 +408,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_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 })
Expand Down Expand Up @@ -456,6 +480,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)
Expand All @@ -478,6 +506,24 @@ def process_url(url, fields)
end
end

def token_auth?
config.authentication == 'token'
end

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

def base_url
config.uri
end
Expand All @@ -504,7 +550,7 @@ def computed_web_api_url(site)

def ethon_easy_json_requester
easy = ethon_easy_requester
easy.headers = { 'accept' => 'application/json;odata=verbose' }
easy.headers = with_bearer_authentication_header({ 'accept' => 'application/json;odata=verbose' })
easy
end

Expand All @@ -513,10 +559,18 @@ 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
if token_auth?
authenticating_with_token do
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(DEFAULT_NTLM_ETHON_OPTIONS.merge(ethon_easy_options))
easy.username = config.username
easy.password = config.password
easy
end
end

# When you send a POST request, the request must include the form digest
Expand Down Expand Up @@ -584,26 +638,63 @@ def extract_paths(url)
}
end

def validate_token_config
valid_config_options(VALID_TOKEN_CONFIG_OPTIONS)
end

def validate_ntlm_config
valid_config_options(VALID_NTLM_CONFIG_OPTIONS)
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!
raise Errors::UsernameConfigurationError.new unless string_not_blank?(@config.username)
raise Errors::PasswordConfigurationError.new unless string_not_blank?(@config.password)
raise Errors::UriConfigurationError.new unless valid_config_uri?
raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash)
raise Errors::InvalidAuthenticationError.new unless valid_authentication?(config.authentication)

validate_token_config! if config.authentication == 'token'
validate_ntlm_config! if config.authentication == 'ntlm'

raise Errors::UriConfigurationError.new unless valid_uri?(config.uri)
raise Errors::EthonOptionsConfigurationError.new unless ethon_easy_options.is_a?(Hash)
end

def validate_token_config!
invalid_token_opts = validate_token_config

raise Errors::InvalidTokenConfigError.new(invalid_token_opts) unless invalid_token_opts.empty?
end

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)
!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)
def valid_uri?(which)
if which and which.is_a? String
uri = URI.parse(which)
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
else
false
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')
Expand Down Expand Up @@ -756,13 +847,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_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,
Expand Down
Loading

0 comments on commit cb17ecb

Please sign in to comment.