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
@@ -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
|