Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 0ba2478848e932b1df7554b8d536d40a0b95978b 0 parents
@stffn authored
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 [name of plugin creator]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
277 README
@@ -0,0 +1,277 @@
+= Authorization
+
+This plugin offers an authorization mechanism inspired by _RBAC_. The most
+notable distinction to existing authorization plugins is the declarative
+authorization approach. That is, authorization rules are not programmatically
+in between business logic but in an authorization configuration.
+
+Currently, Rails authorization plugins only provide for programmatic
+authorization rules. That is, the developer needs to specify which roles are
+allowed to access a specific controller action or a part of a view, which is
+not DRY. With a growing application code base and functions, as it happens
+especially in agile development processes, it may be decided to introduce new
+roles. Then, at several places of the source code the new group needs to be
+added, possibly leading to omissions and thus hard to test errors. Another
+aspect are changing authorization requirements in development or
+even after taking the application into production. Then, privileges of
+certain roles need to be easily adjusted when the original assumptions
+concerning access control prove unrealistic. In these situations, a
+declarative approach as offered by this plugin increases the development
+and maintenance efficiency.
+
+Plugin features
+* Authorization at controller action level
+* Authorization helpers for Views
+* Authorization at model level
+ * Authorize CRUD (Create, Read, Update, Delete) activities
+ * Query rewriting to automatically only fetch authorized records
+* DSL for specifying Authorization rules in an authorization configuration
+
+
+Requirements
+* Authentication solution ([TODO] examples):
+ * User object in Controller.current_user, etc. [TODO]
+* User object needs to respond to a method :roles and return an array of role symbols
+See below.
+
+
+= Authorization Data Model
+
+ ----- App domain ----|-------- Authorization conf ---------|------- App domain ------
+
+ includes includes
+ .--. .---.
+ | v | v
+ .------. can_play .------. has_permission .------------. requires .----------.
+ | User |----------->| Role |----------------->| Permission |<-----------| Activity |
+ '------' * * '------' * * '------------' 1 * '----------'
+ |
+ .-------+------.
+ 1 / | 1 \ *
+ .-----------. .---------. .-----------.
+ | Privilege | | Context | | Attribute |
+ '-----------' '---------' '-----------'
+
+In the application domain, each *User* may be assigned to *Roles* that should
+define the users' job in the application, such as _Administrator_. On the
+right-hand side of this diagram, application developers specify which *Permissions*
+are necessary for users to perform activities, such as calling a controller action,
+viewing parts of a View or acting on records in the database. Note that
+Permissions consist of an *Privilege* that is to be performed, such as _read_,
+and a *Context* in that the Operation takes place, such as _companies_.
+
+In the authorization configuration, Permissions are assigned to Roles and Role
+and Permission hierarchies are defined. *Attributes* may be employed to allow
+authorization according to dynamic information about the context and the
+current user, e.g. "only allow access on employees that belong to the
+current user's branch."
+
+
+= Examples
+
+== Controller
+
+If authentication is in place, enabling user-specific access control may be
+as simple as one call to filter_access_to :all which simply requires the
+according privileges for present actions. E.g. the privilege index_users is
+required for action index. This works as a first default configuration
+for RESTful controllers, with these privileges easily handled in the
+authorization configuration, which will be described below.
+
+ class EmployeeController < ApplicationController
+ filter_access_to :all
+ def index
+ ...
+ end
+ ...
+ end
+
+When custom actions are added to such a controller, it helps to define more
+clearly which privileges are the respective requirements. That is when the
+filter_access_to call may become more verbose:
+
+ class EmployeeController < ApplicationController
+ filter_access_to :all
+ # this one would be included in :all, but :read seems to be
+ # a more suitable privilege than :auto_complete_for_user_name
+ filter_access_to :auto_complete_for_employee_name, :require => :read
+ def auto_complete_for_employee_name
+ ...
+ end
+ ...
+ end
+
+For some actions it might be necessary to check certain attributes of the
+object the action is to be acting on. Then, the object needs to be loaded
+before the action's access control is evaluated. On the other hand, some actions
+might prefer the authorization to ignore specific attribute checks as the object is
+unknown at checking time, so attribute checks and thus automatic loading of
+objects needs to be enabled explicitly.
+
+ class EmployeeController < ApplicationController
+ ...
+ filter_access_to :update, :attribute_check => true
+ def update
+ # @employee is already loaded from param[:id]
+ @employee ||= Employee.find(param[:id])
+ ...
+ end
+ ...
+ end
+
+For further customization of object loading, have a look at the complete
+API documentation of filter_access_to in
+Authorization::AuthorizationInController::ClassMethods.
+
+
+== Views
+
+In views, a simple permitted_to? helper makes showing blocks according to the
+current user's privileges easy:
+
+ <% permitted_to?(:create, :employees) do %>
+ <%= link_to 'New', new_employee_path %>
+ <% end %>
+ ...
+ <% for employee in @employees %>
+ ...
+ <%= link_to 'Edit', edit_employee_path(company) if permitted_to?(:update, employee) %>
+ <% end %>
+
+See also Authorization::AuthorizationHelper.
+
+
+== Models
+
+There are two destinct features for model security built into this plugin:
+authorizing CRUD operations on objects as well as query rewriting to limit
+results according to certain privileges.
+
+See also Authorization::AuthorizationInModel.
+
+=== Model security for CRUD opterations

Just an FYI, "operations" is misspelled here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+To activate model security, all it takes is an explicit enabling for each
+model that model security should be enforced on, i.e.
+
+ class Employee < ActiveRecord::Base
+ using_access_control
+ ...
+ end
+
+As access control on read are costly, with possibly lots of objects being
+loaded at a time in one query, checks on read need to be actived explicitly by
+adding the :include_read option.
+
+TODO: Example, Exceptions
+
+=== Query rewriting using named scopes
+When retrieving large sets of records from databases, any authorization needs
+to be integrated into the query in order to prevent inefficient filtering
+afterwards and to use LIMIT and OFFSET in SQL statements. To keep authorization
+rules out of the source code, this plugin offers query rewriting mechanisms
+through named scopes.
+
+ Employee.having_permission_to(:read)
+
+returns all employee records that the current user is authorized to read. In
+addition, just like normal named scopes, query rewriting may be chained with
+the usual find method:
+
+ Employee.having_permission_to(:read).find(:all, :conditions => ...)
+
+=== Custom Rewriting
+
+
+== Authorization Rules
+
+Authorization rules are defined in config/authorization_rules.rb. E.g.
+
+ authorization do
+ role :admin do
+ has_permission_on :employees, :to => [:create, :read, :update, :delete]
+ end
+ end
+
+Privileges, such as :create, may be put into hierarchies to simplify
+maintenance. So the example above has the same meaning as
+
+ authorization do
+ role :admin do
+ has_permission_on :employees, :to => :manage
+ end
+ end
+
+ privileges do
+ privilege :manage do
+ includes :create, :read, :update, :delete
+ end
+ end
+
+Privilege hierarchies may be context-specific, e.g. applicable to :employees.
+
+ privileges do
+ privilege :manage, :employees, :includes => :increase_salary
+ end
+
+For more complex use cases, authorizations need to be based on attributes. E.g.
+a branch admin should manage only employees of his branch.
+
+ authorization do
+ role :branch_admin do
+ has_permission.on :employees do
+ to :manage
+ # user refers to the current_user when evaluating
+ if_attribute :branch => is {user.branch}
+ end
+ end
+ end
+
+Lastly, not only privileges may be organized in a hierarchy but roles as well.
+Here, project manager inherit the permissions of employees.
+
+ role :project_manager do
+ includes :employee
+ end
+
+See also Authorization::Reader.
+
+= Installation
+TODO: how-to
+
+== Meeting the Requirements
+TODO: Helping to overcome requirements
+* install acts_as_authenticated or restful_authentication
+ or provide Controller.current_user
+
+ restful_authentication
+ ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/
+ ruby script/generate authenticated user sessions
+ move include AuthenticatedSystem to ApplicationController
+ add before_filter :login_required
+
+ $current_user ?
+
+* add roles field to User model
+ Migration for roles table
+ Model for roles
+ has_many :roles in User model
+
+
+== Debugging Authorization
+
+* Logging
+* Exceptions
+
+
+== Contact
+
+Steffen Bartsch
+TZI, Universität Bremen, Germany
+sbartsch at tzi.org
+
+
+== Licence
+
+Copyright (c) 2008 Steffen Bartsch, TZI, Universität Bremen, Germany
+released under the MIT license
+
23 Rakefile
@@ -0,0 +1,23 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the authorization plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the authorization plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Authorization'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.options << '--charset' << 'utf-8'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
8 init.rb
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + "/lib/helper.rb"
+require File.dirname(__FILE__) + "/lib/in_controller.rb"
+require File.dirname(__FILE__) + "/lib/in_model.rb"
+
+ActionController::Base.send :include, Authorization::AuthorizationInController
+ActionController::Base.helper Authorization::AuthorizationHelper
+
+ActiveRecord::Base.send :include, Authorization::AuthorizationInModel
318 lib/authorization.rb
@@ -0,0 +1,318 @@
+# Authorization
+require File.dirname(__FILE__) + '/reader.rb'
+require "set"
+
+
+module Authorization
+ class AuthorizationError < Exception ; end
+ class NotAuthorized < AuthorizationError ; end
+ class AttributeAuthorizationError < NotAuthorized ; end
+ class AuthorizationUsageError < AuthorizationError ; end
+
+ AUTH_DSL_FILE = "#{RAILS_ROOT}/config/authorization_rules.rb"
+
+ # Controller-independent method for retrieving the current user.
+ # Needed for model security where the current controller is not available.
+ def self.current_user
+ $current_user
+ end
+
+ # Controller-independent method for setting the current user.
+ def self.current_user=(user)
+ $current_user = user
+ end
+
+ @@ignore_access_control = false
+ # For use in test cases only
+ def self.ignore_access_control (state = nil) # :nodoc:
+ @@ignore_access_control = state unless state.nil?
+ @@ignore_access_control
+ end
+
+ # Authorization::Engine implements the reference monitor. It may be used
+ # for querying the permission and retrieving obligations under which
+ # a certain privilege is granted for the current user.
+ #
+ class Engine
+ # If +reader+ is not given, a new one is created with the default
+ # authorization configuration of +AUTH_DSL_FILE+. If given, may be either
+ # a Reader object or a path to a configuration file.
+ def initialize (reader = nil)
+ if reader.nil?
+ reader = Reader::DSLReader.load(AUTH_DSL_FILE)
+ elsif reader.is_a?(String)
+ reader = Reader::DSLReader.load(reader)
+ end
+ @privileges = reader.privileges_reader.privileges
+ # {priv => [[priv, ctx],...]}
+ @privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
+ @auth_rules = reader.auth_rules_reader.auth_rules
+ @roles = reader.auth_rules_reader.roles
+ @role_hierarchy = reader.auth_rules_reader.role_hierarchy
+
+ # {[priv, ctx] => [priv, ...]}
+ @rev_priv_hierarchy = {}
+ @privilege_hierarchy.each do |key, value|
+ value.each do |val|
+ @rev_priv_hierarchy[val] ||= []
+ @rev_priv_hierarchy[val] << key
+ end
+ end
+ end
+
+ # Returns true if privilege is met by the current user. Raises
+ # AuthorizationError otherwise. +privilege+ may be given with or
+ # without context. In the latter case, the :+context+ option is
+ # required.
+ #
+ # Options:
+ # [:+context+]
+ # The context part of the privilege.
+ # Defaults either to the +table_name+ of the given :+object+, if given.
+ # That is, either :+users+ for :+object+ of type User.
+ # Raises AuthorizationUsageError if
+ # context is missing and not to be infered.
+ # [:+object+] An context object to test attribute checks against.
+ # [:+skip_attribute_test+]
+ # Skips those attribute checks in the
+ # authorization rules. Defaults to false.
+ # [:+user+]
+ # The user to check the authorization for.
+ # Defaults to Authorization#current_user.
+ #
+ def permit! (privilege, options = {})
+ return true if Authorization.ignore_access_control
+ options = {
+ :object => nil,
+ :skip_attribute_test => false,
+ :context => nil
+ }.merge(options)
+ options[:context] ||= options[:object] && options[:object].class.table_name.to_sym rescue NoMethodError
+
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
+
+ # find a authorization rule that matches for at least one of the roles and
+ # at least one of the given privileges
+ attr_validator = AttributeValidator.new(user, options[:object])
+ #puts "All rules: #{@auth_rules.inspect}"
+ #rules_matching_roles = @auth_rules.select {|r| roles.include?(r.role) }
+ #puts "Matching for roles: #{rules_matching_roles.inspect}"
+ #puts "Matching rules for user #{user.inspect},"
+ #puts " roles #{roles.inspect},"
+ #puts " privs #{privileges.inspect}:"
+ #puts " #{matching_auth_rules(roles, privileges).inspect}"
+ rules = matching_auth_rules(roles, privileges, options[:context])
+ if rules.empty?
+ raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
+ "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
+ "context #{options[:context].inspect})."
+ end
+
+ grant_permission = rules.any? do |rule|
+ options[:skip_attribute_test] or
+ rule.attributes.empty? or
+ rule.attributes.any? {|attr| attr.validate? attr_validator }
+ end
+ unless grant_permission
+ raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{options[:object].inspect}."
+ end
+ true
+ end
+
+ # Calls permit! but rescues the AuthorizationException and returns false
+ # instead. If no exception is raised, permit? returns true and yields
+ # to the optional block.
+ def permit? (privilege, options = {}, &block) # :yields:
+ permit!(privilege, options)
+ yield if block_given?
+ true
+ rescue NotAuthorized
+ false
+ end
+
+ # Returns the obligations to be met by the current user for the given
+ # privilege as an array of obligation hashes in form of
+ # [{:object_attribute => obligation_value, ...}, ...]
+ # where +obligation_value+ is either (recursively) another obligation hash
+ # or a value spec, such as
+ # [operator, literal_value]
+ # The obligation hashes in the array should be OR'ed, conditions inside
+ # the hashes AND'ed.
+ #
+ # Example
+ # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
+ #
+ # Options
+ # [:+context+] See permit!
+ # [:+user+] See permit!
+ #
+ def obligations (privilege, options = {})
+ options = {:context => nil}.merge(options)
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
+ attr_validator = AttributeValidator.new(user)
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
+ #p rule
+ #obligation = {}
+ rule.attributes.collect {|attr| attr.obligation(attr_validator) }
+ #obligation
+ end.flatten
+ end
+
+ # Returns an instance of Engine, which is created if there isn't one
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
+ # a new instance is always created.
+ def self.instance (dsl_file = nil)
+ if dsl_file
+ @@instance = new(dsl_file)
+ else
+ @@instance ||= new
+ end
+ end
+
+ class AttributeValidator # :nodoc:
+ attr_reader :user, :object
+ def initialize (user, object = nil)
+ @user = user
+ @object = object
+ end
+
+ def evaluate (value_block)
+ # TODO cache?
+ instance_eval(&value_block)
+ end
+ end
+
+ private
+ def user_roles_privleges_from_options(privilege, options)
+ options = {
+ :user => Authorization.current_user,
+ :context => nil
+ }.merge(options)
+ user = options[:user]
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
+
+ raise AuthorizationUsageError, "No user object available" unless user
+ raise AuthorizationUsageError, "User object doesn't respond to roles" unless user.respond_to?(:roles)
+
+ roles = flatten_roles user.roles
+ privileges = flatten_privileges privileges, options[:context]
+ [user, roles, privileges]
+ end
+
+ def flatten_roles (roles)
+ # TODO caching?
+ flattened_roles = roles.clone.to_a
+ flattened_roles.each do |role|
+ flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
+ end
+ end
+
+ # Returns the privilege hierarchy flattened for given privileges in context.
+ def flatten_privileges (privileges, context = nil)
+ # TODO caching?
+ #if context.nil?
+ # context = privileges.collect { |p| p.to_s.split('_') }.
+ # reject { |p_p| p_p.length < 2 }.
+ # collect { |p_p| (p_p[1..-1] * '_').to_sym }.first
+ # raise AuthorizationUsageError, "No context given or inferable from privileges #{privileges.inspect}" unless context
+ #end
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
+ #context_regex = Regexp.new "_#{context}$"
+ # TODO work with contextless privileges
+ #flattened_privileges = privileges.collect {|p| p.to_s.sub(context_regex, '')}
+ flattened_privileges = privileges.clone #collect {|p| p.to_s.end_with?(context.to_s) ?
+ # p : [p, "#{p}_#{context}".to_sym] }.flatten
+ flattened_privileges.each do |priv|
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
+ end
+ end
+
+ def matching_auth_rules (roles, privileges, context)
+ @auth_rules.select {|rule| rule.matches? roles, privileges, context}
+ end
+ end
+
+ class AuthorizationRule
+ attr_reader :attributes, :context, :role, :privileges
+
+ def initialize (role, privileges_or_context = [], context = nil)
+ @role = role
+ @privileges = Set.new
+ if privileges_or_context.is_a?(Array)
+ @context = context
+ append_privileges(privileges_or_context)
+ else
+ @context = privileges_or_context
+ end
+ @attributes = []
+ end
+
+ def append_privileges (privs)
+ @privileges.merge(privs)
+ end
+
+ def append_attribute (attribute)
+ @attributes << attribute
+ end
+
+ def matches? (roles, privs, context = nil)
+ roles = [roles] unless roles.is_a?(Array)
+ @context == context and roles.include?(@role) and
+ not (@privileges & privs).empty?
+ end
+ end
+
+ class Attribute
+ # attr_conditions_hash of form
+ # { :object_attribute => [operator, value_block], ... }
+ # { :object_attribute => { :attr => ... } }
+ def initialize (conditions_hash)
+ @conditions_hash = conditions_hash
+ end
+
+ def validate? (attr_validator, object = nil, hash = nil)
+ object ||= attr_validator.object
+ return false unless object
+
+ (hash || @conditions_hash).all? do |attr, value|
+ begin
+ attr_value = object.send(attr)
+ rescue ArgumentError, NoMethodError => e
+ raise AuthorizationUsageError, "Error when calling #{attr} on " +
+ "#{object.inspect} for validating attribute: #{e}"
+ end
+ if value.is_a?(Hash)
+ validate?(attr_validator, attr_value, value)
+ elsif value.is_a?(Array) and value.length == 2
+ evaluated = attr_validator.evaluate(value[1])
+ case value[0]
+ when :is
+ attr_value == evaluated
+ when :contains
+ attr_value.include?(evaluated)
+ else
+ raise AuthorizationError, "Unknown operator #{value[0]}"
+ end
+ else
+ raise AuthorizationError, "Wrong conditions hash format"
+ end
+ end
+ end
+
+ # resolves all the values in condition_hash
+ def obligation (attr_validator, hash = nil)
+ hash = (hash || @conditions_hash).clone
+ hash.each do |attr, value|
+ if value.is_a?(Hash)
+ hash[attr] = obligation(attr_validator, value)
+ elsif value.is_a?(Array) and value.length == 2
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
+ else
+ raise AuthorizationError, "Wrong conditions hash format"
+ end
+ end
+ hash
+ end
+ end
+end
31 lib/helper.rb
@@ -0,0 +1,31 @@
+# Authorization::AuthorizationHelper
+require File.dirname(__FILE__) + '/authorization.rb'
+
+module Authorization
+ module AuthorizationHelper
+
+ # If the current user meets the given privilege, permitted_to? returns true
+ # and yields to the optional block. The attribute checks that are defined
+ # in the authorization rules are only evaluated if an object is given
+ # for context.
+ #
+ # Examples:
+ # <% permitted_to? :create, :users do %>
+ # <%= link_to 'New', new_user_path %>
+ # <% end %>
+ # ...
+ # <% if permitted_to? :create, :users %>
+ # <%= link_to 'New', new_user_path %>
+ # <% else %>
+ # You are not allowed to create new users!
+ # <% end %>
+ # ...
+ # <% for user in @users %>
+ # <%= link_to 'Edit', edit_user_path(user) if permitted_to? :update, user %>
+ # <% end %>
+ #
+ def permitted_to? (privilege, object_or_sym = nil, &block)
+ controller.permitted_to?(privilege, object_or_sym, &block)
+ end
+ end
+end
228 lib/in_controller.rb
@@ -0,0 +1,228 @@
+# Authorization::AuthorizationInController
+require File.dirname(__FILE__) + '/authorization.rb'
+
+module Authorization
+ module AuthorizationInController
+
+ def self.included(base) # :nodoc:
+ base.extend(ClassMethods)
+ end
+
+ # Returns the Authorization::Engine for the current controller.
+ def authorization_engine
+ @authorization_engine ||= Authorization::Engine.new
+ end
+
+ # If the current user meets the given privilege, permitted_to? returns true
+ # and yields to the optional block. The attribute checks that are defined
+ # in the authorization rules are only evaluated if an object is given
+ # for context.
+ #
+ # See examples for Authorization::AuthorizationHelper #permitted_to?
+ #
+ def permitted_to? (privilege, object_or_sym = nil, &block)
+ context = object = nil
+ if object_or_sym.is_a?(Symbol)
+ context = object_or_sym
+ else
+ object = object_or_sym
+ end
+ # TODO infer context also from self.class.name
+ authorization_engine.permit?(privilege,
+ {:user => current_user,
+ :object => object,
+ :context => context,
+ :skip_attribute_test => object.nil?},
+ &block)
+ end
+
+ module ClassMethods
+ #
+ # Defines a filter to be applied according to the authorization of the
+ # current user. Requires at least one symbol corresponding to an
+ # action as parameter. The special symbol :+all+ refers to all action.
+ # class UserController < ActionController
+ # filter_access_to :index
+ # filter_access_to :new, :edit
+ # filter_access_to :all
+ # ...
+ # end
+ #
+ # By default, required privileges are infered from the action name and
+ # the controller name. Thus, in UserController :+edit+ requires
+ # :+edit_users+. To specify required privilege, use the option :+require+
+ # filter_access_to :new, :create, :require => :create_users
+ #
+ # For further customization, a custom filter expression may be formulated
+ # in a block, which is then evaluated in the context of the controller
+ # on a matching request. That is, for checking two objects, use the
+ # following:
+ # filter_access_to :merge do
+ # authorization_engine.permit!(:update, :context => :users,
+ # :object => User.find(params[:original_id]),
+ # :user => current_user) and
+ # authorization_engine.permit!(:delete, :context => :users,
+ # :object => User.find(params[:id]),
+ # :user => current_user)
+ # end
+ # The block should raise a Authorization::AuthorizationError or return
+ # false if the access is to be denied.
+ #
+ # Later calls to filter_access_to with overlapping actions overwrite
+ # previous ones for that action.
+ #
+ # All options:
+ # [:+require+]
+ # Privilege required; defaults to action_name
+ # [:+context+]
+ # The privilege's context, defaults to controller_name, pluralized.
+ # [:+attribute_check+]
+ # Enables the check of attributes defined in the authorization rules.
+ # Defaults to false. If enabled, filter_access_to will try to load
+ # a context object employing either
+ # * the method from the :+load_method+ option or
+ # * a find on the context model, using +params+[:id] as id value.
+ # Any of these loading methods will only be employed if :+attribute_check+
+ # is enabled.
+ # [:+model+]
+ # The data model to load a context object from. Defaults to the
+ # context, singularized.
+ # [:+load_method+]
+ # Specify a method by symbol or a Proc object which should be used
+ # to load the object. Both should return the loaded object.
+ # If a Proc object is given, e.g. by way of
+ # +lambda+, it is called in the instance of the controller.
+ # Example demonstrating the default behaviour:
+ # filter_access_to :show, :attribute_check => true,
+ # :load_method => lambda { User.find(params[:id]) }
+ #
+
+ def filter_access_to (*args, &filter_block)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ options = {
+ :require => nil,
+ :context => nil,
+ :attribute_check => false,
+ :model => nil,
+ :load_method => nil
+ }.merge!(options)
+ privilege = options[:require]
+ context = options[:context]
+ actions = args
+
+ # TODO currently: default deny; make this configurable
+ # collect permits in controller array for use in one before_filter
+ unless class_variable_defined?(:@@permissions)
+ permissions = []
+ class_variable_set(:@@permissions, permissions)
+ before_filter do |contr|
+ all_permissions = permissions.select {|p| p.actions.include?(:all)}
+ matching_permissions = permissions.select {|p| p.matches?(contr.action_name)}
+ allowed = false
+ auth_exception = nil
+ begin
+ allowed = if !matching_permissions.empty?
+ matching_permissions.all? {|perm| perm.permit!(contr)}
+ elsif !all_permissions.empty?
+ all_permissions.all? {|perm| perm.permit!(contr)}
+ else
+ false
+ end
+ rescue AuthorizationError => e
+ auth_exception = e
+ end
+
+ unless allowed
+ if all_permissions.empty? and matching_permissions.empty?
+ contr.logger.warn "Permission denied: No matching filter access " +
+ "rule found for #{contr.class.controller_name}.#{contr.action_name}"
+ elsif auth_exception
+ contr.logger.info "Permission denied: #{auth_exception}"
+ end
+ if contr.respond_to?(:permission_denied)
+ # permission_denied needs to render or redirect
+ contr.send(:permission_denied)
+ else
+ contr.send(:render, :text => "You are not allowed to access this action.")
+ end
+ end
+ end
+ end
+
+ class_variable_get(:@@permissions).each do |perm|
+ perm.remove_actions(actions)
+ end
+ class_variable_get(:@@permissions) <<
+ ControllerPermission.new(actions, privilege, context,
+ options[:attribute_check],
+ options[:model],
+ options[:load_method],
+ filter_block)
+ end
+ end
+ end
+
+ class ControllerPermission # :nodoc:
+ attr_reader :actions, :privilege, :context
+ def initialize (actions, privilege, context, attribute_check = false,
+ load_object_model = nil, load_object_method = nil,
+ filter_block = nil)
+ @actions = actions.to_set
+ @privilege = privilege
+ @context = context
+ @load_object_model = load_object_model
+ @load_object_method = load_object_method
+ @filter_block = filter_block
+ @attribute_check = attribute_check
+ end
+
+ def matches? (action_name)
+ @actions.include?(action_name.to_sym)
+ end
+
+ def permit! (contr)
+ if @filter_block
+ return contr.instance_eval(&@filter_block)
+ end
+ context = @context || contr.class.controller_name.pluralize.to_sym
+ object = @attribute_check ? load_object(contr, context) : nil
+ privilege = @privilege || :"#{contr.action_name}"
+
+ #puts "Trying permit?(#{privilege.inspect}, "
+ #puts " :user => #{contr.send(:current_user).inspect}, "
+ #puts " :object => #{object.inspect},"
+ #puts " :skip_attribute_test => #{!@attribute_check},"
+ #puts " :context => #{contr.class.controller_name.pluralize.to_sym})"
+ res = contr.authorization_engine.permit!(privilege,
+ :user => contr.send(:current_user),
+ :object => object,
+ :skip_attribute_test => !@attribute_check,
+ :context => context)
+ #puts "permit? result: #{res.inspect}"
+ res
+ end
+
+ def remove_actions (actions)
+ @actions -= actions
+ end
+
+ private
+ def load_object(contr, context)
+ if @load_object_method and @load_object_method.is_a?(Symbol)
+ contr.send(@load_object_method)
+ elsif @load_object_method and @load_object_method.is_a?(Proc)
+ contr.instance_eval(&@load_object_method)
+ else
+ load_object_model = @load_object_model || context.to_s.classify.constantize
+ instance_var = :"@#{load_object_model.name.underscore}"
+ object = contr.instance_variable_get(instance_var)
+ unless object
+ # catch ActiveRecord::RecordNotFound?
+ object = load_object_model.find(contr.params[:id])
+ contr.instance_variable_set(instance_var, object)
+ end
+ object
+ end
+ end
+ end
+end
187 lib/in_model.rb
@@ -0,0 +1,187 @@
+# Authorization::AuthorizationInModel
+require File.dirname(__FILE__) + '/authorization.rb'
+
+module Authorization
+
+ module AuthorizationInModel
+
+ def self.included(base) # :nodoc:
+ #base.extend(ClassMethods)
+ base.module_eval do
+ scopes[:with_permissions_to] = lambda do |parent_scope, *args|
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ privilege = (args[0] || :read).to_sym
+ privileges = [privilege]
+ context = options[:context] || :"#{parent_scope.table_name}"
+
+ user = options[:user] || Authorization.current_user
+
+ engine = Authorization::Engine.instance
+ engine.permit!(privileges, :user => user, :skip_attribute_test => true,
+ :context => context)
+
+ scope_options = obligation_conditions(privileges, :user => user,
+ :context => context, :engine => engine, :model => parent_scope)
+
+ ActiveRecord::NamedScope::Scope.new(parent_scope, scope_options)
+ end
+
+ # Provides an conditions hash as expected by find with conditions
+ # matching the obligations for the given privilege, context and
+ # user.
+ #
+ # Options:
+ # [:+user+]
+ # User to create the obligations for, defaults to
+ # Authorization.current_user
+ # [:+context+] The privilege's context
+ # [:+model+]
+ # Model that the obligations should be applied on,
+ # defaults to self.
+ # [:+engine+]
+ # Authorization::Engine to be used for checks, defaults to
+ # Authorization::Engine.instance.
+ #
+ def self.obligation_conditions (privileges, options = {})
+ options = {
+ :user => Authorization.current_user,
+ :context => nil,
+ :model => self,
+ :engine => nil,
+ }.merge(options)
+ engine ||= Authorization::Engine.instance
+
+ conditions = []
+ condition_values = []
+ joins = Set.new
+
+ engine.obligations(privileges, :user => options[:user],
+ :context => options[:context]).each do |obligation|
+ and_conditions = []
+ obligation_conditions!(nil, obligation, options[:model],
+ and_conditions, condition_values, joins)
+ conditions << and_conditions.collect {|c| "#{c}"} * ' AND ' unless and_conditions.empty?
+ end
+
+ scope_options = {}
+ unless conditions.empty?
+ scope_options[:select] = "`#{options[:context]}`.*" if options[:context]
+ scope_options[:conditions] = [conditions.collect {|c| "(#{c})"} * ' OR '] + condition_values
+ scope_options[:joins] = joins.to_a unless joins.empty?
+ end
+ scope_options
+ end
+
+ def self.obligation_conditions!(object_attribute, value, model, and_conditions,
+ condition_values, joins) # :nodoc:
+ if value.is_a?(Hash)
+ value.each do |object_attr, operator_val|
+ joins << object_attribute if object_attribute
+ assoc_model = object_attribute ? model.reflect_on_association(object_attribute).klass : model
+ obligation_conditions!(object_attr, operator_val, assoc_model,
+ and_conditions, condition_values, joins)
+ end
+ elsif value.is_a?(Array) and value.length == 2
+ operator, value = value
+
+ case operator
+ when :contains
+ # contains: {:test_models => [:contains, obj]} <=>
+ # {:test_models => {:id => [:is, obj.id]}}
+ obligation_conditions!(object_attribute, {:id => [:is, value.id]}, model,
+ and_conditions, condition_values, joins)
+ when :is
+ id_obj_attr = :"#{object_attribute}_id"
+ if model.columns_hash[id_obj_attr.to_s]
+ and_conditions << "`#{model.table_name}`.#{id_obj_attr} = ?"
+ else
+ and_conditions << "`#{model.table_name}`.#{object_attribute} = ?"
+ end
+
+ condition_values << (value.is_a?(ActiveRecord::Base) ? value.id : value)
+ else
+ raise AuthorizationError, "Unknown operator #{operator.inspect}"
+ end
+ else
+ raise AuthorizationError, "Unexpected value element: #{value.inspect}"
+ end
+ end
+
+ # Named scope for limiting query results according to the authorization
+ # of the current user. If no privilege is given, :+read+ is assumed.
+ #
+ # User.with_permissions_to
+ # User.with_permissions_to(:update)
+ # User.with_permissions_to(:update, :context => :users)
+ #
+ # As in the case of other named scopes, this one may be chained:
+ # User.with_permission_to.find(:all, :conditions...)
+ #
+ # Options
+ # [:+context+]
+ # Context for the privilege to be evaluated in; defaults to the
+ # model's table name.
+ # [:+user+]
+ # User to be used for gathering obligations; defaults to the
+ # current user.
+ #
+ def self.with_permissions_to (*args)
+ scopes[:with_permissions_to].call(self, *args)
+ end
+
+ # Activates model security for the current model. Then, CRUD operations
+ # are checked against the authorization of the current user. The
+ # privileges are :+create+, :+read+, :+update+ and :+delete+ in the
+ # context of the model. By default, :+read+ is not checked because of
+ # performance impacts, especially with large result sets.
+ #
+ # class User < ActiveRecord::Base
+ # using_access_control
+ # end
+ #
+ # If an operation is not permitted, a Authorization::AuthorizationError
+ # is raised.
+ #
+ # Available options
+ # [:+context+] Specify context different from the models table name.
+ # [:+include_read+] Also check for :+read+ privilege after find.
+ #
+ def self.using_access_control (options = {})
+ options = {
+ :context => nil,
+ :include_read => false
+ }.merge(options)
+ context = (options[:context] || self.table_name).to_sym
+
+ class_eval do
+ before_create do |object|
+ Authorization::Engine.instance.permit!(:create, :object => object,
+ :context => context)
+ end
+
+ before_update do |object|
+ Authorization::Engine.instance.permit!(:update, :object => object,
+ :context => context)
+ end
+
+ before_destroy do |object|
+ Authorization::Engine.instance.permit!(:delete, :object => object,
+ :context => context)
+ end
+
+ # only called if after_find is implemented
+ after_find do |object|
+ Authorization::Engine.instance.permit!(:read, :object => object,
+ :context => context)
+ end
+
+ if options[:include_read]
+ def after_find; end
+ end
+ end
+ end
+ end
+ end
+
+ end
+end
216 lib/reader.rb
@@ -0,0 +1,216 @@
+# Authorization::Reader
+
+require File.dirname(__FILE__) + '/authorization.rb'
+
+module Authorization
+ # Parses an authorization configuration file in the authorization DSL and
+ # constructs a data model of its contents.
+ #
+ # For examples and the modelled data model, see the
+ # README[link:files/README.html].
+ #
+ # Also, see
+ # * AuthorizationRulesReader#role,
+ # * AuthorizationRulesReader#includes,
+ # * AuthorizationRulesReader#has_permission,
+ # * AuthorizationRulesReader#on,
+ # * AuthorizationRulesReader#to,
+ # * AuthorizationRulesReader#if_attribute,
+ # * PrivilegesReader#privilege and
+ # * PrivilegesReader#includes
+ # for details.
+ #
+ module Reader
+ class ParameterError < Exception; end
+ class DSLSyntaxError < Exception; end
+
+ class DSLReader
+ attr_reader :privileges_reader, :auth_rules_reader # :nodoc:
+
+ def initialize ()
+ @privileges_reader = PrivilegesReader.new
+ @auth_rules_reader = AuthorizationRulesReader.new
+ end
+
+ def parse (dsl_data, file_name = nil)
+ if file_name
+ DSLMethods.new(self).instance_eval(dsl_data, file_name)
+ else
+ DSLMethods.new(self).instance_eval(dsl_data)
+ end
+ rescue SyntaxError, NoMethodError, NameError => e
+ raise DSLSyntaxError, "Illegal DSL syntax: #{e}"
+ end
+
+ # TODO cache reader in production mode?
+ def self.load (dsl_file)
+ reader = new
+ reader.parse(File.read(dsl_file), dsl_file)
+ reader
+ end
+
+ # DSL methods
+ class DSLMethods # :nodoc:
+ def initialize (parent)
+ @parent = parent
+ end
+
+ def privileges (&block)
+ @parent.privileges_reader.instance_eval(&block)
+ end
+
+ def contexts (&block)
+ # Not implemented
+ end
+
+ def authorization (&block)
+ @parent.auth_rules_reader.instance_eval(&block)
+ end
+ end
+ end
+
+ # TODO handle privileges with separated context
+ class PrivilegesReader
+ attr_reader :privileges, :privilege_hierarchy # :nodoc:
+
+ def initialize # :nodoc:
+ @current_priv = nil
+ @current_context = nil
+ @privileges = []
+ # {priv => [[priv,ctx], ...]}
+ @privilege_hierarchy = {}
+ end
+
+ def append_privilege (priv) # :nodoc:
+ @privileges << priv unless @privileges.include?(priv)
+ end
+
+ # Defines part of a privilege hierarchy. For the given +privilege+,
+ # included privileges may be defined in the block (through includes)
+ # or as option :+includes+. If the optional context is given,
+ # the privilege hierarchy is limited to that context.
+ #
+ def privilege (privilege, context = nil, options = {}, &block)
+ if context.is_a?(Hash)
+ options = context
+ context = nil
+ end
+ @current_priv = privilege
+ @current_context = context
+ append_privilege privilege
+ instance_eval(&block) if block
+ includes(*options[:includes]) if options[:includes]
+ ensure
+ @current_priv = nil
+ @current_context = nil
+ end
+
+ # Specifies +privileges+ that are to be assigned as lower ones.
+ def includes (*privileges)
+ raise ParameterError, "includes only in privilege block" if @current_priv.nil?
+ privileges.each do |priv|
+ append_privilege priv
+ @privilege_hierarchy[@current_priv] ||= []
+ @privilege_hierarchy[@current_priv] << [priv, @current_context]
+ end
+ end
+ end
+
+ class AuthorizationRulesReader
+ attr_reader :roles, :role_hierarchy, :auth_rules # :nodoc:
+
+ def initialize # :nodoc:
+ @current_role = nil
+ @current_rule = nil
+ @roles = []
+ # higher_role => [lower_roles]
+ @role_hierarchy = {}
+ @auth_rules = []
+ end
+
+ def append_role (role) # :nodoc:
+ @roles << role unless @roles.include? role
+ end
+
+ # Defines the authorization rules for the given +role+ in the
+ # following block.
+ def role (role, &block)
+ append_role role
+ @current_role = role
+ yield
+ ensure
+ @current_role = nil
+ end
+
+ # Roles may inherit all the rights from subroles. The given +roles+
+ # become subroles of the current block's role.
+ def includes (*roles)
+ raise ParameterError, "includes only in role blocks" if @current_role.nil?
+ @role_hierarchy[@current_role] ||= []
+ @role_hierarchy[@current_role] += roles.flatten
+ end
+
+ # Allows the definition of privileges to be allowed for the current role,
+ # either in a has_permission block or directly in has_permission.to.
+ def has_permission_on (context, options = {}, &block)
+ raise ParameterError, "has_permission_on only allowed in role blocks" if @current_role.nil?
+ options = {:to => []}.merge(options)
+
+ privs = options[:to]
+ privs = [privs] unless privs.is_a?(Array)
+ raise ParameterError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
+
+ rule = AuthorizationRule.new(@current_role, privs, context)
+ @auth_rules << rule
+ if block_given?
+ @current_rule = rule
+ yield
+ # TODO ensure?
+ @current_rule = nil
+ end
+ end
+
+ # Used in a has_permission_on block, to may be used to specify privileges
+ # to be assigned to the current role.
+ def to (*privs)
+ # in has_permission block
+ raise ParameterError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
+ @current_rule.append_privileges(privs)
+ end
+
+ # In a has_permission block, if_attribute specifies additional conditions
+ # of dynamic parameters that have to be met for the user to meet the
+ # privileges in this block. Multiple if_attribute statements are OR'ed.
+ def if_attribute (attr_conditions_hash)
+ raise ParameterError, "if_attribute only in has_permission blocks" if @current_rule.nil?
+ parse_attribute_conditions_hash!(attr_conditions_hash)
+ @current_rule.append_attribute Attribute.new(attr_conditions_hash)
+ end
+
+ # In an if_attribute statement, is says that the value has to be exactly
+ # met by the if_attribute attribute.
+ def is (&block)
+ [:is, block]
+ end
+
+ # In an if_attribute statement, contains says that the value has to be
+ # part of the collection specified by the if_attribute attribute.
+ def contains (&block)
+ [:contains, block]
+ end
+
+ private
+ def parse_attribute_conditions_hash! (hash)
+ merge_hash = {}
+ hash.each do |key, value|
+ if value.is_a?(Hash)
+ parse_attribute_conditions_hash!(value)
+ elsif !value.is_a?(Array)
+ merge_hash[key] = [:is, lambda { value }]
+ end
+ end
+ hash.merge!(merge_hash)
+ end
+ end
+ end
+end
82 tasks/authorization_tasks.rake
@@ -0,0 +1,82 @@
+namespace :auth do
+ desc "Lists all privileges used in controllers, views, models"
+ task :used_privileges do
+ # TODO note where privileges are used
+ require File.join(RAILS_ROOT, 'config', 'boot.rb')
+ require File.join(RAILS_ROOT, 'config', 'environment.rb')
+ controllers = [ApplicationController]
+ Dir.new("#{RAILS_ROOT}/app/controllers").entries.each do |controller_file|
+ if controller_file =~ /_controller/
+ controllers << controller_file.gsub(".rb","").camelize.constantize
+ end
+ end
+ perms = controllers.select {|c| c.send(:class_variable_defined?, :@@permissions)}.
+ inject([]) do |all, c|
+ contr_context = c.name.sub("Controller", "").tableize.to_sym
+ contr_perms = c.send(:class_variable_get, :@@permissions).collect do |cp|
+ [cp.privilege, cp.context || contr_context, cp]
+ end
+ if contr_perms.any? {|cp| cp[0].nil?}
+ contr_perms += c.send(:action_methods).collect {|am| am.to_sym}.
+ reject {|am| contr_perms.any? {|cp| cp[2].actions.include?(am)}}.
+ collect {|am| [am, contr_context]}
+ end
+ all += contr_perms.reject {|cp| cp[0].nil?}.collect {|cp| cp[0..1]}
+ end
+
+ model_files = `grep -l "^[[:space:]]*using_access_control" #{RAILS_ROOT}/app/models/*.rb`.split("\n")
+ models_with_ac = model_files.collect {|mf| mf.sub(/^.*\//, "").sub(".rb", "").tableize.to_sym}
+ model_security_privs = [:create, :read, :update, :delete]
+ models_with_ac.each {|m| perms += model_security_privs.collect{|msp| [msp, m]}}
+
+ grep_file_pattern = "#{RAILS_ROOT}/app/models/*.rb #{RAILS_ROOT}/app/views/**/* #{RAILS_ROOT}/app/controllers/*.rb"
+ `grep "permitted_to?" #{grep_file_pattern}`.split("\n").each do |ptu|
+ file, grep_match = ptu.split(':', 2)
+ context = privilege = nil
+ if (match = grep_match.match(/permitted_to\?\(?\s*:(\w+),\s*(:?@?\w+)/))
+ privilege = match[1].to_sym
+ if match[2][0..0] == ':'
+ context = match[2][1..-1].to_sym
+ else
+ c = (match[2][0..0] == '@' ? match[2][1..-1] : match[2]).pluralize.to_sym
+ context = c if perms.any? {|p| p[1] == c}
+ end
+ end
+ if privilege.nil? or context.nil?
+ puts "Could not handle: #{ptu}"
+ else
+ perms << [privilege, context]
+ end
+ end
+
+ `grep ".with_permissions_to" #{grep_file_pattern}`.split("\n").each do |wpt|
+ file, grep_match = wpt.split(':', 2)
+ context = privilege = nil
+ if match = grep_match.match(/(\w+\.)?with_permissions_to(\(:\w+)?/)
+ c = match[1][0..-2].tableize.to_sym if match[1]
+ c ||= File.basename(file, '.rb').tableize.to_sym
+ context = c if perms.any? {|p| p[1] == c}
+ privilege = match[2] && match[2][(match[2][0..0]=='(' ? 2 : 1)..-1].to_sym
+ privilege ||= :read
+ end
+ if privilege.nil? or context.nil?
+ puts "Could not handle: #{ptu}"
+ else
+ perms << [privilege, context]
+ end
+ end
+
+ perms.uniq!
+ perm_hash = {}
+ perms.each do |cp|
+ perm_hash[cp[1]] ||= []
+ perm_hash[cp[1]] << cp[0]
+ end
+
+ puts "Privileges currently in use:"
+ perm_hash.each do |context, privileges|
+ puts " #{context.inspect}:\t#{privileges.collect {|p| p.inspect}.sort * ', '}"
+ #privileges.collect {|p| p.inspect}.sort.each {|p| puts " #{p}"}
+ end
+ end
+end
202 test/authorization_test.rb
@@ -0,0 +1,202 @@
+require File.join(File.dirname(__FILE__), 'test_helper.rb')
+
+class AuthorizationTest < Test::Unit::TestCase
+
+ def test_permit
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test
+ end
+ end
+ }
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role, :test_role_2))
+ assert !engine.permit?(:test_2, :context => :permissions_2,
+ :user => MockUser.new(:test_role))
+ assert !engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role_2))
+ end
+
+ def test_role_hierarchy
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ includes :lower_role
+ has_permission_on :permissions, :to => :test
+ end
+ role :lower_role do
+ has_permission_on :permissions, :to => :lower
+ end
+ end
+ }
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:lower, :context => :permissions,
+ :user => MockUser.new(:test_role))
+ end
+
+ def test_role_hierarchy_infinity
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ includes :lower_role
+ has_permission_on :permissions, :to => :test
+ end
+ role :lower_role do
+ includes :higher_role
+ has_permission_on :permissions, :to => :lower
+ end
+ end
+ }
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:lower, :context => :permissions,
+ :user => MockUser.new(:test_role))
+ end
+
+ def test_privilege_hierarchy
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ privileges do
+ privilege :test, :permissions do
+ includes :lower
+ end
+ end
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test
+ end
+ end
+ }
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:lower, :context => :permissions,
+ :user => MockUser.new(:test_role))
+ end
+
+ def test_privilege_hierarchy_without_context
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ privileges do
+ privilege :read do
+ includes :list, :show
+ end
+ end
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :read
+ end
+ end
+ }
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:list, :context => :permissions,
+ :user => MockUser.new(:test_role))
+ end
+
+ def test_attribute_is
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %|
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test do
+ if_attribute :test_attr => is { user.test_attr }
+ end
+ end
+ end
+ |
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role, :test_attr => 1),
+ :object => MockDataObject.new(:test_attr => 1))
+ assert((not(engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role, :test_attr => 2),
+ :object => MockDataObject.new(:test_attr => 1)))))
+ end
+
+ def test_attribute_contains
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %|
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test do
+ if_attribute :test_attr => contains { user.test_attr }
+ end
+ end
+ end
+ |
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role, :test_attr => 1),
+ :object => MockDataObject.new(:test_attr => [1,2]))
+ assert !engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role, :test_attr => 3),
+ :object => MockDataObject.new(:test_attr => [1,2]))
+ end
+
+ def test_attribute_deep
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %|
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test do
+ if_attribute :test_attr_1 => {:test_attr_2 => contains { 1 }}
+ end
+ end
+ end
+ |
+ engine = Authorization::Engine.new(reader)
+ attr_1_struct = Struct.new(:test_attr_2)
+ assert engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role),
+ :object => MockDataObject.new(:test_attr_1 => attr_1_struct.new([1,2])))
+ assert !engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role),
+ :object => MockDataObject.new(:test_attr_1 => attr_1_struct.new([3,4])))
+ assert_equal [{:test_attr_1 => {:test_attr_2 => [:contains, 1]}}],
+ engine.obligations(:test, :context => :permissions,
+ :user => MockUser.new(:test_role))
+ end
+
+ def test_attribute_non_block
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %|
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test do
+ if_attribute :test_attr => 1
+ end
+ end
+ end
+ |
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role),
+ :object => MockDataObject.new(:test_attr => 1))
+ assert !engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role),
+ :object => MockDataObject.new(:test_attr => 2))
+ end
+
+ def test_attribute_multiple
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test do
+ if_attribute :test_attr => 1
+ if_attribute :test_attr => 2 # or
+ end
+ end
+ end
+ }
+ engine = Authorization::Engine.new(reader)
+ assert engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role),
+ :object => MockDataObject.new(:test_attr => 1))
+ assert engine.permit?(:test, :context => :permissions,
+ :user => MockUser.new(:test_role),
+ :object => MockDataObject.new(:test_attr => 2))
+ end
+end
250 test/controller_test.rb
@@ -0,0 +1,250 @@
+require File.join(File.dirname(__FILE__), 'test_helper.rb')
+require File.dirname(__FILE__) + '/../lib/in_controller.rb'
+
+MockController.send :include, Authorization::AuthorizationInController
+
+class LoadMockObject < MockDataObject
+ def self.find(*args)
+ new :id => args[0]
+ end
+end
+class SpecificMockController < MockController
+ filter_access_to :test_action, :require => :test, :context => :permissions
+ filter_access_to :test_action_2, :require => :test, :context => :permissions_2
+ filter_access_to :show
+ filter_access_to :edit, :create, :require => :test, :context => :permissions
+ filter_access_to :edit_2, :require => :test, :context => :permissions,
+ :attribute_check => true, :model => LoadMockObject
+ filter_access_to :new, :require => :test, :context => :permissions
+end
+
+class AllMockController < MockController
+ filter_access_to :all
+ filter_access_to :view, :require => :test, :context => :permissions
+ action_methods :show, :view
+end
+
+class LoadObjectMockController < MockController
+ filter_access_to :show, :attribute_check => true, :model => LoadMockObject
+ filter_access_to :edit, :attribute_check => true
+ filter_access_to :update, :delete, :attribute_check => true,
+ :load_method => lambda {MockDataObject.new(:test => 1)}
+ filter_access_to :create do
+ authorization_engine.permit!(:edit, :context => :load_mock_objects,
+ :user => current_user)
+ end
+ filter_access_to :view, :attribute_check => true, :load_method => :load_method
+ def self.controller_name
+ "load_mock_object"
+ end
+ def load_method
+ MockDataObject.new(:test => 2)
+ end
+end
+
+class AccessOverwriteController < MockController
+ filter_access_to :test_action, :test_action_2,
+ :require => :test, :context => :permissions_2
+ filter_access_to :test_action, :require => :test, :context => :permissions
+end
+
+
+class ControllerTest < Test::Unit::TestCase
+
+ def test_filter_access
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test
+ has_permission_on :mocks, :to => :show
+ end
+ end
+ }
+ controller = SpecificMockController.new(reader)
+ assert !controller.before_filters.empty?
+
+ controller.request!(MockUser.new(:test_role), "test_action")
+ assert !controller.called_render
+
+ controller.request!(MockUser.new(:test_role), "test_action_2")
+ assert controller.called_render
+
+ controller.request!(MockUser.new(:test_role_2), "test_action")
+ assert controller.called_render
+
+ controller.request!(MockUser.new(:test_role), "show")
+ assert !controller.called_render
+ end
+
+ def test_filter_access_multi_actions
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test
+ end
+ end
+ }
+ controller = SpecificMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "create")
+ assert !controller.called_render
+ end
+
+ def test_filter_access_priv_hierarchy
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ privileges do
+ privilege :read do
+ includes :list, :show
+ end
+ end
+ authorization do
+ role :test_role do
+ has_permission_on :mocks, :to => :read
+ end
+ end
+ }
+ controller = SpecificMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "show")
+ assert !controller.called_render
+ end
+
+ def test_filter_access_skip_attribute_test
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test do
+ if_attribute :id => is { user }
+ end
+ end
+ end
+ }
+ controller = SpecificMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "new")
+ assert !controller.called_render
+
+ controller.request!(MockUser.new(:test_role), "edit_2")
+ assert controller.called_render
+ end
+
+ def test_filter_access_all
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test
+ has_permission_on :mocks, :to => :show
+ end
+ end
+ }
+
+ controller = AllMockController.new(reader)
+
+ controller.request!(MockUser.new(:test_role), "show")
+ assert !controller.called_render
+
+ controller.request!(MockUser.new(:test_role), "view")
+ assert !controller.called_render
+
+ controller.request!(MockUser.new(:test_role_2), "show")
+ assert controller.called_render
+ end
+
+ def test_filter_access_with_object_load
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :load_mock_objects, :to => [:show, :edit] do
+ if_attribute :id => is {1}
+ end
+ end
+ end
+ }
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "show", :id => 2)
+ assert controller.called_render
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "show", :id => 1)
+ assert !controller.called_render
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "edit", :id => 1)
+ assert !controller.called_render
+ assert controller.instance_variable_defined?(:@load_mock_object)
+ end
+
+ def test_filter_access_with_object_load_custom
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :load_mock_objects, :to => :view do
+ if_attribute :test => is {2}
+ end
+ has_permission_on :load_mock_objects, :to => :update do
+ if_attribute :test => is {1}
+ end
+ has_permission_on :load_mock_objects, :to => :delete do
+ if_attribute :test => is {2}
+ end
+ end
+ end
+ }
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "delete")
+ assert controller.called_render
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "view")
+ assert !controller.called_render
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "update")
+ assert !controller.called_render
+ end
+
+ def test_filter_access_custom
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :load_mock_objects, :to => :edit
+ end
+ role :test_role_2 do
+ has_permission_on :load_mock_objects, :to => :create
+ end
+ end
+ }
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role), "create")
+ assert !controller.called_render
+
+ controller = LoadObjectMockController.new(reader)
+ controller.request!(MockUser.new(:test_role_2), "create")
+ assert controller.called_render
+ end
+
+ def test_filter_access_overwrite
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :permissions, :to => :test
+ end
+ end
+ }
+ controller = AccessOverwriteController.new(reader)
+ controller.request!(MockUser.new(:test_role), "test_action_2")
+ assert controller.called_render
+
+ controller.request!(MockUser.new(:test_role), "test_action")
+ assert !controller.called_render
+ end
+end
152 test/dsl_reader_test.rb
@@ -0,0 +1,152 @@
+require File.join(File.dirname(__FILE__), 'test_helper.rb')
+
+class DSLReaderTest < Test::Unit::TestCase
+ def test_privileges
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ privileges do
+ privilege :test_priv do
+ includes :lower_priv
+ end
+ end
+ }
+ assert_equal 2, reader.privileges_reader.privileges.length
+ assert_equal [[:lower_priv, nil]],
+ reader.privileges_reader.privilege_hierarchy[:test_priv]
+ end
+
+ def test_privileges_with_context
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ privileges do
+ privilege :test_priv, :test_context do
+ includes :lower_priv
+ end
+ end
+ }
+ assert_equal [[:lower_priv, :test_context]],
+ reader.privileges_reader.privilege_hierarchy[:test_priv]
+ end
+
+ def test_privileges_one_line
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ privileges do
+ privilege :test_priv, :test_context, :includes => :lower_priv
+ privilege :test_priv_2, :test_context, :includes => [:lower_priv]
+ privilege :test_priv_3, :includes => [:lower_priv]
+ end
+ }
+ assert_equal [[:lower_priv, :test_context]],
+ reader.privileges_reader.privilege_hierarchy[:test_priv]
+ assert_equal [[:lower_priv, :test_context]],
+ reader.privileges_reader.privilege_hierarchy[:test_priv_2]
+ assert_equal [[:lower_priv, nil]],
+ reader.privileges_reader.privilege_hierarchy[:test_priv_3]
+ end
+
+ def test_auth_role
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ includes :lesser_role
+ has_permission_on :items, :to => :read
+ end
+ end
+ }
+ assert_equal 1, reader.auth_rules_reader.roles.length
+ assert_equal [:lesser_role], reader.auth_rules_reader.role_hierarchy[:test_role]
+ assert_equal 1, reader.auth_rules_reader.auth_rules.length
+ end
+
+ def test_auth_role_permit_on
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %|
+ authorization do
+ role :test_role do
+ has_permission_on :test_context do
+ to :test_perm, :manage
+ if_attribute :test_attr => is { user.test_attr }
+ end
+ end
+ end
+ |
+ assert_equal 1, reader.auth_rules_reader.roles.length
+ assert_equal 1, reader.auth_rules_reader.auth_rules.length
+ assert reader.auth_rules_reader.auth_rules[0].matches?(:test_role, [:test_perm], :test_context)
+ assert reader.auth_rules_reader.auth_rules[0].matches?(:test_role, [:manage], :test_context)
+ end
+
+ def test_permit_block
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %|
+ authorization do
+ role :test_role do
+ has_permission_on :perms, :to => :test do
+ if_attribute :test_attr => is { user.test_attr }
+ end
+ end
+ end
+ |
+ assert_equal 1, reader.auth_rules_reader.roles.length
+ assert_equal 1, reader.auth_rules_reader.auth_rules.length
+ assert reader.auth_rules_reader.auth_rules[0].matches?(:test_role, [:test], :perms)
+ end
+
+ def test_has_permission_to_with_context
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %|
+ authorization do
+ role :test_role do
+ has_permission_on :perms, :to => :test
+ end
+ end
+ |
+ assert_equal 1, reader.auth_rules_reader.roles.length
+ assert_equal 1, reader.auth_rules_reader.auth_rules.length
+ assert reader.auth_rules_reader.auth_rules[0].matches?(:test_role, [:test], :perms)
+ end
+
+ def test_context
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ contexts do
+ context :high_level_context do
+ includes :low_level_context_1, :low_level_context_2
+ end
+ end
+ }
+ end
+
+ def test_parameter_error
+ reader = Authorization::Reader::DSLReader.new
+ assert_raise(Authorization::Reader::ParameterError) do
+ reader.parse %{
+ authorization do
+ includes :lesser_role
+ end
+ }
+ end
+ end
+
+ def test_syntax_error
+ reader = Authorization::Reader::DSLReader.new
+ assert_raise(Authorization::Reader::DSLSyntaxError) do
+ reader.parse %{
+ authorizations do
+ end
+ }
+ end
+ end
+
+ def test_syntax_error_2
+ reader = Authorization::Reader::DSLReader.new
+ assert_raise(Authorization::Reader::DSLSyntaxError) do
+ reader.parse %{
+ authorizations
+ end
+ }
+ end
+ end
+end
64 test/helper_test.rb
@@ -0,0 +1,64 @@
+require File.join(File.dirname(__FILE__), 'test_helper.rb')
+require File.dirname(__FILE__) + '/../lib/in_controller.rb'
+require File.dirname(__FILE__) + '/../lib/helper.rb'
+
+MockController.send :include, Authorization::AuthorizationInController
+
+class HelperTest < Test::Unit::TestCase
+ include Authorization::AuthorizationHelper
+ attr_reader :controller
+
+ def test_permit
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :mocks, :to => :show
+ end
+ role :test_role_2 do
+ has_permission_on :mocks, :to => :update
+ end
+ end
+ }
+ user = MockUser.new(:test_role)
+ @controller = MockController.new(reader).request!(user, :action)
+
+ assert permitted_to?(:show, :mocks)
+ assert !permitted_to?(:update, :mocks)
+
+ block_evaled = false
+ permitted_to?(:show, :mocks) do
+ block_evaled = true
+ end
+ assert block_evaled
+
+ block_evaled = false
+ permitted_to?(:update, :mocks) do
+ block_evaled = true
+ end
+ assert !block_evaled
+ end
+
+ def test_permit_with_object
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :mocks do
+ to :show
+ if_attribute :test_attr => is {user.test_attr}
+ end
+ end
+ end
+ }
+ user = MockUser.new(:test_role, :test_attr => 1)
+ mock = MockDataObject.new(:test_attr => 1)
+ mock_2 = MockDataObject.new(:test_attr => 2)
+ @controller = MockController.new(reader).request!(user, :action)
+
+ assert permitted_to?(:show, mock)
+ assert permitted_to?(:show, :mocks)
+ assert !permitted_to?(:show, mock_2)
+ end
+
+end
413 test/model_test.rb
@@ -0,0 +1,413 @@
+require File.join(File.dirname(__FILE__), 'test_helper.rb')
+require File.join(File.dirname(__FILE__), '..', 'lib', 'in_model.rb')
+
+ActiveRecord::Base.send :include, Authorization::AuthorizationInModel
+
+options = {:adapter => 'sqlite3', :timeout => 500, :database => ':memory:'}
+ActiveRecord::Base.establish_connection(options)
+ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
+ActiveRecord::Base.connection
+
+File.read(File.dirname(__FILE__) + "/schema.sql").split(';').each do |sql|
+ ActiveRecord::Base.connection.execute(sql) unless sql.blank?
+end
+
+class TestModel < ActiveRecord::Base
+ has_many :test_attrs
+ has_many :test_attr_throughs, :through => :test_attrs
+ has_many :test_attrs_with_attr, :class_name => "TestAttr", :conditions => {:attr => 1}
+ has_many :test_attr_throughs_with_attr, :through => :test_attrs,
+ :class_name => "TestAttrThrough", :source => :test_attr_throughs,
+ :conditions => "attr = 1"
+end
+
+class TestAttr < ActiveRecord::Base
+ belongs_to :test_model
+ has_many :test_attr_throughs
+ attr_reader :roles
+ def initialize (*args)
+ @roles = []
+ super(*args)
+ end
+end
+
+class TestAttrThrough < ActiveRecord::Base
+ belongs_to :test_attr
+end
+
+class TestModelSecurityModel < ActiveRecord::Base
+ has_many :test_attrs
+ using_access_control
+end
+class TestModelSecurityModelWithFind < ActiveRecord::Base
+ set_table_name "test_model_security_models"
+ has_many :test_attrs
+ using_access_control :include_read => true,
+ :context => :test_model_security_models
+end
+
+class ModelTest < Test::Unit::TestCase
+ def test_named_scope_with_is
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read do
+ if_attribute :id => is { user.test_attr_value }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ TestModel.create!
+
+ user = MockUser.new(:test_role, :test_attr_value => test_model_1.id)
+ assert_equal 1, TestModel.with_permissions_to(:read,
+ :context => :test_models, :user => user).length
+ assert_equal 1, TestModel.with_permissions_to(:read, :user => user).length
+ assert_raise Authorization::NotAuthorized do
+ TestModel.with_permissions_to(:update_test_models, :user => user)
+ end
+ TestModel.delete_all
+ end
+
+ def test_named_scope_with_empty_obligations
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ TestModel.create!
+
+ user = MockUser.new(:test_role)
+ assert_equal 1, TestModel.with_permissions_to(:read, :user => user).length
+ assert_raise Authorization::NotAuthorized do
+ TestModel.with_permissions_to(:update, :user => user)
+ end
+ TestModel.delete_all
+ end
+
+ def test_named_scope_multiple_obligations
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read do
+ if_attribute :id => is { user.test_attr_value }
+ end
+ has_permission_on :test_models, :to => :read do
+ if_attribute :id => is { user.test_attr_value_2 }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ test_model_2 = TestModel.create!
+
+ user = MockUser.new(:test_role, :test_attr_value => test_model_1.id,
+ :test_attr_value_2 => test_model_2.id)
+ assert_equal 2, TestModel.with_permissions_to(:read, :user => user).length
+ TestModel.delete_all
+ end
+
+ def test_named_scope_multiple_attributes
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read do
+ if_attribute :id => is { user.test_attr_value }, :content => "bla"
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create! :content => 'bla'
+ TestModel.create! :content => 'bla'
+ TestModel.create!
+
+ user = MockUser.new(:test_role, :test_attr_value => test_model_1.id)
+ assert_equal 1, TestModel.with_permissions_to(:read, :user => user).length
+ TestModel.delete_all
+ end
+
+ def test_named_scope_with_is_and_priv_hierarchy
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ privileges do
+ privilege :read do
+ includes :list, :show
+ end
+ end
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read do
+ if_attribute :id => is { user.test_attr_value }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ TestModel.create!
+
+ user = MockUser.new(:test_role, :test_attr_value => test_model_1.id)
+ assert_equal 1, TestModel.with_permissions_to(:list,
+ :context => :test_models, :user => user).length
+ assert_equal 1, TestModel.with_permissions_to(:list, :user => user).length
+
+ TestModel.delete_all
+ end
+
+ def test_named_scope_with_is_and_belongs_to
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_attrs, :to => :read do
+ if_attribute :test_model => is { user.test_model }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ test_model_1.test_attrs.create!
+ TestModel.create!.test_attrs.create!
+
+ user = MockUser.new(:test_role, :test_model => test_model_1)
+ assert_equal 1, TestAttr.with_permissions_to(:read,
+ :context => :test_attrs, :user => user).length
+
+ TestModel.delete_all
+ TestAttr.delete_all
+ end
+
+ def test_named_scope_with_deep_attribute
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_attrs, :to => :read do
+ if_attribute :test_model => {:id => is { user.test_model_id } }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ test_model_1.test_attrs.create!
+ TestModel.create!.test_attrs.create!
+
+ user = MockUser.new(:test_role, :test_model_id => test_model_1.id)
+ assert_equal 1, TestAttr.with_permissions_to(:read,
+ :context => :test_attrs, :user => user).length
+
+ TestModel.delete_all
+ TestAttr.delete_all
+ end
+
+ def test_named_scope_with_contains
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read do
+ if_attribute :test_attrs => contains { user }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ test_model_2 = TestModel.create!
+ test_model_1.test_attrs.create!
+ test_model_1.test_attrs.create!
+ test_model_2.test_attrs.create!
+
+ user = MockUser.new(:test_role,
+ :id => test_model_1.test_attrs.first.id)
+ assert_equal 1, TestModel.with_permissions_to(:read, :user => user).length
+ assert_equal 1, TestModel.with_permissions_to(:read, :user => user).find(:all, :conditions => {:id => test_model_1.id}).length
+
+ TestModel.delete_all
+ TestAttr.delete_all
+ end
+
+ def test_named_scope_with_contains_conditions
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read do
+ if_attribute :test_attrs_with_attr => contains { user }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ test_model_2 = TestModel.create!
+ test_model_1.test_attrs_with_attr.create!
+ test_model_1.test_attrs.create!(:attr => 2)
+ test_model_2.test_attrs_with_attr.create!
+ test_model_2.test_attrs.create!(:attr => 2)
+
+ #assert_equal 1, test_model_1.test_attrs_with_attr.length
+ user = MockUser.new(:test_role,
+ :id => test_model_1.test_attrs.first.id)
+ assert_equal 1, TestModel.with_permissions_to(:read, :user => user).length
+ user = MockUser.new(:test_role,
+ :id => test_model_1.test_attrs.last.id)
+ assert_equal 0, TestModel.with_permissions_to(:read, :user => user).length
+
+ TestModel.delete_all
+ TestAttr.delete_all
+ end
+
+ def test_named_scope_with_contains_through_conditions
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role do
+ has_permission_on :test_models, :to => :read do
+ if_attribute :test_attr_throughs_with_attr => contains { user }
+ end
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ test_model_1 = TestModel.create!
+ test_model_2 = TestModel.create!
+ test_model_1.test_attrs.create!(:attr => 1).test_attr_throughs.create!
+ test_model_1.test_attrs.create!(:attr => 2).test_attr_throughs.create!
+ test_model_2.test_attrs.create!(:attr => 1).test_attr_throughs.create!
+ test_model_2.test_attrs.create!(:attr => 2).test_attr_throughs.create!
+
+ #assert_equal 1, test_model_1.test_attrs_with_attr.length
+ user = MockUser.new(:test_role,
+ :id => test_model_1.test_attr_throughs.first.id)
+ assert_equal 1, TestModel.with_permissions_to(:read, :user => user).length
+ user = MockUser.new(:test_role,
+ :id => test_model_1.test_attr_throughs.last.id)
+ assert_equal 0, TestModel.with_permissions_to(:read, :user => user).length
+
+ TestModel.delete_all
+ TestAttr.delete_all
+ end
+
+ def test_model_security
+ reader = Authorization::Reader::DSLReader.new
+ reader.parse %{
+ authorization do
+ role :test_role_unrestricted do
+ has_permission_on :test_model_security_models do
+ to :read, :create, :update, :delete
+ end
+ end
+ role :test_role do
+ has_permission_on :test_model_security_models do
+ to :read, :create, :update, :delete
+ if_attribute :attr => is { 1 }
+ end
+ end
+ role :test_role_restricted do
+ end
+ end
+ }
+ Authorization::Engine.instance(reader)
+
+ $current_user = MockUser.new(:test_role)
+ assert(object = TestModelSecurityModel.create)
+ $current_user = MockUser.new(:test_role_restricted)
+ assert_raise Authorization::NotAuthorized do
+ object.update_attributes(:attr_2 => 2)
+ end
+ $current_user = MockUser.new(:test_role)
+ assert_nothing_raised { object.update_attributes(:attr_2 => 2) }
+ object.reload
+ assert_equal 2, object.attr_2
+ object.destroy
+ assert_raise ActiveRecord::RecordNotFound do
+