cancan 1.4.1 → 1.5.0.beta1
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 +21 -0
- data/Gemfile +17 -0
- data/LICENSE +1 -1
- data/README.rdoc +16 -77
- data/Rakefile +8 -0
- data/lib/cancan.rb +8 -3
- data/lib/cancan/ability.rb +24 -26
- data/lib/cancan/controller_additions.rb +50 -0
- data/lib/cancan/controller_resource.rb +33 -15
- data/lib/cancan/exceptions.rb +3 -0
- data/lib/cancan/model_adapters/abstract_adapter.rb +40 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +119 -0
- data/lib/cancan/model_adapters/data_mapper_adapter.rb +33 -0
- data/lib/cancan/model_adapters/default_adapter.rb +7 -0
- data/lib/cancan/model_adapters/mongoid_adapter.rb +41 -0
- data/lib/cancan/{active_record_additions.rb → model_additions.rb} +5 -16
- data/lib/cancan/{can_definition.rb → rule.rb} +29 -25
- 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 +11 -3
- data/spec/cancan/controller_additions_spec.rb +30 -0
- data/spec/cancan/controller_resource_spec.rb +68 -2
- data/spec/cancan/inherited_resource_spec.rb +3 -1
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +185 -0
- data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +115 -0
- data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +168 -0
- data/spec/cancan/rule_spec.rb +39 -0
- data/spec/spec_helper.rb +2 -24
- metadata +26 -17
- data/lib/cancan/query.rb +0 -97
- data/spec/cancan/active_record_additions_spec.rb +0 -75
- data/spec/cancan/can_definition_spec.rb +0 -57
- data/spec/cancan/query_spec.rb +0 -107
data/lib/cancan/exceptions.rb
CHANGED
@@ -2,6 +2,9 @@ module CanCan
|
|
2
2
|
# A general CanCan exception
|
3
3
|
class Error < StandardError; end
|
4
4
|
|
5
|
+
# Raised when behavior is not implemented, usually used in an abstract class.
|
6
|
+
class NotImplemented < Error; end
|
7
|
+
|
5
8
|
# Raised when removed code is called, an alternative solution is provided in message.
|
6
9
|
class ImplementationRemoved < Error; end
|
7
10
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class AbstractAdapter
|
4
|
+
def self.inherited(subclass)
|
5
|
+
@subclasses ||= []
|
6
|
+
@subclasses << subclass
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.adapter_class(model_class)
|
10
|
+
@subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultAdapter
|
11
|
+
end
|
12
|
+
|
13
|
+
# Used to determine if the given adapter should be used for the passed in class.
|
14
|
+
def self.for_class?(member_class)
|
15
|
+
false # override in subclass
|
16
|
+
end
|
17
|
+
|
18
|
+
# Used to determine if this model adapter will override the matching behavior for a hash of conditions.
|
19
|
+
# If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
|
20
|
+
def self.override_conditions_hash_matching?(subject, conditions)
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
# Override if override_conditions_hash_matching? returns true
|
25
|
+
def self.matches_conditions_hash?(subject, conditions)
|
26
|
+
raise NotImplemented, "This model adapter does not support matching on a conditions hash."
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(model_class, rules)
|
30
|
+
@model_class = model_class
|
31
|
+
@rules = rules
|
32
|
+
end
|
33
|
+
|
34
|
+
def database_records
|
35
|
+
# This should be overridden in a subclass to return records which match @rules
|
36
|
+
raise NotImplemented, "This model adapter does not support fetching records from the database."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class ActiveRecordAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(model_class)
|
5
|
+
model_class <= ActiveRecord::Base
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
9
|
+
# method directly, but instead go through ModelAdditions#accessible_by.
|
10
|
+
#
|
11
|
+
# If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
|
12
|
+
#
|
13
|
+
# can :manage, User, :id => 1
|
14
|
+
# query(:manage, User).conditions # => { :id => 1 }
|
15
|
+
#
|
16
|
+
# If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
|
17
|
+
#
|
18
|
+
# can :manage, User, :id => 1
|
19
|
+
# can :manage, User, :manager_id => 1
|
20
|
+
# cannot :manage, User, :self_managed => true
|
21
|
+
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
22
|
+
#
|
23
|
+
def conditions
|
24
|
+
if @rules.size == 1 && @rules.first.base_behavior
|
25
|
+
# Return the conditions directly if there's just one definition
|
26
|
+
tableized_conditions(@rules.first.conditions).dup
|
27
|
+
else
|
28
|
+
@rules.reverse.inject(false_sql) do |sql, rule|
|
29
|
+
merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def tableized_conditions(conditions)
|
35
|
+
return conditions unless conditions.kind_of? Hash
|
36
|
+
conditions.inject({}) do |result_hash, (name, value)|
|
37
|
+
if value.kind_of? Hash
|
38
|
+
name = @model_class.reflect_on_association(name).table_name
|
39
|
+
value = tableized_conditions(value)
|
40
|
+
end
|
41
|
+
result_hash[name] = value
|
42
|
+
result_hash
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the associations used in conditions for the :joins option of a search.
|
47
|
+
# See ModelAdditions#accessible_by
|
48
|
+
def joins
|
49
|
+
joins_hash = {}
|
50
|
+
@rules.each do |rule|
|
51
|
+
merge_joins(joins_hash, rule.associations_hash)
|
52
|
+
end
|
53
|
+
clean_joins(joins_hash) unless joins_hash.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def database_records
|
57
|
+
if @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
|
58
|
+
@model_class.where(conditions).joins(joins)
|
59
|
+
else
|
60
|
+
@model_class.scoped(:conditions => conditions, :joins => joins)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def merge_conditions(sql, conditions_hash, behavior)
|
67
|
+
if conditions_hash.blank?
|
68
|
+
behavior ? true_sql : false_sql
|
69
|
+
else
|
70
|
+
conditions = sanitize_sql(conditions_hash)
|
71
|
+
case sql
|
72
|
+
when true_sql
|
73
|
+
behavior ? true_sql : "not (#{conditions})"
|
74
|
+
when false_sql
|
75
|
+
behavior ? conditions : false_sql
|
76
|
+
else
|
77
|
+
behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def false_sql
|
83
|
+
sanitize_sql(['?=?', true, false])
|
84
|
+
end
|
85
|
+
|
86
|
+
def true_sql
|
87
|
+
sanitize_sql(['?=?', true, true])
|
88
|
+
end
|
89
|
+
|
90
|
+
def sanitize_sql(conditions)
|
91
|
+
@model_class.send(:sanitize_sql, conditions)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Takes two hashes and does a deep merge.
|
95
|
+
def merge_joins(base, add)
|
96
|
+
add.each do |name, nested|
|
97
|
+
if base[name].is_a?(Hash) && !nested.empty?
|
98
|
+
merge_joins(base[name], nested)
|
99
|
+
else
|
100
|
+
base[name] = nested
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Removes empty hashes and moves everything into arrays.
|
106
|
+
def clean_joins(joins_hash)
|
107
|
+
joins = []
|
108
|
+
joins_hash.each do |name, nested|
|
109
|
+
joins << (nested.empty? ? name : {name => clean_joins(nested)})
|
110
|
+
end
|
111
|
+
joins
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
ActiveRecord::Base.class_eval do
|
118
|
+
include CanCan::ModelAdditions
|
119
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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.override_conditions_hash_matching?(subject, conditions)
|
9
|
+
conditions.any? { |k,v| !k.kind_of?(Symbol) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.matches_conditions_hash?(subject, conditions)
|
13
|
+
subject.class.all(:conditions => conditions).include?(subject) # TODO don't use a database query here for performance and other instances
|
14
|
+
end
|
15
|
+
|
16
|
+
def database_records
|
17
|
+
scope = @model_class.all(:conditions => ["0=1"])
|
18
|
+
conditions.each do |condition|
|
19
|
+
scope += @model_class.all(:conditions => condition)
|
20
|
+
end
|
21
|
+
scope
|
22
|
+
end
|
23
|
+
|
24
|
+
def conditions
|
25
|
+
@rules.map(&:conditions)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
DataMapper::Model.class_eval do
|
32
|
+
include CanCan::ModelAdditions::ClassMethods
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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? { |k,v| !k.kind_of?(Symbol) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.matches_conditions_hash?(subject, conditions)
|
13
|
+
# To avoid hitting the db, retrieve the raw Mongo selector from
|
14
|
+
# the Mongoid Criteria and use Mongoid::Matchers#matches?
|
15
|
+
subject.matches?( subject.class.where(conditions).selector )
|
16
|
+
end
|
17
|
+
|
18
|
+
def database_records
|
19
|
+
@model_class.where(conditions)
|
20
|
+
end
|
21
|
+
|
22
|
+
def conditions
|
23
|
+
if @rules.size == 0
|
24
|
+
false_query
|
25
|
+
else
|
26
|
+
@rules.first.conditions
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def false_query
|
31
|
+
# this query is sure to return no results
|
32
|
+
{:_id => {'$exists' => false, '$type' => 7}} # type 7 is an ObjectID (default for _id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# simplest way to add `accessible_by` to all Mongoid Documents
|
39
|
+
module Mongoid::Document::ClassMethods
|
40
|
+
include CanCan::ModelAdditions::ClassMethods
|
41
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module CanCan
|
2
|
-
|
3
|
-
module
|
2
|
+
|
3
|
+
# This module adds the accessible_by class method to a model. It is included in the model adapters.
|
4
|
+
module ModelAdditions
|
4
5
|
module ClassMethods
|
5
6
|
# Returns a scope which fetches only the records that the passed ability
|
6
7
|
# can perform a given action on. The action defaults to :read. This
|
@@ -17,15 +18,9 @@ module CanCan
|
|
17
18
|
#
|
18
19
|
# @articles = Article.accessible_by(current_ability, :update)
|
19
20
|
#
|
20
|
-
# Here only the articles which the user can update are returned.
|
21
|
-
# internally uses Ability#conditions method, see that for more information.
|
21
|
+
# Here only the articles which the user can update are returned.
|
22
22
|
def accessible_by(ability, action = :read)
|
23
|
-
|
24
|
-
if respond_to?(:where) && respond_to?(:joins)
|
25
|
-
where(query.conditions).joins(query.joins)
|
26
|
-
else
|
27
|
-
scoped(:conditions => query.conditions, :joins => query.joins)
|
28
|
-
end
|
23
|
+
ability.model_adapter(self, action).database_records
|
29
24
|
end
|
30
25
|
end
|
31
26
|
|
@@ -34,9 +29,3 @@ module CanCan
|
|
34
29
|
end
|
35
30
|
end
|
36
31
|
end
|
37
|
-
|
38
|
-
if defined? ActiveRecord
|
39
|
-
ActiveRecord::Base.class_eval do
|
40
|
-
include CanCan::ActiveRecordAdditions
|
41
|
-
end
|
42
|
-
end
|
@@ -2,8 +2,8 @@ module CanCan
|
|
2
2
|
# This class is used internally and should only be called through Ability.
|
3
3
|
# it holds the information about a "can" call made on Ability and provides
|
4
4
|
# helpful methods to determine permission checking and conditions hash generation.
|
5
|
-
class
|
6
|
-
attr_reader :base_behavior, :actions
|
5
|
+
class Rule # :nodoc:
|
6
|
+
attr_reader :base_behavior, :actions, :conditions
|
7
7
|
attr_writer :expanded_actions
|
8
8
|
|
9
9
|
# The first argument when initializing is the base_behavior which is a true/false
|
@@ -41,18 +41,6 @@ module CanCan
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
def tableized_conditions(conditions = @conditions)
|
45
|
-
return conditions unless conditions.kind_of? Hash
|
46
|
-
conditions.inject({}) do |result_hash, (name, value)|
|
47
|
-
if value.kind_of? Hash
|
48
|
-
name = name.to_s.tableize.to_sym
|
49
|
-
value = tableized_conditions(value)
|
50
|
-
end
|
51
|
-
result_hash[name] = value
|
52
|
-
result_hash
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
44
|
def only_block?
|
57
45
|
conditions_empty? && !@block.nil?
|
58
46
|
end
|
@@ -100,19 +88,31 @@ module CanCan
|
|
100
88
|
@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)) }
|
101
89
|
end
|
102
90
|
|
91
|
+
# Checks if the given subject matches the given conditions hash.
|
92
|
+
# This behavior can be overriden by a model adapter by defining two class methods:
|
93
|
+
# override_matching_for_conditions?(subject, conditions) and
|
94
|
+
# matches_conditions_hash?(subject, conditions)
|
103
95
|
def matches_conditions_hash?(subject, conditions = @conditions)
|
104
|
-
conditions.
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
else
|
110
|
-
matches_conditions_hash? attribute, value
|
111
|
-
end
|
112
|
-
elsif value.kind_of?(Array) || value.kind_of?(Range)
|
113
|
-
value.include? attribute
|
96
|
+
if conditions.empty?
|
97
|
+
true
|
98
|
+
else
|
99
|
+
if model_adapter(subject).override_conditions_hash_matching? subject, conditions
|
100
|
+
model_adapter(subject).matches_conditions_hash? subject, conditions
|
114
101
|
else
|
115
|
-
|
102
|
+
conditions.all? do |name, value|
|
103
|
+
attribute = subject.send(name)
|
104
|
+
if value.kind_of?(Hash)
|
105
|
+
if attribute.kind_of? Array
|
106
|
+
attribute.any? { |element| matches_conditions_hash? element, value }
|
107
|
+
else
|
108
|
+
matches_conditions_hash? attribute, value
|
109
|
+
end
|
110
|
+
elsif value.kind_of?(Array) || value.kind_of?(Range)
|
111
|
+
value.include? attribute
|
112
|
+
else
|
113
|
+
attribute == value
|
114
|
+
end
|
115
|
+
end
|
116
116
|
end
|
117
117
|
end
|
118
118
|
end
|
@@ -129,5 +129,9 @@ module CanCan
|
|
129
129
|
@block.call(action, subject.class, subject, *extra_args)
|
130
130
|
end
|
131
131
|
end
|
132
|
+
|
133
|
+
def model_adapter(subject)
|
134
|
+
ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
|
135
|
+
end
|
132
136
|
end
|
133
137
|
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
|