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,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,3 @@
1
+ module Cancannible
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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,5 @@
1
+ class Ability
2
+ include CanCan::Ability
3
+ def initialize(user)
4
+ end
5
+ 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