# Module, welches AccesRules fuer Controller/Actions und
# Model-Object umsetzt.
#
# Die Regeln werden aus der Datei "config/acces_rules.rb" geladen
#
# Author: Bernd Ledig
#
require 'singleton'
module Tuersteher
# Logger to log messages with timestamp and severity
class TLogger < Logger
@@logger = nil
def format_message(severity, timestamp, progname, msg)
"#{timestamp.to_formatted_s(:db)} #{severity} #{msg}\n"
end
def self.logger
return @@logger if @@logger
@@logger = self.new(File.join(Rails.root, 'log', 'tuersteher.log'), 3)
@@logger.level = INFO if Rails.env != 'development'
@@logger
end
def self.logger= logger
@@logger = logger
end
end
class AccessRulesStorage
include Singleton
attr_accessor :rules_config_file # to set own access_rules-path
DEFAULT_RULES_CONFIG_FILE = File.join(Rails.root, 'config', 'access_rules.rb')
def initialize
@path_rules = []
@model_rules = []
end
def path_rules
read_rules unless @was_read
@path_rules
end
def model_rules
read_rules unless @was_read
@model_rules
end
# Laden der AccesRules aus den Dateien
# config/access_rules.rb
def read_rules
config_file = @rules_config_file || DEFAULT_RULES_CONFIG_FILE
rules_file = File.new config_file
if @last_mtime.nil? || rules_file.mtime > @last_mtime
@last_mtime = rules_file.mtime
content = rules_file.read
eval content
Tuersteher::TLogger.logger.info "Tuersteher::AccessRulesStorage: #{@path_rules.size} path-rules and #{@model_rules.size} model-rules"
end
rules_file.close
@was_read = true
end
# definiert HTTP-Pfad-basierende Zugriffsregel
#
# path: :all fuer beliebig, sonst String mit der http-path beginnen muss,
# wird als RegEX-Ausdruck ausgewertet
# method: http-Methode, es sind hier erlaubt :get, :put, :delete, :post, :all
# accepted_roles: Aufzaehlung der erfoderlichen Rolen (oder-Verknuepfung), es sind nur Symbole zulaessig
# hier ist auch ein Array von Symbolen möglich
def grant_path url_path, http_methode, *accepted_roles
@path_rules << PathAccessRule.new(url_path, http_methode, *accepted_roles)
end
# definiert HTTP-Pfad-basierende Ablehnungsregel
#
# path: :all fuer beliebig, sonst String mit der http-path beginnen muss,
# wird als RegEX-Ausdruck ausgewertet
# method: http-Methode, es sind hier erlaubt :get, :put, :delete, :post, :all
# accepted_roles: Aufzaehlung der erfoderlichen Rolen (oder-Verknuepfung), es sind nur Symbole zulaessig
# hier ist auch ein Array von Symbolen möglich
def deny_path url_path, http_methode, *accepted_roles
rule = PathAccessRule.new(url_path, http_methode, *accepted_roles)
rule.deny = true
@path_rules << rule
end
# definiert Model-basierende Zugriffsregel
#
# model_class: Model-Klassenname oder :all fuer alle
# access_type: Zugriffsart (:create, :update, :destroy, :all o.A. selbst definierte Typen)
# roles Aufzählung der erforderliche Rolen (:all für ist egal),
# hier ist auch ein Array von Symbolen möglich
# block optionaler Block, wird mit model und user aufgerufen und muss true oder false liefern
# hier ein Beispiel mit Block:
#
# # Regel, in der sich jeder User selbst aendern darf
# grant_model(User, :update, :all){|model,user| model.id==user.id}
#
#
def grant_model model_class, access_type, *roles, &block
@model_rules << ModelAccessRule.new(model_class, access_type, *roles, &block)
end
end
class AccessRules
# Pruefen Zugriff fuer eine Web-action
# user User, für den der Zugriff geprüft werden soll (muss Methode has_role? haben)
# path Pfad der Webresource (String)
# method http-Methode (:get, :put, :delete, :post), default ist :get
#
def self.path_access?(user, path, method = :get)
rule = AccessRulesStorage.instance.path_rules.detect do |r|
r.fired?(path, method, user)
end
if Tuersteher::TLogger.logger.debug?
if rule.nil?
s = 'denied'
elsif rule.deny
s = "denied with #{rule}"
else
s = "granted with #{rule}"
end
Tuersteher::TLogger.logger.debug("Tuersteher: path_access?(#{path}, #{method}) => #{s}")
end
rule!=nil && !rule.deny
end
# Pruefen Zugriff auf ein Model-Object
#
# user User, für den der Zugriff geprüft werden soll (muss Methode has_role? haben)
# model das Model-Object
# permission das geforderte Zugriffsrecht (:create, :update, :destroy, :get)
#
# liefert true/false
def self.model_access? user, model, permission
raise "Wrong call! Use: model_access(model-instance-or-class, permission)" unless permission.is_a? Symbol
return false unless model
access = AccessRulesStorage.instance.model_rules.detect do |rule|
rule.has_access? model, permission, user
end
if Tuersteher::TLogger.logger.debug?
if model.instance_of?(Class)
Tuersteher::TLogger.logger.debug("Tuersteher: model_access?(#{model}, #{permission}) => #{access ? access : 'denied'}")
else
Tuersteher::TLogger.logger.debug("Tuersteher: model_access?(#{model.class}(#{model.respond_to?(:id) ? model.id : model.object_id }), #{permission}) => #{access ? access : 'denied'}")
end
end
access!=nil
end
end
# Module zum Include in Controllers
# Dieser muss die folgenden Methoden bereitstellen:
#
# current_user : akt. Login-User
# access_denied : Methode aus dem authenticated_system, welche ein redirect zum login auslöst
#
# Der Loginuser muss fuer die hier benoetigte Funktionalitaet
# die Methode:
# has_role?(*roles) # roles is Array of Symbols
# besitzen.
#
# Beispiel der Einbindung in den ApplicationController
# include Tuersteher::ControllerExtensions
# before_filter :check_access # methode is from Tuersteher::ControllerExtensions
#
module ControllerExtensions
# Pruefen Zugriff fuer eine Web-action
#
# path Pfad der Webresource (String oder Hash mit Options)
# method http-Methode (:get, :put, :delete, :post), default ist :get
#
def path_access?(path, method = :get)
# ist path eine Hash (also der alte Stil mit :controller=> .., :action=>..)
# dann diese in ein http-path wandeln
path = url_for(path.merge(:only_path => true)) if path.instance_of?(Hash)
AccessRules.path_access? current_user, path, method
end
# Pruefen Zugriff auf ein Model-Object
#
# model das Model-Object
# permission das geforderte Zugriffsrecht (:create, :update, :destroy, :get)
#
# liefert true/false
def model_access? model, permission
AccessRules.model_access? current_user, model, permission
end
def self.included(base)
base.class_eval do
# Methoden path_access? und model_access? auch als Helper fuer die Views bereitstellen
helper_method :path_access?, :model_access?
end
end
protected
# Pruefen, ob Zugriff des current_user
# fuer aktullen Request erlaubt ist
def check_access
# im dev-mode rules bei jeden request auf Änderungen prüfen
AccessRulesStorage.instance.read_rules if Rails.env=='development'
unless path_access?(request.request_uri, request.method)
msg = "Tuersteher#check_access: access denied for #{request.request_uri} :#{request.method}"
Tuersteher::TLogger.logger.warn msg
logger.warn msg # log message also for Rails-Default logger
access_denied # Methode aus dem authenticated_system, welche ein redirect zum login auslöst
end
end
end
class PathAccessRule
attr_reader :path, :method, :roles
attr_accessor :deny
METHOD_NAMES = [:get, :edit, :put, :delete, :post, :all].freeze
# Zugriffsregel
#
# path :all fuer beliebig, sonst String mit der http-path beginnen muss
# method http-Methode, es sind hier erlaubt :get, :put, :delete, :post, :all
# needed_roles Aufzaehlung der erfoderlichen Rolen (oder-Verknuepfung), es sind nur Symbole zulaessig
#
def initialize(path, method, *needed_roles)
raise "wrong path '#{path}'! Must be a String or :all ." unless path==:all or path.is_a?(String)
raise "wrong method '#{method}'! Must be #{METHOD_NAMES.join(', ')} !" unless METHOD_NAMES.include?(method)
raise "needed_roles expected!" if needed_roles.empty?
@roles = needed_roles.flatten
for r in @roles
raise "wrong role '#{r}'! Must be a symbol " unless r.is_a?(Symbol)
end
@path = path
if path != :all
# path in regex ^#{path} wandeln ausser bei "/",
# dies darf keine Regex mit ^/ werden,
# da diese ja immer matchen wuerde
if path == "/"
@path = /^\/$/
else
@path = /^#{path}/
end
end
@method = method
end
# pruefen, ob Zugriff fuer angegebenen
# path / method fuer den current_user erlaubt ist
#
# user ist ein Object (meist der Loginuser),
# welcher die Methode 'has_role?(*roles)' besitzen muss.
# *roles ist dabei eine Array aus Symbolen
#
def fired?(path, method, user)
user = nil if user==:false # manche Authenticate-System setzen den user auf :false
if @path!=:all && !(@path =~ path)
return false
end
if @method!=:all && @method != method
return false
end
# ist jetzt role :all, dann prinzipiell Zugriff erlaubt
return true if @roles.first == :all
if user && user.has_role?(*@roles)
return true
end
false
end
def to_s
"PathAccesRule[#{@path}, #{@method}, #{@roles.join(' ')}#{@deny ? ' deny' : ''}]"
end
end
class ModelAccessRule
attr_reader :clazz, :access_type, :role, :block
# erzeugt neue Object-Zugriffsregel
#
# clazz Model-Klassenname oder :all fuer alle
# access_type Zugriffsart (:create, :update, :destroy, :all o.A. selbst definierte Typem)
# roles Aufzählung der erforderliche Rolen (:all für ist egal),
# hier ist auch ein Array von Symbolen möglich
# block optionaler Block, wird mit model und user aufgerufen und muss true oder false liefern
# hier ein Beispiel mit Block:
#
# # Regel, in der sich jeder User selbst aendern darf
# ModelAccessRule.new(User, :update, :all){|model,user| model.id==user.id}
#
#
def initialize(clazz, access_type, *roles, &block)
raise "wrong clazz '#{clazz}'! Must be a Class or :all ." unless clazz==:all or clazz.is_a?(Class)
raise "wrong access_type '#{ access_type}'! Must be a Symbol ." unless access_type.is_a?(Symbol)
@roles = roles.flatten
for r in @roles
raise "wrong role '#{r}'! Must be a symbol " unless r.is_a?(Symbol)
end
@clazz = clazz.instance_of?(Symbol) ? clazz : clazz.to_s
@access_type = access_type
@block = block
end
# liefert true, wenn zugriff fuer das angegebene model mit
# der Zugriffsart perm für das security_object hat
#
# model des zupruefende ModelObject
# perm gewunschte Zugriffsart (Symbol :create, :update, :destroy)
#
# user ist ein User-Object (meist der Loginuser),
# welcher die Methode 'has_role?(*roles)' besitzen muss.
# *roles ist dabei eine Array aus Symbolen
#
#
def has_access? model, perm, user
user = nil if user==:false # manche Authenticate-System setzen den user auf :false
m_class = model.instance_of?(Class) ? model : model.class
if @clazz!=m_class.to_s && @clazz!=:all
#Tuersteher::TLogger.logger.debug("#{to_s}.has_access? => false why #{@clazz}!=#{model.class.to_s} && #{@clazz}!=:all")
return false
end
if @access_type!=:all && @access_type!=perm
#Tuersteher::TLogger.logger.debug("#{to_s}.has_access? => false why #{@access_type}!=:all && #{@access_type}!=#{perm}")
return false
end
if @roles.first!=:all && (user.nil? || !user.has_role?(*@roles))
#Tuersteher::TLogger.logger.debug("#{to_s}.has_access? => false why #{@roles.first}!=:all && #{!user.has_role?(*@roles)}")
return false
end
if @block
unless @block.call(model, user)
#Tuersteher::TLogger.logger.debug("#{to_s}.has_access? => false why block return false")
return false
end
end
true
end
def to_s
"ModelAccessRule[#{@clazz}, #{@access_type}, #{@roles.join(' ')}]"
end
end
# ActiveRecord erweitern mit
# Sicherheits-Check
#
# class ActiveRecord::Base
# before_create {|model| SecurityModule::SecurityService.check_model_access model, :create }
# before_update {|model| SecurityModule::SecurityService.check_model_access model, :update }
# before_destroy{|model| SecurityModule::SecurityService.check_model_access model, :destroy }
# end
end