cancannible 0.0.1
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.
- checksums.yaml +15 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +8 -0
- data/Guardfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +85 -0
- data/Rakefile +11 -0
- data/cancannible.gemspec +31 -0
- data/lib/cancannible.rb +8 -0
- data/lib/cancannible/ability_preload_adapter.rb +15 -0
- data/lib/cancannible/base.rb +213 -0
- data/lib/cancannible/config.rb +26 -0
- data/lib/cancannible/version.rb +3 -0
- data/lib/generators/cancannible/install_generator.rb +39 -0
- data/lib/generators/cancannible/templates/cancannible_initializer.rb +97 -0
- data/lib/generators/cancannible/templates/migration.rb +15 -0
- data/lib/generators/cancannible/templates/permission.rb +8 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/ability.rb +5 -0
- data/spec/support/migrations_helper.rb +61 -0
- data/spec/support/models.rb +45 -0
- data/spec/unit/base_spec.rb +152 -0
- data/spec/unit/cached_abilities_spec.rb +67 -0
- data/spec/unit/config_spec.rb +17 -0
- data/spec/unit/custom_refinements_spec.rb +223 -0
- data/spec/unit/inherited_permissions_spec.rb +145 -0
- metadata +207 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YTJjNWE0NzMzZTcyNDAxMDRhOTMxZmE4ODRkYWZkMWU1ZDcwNjczMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NGY4MDliNGJlMGMwZjBkYTc5MjYyYzcyNWI0MWZiNWJmMmU1ZjU3Mw==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MTliYzQ2MGYzZWQyODhiMTk0ZTgzODhjNDQ1MDEwMzdmZDNjOWY0ZjRiY2Q0
|
10
|
+
NDNjZGUwYmE4OTQ3Zjc2MjZkMDc1N2JjOTYyOWE2ZGY4OWZhNGE5NTkyYjg4
|
11
|
+
ODFjZTYyODc0Yjg5ZGM4YWRmMjI1ZmU4ZmFmZGU5NmExMzhjZTE=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NmM5ODQ3NmUwNDIzYjJkYTZjZTZlYzhiNDEzMTAxYWQzMmE0ODU1OGJiNTFm
|
14
|
+
OWRjMzUyZWUzMmQxYTI1ZmQ2MGIzMjZjOWRkNTI0N2VlZjkyN2NjNjljM2Ez
|
15
|
+
ZmY5M2RkMzgyYTk1OWE5OGFkYzUwNTE1ZjRlNmQyNzVkYjkxNWE=
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.rvm*
|
24
|
+
.ruby*
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
5
|
+
# rspec may be run, below are examples of the most common uses.
|
6
|
+
# * bundler: 'bundle exec rspec'
|
7
|
+
# * bundler binstubs: 'bin/rspec'
|
8
|
+
# * spring: 'bin/rsspec' (This will use spring if running and you have
|
9
|
+
# installed the spring binstubs per the docs)
|
10
|
+
# * zeus: 'zeus rspec' (requires the server to be started separetly)
|
11
|
+
# * 'just' rspec: 'rspec'
|
12
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
13
|
+
watch(%r{^spec/.+_spec\.rb$})
|
14
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
15
|
+
watch('spec/spec_helper.rb') { "spec" }
|
16
|
+
|
17
|
+
end
|
18
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2011 Paul Gallagher
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Cancannible
|
2
|
+
[](https://travis-ci.org/evendis/cancannible)
|
3
|
+
|
4
|
+
Cancannible is a gem that extends CanCan with a range of capabilities:
|
5
|
+
* database-persisted permissions
|
6
|
+
* export CanCan methods to the model layer (so that permissions can be applied in model methods, and easily set in a test case)
|
7
|
+
* permissions inheritance (so that, for example, a User can inherit permissions from Roles and/or Groups)
|
8
|
+
* caching of abilities (so that they don't need to be recalculated on each web request)
|
9
|
+
* general-purpose access refinements (so that, for example, CanCan will automatically enforce multi-tenant or other security restrictions)
|
10
|
+
|
11
|
+
## Limitations
|
12
|
+
Cancannible's origin was in a web application that's been in production for over 3 years.
|
13
|
+
This gem is an initial refactoring as a separate component. It continues to be used in production, but
|
14
|
+
there are some limitations and constraints that will ideally be removed or changed over time:
|
15
|
+
|
16
|
+
* It only supports ActiveRecord for permissions storage (specifically, it has been tested with PostgreSQL and SQLite)
|
17
|
+
* It currently assumes permissions are stored in a Permission model with a specific structure
|
18
|
+
* It works with the [CanCan](https://github.com/ryanb/cancan) gem. It has not yet been tested with the new [CanCanCan](https://github.com/CanCanCommunity/cancancan) gem.
|
19
|
+
* It assumes and is only tested with Rails 3.2. Not yet with Rails 4.
|
20
|
+
* It assumes your CanCan rules are setup with the default `Ability` class
|
21
|
+
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
gem 'cancannible'
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install cancannible
|
36
|
+
|
37
|
+
## Configuration
|
38
|
+
|
39
|
+
A generator is provided to create:
|
40
|
+
* a default initialization template
|
41
|
+
* a Permission model and migration
|
42
|
+
|
43
|
+
After installing the gem, run the generator:
|
44
|
+
|
45
|
+
$ rails generate cancannible:install
|
46
|
+
|
47
|
+
## The Cancannible initialization file
|
48
|
+
|
49
|
+
See the initialization file template for specific instructions. Use the initialization file to configure:
|
50
|
+
* abilities caching
|
51
|
+
* general-purpose access refinements
|
52
|
+
|
53
|
+
## Configuring cached abilities storage
|
54
|
+
|
55
|
+
Cancannible does not implement any specific storage mechanism - that is up to you to provide if you wish.
|
56
|
+
|
57
|
+
Cached abilities storage is enabled by setting the `get_cached_abilities` and `store_cached_abilities` hooks with
|
58
|
+
the appropriate implementation for your caching infrastructure.
|
59
|
+
|
60
|
+
For example, this is a simple scheme using Redis:
|
61
|
+
|
62
|
+
Cancannible.setup do |config|
|
63
|
+
|
64
|
+
# Return an Ability object for +grantee+ or nil if not found
|
65
|
+
config.get_cached_abilities = proc{|grantee|
|
66
|
+
key = "user:#{grantee.id}:abilities"
|
67
|
+
Marshal.load(@redis.get(key))
|
68
|
+
}
|
69
|
+
|
70
|
+
# Command: put the +ability+ object for +grantee+ in the cache storage
|
71
|
+
config.store_cached_abilities = proc{|grantee,ability|
|
72
|
+
key = "user:#{grantee.id}:abilities"
|
73
|
+
@redis.set(key, Marshal.dump(ability))
|
74
|
+
}
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
## Contributing
|
80
|
+
|
81
|
+
1. Fork it ( https://github.com/evendis/cancannible/fork )
|
82
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
83
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
84
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
85
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/cancannible.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cancannible/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "cancannible"
|
8
|
+
spec.version = Cancannible::VERSION
|
9
|
+
spec.authors = ["Paul Gallagher"]
|
10
|
+
spec.email = ["paul@evendis.com"]
|
11
|
+
spec.summary = "Dynamic, configurable permissions for CanCan"
|
12
|
+
spec.description = "Extends CanCan with dynamic, inheritable permissions stored in a database, with caching and multi-tenant refinements"
|
13
|
+
spec.homepage = "https://github.com/evendis/cancannible"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "activesupport", "~> 3.2"
|
22
|
+
spec.add_runtime_dependency "activemodel", "~> 3.2"
|
23
|
+
spec.add_runtime_dependency "cancan", "~> 1.6"
|
24
|
+
|
25
|
+
spec.add_development_dependency "activerecord", "~> 3.2"
|
26
|
+
spec.add_development_dependency "sqlite3", "~> 1.3"
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
spec.add_development_dependency "guard-rspec", "~> 4.0"
|
31
|
+
end
|
data/lib/cancannible.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Cancannible::AbilityPreloadAdapter
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
|
6
|
+
# Tap Ability.new to first preload permissions via Cancannible
|
7
|
+
alias_method :cancan_initialize, :initialize
|
8
|
+
def initialize(user)
|
9
|
+
user.preload_abilities(self) if user.respond_to? :preload_abilities
|
10
|
+
cancan_initialize(user)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
module Cancannible
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
class_attribute :inheritable_permissions
|
6
|
+
self.inheritable_permissions = [] # default
|
7
|
+
|
8
|
+
has_many :permissions, as: :permissible, dependent: :destroy do
|
9
|
+
# generally use instance.can method, not permissions<< directly
|
10
|
+
def <<(arg)
|
11
|
+
ability, resource = arg
|
12
|
+
resource = nil if resource.blank?
|
13
|
+
asserted = arg[2].nil? ? true : arg[2]
|
14
|
+
|
15
|
+
case resource
|
16
|
+
when Class, Symbol
|
17
|
+
resource_type = resource.to_s
|
18
|
+
resource_id = nil
|
19
|
+
when nil
|
20
|
+
resource_type = resource_id = nil
|
21
|
+
else
|
22
|
+
resource_type = resource.class.to_s
|
23
|
+
resource_id = resource.try(:id)
|
24
|
+
end
|
25
|
+
|
26
|
+
permission = find_by_asserted_and_ability_and_resource_id_and_resource_type(
|
27
|
+
asserted, ability, resource_id, resource_type)
|
28
|
+
unless permission
|
29
|
+
permission = find_or_initialize_by_asserted_and_ability_and_resource_id_and_resource_type(
|
30
|
+
!asserted, ability, resource_id, resource_type)
|
31
|
+
permission.asserted = asserted
|
32
|
+
permission.save!
|
33
|
+
end
|
34
|
+
|
35
|
+
# if Rails.version =~ /3\.0/ # the rails 3.0 way
|
36
|
+
# proxy_owner.instance_variable_set :@permissions, nil # invalidate the owner's permissions collection
|
37
|
+
# proxy_owner.instance_variable_set :@abilities, nil # invalidate the owner's ability collection
|
38
|
+
# else
|
39
|
+
proxy_association.owner.instance_variable_set :@abilities, nil # invalidate the owner's ability collection
|
40
|
+
# end
|
41
|
+
permission
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def inherit_permissions_from(*relations)
|
49
|
+
self.inheritable_permissions = relations
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the Ability set for the owner.
|
54
|
+
# Set +refresh+ to true to force a reload of permissions.
|
55
|
+
def abilities(refresh = false)
|
56
|
+
@abilities = if refresh
|
57
|
+
nil
|
58
|
+
elsif get_cached_abilities.respond_to?(:call)
|
59
|
+
get_cached_abilities.call(self)
|
60
|
+
end
|
61
|
+
return @abilities if @abilities
|
62
|
+
|
63
|
+
@abilities ||= if ability_class = ('Ability'.constantize rescue nil)
|
64
|
+
unless ability_class.included_modules.include?(Cancannible::AbilityPreloadAdapter)
|
65
|
+
ability_class.send :include, Cancannible::AbilityPreloadAdapter
|
66
|
+
end
|
67
|
+
ability_class.new(self)
|
68
|
+
end
|
69
|
+
|
70
|
+
store_cached_abilities.call(self,@abilities) if store_cached_abilities.respond_to?(:call)
|
71
|
+
@abilities
|
72
|
+
end
|
73
|
+
|
74
|
+
def preload_abilities(cancan_ability_object)
|
75
|
+
# load inherited permissions to CanCan Abilities
|
76
|
+
preload_abilities_from_permissions(cancan_ability_object, inherited_permissions)
|
77
|
+
# load user-based permissions from database to CanCan Abilities
|
78
|
+
preload_abilities_from_permissions(cancan_ability_object, self.permissions.reload)
|
79
|
+
cancan_ability_object
|
80
|
+
end
|
81
|
+
|
82
|
+
def inherited_permissions
|
83
|
+
inherited_perms = []
|
84
|
+
self.class.inheritable_permissions.each do |relation|
|
85
|
+
Array(self.send(relation)).each do |record|
|
86
|
+
inherited_perms.concat(record.permissions.reload)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
inherited_perms
|
90
|
+
end
|
91
|
+
|
92
|
+
# test for a permission - persisted or dynamic (delegated to CanCan)
|
93
|
+
def can?(ability, resource)
|
94
|
+
abilities.can?(ability, resource)
|
95
|
+
end
|
96
|
+
|
97
|
+
# test for a prohibition - persisted or dynamic (delegated to CanCan)
|
98
|
+
def cannot?(ability, resource)
|
99
|
+
abilities.cannot?(ability, resource)
|
100
|
+
end
|
101
|
+
|
102
|
+
# define a persisted permission
|
103
|
+
def can(ability, resource)
|
104
|
+
permissions << [ability, resource]
|
105
|
+
end
|
106
|
+
|
107
|
+
# define a persisted prohibition
|
108
|
+
def cannot(ability, resource)
|
109
|
+
permissions << [ability, resource, false]
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def preload_abilities_from_permissions(cancan_ability_object,perms)
|
115
|
+
perms.each do |permission|
|
116
|
+
ability = permission.ability.to_sym
|
117
|
+
action = permission.asserted ? :can : :cannot
|
118
|
+
|
119
|
+
if resource_type = permission.resource_type
|
120
|
+
begin
|
121
|
+
resource_type = resource_type==resource_type.downcase ? resource_type.to_sym : resource_type.constantize
|
122
|
+
model_resource = resource_type.respond_to?(:new) ? resource_type.new : resource_type
|
123
|
+
rescue
|
124
|
+
model_resource = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
if !resource_type || resource_type.is_a?(Symbol)
|
129
|
+
# nil or symbolic resource types:
|
130
|
+
# apply generic unrestricted permission to the resource_type
|
131
|
+
cancan_ability_object.send( action, ability, resource_type )
|
132
|
+
next
|
133
|
+
else
|
134
|
+
# model-based resource types:
|
135
|
+
# skip if we cannot get a model instance
|
136
|
+
next unless model_resource
|
137
|
+
end
|
138
|
+
|
139
|
+
if permission.resource_id.nil?
|
140
|
+
|
141
|
+
if action == :cannot
|
142
|
+
# apply generic unrestricted permission to the class
|
143
|
+
cancan_ability_object.send( action, ability, resource_type )
|
144
|
+
else
|
145
|
+
|
146
|
+
refinements = Cancannible.refinements.each_with_object([]) do |refinement,memo|
|
147
|
+
refinement_attributes = refinement.dup
|
148
|
+
|
149
|
+
allow_nil = !!(refinement_attributes.delete(:allow_nil))
|
150
|
+
|
151
|
+
refinement_if_condition = refinement_attributes.delete(:if)
|
152
|
+
next if refinement_if_condition.respond_to?(:call) && !refinement_if_condition.call(self,model_resource)
|
153
|
+
|
154
|
+
refinement_scope = Array(refinement_attributes.delete(:scope))
|
155
|
+
next if refinement_scope.present? && !refinement_scope.include?(ability)
|
156
|
+
|
157
|
+
refinement_except = Array(refinement_attributes.delete(:except))
|
158
|
+
next if refinement_except.present? && refinement_except.include?(ability)
|
159
|
+
|
160
|
+
refinement_attribute_names = refinement_attributes.keys.map{|k| "#{k}" }
|
161
|
+
next unless (refinement_attribute_names - model_resource.attribute_names).empty?
|
162
|
+
|
163
|
+
restriction = {}
|
164
|
+
refinement_attributes.each do |key,value|
|
165
|
+
if value.is_a?(Symbol)
|
166
|
+
if self.respond_to?(value)
|
167
|
+
restriction[key] = if allow_nil
|
168
|
+
Array(self.send(value)) + [nil]
|
169
|
+
else
|
170
|
+
self.send(value)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
else
|
174
|
+
restriction[key] = value
|
175
|
+
end
|
176
|
+
end
|
177
|
+
memo.push(restriction) if restriction.present?
|
178
|
+
end
|
179
|
+
|
180
|
+
if refinements.empty?
|
181
|
+
# apply generic unrestricted permission to the class
|
182
|
+
cancan_ability_object.send( action, ability, resource_type )
|
183
|
+
else
|
184
|
+
refinements.each do |refinement|
|
185
|
+
cancan_ability_object.send( action, ability, resource_type, refinement)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
elsif resource_type.find_by_id(permission.resource_id)
|
192
|
+
cancan_ability_object.send( action, ability, resource_type, id: permission.resource_id)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
module Cancannible
|
200
|
+
# This module is automatically included into all controllers.
|
201
|
+
# It overrides some CanCan ControllerAdditions
|
202
|
+
module ControllerAdditions
|
203
|
+
def current_ability
|
204
|
+
current_user.try(:abilities)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
if defined? ActionController::Base
|
210
|
+
ActionController::Base.class_eval do
|
211
|
+
include Cancannible::ControllerAdditions
|
212
|
+
end
|
213
|
+
end
|