Permalink
Fetching contributors…
Cannot retrieve contributors at this time
198 lines (181 sloc) 6.53 KB
module Padrino
module Admin
class AccessControlError < StandardError
end
##
# This module enables access control functionality within a Padrino application.
#
module AccessControl
class << self
##
# Method used by Padrino::Application when we register the extension.
#
def registered(app)
app.register Padrino::Admin unless app.extensions.include?(Padrino::Admin)
app.set :session_id, "_padrino_#{Padrino.env}_#{app.app_name}" unless app.respond_to?(:session_id)
app.set :admin_model, 'Account' unless app.respond_to?(:admin_model)
app.helpers Padrino::Admin::Helpers::AuthenticationHelpers
app.helpers Padrino::Admin::Helpers::ViewHelpers
app.before { login_required }
app.class_eval do
class << self
attr_accessor :access_control
end
def access_control
self.class.access_control
end
end
app.send(:access_control=, Padrino::Admin::AccessControl::Base.new)
end
alias :included :registered
end
##
# This base access control class where roles are defined as are authorizations.
#
class Base
def initialize
@roles, @authorizations, @project_modules = [], [], []
end
##
# We map project modules for a given role or roles.
#
def roles_for(*roles, &block)
raise Padrino::Admin::AccessControlError, "Role #{role} must be present and must be a symbol!" if roles.any? { |r| !r.kind_of?(Symbol) } || roles.empty?
raise Padrino::Admin::AccessControlError, "You can't merge :any with other roles" if roles.size > 1 && roles.any? { |r| r == :any }
@roles += roles
@authorizations << Authorization.new(*roles, &block)
end
##
# Return an array of roles.
#
def roles
@roles.uniq.reject { |r| r == :any }
end
##
# Return an array of project_modules.
#
def project_modules(account)
role = account.role.to_sym rescue :any
authorizations = @authorizations.find_all { |auth| auth.roles.include?(role) }
authorizations.flat_map(&:project_modules).uniq
end
##
# Return true if the given account is allowed to see the given path.
#
# @example Hiding a disallowed link from a user.
#
# # File: config/apps.rb
# # [...]
# Padrino.mount('Admin').to('/admin')
#
# # File: admin/app.rb
# class Admin < Padrino::Application
# # [...]
# register Padrino::Admin::AccessControl
# # [...]
#
# # Goals:
# # * Admins can manage widgets and accounts.
# # * Workers can only manage widgets.
#
# access_control.roles_for :admin do |role|
# role.project_module :accounts, '/accounts'
# role.project_module :widgets, '/widgets'
# end
#
# access_control.roles_for :worker do |role|
# role.project_module :widgets, '/widgets'
# end
# end
#
# # File: admin/views/layouts/application.haml
# # NOTE The un-mounted path is used ('/accounts' instead of '/admin/accounts')
# - if access_control.allowed?(current_account, '/accounts')
# # Admins see the "Profile" link, but Workers do not
# = link_to 'Profile', url(:accounts, :edit, :id => current_account.id)
#
def allowed?(account=nil, path=nil)
path = "/" if path.nil? || path.empty?
role = account.role.to_sym rescue nil
authorizations = @authorizations.find_all { |auth| auth.roles.include?(:any) }
allowed_paths = authorizations.map(&:allowed).flatten.uniq
denied_paths = authorizations.map(&:denied).flatten.uniq
if account
denied_paths.clear
# explicit authorizations for the role associated with the given account
authorizations = @authorizations.find_all { |auth| auth.roles.include?(role) }
allowed_paths += authorizations.map(&:allowed).flatten.uniq
# other explicit authorizations
authorizations = @authorizations.find_all { |auth| !auth.roles.include?(role) && !auth.roles.include?(:any) }
denied_paths += authorizations.map(&:allowed).flatten.uniq # remove paths explicitly allowed for other roles
denied_paths += authorizations.map(&:denied).flatten.uniq # remove paths explicitly denied to other roles
end
return true if allowed_paths.any? { |p| path =~ /^#{p}/ }
return false if denied_paths.any? { |p| path =~ /^#{p}/ }
true
end
end
###
# Project Authorization Class.
#
class Authorization
attr_reader :allowed, :denied, :project_modules, :roles
def initialize(*roles, &block)
@roles = roles
@allowed = []
@denied = []
@project_modules = []
yield self
end
##
# Allow a specified path.
#
def allow(path)
@allowed << path unless @allowed.include?(path)
end
##
# Protect access from.
#
def protect(path)
@denied << path unless @denied.include?(path)
end
##
# Create a project module.
#
def project_module(name, path, options={})
allow(path)
@project_modules << ProjectModule.new(name, path, options)
end
end
##
# Project Module class.
#
class ProjectModule
attr_reader :name, :options
def initialize(name, path, options={})
@name, @path, @options = name, path, options
end
##
# Returns the name of the project module humanize them for you.
#
def human_name
@name.to_s.humanize
end
##
# Return the path of the project module. If a prefix given will be pre pended.
#
# @example
# # => /accounts/new
# project_module.path
# # => /admin/accounts
# project_module.path("/admin")
#
def path(prefix=nil)
path = prefix ? File.join(prefix, @path) : @path
path = File.join(ENV['RACK_BASE_URI'].to_s, path) if ENV['RACK_BASE_URI']
path
end
end
end
end
end