public
Description: An authorization and workflow mechanism built on top of restful_authentication
Clone URL: git://github.com/jbarket/restful-authorization.git
Jonathan Barket (author)
Tue Apr 29 12:59:06 -0700 2008
commit  5a29629e196faf13f7d3dc6ffd8a9b94e85bedf9
tree    71e4e997699d1cb0725ef1a041b2ee9350731416
parent  a1c993d29421d6ea200e494a1a38a28cd94bda35
restful-authorization / generators / authorized / templates / authorized_system.rb.erb
100644 249 lines (216 sloc) 10.463 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
module AuthorizedSystem
  def self.included(base)
    base.send :class_inheritable_array, :auth_requirements
    base.send :include, AuthorizationSecurityInstanceMethods
    base.send :extend, AuthorizationSecurityClassMethods
    base.send :helper_method, :url_options_authenticate?
    base.send :helper_method, :next_authorized_url?
    
    base.send :auth_requirements=, []
    
  end
 
  module AuthorizationSecurityClassMethods
        
    # This is the core of AuthorizedSystem.
    #
    # By calling require_authorization at the top of your
    # controller via dynamically create methods, you can
    # test for a variety of conditions.
    #
    # Example Usage:
    #
    # authorize_role "contractor"
    # authorize_role "admin", :only => :destroy # don't allow contractors to destroy
    #
    # authorize_state "active", :only => [:new, :create] # only allow active <%= users_name %>s to create new items
    #
    #
    # Valid options
    #
    # * :only - authorization is only required for these actions
    # * :only_if_logged_in? - authorization is only required for these actions if the user is logged_in?
    # * :except - authorization is required for all other actions
    # * :if - a Proc or a string to evaluate; the authorization is required if it returns true
    # * :unless - the inverse of :if
    #
    # * :redirect_url - takes a named route as a symbol (:new_example_path), string "/example/new",
    # or hash { :controller => "example", :action => "new" }
    #
    # The <%= users_name %> is redirected to :redirect_url if authorization fails. If nothing is specified, it defaults
    # to restful_authentication's access_denied
    #
    # * :render_url - takes a hash of parameters to pass to render such as
    # { :file => "#{RAILS_ROOT}/public/404.html" }
    #
    # * :status - takes a status code ("304", "404", "500") as a string
    #
    # Rather than redirecting the <%= users_name %>, the action specified in :render_url is rendered with the
    # given status. If no :render_url is specified, it renders a blank page with the status code given.
    #
    def require_authorization(type, values, options = {})
      options.assert_valid_keys(:if, :unless, :only, :only_if_logged_in?, :except, :redirect_url, :render_url, :status)
      
      # only declare the before filter once
      unless @before_filter_declared ||= false
        @before_filter_declared = true
        before_filter :check_authorization
      end
      
      # convert values to an array if it isn't already one
      values = [values] unless Array === values
            
      # convert any keys into symbols
      for key in [:only, :except]
        if options.has_key?(key)
          options[key] = [options[key]] unless Array === options[key]
          options[key] = options[key].compact.collect{|v| v.to_sym}
        end
      end
      
      self.auth_requirements||=[]
      self.auth_requirements << {:type => type, :values => values, :options => options }
    end
      
    # The method_missing helper for AuthorizedSystem catches
    # all class level methods beginning with authorize_foo
    # that have a matching foo_is_authorized? method.
    #
    # It then executes request_authorization and passes
    # foo as the authorization type.
    def method_missing(method_id, *arguments)
      super unless method_id.to_s.match(/^authorize_([_a-zA-Z]\w*)$/) and respond_to?("#{method_id.to_s.gsub(/^authorize_/,'')}_is_authorized?")
      require_authorization(method_id.to_s.gsub(/^authorize_/,''), *arguments )
    end
 
    # This method takes a <%= users_name %>, params (which includes a :controller
    # and :action) and a binding. It evalutes all authorization
    # requirements for this particular route and returns the
    # :redirect_url and :status for the first requirement it fails
    # to meet.
    def next_authorized_url_for?(<%= users_name %>, params = {}, binding = self.binding)
      return nil unless Array===self.auth_requirements
      self.auth_requirements.each do |requirement|
        type = requirement[:type]
        values = requirement[:values]
        options = requirement[:options]
    
        # handle the restriction keys associated with this requirement
        if options.has_key?(:only_if_logged_in?)
          next unless (options[:only_if_logged_in?].include?( (params[:action]||"index").to_sym) and <%= users_name %>)
        end
 
        if options.has_key?(:only)
          next unless options[:only].include?( (params[:action]||"index").to_sym)
        end
    
        if options.has_key?(:except)
          next if options[:except].include?( (params[:action]||"index").to_sym)
        end
    
        if options.has_key?(:if)
          next unless ( String===options[:if] ? eval(options[:if], binding) : options[:if].call(params) )
        end
    
        if options.has_key?(:unless)
          next if ( String===options[:unless] ? eval(options[:unless], binding) : options[:unless].call(params) )
        end
        
        if options.has_key?(:render_url) && options.has_key?(:status)
          options[:redirect_url] = options[:render_url]
        end
    
        # run the authorization test method associated with the current requirement
        values.each { |value|
          return { :url => options[:redirect_url], :status => options[:status] } unless <%= users_name %> and send("#{type}_is_authorized?", <%= users_name %>, value)
        } unless (<%= users_name %>==:false || <%= users_name %>==false)
      end
    
      return nil
    end
 
 
    # This is a wrapper method for next_authorized_url_for? that makes
    # up the core of AuthorizedSystem. If next_authorized_url_for?
    # returns nil, the <%= users_name %> is authorized to view the current page.
    def <%= users_name %>_authorized_for?(<%= users_name %>, params = {}, binding = self.binding)
      unless next_authorized_url_for?(<%= users_name %>, params, binding)
        return true
      end
    
      return false
    end
  
    def reset_auth_requirements!
      self.auth_requirements.clear
    end
 
 
    # == Authorization Test Methods
    #
    # These methods correspond to types stored in auth_requirement.
    #
    # For a particular form of authorization to exist, it must have
    # a method here in the format #{type}_is_authorized?(<%= users_name %>, value)
    # that returns a boolean value
    #
    # next_authorized_url_for? will dynamically allow for new
    # authorization requirements based on these methods. In other words,
    # if the following method existed:
    #
    # def foo_is_authorized?(<%= users_name %>, value)
    # true
    # end
    #
    # the dynamic method authorize_foo would be available in your
    # controllers.
  
    # This method invokes the generated has_role? method to
    # determine if a <%= users_name %> has a given role. By default,
    # has_role? downcases both the stored roles and the
    # requested match, so Admin == admin == ADMIN and so on.
    def role_is_authorized?(<%= users_name %>, value)
      <%= users_name %>.has_role?(value)
    end
 
    # This method is provided as an example of authorization beyond
    # the internal role system. It verifies that the <%= users_name %> has
    # a field "state" that matches the required value. This works
    # out of the box with Scott Barron's acts_as_state_machine.
    def state_is_authorized?(<%= users_name %>, value)
      <%= users_name %>.state.downcase == value.downcase
    end
  
  end
 
    module AuthorizationSecurityInstanceMethods
      # Verifies that restful_authentication is properly required before restful-authorization gets going
      def self.included(base)
        raise "Because restful-authorization extends restful_authentication, AuthenticatedSystem must be included before first before AuthorizedSystem!" unless base.included_modules.include?(AuthenticatedSystem)
      end
 
      # When <%= users_name %>_authorized_for fails, authorization_denied stores the current location
      # in the session and then handles :redirect_to and :status as described in
      # the require_authorization documentation.
      #
      # It's important to use restful_authentication's redirect_back_or_default
      # instead of redirect_to to make sure that the workflow can move forward
      # as well as backward.
      def authorization_denied
        store_location
        next_url = self.next_authorized_url?(params)
        if status = next_url[:status]
          if next_url[:url]
            render next_url[:url].merge(:status => status)
          else
            render :nothing => true, :status => status
          end
        else
          if next_url[:url]
            redirect_to(Symbol===next_url[:url] ? eval(next_url[:url].to_s) : next_url[:url])
          else
            access_denied
          end
        end
      end
 
      # This is the before filter called by require_authorization
      def check_authorization
        return authorization_denied unless self.url_options_authenticate?(params)
        true
      end
 
    protected
      # This calls <%= users_name %>_authorized_for? on the given parameters. If no controller
      # is specified, it defaults to the current one.
      def url_options_authenticate?(params = {})
        params = params.symbolize_keys
        if params[:controller]
          base = eval("#{params[:controller]}_controller".classify)
        else
          base = self.class
        end
        base.<%= users_name %>_authorized_for?(current_<%= users_name %>, params, binding)
      end
 
      # This calls next_authorized_url_for? on the given parameters. If no controller
      # is specified, it defaults to the current one.
      def next_authorized_url?(params = {})
        params = params.symbolize_keys
        if params[:controller]
          base = eval("#{params[:controller]}_controller".classify)
        else
          base = self.class
        end
        base.next_authorized_url_for?(current_<%= users_name %>, params, binding)
      end
 
    end
  end