culturecode-cancan 2.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +381 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +108 -0
  6. data/Rakefile +18 -0
  7. data/init.rb +1 -0
  8. data/lib/cancan.rb +13 -0
  9. data/lib/cancan/ability.rb +348 -0
  10. data/lib/cancan/controller_additions.rb +392 -0
  11. data/lib/cancan/controller_resource.rb +266 -0
  12. data/lib/cancan/exceptions.rb +53 -0
  13. data/lib/cancan/inherited_resource.rb +20 -0
  14. data/lib/cancan/matchers.rb +14 -0
  15. data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
  16. data/lib/cancan/model_adapters/active_record_adapter.rb +172 -0
  17. data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
  18. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  19. data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
  20. data/lib/cancan/model_additions.rb +29 -0
  21. data/lib/cancan/rule.rb +178 -0
  22. data/lib/generators/cancan/ability/USAGE +5 -0
  23. data/lib/generators/cancan/ability/ability_generator.rb +16 -0
  24. data/lib/generators/cancan/ability/templates/ability.rb +24 -0
  25. data/lib/generators/cancan/ability/templates/ability_spec.rb +16 -0
  26. data/lib/generators/cancan/ability/templates/ability_test.rb +10 -0
  27. data/spec/README.rdoc +28 -0
  28. data/spec/cancan/ability_spec.rb +541 -0
  29. data/spec/cancan/controller_additions_spec.rb +118 -0
  30. data/spec/cancan/controller_resource_spec.rb +551 -0
  31. data/spec/cancan/exceptions_spec.rb +58 -0
  32. data/spec/cancan/inherited_resource_spec.rb +58 -0
  33. data/spec/cancan/matchers_spec.rb +33 -0
  34. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +278 -0
  35. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +120 -0
  36. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  37. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +226 -0
  38. data/spec/cancan/rule_spec.rb +55 -0
  39. data/spec/matchers.rb +13 -0
  40. data/spec/spec_helper.rb +49 -0
  41. metadata +194 -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,7 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class DefaultAdapter < AbstractAdapter
4
+ # This adapter is used when no matching adapter is found
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,54 @@
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? && rule.base_behavior }
34
+ process_can_rules = @rules.count == rules.count
35
+
36
+ rules.inject(@model_class.all) do |records, rule|
37
+ if process_can_rules && rule.base_behavior
38
+ records.or rule.conditions
39
+ elsif !rule.base_behavior
40
+ records.excludes rule.conditions
41
+ else
42
+ records
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # simplest way to add `accessible_by` to all Mongoid Documents
52
+ module Mongoid::Document::ClassMethods
53
+ include CanCan::ModelAdditions::ClassMethods
54
+ end
@@ -0,0 +1,29 @@
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
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Returns a scope which fetches only the records that the passed ability
9
+ # can perform a given action on. The action defaults to :index. This
10
+ # is usually called from a controller and passed the +current_ability+.
11
+ #
12
+ # @articles = Article.accessible_by(current_ability)
13
+ #
14
+ # Here only the articles which the user is able to read will be returned.
15
+ # If the user does not have permission to read any articles then an empty
16
+ # result is returned. Since this is a scope it can be combined with any
17
+ # other scopes or pagination.
18
+ #
19
+ # An alternative action can optionally be passed as a second argument.
20
+ #
21
+ # @articles = Article.accessible_by(current_ability, :update)
22
+ #
23
+ # Here only the articles which the user can update are returned.
24
+ def accessible_by(ability, action = :index)
25
+ ability.model_adapter(self, action).database_records
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,178 @@
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, :expanded_subjects
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 = nil, subject = nil, *extra_args, &block)
14
+ @match_all = action.nil? && subject.nil?
15
+ @base_behavior = base_behavior
16
+ @actions = [action].flatten
17
+ @subjects = [subject].flatten
18
+ @attributes = [extra_args.shift].flatten if extra_args.first.kind_of?(Symbol) || extra_args.first.kind_of?(Array) && extra_args.first.first.kind_of?(Symbol)
19
+ raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if extra_args.first.kind_of?(Hash) && !block.nil?
20
+ @conditions = extra_args.first || {}
21
+ @block = block
22
+ end
23
+
24
+ # Matches the subject, action, and given attribute. Conditions are not checked here.
25
+ def relevant?(action, subject, attribute)
26
+ subject = subject.values.first if subject.class == Hash
27
+ @match_all || (matches_action?(action) && matches_subject?(subject) && matches_attribute?(attribute))
28
+ end
29
+
30
+ # Matches the block or conditions hash
31
+ def matches_conditions?(action, subject, attribute)
32
+ if @match_all
33
+ call_block_with_all(action, subject, attribute)
34
+ elsif @block && subject_object?(subject)
35
+ @block.arity == 1 ? @block.call(subject) : @block.call(subject, attribute)
36
+ elsif @conditions.kind_of?(Hash) && subject.class == Hash
37
+ nested_subject_matches_conditions?(subject)
38
+ elsif @conditions.kind_of?(Hash) && subject_object?(subject)
39
+ matches_conditions_hash?(subject)
40
+ else
41
+ # Don't stop at "cannot" definitions when there are conditions.
42
+ @conditions.empty? ? true : @base_behavior
43
+ end
44
+ end
45
+
46
+ def only_block?
47
+ !conditions? && !@block.nil?
48
+ end
49
+
50
+ def only_raw_sql?
51
+ @block.nil? && conditions? && !@conditions.kind_of?(Hash)
52
+ end
53
+
54
+ def attributes?
55
+ @attributes.present?
56
+ end
57
+
58
+ def conditions?
59
+ @conditions.present?
60
+ end
61
+
62
+ def instance_conditions?
63
+ @block || conditions?
64
+ end
65
+
66
+ def unmergeable?
67
+ @conditions.respond_to?(:keys) && (! @conditions.keys.first.kind_of? Symbol)
68
+ end
69
+
70
+ def associations_hash(conditions = @conditions)
71
+ hash = {}
72
+ conditions.map do |name, value|
73
+ hash[name] = associations_hash(value) if value.kind_of? Hash
74
+ end if conditions.kind_of? Hash
75
+ hash
76
+ end
77
+
78
+ def attributes_from_conditions
79
+ attributes = {}
80
+ @conditions.each do |key, value|
81
+ attributes[key] = value unless [Array, Range, Hash].include? value.class
82
+ end if @conditions.kind_of? Hash
83
+ attributes
84
+ end
85
+
86
+ def specificity
87
+ specificity = 1
88
+ specificity += 1 if attributes? || conditions?
89
+ specificity += 2 unless base_behavior
90
+ specificity
91
+ end
92
+
93
+ private
94
+
95
+ def subject_object?(subject)
96
+ # klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
97
+ # klass == Class || klass == Module
98
+ !subject.kind_of?(Symbol) && !subject.kind_of?(String)
99
+ end
100
+
101
+ def matches_action?(action)
102
+ @expanded_actions.include?(:access) || @expanded_actions.include?(action.to_sym)
103
+ end
104
+
105
+ def matches_subject?(subject)
106
+ subject = subject_name(subject) if subject_object? subject
107
+ @expanded_subjects.include?(:all) || @expanded_subjects.include?(subject.to_sym) || @expanded_subjects.include?(subject) # || matches_subject_class?(subject)
108
+ end
109
+
110
+ def matches_attribute?(attribute)
111
+ # don't consider attributes in a cannot clause when not matching - this can probably be refactored
112
+ if !@base_behavior && @attributes && attribute.nil?
113
+ false
114
+ else
115
+ @attributes.nil? || attribute.nil? || @attributes.include?(attribute.to_sym)
116
+ end
117
+ end
118
+
119
+ # TODO deperecate this
120
+ def matches_subject_class?(subject)
121
+ @expanded_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)) }
122
+ end
123
+
124
+ # Checks if the given subject matches the given conditions hash.
125
+ # This behavior can be overriden by a model adapter by defining two class methods:
126
+ # override_matching_for_conditions?(subject, conditions) and
127
+ # matches_conditions_hash?(subject, conditions)
128
+ def matches_conditions_hash?(subject, conditions = @conditions)
129
+ if conditions.empty?
130
+ true
131
+ else
132
+ if model_adapter(subject).override_conditions_hash_matching? subject, conditions
133
+ model_adapter(subject).matches_conditions_hash? subject, conditions
134
+ else
135
+ conditions.all? do |name, value|
136
+ if model_adapter(subject).override_condition_matching? subject, name, value
137
+ model_adapter(subject).matches_condition? subject, name, value
138
+ else
139
+ attribute = subject.send(name)
140
+ if value.kind_of?(Hash)
141
+ if attribute.kind_of? Array
142
+ attribute.any? { |element| matches_conditions_hash? element, value }
143
+ else
144
+ attribute && matches_conditions_hash?(attribute, value)
145
+ end
146
+ elsif value.kind_of?(Enumerable)
147
+ value.include? attribute
148
+ else
149
+ attribute == value
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def nested_subject_matches_conditions?(subject_hash)
158
+ parent, child = subject_hash.first
159
+ matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
160
+ end
161
+
162
+ def call_block_with_all(action, subject, attribute)
163
+ if subject_object? subject
164
+ @block.call(action, subject_name(subject), subject, attribute)
165
+ else
166
+ @block.call(action, subject, nil, attribute)
167
+ end
168
+ end
169
+
170
+ def subject_name(subject)
171
+ subject.class.to_s.underscore.pluralize.to_sym
172
+ end
173
+
174
+ def model_adapter(subject)
175
+ CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_object?(subject) ? subject.class : subject)
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,5 @@
1
+ Description:
2
+ The cancan:ability generator creates an Ability class in the models
3
+ directory. You can move this file anywhere you want as long as it
4
+ is in the load path. A test/spec file is also generated depending
5
+ on if a spec directory exists.
@@ -0,0 +1,16 @@
1
+ module Cancan
2
+ module Generators
3
+ class AbilityGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ def generate_ability
7
+ copy_file "ability.rb", "app/models/ability.rb"
8
+ if File.exist?(File.join(destination_root, "spec"))
9
+ copy_file "ability_spec.rb", "spec/models/ability_spec.rb"
10
+ else
11
+ copy_file "ability_test.rb", "test/unit/ability_test.rb"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ class Ability
2
+ include CanCan::Ability
3
+
4
+ def initialize(user)
5
+ # Define abilities for the passed in (current) user. For example:
6
+ #
7
+ # if user
8
+ # can :access, :all
9
+ # else
10
+ # can :access, :home
11
+ # can :create, [:users, :sessions]
12
+ # end
13
+ #
14
+ # Here if there is a user he will be able to perform any action on any controller.
15
+ # If someone is not logged in he can only access the home, users, and sessions controllers.
16
+ #
17
+ # The first argument to `can` is the action the user can perform. The second argument
18
+ # is the controller name they can perform that action on. You can pass :access and :all
19
+ # to represent any action and controller respectively. Passing an array to either of
20
+ # these will grant permission on each item in the array.
21
+ #
22
+ # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ require "spec_helper"
2
+ require "cancan/matchers"
3
+
4
+ describe Ability do
5
+ describe "as guest" do
6
+ before(:each) do
7
+ @ability = Ability.new(nil)
8
+ end
9
+
10
+ it "can only create a user" do
11
+ # Define what a guest can and cannot do
12
+ # @ability.should be_able_to(:create, :users)
13
+ # @ability.should_not be_able_to(:update, :users)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ require "test_helper"
2
+
3
+ class AbilityTest < ActiveSupport::TestCase
4
+ def guest_can_only_create_user
5
+ ability = Ability.new(nil)
6
+ # Define what a guest can and cannot do
7
+ # assert ability.can?(:create, :users)
8
+ # assert ability.cannot?(:update, :users)
9
+ end
10
+ end
@@ -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,541 @@
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
+
10
+ # Basic Action & Subject
11
+
12
+ it "allows access to only what is defined" do
13
+ @ability.can?(:paint, :fences).should be_false
14
+ @ability.can :paint, :fences
15
+ @ability.can?(:paint, :fences).should be_true
16
+ @ability.can?(:wax, :fences).should be_false
17
+ @ability.can?(:paint, :cars).should be_false
18
+ end
19
+
20
+ it "allows access to everything when :access, :all is used" do
21
+ @ability.can?(:paint, :fences).should be_false
22
+ @ability.can :access, :all
23
+ @ability.can?(:paint, :fences).should be_true
24
+ @ability.can?(:wax, :fences).should be_true
25
+ @ability.can?(:paint, :cars).should be_true
26
+ end
27
+
28
+ it "allows access to multiple actions and subjects" do
29
+ @ability.can [:paint, :sand], [:fences, :decks]
30
+ @ability.can?(:paint, :fences).should be_true
31
+ @ability.can?(:sand, :fences).should be_true
32
+ @ability.can?(:paint, :decks).should be_true
33
+ @ability.can?(:sand, :decks).should be_true
34
+ @ability.can?(:wax, :fences).should be_false
35
+ @ability.can?(:paint, :cars).should be_false
36
+ end
37
+
38
+ it "allows strings instead of symbols in ability check" do
39
+ @ability.can :paint, :fences
40
+ @ability.can?("paint", "fences").should be_true
41
+ end
42
+
43
+
44
+ # Aliases
45
+
46
+ it "has default index, show, new, update, delete aliases" do
47
+ @ability.can :read, :projects
48
+ @ability.can?(:index, :projects).should be_true
49
+ @ability.can?(:show, :projects).should be_true
50
+ @ability.can :create, :projects
51
+ @ability.can?(:new, :projects).should be_true
52
+ @ability.can :update, :projects
53
+ @ability.can?(:edit, :projects).should be_true
54
+ @ability.can :destroy, :projects
55
+ @ability.can?(:delete, :projects).should be_true
56
+ end
57
+
58
+ it "follows deep action aliases" do
59
+ @ability.alias_action :update, :destroy, :to => :modify
60
+ @ability.can :modify, :projects
61
+ @ability.can?(:update, :projects).should be_true
62
+ @ability.can?(:destroy, :projects).should be_true
63
+ @ability.can?(:edit, :projects).should be_true
64
+ end
65
+
66
+ it "adds up action aliases" do
67
+ @ability.alias_action :update, :to => :modify
68
+ @ability.alias_action :destroy, :to => :modify
69
+ @ability.can :modify, :projects
70
+ @ability.can?(:update, :projects).should be_true
71
+ @ability.can?(:destroy, :projects).should be_true
72
+ end
73
+
74
+ it "follows deep subject aliases" do
75
+ @ability.alias_subject :mammals, :to => :animals
76
+ @ability.alias_subject :cats, :to => :mammals
77
+ @ability.can :pet, :animals
78
+ @ability.can?(:pet, :mammals).should be_true
79
+ end
80
+
81
+ it "clears current and default aliases" do
82
+ @ability.alias_action :update, :destroy, :to => :modify
83
+ @ability.clear_aliases
84
+ @ability.can :modify, :projects
85
+ @ability.can?(:update, :projects).should be_false
86
+ @ability.can :read, :projects
87
+ @ability.can?(:show, :projects).should be_false
88
+ end
89
+
90
+
91
+ # Hash Conditions
92
+
93
+ it "maps object to pluralized subject name" do
94
+ @ability.can :read, :ranges
95
+ @ability.can?(:read, :ranges).should be_true
96
+ @ability.can?(:read, 1..3).should be_true
97
+ @ability.can?(:read, 123).should be_false
98
+ end
99
+
100
+ it "checks conditions hash on instances only" do
101
+ @ability.can :read, :ranges, :begin => 1
102
+ @ability.can?(:read, :ranges).should be_true
103
+ @ability.can?(:read, 1..3).should be_true
104
+ @ability.can?(:read, 2..4).should be_false
105
+ end
106
+
107
+ it "checks conditions on both rules and matches either one" do
108
+ @ability.can :read, :ranges, :begin => 1
109
+ @ability.can :read, :ranges, :begin => 2
110
+ @ability.can?(:read, 1..3).should be_true
111
+ @ability.can?(:read, 2..4).should be_true
112
+ @ability.can?(:read, 3..5).should be_false
113
+ end
114
+
115
+ it "checks an array of options in conditions hash" do
116
+ @ability.can :read, :ranges, :begin => [1, 3, 5]
117
+ @ability.can?(:read, 1..3).should be_true
118
+ @ability.can?(:read, 2..4).should be_false
119
+ @ability.can?(:read, 3..5).should be_true
120
+ end
121
+
122
+ it "checks a range of options in conditions hash" do
123
+ @ability.can :read, :ranges, :begin => 1..3
124
+ @ability.can?(:read, 1..10).should be_true
125
+ @ability.can?(:read, 3..30).should be_true
126
+ @ability.can?(:read, 4..40).should be_false
127
+ end
128
+
129
+ it "checks nested conditions hash" do
130
+ @ability.can :read, :ranges, :begin => { :to_i => 5 }
131
+ @ability.can?(:read, 5..7).should be_true
132
+ @ability.can?(:read, 6..8).should be_false
133
+ end
134
+
135
+ it "matches any element passed in to nesting if it's an array (for has_many associations)" do
136
+ @ability.can :read, :ranges, :to_a => { :to_i => 3 }
137
+ @ability.can?(:read, 1..5).should be_true
138
+ @ability.can?(:read, 4..6).should be_false
139
+ end
140
+
141
+ it "takes presedence over rule defined without a condition" do
142
+ @ability.can :read, :ranges
143
+ @ability.can :read, :ranges, :begin => 1
144
+ @ability.can?(:read, 1..5).should be_true
145
+ @ability.can?(:read, 4..6).should be_false
146
+ end
147
+
148
+
149
+ # Block Conditions
150
+
151
+ it "executes block passing object only when instance is used" do
152
+ @ability.can :read, :ranges do |range|
153
+ range.begin == 5
154
+ end
155
+ @ability.can?(:read, :ranges).should be_true
156
+ @ability.can?(:read, 5..7).should be_true
157
+ @ability.can?(:read, 6..8).should be_false
158
+ end
159
+
160
+ it "returns true when other object is returned in block" do
161
+ @ability.can :read, :ranges do |range|
162
+ "foo"
163
+ end
164
+ @ability.can?(:read, 5..7).should be_true
165
+ end
166
+
167
+ it "passes to previous rule when block returns false" do
168
+ @ability.can :read, :fixnums do |i|
169
+ i < 5
170
+ end
171
+ @ability.can :read, :fixnums do |i|
172
+ i > 10
173
+ end
174
+ @ability.can?(:read, 11).should be_true
175
+ @ability.can?(:read, 1).should be_true
176
+ @ability.can?(:read, 6).should be_false
177
+ end
178
+
179
+ it "calls block passing arguments when no arguments are given to can" do
180
+ @ability.can do |action, subject, object|
181
+ action.should == :read
182
+ subject.should == :ranges
183
+ object.should == (2..4)
184
+ @block_called = true
185
+ end
186
+ @ability.can?(:read, 2..4)
187
+ @block_called.should be_true
188
+ end
189
+
190
+ it "raises an error when attempting to use a block with a hash condition since it's not likely what they want" do
191
+ lambda {
192
+ @ability.can :read, :ranges, :published => true do
193
+ false
194
+ end
195
+ }.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read ranges ability. Use either one.")
196
+ end
197
+
198
+ it "does not raise an error when attempting to use a block with an array of SQL conditions" do
199
+ lambda {
200
+ @ability.can :read, :ranges, ["published = ?", true] do
201
+ false
202
+ end
203
+ }.should_not raise_error(CanCan::Error)
204
+ end
205
+
206
+
207
+ # Attributes
208
+
209
+ it "allows permission on attributes" do
210
+ @ability.can :update, :users, :name
211
+ @ability.can :update, :users, [:email, :age]
212
+ @ability.can?(:update, :users, :name).should be_true
213
+ @ability.can?(:update, :users, :email).should be_true
214
+ @ability.can?(:update, :users, :password).should be_false
215
+ end
216
+
217
+ it "allows permission on all attributes when none are given" do
218
+ @ability.can :update, :users
219
+ @ability.can?(:update, :users, :password).should be_true
220
+ end
221
+
222
+ it "allows strings when chekcing attributes" do
223
+ @ability.can :update, :users, :name
224
+ @ability.can?(:update, :users, "name").should be_true
225
+ end
226
+
227
+ it "combines attribute check with conditions hash" do
228
+ @ability.can :update, :ranges, :begin => 1
229
+ @ability.can :update, :ranges, :name, :begin => 2
230
+ @ability.can?(:update, 1..3, :foobar).should be_true
231
+ @ability.can?(:update, 2..4, :foobar).should be_false
232
+ @ability.can?(:update, 2..4, :name).should be_true
233
+ @ability.can?(:update, 3..5, :name).should be_false
234
+ end
235
+
236
+ it "passes attribute to block and nil if no attribute checked" do
237
+ @ability.can :update, :ranges do |range, attribute|
238
+ attribute == :name
239
+ end
240
+ @ability.can?(:update, 1..3, :name).should be_true
241
+ @ability.can?(:update, 2..4).should be_false
242
+ end
243
+
244
+ it "passes attribute to block for global can definition" do
245
+ @ability.can do |action, subject, object, attribute|
246
+ attribute == :name
247
+ end
248
+ @ability.can?(:update, 1..3, :name).should be_true
249
+ @ability.can?(:update, 2..4).should be_false
250
+ end
251
+
252
+
253
+ # Checking if Fully Authorized
254
+
255
+ it "is not fully authorized when no authorize! call is made" do
256
+ @ability.can :update, :ranges, :begin => 1
257
+ @ability.can?(:update, :ranges).should be_true
258
+ @ability.should_not be_fully_authorized(:update, :ranges)
259
+ end
260
+
261
+ it "is fully authorized when calling authorize! with a matching action and subject" do
262
+ @ability.can :update, :ranges
263
+ @ability.authorize! :update, :ranges
264
+ @ability.should be_fully_authorized(:update, :ranges)
265
+ @ability.should_not be_fully_authorized(:create, :ranges)
266
+ end
267
+
268
+ it "is fully authorized when marking action and subject as such" do
269
+ @ability.fully_authorized! :update, :ranges
270
+ @ability.should be_fully_authorized(:update, :ranges)
271
+ end
272
+
273
+ it "is not fully authorized when a conditions hash exists but no instance is used" do
274
+ @ability.can :update, :ranges, :begin => 1
275
+ @ability.authorize! :update, :ranges
276
+ @ability.should_not be_fully_authorized(:update, :ranges)
277
+ @ability.authorize! "update", "ranges"
278
+ @ability.should_not be_fully_authorized(:update, :ranges)
279
+ @ability.authorize! :update, 1..3
280
+ @ability.should be_fully_authorized(:update, :ranges)
281
+ end
282
+
283
+ it "is not fully authorized when a block exists but no instance is used" do
284
+ @ability.can :update, :ranges do |range|
285
+ range.begin == 1
286
+ end
287
+ @ability.authorize! :update, :ranges
288
+ @ability.should_not be_fully_authorized(:update, :ranges)
289
+ @ability.authorize! :update, 1..3
290
+ @ability.should be_fully_authorized(:update, :ranges)
291
+ end
292
+
293
+ it "should accept a set as a condition value" do
294
+ object_with_foo_2 = Object.new
295
+ object_with_foo_2.should_receive(:foo) { 2 }
296
+ object_with_foo_3 = Object.new
297
+ object_with_foo_3.should_receive(:foo) { 3 }
298
+ @ability.can :read, :objects, :foo => [1, 2, 5].to_set
299
+ @ability.can?(:read, object_with_foo_2).should be_true
300
+ @ability.can?(:read, object_with_foo_3).should be_false
301
+ end
302
+
303
+ it "does not match subjects return nil for methods that must match nested a nested conditions hash" do
304
+ object_with_foo = Object.new
305
+ object_with_foo.should_receive(:foo) { :bar }
306
+ @ability.can :read, :arrays, :first => { :foo => :bar }
307
+ @ability.can?(:read, [object_with_foo]).should be_true
308
+ @ability.can?(:read, []).should be_false
309
+ end
310
+
311
+ it "is not fully authorized when attributes are required but not checked in update/create actions" do
312
+ @ability.can :access, :users, :name
313
+ @ability.authorize! :update, :users
314
+ @ability.should_not be_fully_authorized(:update, :users)
315
+ @ability.authorize! :create, :users
316
+ @ability.should_not be_fully_authorized(:create, :users)
317
+ @ability.authorize! :create, :users, :name
318
+ @ability.should be_fully_authorized(:create, :users)
319
+ @ability.authorize! :destroy, :users
320
+ @ability.should be_fully_authorized(:destroy, :users)
321
+ end
322
+
323
+ it "marks as fully authorized when authorizing with strings instead of symbols" do
324
+ @ability.fully_authorized! "update", "ranges"
325
+ @ability.should be_fully_authorized(:update, :ranges)
326
+ @ability.should be_fully_authorized("update", "ranges")
327
+ @ability.can :update, :users
328
+ @ability.authorize! "update", "users"
329
+ @ability.should be_fully_authorized(:update, :users)
330
+ end
331
+
332
+
333
+ # Cannot
334
+
335
+ it "offers cannot? method which inverts can?" do
336
+ @ability.cannot?(:wax, :cars).should be_true
337
+ end
338
+
339
+ it "supports 'cannot' method to define what user cannot do" do
340
+ @ability.can :read, :all
341
+ @ability.cannot :read, :ranges
342
+ @ability.can?(:read, :books).should be_true
343
+ @ability.can?(:read, 1..3).should be_false
344
+ @ability.can?(:read, :ranges).should be_false
345
+ end
346
+
347
+ it "passes to previous rule if cannot check returns false" do
348
+ @ability.can :read, :all
349
+ @ability.cannot :read, :ranges, :begin => 3
350
+ @ability.cannot :read, :ranges do |range|
351
+ range.begin == 5
352
+ end
353
+ @ability.can?(:read, :books).should be_true
354
+ @ability.can?(:read, 2..4).should be_true
355
+ @ability.can?(:read, 3..7).should be_false
356
+ @ability.can?(:read, 5..9).should be_false
357
+ end
358
+
359
+ it "rejects permission only to a given attribute" do
360
+ @ability.can :update, :books
361
+ @ability.cannot :update, :books, :author
362
+ @ability.can?(:update, :books).should be_true
363
+ @ability.can?(:update, :books, :author).should be_false
364
+ end
365
+
366
+ # Hash Association
367
+
368
+ it "checks permission through association when hash is passed as subject" do
369
+ @ability.can :read, :books, :range => {:begin => 3}
370
+ @ability.can?(:read, (1..4) => :books).should be_false
371
+ @ability.can?(:read, (3..5) => :books).should be_true
372
+ @ability.can?(:read, 123 => :books).should be_true
373
+ end
374
+
375
+ it "checks permissions on association hash with multiple rules" do
376
+ @ability.can :read, :books, :range => {:begin => 3}
377
+ @ability.can :read, :books, :range => {:end => 6}
378
+ @ability.can?(:read, (1..4) => :books).should be_false
379
+ @ability.can?(:read, (3..5) => :books).should be_true
380
+ @ability.can?(:read, (1..6) => :books).should be_true
381
+ @ability.can?(:read, 123 => :books).should be_true
382
+ end
383
+
384
+ it "checks ability on hash subclass" do
385
+ class Container < Hash; end
386
+ @ability.can :read, :containers
387
+ @ability.can?(:read, Container.new).should be_true
388
+ end
389
+
390
+
391
+ # Initial Attributes
392
+
393
+ it "has initial attributes based on hash conditions for a given action" do
394
+ @ability.can :access, :ranges, :foo => "foo", :hash => {:skip => "hashes"}
395
+ @ability.can :create, :ranges, :bar => 123, :array => %w[skip arrays]
396
+ @ability.can :new, :ranges, :baz => "baz", :range => 1..3
397
+ @ability.cannot :new, :ranges, :ignore => "me"
398
+ @ability.attributes_for(:new, :ranges).should == {:foo => "foo", :bar => 123, :baz => "baz"}
399
+ end
400
+
401
+
402
+ # Unauthorized Exception
403
+
404
+ it "raises CanCan::Unauthorized when calling authorize! on unauthorized action" do
405
+ begin
406
+ @ability.authorize! :read, :books, :message => "Access denied!"
407
+ rescue CanCan::Unauthorized => e
408
+ e.message.should == "Access denied!"
409
+ e.action.should == :read
410
+ e.subject.should == :books
411
+ else
412
+ fail "Expected CanCan::Unauthorized exception to be raised"
413
+ end
414
+ end
415
+
416
+ it "does not raise access denied exception if ability is authorized to perform an action and return subject" do
417
+ @ability.can :read, :foo
418
+ lambda {
419
+ @ability.authorize!(:read, :foo).should == :foo
420
+ }.should_not raise_error
421
+ end
422
+
423
+ it "knows when block is used in conditions" do
424
+ @ability.can :read, :foo
425
+ @ability.should_not have_block(:read, :foo)
426
+ @ability.can :read, :foo do |foo|
427
+ false
428
+ end
429
+ @ability.should have_block(:read, :foo)
430
+ end
431
+
432
+ it "knows when raw sql is used in conditions" do
433
+ @ability.can :read, :foo
434
+ @ability.should_not have_raw_sql(:read, :foo)
435
+ @ability.can :read, :foo, 'false'
436
+ @ability.should have_raw_sql(:read, :foo)
437
+ end
438
+
439
+ it "raises access denied exception with default message if not specified" do
440
+ begin
441
+ @ability.authorize! :read, :books
442
+ rescue CanCan::Unauthorized => e
443
+ e.default_message = "Access denied!"
444
+ e.message.should == "Access denied!"
445
+ else
446
+ fail "Expected CanCan::Unauthorized exception to be raised"
447
+ end
448
+ end
449
+
450
+ it "does not raise access denied exception if ability is authorized to perform an action and return subject" do
451
+ @ability.can :read, :books
452
+ lambda {
453
+ @ability.authorize!(:read, :books).should == :books
454
+ }.should_not raise_error
455
+ end
456
+
457
+
458
+ # Determining Kind of Conditions
459
+
460
+ it "knows when a block is used for conditions" do
461
+ @ability.can :read, :books
462
+ @ability.should_not have_block(:read, :books)
463
+ @ability.can :read, :books do |foo|
464
+ false
465
+ end
466
+ @ability.should have_block(:read, :books)
467
+ end
468
+
469
+ it "knows when raw sql is used for conditions" do
470
+ @ability.can :read, :books
471
+ @ability.should_not have_raw_sql(:read, :books)
472
+ @ability.can :read, :books, 'false'
473
+ @ability.should have_raw_sql(:read, :books)
474
+ end
475
+
476
+ it "determines model adapter class by asking AbstractAdapter" do
477
+ model_class = Object.new
478
+ adapter_class = Object.new
479
+ CanCan::ModelAdapters::AbstractAdapter.stub(:adapter_class).with(model_class) { adapter_class }
480
+ adapter_class.stub(:new).with(model_class, []) { :adapter_instance }
481
+ @ability.model_adapter(model_class, :read).should == :adapter_instance
482
+ end
483
+
484
+
485
+ # Unauthorized I18n Message
486
+
487
+ describe "unauthorized message" do
488
+ after(:each) do
489
+ I18n.backend = nil
490
+ end
491
+
492
+ it "uses action/subject in i18n" do
493
+ I18n.backend.store_translations :en, :unauthorized => {:update => {:ranges => "update ranges"}}
494
+ @ability.unauthorized_message(:update, :ranges).should == "update ranges"
495
+ @ability.unauthorized_message(:update, 2..4).should == "update ranges"
496
+ @ability.unauthorized_message(:update, :missing).should be_nil
497
+ end
498
+
499
+ it "uses symbol as subject directly" do
500
+ I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
501
+ @ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it."
502
+ end
503
+
504
+ it "falls back to 'access' and 'all'" do
505
+ I18n.backend.store_translations :en, :unauthorized => {
506
+ :access => {:all => "access all", :ranges => "access ranges"},
507
+ :update => {:all => "update all", :ranges => "update ranges"}
508
+ }
509
+ @ability.unauthorized_message(:update, :ranges).should == "update ranges"
510
+ @ability.unauthorized_message(:update, :hashes).should == "update all"
511
+ @ability.unauthorized_message(:create, :ranges).should == "access ranges"
512
+ @ability.unauthorized_message(:create, :hashes).should == "access all"
513
+ end
514
+
515
+ it "follows aliases" do
516
+ I18n.backend.store_translations :en, :unauthorized => {:modify => {:ranges => "modify ranges"}}
517
+ @ability.alias_action :update, :to => :modify
518
+ @ability.alias_subject :areas, :to => :ranges
519
+ @ability.unauthorized_message(:update, :areas).should == "modify ranges"
520
+ @ability.unauthorized_message(:edit, :ranges).should == "modify ranges"
521
+ end
522
+
523
+ it "has variables for action and subject" do
524
+ I18n.backend.store_translations :en, :unauthorized => {:access => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
525
+ @ability.unauthorized_message(:update, :ranges).should == "update ranges"
526
+ @ability.unauthorized_message(:edit, 1..3).should == "edit ranges"
527
+ # @ability.unauthorized_message(:update, ArgumentError).should == "update argument error"
528
+ end
529
+ end
530
+
531
+ it "merges the rules from another ability" do
532
+ @ability.can :use, :tools
533
+ another_ability = Object.new
534
+ another_ability.extend(CanCan::Ability)
535
+ another_ability.can :use, :search
536
+
537
+ @ability.merge(another_ability)
538
+ @ability.can?(:use, :search).should be_true
539
+ @ability.send(:rules).size.should == 2
540
+ end
541
+ end