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
@@ -0,0 +1,26 @@
|
|
1
|
+
module Cancannible
|
2
|
+
|
3
|
+
mattr_accessor :refinements
|
4
|
+
mattr_accessor :get_cached_abilities
|
5
|
+
mattr_accessor :store_cached_abilities
|
6
|
+
|
7
|
+
# Default way to configure the gem. Yields a block that gives access to all the config variables.
|
8
|
+
# Calling setup will reset all existing values.
|
9
|
+
def self.setup
|
10
|
+
reset!
|
11
|
+
yield self
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.reset!
|
16
|
+
self.refinements = []
|
17
|
+
self.get_cached_abilities = nil
|
18
|
+
self.store_cached_abilities = nil
|
19
|
+
end
|
20
|
+
reset!
|
21
|
+
|
22
|
+
def self.refine_access(refinement={})
|
23
|
+
self.refinements << refinement
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
require 'rails/generators/active_record'
|
4
|
+
|
5
|
+
module Cancannible
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
|
10
|
+
self.source_paths << File.join(File.dirname(__FILE__), 'templates')
|
11
|
+
|
12
|
+
desc "This generator creates a cancannible initializer file and permissions model migration"
|
13
|
+
|
14
|
+
def create_initializer_file
|
15
|
+
template 'cancannible_initializer.rb', 'config/initializers/cancannible.rb'
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_permission_migration_file
|
19
|
+
migration_template 'migration.rb', 'db/migrate/create_cancannible_permissions.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_permission_model_file
|
23
|
+
template 'permission.rb', 'app/models/permission.rb'
|
24
|
+
end
|
25
|
+
|
26
|
+
# while methods have moved around this has been the implementation
|
27
|
+
# since ActiveRecord 3.0
|
28
|
+
def self.next_migration_number(dirname)
|
29
|
+
next_migration_number = current_migration_number(dirname) + 1
|
30
|
+
if ActiveRecord::Base.timestamped_migrations
|
31
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
32
|
+
else
|
33
|
+
"%.3d" % next_migration_number
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
Cancannible.setup do |config|
|
2
|
+
|
3
|
+
# ABILITY CACHING
|
4
|
+
# ===============
|
5
|
+
# Cancannible supports optional ability caching. This can provide a significant performance
|
6
|
+
# improvement, since abilities do not need to be recalculated on each web request.
|
7
|
+
# No specific caching technology is enforced: you can use whatever makes sense in your application
|
8
|
+
# environment. To enable caching, you just need to provide two methods here: `get_cached_abilities`
|
9
|
+
# and `store_cached_abilities`.
|
10
|
+
|
11
|
+
# Return an Ability object for +grantee+ or nil if not found
|
12
|
+
# config.get_cached_abilities = proc{|grantee|
|
13
|
+
# # This is a simple example of using Redis (assumes @redis correctly connected)
|
14
|
+
# key = "user:#{grantee.id}:abilities"
|
15
|
+
# Marshal.load(@redis.get(key))
|
16
|
+
# }
|
17
|
+
|
18
|
+
# Command: put the +ability+ object for +grantee+ in the cache storage
|
19
|
+
# config.store_cached_abilities = proc{|grantee,ability|
|
20
|
+
# # This is a simple example of using Redis (assumes @redis correctly connected)
|
21
|
+
# key = "user:#{grantee.id}:abilities"
|
22
|
+
# @redis.set(key, Marshal.dump(ability))
|
23
|
+
# }
|
24
|
+
|
25
|
+
|
26
|
+
# ACCESS REFINMENTS
|
27
|
+
# Cancannible allows general-purpose access refinements to be declared here. This will be enforced
|
28
|
+
# in addition to any rules defined in you Ability.rb file.
|
29
|
+
|
30
|
+
# These are primarily intended to enforce data partitioning (multi-tenancy) and general security rules such as:
|
31
|
+
# "only show data related to customers that I have permissions to see", or
|
32
|
+
# "make sure I can see records that I created (regardless of other restrictions)"
|
33
|
+
|
34
|
+
# The syntax here is not as flexible as that supported in the Ability.rb file, but have the benefit of
|
35
|
+
# potentially refining any/all permissions that are configured in the permissions tables.
|
36
|
+
|
37
|
+
# The following examples illustrate the options that are available. These can be used individually or in combination.
|
38
|
+
|
39
|
+
|
40
|
+
# Basic syntax example:
|
41
|
+
# config.refine_access customer_id: :accessible_customer_ids
|
42
|
+
#
|
43
|
+
# This means:
|
44
|
+
# - for any resource permission loaded/inherited from the database
|
45
|
+
# - where the resource has a :customer_id attribute
|
46
|
+
# - restrict access to only those with values from my (the grantee) :accessible_customer_ids method
|
47
|
+
#
|
48
|
+
# In other words, say we have a Contract model that has a :customer_id attribute, and permissions are granted thus:
|
49
|
+
# user.can(:read,Contract)
|
50
|
+
# Then if we define a user.accessible_customer_ids method, these values will be used to restrict user's access to Contract records
|
51
|
+
#
|
52
|
+
# Note: the rule is ignored if the resource does not sport the attribute mentioned, or if the grantee method is not defined.
|
53
|
+
|
54
|
+
|
55
|
+
# Multiple conditions syntax example:
|
56
|
+
# config.refine_access customer_id: :accessible_customer_ids, product_id: :accessible_product_ids
|
57
|
+
#
|
58
|
+
# This restricts access to only records matching all conditions
|
59
|
+
|
60
|
+
|
61
|
+
# Fixed value conditions syntax example:
|
62
|
+
# config.refine_access customer_id: 42, status: 'open'
|
63
|
+
#
|
64
|
+
# Fixed match-values may be provided instead of methods
|
65
|
+
|
66
|
+
|
67
|
+
# Limited ability scope syntax example:
|
68
|
+
# config.refine_access customer_id: :accessible_customer_ids, scope: :read
|
69
|
+
# config.refine_access customer_id: :accessible_customer_ids, scope: [:read,:update]
|
70
|
+
#
|
71
|
+
# The `scope` parameter causes the rule to only be applied for the specified ability rules
|
72
|
+
|
73
|
+
|
74
|
+
# Limited ability scope syntax example:
|
75
|
+
# config.refine_access customer_id: :accessible_customer_ids, except: :read
|
76
|
+
# config.refine_access customer_id: :accessible_customer_ids, except: [:read,:update]
|
77
|
+
#
|
78
|
+
# The `except` parameter causes the rule to be applied for all abilities except those specified.
|
79
|
+
|
80
|
+
|
81
|
+
# Allow nil syntax example:
|
82
|
+
# config.refine_access customer_id: :accessible_customer_ids, allow_nil: true
|
83
|
+
#
|
84
|
+
# The `allow_nil` parameter changes the rule so that nil/NULL values are allowed through. By default, this is false.
|
85
|
+
|
86
|
+
|
87
|
+
# Conditional syntax example:
|
88
|
+
# config.refine_access customer_id: :accessible_customer_ids, if: proc{|grantee,model_resource|
|
89
|
+
# grantee.name = 'Paul' && model_resource.is_a?(Special)
|
90
|
+
# }
|
91
|
+
#
|
92
|
+
# The `if` parameter allowws you to provide a procedure that will dynamically determine if the rule should be applied.
|
93
|
+
# It should return true or false. The `grantee` is the actual instance that permissions are being applied to,
|
94
|
+
# and `model_resource` is an example record (unsaved) of the kind of resource that the rule is being applied to.
|
95
|
+
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateCancanniblePermissions < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :permissions, force: true do |table|
|
4
|
+
table.integer :permissible_id
|
5
|
+
table.string :permissible_type
|
6
|
+
table.integer :resource_id
|
7
|
+
table.string :resource_type
|
8
|
+
table.string :ability
|
9
|
+
table.boolean :asserted
|
10
|
+
table.datetime :created_at
|
11
|
+
table.datetime :updated_at
|
12
|
+
end
|
13
|
+
add_index :permissions, [:permissible_id, :permissible_type], name: "index_permissions_permissible"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# The Permission class stores permissions managed by CanCan and Cancannible
|
2
|
+
class Permission < ActiveRecord::Base
|
3
|
+
belongs_to :permissible, polymorphic: true
|
4
|
+
belongs_to :resource, polymorphic: true
|
5
|
+
|
6
|
+
validates_uniqueness_of :ability, scope: [:resource_id, :resource_type, :permissible_id, :permissible_type]
|
7
|
+
|
8
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
ENV["RAILS_ENV"] ||= 'test'
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
3
|
+
require 'cancannible'
|
4
|
+
require 'active_record'
|
5
|
+
require 'sqlite3'
|
6
|
+
|
7
|
+
# Requires supporting files with custom matchers and macros, etc,
|
8
|
+
# in ./support/ and its subdirectories.
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.before do
|
13
|
+
Cancannible.reset!
|
14
|
+
run_migrations
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module MigrationsHelper
|
2
|
+
|
3
|
+
def run_migrations
|
4
|
+
ActiveRecord::Base.establish_connection({
|
5
|
+
adapter: 'sqlite3',
|
6
|
+
database: ':memory:'
|
7
|
+
})
|
8
|
+
|
9
|
+
ActiveRecord::Migration.suppress_messages do
|
10
|
+
ActiveRecord::Schema.define(:version => 0) do
|
11
|
+
|
12
|
+
create_table "members", :force => true do |t|
|
13
|
+
t.string "name"
|
14
|
+
t.string "email"
|
15
|
+
end
|
16
|
+
|
17
|
+
create_table "users", :force => true do |t|
|
18
|
+
t.string "username"
|
19
|
+
t.string "email"
|
20
|
+
t.integer "group_id"
|
21
|
+
end
|
22
|
+
|
23
|
+
create_table "permissions", :force => true do |t|
|
24
|
+
t.boolean "asserted"
|
25
|
+
t.integer "permissible_id"
|
26
|
+
t.string "permissible_type"
|
27
|
+
t.integer "resource_id"
|
28
|
+
t.string "resource_type"
|
29
|
+
t.string "ability"
|
30
|
+
t.datetime "created_at"
|
31
|
+
t.datetime "updated_at"
|
32
|
+
end
|
33
|
+
|
34
|
+
create_table "roles", :force => true do |t|
|
35
|
+
t.string "name"
|
36
|
+
end
|
37
|
+
|
38
|
+
create_table "roles_users", :force => true do |t|
|
39
|
+
t.string "name"
|
40
|
+
t.integer "role_id"
|
41
|
+
t.integer "user_id"
|
42
|
+
end
|
43
|
+
|
44
|
+
create_table "groups", :force => true do |t|
|
45
|
+
t.string "name"
|
46
|
+
end
|
47
|
+
|
48
|
+
create_table "widgets", :force => true do |t|
|
49
|
+
t.string "name"
|
50
|
+
t.integer "category_id"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
RSpec.configure do |conf|
|
60
|
+
conf.include MigrationsHelper
|
61
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# These model definitions are just used for the test scenarios
|
2
|
+
|
3
|
+
# The Permission class stores permissions maanged by CanCan and Cancannible
|
4
|
+
class Permission < ActiveRecord::Base
|
5
|
+
belongs_to :permissible, polymorphic: true
|
6
|
+
belongs_to :resource, polymorphic: true
|
7
|
+
|
8
|
+
validates_uniqueness_of :ability,
|
9
|
+
:scope => [:resource_id, :resource_type,
|
10
|
+
:permissible_id, :permissible_type]
|
11
|
+
end
|
12
|
+
|
13
|
+
class Member < ActiveRecord::Base
|
14
|
+
include Cancannible
|
15
|
+
end
|
16
|
+
|
17
|
+
class User < ActiveRecord::Base
|
18
|
+
has_many :roles_users, class_name: 'RolesUsers'
|
19
|
+
has_many :roles, :through => :roles_users
|
20
|
+
belongs_to :group
|
21
|
+
|
22
|
+
include Cancannible
|
23
|
+
inherit_permissions_from :roles, :group
|
24
|
+
end
|
25
|
+
|
26
|
+
class RolesUsers < ActiveRecord::Base
|
27
|
+
belongs_to :role
|
28
|
+
belongs_to :user
|
29
|
+
end
|
30
|
+
|
31
|
+
class Role < ActiveRecord::Base
|
32
|
+
has_many :roles_users, :class_name => 'RolesUsers'
|
33
|
+
has_many :users, :through => :roles_users
|
34
|
+
|
35
|
+
include Cancannible
|
36
|
+
end
|
37
|
+
|
38
|
+
class Group < ActiveRecord::Base
|
39
|
+
has_many :users
|
40
|
+
|
41
|
+
include Cancannible
|
42
|
+
end
|
43
|
+
|
44
|
+
class Widget < ActiveRecord::Base
|
45
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cancannible do
|
4
|
+
let(:grantee_class) { Member }
|
5
|
+
|
6
|
+
context "without permissions inheritance" do
|
7
|
+
|
8
|
+
describe "##inheritable_permissions" do
|
9
|
+
subject { grantee_class.inheritable_permissions }
|
10
|
+
it { should be_empty }
|
11
|
+
end
|
12
|
+
|
13
|
+
subject(:grantee) { grantee_class.create! }
|
14
|
+
|
15
|
+
describe "#abilities" do
|
16
|
+
subject(:abilities) { grantee.abilities }
|
17
|
+
it { should be_a(Ability) }
|
18
|
+
it "should initialise @abilities instance var" do
|
19
|
+
expect { abilities }.to change { grantee.instance_variable_get("@abilities") }.from(nil)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "with a symbolic resource" do
|
24
|
+
let!(:resource) { :something }
|
25
|
+
|
26
|
+
describe "#can?" do
|
27
|
+
subject { grantee.can?(:read, resource) }
|
28
|
+
context "when permission is not set" do
|
29
|
+
it { should be_falsey }
|
30
|
+
end
|
31
|
+
context "when permission is set" do
|
32
|
+
before { grantee.can(:read, resource) }
|
33
|
+
it { should be_truthy }
|
34
|
+
end
|
35
|
+
context "when cannot is asserted" do
|
36
|
+
before { grantee.cannot(:read, resource) }
|
37
|
+
it { should be_falsey }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#cannot?" do
|
42
|
+
subject { grantee.cannot?(:read, resource) }
|
43
|
+
context "when permission is not asserted" do
|
44
|
+
it { should be_truthy }
|
45
|
+
end
|
46
|
+
context "when permission is not asserted but can is" do
|
47
|
+
before { grantee.can(:read, resource) }
|
48
|
+
it { should be_falsey }
|
49
|
+
end
|
50
|
+
context "when permission is asserted" do
|
51
|
+
before { grantee.cannot(:read, resource) }
|
52
|
+
it { should be_truthy }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "with a nil resource" do
|
58
|
+
let!(:resource) { nil }
|
59
|
+
describe "#can? -> nil" do
|
60
|
+
subject { grantee.can?(:read, nil) }
|
61
|
+
context "when permission is not set" do
|
62
|
+
it { should be_falsey }
|
63
|
+
end
|
64
|
+
context "when permission is set" do
|
65
|
+
before { grantee.can(:read, resource) }
|
66
|
+
it { should be_truthy }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
describe "#can? -> ''" do
|
70
|
+
subject { grantee.can?(:read, '') }
|
71
|
+
context "when permission is not set" do
|
72
|
+
it { should be_falsey }
|
73
|
+
end
|
74
|
+
context "when permission is set" do
|
75
|
+
before { grantee.can(:read, resource) }
|
76
|
+
it { should be_falsey }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
context "with a resource class" do
|
83
|
+
let!(:resource) { Widget }
|
84
|
+
|
85
|
+
describe "#can?" do
|
86
|
+
subject { grantee.can?(:read, resource) }
|
87
|
+
context "when permission is not set" do
|
88
|
+
it { should be_falsey }
|
89
|
+
end
|
90
|
+
context "when permission is set" do
|
91
|
+
before { grantee.can(:read, resource) }
|
92
|
+
it { should be_truthy }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "with a resource instance" do
|
98
|
+
let!(:resource) { Widget.create! }
|
99
|
+
let!(:other_resource) { Widget.create! }
|
100
|
+
|
101
|
+
describe "#can?" do
|
102
|
+
subject { grantee.can?(:read, resource) }
|
103
|
+
context "when permission is not set" do
|
104
|
+
it { should be_falsey }
|
105
|
+
end
|
106
|
+
context "when permission is set" do
|
107
|
+
before { grantee.can(:read, resource) }
|
108
|
+
it { should be_truthy }
|
109
|
+
context "but for other instances" do
|
110
|
+
subject { grantee.can?(:read, other_resource) }
|
111
|
+
it { should be_falsey }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "with a non-existent model" do
|
118
|
+
describe "instance" do
|
119
|
+
let!(:obsolete_permission) {grantee.permissions.create!(asserted: true, ability: 'manage', resource_type: 'Bogative', resource_id: 33) }
|
120
|
+
it "should not error on load" do
|
121
|
+
expect { grantee.abilities }.to_not raise_error
|
122
|
+
end
|
123
|
+
end
|
124
|
+
describe "class" do
|
125
|
+
let!(:obsolete_permission) {grantee.permissions.create!(asserted: true, ability: 'manage', resource_type: 'Bogative') }
|
126
|
+
it "should not error on load" do
|
127
|
+
grantee.abilities
|
128
|
+
expect { grantee.abilities }.to_not raise_error
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "with an invalid model" do
|
134
|
+
class SuperBogative < ActiveRecord::Base
|
135
|
+
end
|
136
|
+
context "instance" do
|
137
|
+
let!(:obsolete_permission) { grantee.permissions.create!(asserted: true, ability: 'manage', resource_type: 'SuperBogative', resource_id: 33) }
|
138
|
+
it "should not error on load" do
|
139
|
+
expect { grantee.abilities }.to_not raise_error
|
140
|
+
end
|
141
|
+
end
|
142
|
+
context "class" do
|
143
|
+
let!(:obsolete_permission) { grantee.permissions.create!(asserted: true, ability: 'manage', resource_type: 'SuperBogative') }
|
144
|
+
it "should not error on load" do
|
145
|
+
expect { grantee.abilities }.to_not raise_error
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|