cancancan 1.7.0
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.
- checksums.yaml +15 -0
- data/CHANGELOG.rdoc +427 -0
- data/CONTRIBUTING.md +11 -0
- data/Gemfile +23 -0
- data/LICENSE +20 -0
- data/README.rdoc +161 -0
- data/Rakefile +18 -0
- data/init.rb +1 -0
- data/lib/cancan.rb +13 -0
- data/lib/cancan/ability.rb +324 -0
- data/lib/cancan/controller_additions.rb +397 -0
- data/lib/cancan/controller_resource.rb +286 -0
- data/lib/cancan/exceptions.rb +50 -0
- data/lib/cancan/inherited_resource.rb +20 -0
- data/lib/cancan/matchers.rb +14 -0
- data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +180 -0
- data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
- data/lib/cancan/model_adapters/default_adapter.rb +7 -0
- data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
- data/lib/cancan/model_additions.rb +31 -0
- data/lib/cancan/rule.rb +147 -0
- data/lib/cancancan.rb +1 -0
- data/lib/generators/cancan/ability/USAGE +4 -0
- data/lib/generators/cancan/ability/ability_generator.rb +11 -0
- data/lib/generators/cancan/ability/templates/ability.rb +32 -0
- data/spec/README.rdoc +28 -0
- data/spec/cancan/ability_spec.rb +455 -0
- data/spec/cancan/controller_additions_spec.rb +141 -0
- data/spec/cancan/controller_resource_spec.rb +553 -0
- data/spec/cancan/exceptions_spec.rb +58 -0
- data/spec/cancan/inherited_resource_spec.rb +60 -0
- data/spec/cancan/matchers_spec.rb +29 -0
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +358 -0
- data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +118 -0
- data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +226 -0
- data/spec/cancan/rule_spec.rb +52 -0
- data/spec/matchers.rb +13 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +77 -0
- metadata +126 -0
data/lib/cancan/rule.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
module CanCan
|
2
|
+
# This class is used internally and should only be called through Ability.
|
3
|
+
# it holds the information about a "can" call made on Ability and provides
|
4
|
+
# helpful methods to determine permission checking and conditions hash generation.
|
5
|
+
class Rule # :nodoc:
|
6
|
+
attr_reader :base_behavior, :subjects, :actions, :conditions
|
7
|
+
attr_writer :expanded_actions
|
8
|
+
|
9
|
+
# The first argument when initializing is the base_behavior which is a true/false
|
10
|
+
# value. True for "can" and false for "cannot". The next two arguments are the action
|
11
|
+
# and subject respectively (such as :read, @project). The third argument is a hash
|
12
|
+
# of conditions and the last one is the block passed to the "can" call.
|
13
|
+
def initialize(base_behavior, action, subject, conditions, block)
|
14
|
+
raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if conditions.kind_of?(Hash) && !block.nil?
|
15
|
+
@match_all = action.nil? && subject.nil?
|
16
|
+
@base_behavior = base_behavior
|
17
|
+
@actions = [action].flatten
|
18
|
+
@subjects = [subject].flatten
|
19
|
+
@conditions = conditions || {}
|
20
|
+
@block = block
|
21
|
+
end
|
22
|
+
|
23
|
+
# Matches both the subject and action, not necessarily the conditions
|
24
|
+
def relevant?(action, subject)
|
25
|
+
subject = subject.values.first if subject.class == Hash
|
26
|
+
@match_all || (matches_action?(action) && matches_subject?(subject))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Matches the block or conditions hash
|
30
|
+
def matches_conditions?(action, subject, extra_args)
|
31
|
+
if @match_all
|
32
|
+
call_block_with_all(action, subject, extra_args)
|
33
|
+
elsif @block && !subject_class?(subject)
|
34
|
+
@block.call(subject, *extra_args)
|
35
|
+
elsif @conditions.kind_of?(Hash) && subject.class == Hash
|
36
|
+
nested_subject_matches_conditions?(subject)
|
37
|
+
elsif @conditions.kind_of?(Hash) && !subject_class?(subject)
|
38
|
+
matches_conditions_hash?(subject)
|
39
|
+
else
|
40
|
+
# Don't stop at "cannot" definitions when there are conditions.
|
41
|
+
conditions_empty? ? true : @base_behavior
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def only_block?
|
46
|
+
conditions_empty? && !@block.nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
def only_raw_sql?
|
50
|
+
@block.nil? && !conditions_empty? && !@conditions.kind_of?(Hash)
|
51
|
+
end
|
52
|
+
|
53
|
+
def conditions_empty?
|
54
|
+
@conditions == {} || @conditions.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
def unmergeable?
|
58
|
+
@conditions.respond_to?(:keys) && @conditions.present? &&
|
59
|
+
(!@conditions.keys.first.kind_of? Symbol)
|
60
|
+
end
|
61
|
+
|
62
|
+
def associations_hash(conditions = @conditions)
|
63
|
+
hash = {}
|
64
|
+
conditions.map do |name, value|
|
65
|
+
hash[name] = associations_hash(value) if value.kind_of? Hash
|
66
|
+
end if conditions.kind_of? Hash
|
67
|
+
hash
|
68
|
+
end
|
69
|
+
|
70
|
+
def attributes_from_conditions
|
71
|
+
attributes = {}
|
72
|
+
@conditions.each do |key, value|
|
73
|
+
attributes[key] = value unless [Array, Range, Hash].include? value.class
|
74
|
+
end if @conditions.kind_of? Hash
|
75
|
+
attributes
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def subject_class?(subject)
|
81
|
+
klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
|
82
|
+
klass == Class || klass == Module
|
83
|
+
end
|
84
|
+
|
85
|
+
def matches_action?(action)
|
86
|
+
@expanded_actions.include?(:manage) || @expanded_actions.include?(action)
|
87
|
+
end
|
88
|
+
|
89
|
+
def matches_subject?(subject)
|
90
|
+
@subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
|
91
|
+
end
|
92
|
+
|
93
|
+
def matches_subject_class?(subject)
|
94
|
+
@subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Checks if the given subject matches the given conditions hash.
|
98
|
+
# This behavior can be overriden by a model adapter by defining two class methods:
|
99
|
+
# override_matching_for_conditions?(subject, conditions) and
|
100
|
+
# matches_conditions_hash?(subject, conditions)
|
101
|
+
def matches_conditions_hash?(subject, conditions = @conditions)
|
102
|
+
if conditions.empty?
|
103
|
+
true
|
104
|
+
else
|
105
|
+
if model_adapter(subject).override_conditions_hash_matching? subject, conditions
|
106
|
+
model_adapter(subject).matches_conditions_hash? subject, conditions
|
107
|
+
else
|
108
|
+
conditions.all? do |name, value|
|
109
|
+
if model_adapter(subject).override_condition_matching? subject, name, value
|
110
|
+
model_adapter(subject).matches_condition? subject, name, value
|
111
|
+
else
|
112
|
+
attribute = subject.send(name)
|
113
|
+
if value.kind_of?(Hash)
|
114
|
+
if attribute.kind_of?(Array) || attribute.kind_of?(ActiveRecord::Relation)
|
115
|
+
attribute.any? { |element| matches_conditions_hash? element, value }
|
116
|
+
else
|
117
|
+
!attribute.nil? && matches_conditions_hash?(attribute, value)
|
118
|
+
end
|
119
|
+
elsif !value.is_a?(String) && value.kind_of?(Enumerable)
|
120
|
+
value.include? attribute
|
121
|
+
else
|
122
|
+
attribute == value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def nested_subject_matches_conditions?(subject_hash)
|
131
|
+
parent, child = subject_hash.first
|
132
|
+
matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
|
133
|
+
end
|
134
|
+
|
135
|
+
def call_block_with_all(action, subject, extra_args)
|
136
|
+
if subject.class == Class
|
137
|
+
@block.call(action, subject, nil, *extra_args)
|
138
|
+
else
|
139
|
+
@block.call(action, subject.class, subject, *extra_args)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def model_adapter(subject)
|
144
|
+
CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/cancancan.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'cancan'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Ability
|
2
|
+
include CanCan::Ability
|
3
|
+
|
4
|
+
def initialize(user)
|
5
|
+
# Define abilities for the passed in user here. For example:
|
6
|
+
#
|
7
|
+
# user ||= User.new # guest user (not logged in)
|
8
|
+
# if user.admin?
|
9
|
+
# can :manage, :all
|
10
|
+
# else
|
11
|
+
# can :read, :all
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# The first argument to `can` is the action you are giving the user
|
15
|
+
# permission to do.
|
16
|
+
# If you pass :manage it will apply to every action. Other common actions
|
17
|
+
# here are :read, :create, :update and :destroy.
|
18
|
+
#
|
19
|
+
# The second argument is the resource the user can perform the action on.
|
20
|
+
# If you pass :all it will apply to every resource. Otherwise pass a Ruby
|
21
|
+
# class of the resource.
|
22
|
+
#
|
23
|
+
# The third argument is an optional hash of conditions to further filter the
|
24
|
+
# objects.
|
25
|
+
# For example, here the user can only update published articles.
|
26
|
+
#
|
27
|
+
# can :update, Article, :published => true
|
28
|
+
#
|
29
|
+
# See the wiki for details:
|
30
|
+
# https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
|
31
|
+
end
|
32
|
+
end
|
data/spec/README.rdoc
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
= CanCan Specs
|
2
|
+
|
3
|
+
== Running the specs
|
4
|
+
|
5
|
+
To run the specs first run the +bundle+ command to install the necessary gems and the +rake+ command to run the specs.
|
6
|
+
|
7
|
+
bundle
|
8
|
+
rake
|
9
|
+
|
10
|
+
The specs currently require Ruby 1.8.7. Ruby 1.9.2 support will be coming soon.
|
11
|
+
|
12
|
+
|
13
|
+
== Model Adapters
|
14
|
+
|
15
|
+
CanCan offers separate specs for different model adapters (such as Mongoid and Data Mapper). By default it will use Active Record but you can change this by setting the +MODEL_ADAPTER+ environment variable before running. You can run the +bundle+ command with this as well to ensure you have the installed gems.
|
16
|
+
|
17
|
+
MODEL_ADAPTER=data_mapper bundle
|
18
|
+
MODEL_ADAPTER=data_mapper rake
|
19
|
+
|
20
|
+
The different model adapters you can specify are:
|
21
|
+
|
22
|
+
* active_record (default)
|
23
|
+
* data_mapper
|
24
|
+
* mongoid
|
25
|
+
|
26
|
+
You can also run the +spec_all+ rake task to run specs for each adapter.
|
27
|
+
|
28
|
+
rake spec_all
|
@@ -0,0 +1,455 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe CanCan::Ability do
|
4
|
+
before(:each) do
|
5
|
+
(@ability = double).extend(CanCan::Ability)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "is able to :read anything" do
|
9
|
+
@ability.can :read, :all
|
10
|
+
expect(@ability.can?(:read, String)).to be_true
|
11
|
+
expect(@ability.can?(:read, 123)).to be_true
|
12
|
+
end
|
13
|
+
|
14
|
+
it "does not have permission to do something it doesn't know about" do
|
15
|
+
expect(@ability.can?(:foodfight, String)).to be_false
|
16
|
+
end
|
17
|
+
|
18
|
+
it "passes true to `can?` when non false/nil is returned in block" do
|
19
|
+
@ability.can :read, :all
|
20
|
+
@ability.can :read, Symbol do |sym|
|
21
|
+
"foo" # TODO test that sym is nil when no instance is passed
|
22
|
+
end
|
23
|
+
expect(@ability.can?(:read, :some_symbol)).to be_true
|
24
|
+
end
|
25
|
+
|
26
|
+
it "passes nil to a block when no instance is passed" do
|
27
|
+
@ability.can :read, Symbol do |sym|
|
28
|
+
expect(sym).to be_nil
|
29
|
+
true
|
30
|
+
end
|
31
|
+
expect(@ability.can?(:read, Symbol)).to be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it "passes to previous rule, if block returns false or nil" do
|
35
|
+
@ability.can :read, Symbol
|
36
|
+
@ability.can :read, Integer do |i|
|
37
|
+
i < 5
|
38
|
+
end
|
39
|
+
@ability.can :read, Integer do |i|
|
40
|
+
i > 10
|
41
|
+
end
|
42
|
+
expect(@ability.can?(:read, Symbol)).to be_true
|
43
|
+
expect(@ability.can?(:read, 11)).to be_true
|
44
|
+
expect(@ability.can?(:read, 1)).to be_true
|
45
|
+
expect(@ability.can?(:read, 6)).to be_false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "does not pass class with object if :all objects are accepted" do
|
49
|
+
@ability.can :preview, :all do |object|
|
50
|
+
expect(object).to eq(123)
|
51
|
+
@block_called = true
|
52
|
+
end
|
53
|
+
@ability.can?(:preview, 123)
|
54
|
+
expect(@block_called).to be_true
|
55
|
+
end
|
56
|
+
|
57
|
+
it "does not call block when only class is passed, only return true" do
|
58
|
+
@block_called = false
|
59
|
+
@ability.can :preview, :all do |object|
|
60
|
+
@block_called = true
|
61
|
+
end
|
62
|
+
expect(@ability.can?(:preview, Hash)).to be_true
|
63
|
+
expect(@block_called).to be_false
|
64
|
+
end
|
65
|
+
|
66
|
+
it "passes only object for global manage actions" do
|
67
|
+
@ability.can :manage, String do |object|
|
68
|
+
expect(object).to eq("foo")
|
69
|
+
@block_called = true
|
70
|
+
end
|
71
|
+
expect(@ability.can?(:stuff, "foo")).to be_true
|
72
|
+
expect(@block_called).to be_true
|
73
|
+
end
|
74
|
+
|
75
|
+
it "makes alias for update or destroy actions to modify action" do
|
76
|
+
@ability.alias_action :update, :destroy, :to => :modify
|
77
|
+
@ability.can :modify, :all
|
78
|
+
expect(@ability.can?(:update, 123)).to be_true
|
79
|
+
expect(@ability.can?(:destroy, 123)).to be_true
|
80
|
+
end
|
81
|
+
|
82
|
+
it "allows deeply nested aliased actions" do
|
83
|
+
@ability.alias_action :increment, :to => :sort
|
84
|
+
@ability.alias_action :sort, :to => :modify
|
85
|
+
@ability.can :modify, :all
|
86
|
+
expect(@ability.can?(:increment, 123)).to be_true
|
87
|
+
end
|
88
|
+
|
89
|
+
it "raises an Error if alias target is an exist action" do
|
90
|
+
expect { @ability.alias_action :show, :to => :show }.to raise_error(CanCan::Error, "You can't specify target (show) as alias because it is real action name")
|
91
|
+
end
|
92
|
+
|
93
|
+
it "always calls block with arguments when passing no arguments to can" do
|
94
|
+
@ability.can do |action, object_class, object|
|
95
|
+
expect(action).to eq(:foo)
|
96
|
+
expect(object_class).to eq(123.class)
|
97
|
+
expect(object).to eq(123)
|
98
|
+
@block_called = true
|
99
|
+
end
|
100
|
+
@ability.can?(:foo, 123)
|
101
|
+
expect(@block_called).to be_true
|
102
|
+
end
|
103
|
+
|
104
|
+
it "passes nil to object when comparing class with can check" do
|
105
|
+
@ability.can do |action, object_class, object|
|
106
|
+
expect(action).to eq(:foo)
|
107
|
+
expect(object_class).to eq(Hash)
|
108
|
+
expect(object).to be_nil
|
109
|
+
@block_called = true
|
110
|
+
end
|
111
|
+
@ability.can?(:foo, Hash)
|
112
|
+
expect(@block_called).to be_true
|
113
|
+
end
|
114
|
+
|
115
|
+
it "automatically makes alias for index and show into read calls" do
|
116
|
+
@ability.can :read, :all
|
117
|
+
expect(@ability.can?(:index, 123)).to be_true
|
118
|
+
expect(@ability.can?(:show, 123)).to be_true
|
119
|
+
end
|
120
|
+
|
121
|
+
it "automatically makes alias for new and edit into create and update respectively" do
|
122
|
+
@ability.can :create, :all
|
123
|
+
@ability.can :update, :all
|
124
|
+
expect(@ability.can?(:new, 123)).to be_true
|
125
|
+
expect(@ability.can?(:edit, 123)).to be_true
|
126
|
+
end
|
127
|
+
|
128
|
+
it "does not respond to prepare (now using initialize)" do
|
129
|
+
expect(@ability).to_not respond_to(:prepare)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "offers cannot? method which is simply invert of can?" do
|
133
|
+
expect(@ability.cannot?(:tie, String)).to be_true
|
134
|
+
end
|
135
|
+
|
136
|
+
it "is able to specify multiple actions and match any" do
|
137
|
+
@ability.can [:read, :update], :all
|
138
|
+
expect(@ability.can?(:read, 123)).to be_true
|
139
|
+
expect(@ability.can?(:update, 123)).to be_true
|
140
|
+
expect(@ability.can?(:count, 123)).to be_false
|
141
|
+
end
|
142
|
+
|
143
|
+
it "is able to specify multiple classes and match any" do
|
144
|
+
@ability.can :update, [String, Range]
|
145
|
+
expect(@ability.can?(:update, "foo")).to be_true
|
146
|
+
expect(@ability.can?(:update, 1..3)).to be_true
|
147
|
+
expect(@ability.can?(:update, 123)).to be_false
|
148
|
+
end
|
149
|
+
|
150
|
+
it "supports custom objects in the rule" do
|
151
|
+
@ability.can :read, :stats
|
152
|
+
expect(@ability.can?(:read, :stats)).to be_true
|
153
|
+
expect(@ability.can?(:update, :stats)).to be_false
|
154
|
+
expect(@ability.can?(:read, :nonstats)).to be_false
|
155
|
+
end
|
156
|
+
|
157
|
+
it "checks ancestors of class" do
|
158
|
+
@ability.can :read, Numeric
|
159
|
+
expect(@ability.can?(:read, Integer)).to be_true
|
160
|
+
expect(@ability.can?(:read, 1.23)).to be_true
|
161
|
+
expect(@ability.can?(:read, "foo")).to be_false
|
162
|
+
end
|
163
|
+
|
164
|
+
it "supports 'cannot' method to define what user cannot do" do
|
165
|
+
@ability.can :read, :all
|
166
|
+
@ability.cannot :read, Integer
|
167
|
+
expect(@ability.can?(:read, "foo")).to be_true
|
168
|
+
expect(@ability.can?(:read, 123)).to be_false
|
169
|
+
end
|
170
|
+
|
171
|
+
it "passes to previous rule, if block returns false or nil" do
|
172
|
+
@ability.can :read, :all
|
173
|
+
@ability.cannot :read, Integer do |int|
|
174
|
+
int > 10 ? nil : ( int > 5 )
|
175
|
+
end
|
176
|
+
expect(@ability.can?(:read, "foo")).to be_true
|
177
|
+
expect(@ability.can?(:read, 3)).to be_true
|
178
|
+
expect(@ability.can?(:read, 8)).to be_false
|
179
|
+
expect(@ability.can?(:read, 123)).to be_true
|
180
|
+
end
|
181
|
+
|
182
|
+
it "always returns `false` for single cannot definition" do
|
183
|
+
@ability.cannot :read, Integer do |int|
|
184
|
+
int > 10 ? nil : ( int > 5 )
|
185
|
+
end
|
186
|
+
expect(@ability.can?(:read, "foo")).to be_false
|
187
|
+
expect(@ability.can?(:read, 3)).to be_false
|
188
|
+
expect(@ability.can?(:read, 8)).to be_false
|
189
|
+
expect(@ability.can?(:read, 123)).to be_false
|
190
|
+
end
|
191
|
+
|
192
|
+
it "passes to previous cannot definition, if block returns false or nil" do
|
193
|
+
@ability.cannot :read, :all
|
194
|
+
@ability.can :read, Integer do |int|
|
195
|
+
int > 10 ? nil : ( int > 5 )
|
196
|
+
end
|
197
|
+
expect(@ability.can?(:read, "foo")).to be_false
|
198
|
+
expect(@ability.can?(:read, 3)).to be_false
|
199
|
+
expect(@ability.can?(:read, 10)).to be_true
|
200
|
+
expect(@ability.can?(:read, 123)).to be_false
|
201
|
+
end
|
202
|
+
|
203
|
+
it "appends aliased actions" do
|
204
|
+
@ability.alias_action :update, :to => :modify
|
205
|
+
@ability.alias_action :destroy, :to => :modify
|
206
|
+
expect(@ability.aliased_actions[:modify]).to eq([:update, :destroy])
|
207
|
+
end
|
208
|
+
|
209
|
+
it "clears aliased actions" do
|
210
|
+
@ability.alias_action :update, :to => :modify
|
211
|
+
@ability.clear_aliased_actions
|
212
|
+
expect(@ability.aliased_actions[:modify]).to be_nil
|
213
|
+
end
|
214
|
+
|
215
|
+
it "passes additional arguments to block from can?" do
|
216
|
+
@ability.can :read, Integer do |int, x|
|
217
|
+
int > x
|
218
|
+
end
|
219
|
+
expect(@ability.can?(:read, 2, 1)).to be_true
|
220
|
+
expect(@ability.can?(:read, 2, 3)).to be_false
|
221
|
+
end
|
222
|
+
|
223
|
+
it "uses conditions as third parameter and determine abilities from it" do
|
224
|
+
@ability.can :read, Range, :begin => 1, :end => 3
|
225
|
+
expect(@ability.can?(:read, 1..3)).to be_true
|
226
|
+
expect(@ability.can?(:read, 1..4)).to be_false
|
227
|
+
expect(@ability.can?(:read, Range)).to be_true
|
228
|
+
end
|
229
|
+
|
230
|
+
it "allows an array of options in conditions hash" do
|
231
|
+
@ability.can :read, Range, :begin => [1, 3, 5]
|
232
|
+
expect(@ability.can?(:read, 1..3)).to be_true
|
233
|
+
expect(@ability.can?(:read, 2..4)).to be_false
|
234
|
+
expect(@ability.can?(:read, 3..5)).to be_true
|
235
|
+
end
|
236
|
+
|
237
|
+
it "allows a range of options in conditions hash" do
|
238
|
+
@ability.can :read, Range, :begin => 1..3
|
239
|
+
expect(@ability.can?(:read, 1..10)).to be_true
|
240
|
+
expect(@ability.can?(:read, 3..30)).to be_true
|
241
|
+
expect(@ability.can?(:read, 4..40)).to be_false
|
242
|
+
end
|
243
|
+
|
244
|
+
it "allows nested hashes in conditions hash" do
|
245
|
+
@ability.can :read, Range, :begin => { :to_i => 5 }
|
246
|
+
expect(@ability.can?(:read, 5..7)).to be_true
|
247
|
+
expect(@ability.can?(:read, 6..8)).to be_false
|
248
|
+
end
|
249
|
+
|
250
|
+
it "matches any element passed in to nesting if it's an array (for has_many associations)" do
|
251
|
+
@ability.can :read, Range, :to_a => { :to_i => 3 }
|
252
|
+
expect(@ability.can?(:read, 1..5)).to be_true
|
253
|
+
expect(@ability.can?(:read, 4..6)).to be_false
|
254
|
+
end
|
255
|
+
|
256
|
+
it "accepts a set as a condition value" do
|
257
|
+
expect(object_with_foo_2 = double(:foo => 2)).to receive(:foo)
|
258
|
+
expect(object_with_foo_3 = double(:foo => 3)).to receive(:foo)
|
259
|
+
@ability.can :read, Object, :foo => [1, 2, 5].to_set
|
260
|
+
expect(@ability.can?(:read, object_with_foo_2)).to be_true
|
261
|
+
expect(@ability.can?(:read, object_with_foo_3)).to be_false
|
262
|
+
end
|
263
|
+
|
264
|
+
it "does not match subjects return nil for methods that must match nested a nested conditions hash" do
|
265
|
+
expect(object_with_foo = double(:foo => :bar)).to receive(:foo)
|
266
|
+
@ability.can :read, Array, :first => { :foo => :bar }
|
267
|
+
expect(@ability.can?(:read, [object_with_foo])).to be_true
|
268
|
+
expect(@ability.can?(:read, [])).to be_false
|
269
|
+
end
|
270
|
+
|
271
|
+
it "matches strings but not substrings specified in a conditions hash" do
|
272
|
+
@ability.can :read, String, :presence => "declassified"
|
273
|
+
expect(@ability.can?(:read, "declassified")).to be_true
|
274
|
+
expect(@ability.can?(:read, "classified")).to be_false
|
275
|
+
end
|
276
|
+
|
277
|
+
it "does not stop at cannot definition when comparing class" do
|
278
|
+
@ability.can :read, Range
|
279
|
+
@ability.cannot :read, Range, :begin => 1
|
280
|
+
expect(@ability.can?(:read, 2..5)).to be_true
|
281
|
+
expect(@ability.can?(:read, 1..5)).to be_false
|
282
|
+
expect(@ability.can?(:read, Range)).to be_true
|
283
|
+
end
|
284
|
+
|
285
|
+
it "stops at cannot definition when no hash is present" do
|
286
|
+
@ability.can :read, :all
|
287
|
+
@ability.cannot :read, Range
|
288
|
+
expect(@ability.can?(:read, 1..5)).to be_false
|
289
|
+
expect(@ability.can?(:read, Range)).to be_false
|
290
|
+
end
|
291
|
+
|
292
|
+
it "allows to check ability for Module" do
|
293
|
+
module B; end
|
294
|
+
class A; include B; end
|
295
|
+
@ability.can :read, B
|
296
|
+
expect(@ability.can?(:read, A)).to be_true
|
297
|
+
expect(@ability.can?(:read, A.new)).to be_true
|
298
|
+
end
|
299
|
+
|
300
|
+
it "passes nil to a block for ability on Module when no instance is passed" do
|
301
|
+
module B; end
|
302
|
+
class A; include B; end
|
303
|
+
@ability.can :read, B do |sym|
|
304
|
+
expect(sym).to be_nil
|
305
|
+
true
|
306
|
+
end
|
307
|
+
expect(@ability.can?(:read, B)).to be_true
|
308
|
+
expect(@ability.can?(:read, A)).to be_true
|
309
|
+
end
|
310
|
+
|
311
|
+
it "checks permissions through association when passing a hash of subjects" do
|
312
|
+
@ability.can :read, Range, :string => {:length => 3}
|
313
|
+
expect(@ability.can?(:read, "foo" => Range)).to be_true
|
314
|
+
expect(@ability.can?(:read, "foobar" => Range)).to be_false
|
315
|
+
expect(@ability.can?(:read, 123 => Range)).to be_true
|
316
|
+
end
|
317
|
+
|
318
|
+
it "checks permissions correctly when passing a hash of subjects with multiple definitions" do
|
319
|
+
@ability.can :read, Range, :string => {:length => 4}
|
320
|
+
@ability.can [:create, :read], Range, :string => {:upcase => 'FOO'}
|
321
|
+
expect(@ability.can?(:read, "foo" => Range)).to be_true
|
322
|
+
expect(@ability.can?(:read, "foobar" => Range)).to be_false
|
323
|
+
expect(@ability.can?(:read, 1234 => Range)).to be_true
|
324
|
+
end
|
325
|
+
|
326
|
+
it "allows to check ability on Hash-like object" do
|
327
|
+
class Container < Hash; end
|
328
|
+
@ability.can :read, Container
|
329
|
+
expect(@ability.can?(:read, Container.new)).to be_true
|
330
|
+
end
|
331
|
+
|
332
|
+
it "has initial attributes based on hash conditions of 'new' action" do
|
333
|
+
@ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"}
|
334
|
+
@ability.can :create, Range, :bar => 123, :array => %w[skip arrays]
|
335
|
+
@ability.can :new, Range, :baz => "baz", :range => 1..3
|
336
|
+
@ability.cannot :new, Range, :ignore => "me"
|
337
|
+
expect(@ability.attributes_for(:new, Range)).to eq({:foo => "foo", :bar => 123, :baz => "baz"})
|
338
|
+
end
|
339
|
+
|
340
|
+
it "raises access denied exception if ability us unauthorized to perform a certain action" do
|
341
|
+
begin
|
342
|
+
@ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!"
|
343
|
+
rescue CanCan::AccessDenied => e
|
344
|
+
expect(e.message).to eq("Access denied!")
|
345
|
+
expect(e.action).to eq(:read)
|
346
|
+
expect(e.subject).to eq(:foo)
|
347
|
+
else
|
348
|
+
fail "Expected CanCan::AccessDenied exception to be raised"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
it "does not raise access denied exception if ability is authorized to perform an action and return subject" do
|
353
|
+
@ability.can :read, :foo
|
354
|
+
expect {
|
355
|
+
expect(@ability.authorize!(:read, :foo)).to eq(:foo)
|
356
|
+
}.to_not raise_error
|
357
|
+
end
|
358
|
+
|
359
|
+
it "knows when block is used in conditions" do
|
360
|
+
@ability.can :read, :foo
|
361
|
+
expect(@ability).to_not have_block(:read, :foo)
|
362
|
+
@ability.can :read, :foo do |foo|
|
363
|
+
false
|
364
|
+
end
|
365
|
+
expect(@ability).to have_block(:read, :foo)
|
366
|
+
end
|
367
|
+
|
368
|
+
it "knows when raw sql is used in conditions" do
|
369
|
+
@ability.can :read, :foo
|
370
|
+
expect(@ability).to_not have_raw_sql(:read, :foo)
|
371
|
+
@ability.can :read, :foo, 'false'
|
372
|
+
expect(@ability).to have_raw_sql(:read, :foo)
|
373
|
+
end
|
374
|
+
|
375
|
+
it "raises access denied exception with default message if not specified" do
|
376
|
+
begin
|
377
|
+
@ability.authorize! :read, :foo
|
378
|
+
rescue CanCan::AccessDenied => e
|
379
|
+
e.default_message = "Access denied!"
|
380
|
+
expect(e.message).to eq("Access denied!")
|
381
|
+
else
|
382
|
+
fail "Expected CanCan::AccessDenied exception to be raised"
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
it "determines model adapterO class by asking AbstractAdapter" do
|
387
|
+
adapter_class, model_class = double, double
|
388
|
+
allow(CanCan::ModelAdapters::AbstractAdapter).to receive(:adapter_class).with(model_class) { adapter_class }
|
389
|
+
allow(adapter_class).to receive(:new).with(model_class, []) { :adapter_instance }
|
390
|
+
expect(@ability.model_adapter(model_class, :read)).to eq(:adapter_instance)
|
391
|
+
end
|
392
|
+
|
393
|
+
it "raises an error when attempting to use a block with a hash condition since it's not likely what they want" do
|
394
|
+
expect {
|
395
|
+
@ability.can :read, Array, :published => true do
|
396
|
+
false
|
397
|
+
end
|
398
|
+
}.to raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.")
|
399
|
+
end
|
400
|
+
|
401
|
+
describe "unauthorized message" do
|
402
|
+
after(:each) do
|
403
|
+
I18n.backend = nil
|
404
|
+
end
|
405
|
+
|
406
|
+
it "uses action/subject in i18n" do
|
407
|
+
I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}}
|
408
|
+
expect(@ability.unauthorized_message(:update, Array)).to eq("foo")
|
409
|
+
expect(@ability.unauthorized_message(:update, [1, 2, 3])).to eq("foo")
|
410
|
+
expect(@ability.unauthorized_message(:update, :missing)).to be_nil
|
411
|
+
end
|
412
|
+
|
413
|
+
it "uses symbol as subject directly" do
|
414
|
+
I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
|
415
|
+
expect(@ability.unauthorized_message(:has, :cheezburger)).to eq("Nom nom nom. I eated it.")
|
416
|
+
end
|
417
|
+
|
418
|
+
it "falls back to 'manage' and 'all'" do
|
419
|
+
I18n.backend.store_translations :en, :unauthorized => {
|
420
|
+
:manage => {:all => "manage all", :array => "manage array"},
|
421
|
+
:update => {:all => "update all", :array => "update array"}
|
422
|
+
}
|
423
|
+
expect(@ability.unauthorized_message(:update, Array)).to eq("update array")
|
424
|
+
expect(@ability.unauthorized_message(:update, Hash)).to eq("update all")
|
425
|
+
expect(@ability.unauthorized_message(:foo, Array)).to eq("manage array")
|
426
|
+
expect(@ability.unauthorized_message(:foo, Hash)).to eq("manage all")
|
427
|
+
end
|
428
|
+
|
429
|
+
it "follows aliased actions" do
|
430
|
+
I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}}
|
431
|
+
@ability.alias_action :update, :to => :modify
|
432
|
+
expect(@ability.unauthorized_message(:update, Array)).to eq("modify array")
|
433
|
+
expect(@ability.unauthorized_message(:edit, Array)).to eq("modify array")
|
434
|
+
end
|
435
|
+
|
436
|
+
it "has variables for action and subject" do
|
437
|
+
I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
|
438
|
+
expect(@ability.unauthorized_message(:update, Array)).to eq("update array")
|
439
|
+
expect(@ability.unauthorized_message(:update, ArgumentError)).to eq("update argument error")
|
440
|
+
expect(@ability.unauthorized_message(:edit, 1..3)).to eq("edit range")
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
describe "#merge" do
|
445
|
+
it "adds the rules from the given ability" do
|
446
|
+
@ability.can :use, :tools
|
447
|
+
(another_ability = double).extend(CanCan::Ability)
|
448
|
+
another_ability.can :use, :search
|
449
|
+
|
450
|
+
@ability.merge(another_ability)
|
451
|
+
expect(@ability.can?(:use, :search)).to be_true
|
452
|
+
expect(@ability.send(:rules).size).to eq(2)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|