acts as permissible

This commit is contained in:
Espen Antonsen 2009-06-05 14:18:03 +02:00
parent a67066ea05
commit 553899d9eb
19 changed files with 752 additions and 10 deletions

3
.gitignore vendored
View file

@ -5,3 +5,6 @@ doc/api
doc/app
public/files
public/thumbs
config/deploy.rb
config/deploy
Capfile

19
app/models/permission.rb Normal file
View file

@ -0,0 +1,19 @@
class Permission < ActiveRecord::Base
# uncomment any of the following lines which is relevant to your application,
# or create your own with the name of the model which acts_as_permissible.
belongs_to :user
belongs_to :role
belongs_to :permissible, :polymorphic => true, :dependent => :destroy
validates_presence_of :permissible_id, :permissible_type, :action
validates_format_of :action, :with => /^[a-z_]+$/
validates_numericality_of :permissible_id
validates_uniqueness_of :action, :scope => [:permissible_id,:permissible_type]
def to_hash
self.new_record? ? {} : {self.action => self.granted}
end
end

12
app/models/role.rb Normal file
View file

@ -0,0 +1,12 @@
class Role < ActiveRecord::Base
has_many :role_memberships, :as => :roleable, :dependent => :destroy
has_many :roles, :through => :role_memberships, :source => :role
has_many :roleables, :class_name => "RoleMembership", :foreign_key => "role_id", :dependent => :destroy
has_many :subroles, :through => :roleables, :source => :roleable, :source_type => 'Role'
has_many :users, :through => :roleables, :source => :roleable, :source_type => 'User'
validates_uniqueness_of :name
acts_as_permissible
end

View file

@ -0,0 +1,36 @@
class RoleMembership < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :roleable, :polymorphic => true
validates_presence_of :roleable_id, :roleable_type, :role_id
validates_uniqueness_of :role_id, :scope => [:roleable_id, :roleable_type]
validates_numericality_of :roleable_id, :role_id
validates_format_of :roleable_type, :with => /^[A-Z]{1}[a-z0-9]+([A-Z]{1}[a-z0-9]+)*$/
validate :role_does_not_belong_to_itself_in_a_loop
protected
def role_does_not_belong_to_itself_in_a_loop
if roleable_type == "Role"
if role_id == roleable_id
errors.add_to_base("A role cannot belong to itself.")
else
if belongs_to_itself_through_other?(roleable_id, role_id)
errors.add_to_base("A role cannot belong to a role which belongs to it.")
end
end
end
end
def belongs_to_itself_through_other?(original_roleable_id, current_role_id)
if self.class.find(:first, :select => "id", :conditions => ["roleable_id=? AND roleable_type='Role' AND role_id=?",current_role_id,original_roleable_id])
return true
else
memberships = self.class.find(:all, :select => "role_id", :conditions => ["roleable_id=? AND roleable_type='Role'",current_role_id])
if memberships.any? {|membership| belongs_to_itself_through_other?(original_roleable_id,membership.role_id)}
return true
end
end
return false
end
end

View file

@ -1,3 +1,4 @@
class User < ActiveRecord::Base
acts_as_authentic
acts_as_permissible
end

View file

@ -1,6 +1,11 @@
<h1><%= @album.title %></h1>
<% for photo in @album.photos %>
<%= link_to image_tag( photo.path_modified_public("album") ), photo %>
<% end %>
<p><%= @album.description %></p>
<% if current_user && current_user.has_permission?("see_album_note") %>
<p><%= @album.note %></p>
<% end %>
<br /><%= link_to "Update album", edit_album_path(@album) %>
<br /><%= link_to "Upload photos", upload_album_path(@album) %>

View file

@ -11,7 +11,8 @@ Rails::Initializer.run do |config|
config.gem "authlogic"
config.gem 'mime-types', :lib => 'mime/types'
#config.gem "image_science"
config.gem "image_science"
config.gem "mini_exiftool"
config.load_paths += %W( #{RAILS_ROOT}/app/middleware )
@ -19,12 +20,4 @@ Rails::Initializer.run do |config|
config.i18n.default_locale = 'no-NB'
config.action_mailer.smtp_settings = {
:address => "smtp.gmail.com",
:port => 587,
:domain => "espen@inspired.no",
:authentication => :plain,
:user_name => "espen@inspired.no",
:password => "tkg5megmeg"
}
end

View file

@ -0,0 +1,16 @@
class CreatePermissions < ActiveRecord::Migration
def self.up
create_table "permissions", :force => true do |t|
t.integer :permissible_id
t.string :permissible_type
t.string :action
t.boolean :granted
t.timestamps
end
end
def self.down
drop_table "permissions"
end
end

View file

@ -0,0 +1,15 @@
class CreateRoleMemberships < ActiveRecord::Migration
def self.up
create_table :role_memberships do |t|
t.integer :roleable_id
t.string :roleable_type
t.integer :role_id
t.timestamps
end
end
def self.down
drop_table :role_memberships
end
end

View file

@ -0,0 +1,13 @@
class CreateRoles < ActiveRecord::Migration
def self.up
create_table :roles do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :roles
end
end

View file

@ -9,7 +9,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20090602131547) do
ActiveRecord::Schema.define(:version => 20090604202930) do
create_table "albums", :force => true do |t|
t.string "title", :null => false
@ -37,6 +37,15 @@ ActiveRecord::Schema.define(:version => 20090602131547) do
t.datetime "updated_at"
end
create_table "permissions", :force => true do |t|
t.integer "permissible_id"
t.string "permissible_type"
t.string "action"
t.boolean "granted"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "photo_tags", :force => true do |t|
t.integer "tag_id"
t.integer "photo_id"
@ -55,6 +64,20 @@ ActiveRecord::Schema.define(:version => 20090602131547) do
t.float "latitude"
end
create_table "role_memberships", :force => true do |t|
t.integer "roleable_id"
t.string "roleable_type"
t.integer "role_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "roles", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "tags", :force => true do |t|
t.string "title", :null => false
t.datetime "created_at"

106
lib/acts_as_permissible.rb Normal file
View file

@ -0,0 +1,106 @@
# ActsAsPermissible
module NoamBenAri
module Acts #:nodoc:
module Permissible #:nodoc:
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_permissible
has_many :permissions, :as => :permissible, :dependent => :destroy
has_many :role_memberships, :as => :roleable, :dependent => :destroy
has_many :roles, :through => :role_memberships, :source => :role
include NoamBenAri::Acts::Permissible::InstanceMethods
extend NoamBenAri::Acts::Permissible::SingletonMethods
alias_method :full_permissions_hash, :permissions_hash
end
end
# This module contains class methods
module SingletonMethods
# Helper method to lookup for permissions for a given object.
# This method is equivalent to obj.permissions.
def find_permissions_for(obj)
permissible = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
Permission.find(:all,
:conditions => ["permissible_id = ? and permissible_type = ?", obj.id, permissible]
)
end
end
# This module contains instance methods
module InstanceMethods
# returns permissions in hash form
# from all levels recursively
def permissions_hash
@permissions_hash ||= lambda do
@permissions_hash = permissions.inject({}) { |hsh,perm| hsh.merge(perm.to_hash) }.symbolize_keys!
roles.each do |role|
merge_permissions!(role.permissions_hash)
end
@permissions_hash
end.call()
end
# accepts a permission identifier string or an array of permission identifier strings
# and return true if the user has all of the permissions given by the parameters
# false if not.
def has_permission?(*perms)
perms.all? {|perm| permissions_hash.include?(perm.to_sym) && (permissions_hash[perm.to_sym] == true) }
end
# accepts a permission identifier string or an array of permission identifier strings
# and return true if the user has any of the permissions given by the parameters
# false if none.
def has_any_permission?(*perms)
perms.any? {|perm| permissions_hash.include?(perm.to_sym) && (permissions_hash[perm.to_sym] == true) }
end
# Merges another permissible object's permissions into this permissible's permissions hash
# In the case of identical keys, a false value wins over a true value.
def merge_permissions!(other_permissions_hash)
permissions_hash.merge!(other_permissions_hash) {|key,oldval,newval| oldval.nil? ? newval : oldval && newval}
end
# Resets permissions and then loads them.
def reload_permissions!
reset_permissions!
permissions_hash
end
def roles_list
list = []
roles.inject(list) do |list,role|
list << role.name
role.roles_list.inject(list) {|list,role| list << role}
end
list.uniq
end
def in_role?(*role_names)
role_names.all? {|role| roles_list.include?(role) }
end
def in_any_role?(*role_names)
role_names.any? {|role| roles_list.include?(role) }
end
private
# Nilifies permissions_hash instance variable.
def reset_permissions!
@permissions_hash = nil
end
end
end
end
end

48
spec/fixtures/permissions.yml vendored Normal file
View file

@ -0,0 +1,48 @@
one:
id: 1
permissible_id: 7
permissible_type: "Permission"
action: "view_something"
granted: 1
two:
id: 2
permissible_id: 7
permissible_type: "Permission"
action: "delete_something"
granted: 0
three:
id: 3
permissible_id: 8
permissible_type: "Permission"
action: "view_something"
granted: 1
four:
id: 4
permissible_id: 8
permissible_type: "Permission"
action: "delete_something"
granted: 1
five:
id: 5
permissible_id: 8
permissible_type: "Permission"
action: "update_something"
granted: 1
six:
id: 6
permissible_id: 8
permissible_type: "Permission"
action: "create_something"
granted: 0
perm:
id: 7
permissible_id: 47
permissible_type: "User"
action: "non_important"
granted: 1
perm2:
id: 8
permissible_id: 48
permissible_type: "User"
action: "non_important"
granted: 1

25
spec/fixtures/role_memberships.yml vendored Normal file
View file

@ -0,0 +1,25 @@
publishers_to_customers:
id: 1
roleable_id: 1
roleable_type: "Role"
role_id: 3
advertisers_to_customers:
id: 2
roleable_id: 2
roleable_type: "Role"
role_id: 3
admins_to_company:
id: 3
roleable_id: 5
roleable_type: "Role"
role_id: 4
company_to_admins:
id: 4
roleable_id: 4
roleable_type: "Role"
role_id: 5
publishers_to_company:
id: 5
roleable_id: 1
roleable_type: "Role"
role_id: 4

15
spec/fixtures/roles.yml vendored Normal file
View file

@ -0,0 +1,15 @@
publishers:
id: 1
name: "Publishers"
advertisers:
id: 2
name: "Advertisers"
customers:
id: 3
name: "Customers"
company:
id: 4
name: "Company"
admins:
id: 5
name: "Admins"

View file

@ -0,0 +1,208 @@
require File.dirname(__FILE__) + '/../spec_helper'
class Permission < ActiveRecord::Base
acts_as_permissible
end
describe "acts_as_permissible" do
fixtures :permissions
before(:each) do
@perm = permissions(:perm)
end
describe "class methods" do
it "should find_permissions_for(obj) correctly" do
Permission.find_permissions_for(@perm).size.should == 2
Permission.find_permissions_for(@perm).first.action.should == "view_something"
Permission.find_permissions_for(@perm).last.action.should == "delete_something"
end
end
describe "permissions_hash" do
it "should return the correct permissions_hash" do
@perm.permissions_hash.should == {:view_something => true, :delete_something => false}
end
end
describe "has_permission?" do
it "should return true if permission found" do
@perm.has_permission?("view_something").should == true
end
it "should return false if permission not found" do
@perm.has_permission?("create_something").should == false
end
it "should return false if permission found and is denied" do
@perm.has_permission?("delete_something").should == false
end
end
describe "merge_permissions!" do
before(:each) do
@perm2 = permissions(:perm2)
@merged_permissions = @perm.merge_permissions!(@perm2.permissions_hash)
# {:update_something=>true, :view_something=>true, :delete_something=>false, :create_something=>false}
end
it "should include all keys from both hashes" do
@merged_permissions.keys.should ==
(@perm.permissions_hash.keys + @perm2.permissions_hash.keys).uniq
end
it "should override identical keys with false value" do
@merged_permissions[:delete_something].should == false
end
end
describe "reload_permissions!" do
before(:each) do
@original_hash = @perm.permissions_hash
@perm.permissions << Permission.new(:action => "add_something", :granted => true)
end
it "should catch-up with database changes" do
@perm.permissions_hash.should == @original_hash
reloaded_hash = @perm.reload_permissions!
reloaded_hash.should_not == @original_hash
end
it "should get the changes correctly" do
reloaded_hash = @perm.reload_permissions!
reloaded_hash.keys.should include(:add_something)
end
end
describe "roles_list" do
before(:each) do
@perm.roles_list.should == []
@mutables = Role.new(:name => "mutables")
@mutables.save!
@wierdos = Role.new(:name => "wierdos")
@wierdos.save!
@mutables.roles << @wierdos
end
after(:each) do
@mutables.destroy
@wierdos.destroy
@perm.roles.reset
@perm.roles_list.should == []
end
it "should return the correct list" do
@perm.roles << @wierdos
@perm.roles_list.size.should == 1
@perm.roles_list.should include("wierdos")
end
it "should return the correct list including parent roles of roles recursively." do
@perm.roles << @mutables
@perm.roles_list.size.should == 2
@perm.roles_list.should include("mutables")
@perm.roles_list.should include("wierdos")
end
end
describe "in_role?" do
before(:each) do
@mutables = Role.new(:name => "mutables")
@mutables.save!
@immutables = Role.new(:name => "immutables")
@immutables.save!
end
after(:each) do
@mutables.destroy
@immutables.destroy
@perm.roles.reset
end
it "should return true if member of one" do
@perm.roles << @mutables
@perm.in_role?("mutables").should == true
end
it "should return false if not a member" do
@perm.in_role?("mutables").should == false
end
it "should return true if member of all" do
@perm.roles << @mutables
@perm.roles << @immutables
@perm.in_role?("mutables","immutables").should == true
end
it "should return false if member of some" do
@perm.roles << @mutables
@perm.in_role?("mutables","immutables").should == false
end
end
describe "in_any_role?" do
before(:each) do
@mutables = Role.new(:name => "mutables")
@mutables.save!
@immutables = Role.new(:name => "immutables")
@immutables.save!
end
it "should return true if member of one" do
@perm.roles << @mutables
@perm.in_any_role?("mutables","immutables").should == true
end
it "should return false if not a member" do
@perm.in_any_role?("mutables","immutables").should == false
end
it "should return true if member of all" do
@perm.roles << @mutables
@perm.roles << @immutables
@perm.in_any_role?("mutables","immutables").should == true
end
end
describe "full_permissions_hash" do
before(:each) do
@mutables = Role.new(:name => "mutables")
@mutables.save!
@mutable_permission = Permission.new(:permissible_id => @mutables.id, :permissible_type => @mutables.class.to_s, :action => "view_something", :granted => false)
@mutable_permission.save!
@immutables = Role.new(:name => "immutables")
@immutables.save!
@immutable_permission = Permission.new(:permissible_id => @immutables.id, :permissible_type => @immutables.class.to_s, :action => "download_something", :granted => true)
@immutable_permission.save!
end
it "should return the correct hash if object doesn't belong to roles" do
@perm.roles.should == []
@perm.full_permissions_hash.should == {:view_something=>true, :delete_something=>false}
end
it "should return the correct hash if object belongs to one role" do
@perm.roles << @mutables
@perm.full_permissions_hash.should == {:view_something=>false, :delete_something=>false}
end
it "should return the correct hash if object belongs to one role which belongs to another role" do
@mutables.roles << @immutables
@perm.roles << @mutables
@perm.full_permissions_hash.should == {:view_something=>false, :delete_something=>false, :download_something=>true}
end
it "should return the correct hash if object belongs to 2 roles" do
@perm.roles << @immutables
@perm.roles << @mutables
@perm.full_permissions_hash.should == {:view_something=>false, :delete_something=>false, :download_something=>true}
end
after(:each) do
@mutables.destroy
@immutables.destroy
@perm.roles.reset
end
end
end

View file

@ -0,0 +1,51 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe Permission, "to_hash" do
before(:each) do
@permission = Permission.new(:permissible_id => 1, :permissible_type => "User", :action => "some_action", :granted => 1)
end
it "to_hash returns {} if new record" do
@permission.to_hash.should == {}
end
it "to_hash returns {action => granted}" do
@permission.save
@permission.to_hash.should == {"some_action" => true}
end
end
describe Permission, "validations" do
before(:each) do
@permission = Permission.new(:permissible_id => 1, :permissible_type => "User", :action => "some_action", :granted => 1)
end
it "should be valid" do
@permission.should be_valid
end
it "action should be unique to a permissible id and type" do
@permission.save
@permission2 = Permission.new(:permissible_id => 1, :permissible_type => "User", :action => "some_action", :granted => 0)
@permission2.should_not be_valid
end
it "must have a permissible_id" do
@permission.permissible_id = nil
@permission.should_not be_valid
end
it "must have a permissible_type" do
@permission.permissible_type = nil
@permission.should_not be_valid
end
it "must have an action" do
@permission.action = nil
@permission.should_not be_valid
@permission.action = ""
@permission.should_not be_valid
end
end

View file

@ -0,0 +1,100 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe "RoleMembership" do
describe "validations" do
before(:all) do
@roles = []
@roles[0] = Role.new(:name => "role0")
@roles[1] = Role.new(:name => "role1")
@roles[2] = Role.new(:name => "role2")
@roles[3] = Role.new(:name => "role3")
@roles[4] = Role.new(:name => "role4")
@roles[5] = Role.new(:name => "role5")
@roles[6] = Role.new(:name => "role6")
@roles[7] = Role.new(:name => "role7")
@roles[8] = Role.new(:name => "role8")
@roles[9] = Role.new(:name => "role9")
@roles[10] = Role.new(:name => "role10")
@roles[11] = Role.new(:name => "role11")
@roles.each {|role| role.save!}
end
before(:each) do
@membership = RoleMembership.new(:roleable_id => @roles[0].id, :roleable_type => "Role", :role_id => @roles[1].id)
end
it "should be valid" do
@membership.should be_valid
end
# roleable_id
it "should have a roleable_id" do
@membership.roleable_id = nil
@membership.should_not be_valid
end
it "roleable_id should be an integer" do
@membership.roleable_id = "asd"
@membership.should_not be_valid
end
# roleable_type
it "should have a roleable_type" do
@membership.roleable_type = nil
@membership.should_not be_valid
end
it "roleable_type should be a string" do
@membership.roleable_type = 123
@membership.should_not be_valid
end
it "roleable_type should have a class name format" do
@membership.roleable_type = "asd"
@membership.should_not be_valid
@membership.roleable_type = "User"
@membership.should be_valid
@membership.roleable_type = "Some95WierdClassN4m3"
@membership.should be_valid
end
# role_id
it "should have a role_id" do
@membership.role_id = nil
@membership.should_not be_valid
end
it "role_id should be an integer" do
@membership.role_id = "asd"
@membership.should_not be_valid
end
it "should not allow a role to belong to itself" do
@membership.role_id = @roles[0].id
@membership.should_not be_valid
end
# roles cannot belong to each other in a loop
it "should not a allow a role to belong to a role which belongs to it in a loop" do
@roles[0].roles << @roles[1]
@roles[1].roles << @roles[2]
@roles[2].roles << @roles[3]
@roles[2].roles << @roles[4]
@roles[2].roles << @roles[5]
@roles[3].roles << @roles[6]
@roles[1].roles << @roles[7]
@roles[3].roles << @roles[8]
@roles[4].roles << @roles[9]
@roles[4].roles << @roles[10]
@roles[5].roles << @roles[11]
@membership3 = RoleMembership.new(:roleable_id => @roles[11].id, :roleable_type => "Role", :role_id => @roles[0].id)
@membership3.should_not be_valid
@membership3.errors.full_messages.should include("A role cannot belong to a role which belongs to it.")
end
after(:all) do
@roles.each {|role| role.destroy}
end
end
end

53
spec/models/role_spec.rb Normal file
View file

@ -0,0 +1,53 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe "Role" do
describe "validations" do
before(:each) do
@role = Role.new(:name => "Hunters")
end
it "should be valid" do
@role.should be_valid
end
it "should have a unique name" do
@role.save
@role2 = Role.new(:name => "Hunters")
@role2.should_not be_valid
end
end
describe "associations" do
fixtures :roles, :role_memberships
it "should get subgroups correctly" do
roles(:company).subroles.size.should == 2
arr = []
arr << roles(:publishers)
arr << roles(:admins)
roles(:company).subroles.should include(arr.first)
roles(:company).subroles.should include(arr.last)
roles(:customers).subroles.size.should == 2
arr = []
arr << roles(:publishers)
arr << roles(:advertisers)
roles(:customers).subroles.should include(arr.first)
roles(:customers).subroles.should include(arr.last)
end
it "should get roles correctly" do
roles(:publishers).roles.size.should == 2
arr = []
arr << roles(:customers)
arr << roles(:company)
roles(:publishers).roles.should == arr
roles(:admins).roles.size.should == 1
arr = []
arr << roles(:company)
roles(:admins).roles.should == arr
end
end
end