rbacanable 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/Changes.rdoc +141 -0
- data/LICENSE +20 -0
- data/README.rdoc +164 -0
- data/Rakefile +44 -0
- data/examples/basic.rb +41 -0
- data/examples/roles.rb +100 -0
- data/lib/canable.rb +191 -0
- data/specs.watchr +47 -0
- data/test/helper.rb +33 -0
- data/test/test_ables.rb +83 -0
- data/test/test_canable.rb +26 -0
- data/test/test_cans.rb +51 -0
- data/test/test_enforcers.rb +32 -0
- data/test/test_roles.rb +353 -0
- metadata +124 -0
data/lib/canable.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
module Canable
|
2
|
+
Version = '0.2'
|
3
|
+
|
4
|
+
# Module that holds all the can_action? methods.
|
5
|
+
module Cans; end
|
6
|
+
|
7
|
+
# Module that holds all the [method]able_by? methods.
|
8
|
+
module Ables; end
|
9
|
+
|
10
|
+
# Module that is included by a role implementation
|
11
|
+
module Role
|
12
|
+
include Cans # each role has a distinct set of responses to all the can_action? methods
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
# These are applied to the actual Role module 'instance' that includes this (Canable::Role) module
|
19
|
+
module ClassMethods
|
20
|
+
# Each role has a default query response, found in this variable
|
21
|
+
attr_accessor :_default_response
|
22
|
+
|
23
|
+
|
24
|
+
# Called when another Role imeplementation module tries to inherit an existing Role implementation
|
25
|
+
# Notice this method isn't self.included, this method becomes self.included on the module including this (Canable::Role) module
|
26
|
+
# This is nesscary to emulate inhertance of the default response and any other variables in the future
|
27
|
+
def included(base)
|
28
|
+
base._default_response = self._default_response
|
29
|
+
end
|
30
|
+
|
31
|
+
# Called when an Actor decides its role and extends itself (an instance) with a Role implementation
|
32
|
+
# Creates the default instance methods for an Actor and persists the can_action? response default down
|
33
|
+
def extended(base)
|
34
|
+
base.extend(RoleEnabledCanInstanceMethods)
|
35
|
+
this_role = self # can't use self inside the instance eval
|
36
|
+
base.instance_eval { @_canable_role = this_role }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Methods given to an instance of an Actor
|
40
|
+
module RoleEnabledCanInstanceMethods
|
41
|
+
def _canable_default # the role default response
|
42
|
+
@_canable_role._default_response
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# ----------------------
|
47
|
+
# Role building DSL
|
48
|
+
# ----------------------
|
49
|
+
def default_response(val)
|
50
|
+
self._default_response = val
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module Actor
|
56
|
+
attr_accessor :canable_included_role
|
57
|
+
|
58
|
+
def self.included(base)
|
59
|
+
base.extend(ClassMethods)
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMethods
|
63
|
+
attr_accessor :canable_default_role
|
64
|
+
attr_accessor :canable_role_attribute
|
65
|
+
# ---------------
|
66
|
+
# RBAC Actor building DSL
|
67
|
+
# ---------------
|
68
|
+
|
69
|
+
def default_role(role)
|
70
|
+
self.canable_default_role = role
|
71
|
+
end
|
72
|
+
|
73
|
+
def role_attribute(attribute)
|
74
|
+
self.canable_role_attribute = attribute
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(*args)
|
80
|
+
super(*args)
|
81
|
+
self.__initialize_canable_role
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def __initialize_canable_role
|
86
|
+
attribute = self.class.canable_role_attribute
|
87
|
+
attribute ||= :@role
|
88
|
+
role_constant = self.instance_variable_get(attribute)
|
89
|
+
if role_constant == nil
|
90
|
+
default_role = self.class.canable_default_role
|
91
|
+
self.act(default_role) unless default_role == nil
|
92
|
+
else
|
93
|
+
self.act(role_constant)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sets the role of this actor by including a role module
|
98
|
+
def act(role)
|
99
|
+
self.canable_included_role = role
|
100
|
+
if(role.respond_to?(:included))
|
101
|
+
self.extend role
|
102
|
+
else
|
103
|
+
self.extend Canable::Roles.const_get((role.to_s.capitalize+"Role").intern)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Holds all the different roles that an actor may assume
|
109
|
+
module Roles
|
110
|
+
# Make one default role that is false for everything
|
111
|
+
module Role
|
112
|
+
include Canable::Role
|
113
|
+
default_response false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Module that holds all the enforce_[action]_permission methods for use in controllers.
|
118
|
+
module Enforcers
|
119
|
+
def self.included(controller)
|
120
|
+
controller.class_eval do
|
121
|
+
Canable.actions.each do |can, able|
|
122
|
+
delegate "can_#{can}?", :to => :current_user
|
123
|
+
helper_method "can_#{can}?" if controller.respond_to?(:helper_method)
|
124
|
+
hide_action "can_#{can}?" if controller.respond_to?(:hide_action)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Exception that gets raised when permissions are broken for whatever reason.
|
131
|
+
class Transgression < StandardError; end
|
132
|
+
|
133
|
+
# Default actions to an empty hash.
|
134
|
+
@actions = {}
|
135
|
+
|
136
|
+
# Returns hash of actions that have been added.
|
137
|
+
# {:view => :viewable, ...}
|
138
|
+
def self.actions
|
139
|
+
@actions
|
140
|
+
end
|
141
|
+
|
142
|
+
# Adds an action to actions and the correct methods to can and able modules.
|
143
|
+
#
|
144
|
+
# @param [Symbol] can_method The name of the can_[action]? method.
|
145
|
+
# @param [Symbol] resource_method The name of the [resource_method]_by? method.
|
146
|
+
def self.add(can, able)
|
147
|
+
@actions[can] = able
|
148
|
+
add_can_method(can)
|
149
|
+
add_able_method(can, able)
|
150
|
+
add_enforcer_method(can)
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
def self.add_can_method(can)
|
155
|
+
Cans.module_eval <<-EOM
|
156
|
+
def can_#{can}?(resource)
|
157
|
+
method = ("can_#{can}_"+resource.class.name.gsub(/::/,"_").downcase+"?").intern
|
158
|
+
if self.respond_to?(method, true)
|
159
|
+
self.send method, resource
|
160
|
+
elsif self.respond_to?(:_canable_default)
|
161
|
+
self._canable_default
|
162
|
+
else
|
163
|
+
false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
EOM
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.add_able_method(can, able)
|
170
|
+
Ables.module_eval <<-EOM
|
171
|
+
def #{able}_by?(actor)
|
172
|
+
return false if actor.blank?
|
173
|
+
actor.can_#{can}?(self)
|
174
|
+
end
|
175
|
+
EOM
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.add_enforcer_method(can)
|
179
|
+
Enforcers.module_eval <<-EOM
|
180
|
+
def enforce_#{can}_permission(resource)
|
181
|
+
raise Canable::Transgression unless can_#{can}?(resource)
|
182
|
+
end
|
183
|
+
private :enforce_#{can}_permission
|
184
|
+
EOM
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
Canable.add(:view, :viewable)
|
189
|
+
Canable.add(:create, :creatable)
|
190
|
+
Canable.add(:update, :updatable)
|
191
|
+
Canable.add(:destroy, :destroyable)
|
data/specs.watchr
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
def growl(title, msg, img)
|
2
|
+
%x{growlnotify -m #{ msg.inspect} -t #{title.inspect} --image ~/.watchr/#{img}.png}
|
3
|
+
end
|
4
|
+
|
5
|
+
def form_growl_message(str)
|
6
|
+
results = str.split("\n").last
|
7
|
+
if results =~ /[1-9]\s(failure|error)s?/
|
8
|
+
growl "Test Results", "#{results}", "fail"
|
9
|
+
elsif results != ""
|
10
|
+
growl "Test Results", "#{results}", "pass"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(cmd)
|
15
|
+
puts(cmd)
|
16
|
+
output = ""
|
17
|
+
IO.popen(cmd) do |com|
|
18
|
+
com.each_char do |c|
|
19
|
+
print c
|
20
|
+
output << c
|
21
|
+
$stdout.flush
|
22
|
+
end
|
23
|
+
end
|
24
|
+
form_growl_message output
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_test_file(file)
|
28
|
+
run %Q(ruby -I"lib:test" -rubygems #{file})
|
29
|
+
end
|
30
|
+
|
31
|
+
def run_all_tests
|
32
|
+
run "rake test"
|
33
|
+
end
|
34
|
+
|
35
|
+
watch('test/helper\.rb') { system('clear'); run_all_tests }
|
36
|
+
watch('test/test_.*\.rb') { |m| system('clear'); run_test_file(m[0]) }
|
37
|
+
watch('lib/.*') { |m| system('clear'); run_all_tests }
|
38
|
+
|
39
|
+
# Ctrl-\
|
40
|
+
Signal.trap('QUIT') do
|
41
|
+
puts " --- Running all tests ---\n\n"
|
42
|
+
run_all_tests
|
43
|
+
end
|
44
|
+
|
45
|
+
# Ctrl-C
|
46
|
+
Signal.trap('INT') { abort("\n") }
|
47
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
gem 'mocha', '0.9.8'
|
5
|
+
gem 'shoulda', '2.10.3'
|
6
|
+
gem 'activesupport', '2.3.5'
|
7
|
+
|
8
|
+
require 'mocha'
|
9
|
+
require 'shoulda'
|
10
|
+
require 'active_support'
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
13
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
14
|
+
require 'canable'
|
15
|
+
|
16
|
+
class Test::Unit::TestCase
|
17
|
+
end
|
18
|
+
|
19
|
+
def Doc(name=nil, &block)
|
20
|
+
klass = Class.new do
|
21
|
+
|
22
|
+
if name
|
23
|
+
class_eval "def self.name; '#{name}' end"
|
24
|
+
class_eval "def self.to_s; '#{name}' end"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
klass.class_eval(&block) if block_given?
|
29
|
+
klass
|
30
|
+
end
|
31
|
+
|
32
|
+
test_dir = File.expand_path(File.dirname(__FILE__) + '/../tmp')
|
33
|
+
FileUtils.mkdir_p(test_dir) unless File.exist?(test_dir)
|
data/test/test_ables.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class AblesTest < Test::Unit::TestCase
|
4
|
+
context "Class with Canable::Ables included" do
|
5
|
+
setup do
|
6
|
+
klass = Doc do
|
7
|
+
include Canable::Ables
|
8
|
+
end
|
9
|
+
|
10
|
+
@resource = klass.new
|
11
|
+
@user = mock('user')
|
12
|
+
end
|
13
|
+
|
14
|
+
context "viewable_by?" do
|
15
|
+
should "be false if user cannot view" do
|
16
|
+
user = mock('user', :can_view? => false)
|
17
|
+
assert ! @resource.viewable_by?(user)
|
18
|
+
end
|
19
|
+
|
20
|
+
should "be true if user can view" do
|
21
|
+
user = mock('user', :can_view? => true)
|
22
|
+
assert @resource.viewable_by?(user)
|
23
|
+
end
|
24
|
+
|
25
|
+
should "be false if resource is blank" do
|
26
|
+
assert ! @resource.viewable_by?(nil)
|
27
|
+
assert ! @resource.viewable_by?('')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "creatable_by?" do
|
32
|
+
should "be false if user cannot create" do
|
33
|
+
user = mock('user', :can_create? => false)
|
34
|
+
assert ! @resource.creatable_by?(user)
|
35
|
+
end
|
36
|
+
|
37
|
+
should "be true if user can create" do
|
38
|
+
user = mock('user', :can_create? => true)
|
39
|
+
assert @resource.creatable_by?(user)
|
40
|
+
end
|
41
|
+
|
42
|
+
should "be false if resource is blank" do
|
43
|
+
assert ! @resource.creatable_by?(nil)
|
44
|
+
assert ! @resource.creatable_by?('')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "updatable_by?" do
|
49
|
+
should "be false if user cannot update" do
|
50
|
+
user = mock('user', :can_update? => false)
|
51
|
+
assert ! @resource.updatable_by?(user)
|
52
|
+
end
|
53
|
+
|
54
|
+
should "be true if user can update" do
|
55
|
+
user = mock('user', :can_update? => true)
|
56
|
+
assert @resource.updatable_by?(user)
|
57
|
+
end
|
58
|
+
|
59
|
+
should "be false if resource is blank" do
|
60
|
+
assert ! @resource.updatable_by?(nil)
|
61
|
+
assert ! @resource.updatable_by?('')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "destroyable_by?" do
|
66
|
+
should "be false if user cannot destroy" do
|
67
|
+
user = mock('user', :can_destroy? => false)
|
68
|
+
assert ! @resource.destroyable_by?(user)
|
69
|
+
end
|
70
|
+
|
71
|
+
should "be true if user can destroy" do
|
72
|
+
user = mock('user', :can_destroy? => true)
|
73
|
+
assert @resource.destroyable_by?(user)
|
74
|
+
end
|
75
|
+
|
76
|
+
should "be false if resource is blank" do
|
77
|
+
assert ! @resource.destroyable_by?(nil)
|
78
|
+
assert ! @resource.destroyable_by?('')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestCanable < Test::Unit::TestCase
|
4
|
+
context "Canable" do
|
5
|
+
should "have view action by default" do
|
6
|
+
assert_equal :viewable, Canable.actions[:view]
|
7
|
+
end
|
8
|
+
|
9
|
+
should "have create action by default" do
|
10
|
+
assert_equal :creatable, Canable.actions[:create]
|
11
|
+
end
|
12
|
+
|
13
|
+
should "have update action by default" do
|
14
|
+
assert_equal :updatable, Canable.actions[:update]
|
15
|
+
end
|
16
|
+
|
17
|
+
should "have destroy action by default" do
|
18
|
+
assert_equal :destroyable, Canable.actions[:destroy]
|
19
|
+
end
|
20
|
+
|
21
|
+
should "be able to add another action" do
|
22
|
+
Canable.add(:publish, :publishable)
|
23
|
+
assert_equal :publishable, Canable.actions[:publish]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/test/test_cans.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class CansTest < Test::Unit::TestCase
|
4
|
+
context "Class with Canable::Cans included" do
|
5
|
+
setup do
|
6
|
+
klass = Class.new do
|
7
|
+
include Canable::Cans
|
8
|
+
end
|
9
|
+
|
10
|
+
@user = klass.new
|
11
|
+
@resource = mock('resource')
|
12
|
+
end
|
13
|
+
|
14
|
+
should "default viewable_by? to false" do
|
15
|
+
assert ! @user.can_view?(@resource)
|
16
|
+
end
|
17
|
+
|
18
|
+
should "default creatable_by? to false" do
|
19
|
+
assert ! @user.can_create?(@resource)
|
20
|
+
end
|
21
|
+
|
22
|
+
should "default updatable_by? to false" do
|
23
|
+
assert ! @user.can_update?(@resource)
|
24
|
+
end
|
25
|
+
|
26
|
+
should "default destroyable_by? to false" do
|
27
|
+
assert ! @user.can_destroy?(@resource)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "Class that overrides a can method" do
|
32
|
+
setup do
|
33
|
+
klass = Doc do
|
34
|
+
include Canable::Cans
|
35
|
+
|
36
|
+
def can_view?(resource)
|
37
|
+
resource.owner == 'John'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@user = klass.new
|
42
|
+
@johns = mock('resource', :owner => 'John')
|
43
|
+
@steves = mock('resource', :owner => 'Steve')
|
44
|
+
end
|
45
|
+
|
46
|
+
should "use the overriden method and default to false" do
|
47
|
+
assert @user.can_view?(@johns)
|
48
|
+
assert ! @user.can_view?(@steves)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class EnforcersTest < Test::Unit::TestCase
|
4
|
+
context "Including Canable::Enforcers in a class" do
|
5
|
+
setup do
|
6
|
+
klass = Class.new do
|
7
|
+
include Canable::Enforcers
|
8
|
+
attr_accessor :current_user, :article
|
9
|
+
|
10
|
+
def show
|
11
|
+
enforce_view_permission(article)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
@article = mock('article')
|
16
|
+
@user = mock('user')
|
17
|
+
@controller = klass.new
|
18
|
+
@controller.article = @article
|
19
|
+
@controller.current_user = @user
|
20
|
+
end
|
21
|
+
|
22
|
+
should "not raise error if can" do
|
23
|
+
@user.expects(:can_view?).with(@article).returns(true)
|
24
|
+
assert_nothing_raised { @controller.show }
|
25
|
+
end
|
26
|
+
|
27
|
+
should "raise error if cannot" do
|
28
|
+
@user.expects(:can_view?).with(@article).returns(false)
|
29
|
+
assert_raises(Canable::Transgression) { @controller.show }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|