Skip to content

Commit

Permalink
Allow NTLM and Token authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
lleirborras committed Apr 18, 2024
1 parent 4ba0cca commit 7100801
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 146 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions env-example
Original file line number Diff line number Diff line change
@@ -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
115 changes: 44 additions & 71 deletions lib/sharepoint/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -94,6 +35,9 @@ def bearer_auth
#
# @param [Hash] options 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
Expand Down Expand Up @@ -595,10 +539,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

Expand Down Expand Up @@ -666,20 +616,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
Expand All @@ -697,6 +666,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')
Expand Down
64 changes: 64 additions & 0 deletions lib/sharepoint/client/token.rb
Original file line number Diff line number Diff line change
@@ -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
20 changes: 18 additions & 2 deletions lib/sharepoint/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,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(',')
Expand Down
5 changes: 4 additions & 1 deletion lib/sharepoint/spec_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,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

Expand Down
Loading

0 comments on commit 7100801

Please sign in to comment.