rbacanable 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|