Skip to content

Commit

Permalink
Initial import of refactored acl_system
Browse files Browse the repository at this point in the history
git-svn-id: http://opensvn.csie.org/ezra/rails/plugins/dev/acl_system2@75 e8081bd4-020a-0410-a7c4-8039225954a7
  • Loading branch information
loob2 committed Mar 10, 2006
0 parents commit ecc0327
Show file tree
Hide file tree
Showing 8 changed files with 542 additions and 0 deletions.
20 changes: 20 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2006 Ezra Zygmuntowicz & Fabien Franzen

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.
117 changes: 117 additions & 0 deletions README
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
Welcome to the acl_system plugin for rails. This plugin is designed to give you a
flexible declarative way of protecting your various controller actions using roles.
It's made to site on top of any authentication framework that follows a few conventions.
You will need to have a current_user method that returns the currently logged in user.
And you will need to make your User or Account model(or whatever you named it) have a
has_and_belongs_to_many :roles. So you need a model called Role that has a title attribute.
Once these two things are satisfied you can use this plugin.

So lets take a look at the sugar you get from using this plugin. Keep in mind that the
!blacklist part isn’t really necessary here. I was just showing it as an example of how
flexible the permissions string logic parser is.

class PostController < ApplicationController
before_filter :login_required, :except => [:list, :index]
access_control [:new, :create, :update, :edit] => '(admin | user | moderator)',
:delete => 'admin & (!moderator & !blacklist)'

Of course you can define them all seperately if they differ at all.

class PostController < ApplicationController
before_filter :login_required, :except => [:list, :index]
access_control :new => '(admin | user | moderator) & !blacklist',
:create => 'admin & !blacklist',
:edit => '(admin | moderator) & !blacklist',
:update => '(admin | moderator) & !blacklist',
:delete => 'admin & (!moderator | !blacklist)'

And you can also use :DEFAULT if you have a lot of actions that need the same permissions.

class PostController < ApplicationController
before_filter :login_required, :except => [:list, :index]
access_control :DEFAULT => '!guest'
[:new, :create, :update, :edit] => '(admin | user | moderator)',
:delete => 'admin & (!moderator & !blacklist)'

There are two callback methods you can use to define your own success and failure behaviors.
If you define access_allowed and/or access_denied as protected methods in your controller you
can redirect or render and error page or whatever else you might want to do if access is allowed
or denied.

class PostController < ApplicationController
before_filter :login_required, :except => [:list, :index]
access_control :DEFAULT => '!guest'
[:new, :create, :update, :edit] => '(admin | user | moderator)',
:delete => 'admin & (!moderator & !blacklist)'

# the rest of your controller here

protected

def access_denied
flash[:notice] = "You don't have privileges to access this action"
return redirect_to :action => 'denied'
end

def access_allowed
flash[:notice] = "Welcome to the secure area of foo.com!"
end

end

There is also a helper method that can be used in the view or controller. In the view its
handy for conditional menus or stuff like that.

<% if restrict_to "(admin | moderator) & !blacklist" %>
<%= link_to "Admin & Moderator only link", :action =>'foo' %>
<% end %>

So the gist of it is that in the access_control controller macro, you can assign
permission logic strings to actions in your controller. You supply a hash of
:action => ‘permissions string” pairs. Any action not in the list is left open to
any user. Any action with a logic string gets evaluated on each request to see if
the current user has the right role to access the action. The plugin has a small
recursive descent parser that evaluates the permission logic strings against the
current_user.roles.

The way this works is that you have your User model and a Role model. User <= habtm => Role.
So when an action that is access_control’ed gets requested the permission logic string
gets evaluated against the current_user.roles . So a prerequisite of using this plugin
is that you add a Role model with a title attribute that has_and_belongs_to_many User
models. And you need to have a current_user method defined somewhere in your controllers
or user system. Luckily the acts_as_authenticated plugin has the current_user defined already.

So here is the schema of this application including the Post model and the User and Role
model plus the habtm join table:

ActiveRecord::Schema.define(:version => 3) do
create_table "posts", :force => true do |t|
t.column "title", :string, :limit => 40
t.column "body", :text
end
create_table "roles", :force => true do |t|
t.column "title", :string
end
create_table "roles_users", :id => false, :force => true do |t|
t.column "role_id", :integer
t.column "user_id", :integer
end
create_table "users", :force => true do |t|
t.column "login", :string, :limit => 40
t.column "email", :string, :limit => 100
t.column "crypted_password", :string, :limit => 40
t.column "salt", :string, :limit => 40
t.column "created_at", :datetime
t.column "updated_at", :datetime
end
end
And so thats pretty much it for now. You add the roles to the Role.title attribute like admin,
moderator and blacklist like above. These can be anything you want them to be, roles, groups
or whatever. Then you can use as many nested parens and logic with & | ! as you want to
define your complex permissions for accessing your controller. Make sure that your access_control
gets called after the login_required before_filter because we assume that you are already
logged in if you made it this far and then we eval the permissions logic.

You will want to define these access_control in each controller that needs specific permissions.
unless you want to protect the same actions in all controllers, then you can put it in
application.rb but I dont recommend it.
8 changes: 8 additions & 0 deletions init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'caboose/logic_parser'
require 'caboose/role_handler'
require 'caboose/access_control'

#
ActionController::Base.send :include, Caboose
ActionController::Base.send :include, Caboose::AccessControl

109 changes: 109 additions & 0 deletions lib/caboose/access_control.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@

module Caboose

module AccessControl

def self.included(subject)
subject.extend(ClassMethods)
if subject.respond_to? :helper_method
subject.helper_method(:permit?)
subject.helper_method(:restrict_to)
end
end

module ClassMethods
# access_control [:create, :edit] => 'admin & !blacklist',
# :update => '(admin | moderator) & !blacklist',
# :list => '(admin | moderator | user) & !blacklist'
def access_control(actions={})
# Add class-wide permission callback to before_filter
defaults = {}
yield defaults if block_given?
before_filter do |c|
c.default_access_context = defaults
@access = AccessSentry.new(c, actions)
if @access.allowed?(c.action_name)
c.access_allowed if c.respond_to?(:access_allowed)
else
if c.respond_to?(:access_denied)
c.access_denied
else
c.send(:render_text, "You have insuffient permissions")
end
end
end
end
end # ClassMethods

# return the active access handler, fallback to RoleHandler
# implement #retrieve_access_handler to return non-default handler
def access_handler
if respond_to?(:retrieve_access_handler)
@handler ||= retrieve_access_handler
else
@handler ||= RoleHandler.new(Role)
end
end

# the current access context; will be created if not setup
# will add current_user and merge any other elements of context
def access_context(context = {})
default_access_context.merge(context)
end

def default_access_context
@default_access_context ||= {}
@default_access_context[:user] ||= send(:current_user) if respond_to?(:current_user)
@default_access_context
end

def default_access_context=(defaults)
@default_access_context = defaults
end

def permit?(logicstring, context = {})
access_handler.process(logicstring, access_context(context))
end

# restrict_to "admin | moderator" do
# link_to "foo"
# end
def restrict_to(logicstring, context = {})
return false if current_user.nil?
result = ''
if permit?(logicstring, context)
result = yield if block_given?
end
result
end

class AccessSentry

def initialize(subject, actions={})
@actions = actions.inject({}) do |auth, current|
[current.first].flatten.each { |action| auth[action] = current.last }
auth
end
@subject = subject
end

def allowed?(action)
if @actions.has_key? action.to_sym
return @subject.access_handler.process(@actions[action.to_sym].dup, @subject.access_context)
elsif @actions.has_key? :DEFAULT
return @subject.access_handler.process(@actions[:DEFAULT].dup, @subject.access_context)
else
return true
end
end

end # AccessSentry

end # AccessControl

end # Caboose





48 changes: 48 additions & 0 deletions lib/caboose/logic_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module Caboose

module LogicParser
# This module holds our recursive descent parser that take a logic string
# the logic string is tested by the enclosing Handler class' #check method
# Include this module in your Handler class.

# recursively processes an permission string and returns true or false
def process(logicstring, context)
# if logicstring contains any parenthasized patterns, call process recursively on them
while logicstring =~ /\(/
logicstring.sub!(/\(([^\)]+)\)/) {
process($1, context)
}
end

# process each operator in order of precedence
#!
while logicstring =~ /!/
logicstring.sub!(/!([^ &|]+)/) {
(!check(logicstring[$1], context)).to_s
}
end

#&
if logicstring =~ /&/
return (process(logicstring[/^[^&]+/], context) and process(logicstring[/^[^&]+&(.*)$/,1], context))
end

#|
if logicstring =~ /\|/
return (process(logicstring[/^[^\|]+/], context) or process(logicstring[/^[^\|]+\|(.*)$/,1], context))
end

# constants
if logicstring =~ /^\s*true\s*$/i
return true
elsif logicstring =~ /^\s*false\s*$/i
return false
end

# single list items
(check(logicstring.strip, context))
end

end # LogicParser

end
65 changes: 65 additions & 0 deletions lib/caboose/role_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module Caboose

class AccessHandler
include LogicParser

#@acls = {}

#def register_key(key)
# key.downcase!
# if @acls.has_key? key
# raise DuplicateKeyError, "An Key named '#{key}' is already registered"
# end
# @acls[key] = KeyLookup.new(key)
#end

def check(key, context)
false
end

end

# The RoleHandler hold a collection of RoleLookup objects
# one RoleLookup object gets initialized fror every key.title
# returned from Role.find(:all). so your set up a RoleHandler
# and give it the Role model object.
# @handler = RoleHandler.new(Role)
# @handler.process("(admin | moderator) & !blacklisted", @user)
class RoleHandler < AccessHandler

# loads an ActiveRecord Role model and registers all the keys.
def initialize(model)
#@acls = {}
#model.find(:all).each do |role|
# register_key(role.title)
#end
end

def check(key, context)
#return false unless @acls[key]
context[:user].roles.map{ |role| role.title.downcase}.include? key
end

end # End RoleHandler

=begin
# Does a check using an active record lookup on a key model
class KeyLookup
# key gets passed in here.
def initialize(key)
@key = key
end
# Check gets called with context. context has a user model object
# in context[:user]. We collect all the user.keys title attribute
# and see if @key is included in those key.title's
def check(user)
user.roles.map{ |role| role.title.downcase}.include? @key
end
end # END KeyLookup
# raised when two Key's of the same name are registered
class DuplicateKeyError < StandardError
end
=end

end
Loading

0 comments on commit ecc0327

Please sign in to comment.