Add user and password configuration options to ActiveResource::Base, 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
This commit is contained in:
Michael Koziarski
2008-02-18 00:21:18 +00:00
parent f2546164d6
commit 8bbabd43a9
5 changed files with 271 additions and 12 deletions

View File

@@ -85,16 +85,26 @@ module ActiveResource
# == Authentication
#
# Many REST APIs will require authentication, usually in the form of basic
# HTTP authentication. Authentication can be specified by putting the credentials
# in the +site+ variable of the Active Resource class you need to authenticate.
# HTTP authentication. Authentication can be specified by:
# * putting the credentials in the URL for the +site+ variable.
#
# class Person < ActiveResource::Base
# self.site = "http://ryan:password@api.people.com:3000/"
# end
# class Person < ActiveResource::Base
# self.site = "http://ryan:password@api.people.com:3000/"
# 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
# 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
#
# Error handling and validation is handled in much the same manner as you're used to seeing in
@@ -164,6 +174,21 @@ module ActiveResource
# Gets the URI of the REST resources to map for this class. The site variable is required
# ActiveResource's mapping to work.
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)
@site
elsif superclass != Object && superclass.site
@@ -175,7 +200,45 @@ module ActiveResource
# The site variable is required ActiveResource's mapping to work.
def site=(site)
@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
# Sets the format that attributes are sent and received in from a mime type reference. Example:
@@ -206,6 +269,8 @@ module ActiveResource
def connection(refresh = false)
if defined?(@connection) || superclass == Object
@connection = Connection.new(site, format) if refresh || @connection.nil?
@connection.user = user if user
@connection.password = password if password
@connection
else
superclass.connection

View File

@@ -55,7 +55,7 @@ module ActiveResource
# This class is used by ActiveResource::Base to interface with REST
# services.
class Connection
attr_reader :site
attr_reader :site, :user, :password
attr_accessor :format
class << self
@@ -68,6 +68,7 @@ module ActiveResource
# attribute to the URI for the remote resource service.
def initialize(site, format = ActiveResource::Formats[:xml])
raise ArgumentError, 'Missing site URI' unless site
@user = @password = nil
self.site = site
self.format = format
end
@@ -75,6 +76,18 @@ module ActiveResource
# Set URI for remote service.
def site=(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
# Execute a GET request.
@@ -166,9 +179,9 @@ module ActiveResource
authorization_header.update(default_header).update(headers)
end
# Sets authorization header; authentication information is pulled from credentials provided with site URI.
# Sets 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
def logger #:nodoc:

View File

@@ -45,6 +45,38 @@ class AuthorizationTest < Test::Unit::TestCase
assert_equal ["", "test123"], ActiveSupport::Base64.decode64(authorization[1]).split(":")[0..1]
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
david = @authenticated_conn.get("/people/2.xml")
assert_equal "David", david["name"]

View File

@@ -32,6 +32,9 @@ class CustomMethodsTest < Test::Unit::TestCase
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'
end
Person.user = nil
Person.password = nil
end
def teardown

View File

@@ -42,6 +42,9 @@ class BaseTest < Test::Unit::TestCase
mock.head "/people/1/addresses/2.xml", {}, nil, 404
mock.head "/people/2/addresses/1.xml", {}, nil, 404
end
Person.user = nil
Person.password = nil
end
@@ -68,6 +71,38 @@ class BaseTest < Test::Unit::TestCase
assert_nil actor.site
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
# Superclass is Object so returns nil.
assert_nil ActiveResource::Base.site
@@ -103,12 +138,88 @@ class BaseTest < Test::Unit::TestCase
apple = Class.new(fruit)
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'
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
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
# Subclasses are always equal to superclass site when not overridden
fruit = Class.new(ActiveResource::Base)
@@ -116,9 +227,44 @@ class BaseTest < Test::Unit::TestCase
fruit.site = 'http://market'
assert_equal fruit.connection.site, apple.connection.site
first_connection = apple.connection.object_id
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
def test_collection_name