Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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
  • Loading branch information...
commit 8bbabd43a9f9ac62e1f8e48981913d45ed485eb7 1 parent f254616
@NZKoz NZKoz authored
View
77 activeresource/lib/active_resource/base.rb
@@ -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 @@ class << self
# 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 @@ def site
# 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 @@ def format # :nodoc:
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
19 activeresource/lib/active_resource/connection.rb
@@ -55,7 +55,7 @@ def allowed_methods
# 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 @@ def requests
# 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 @@ def initialize(site, format = ActiveResource::Formats[:xml])
# 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 @@ def build_request_headers(headers)
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
32 activeresource/test/authorization_test.rb
@@ -45,6 +45,38 @@ def test_authorization_header_with_password_but_no_username
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
3  activeresource/test/base/custom_methods_test.rb
@@ -32,6 +32,9 @@ def setup
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
152 activeresource/test/base_test.rb
@@ -42,6 +42,9 @@ def setup
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 @@ def test_site_variable_can_be_reset
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 @@ def test_site_reader_uses_superclass_site_until_written
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 @@ def test_updating_baseclass_site_object_wipes_descendent_cached_connection_objec
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
Please sign in to comment.
Something went wrong with that request. Please try again.