diff options
-rw-r--r-- | activeresource/lib/active_resource/base.rb | 77 | ||||
-rw-r--r-- | activeresource/lib/active_resource/connection.rb | 19 | ||||
-rw-r--r-- | activeresource/test/authorization_test.rb | 32 | ||||
-rw-r--r-- | activeresource/test/base/custom_methods_test.rb | 3 | ||||
-rw-r--r-- | activeresource/test/base_test.rb | 152 |
5 files changed, 271 insertions, 12 deletions
diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 97baa46682..d79198f49b 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/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 @@ 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 diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index e4e6da50c9..cf4681b25b 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -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: diff --git a/activeresource/test/authorization_test.rb b/activeresource/test/authorization_test.rb index fabf4a42c2..05be7e3ef0 100644 --- a/activeresource/test/authorization_test.rb +++ b/activeresource/test/authorization_test.rb @@ -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"] diff --git a/activeresource/test/base/custom_methods_test.rb b/activeresource/test/base/custom_methods_test.rb index c6dda524fe..62c33ef9bc 100644 --- a/activeresource/test/base/custom_methods_test.rb +++ b/activeresource/test/base/custom_methods_test.rb @@ -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 diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb index 7606c56bcf..5db6f9113d 100644 --- a/activeresource/test/base_test.rb +++ b/activeresource/test/base_test.rb @@ -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 |