marnen-cancan 2.0.0.alpha.pre.f1cebde51a87be149b4970a3287826bb63c0ac0b

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.
Files changed (41) hide show
  1. checksums.yaml +15 -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 +265 -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 +535 -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 +227 -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 +197 -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