codeprimate-cancan 1.6.5
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/CHANGELOG.rdoc +291 -0
- data/Gemfile +20 -0
- data/LICENSE +20 -0
- data/README.rdoc +111 -0
- data/Rakefile +18 -0
- data/init.rb +1 -0
- data/lib/cancan.rb +13 -0
- data/lib/cancan/ability.rb +298 -0
- data/lib/cancan/controller_additions.rb +389 -0
- data/lib/cancan/controller_resource.rb +222 -0
- data/lib/cancan/exceptions.rb +50 -0
- data/lib/cancan/inherited_resource.rb +19 -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 +165 -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 +53 -0
- data/lib/cancan/model_additions.rb +31 -0
- data/lib/cancan/rule.rb +142 -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 +28 -0
- data/spec/README.rdoc +28 -0
- data/spec/cancan/ability_spec.rb +419 -0
- data/spec/cancan/controller_additions_spec.rb +137 -0
- data/spec/cancan/controller_resource_spec.rb +412 -0
- data/spec/cancan/exceptions_spec.rb +58 -0
- data/spec/cancan/inherited_resource_spec.rb +42 -0
- data/spec/cancan/matchers_spec.rb +33 -0
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +278 -0
- data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +119 -0
- data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +216 -0
- data/spec/cancan/rule_spec.rb +39 -0
- data/spec/matchers.rb +13 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +41 -0
- metadata +167 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class DataMapperAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(model_class)
|
5
|
+
model_class <= DataMapper::Resource
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.find(model_class, id)
|
9
|
+
model_class.get(id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.override_conditions_hash_matching?(subject, conditions)
|
13
|
+
conditions.any? { |k,v| !k.kind_of?(Symbol) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.matches_conditions_hash?(subject, conditions)
|
17
|
+
collection = DataMapper::Collection.new(subject.query, [ subject ])
|
18
|
+
!!collection.first(conditions)
|
19
|
+
end
|
20
|
+
|
21
|
+
def database_records
|
22
|
+
scope = @model_class.all(:conditions => ["0 = 1"])
|
23
|
+
cans, cannots = @rules.partition { |r| r.base_behavior }
|
24
|
+
return scope if cans.empty?
|
25
|
+
# apply unions first, then differences. this mean cannot overrides can
|
26
|
+
cans.each { |r| scope += @model_class.all(:conditions => r.conditions) }
|
27
|
+
cannots.each { |r| scope -= @model_class.all(:conditions => r.conditions) }
|
28
|
+
scope
|
29
|
+
end
|
30
|
+
end # class DataMapper
|
31
|
+
end # module ModelAdapters
|
32
|
+
end # module CanCan
|
33
|
+
|
34
|
+
DataMapper::Model.append_extensions(CanCan::ModelAdditions::ClassMethods)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class MongoidAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(model_class)
|
5
|
+
model_class <= Mongoid::Document
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.override_conditions_hash_matching?(subject, conditions)
|
9
|
+
conditions.any? do |k,v|
|
10
|
+
key_is_not_symbol = lambda { !k.kind_of?(Symbol) }
|
11
|
+
subject_value_is_array = lambda do
|
12
|
+
subject.respond_to?(k) && subject.send(k).is_a?(Array)
|
13
|
+
end
|
14
|
+
|
15
|
+
key_is_not_symbol.call || subject_value_is_array.call
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.matches_conditions_hash?(subject, conditions)
|
20
|
+
# To avoid hitting the db, retrieve the raw Mongo selector from
|
21
|
+
# the Mongoid Criteria and use Mongoid::Matchers#matches?
|
22
|
+
subject.matches?( subject.class.where(conditions).selector )
|
23
|
+
end
|
24
|
+
|
25
|
+
def database_records
|
26
|
+
if @rules.size == 0
|
27
|
+
@model_class.where(:_id => {'$exists' => false, '$type' => 7}) # return no records in Mongoid
|
28
|
+
elsif @rules.size == 1 && @rules[0].conditions.is_a?(Mongoid::Criteria)
|
29
|
+
@rules[0].conditions
|
30
|
+
else
|
31
|
+
# we only need to process can rules if
|
32
|
+
# there are no rules with empty conditions
|
33
|
+
rules = @rules.reject { |rule| rule.conditions.empty? }
|
34
|
+
process_can_rules = @rules.count == rules.count
|
35
|
+
rules.inject(@model_class.all) do |records, rule|
|
36
|
+
if process_can_rules && rule.base_behavior
|
37
|
+
records.or rule.conditions
|
38
|
+
elsif !rule.base_behavior
|
39
|
+
records.excludes rule.conditions
|
40
|
+
else
|
41
|
+
records
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# simplest way to add `accessible_by` to all Mongoid Documents
|
51
|
+
module Mongoid::Document::ClassMethods
|
52
|
+
include CanCan::ModelAdditions::ClassMethods
|
53
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module CanCan
|
2
|
+
|
3
|
+
# This module adds the accessible_by class method to a model. It is included in the model adapters.
|
4
|
+
module ModelAdditions
|
5
|
+
module ClassMethods
|
6
|
+
# Returns a scope which fetches only the records that the passed ability
|
7
|
+
# can perform a given action on. The action defaults to :index. This
|
8
|
+
# is usually called from a controller and passed the +current_ability+.
|
9
|
+
#
|
10
|
+
# @articles = Article.accessible_by(current_ability)
|
11
|
+
#
|
12
|
+
# Here only the articles which the user is able to read will be returned.
|
13
|
+
# If the user does not have permission to read any articles then an empty
|
14
|
+
# result is returned. Since this is a scope it can be combined with any
|
15
|
+
# other scopes or pagination.
|
16
|
+
#
|
17
|
+
# An alternative action can optionally be passed as a second argument.
|
18
|
+
#
|
19
|
+
# @articles = Article.accessible_by(current_ability, :update)
|
20
|
+
#
|
21
|
+
# Here only the articles which the user can update are returned.
|
22
|
+
def accessible_by(ability, action = :index)
|
23
|
+
ability.model_adapter(self, action).database_records
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.included(base)
|
28
|
+
base.extend ClassMethods
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/cancan/rule.rb
ADDED
@@ -0,0 +1,142 @@
|
|
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 associations_hash(conditions = @conditions)
|
58
|
+
hash = {}
|
59
|
+
conditions.map do |name, value|
|
60
|
+
hash[name] = associations_hash(value) if value.kind_of? Hash
|
61
|
+
end if conditions.kind_of? Hash
|
62
|
+
hash
|
63
|
+
end
|
64
|
+
|
65
|
+
def attributes_from_conditions
|
66
|
+
attributes = {}
|
67
|
+
@conditions.each do |key, value|
|
68
|
+
attributes[key] = value unless [Array, Range, Hash].include? value.class
|
69
|
+
end if @conditions.kind_of? Hash
|
70
|
+
attributes
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def subject_class?(subject)
|
76
|
+
klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
|
77
|
+
klass == Class || klass == Module
|
78
|
+
end
|
79
|
+
|
80
|
+
def matches_action?(action)
|
81
|
+
@expanded_actions.include?(:manage) || @expanded_actions.include?(action)
|
82
|
+
end
|
83
|
+
|
84
|
+
def matches_subject?(subject)
|
85
|
+
@subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
|
86
|
+
end
|
87
|
+
|
88
|
+
def matches_subject_class?(subject)
|
89
|
+
@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)) }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Checks if the given subject matches the given conditions hash.
|
93
|
+
# This behavior can be overriden by a model adapter by defining two class methods:
|
94
|
+
# override_matching_for_conditions?(subject, conditions) and
|
95
|
+
# matches_conditions_hash?(subject, conditions)
|
96
|
+
def matches_conditions_hash?(subject, conditions = @conditions)
|
97
|
+
if conditions.empty?
|
98
|
+
true
|
99
|
+
else
|
100
|
+
if model_adapter(subject).override_conditions_hash_matching? subject, conditions
|
101
|
+
model_adapter(subject).matches_conditions_hash? subject, conditions
|
102
|
+
else
|
103
|
+
conditions.all? do |name, value|
|
104
|
+
if model_adapter(subject).override_condition_matching? subject, name, value
|
105
|
+
model_adapter(subject).matches_condition? subject, name, value
|
106
|
+
else
|
107
|
+
attribute = subject.send(name)
|
108
|
+
if value.kind_of?(Hash)
|
109
|
+
if attribute.kind_of? Array
|
110
|
+
attribute.any? { |element| matches_conditions_hash? element, value }
|
111
|
+
else
|
112
|
+
!attribute.nil? && matches_conditions_hash?(attribute, value)
|
113
|
+
end
|
114
|
+
elsif value.kind_of?(Array) || value.kind_of?(Range)
|
115
|
+
value.include? attribute
|
116
|
+
else
|
117
|
+
attribute == value
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def nested_subject_matches_conditions?(subject_hash)
|
126
|
+
parent, child = subject_hash.shift
|
127
|
+
matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
|
128
|
+
end
|
129
|
+
|
130
|
+
def call_block_with_all(action, subject, extra_args)
|
131
|
+
if subject.class == Class
|
132
|
+
@block.call(action, subject, nil, *extra_args)
|
133
|
+
else
|
134
|
+
@block.call(action, subject.class, subject, *extra_args)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def model_adapter(subject)
|
139
|
+
ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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 permission to do.
|
15
|
+
# If you pass :manage it will apply to every action. Other common actions here are
|
16
|
+
# :read, :create, :update and :destroy.
|
17
|
+
#
|
18
|
+
# The second argument is the resource the user can perform the action on. If you pass
|
19
|
+
# :all it will apply to every resource. Otherwise pass a Ruby class of the resource.
|
20
|
+
#
|
21
|
+
# The third argument is an optional hash of conditions to further filter the objects.
|
22
|
+
# For example, here the user can only update published articles.
|
23
|
+
#
|
24
|
+
# can :update, Article, :published => true
|
25
|
+
#
|
26
|
+
# See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
|
27
|
+
end
|
28
|
+
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,419 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe CanCan::Ability do
|
4
|
+
before(:each) do
|
5
|
+
@ability = Object.new
|
6
|
+
@ability.extend(CanCan::Ability)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be able to :read anything" do
|
10
|
+
@ability.can :read, :all
|
11
|
+
@ability.can?(:read, String).should be_true
|
12
|
+
@ability.can?(:read, 123).should be_true
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should not have permission to do something it doesn't know about" do
|
16
|
+
@ability.can?(:foodfight, String).should be_false
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should pass true to `can?` when non false/nil is returned in block" do
|
20
|
+
@ability.can :read, :all
|
21
|
+
@ability.can :read, Symbol do |sym|
|
22
|
+
"foo" # TODO test that sym is nil when no instance is passed
|
23
|
+
end
|
24
|
+
@ability.can?(:read, :some_symbol).should == true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should pass nil to a block when no instance is passed" do
|
28
|
+
@ability.can :read, Symbol do |sym|
|
29
|
+
sym.should be_nil
|
30
|
+
true
|
31
|
+
end
|
32
|
+
@ability.can?(:read, Symbol).should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should pass to previous rule, if block returns false or nil" do
|
36
|
+
@ability.can :read, Symbol
|
37
|
+
@ability.can :read, Integer do |i|
|
38
|
+
i < 5
|
39
|
+
end
|
40
|
+
@ability.can :read, Integer do |i|
|
41
|
+
i > 10
|
42
|
+
end
|
43
|
+
@ability.can?(:read, Symbol).should be_true
|
44
|
+
@ability.can?(:read, 11).should be_true
|
45
|
+
@ability.can?(:read, 1).should be_true
|
46
|
+
@ability.can?(:read, 6).should be_false
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not pass class with object if :all objects are accepted" do
|
50
|
+
@ability.can :preview, :all do |object|
|
51
|
+
object.should == 123
|
52
|
+
@block_called = true
|
53
|
+
end
|
54
|
+
@ability.can?(:preview, 123)
|
55
|
+
@block_called.should be_true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not call block when only class is passed, only return true" do
|
59
|
+
@block_called = false
|
60
|
+
@ability.can :preview, :all do |object|
|
61
|
+
@block_called = true
|
62
|
+
end
|
63
|
+
@ability.can?(:preview, Hash).should be_true
|
64
|
+
@block_called.should be_false
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should pass only object for global manage actions" do
|
68
|
+
@ability.can :manage, String do |object|
|
69
|
+
object.should == "foo"
|
70
|
+
@block_called = true
|
71
|
+
end
|
72
|
+
@ability.can?(:stuff, "foo").should
|
73
|
+
@block_called.should be_true
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should alias update or destroy actions to modify action" do
|
77
|
+
@ability.alias_action :update, :destroy, :to => :modify
|
78
|
+
@ability.can :modify, :all
|
79
|
+
@ability.can?(:update, 123).should be_true
|
80
|
+
@ability.can?(:destroy, 123).should be_true
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should allow deeply nested aliased actions" do
|
84
|
+
@ability.alias_action :increment, :to => :sort
|
85
|
+
@ability.alias_action :sort, :to => :modify
|
86
|
+
@ability.can :modify, :all
|
87
|
+
@ability.can?(:increment, 123).should be_true
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should always call block with arguments when passing no arguments to can" do
|
91
|
+
@ability.can do |action, object_class, object|
|
92
|
+
action.should == :foo
|
93
|
+
object_class.should == 123.class
|
94
|
+
object.should == 123
|
95
|
+
@block_called = true
|
96
|
+
end
|
97
|
+
@ability.can?(:foo, 123)
|
98
|
+
@block_called.should be_true
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should pass nil to object when comparing class with can check" do
|
102
|
+
@ability.can do |action, object_class, object|
|
103
|
+
action.should == :foo
|
104
|
+
object_class.should == Hash
|
105
|
+
object.should be_nil
|
106
|
+
@block_called = true
|
107
|
+
end
|
108
|
+
@ability.can?(:foo, Hash)
|
109
|
+
@block_called.should be_true
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should automatically alias index and show into read calls" do
|
113
|
+
@ability.can :read, :all
|
114
|
+
@ability.can?(:index, 123).should be_true
|
115
|
+
@ability.can?(:show, 123).should be_true
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should automatically alias new and edit into create and update respectively" do
|
119
|
+
@ability.can :create, :all
|
120
|
+
@ability.can :update, :all
|
121
|
+
@ability.can?(:new, 123).should be_true
|
122
|
+
@ability.can?(:edit, 123).should be_true
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should not respond to prepare (now using initialize)" do
|
126
|
+
@ability.should_not respond_to(:prepare)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should offer cannot? method which is simply invert of can?" do
|
130
|
+
@ability.cannot?(:tie, String).should be_true
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should be able to specify multiple actions and match any" do
|
134
|
+
@ability.can [:read, :update], :all
|
135
|
+
@ability.can?(:read, 123).should be_true
|
136
|
+
@ability.can?(:update, 123).should be_true
|
137
|
+
@ability.can?(:count, 123).should be_false
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should be able to specify multiple classes and match any" do
|
141
|
+
@ability.can :update, [String, Range]
|
142
|
+
@ability.can?(:update, "foo").should be_true
|
143
|
+
@ability.can?(:update, 1..3).should be_true
|
144
|
+
@ability.can?(:update, 123).should be_false
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should support custom objects in the rule" do
|
148
|
+
@ability.can :read, :stats
|
149
|
+
@ability.can?(:read, :stats).should be_true
|
150
|
+
@ability.can?(:update, :stats).should be_false
|
151
|
+
@ability.can?(:read, :nonstats).should be_false
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should check ancestors of class" do
|
155
|
+
@ability.can :read, Numeric
|
156
|
+
@ability.can?(:read, Integer).should be_true
|
157
|
+
@ability.can?(:read, 1.23).should be_true
|
158
|
+
@ability.can?(:read, "foo").should be_false
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should support 'cannot' method to define what user cannot do" do
|
162
|
+
@ability.can :read, :all
|
163
|
+
@ability.cannot :read, Integer
|
164
|
+
@ability.can?(:read, "foo").should be_true
|
165
|
+
@ability.can?(:read, 123).should be_false
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should pass to previous rule, if block returns false or nil" do
|
169
|
+
@ability.can :read, :all
|
170
|
+
@ability.cannot :read, Integer do |int|
|
171
|
+
int > 10 ? nil : ( int > 5 )
|
172
|
+
end
|
173
|
+
@ability.can?(:read, "foo").should be_true
|
174
|
+
@ability.can?(:read, 3).should be_true
|
175
|
+
@ability.can?(:read, 8).should be_false
|
176
|
+
@ability.can?(:read, 123).should be_true
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should always return `false` for single cannot definition" do
|
180
|
+
@ability.cannot :read, Integer do |int|
|
181
|
+
int > 10 ? nil : ( int > 5 )
|
182
|
+
end
|
183
|
+
@ability.can?(:read, "foo").should be_false
|
184
|
+
@ability.can?(:read, 3).should be_false
|
185
|
+
@ability.can?(:read, 8).should be_false
|
186
|
+
@ability.can?(:read, 123).should be_false
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should pass to previous cannot definition, if block returns false or nil" do
|
190
|
+
@ability.cannot :read, :all
|
191
|
+
@ability.can :read, Integer do |int|
|
192
|
+
int > 10 ? nil : ( int > 5 )
|
193
|
+
end
|
194
|
+
@ability.can?(:read, "foo").should be_false
|
195
|
+
@ability.can?(:read, 3).should be_false
|
196
|
+
@ability.can?(:read, 10).should be_true
|
197
|
+
@ability.can?(:read, 123).should be_false
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should append aliased actions" do
|
201
|
+
@ability.alias_action :update, :to => :modify
|
202
|
+
@ability.alias_action :destroy, :to => :modify
|
203
|
+
@ability.aliased_actions[:modify].should == [:update, :destroy]
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should clear aliased actions" do
|
207
|
+
@ability.alias_action :update, :to => :modify
|
208
|
+
@ability.clear_aliased_actions
|
209
|
+
@ability.aliased_actions[:modify].should be_nil
|
210
|
+
end
|
211
|
+
|
212
|
+
it "should pass additional arguments to block from can?" do
|
213
|
+
@ability.can :read, Integer do |int, x|
|
214
|
+
int > x
|
215
|
+
end
|
216
|
+
@ability.can?(:read, 2, 1).should be_true
|
217
|
+
@ability.can?(:read, 2, 3).should be_false
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should use conditions as third parameter and determine abilities from it" do
|
221
|
+
@ability.can :read, Range, :begin => 1, :end => 3
|
222
|
+
@ability.can?(:read, 1..3).should be_true
|
223
|
+
@ability.can?(:read, 1..4).should be_false
|
224
|
+
@ability.can?(:read, Range).should be_true
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should allow an array of options in conditions hash" do
|
228
|
+
@ability.can :read, Range, :begin => [1, 3, 5]
|
229
|
+
@ability.can?(:read, 1..3).should be_true
|
230
|
+
@ability.can?(:read, 2..4).should be_false
|
231
|
+
@ability.can?(:read, 3..5).should be_true
|
232
|
+
end
|
233
|
+
|
234
|
+
it "should allow a range of options in conditions hash" do
|
235
|
+
@ability.can :read, Range, :begin => 1..3
|
236
|
+
@ability.can?(:read, 1..10).should be_true
|
237
|
+
@ability.can?(:read, 3..30).should be_true
|
238
|
+
@ability.can?(:read, 4..40).should be_false
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should allow nested hashes in conditions hash" do
|
242
|
+
@ability.can :read, Range, :begin => { :to_i => 5 }
|
243
|
+
@ability.can?(:read, 5..7).should be_true
|
244
|
+
@ability.can?(:read, 6..8).should be_false
|
245
|
+
end
|
246
|
+
|
247
|
+
it "should match any element passed in to nesting if it's an array (for has_many associations)" do
|
248
|
+
@ability.can :read, Range, :to_a => { :to_i => 3 }
|
249
|
+
@ability.can?(:read, 1..5).should be_true
|
250
|
+
@ability.can?(:read, 4..6).should be_false
|
251
|
+
end
|
252
|
+
|
253
|
+
it "should not match subjects return nil for methods that must match nested a nested conditions hash" do
|
254
|
+
mock(object_with_foo = Object.new).foo { :bar }
|
255
|
+
@ability.can :read, Array, :first => { :foo => :bar }
|
256
|
+
@ability.can?(:read, [object_with_foo]).should be_true
|
257
|
+
@ability.can?(:read, []).should be_false
|
258
|
+
end
|
259
|
+
|
260
|
+
it "should not stop at cannot definition when comparing class" do
|
261
|
+
@ability.can :read, Range
|
262
|
+
@ability.cannot :read, Range, :begin => 1
|
263
|
+
@ability.can?(:read, 2..5).should be_true
|
264
|
+
@ability.can?(:read, 1..5).should be_false
|
265
|
+
@ability.can?(:read, Range).should be_true
|
266
|
+
end
|
267
|
+
|
268
|
+
it "should stop at cannot definition when no hash is present" do
|
269
|
+
@ability.can :read, :all
|
270
|
+
@ability.cannot :read, Range
|
271
|
+
@ability.can?(:read, 1..5).should be_false
|
272
|
+
@ability.can?(:read, Range).should be_false
|
273
|
+
end
|
274
|
+
|
275
|
+
it "should allow to check ability for Module" do
|
276
|
+
module B; end
|
277
|
+
class A; include B; end
|
278
|
+
@ability.can :read, B
|
279
|
+
@ability.can?(:read, A).should be_true
|
280
|
+
@ability.can?(:read, A.new).should be_true
|
281
|
+
end
|
282
|
+
|
283
|
+
it "should pass nil to a block for ability on Module when no instance is passed" do
|
284
|
+
module B; end
|
285
|
+
class A; include B; end
|
286
|
+
@ability.can :read, B do |sym|
|
287
|
+
sym.should be_nil
|
288
|
+
true
|
289
|
+
end
|
290
|
+
@ability.can?(:read, B).should be_true
|
291
|
+
@ability.can?(:read, A).should be_true
|
292
|
+
end
|
293
|
+
|
294
|
+
it "passing a hash of subjects should check permissions through association" do
|
295
|
+
@ability.can :read, Range, :string => {:length => 3}
|
296
|
+
@ability.can?(:read, "foo" => Range).should be_true
|
297
|
+
@ability.can?(:read, "foobar" => Range).should be_false
|
298
|
+
@ability.can?(:read, 123 => Range).should be_true
|
299
|
+
end
|
300
|
+
|
301
|
+
it "should allow to check ability on Hash-like object" do
|
302
|
+
class Container < Hash; end
|
303
|
+
@ability.can :read, Container
|
304
|
+
@ability.can?(:read, Container.new).should be_true
|
305
|
+
end
|
306
|
+
|
307
|
+
it "should have initial attributes based on hash conditions of 'new' action" do
|
308
|
+
@ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"}
|
309
|
+
@ability.can :create, Range, :bar => 123, :array => %w[skip arrays]
|
310
|
+
@ability.can :new, Range, :baz => "baz", :range => 1..3
|
311
|
+
@ability.cannot :new, Range, :ignore => "me"
|
312
|
+
@ability.attributes_for(:new, Range).should == {:foo => "foo", :bar => 123, :baz => "baz"}
|
313
|
+
end
|
314
|
+
|
315
|
+
it "should raise access denied exception if ability us unauthorized to perform a certain action" do
|
316
|
+
begin
|
317
|
+
@ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!"
|
318
|
+
rescue CanCan::AccessDenied => e
|
319
|
+
e.message.should == "Access denied!"
|
320
|
+
e.action.should == :read
|
321
|
+
e.subject.should == :foo
|
322
|
+
else
|
323
|
+
fail "Expected CanCan::AccessDenied exception to be raised"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
it "should not raise access denied exception if ability is authorized to perform an action and return subject" do
|
328
|
+
@ability.can :read, :foo
|
329
|
+
lambda {
|
330
|
+
@ability.authorize!(:read, :foo).should == :foo
|
331
|
+
}.should_not raise_error
|
332
|
+
end
|
333
|
+
|
334
|
+
it "should know when block is used in conditions" do
|
335
|
+
@ability.can :read, :foo
|
336
|
+
@ability.should_not have_block(:read, :foo)
|
337
|
+
@ability.can :read, :foo do |foo|
|
338
|
+
false
|
339
|
+
end
|
340
|
+
@ability.should have_block(:read, :foo)
|
341
|
+
end
|
342
|
+
|
343
|
+
it "should know when raw sql is used in conditions" do
|
344
|
+
@ability.can :read, :foo
|
345
|
+
@ability.should_not have_raw_sql(:read, :foo)
|
346
|
+
@ability.can :read, :foo, 'false'
|
347
|
+
@ability.should have_raw_sql(:read, :foo)
|
348
|
+
end
|
349
|
+
|
350
|
+
it "should raise access denied exception with default message if not specified" do
|
351
|
+
begin
|
352
|
+
@ability.authorize! :read, :foo
|
353
|
+
rescue CanCan::AccessDenied => e
|
354
|
+
e.default_message = "Access denied!"
|
355
|
+
e.message.should == "Access denied!"
|
356
|
+
else
|
357
|
+
fail "Expected CanCan::AccessDenied exception to be raised"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
it "should determine model adapter class by asking AbstractAdapter" do
|
362
|
+
model_class = Object.new
|
363
|
+
adapter_class = Object.new
|
364
|
+
stub(CanCan::ModelAdapters::AbstractAdapter).adapter_class(model_class) { adapter_class }
|
365
|
+
stub(adapter_class).new(model_class, []) { :adapter_instance }
|
366
|
+
@ability.model_adapter(model_class, :read).should == :adapter_instance
|
367
|
+
end
|
368
|
+
|
369
|
+
it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do
|
370
|
+
lambda {
|
371
|
+
@ability.can :read, Array, :published => true do
|
372
|
+
false
|
373
|
+
end
|
374
|
+
}.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.")
|
375
|
+
end
|
376
|
+
|
377
|
+
describe "unauthorized message" do
|
378
|
+
after(:each) do
|
379
|
+
I18n.backend = nil
|
380
|
+
end
|
381
|
+
|
382
|
+
it "should use action/subject in i18n" do
|
383
|
+
I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}}
|
384
|
+
@ability.unauthorized_message(:update, Array).should == "foo"
|
385
|
+
@ability.unauthorized_message(:update, [1, 2, 3]).should == "foo"
|
386
|
+
@ability.unauthorized_message(:update, :missing).should be_nil
|
387
|
+
end
|
388
|
+
|
389
|
+
it "should use symbol as subject directly" do
|
390
|
+
I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
|
391
|
+
@ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it."
|
392
|
+
end
|
393
|
+
|
394
|
+
it "should fall back to 'manage' and 'all'" do
|
395
|
+
I18n.backend.store_translations :en, :unauthorized => {
|
396
|
+
:manage => {:all => "manage all", :array => "manage array"},
|
397
|
+
:update => {:all => "update all", :array => "update array"}
|
398
|
+
}
|
399
|
+
@ability.unauthorized_message(:update, Array).should == "update array"
|
400
|
+
@ability.unauthorized_message(:update, Hash).should == "update all"
|
401
|
+
@ability.unauthorized_message(:foo, Array).should == "manage array"
|
402
|
+
@ability.unauthorized_message(:foo, Hash).should == "manage all"
|
403
|
+
end
|
404
|
+
|
405
|
+
it "should follow aliased actions" do
|
406
|
+
I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}}
|
407
|
+
@ability.alias_action :update, :to => :modify
|
408
|
+
@ability.unauthorized_message(:update, Array).should == "modify array"
|
409
|
+
@ability.unauthorized_message(:edit, Array).should == "modify array"
|
410
|
+
end
|
411
|
+
|
412
|
+
it "should have variables for action and subject" do
|
413
|
+
I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
|
414
|
+
@ability.unauthorized_message(:update, Array).should == "update array"
|
415
|
+
@ability.unauthorized_message(:update, ArgumentError).should == "update argument error"
|
416
|
+
@ability.unauthorized_message(:edit, 1..3).should == "edit range"
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|