cancan 1.4.1 → 1.5.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|