Skip to content
This repository
Browse code

initial commit

  • Loading branch information...
commit 0ba2478848e932b1df7554b8d536d40a0b95978b 0 parents
Steffen Bartsch authored August 14, 2008
20  MIT-LICENSE
... ...
@@ -0,0 +1,20 @@
  1
+Copyright (c) 2008 [name of plugin creator]
  2
+
  3
+Permission is hereby granted, free of charge, to any person obtaining
  4
+a copy of this software and associated documentation files (the
  5
+"Software"), to deal in the Software without restriction, including
  6
+without limitation the rights to use, copy, modify, merge, publish,
  7
+distribute, sublicense, and/or sell copies of the Software, and to
  8
+permit persons to whom the Software is furnished to do so, subject to
  9
+the following conditions:
  10
+
  11
+The above copyright notice and this permission notice shall be
  12
+included in all copies or substantial portions of the Software.
  13
+
  14
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
277  README
... ...
@@ -0,0 +1,277 @@
  1
+= Authorization
  2
+
  3
+This plugin offers an authorization mechanism inspired by _RBAC_.  The most 
  4
+notable distinction to existing authorization plugins is the declarative
  5
+authorization approach.  That is, authorization rules are not programmatically
  6
+in between business logic but in an authorization configuration.
  7
+
  8
+Currently, Rails authorization plugins only provide for programmatic 
  9
+authorization rules.  That is, the developer needs to specify which roles are 
  10
+allowed to access a specific controller action or a part of a view, which is
  11
+not DRY.  With a growing application code base and functions, as it happens 
  12
+especially in agile development processes, it may be decided to introduce new 
  13
+roles.  Then, at several places of the source code the new group needs to be 
  14
+added, possibly leading to omissions and thus hard to test errors.  Another 
  15
+aspect are changing authorization requirements in development or
  16
+even after taking the application into production.  Then, privileges of
  17
+certain roles need to be easily adjusted when the original assumptions
  18
+concerning access control prove unrealistic.  In these situations, a
  19
+declarative approach as offered by this plugin increases the development
  20
+and maintenance efficiency.
  21
+
  22
+Plugin features
  23
+* Authorization at controller action level
  24
+* Authorization helpers for Views
  25
+* Authorization at model level
  26
+  * Authorize CRUD (Create, Read, Update, Delete) activities
  27
+  * Query rewriting to automatically only fetch authorized records
  28
+* DSL for specifying Authorization rules in an authorization configuration
  29
+
  30
+
  31
+Requirements
  32
+* Authentication solution ([TODO] examples): 
  33
+  * User object in Controller.current_user, etc. [TODO]
  34
+* User object needs to respond to a method :roles and return an array of role symbols
  35
+See below.
  36
+
  37
+
  38
+= Authorization Data Model
  39
+
  40
+ ----- App domain ----|-------- Authorization conf ---------|------- App domain ------
  41
+
  42
+                       includes                   includes
  43
+                        .--.                        .---.
  44
+                        |  v                        |   v
  45
+  .------.  can_play  .------.  has_permission  .------------.  requires  .----------.
  46
+  | User |----------->| Role |----------------->| Permission |<-----------| Activity |
  47
+  '------' *        * '------' *              * '------------' 1        * '----------'
  48
+                                                      |
  49
+                                              .-------+------.
  50
+                                           1 /        | 1     \ *
  51
+                                 .-----------.   .---------.  .-----------.
  52
+                                 | Privilege |   | Context |  | Attribute |
  53
+                                 '-----------'   '---------'  '-----------'
  54
+
  55
+In the application domain, each *User* may be assigned to *Roles* that should 
  56
+define the users' job in the application, such as _Administrator_.  On the 
  57
+right-hand side of this diagram, application developers specify which *Permissions* 
  58
+are necessary for users to perform activities, such as calling a controller action,
  59
+viewing parts of a View or acting on records in the database.  Note that
  60
+Permissions consist of an *Privilege* that is to be performed, such as _read_, 
  61
+and a *Context* in that the Operation takes place, such as _companies_.
  62
+
  63
+In the authorization configuration, Permissions are assigned to Roles and Role
  64
+and Permission hierarchies are defined.  *Attributes* may be employed to allow
  65
+authorization according to dynamic information about the context and the
  66
+current user, e.g. "only allow access on employees that belong to the
  67
+current user's branch."
  68
+
  69
+
  70
+= Examples
  71
+
  72
+== Controller
  73
+
  74
+If authentication is in place, enabling user-specific access control may be
  75
+as simple as one call to filter_access_to :all which simply requires the 
  76
+according privileges for present actions.  E.g. the privilege index_users is
  77
+required for action index.  This works as a first default configuration
  78
+for RESTful controllers, with these privileges easily handled in the
  79
+authorization configuration, which will be described below.
  80
+
  81
+    class EmployeeController < ApplicationController
  82
+      filter_access_to :all
  83
+      def index
  84
+        ...
  85
+      end
  86
+      ...
  87
+    end
  88
+
  89
+When custom actions are added to such a controller, it helps to define more
  90
+clearly which privileges are the respective requirements.  That is when the
  91
+filter_access_to call may become more verbose:
  92
+
  93
+    class EmployeeController < ApplicationController
  94
+      filter_access_to :all
  95
+      # this one would be included in :all, but :read seems to be
  96
+      # a more suitable privilege than :auto_complete_for_user_name
  97
+      filter_access_to :auto_complete_for_employee_name, :require => :read
  98
+      def auto_complete_for_employee_name
  99
+        ...
  100
+      end
  101
+      ...
  102
+    end
  103
+
  104
+For some actions it might be necessary to check certain attributes of the
  105
+object the action is to be acting on.  Then, the object needs to be loaded 
  106
+before the action's access control is evaluated.  On the other hand, some actions
  107
+might prefer the authorization to ignore specific attribute checks as the object is
  108
+unknown at checking time, so attribute checks and thus automatic loading of
  109
+objects needs to be enabled explicitly.
  110
+
  111
+    class EmployeeController < ApplicationController
  112
+      ...
  113
+      filter_access_to :update, :attribute_check => true
  114
+      def update
  115
+        # @employee is already loaded from param[:id]
  116
+        @employee ||= Employee.find(param[:id])
  117
+        ...
  118
+      end
  119
+      ...
  120
+    end
  121
+
  122
+For further customization of object loading, have a look at the complete
  123
+API documentation of filter_access_to in 
  124
+Authorization::AuthorizationInController::ClassMethods.
  125
+
  126
+
  127
+== Views
  128
+
  129
+In views, a simple permitted_to? helper makes showing blocks according to the
  130
+current user's privileges easy:
  131
+
  132
+    <% permitted_to?(:create, :employees) do %>
  133
+    <%= link_to 'New', new_employee_path %>
  134
+    <% end %>
  135
+    ...
  136
+    <% for employee in @employees %>
  137
+    ...
  138
+    <%= link_to 'Edit', edit_employee_path(company) if permitted_to?(:update, employee) %>
  139
+    <% end %>
  140
+
  141
+See also Authorization::AuthorizationHelper.
  142
+
  143
+
  144
+== Models
  145
+
  146
+There are two destinct features for model security built into this plugin:
  147
+authorizing CRUD operations on objects as well as query rewriting to limit
  148
+results according to certain privileges.
  149
+
  150
+See also Authorization::AuthorizationInModel.
  151
+
  152
+=== Model security for CRUD opterations
  153
+To activate model security, all it takes is an explicit enabling for each
  154
+model that model security should be enforced on, i.e.
  155
+
  156
+    class Employee < ActiveRecord::Base
  157
+      using_access_control
  158
+      ...
  159
+    end
  160
+
  161
+As access control on read are costly, with possibly lots of objects being
  162
+loaded at a time in one query, checks on read need to be actived explicitly by
  163
+adding the :include_read option.
  164
+
  165
+TODO: Example, Exceptions
  166
+
  167
+=== Query rewriting using named scopes
  168
+When retrieving large sets of records from databases, any authorization needs
  169
+to be integrated into the query in order to prevent inefficient filtering
  170
+afterwards and to use LIMIT and OFFSET in SQL statements.  To keep authorization
  171
+rules out of the source code, this plugin offers query rewriting mechanisms
  172
+through named scopes.
  173
+
  174
+    Employee.having_permission_to(:read)
  175
+
  176
+returns all employee records that the current user is authorized to read.  In
  177
+addition, just like normal named scopes, query rewriting may be chained with
  178
+the usual find method:
  179
+
  180
+    Employee.having_permission_to(:read).find(:all, :conditions => ...)
  181
+
  182
+=== Custom Rewriting
  183
+
  184
+
  185
+== Authorization Rules
  186
+
  187
+Authorization rules are defined in config/authorization_rules.rb.  E.g.
  188
+
  189
+    authorization do
  190
+      role :admin do
  191
+        has_permission_on :employees, :to => [:create, :read, :update, :delete]
  192
+      end
  193
+    end
  194
+
  195
+Privileges, such as :create, may be put into hierarchies to simplify
  196
+maintenance.  So the example above has the same meaning as
  197
+
  198
+    authorization do
  199
+      role :admin do
  200
+        has_permission_on :employees, :to => :manage
  201
+      end
  202
+    end
  203
+
  204
+    privileges do
  205
+      privilege :manage do
  206
+        includes :create, :read, :update, :delete
  207
+      end
  208
+    end
  209
+
  210
+Privilege hierarchies may be context-specific, e.g. applicable to :employees.
  211
+
  212
+    privileges do
  213
+      privilege :manage, :employees, :includes => :increase_salary
  214
+    end
  215
+
  216
+For more complex use cases, authorizations need to be based on attributes.  E.g.
  217
+a branch admin should manage only employees of his branch.
  218
+
  219
+    authorization do
  220
+      role :branch_admin do
  221
+        has_permission.on :employees do
  222
+          to :manage
  223
+          # user refers to the current_user when evaluating
  224
+          if_attribute :branch => is {user.branch}
  225
+        end
  226
+      end
  227
+    end
  228
+
  229
+Lastly, not only privileges may be organized in a hierarchy but roles as well.
  230
+Here, project manager inherit the permissions of employees.
  231
+
  232
+      role :project_manager do
  233
+        includes :employee
  234
+      end
  235
+
  236
+See also Authorization::Reader.
  237
+
  238
+= Installation
  239
+TODO: how-to
  240
+
  241
+== Meeting the Requirements
  242
+TODO: Helping to overcome requirements
  243
+* install acts_as_authenticated or restful_authentication
  244
+  or provide Controller.current_user
  245
+
  246
+  restful_authentication
  247
+   ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/
  248
+   ruby script/generate authenticated user sessions
  249
+   move include AuthenticatedSystem to ApplicationController
  250
+   add before_filter :login_required
  251
+
  252
+   $current_user ?
  253
+
  254
+* add roles field to User model
  255
+  Migration for roles table
  256
+  Model for roles
  257
+  has_many :roles in User model
  258
+
  259
+
  260
+== Debugging Authorization
  261
+
  262
+* Logging
  263
+* Exceptions
  264
+
  265
+
  266
+== Contact
  267
+
  268
+Steffen Bartsch
  269
+TZI, Universität Bremen, Germany
  270
+sbartsch at tzi.org
  271
+
  272
+
  273
+== Licence
  274
+
  275
+Copyright (c) 2008 Steffen Bartsch, TZI, Universität Bremen, Germany
  276
+released under the MIT license
  277
+
23  Rakefile
... ...
@@ -0,0 +1,23 @@
  1
+require 'rake'
  2
+require 'rake/testtask'
  3
+require 'rake/rdoctask'
  4
+
  5
+desc 'Default: run unit tests.'
  6
+task :default => :test
  7
+
  8
+desc 'Test the authorization plugin.'
  9
+Rake::TestTask.new(:test) do |t|
  10
+  t.libs << 'lib'
  11
+  t.pattern = 'test/**/*_test.rb'
  12
+  t.verbose = true
  13
+end
  14
+
  15
+desc 'Generate documentation for the authorization plugin.'
  16
+Rake::RDocTask.new(:rdoc) do |rdoc|
  17
+  rdoc.rdoc_dir = 'rdoc'
  18
+  rdoc.title    = 'Authorization'
  19
+  rdoc.options << '--line-numbers' << '--inline-source'
  20
+  rdoc.options << '--charset' << 'utf-8'
  21
+  rdoc.rdoc_files.include('README')
  22
+  rdoc.rdoc_files.include('lib/**/*.rb')
  23
+end
8  init.rb
... ...
@@ -0,0 +1,8 @@
  1
+require File.dirname(__FILE__) + "/lib/helper.rb"
  2
+require File.dirname(__FILE__) + "/lib/in_controller.rb"
  3
+require File.dirname(__FILE__) + "/lib/in_model.rb"
  4
+
  5
+ActionController::Base.send :include, Authorization::AuthorizationInController
  6
+ActionController::Base.helper Authorization::AuthorizationHelper
  7
+
  8
+ActiveRecord::Base.send :include, Authorization::AuthorizationInModel
318  lib/authorization.rb
... ...
@@ -0,0 +1,318 @@
  1
+# Authorization
  2
+require File.dirname(__FILE__) + '/reader.rb'
  3
+require "set"
  4
+
  5
+
  6
+module Authorization
  7
+  class AuthorizationError < Exception ; end
  8
+  class NotAuthorized < AuthorizationError ; end
  9
+  class AttributeAuthorizationError < NotAuthorized ; end
  10
+  class AuthorizationUsageError < AuthorizationError ; end
  11
+  
  12
+  AUTH_DSL_FILE = "#{RAILS_ROOT}/config/authorization_rules.rb"
  13
+  
  14
+  # Controller-independent method for retrieving the current user.
  15
+  # Needed for model security where the current controller is not available.
  16
+  def self.current_user
  17
+    $current_user
  18
+  end
  19
+  
  20
+  # Controller-independent method for setting the current user.
  21
+  def self.current_user=(user)
  22
+    $current_user = user
  23
+  end
  24
+  
  25
+  @@ignore_access_control = false
  26
+  # For use in test cases only
  27
+  def self.ignore_access_control (state = nil) # :nodoc:
  28
+    @@ignore_access_control = state unless state.nil?
  29
+    @@ignore_access_control
  30
+  end
  31
+  
  32
+  # Authorization::Engine implements the reference monitor.  It may be used
  33
+  # for querying the permission and retrieving obligations under which
  34
+  # a certain privilege is granted for the current user.
  35
+  #
  36
+  class Engine
  37
+    # If +reader+ is not given, a new one is created with the default
  38
+    # authorization configuration of +AUTH_DSL_FILE+.  If given, may be either
  39
+    # a Reader object or a path to a configuration file.
  40
+    def initialize (reader = nil)
  41
+      if reader.nil?
  42
+        reader = Reader::DSLReader.load(AUTH_DSL_FILE)
  43
+      elsif reader.is_a?(String)
  44
+        reader = Reader::DSLReader.load(reader)
  45
+      end
  46
+      @privileges = reader.privileges_reader.privileges
  47
+      # {priv => [[priv, ctx],...]}
  48
+      @privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
  49
+      @auth_rules = reader.auth_rules_reader.auth_rules
  50
+      @roles = reader.auth_rules_reader.roles
  51
+      @role_hierarchy = reader.auth_rules_reader.role_hierarchy
  52
+      
  53
+      # {[priv, ctx] => [priv, ...]}
  54
+      @rev_priv_hierarchy = {}
  55
+      @privilege_hierarchy.each do |key, value|
  56
+        value.each do |val| 
  57
+          @rev_priv_hierarchy[val] ||= []
  58
+          @rev_priv_hierarchy[val] << key
  59
+        end
  60
+      end
  61
+    end
  62
+    
  63
+    # Returns true if privilege is met by the current user.  Raises
  64
+    # AuthorizationError otherwise.  +privilege+ may be given with or
  65
+    # without context.  In the latter case, the :+context+ option is
  66
+    # required.
  67
+    #  
  68
+    # Options:
  69
+    # [:+context+]
  70
+    #   The context part of the privilege.
  71
+    #   Defaults either to the +table_name+ of the given :+object+, if given.
  72
+    #   That is, either :+users+ for :+object+ of type User.  
  73
+    #   Raises AuthorizationUsageError if
  74
+    #   context is missing and not to be infered.
  75
+    # [:+object+] An context object to test attribute checks against.
  76
+    # [:+skip_attribute_test+]
  77
+    #   Skips those attribute checks in the 
  78
+    #   authorization rules. Defaults to false.
  79
+    # [:+user+] 
  80
+    #   The user to check the authorization for.
  81
+    #   Defaults to Authorization#current_user.
  82
+    #
  83
+    def permit! (privilege, options = {})
  84
+      return true if Authorization.ignore_access_control
  85
+      options = {
  86
+        :object => nil,
  87
+        :skip_attribute_test => false,
  88
+        :context => nil
  89
+      }.merge(options)
  90
+      options[:context] ||= options[:object] && options[:object].class.table_name.to_sym rescue NoMethodError
  91
+      
  92
+      user, roles, privileges = user_roles_privleges_from_options(privilege, options)
  93
+
  94
+      # find a authorization rule that matches for at least one of the roles and 
  95
+      # at least one of the given privileges
  96
+      attr_validator = AttributeValidator.new(user, options[:object])
  97
+      #puts "All rules: #{@auth_rules.inspect}"
  98
+      #rules_matching_roles = @auth_rules.select {|r| roles.include?(r.role) }
  99
+      #puts "Matching for roles: #{rules_matching_roles.inspect}"
  100
+      #puts "Matching rules for user   #{user.inspect},"
  101
+      #puts "                   roles  #{roles.inspect},"
  102
+      #puts "                   privs  #{privileges.inspect}:"
  103
+      #puts "   #{matching_auth_rules(roles, privileges).inspect}"
  104
+      rules = matching_auth_rules(roles, privileges, options[:context])
  105
+      if rules.empty?
  106
+        raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
  107
+          "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
  108
+          "context #{options[:context].inspect})."
  109
+      end
  110
+      
  111
+      grant_permission = rules.any? do |rule|
  112
+        options[:skip_attribute_test] or
  113
+          rule.attributes.empty? or
  114
+          rule.attributes.any? {|attr| attr.validate? attr_validator }
  115
+      end
  116
+      unless grant_permission
  117
+        raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{options[:object].inspect}."
  118
+      end
  119
+      true
  120
+    end
  121
+    
  122
+    # Calls permit! but rescues the AuthorizationException and returns false
  123
+    # instead.  If no exception is raised, permit? returns true and yields
  124
+    # to the optional block.
  125
+    def permit? (privilege, options = {}, &block) # :yields:
  126
+      permit!(privilege, options)
  127
+      yield if block_given?
  128
+      true
  129
+    rescue NotAuthorized
  130
+      false
  131
+    end
  132
+    
  133
+    # Returns the obligations to be met by the current user for the given 
  134
+    # privilege as an array of obligation hashes in form of 
  135
+    #   [{:object_attribute => obligation_value, ...}, ...]
  136
+    # where +obligation_value+ is either (recursively) another obligation hash
  137
+    # or a value spec, such as
  138
+    #   [operator, literal_value]
  139
+    # The obligation hashes in the array should be OR'ed, conditions inside
  140
+    # the hashes AND'ed.
  141
+    # 
  142
+    # Example
  143
+    #   {:branch => {:company => [:is, 24]}, :active => [:is, true]}
  144
+    # 
  145
+    # Options
  146
+    # [:+context+]  See permit!
  147
+    # [:+user+]  See permit!
  148
+    # 
  149
+    def obligations (privilege, options = {})
  150
+      options = {:context => nil}.merge(options)
  151
+      user, roles, privileges = user_roles_privleges_from_options(privilege, options)
  152
+      attr_validator = AttributeValidator.new(user)
  153
+      matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
  154
+        #p rule
  155
+        #obligation = {}
  156
+        rule.attributes.collect {|attr| attr.obligation(attr_validator) }
  157
+        #obligation
  158
+      end.flatten
  159
+    end
  160
+    
  161
+    # Returns an instance of Engine, which is created if there isn't one
  162
+    # yet.  If +dsl_file+ is given, it is passed on to Engine.new and 
  163
+    # a new instance is always created.
  164
+    def self.instance (dsl_file = nil)
  165
+      if dsl_file
  166
+        @@instance = new(dsl_file)
  167
+      else
  168
+        @@instance ||= new
  169
+      end
  170
+    end
  171
+    
  172
+    class AttributeValidator # :nodoc:
  173
+      attr_reader :user, :object
  174
+      def initialize (user, object = nil)
  175
+        @user = user
  176
+        @object = object
  177
+      end
  178
+      
  179
+      def evaluate (value_block)
  180
+        # TODO cache?
  181
+        instance_eval(&value_block)
  182
+      end
  183
+    end
  184
+    
  185
+    private
  186
+    def user_roles_privleges_from_options(privilege, options)
  187
+      options = {
  188
+        :user => Authorization.current_user,
  189
+        :context => nil
  190
+      }.merge(options)
  191
+      user = options[:user]
  192
+      privileges = privilege.is_a?(Array) ? privilege : [privilege]
  193
+      
  194
+      raise AuthorizationUsageError, "No user object available" unless user
  195
+      raise AuthorizationUsageError, "User object doesn't respond to roles" unless user.respond_to?(:roles)
  196
+      
  197
+      roles = flatten_roles user.roles
  198
+      privileges = flatten_privileges privileges, options[:context]
  199
+      [user, roles, privileges]
  200
+    end
  201
+    
  202
+    def flatten_roles (roles)
  203
+      # TODO caching?
  204
+      flattened_roles = roles.clone.to_a
  205
+      flattened_roles.each do |role|
  206
+        flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
  207
+      end
  208
+    end
  209
+    
  210
+    # Returns the privilege hierarchy flattened for given privileges in context.
  211
+    def flatten_privileges (privileges, context = nil)
  212
+      # TODO caching?
  213
+      #if context.nil?
  214
+      #  context = privileges.collect { |p| p.to_s.split('_') }.
  215
+      #                       reject { |p_p| p_p.length < 2 }.
  216
+      #                       collect { |p_p| (p_p[1..-1] * '_').to_sym }.first
  217
+      #  raise AuthorizationUsageError, "No context given or inferable from privileges #{privileges.inspect}" unless context
  218
+      #end
  219
+      raise AuthorizationUsageError, "No context given or inferable from object" unless context
  220
+      #context_regex = Regexp.new "_#{context}$"
  221
+      # TODO work with contextless privileges
  222
+      #flattened_privileges = privileges.collect {|p| p.to_s.sub(context_regex, '')}
  223
+      flattened_privileges = privileges.clone #collect {|p| p.to_s.end_with?(context.to_s) ?
  224
+                                              #       p : [p, "#{p}_#{context}".to_sym] }.flatten
  225
+      flattened_privileges.each do |priv|
  226
+        flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
  227
+        flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
  228
+      end
  229
+    end
  230
+    
  231
+    def matching_auth_rules (roles, privileges, context)
  232
+      @auth_rules.select {|rule| rule.matches? roles, privileges, context}
  233
+    end
  234
+  end
  235
+  
  236
+  class AuthorizationRule
  237
+    attr_reader :attributes, :context, :role, :privileges
  238
+    
  239
+    def initialize (role, privileges_or_context = [], context = nil)
  240
+      @role = role
  241
+      @privileges = Set.new
  242
+      if privileges_or_context.is_a?(Array)
  243
+        @context = context
  244
+        append_privileges(privileges_or_context)
  245
+      else
  246
+        @context = privileges_or_context
  247
+      end
  248
+      @attributes = []
  249
+    end
  250
+    
  251
+    def append_privileges (privs)
  252
+      @privileges.merge(privs)
  253
+    end
  254
+    
  255
+    def append_attribute (attribute)
  256
+      @attributes << attribute
  257
+    end
  258
+    
  259
+    def matches? (roles, privs, context = nil)
  260
+      roles = [roles] unless roles.is_a?(Array)
  261
+      @context == context and roles.include?(@role) and 
  262
+        not (@privileges & privs).empty?
  263
+    end
  264
+  end
  265
+  
  266
+  class Attribute
  267
+    # attr_conditions_hash of form
  268
+    # { :object_attribute => [operator, value_block], ... }
  269
+    # { :object_attribute => { :attr => ... } }
  270
+    def initialize (conditions_hash)
  271
+      @conditions_hash = conditions_hash
  272
+    end
  273
+    
  274
+    def validate? (attr_validator, object = nil, hash = nil)
  275
+      object ||= attr_validator.object
  276
+      return false unless object
  277
+      
  278
+      (hash || @conditions_hash).all? do |attr, value|
  279
+        begin
  280
+          attr_value = object.send(attr)
  281
+        rescue ArgumentError, NoMethodError => e
  282
+          raise AuthorizationUsageError, "Error when calling #{attr} on " +
  283
+           "#{object.inspect} for validating attribute: #{e}"
  284
+        end
  285
+        if value.is_a?(Hash)
  286
+          validate?(attr_validator, attr_value, value)
  287
+        elsif value.is_a?(Array) and value.length == 2
  288
+          evaluated = attr_validator.evaluate(value[1])
  289
+          case value[0]
  290
+          when :is
  291
+            attr_value == evaluated
  292
+          when :contains
  293
+            attr_value.include?(evaluated)
  294
+          else
  295
+            raise AuthorizationError, "Unknown operator #{value[0]}"
  296
+          end
  297
+        else
  298
+          raise AuthorizationError, "Wrong conditions hash format"
  299
+        end
  300
+      end
  301
+    end
  302
+    
  303
+    # resolves all the values in condition_hash
  304
+    def obligation (attr_validator, hash = nil)
  305
+      hash = (hash || @conditions_hash).clone
  306
+      hash.each do |attr, value|
  307
+        if value.is_a?(Hash)
  308
+          hash[attr] = obligation(attr_validator, value)
  309
+        elsif value.is_a?(Array) and value.length == 2
  310
+          hash[attr] = [value[0], attr_validator.evaluate(value[1])]
  311
+        else
  312
+          raise AuthorizationError, "Wrong conditions hash format"
  313
+        end
  314
+      end
  315
+      hash
  316
+    end
  317
+  end
  318
+end
31  lib/helper.rb
... ...
@@ -0,0 +1,31 @@
  1
+# Authorization::AuthorizationHelper
  2
+require File.dirname(__FILE__) + '/authorization.rb'
  3
+
  4
+module Authorization
  5
+  module AuthorizationHelper
  6
+  
  7
+    # If the current user meets the given privilege, permitted_to? returns true
  8
+    # and yields to the optional block.  The attribute checks that are defined
  9
+    # in the authorization rules are only evaluated if an object is given
  10
+    # for context.
  11
+    # 
  12
+    # Examples:
  13
+    #     <% permitted_to? :create, :users do %>
  14
+    #     <%= link_to 'New', new_user_path %>
  15
+    #     <% end %>
  16
+    #     ...
  17
+    #     <% if permitted_to? :create, :users %>
  18
+    #     <%= link_to 'New', new_user_path %>
  19
+    #     <% else %>
  20
+    #     You are not allowed to create new users!
  21
+    #     <% end %>
  22
+    #     ...
  23
+    #     <% for user in @users %>
  24
+    #     <%= link_to 'Edit', edit_user_path(user) if permitted_to? :update, user %>
  25
+    #     <% end %>
  26
+    # 
  27
+    def permitted_to? (privilege, object_or_sym = nil, &block)
  28
+      controller.permitted_to?(privilege, object_or_sym, &block)
  29
+    end
  30
+  end
  31
+end
228  lib/in_controller.rb
... ...
@@ -0,0 +1,228 @@
  1
+# Authorization::AuthorizationInController
  2
+require File.dirname(__FILE__) + '/authorization.rb'
  3
+
  4
+module Authorization
  5
+  module AuthorizationInController
  6
+  
  7
+    def self.included(base) # :nodoc:
  8
+      base.extend(ClassMethods)
  9
+    end
  10
+    
  11
+    # Returns the Authorization::Engine for the current controller.
  12
+    def authorization_engine
  13
+      @authorization_engine ||= Authorization::Engine.new
  14
+    end
  15
+    
  16
+    # If the current user meets the given privilege, permitted_to? returns true
  17
+    # and yields to the optional block.  The attribute checks that are defined
  18
+    # in the authorization rules are only evaluated if an object is given
  19
+    # for context.
  20
+    # 
  21
+    # See examples for Authorization::AuthorizationHelper #permitted_to?
  22
+    #
  23
+    def permitted_to? (privilege, object_or_sym = nil, &block)
  24
+      context = object = nil
  25
+      if object_or_sym.is_a?(Symbol)
  26
+        context = object_or_sym
  27
+      else
  28
+        object = object_or_sym
  29
+      end
  30
+      # TODO infer context also from self.class.name
  31
+      authorization_engine.permit?(privilege, 
  32
+          {:user => current_user, 
  33
+           :object => object,
  34
+           :context => context,
  35
+           :skip_attribute_test => object.nil?}, 
  36
+          &block)
  37
+    end
  38
+    
  39
+    module ClassMethods
  40
+      #
  41
+      # Defines a filter to be applied according to the authorization of the
  42
+      # current user.  Requires at least one symbol corresponding to an
  43
+      # action as parameter.  The special symbol :+all+ refers to all action.
  44
+      #   class UserController < ActionController
  45
+      #     filter_access_to :index
  46
+      #     filter_access_to :new, :edit
  47
+      #     filter_access_to :all
  48
+      #     ...
  49
+      #   end
  50
+      # 
  51
+      # By default, required privileges are infered from the action name and
  52
+      # the controller name.  Thus, in UserController :+edit+ requires
  53
+      # :+edit_users+.  To specify required privilege, use the option :+require+
  54
+      #   filter_access_to :new, :create, :require => :create_users
  55
+      #   
  56
+      # For further customization, a custom filter expression may be formulated
  57
+      # in a block, which is then evaluated in the context of the controller
  58
+      # on a matching request.  That is, for checking two objects, use the 
  59
+      # following:
  60
+      #   filter_access_to :merge do
  61
+      #     authorization_engine.permit!(:update, :context => :users,
  62
+      #                                  :object => User.find(params[:original_id]),
  63
+      #                                  :user => current_user) and
  64
+      #       authorization_engine.permit!(:delete, :context => :users,
  65
+      #                                    :object => User.find(params[:id]),
  66
+      #                                    :user => current_user)
  67
+      #   end
  68
+      # The block should raise a Authorization::AuthorizationError or return
  69
+      # false if the access is to be denied.
  70
+      # 
  71
+      # Later calls to filter_access_to with overlapping actions overwrite
  72
+      # previous ones for that action.
  73
+      # 
  74
+      # All options:
  75
+      # [:+require+] 
  76
+      #   Privilege required; defaults to action_name
  77
+      # [:+context+] 
  78
+      #   The privilege's context, defaults to controller_name, pluralized.
  79
+      # [:+attribute_check+]
  80
+      #   Enables the check of attributes defined in the authorization rules.
  81
+      #   Defaults to false.  If enabled, filter_access_to will try to load
  82
+      #   a context object employing either 
  83
+      #   * the method from the :+load_method+ option or 
  84
+      #   * a find on the context model, using +params+[:id] as id value.
  85
+      #   Any of these loading methods will only be employed if :+attribute_check+
  86
+      #   is enabled.
  87
+      # [:+model+] 
  88
+      #   The data model to load a context object from.  Defaults to the
  89
+      #   context, singularized.
  90
+      # [:+load_method+]
  91
+      #   Specify a method by symbol or a Proc object which should be used 
  92
+      #   to load the object.  Both should return the loaded object.
  93
+      #   If a Proc object is given, e.g. by way of
  94
+      #   +lambda+, it is called in the instance of the controller.  
  95
+      #   Example demonstrating the default behaviour:
  96
+      #     filter_access_to :show, :attribute_check => true,
  97
+      #                      :load_method => lambda { User.find(params[:id]) }
  98
+      # 
  99
+      
  100
+      def filter_access_to (*args, &filter_block)
  101
+        options = args.last.is_a?(Hash) ? args.pop : {}
  102
+        options = {
  103
+          :require => nil,
  104
+          :context => nil,
  105
+          :attribute_check => false,
  106
+          :model => nil,
  107
+          :load_method => nil
  108
+        }.merge!(options)
  109
+        privilege = options[:require]
  110
+        context = options[:context]
  111
+        actions = args
  112
+
  113
+        # TODO currently: default deny; make this configurable
  114
+        # collect permits in controller array for use in one before_filter
  115
+        unless class_variable_defined?(:@@permissions)
  116
+          permissions = []
  117
+          class_variable_set(:@@permissions, permissions)
  118
+          before_filter do |contr|
  119
+            all_permissions = permissions.select {|p| p.actions.include?(:all)}
  120
+            matching_permissions = permissions.select {|p| p.matches?(contr.action_name)}
  121
+            allowed = false
  122
+            auth_exception = nil
  123
+            begin
  124
+              allowed = if !matching_permissions.empty?
  125
+                          matching_permissions.all? {|perm| perm.permit!(contr)}
  126
+                        elsif !all_permissions.empty?
  127
+                          all_permissions.all? {|perm| perm.permit!(contr)}
  128
+                        else
  129
+                          false
  130
+                        end
  131
+            rescue AuthorizationError => e
  132
+              auth_exception = e
  133
+            end
  134
+            
  135
+            unless allowed
  136
+              if all_permissions.empty? and matching_permissions.empty?
  137
+                contr.logger.warn "Permission denied: No matching filter access " +
  138
+                  "rule found for #{contr.class.controller_name}.#{contr.action_name}"
  139
+              elsif auth_exception
  140
+                contr.logger.info "Permission denied: #{auth_exception}"
  141
+              end
  142
+              if contr.respond_to?(:permission_denied)
  143
+                # permission_denied needs to render or redirect
  144
+                contr.send(:permission_denied)
  145
+              else
  146
+                contr.send(:render, :text => "You are not allowed to access this action.")
  147
+              end
  148
+            end
  149
+          end
  150
+        end
  151
+        
  152
+        class_variable_get(:@@permissions).each do |perm|
  153
+          perm.remove_actions(actions)
  154
+        end
  155
+        class_variable_get(:@@permissions) << 
  156
+          ControllerPermission.new(actions, privilege, context,
  157
+                                   options[:attribute_check],
  158
+                                   options[:model],
  159
+                                   options[:load_method],
  160
+                                   filter_block)
  161
+      end
  162
+    end 
  163
+  end
  164
+  
  165
+  class ControllerPermission # :nodoc:
  166
+    attr_reader :actions, :privilege, :context
  167
+    def initialize (actions, privilege, context, attribute_check = false, 
  168
+                    load_object_model = nil, load_object_method = nil,
  169
+                    filter_block = nil)
  170
+      @actions = actions.to_set
  171
+      @privilege = privilege
  172
+      @context = context
  173
+      @load_object_model = load_object_model
  174
+      @load_object_method = load_object_method
  175
+      @filter_block = filter_block
  176
+      @attribute_check = attribute_check
  177
+    end
  178
+    
  179
+    def matches? (action_name)
  180
+      @actions.include?(action_name.to_sym)
  181
+    end
  182
+    
  183
+    def permit! (contr)
  184
+      if @filter_block
  185
+        return contr.instance_eval(&@filter_block)
  186
+      end
  187
+      context = @context || contr.class.controller_name.pluralize.to_sym
  188
+      object = @attribute_check ? load_object(contr, context) : nil
  189
+      privilege = @privilege || :"#{contr.action_name}"
  190
+      
  191
+      #puts "Trying permit?(#{privilege.inspect}, "
  192
+      #puts "               :user => #{contr.send(:current_user).inspect}, "
  193
+      #puts "               :object => #{object.inspect}," 
  194
+      #puts "               :skip_attribute_test => #{!@attribute_check}," 
  195
+      #puts "               :context => #{contr.class.controller_name.pluralize.to_sym})"
  196
+      res = contr.authorization_engine.permit!(privilege, 
  197
+                                         :user => contr.send(:current_user),
  198
+                                         :object => object,
  199
+                                         :skip_attribute_test => !@attribute_check,
  200
+                                         :context => context)
  201
+      #puts "permit? result: #{res.inspect}"
  202
+      res
  203
+    end
  204
+    
  205
+    def remove_actions (actions)
  206
+      @actions -= actions
  207
+    end
  208
+    
  209
+    private
  210
+    def load_object(contr, context)
  211
+      if @load_object_method and @load_object_method.is_a?(Symbol)
  212
+        contr.send(@load_object_method)
  213
+      elsif @load_object_method and @load_object_method.is_a?(Proc)
  214
+        contr.instance_eval(&@load_object_method)
  215
+      else
  216
+        load_object_model = @load_object_model || context.to_s.classify.constantize
  217
+        instance_var = :"@#{load_object_model.name.underscore}"
  218
+        object = contr.instance_variable_get(instance_var)
  219
+        unless object
  220
+          # catch ActiveRecord::RecordNotFound?
  221
+          object = load_object_model.find(contr.params[:id])
  222
+          contr.instance_variable_set(instance_var, object)
  223
+        end
  224
+        object
  225
+      end
  226
+    end
  227
+  end
  228
+end
187  lib/in_model.rb
... ...
@@ -0,0 +1,187 @@
  1
+# Authorization::AuthorizationInModel
  2
+require File.dirname(__FILE__) + '/authorization.rb'
  3
+
  4
+module Authorization
  5
+  
  6
+  module AuthorizationInModel
  7
+    
  8
+    def self.included(base) # :nodoc:
  9
+      #base.extend(ClassMethods)
  10
+      base.module_eval do
  11
+        scopes[:with_permissions_to] = lambda do |parent_scope, *args|
  12
+          options = args.last.is_a?(Hash) ? args.pop : {}
  13
+          privilege = (args[0] || :read).to_sym
  14
+          privileges = [privilege]
  15
+          context = options[:context] || :"#{parent_scope.table_name}"
  16
+          
  17
+          user = options[:user] || Authorization.current_user
  18
+
  19
+          engine = Authorization::Engine.instance
  20
+          engine.permit!(privileges, :user => user, :skip_attribute_test => true,
  21
+                         :context => context)
  22
+
  23
+          scope_options = obligation_conditions(privileges, :user => user,
  24
+            :context => context, :engine => engine, :model => parent_scope)
  25
+          
  26
+          ActiveRecord::NamedScope::Scope.new(parent_scope, scope_options)
  27
+        end
  28
+        
  29
+        # Provides an conditions hash as expected by find with conditions
  30
+        # matching the obligations for the given privilege, context and 
  31
+        # user.
  32
+        # 
  33
+        # Options:
  34
+        # [:+user+] 
  35
+        #   User to create the obligations for, defaults to 
  36
+        #   Authorization.current_user
  37
+        # [:+context+] The privilege's context
  38
+        # [:+model+] 
  39
+        #   Model that the obligations should be applied on,
  40
+        #   defaults to self.
  41
+        # [:+engine+] 
  42
+        #   Authorization::Engine to be used for checks, defaults to
  43
+        #   Authorization::Engine.instance.
  44
+        #
  45
+        def self.obligation_conditions (privileges, options = {})
  46
+          options = {
  47
+            :user => Authorization.current_user,
  48
+            :context => nil,
  49
+            :model => self,
  50
+            :engine => nil,
  51
+          }.merge(options)
  52
+          engine ||= Authorization::Engine.instance
  53
+          
  54
+          conditions = []
  55
+          condition_values = []
  56
+          joins = Set.new
  57
+
  58
+          engine.obligations(privileges, :user => options[:user], 
  59
+                             :context => options[:context]).each do |obligation|
  60
+            and_conditions = []
  61
+            obligation_conditions!(nil, obligation, options[:model], 
  62
+                                   and_conditions, condition_values, joins)
  63
+            conditions << and_conditions.collect {|c| "#{c}"} * ' AND ' unless and_conditions.empty?
  64
+          end
  65
+
  66
+          scope_options = {}
  67
+          unless conditions.empty?
  68
+            scope_options[:select] = "`#{options[:context]}`.*" if options[:context]
  69
+            scope_options[:conditions] = [conditions.collect {|c| "(#{c})"} * ' OR '] + condition_values
  70
+            scope_options[:joins] = joins.to_a unless joins.empty?
  71
+          end
  72
+          scope_options
  73
+        end
  74
+        
  75
+        def self.obligation_conditions!(object_attribute, value, model, and_conditions,
  76
+                                        condition_values, joins) # :nodoc:
  77
+          if value.is_a?(Hash)
  78
+            value.each do |object_attr, operator_val|
  79
+              joins << object_attribute if object_attribute
  80
+              assoc_model = object_attribute ? model.reflect_on_association(object_attribute).klass : model
  81
+              obligation_conditions!(object_attr, operator_val, assoc_model, 
  82
+                                     and_conditions, condition_values, joins)
  83
+            end
  84
+          elsif value.is_a?(Array) and value.length == 2
  85
+            operator, value = value
  86
+            
  87
+            case operator
  88
+            when :contains
  89
+              # contains: {:test_models => [:contains, obj]} <=>
  90
+              #           {:test_models => {:id => [:is, obj.id]}}
  91
+              obligation_conditions!(object_attribute, {:id => [:is, value.id]}, model,
  92
+                                     and_conditions, condition_values, joins)
  93
+            when :is
  94
+              id_obj_attr = :"#{object_attribute}_id"
  95
+              if model.columns_hash[id_obj_attr.to_s]
  96
+                and_conditions << "`#{model.table_name}`.#{id_obj_attr} = ?"
  97
+              else
  98
+                and_conditions << "`#{model.table_name}`.#{object_attribute} = ?"
  99
+              end
  100
+
  101
+              condition_values << (value.is_a?(ActiveRecord::Base) ? value.id : value)
  102
+            else
  103
+              raise AuthorizationError, "Unknown operator #{operator.inspect}"
  104
+            end
  105
+          else
  106
+            raise AuthorizationError, "Unexpected value element: #{value.inspect}"
  107
+          end
  108
+        end
  109
+
  110
+        # Named scope for limiting query results according to the authorization
  111
+        # of the current user.  If no privilege is given, :+read+ is assumed.
  112
+        # 
  113
+        #   User.with_permissions_to
  114
+        #   User.with_permissions_to(:update)
  115
+        #   User.with_permissions_to(:update, :context => :users)
  116
+        #   
  117
+        # As in the case of other named scopes, this one may be chained:
  118
+        #   User.with_permission_to.find(:all, :conditions...)
  119
+        # 
  120
+        # Options
  121
+        # [:+context+]
  122
+        #   Context for the privilege to be evaluated in; defaults to the
  123
+        #   model's table name.
  124
+        # [:+user+]
  125
+        #   User to be used for gathering obligations; defaults to the
  126
+        #   current user.
  127
+        #
  128
+        def self.with_permissions_to (*args)
  129
+          scopes[:with_permissions_to].call(self, *args)
  130
+        end
  131
+        
  132
+        # Activates model security for the current model.  Then, CRUD operations
  133
+        # are checked against the authorization of the current user.  The
  134
+        # privileges are :+create+, :+read+, :+update+ and :+delete+ in the
  135
+        # context of the model.  By default, :+read+ is not checked because of
  136
+        # performance impacts, especially with large result sets.
  137
+        # 
  138
+        #   class User < ActiveRecord::Base
  139
+        #     using_access_control
  140
+        #   end
  141
+        #   
  142
+        # If an operation is not permitted, a Authorization::AuthorizationError
  143
+        # is raised.
  144
+        # 
  145
+        # Available options
  146
+        # [:+context+] Specify context different from the models table name.
  147
+        # [:+include_read+] Also check for :+read+ privilege after find.
  148
+        #
  149
+        def self.using_access_control (options = {})
  150
+          options = {
  151
+            :context => nil,
  152
+            :include_read => false
  153
+          }.merge(options)
  154
+          context = (options[:context] || self.table_name).to_sym
  155
+          
  156
+          class_eval do
  157
+            before_create do |object|
  158
+              Authorization::Engine.instance.permit!(:create, :object => object,
  159
+                :context => context)
  160
+            end
  161
+            
  162
+            before_update do |object|
  163
+              Authorization::Engine.instance.permit!(:update, :object => object,
  164
+                :context => context)
  165
+            end
  166
+            
  167
+            before_destroy do |object|
  168
+              Authorization::Engine.instance.permit!(:delete, :object => object,
  169
+                :context => context)
  170
+            end
  171
+            
  172
+            # only called if after_find is implemented
  173
+            after_find do |object|
  174
+              Authorization::Engine.instance.permit!(:read, :object => object,
  175
+                :context => context)
  176
+            end
  177
+            
  178
+            if options[:include_read]
  179
+              def after_find; end
  180
+            end
  181
+          end
  182
+        end
  183
+      end
  184
+    end
  185
+
  186
+  end
  187
+end
216  lib/reader.rb
... ...
@@ -0,0 +1,216 @@
  1
+# Authorization::Reader
  2
+
  3
+require File.dirname(__FILE__) + '/authorization.rb'
  4
+
  5
+module Authorization
  6
+  # Parses an authorization configuration file in the authorization DSL and
  7
+  # constructs a data model of its contents.
  8
+  # 
  9
+  # For examples and the modelled data model, see the 
  10
+  # README[link:files/README.html].
  11
+  #
  12
+  # Also, see 
  13
+  # * AuthorizationRulesReader#role,
  14
+  # * AuthorizationRulesReader#includes,
  15
+  # * AuthorizationRulesReader#has_permission,
  16
+  # * AuthorizationRulesReader#on,
  17
+  # * AuthorizationRulesReader#to,
  18
+  # * AuthorizationRulesReader#if_attribute,
  19
+  # * PrivilegesReader#privilege and
  20
+  # * PrivilegesReader#includes
  21
+  # for details.
  22
+  #
  23
+  module Reader
  24
+    class ParameterError < Exception; end
  25
+    class DSLSyntaxError < Exception; end
  26
+    
  27
+    class DSLReader
  28
+      attr_reader :privileges_reader, :auth_rules_reader # :nodoc:
  29
+
  30
+      def initialize ()
  31
+        @privileges_reader = PrivilegesReader.new
  32
+        @auth_rules_reader = AuthorizationRulesReader.new
  33
+      end
  34
+
  35
+      def parse (dsl_data, file_name = nil)
  36
+        if file_name
  37
+          DSLMethods.new(self).instance_eval(dsl_data, file_name)
  38
+        else
  39
+          DSLMethods.new(self).instance_eval(dsl_data)
  40
+        end
  41
+      rescue SyntaxError, NoMethodError, NameError => e
  42
+        raise DSLSyntaxError, "Illegal DSL syntax: #{e}"
  43
+      end
  44
+
  45
+      # TODO cache reader in production mode?
  46
+      def self.load (dsl_file)
  47
+        reader = new
  48
+        reader.parse(File.read(dsl_file), dsl_file)
  49
+        reader
  50
+      end
  51
+
  52
+      # DSL methods
  53
+      class DSLMethods # :nodoc:
  54
+        def initialize (parent)
  55
+          @parent = parent
  56
+        end
  57
+
  58
+        def privileges (&block)
  59
+          @parent.privileges_reader.instance_eval(&block)
  60
+        end
  61
+
  62
+        def contexts (&block)
  63
+          # Not implemented
  64
+        end
  65
+
  66
+        def authorization (&block)
  67
+          @parent.auth_rules_reader.instance_eval(&block)
  68
+        end
  69
+      end
  70
+    end
  71
+
  72
+    # TODO handle privileges with separated context
  73
+    class PrivilegesReader
  74
+      attr_reader :privileges, :privilege_hierarchy # :nodoc:
  75
+
  76
+      def initialize # :nodoc:
  77
+        @current_priv = nil
  78
+        @current_context = nil
  79
+        @privileges = []