Skip to content

Commit

Permalink
Add user and password configuration options to ActiveResource::Base, …
Browse files Browse the repository at this point in the history
…not all credentials can be specified inline. Closes #11112 [ernesto.jimenez]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8891 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
NZKoz committed Feb 18, 2008
1 parent f254616 commit 8bbabd4
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 12 deletions.
77 changes: 71 additions & 6 deletions activeresource/lib/active_resource/base.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -85,16 +85,26 @@ module ActiveResource
# == Authentication # == Authentication
# #
# Many REST APIs will require authentication, usually in the form of basic # Many REST APIs will require authentication, usually in the form of basic
# HTTP authentication. Authentication can be specified by putting the credentials # HTTP authentication. Authentication can be specified by:
# in the +site+ variable of the Active Resource class you need to authenticate. # * putting the credentials in the URL for the +site+ variable.
# #
# class Person < ActiveResource::Base # class Person < ActiveResource::Base
# self.site = "http://ryan:password@api.people.com:3000/" # self.site = "http://ryan:password@api.people.com:3000/"
# end # end
# #
# * defining +user+ and/or +password+ variables
#
# class Person < ActiveResource::Base
# self.site = "http://api.people.com:3000/"
# self.user = "ryan"
# self.password = "password"
# end
#
# For obvious security reasons, it is probably best if such services are available # For obvious security reasons, it is probably best if such services are available
# over HTTPS. # over HTTPS.
# #
# Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
# as usernames. In those situations you should use the seperate user and password option.
# == Errors & Validation # == Errors & Validation
# #
# Error handling and validation is handled in much the same manner as you're used to seeing in # Error handling and validation is handled in much the same manner as you're used to seeing in
Expand Down Expand Up @@ -164,6 +174,21 @@ class << self
# Gets the URI of the REST resources to map for this class. The site variable is required # Gets the URI of the REST resources to map for this class. The site variable is required
# ActiveResource's mapping to work. # ActiveResource's mapping to work.
def site def site
# Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
#
# With superclass_delegating_reader
#
# Parent.site = 'http://anonymous@test.com'
# Subclass.site # => 'http://anonymous@test.com'
# Subclass.site.user = 'david'
# Parent.site # => 'http://david@test.com'
#
# Without superclass_delegating_reader (expected behaviour)
#
# Parent.site = 'http://anonymous@test.com'
# Subclass.site # => 'http://anonymous@test.com'
# Subclass.site.user = 'david' # => TypeError: can't modify frozen object
#
if defined?(@site) if defined?(@site)
@site @site
elsif superclass != Object && superclass.site elsif superclass != Object && superclass.site
Expand All @@ -175,7 +200,45 @@ def site
# The site variable is required ActiveResource's mapping to work. # The site variable is required ActiveResource's mapping to work.
def site=(site) def site=(site)
@connection = nil @connection = nil
@site = site.nil? ? nil : create_site_uri_from(site) if site.nil?
@site = nil
else
@site = create_site_uri_from(site)
@user = @site.user if @site.user
@password = @site.password if @site.password
end
end

# Gets the user for REST HTTP authentication
def user
# Not using superclass_delegating_reader. See +site+ for explanation
if defined?(@user)
@user
elsif superclass != Object && superclass.user
superclass.user.dup.freeze
end
end

# Sets the user for REST HTTP authentication
def user=(user)
@connection = nil
@user = user
end

# Gets the password for REST HTTP authentication
def password
# Not using superclass_delegating_reader. See +site+ for explanation
if defined?(@password)
@password
elsif superclass != Object && superclass.password
superclass.password.dup.freeze
end
end

# Sets the password for REST HTTP authentication
def password=(password)
@connection = nil
@password = password
end end


# Sets the format that attributes are sent and received in from a mime type reference. Example: # Sets the format that attributes are sent and received in from a mime type reference. Example:
Expand Down Expand Up @@ -206,6 +269,8 @@ def format # :nodoc:
def connection(refresh = false) def connection(refresh = false)
if defined?(@connection) || superclass == Object if defined?(@connection) || superclass == Object
@connection = Connection.new(site, format) if refresh || @connection.nil? @connection = Connection.new(site, format) if refresh || @connection.nil?
@connection.user = user if user
@connection.password = password if password
@connection @connection
else else
superclass.connection superclass.connection
Expand Down
19 changes: 16 additions & 3 deletions activeresource/lib/active_resource/connection.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def allowed_methods
# This class is used by ActiveResource::Base to interface with REST # This class is used by ActiveResource::Base to interface with REST
# services. # services.
class Connection class Connection
attr_reader :site attr_reader :site, :user, :password
attr_accessor :format attr_accessor :format


class << self class << self
Expand All @@ -68,13 +68,26 @@ def requests
# attribute to the URI for the remote resource service. # attribute to the URI for the remote resource service.
def initialize(site, format = ActiveResource::Formats[:xml]) def initialize(site, format = ActiveResource::Formats[:xml])
raise ArgumentError, 'Missing site URI' unless site raise ArgumentError, 'Missing site URI' unless site
@user = @password = nil
self.site = site self.site = site
self.format = format self.format = format
end end


# Set URI for remote service. # Set URI for remote service.
def site=(site) def site=(site)
@site = site.is_a?(URI) ? site : URI.parse(site) @site = site.is_a?(URI) ? site : URI.parse(site)
@user = @site.user if @site.user
@password = @site.password if @site.password
end

# Set user for remote service.
def user=(user)
@user = user
end

# Set password for remote service.
def password=(password)
@password = password
end end


# Execute a GET request. # Execute a GET request.
Expand Down Expand Up @@ -166,9 +179,9 @@ def build_request_headers(headers)
authorization_header.update(default_header).update(headers) authorization_header.update(default_header).update(headers)
end end


# Sets authorization header; authentication information is pulled from credentials provided with site URI. # Sets authorization header
def authorization_header def authorization_header
(@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {}) (@user || @password ? { 'Authorization' => 'Basic ' + ["#{@user}:#{ @password}"].pack('m').delete("\r\n") } : {})
end end


def logger #:nodoc: def logger #:nodoc:
Expand Down
32 changes: 32 additions & 0 deletions activeresource/test/authorization_test.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ def test_authorization_header_with_password_but_no_username
assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1] assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
end end


def test_authorization_header_explicitly_setting_username_and_password
@authenticated_conn = ActiveResource::Connection.new("http://@localhost")
@authenticated_conn.user = 'david'
@authenticated_conn.password = 'test123'
authorization_header = @authenticated_conn.send!(:authorization_header)
assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
authorization = authorization_header["Authorization"].to_s.split

assert_equal "Basic", authorization[0]
assert_equal ["david", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
end

def test_authorization_header_explicitly_setting_username_but_no_password
@conn = ActiveResource::Connection.new("http://@localhost")
@conn.user = "david"
authorization_header = @conn.send!(:authorization_header)
authorization = authorization_header["Authorization"].to_s.split

assert_equal "Basic", authorization[0]
assert_equal ["david"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
end

def test_authorization_header_explicitly_setting_password_but_no_username
@conn = ActiveResource::Connection.new("http://@localhost")
@conn.password = "test123"
authorization_header = @conn.send!(:authorization_header)
authorization = authorization_header["Authorization"].to_s.split

assert_equal "Basic", authorization[0]
assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
end

def test_get def test_get
david = @authenticated_conn.get("/people/2.xml") david = @authenticated_conn.get("/people/2.xml")
assert_equal "David", david["name"] assert_equal "David", david["name"]
Expand Down
3 changes: 3 additions & 0 deletions activeresource/test/base/custom_methods_test.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def setup
mock.put "/people/1/addresses/sort.xml?by=name", {}, nil, 204 mock.put "/people/1/addresses/sort.xml?by=name", {}, nil, 204
mock.post "/people/1/addresses/new/link.xml", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml' mock.post "/people/1/addresses/new/link.xml", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml'
end end

Person.user = nil
Person.password = nil
end end


def teardown def teardown
Expand Down
152 changes: 149 additions & 3 deletions activeresource/test/base_test.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def setup
mock.head "/people/1/addresses/2.xml", {}, nil, 404 mock.head "/people/1/addresses/2.xml", {}, nil, 404
mock.head "/people/2/addresses/1.xml", {}, nil, 404 mock.head "/people/2/addresses/1.xml", {}, nil, 404
end end

Person.user = nil
Person.password = nil
end end




Expand All @@ -68,6 +71,38 @@ def test_site_variable_can_be_reset
assert_nil actor.site assert_nil actor.site
end end


def test_should_accept_setting_user
Forum.user = 'david'
assert_equal('david', Forum.user)
assert_equal('david', Forum.connection.user)
end

def test_should_accept_setting_password
Forum.password = 'test123'
assert_equal('test123', Forum.password)
assert_equal('test123', Forum.connection.password)
end

def test_user_variable_can_be_reset
actor = Class.new(ActiveResource::Base)
actor.site = 'http://cinema'
assert_nil actor.user
actor.user = 'username'
actor.user = nil
assert_nil actor.user
assert_nil actor.connection.user
end

def test_password_variable_can_be_reset
actor = Class.new(ActiveResource::Base)
actor.site = 'http://cinema'
assert_nil actor.password
actor.password = 'username'
actor.password = nil
assert_nil actor.password
assert_nil actor.connection.password
end

def test_site_reader_uses_superclass_site_until_written def test_site_reader_uses_superclass_site_until_written
# Superclass is Object so returns nil. # Superclass is Object so returns nil.
assert_nil ActiveResource::Base.site assert_nil ActiveResource::Base.site
Expand Down Expand Up @@ -103,22 +138,133 @@ def test_site_reader_uses_superclass_site_until_written
apple = Class.new(fruit) apple = Class.new(fruit)


fruit.site = 'http://market' fruit.site = 'http://market'
assert_equal fruit.site, apple.site, 'subclass did not adopt changes to parent class' assert_equal fruit.site, apple.site, 'subclass did not adopt changes from parent class'


fruit.site = 'http://supermarket' fruit.site = 'http://supermarket'
assert_equal fruit.site, apple.site, 'subclass did not adopt changes to parent class' assert_equal fruit.site, apple.site, 'subclass did not adopt changes from parent class'
end end


def test_user_reader_uses_superclass_user_until_written
# Superclass is Object so returns nil.
assert_nil ActiveResource::Base.user
assert_nil Class.new(ActiveResource::Base).user
Person.user = 'anonymous'

# Subclass uses superclass user.
actor = Class.new(Person)
assert_equal Person.user, actor.user

# Subclass returns frozen superclass copy.
assert !Person.user.frozen?
assert actor.user.frozen?

# Changing subclass user doesn't change superclass user.
actor.user = 'david'
assert_not_equal Person.user, actor.user

# Changing superclass user doesn't overwrite subclass user.
Person.user = 'john'
assert_not_equal Person.user, actor.user

# Changing superclass user after subclassing changes subclass user.
jester = Class.new(actor)
actor.user = 'john.doe'
assert_equal actor.user, jester.user

# Subclasses are always equal to superclass user when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)

fruit.user = 'manager'
assert_equal fruit.user, apple.user, 'subclass did not adopt changes from parent class'

fruit.user = 'client'
assert_equal fruit.user, apple.user, 'subclass did not adopt changes from parent class'
end

def test_password_reader_uses_superclass_password_until_written
# Superclass is Object so returns nil.
assert_nil ActiveResource::Base.password
assert_nil Class.new(ActiveResource::Base).password
Person.password = 'my-password'

# Subclass uses superclass password.
actor = Class.new(Person)
assert_equal Person.password, actor.password

# Subclass returns frozen superclass copy.
assert !Person.password.frozen?
assert actor.password.frozen?

# Changing subclass password doesn't change superclass password.
actor.password = 'secret'
assert_not_equal Person.password, actor.password

# Changing superclass password doesn't overwrite subclass password.
Person.password = 'super-secret'
assert_not_equal Person.password, actor.password

# Changing superclass password after subclassing changes subclass password.
jester = Class.new(actor)
actor.password = 'even-more-secret'
assert_equal actor.password, jester.password

# Subclasses are always equal to superclass password when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)

fruit.password = 'mega-secret'
assert_equal fruit.password, apple.password, 'subclass did not adopt changes from parent class'

fruit.password = 'ok-password'
assert_equal fruit.password, apple.password, 'subclass did not adopt changes from parent class'
end

def test_updating_baseclass_site_object_wipes_descendent_cached_connection_objects def test_updating_baseclass_site_object_wipes_descendent_cached_connection_objects
# Subclasses are always equal to superclass site when not overridden # Subclasses are always equal to superclass site when not overridden
fruit = Class.new(ActiveResource::Base) fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit) apple = Class.new(fruit)


fruit.site = 'http://market' fruit.site = 'http://market'
assert_equal fruit.connection.site, apple.connection.site assert_equal fruit.connection.site, apple.connection.site
first_connection = apple.connection.object_id


fruit.site = 'http://supermarket' fruit.site = 'http://supermarket'
assert_equal fruit.connection.site, apple.connection.site assert_equal fruit.connection.site, apple.connection.site
second_connection = apple.connection.object_id
assert_not_equal(first_connection, second_connection, 'Connection should be re-created')
end

def test_updating_baseclass_user_wipes_descendent_cached_connection_objects
# Subclasses are always equal to superclass user when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)
fruit.site = 'http://market'

fruit.user = 'david'
assert_equal fruit.connection.user, apple.connection.user
first_connection = apple.connection.object_id

fruit.user = 'john'
assert_equal fruit.connection.user, apple.connection.user
second_connection = apple.connection.object_id
assert_not_equal(first_connection, second_connection, 'Connection should be re-created')
end

def test_updating_baseclass_password_wipes_descendent_cached_connection_objects
# Subclasses are always equal to superclass password when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)
fruit.site = 'http://market'

fruit.password = 'secret'
assert_equal fruit.connection.password, apple.connection.password
first_connection = apple.connection.object_id

fruit.password = 'supersecret'
assert_equal fruit.connection.password, apple.connection.password
second_connection = apple.connection.object_id
assert_not_equal(first_connection, second_connection, 'Connection should be re-created')
end end


def test_collection_name def test_collection_name
Expand Down

0 comments on commit 8bbabd4

Please sign in to comment.