# Copyright (c) 2012 Bingo Entreprenøren AS
# Copyright (c) 2012 Teknobingo Scandinavia AS
# Copyright (c) 2012 Knut I. Stenmark
# Copyright (c) 2012 Patrick Hanevold
#
# 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.
module Trust
class Permissions
# = Trust::Permissions
# Permissions should be specified in a separate file in you app/model directory. The file could look like this:
#
# module Permissions
# class Default < Trust::Permissions
# ...
# end
# ...
# end
#
# The above is the minimum required definitions that must exist in you file. Default will be used if no classes
# match the permissions requested, so the Default class definition is mandatory.
#
# If you want to separate the permissions into separate files that is ok. Then you shoud place these files in the
# /app/model/permissions directory.
#
# === Defining permisions
#
# The basic rules is to define classes in the Permissions module that matches your models.
# Here are some examples:
# * Project should have a matching class Permissions::Project
# * Account should have a matching class Permissions::Account
# * Account:Credit may have a matching class Permissions::Account::Credit, but if its inheriting from
# Account and no special handling is necessary, it is not necessary to create the permissions class.
#
# === Using inheritance
#
# Inheritance is also fully supported, but should generally follow your own inheritance model
#
# module Permissions
# class Account < Default
# role :admin, :accountant do
# ...
# end
# end
# class Account::Credit < Account
# ...
# end
# end
#
# === Action aliases
#
# You can define aliases for actions. You do this by setting the action_aliases attribute on Trust::Permissions class
# Example:
# Trust::Permissions.action_aliases = {
# read: [:index, :show],
# create: [:create, :new]
# }
#
# Keep in mind that all permissions are expanded upon declaration, so when using the can? method you must refer to
# the actual action and not the alias. The alias will never give a positive permission.
#
# === Accessors
#
# Accessors that can be used when testing permissions:
# * user - the user currently logged in
# * action - the action request from the controller such as :edit, or the action tested from helper or
# from the object itself when using ActiveRecord::can? is being used.
# * subject - the object that is being tested for permissions. This may be a an existing object, a new object
# (such as for +:create+ and +:new+ action), or nil if no object has been instantiated.
# * parent - the parent object if in a nested route, specified by +belongs_to+ in the controller.
# * klass - the class of involed in the request. It can be a base class or the real class, depending on
# your controller design.
#
# === Defining your own accessors or instance methods
#
# You can easily define your own accessors in the classes. These can be helpful when declaring permissions.
# Example:
#
# class Account < Trust::Permissions
# role :admin, :accountant do
# can :update, :unless => :closed?
# end
# def closed?
# subject.closed?
# end
# end
#
# In the above example closed is testing on the subject to see if it is closed. The permission is referring to
# this method when evaluated.
# Keep in mind that you must refer to the +subject+, as you do not access the inctance of the object directly.
#
include InheritableAttribute
attr_reader :user, :action, :klass, :subject, :parent
inheritable_attr :permissions
class_attribute :action_aliases, :instance_writer => false, :instance_reader => false
self.permissions = {}
self.action_aliases = {
read: [:index, :show],
create: [:create, :new],
update: [:update, :edit],
manage: [:index, :show, :create, :new, :update, :edit, :destroy]
}
@@can_expressions = 0
# initializes the permission object
# calling the +authorized?+ method on the instance later will test for the authorization.
# Parameters:
# user - user object, must respond to role_symbols
# action - action, such as :create, :show, etc. Should not be an alias
# klass - the class of the subject.
# subject - the subject tested for authorization
# parent - the parent object, normally declared through belongs_to
#
# See Trust::Authorization for more details
def initialize(user, action, klass, subject, parent)
@user, @action, @klass, @subject, @parent = user, action, klass, subject, parent
end
# returns true if the user is authorized to perform the action
def authorized?
authorized = nil
user && user.role_symbols.each do |role|
(permissions[role] || {}).each do |act, opt|
if act == action
break if (authorized = opt.any? ? eval_expr(opt) : true)
end
end
break if authorized
end
authorized
end
protected
def eval_expr(options) #:nodoc:
options.collect do |oper, expr|
res = case expr
when Symbol then send(expr)
when Proc
if expr.lambda?
instance_exec &expr
else
instance_eval &expr
end
else
expr
end
case oper
when :if then res
when :unless then !res
else
raise UnsupportedCondition, expr.inspect
end
end.all?
end
class << self
# Assign permissions to one or more roles.
# You may call role or roles, they are the same function like role :admin or roles :admin, :accountant
#
# There are two ways to call role, with or without block. If you want to set multiple permissions with different conditons
# then you should use a block.
#
# module Permissions
# class Account < Trust::Permissions
# role :admin, can(:manage, :audit)
# end
# end
#
# The above assigns the manage and audit permissions to admin.
#
# module Permissions
# class Account < Trust::Permissions
# role :admin, :accountant do
# can :read
# can :update
# end
# end
# end
#
# The above permits admin and accountant to read accounts.
def role(*roles, &block)
if block_given?
if @@can_expressions > 0
@@can_expressions = 0
raise RoleAssigmnentMissing
end
@perms = []
@in_role_block = true
yield
@in_role_block = false
perms = @perms
else
if @@can_expressions > 1
@@can_expressions = 0
raise RoleAssigmnentMissing
end
options = roles.extract_options!
raise ArgumentError, "Must have a block or a can expression" unless perms = options[:can]
@@can_expressions = 0
end
roles.flatten.each do |role|
self.permissions[role] ||= []
self.permissions[role] += perms
end
end
alias :roles :role
# Defines permissions
# action - can be an alias or an actions of some kind
# options - :if/:unless :symbol or proc that will be called to evaluate an expression
#
# module Permissions
# class Account < Trust::Permissions
# role :admin, :accountant do
# can :read
# can :update, :unless => :closed?
# end
# end
# end
#
# The above permits admin and accountant to read accounts, but can update only if the account is not closed.
# In the example above a method is used to test data on the actual record when testing for permissions.
def can(*args)
options = args.extract_options!
p = expand_aliases(args).collect { |action| [action, options] }
if @in_role_block
@perms += p
else
@@can_expressions += 1
return {:can => p }
end
end
private
def expand_aliases(actions) #:nodoc:
expanded = []
Array.wrap(actions).each do |action|
if self.action_aliases[action]
expanded += Array.wrap(self.action_aliases[action])
else
expanded << action
end
end
expanded
end
end
end
end