cancannible 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/evendis/cancannible.svg?branch=master)](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
|