module Lipsiadmin
module AccessControl
module Helper#:nodoc:
def recognize_path(path)#:nodoc:
case path
when String then ActionController::Routing::Routes.recognize_path(path, :method => :get)
when Hash then path
end
end
end
# This Class map and get roles/projects for accounts
#
# Examples:
#
# roles_for :administrator do |role, current_account|
# role.allow_all_actions "/backend/base"
# role.deny_action_of "/backend/accounts/details"
#
# role.project_module :administration do |project|
# project.menu :general_settings, "/backend/settings" do |submenu|
# submenu.add :accounts, "/backend/accounts" do |submenu|
# submenu.add :sub_accounts, "/backend/accounts/subaccounts"
# end
# end
# end
#
# role.project_module :categories do |project|
# current_account.categories.each do |cat|
# project.menu cat.name, "/backend/categories/#{cat.id}.js"
# end
# end
# end
#
# If a user logged with role administrator or that have a project_module administrator can:
#
# - Access in all actions of "/backend/base" controller
# - Denied access to ONLY action "/backend/accounts/details"
# - Access to a project module called Administration
# - Access to all actions of the controller "/backend/settings"
# - Access to all actions of the controller "/backend/categories"
# - Access to all actions EXCEPT details of controller "/backend/accounts"
#
class Base
@@cache = {}
cattr_accessor :cache
class << self
# We map project modules for a given role or roles
def roles_for(*roles, &block)
roles.each { |role| raise AccessControlError, "Role #{role} must be a symbol!" unless role.is_a?(Symbol) }
@mappers ||= []
@roles ||= []
@roles.concat(roles)
@mappers << Proc.new { |account| Mapper.new(account, *roles, &block) }
end
# Returns all roles
def roles
@roles.nil? ? [] : @roles.collect(&:to_s)
end
# Returns maps (allowed && denied actions) for the given account
def maps_for(account)
@@cache[account.id] ||= @mappers.collect { |m| m.call(account) }.
reject { |m| !m.allowed? }
@@cache[account.id]
end
end
end
class Mapper
include Helper
attr_reader :project_modules, :roles
def initialize(account, *roles, &block)#:nodoc:
@project_modules = []
@allowed = []
@denied = []
@roles = roles
@account_id = account.is_a?(Account) ? account.id : account
# Mantain backward compatibility
yield(self, Account.find(@account_id)) rescue yield(self)
end
# Create a new project module
def project_module(name, controller=nil, &block)
@project_modules << ProjectModule.new(name, controller, &block)
end
# Globally allow an action of a controller for the current role
def allow_action(path)
@allowed << recognize_path(path)
end
# Globally deny an action of a controllerfor the current role
def deny_action(path)
@denied << recognize_path(path)
end
# Globally allow all actions from a controller for the current role
def allow_all_actions(path)
@allowed << { :controller => recognize_path(path)[:controller] }
end
# Globally denty all actions from a controller for the current role
def deny_all_actions(path)
@denied << { :controller => recognize_path(path)[:controller] }
end
# Return true if current_account role is included in given roles
def allowed?
@roles.any? { |r| r.to_s.downcase == Account.find(@account_id).role.downcase }
end
# Return allowed actions/controllers
def allowed
# I know is a double check but is better 2 times that no one.
if allowed?
@project_modules.each { |pm| @allowed.concat pm.allowed }
@allowed.uniq
else
[]
end
end
# Return denied actions/controllers
def denied
@denied.uniq
end
end
class ProjectModule
include Helper
include ActionController::UrlWriter
attr_reader :name, :menus, :url
def initialize(name, path=nil, options={}, &block)#:nodoc:
@name = name
@options = options
@allowed = []
@menus = []
if path
@url = recognize_path(path)
@allowed << { :controller => @url[:controller] }
end
yield self
end
# Build a new menu and automaitcally add the action on the allowed actions.
def menu(name, path=nil, options={}, &block)
@menus << Menu.new(name, path, options, &block)
end
# Return allowed controllers
def allowed
@menus.each { |m| @allowed.concat(m.allowed) }
@allowed.uniq
end
# Return the original name or try to translate or humanize the symbol
def human_name
@name.is_a?(Symbol) ? I18n.t("backend.menus.#{@name}", :default => @name.to_s.humanize) : @name
end
# Return a unique id for the given project module
def uid
@name.to_s.downcase.gsub(/[^a-z0-9]+/, '').gsub(/-+$/, '').gsub(/^-+$/, '')
end
# Return ExtJs Config for this project module
def config
options = @options.merge(:text => human_name)
options.merge!(:menu => @menus.collect(&:config)) if @menus.size > 0
options.merge!(:handler => "function(){ Backend.app.load('#{url_for(@url.merge(:only_path => true))}') }".to_l) if @url
options
end
end
class Menu
include Helper
include ActionController::UrlWriter
attr_reader :name, :options, :items
def initialize(name, path=nil, options={}, &block)#:nodoc:
@name = name
@url = path
@options = options
@allowed = []
@items = []
@allowed << { :controller => recognize_path(path)[:controller] } if @url
yield self if block_given?
end
# Return the url of this menu
def url
@url.is_a?(Hash) ? url_for(@url.merge(:only_path => true)) : @url
end
# Add a new submenu to the menu
def add(name, path=nil, options={}, &block)
@items << Menu.new(name, path, options, &block)
end
# Return allowed controllers
def allowed
@items.each { |i| @allowed.concat i.allowed }
@allowed.uniq
end
# Return the original name or try to translate or humanize the symbol
def human_name
@name.is_a?(Symbol) ? I18n.t("backend.menus.#{@name}", :default => @name.to_s.humanize) : @name
end
# Return a unique id for the given project module
def uid
@name.to_s.downcase.gsub(/[^a-z0-9]+/, '').gsub(/-+$/, '').gsub(/^-+$/, '')
end
# Return ExtJs Config for this menu
def config
if @url.blank? && @items.empty?
options = human_name
else
options = @options.merge(:text => human_name)
options.merge!(:menu => @items.collect(&:config)) if @items.size > 0
options.merge!(:handler => "function(){ Backend.app.load('#{url}') }".to_l) if !@url.blank?
end
options
end
end
class AccessControlError < StandardError#:nodoc:
end
end
end