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.
@@ -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=
@@ -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
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ # These are specific configuration settings required for travis-ci
2
+ language: ruby
3
+ rvm:
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # currently testing on the 3.2 branch of rails
4
+ # gem "activemodel", '~> 3.2'
5
+ # gem "activerecord", '~> 3.2'
6
+
7
+ # Specify your gem's dependencies in cancannible.gemspec
8
+ gemspec
@@ -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
+
@@ -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.
@@ -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
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ desc "Open an irb session preloaded with this library"
9
+ task :console do
10
+ sh "irb -rubygems -I lib -r cancannible.rb"
11
+ end
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'cancan'
4
+
5
+ require "cancannible/version"
6
+ require "cancannible/config"
7
+ require "cancannible/ability_preload_adapter"
8
+ require "cancannible/base"
@@ -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