From 75fca04590a310bedc66a455d7508168ae932ba4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 9 Jan 2005 15:20:00 +0000 Subject: Added authentication framework to protect actions behind a condition and redirect on failure. See ActionController::Authentication for more. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@351 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/CHANGELOG | 2 + actionpack/lib/action_controller.rb | 3 + actionpack/lib/action_controller/authentication.rb | 99 ++++++++++++++++++++++ actionpack/lib/action_controller/base.rb | 2 +- actionpack/lib/action_controller/filters.rb | 2 +- actionpack/test/controller/authentication_test.rb | 89 +++++++++++++++++++ actionpack/test/controller/render_test.rb | 5 ++ 7 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 actionpack/lib/action_controller/authentication.rb create mode 100644 actionpack/test/controller/authentication_test.rb diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index ca5b153552..94f10207c4 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Added authentication framework to protect actions behind a condition and redirect on failure. See ActionController::Authentication for more. + * Added the possibility of passing nil to UrlHelper#link_to to use the link itself as the name diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index d090fd4166..12a01cc4bb 100755 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -23,6 +23,7 @@ $:.unshift(File.dirname(__FILE__)) +require 'action_controller/support/core_ext' require 'action_controller/support/clean_logger' require 'action_controller/support/misc' require 'action_controller/support/dependencies' @@ -40,6 +41,7 @@ require 'action_controller/helpers' require 'action_controller/cookies' require 'action_controller/cgi_process' require 'action_controller/caching' +require 'action_controller/authentication' ActionController::Base.class_eval do include ActionController::Filters @@ -53,6 +55,7 @@ ActionController::Base.class_eval do include ActionController::Cookies include ActionController::Session include ActionController::Caching + include ActionController::Authentication end require 'action_view' diff --git a/actionpack/lib/action_controller/authentication.rb b/actionpack/lib/action_controller/authentication.rb new file mode 100644 index 0000000000..3ba193c1b2 --- /dev/null +++ b/actionpack/lib/action_controller/authentication.rb @@ -0,0 +1,99 @@ +module ActionController #:nodoc: + module Authentication #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # Authentication standardizes the need to protect certain actions unless a given condition is fulfilled. It doesn't address + # _how_ someone becomes authorized, but only that if the condition isn't fulfilled a redirect to a given place will happen. + # + # The authentication model is setup up in two stages. One to configure the authentication, which is often done in the super-most + # class (such as ApplicationController in Rails), and then the protection of actions in the individual controller subclasses: + # + # class ApplicationController < ActionController::Base + # authentication :by => '@session[:authenticated]', :failure => { :controller => "login" } + # end + # + # class WeblogController < ApplicationController + # authenticates :edit, :update + # + # def show() render_text "I showed something" end + # def index() render_text "I indexed something" end + # def edit() render_text "I edited something" end + # def update() render_text "I updated something" end + # def login() @session[:authenticated] = true; render_nothing end + # end + # + # In the example above, the edit and update methods are protected by an authentication condition that requires + # @session[:authenticated] to be true. If that is not the case, the request is redirected to LoginController#index. + # Note that the :by condition is enclosed in single quotes. This is because we want to defer evaluation of the condition until + # we're at run time. Also note, that the :failure option uses the same format as Base#url_for and friends do to perform the redirect. + module ClassMethods + # Enables authentication for this class and all its subclasses. + # + # Options are: + # * :by - the code fragment that will be evaluated on each request to determine whether the request is authenticated. + # * :failure - redirection options following the format of Base#url_for. + def authentication(options) + options.assert_valid_keys([:by, :failure]) + class_eval <<-EOV + protected + def actions_excepted_from_authentication + self.class.read_inheritable_attribute("actions_excepted_from_authentication") || [] + end + + def actions_included_in_authentication + actions = self.class.read_inheritable_attribute("actions_included_in_authentication") + + if actions == :all + action_methods.collect { |action| action.intern } + elsif actions.is_a?(Array) + actions + else + [] + end + end + + def action_needs_authentication? + if actions_excepted_from_authentication.include?(action_name.intern) + false + elsif actions_included_in_authentication.include?(action_name.intern) + true + elsif actions_excepted_from_authentication.length > 0 + true + else + false + end + end + + def authenticate + if !action_needs_authentication? || #{options[:by]} + return true + else + redirect_to(#{options[:failure].inspect}) + return false + end + end + EOV + + before_filter :authenticate + end + + # Protects the actions specified behind the authentication condition. + def authenticates(*actions) + write_inheritable_array("actions_included_in_authentication", actions) + end + + # Protects all the actions of this controller behind the authentication condition. + def authenticates_all + write_inheritable_attribute("actions_included_in_authentication", :all) + end + + # Protects all the actions of this controller _except_ the listed behind the authentication condition. + def authenticates_all_except(*actions) + write_inheritable_array("actions_excepted_from_authentication", actions) + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 851a382682..dbdce1bbfd 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -602,7 +602,7 @@ module ActionController #:nodoc: def action_methods action_controller_classes = self.class.ancestors.reject{ |a| [Object, Kernel].include?(a) } - action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.instance_methods(false) } + action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.public_instance_methods(false) } end def add_variables_to_assigns diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb index 1691f031f8..1ae2ea2251 100644 --- a/actionpack/lib/action_controller/filters.rb +++ b/actionpack/lib/action_controller/filters.rb @@ -3,7 +3,7 @@ module ActionController #:nodoc: def self.append_features(base) super base.extend(ClassMethods) - base.class_eval { include ActionController::Filters::InstanceMethods } + base.send(:include, ActionController::Filters::InstanceMethods) end # Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do diff --git a/actionpack/test/controller/authentication_test.rb b/actionpack/test/controller/authentication_test.rb new file mode 100644 index 0000000000..abf0409d08 --- /dev/null +++ b/actionpack/test/controller/authentication_test.rb @@ -0,0 +1,89 @@ +require File.dirname(__FILE__) + '/../abstract_unit' + +class AuthenticationTest < Test::Unit::TestCase + class ApplicationController < ActionController::Base + authentication :by => '@session[:authenticated]', :failure => { :controller => "login" } + end + + class WeblogController < ApplicationController + def show() render_text "I showed something" end + def index() render_text "I indexed something" end + def edit() render_text "I edited something" end + def update() render_text "I updated something" end + def login() @session[:authenticated] = true; render_nothing end + end + + class AuthenticatesWeblogController < WeblogController + authenticates :edit, :update + end + + class AuthenticatesAllWeblogController < WeblogController + authenticates_all + end + + class AuthenticatesAllExceptWeblogController < WeblogController + authenticates_all_except :show, :index, :login + end + + class AuthenticatesSomeController < AuthenticatesAllWeblogController + authenticates_all_except :show + end + + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_access_on_authenticates + @controller = AuthenticatesWeblogController.new + + get :show + assert_success + + get :edit + assert_redirected_to :controller => "login" + end + + def test_access_on_authenticates_all + @controller = AuthenticatesAllWeblogController.new + + get :show + assert_redirected_to :controller => "login" + + get :edit + assert_redirected_to :controller => "login" + end + + def test_access_on_authenticates_all_except + @controller = AuthenticatesAllExceptWeblogController.new + + get :show + assert_success + + get :edit + assert_redirected_to :controller => "login" + end + + def test_access_on_authenticates_some + @controller = AuthenticatesSomeController.new + + get :show + assert_success + + get :edit + assert_redirected_to :controller => "login" + end + + def test_authenticated_access_on_authenticates + @controller = AuthenticatesWeblogController.new + + get :login + assert_success + + get :show + assert_success + + get :edit + assert_success + end +end \ No newline at end of file diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index ce778e1d7d..f983960e2e 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -126,6 +126,11 @@ class RenderTest < Test::Unit::TestCase assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { process_request } end + def test_private_methods + @request.action = "determine_layout" + assert_raises(ActionController::UnknownAction, "No action responded to [determine_layout]") { process_request } + end + def test_access_to_request_in_view ActionController::Base.view_controller_internals = false -- cgit v1.2.3