Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow reconfiguring Faraday with block and explain NTLM auth. #68

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,29 @@ Basic HTTP Authentication is supported via sending a username and password as se

svc = OData::Service.new "http://127.0.0.1:8989/SampleService/RubyOData.svc", { :username => "bob", :password=> "12345" }

NTLM authentication is also possible. Faraday lacks documentation how to use NTLM, even though multiple backends support it. Therefore, it is unclear what is the best way to achieve NTLM authentication, but a possibility is shown below.

require 'ruby_odata'
require 'httpclient'

class ConfigurableHTTPClient < Faraday::Adapter::HTTPClient
def initialize(*, &block)
@block = block
super
end

def call(env)
@block.call self if @block
super
end
end
Faraday::Adapter.register_middleware(configurable_httpclient: ConfigurableHTTPClient)

url = "http://127.0.0.1:8989/SampleService/RubyOData.svc"
svc = OData::Service.new url do |faraday|
faraday.adapter(:configurable_httpclient) { |a| a.client.set_auth url, "bob", "12345" }
end

### SSL/https Certificate Verification
The certificate verification mode can be passed in the options hash via the :verify_ssl key. For example, to ignore verification in order to use a self-signed certificate:

Expand Down
1 change: 0 additions & 1 deletion lib/ruby_odata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
require "active_support/inflector"
require "active_support/core_ext"
require "cgi"
require "excon"
require "faraday_middleware"
require "faraday"
require "nokogiri"
Expand Down
104 changes: 14 additions & 90 deletions lib/ruby_odata/resource.rb
Original file line number Diff line number Diff line change
@@ -1,149 +1,73 @@
module OData
class Resource
attr_reader :url, :options, :block

def initialize(url, options={}, backwards_compatibility=nil, &block)
@url = url
@block = block
@options = options.is_a?(Hash) ? options : { user: options, password: backwards_compatibility }

@conn = Faraday.new(url: url, ssl: { verify: verify_ssl }) do |faraday|
def initialize(url, options={})
@conn = Faraday.new(url: url, ssl: { verify: options[:verify_ssl] }) do |faraday|
faraday.use :gzip
faraday.response :raise_error
faraday.adapter :excon

faraday.options.timeout = timeout if timeout
faraday.options.open_timeout = open_timeout if open_timeout
faraday.options.timeout = options[:timeout] if options[:timeout]
faraday.options.open_timeout = options[:open_timeout] if options[:open_timeout]

faraday.headers = (faraday.headers || {}).merge(@options[:headers] || {})
faraday.headers = (faraday.headers || {}).merge(options[:headers] || {})
faraday.headers = (faraday.headers).merge({
:accept => '*/*; q=0.5, application/xml',
})

faraday.basic_auth user, password if user# this adds to headers so must be behind
end
faraday.basic_auth options[:user], options[:password] if options[:user] # this adds to headers so must be behind

@conn.headers[:user_agent] = 'Ruby'
yield faraday if block_given?
end
end

def get(additional_headers={})
def get(url, additional_headers={})
@conn.get do |req|
req.url url
req.headers = (headers || {}).merge(additional_headers)
end
end

def head(additional_headers={})
def head(url, additional_headers={})
@conn.head do |req|
req.url url
req.headers = (headers || {}).merge(additional_headers)
end
end

def post(payload, additional_headers={})
def post(url, payload, additional_headers={})
@conn.post do |req|
req.url url
req.headers = (headers || {}).merge(additional_headers)
req.body = prepare_payload payload
end
end

def put(payload, additional_headers={})
def put(url, payload, additional_headers={})
@conn.put do |req|
req.url url
req.headers = (headers || {}).merge(additional_headers)
req.body = prepare_payload payload
end
end

def patch(payload, additional_headers={})
def patch(url, payload, additional_headers={})
@conn.patch do |req|
req.url url
req.headers = (headers || {}).merge(additional_headers)
req.body = prepare_payload payload
end
end

def delete(additional_headers={})
def delete(url, additional_headers={})
@conn.delete do |req|
req.url url
req.headers = (headers || {}).merge(additional_headers)
end
end

def to_s
url
end

def user
options[:user]
end

def password
options[:password]
end

def verify_ssl
options[:verify_ssl]
end

def headers
@conn.headers || {}
end

def timeout
options[:timeout]
end

def open_timeout
options[:open_timeout]
end

# Construct a subresource, preserving authentication.
#
# Example:
#
# site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
# site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
#
# This is especially useful if you wish to define your site in one place and
# call it in multiple locations:
#
# def orders
# RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
# end
#
# orders.get # GET http://example.com/orders
# orders['1'].get # GET http://example.com/orders/1
# orders['1/items'].delete # DELETE http://example.com/orders/1/items
#
# Nest resources as far as you want:
#
# site = RestClient::Resource.new('http://example.com')
# posts = site['posts']
# first_post = posts['1']
# comments = first_post['comments']
# comments.post 'Hello', :content_type => 'text/plain'
#
def [](suburl, &new_block)
case
when block_given? then self.class.new(concat_urls(url, suburl), options, &new_block)
when block then self.class.new(concat_urls(url, suburl), options, &block)
else
self.class.new(concat_urls(url, suburl), options)
end
end

def concat_urls(url, suburl) # :nodoc:
url = url.to_s
suburl = suburl.to_s
if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
url + suburl
else
"#{url}/#{suburl}"
end
end

def prepare_payload payload
JSON.generate(payload)
rescue JSON::GeneratorError
Expand Down
27 changes: 14 additions & 13 deletions lib/ruby_odata/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class Service
# @option options [Hash] :rest_options a hash of rest-client options that will be passed to all OData::Resource.new calls
# @option options [Hash] :additional_params a hash of query string params that will be passed on all calls
# @option options [Boolean, true] :eager_partial true if queries should consume partial feeds until the feed is complete, false if explicit calls to next must be performed
def initialize(service_uri, options = {})
def initialize(service_uri, options = {}, &block)
@uri = service_uri.gsub!(/\/?$/, '')
set_options! options
default_instance_vars!
default_instance_vars!(service_uri, &block)
set_namespaces
build_collections_and_classes
end
Expand Down Expand Up @@ -97,7 +97,7 @@ def save_changes
# @raise [ServiceError] if there is an error when talking to the service
def execute
begin
@response = OData::Resource.new(build_query_uri, @rest_options).get
@response = @resource.get(build_query_uri)
rescue Exception => e
handle_exception(e)
end
Expand Down Expand Up @@ -147,7 +147,7 @@ def load_property(obj, nav_prop)
raise NotSupportedError, "You cannot load a property on an entity that isn't tracked" if obj.send(:__metadata).nil?
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless obj.respond_to?(nav_prop.to_sym)
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless @class_metadata[obj.class.to_s][nav_prop].nav_prop
results = OData::Resource.new(build_load_property_uri(obj, nav_prop), @rest_options).get
results = @resource.get build_load_property_uri(obj, nav_prop)
prop_results = build_classes_from_result(results.body)
obj.send "#{nav_prop}=", (singular?(nav_prop) ? prop_results.first : prop_results)
end
Expand Down Expand Up @@ -227,16 +227,17 @@ def set_options!(options)
@json_type = options[:json_type] || 'application/json'
end

def default_instance_vars!
def default_instance_vars!(service_uri, &block)
@collections = {}
@function_imports = {}
@save_operations = []
@has_partial = false
@next_uri = nil
@resource = OData::Resource.new(service_uri, @rest_options, &block)
end

def set_namespaces
@edmx = Nokogiri::XML(OData::Resource.new(build_metadata_uri, @rest_options).get.body)
@edmx = Nokogiri::XML(@resource.get(build_metadata_uri).body)
@ds_namespaces = {
"m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
"edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx",
Expand Down Expand Up @@ -509,7 +510,7 @@ def extract_partial(doc)

def handle_partial
if @next_uri
result = OData::Resource.new(@next_uri, @rest_options).get
result = @resource.get(@next_uri)
results = handle_collection_result(result.body)
end
results
Expand Down Expand Up @@ -600,21 +601,21 @@ def single_save(operation)
if operation.kind == "Add"
save_uri = build_save_uri(operation)
json_klass = operation.klass.to_json(:type => :add)
post_result = OData::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type}
post_result = @resource.post save_uri, json_klass, {:content_type => @json_type}
return build_classes_from_result(post_result.body)
elsif operation.kind == "Update"
update_uri = build_resource_uri(operation)
json_klass = operation.klass.to_json
update_result = OData::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => @json_type}
update_result = @resource.put update_uri, json_klass, {:content_type => @json_type}
return (update_result.status == 204)
elsif operation.kind == "Delete"
delete_uri = build_resource_uri(operation)
delete_result = OData::Resource.new(delete_uri, @rest_options).delete
delete_result = @resource.delete delete_uri
return (delete_result.status == 204)
elsif operation.kind == "AddLink"
save_uri = build_add_link_uri(operation)
json_klass = operation.child_klass.to_json(:type => :link)
post_result = OData::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type}
post_result = @resource.post save_uri, json_klass, {:content_type => @json_type}

# Attach the child to the parent
link_child_to_parent(operation) if (post_result.status == 204)
Expand All @@ -633,7 +634,7 @@ def batch_save(operations)
batch_uri = build_batch_uri

body = build_batch_body(operations, batch_num, changeset_num)
result = OData::Resource.new( batch_uri, @rest_options).post body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"}
result = @resource.post batch_uri, body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"}

# TODO: More result validation needs to be done.
# The result returns HTTP 202 even if there is an error in the batch
Expand Down Expand Up @@ -827,7 +828,7 @@ def execute_import_function(name, *args)
func[:parameters].keys.each_with_index { |key, i| params[key] = args[0][i] } unless func[:parameters].nil?

function_uri = build_function_import_uri(name, params)
result = OData::Resource.new(function_uri, @rest_options).send(func[:http_method].downcase, {})
result = @resource.send func[:http_method].downcase, function_uri, {}

# Is this a 204 (No content) result?
return true if result.status == 204
Expand Down
1 change: 0 additions & 1 deletion ruby_odata.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Gem::Specification.new do |s|
s.add_dependency("addressable", ">= 2.3.4")
s.add_dependency("i18n", ">= 0.7.0")
s.add_dependency("activesupport", ">= 3.0.0")
s.add_dependency("excon", "~> 0.45.3")
s.add_dependency("faraday_middleware")
s.add_dependency("faraday", "~> 0.9.1")
s.add_dependency("nokogiri", ">= 1.4.2")
Expand Down