dynamican 1.0.1 → 2.0.0
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 +4 -4
- data/README.md +52 -8
- data/dynamican.gemspec +2 -2
- data/lib/dynamican/{evaluator.rb → authorizer.rb} +1 -1
- data/lib/dynamican/{model.rb → permittable.rb} +2 -2
- data/lib/dynamican/skimmable.rb +20 -0
- data/lib/dynamican/skimmer.rb +49 -0
- data/lib/dynamican.rb +6 -2
- data/lib/generators/dynamican_migration_generator.rb +50 -36
- data/lib/models/dynamican/action.rb +2 -2
- data/lib/models/dynamican/condition.rb +2 -0
- data/lib/models/dynamican/filter.rb +14 -0
- data/lib/models/dynamican/item.rb +3 -2
- data/lib/models/dynamican/permission.rb +4 -2
- data/lib/models/dynamican/rule.rb +10 -0
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07765d12ff4c1d8450e780bc29414d1db951b7ae2645876de19b723f92a9d688
|
4
|
+
data.tar.gz: 784a582fab983684cd45d41205e9a2a050036e13bad45d70ebd3f88a15be4d66
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: deabb5e5032c2c99eff13b680fcc816c712430de25ff791a2803bfd91eb5a26c6babf0bf524872195be4d67c50e7e95103910fb60331da43236869f6e7d2f687
|
7
|
+
data.tar.gz: 9b805a8fe35cd6c131988fdc81d7a7d17a07b8526db04208028462f7c394229767a3bca7e1cd21bd8b670b857eba23f3c2a18e22aeec861a2588b19ec69b630c
|
data/README.md
CHANGED
@@ -14,9 +14,11 @@ This command will generate a migration file in your project, which you can run w
|
|
14
14
|
|
15
15
|
rails db:migrate
|
16
16
|
|
17
|
-
|
17
|
+
## Permissions
|
18
18
|
|
19
|
-
|
19
|
+
In each model you want to have permissions, put the following.
|
20
|
+
|
21
|
+
include Dynamican::Permittable
|
20
22
|
|
21
23
|
### Using a model permissions on another model
|
22
24
|
|
@@ -43,9 +45,9 @@ I wanted to have the possibility to assign permissions both directly to my User
|
|
43
45
|
end
|
44
46
|
end
|
45
47
|
|
46
|
-
I
|
48
|
+
I have put this in `app/decorators/models/user/dynamican_overrides.rb` (rails needs to load the folder of course); you can make it work as you please but i recommend to keep it separate from the original User model.
|
47
49
|
|
48
|
-
|
50
|
+
### Configuration and Usage
|
49
51
|
|
50
52
|
Now the hard part: the real configuration.
|
51
53
|
|
@@ -56,7 +58,7 @@ Create one `Dynamican::Action` for each action you need. For example i created C
|
|
56
58
|
a3 = Dynamican::Action.create(name: 'update')
|
57
59
|
a4 = Dynamican::Action.create(name: 'delete')
|
58
60
|
|
59
|
-
|
61
|
+
Make sure you have one `Dynamican::Item` for each resource you want your permittables to have permissions to act on. This is not mandatory for every permission though: you can also set a permission to do a general action, like `login`. Right now i'm trying to setup permissions for my permittable (Role model) to act on Order model.
|
60
62
|
|
61
63
|
i1 = Dynamican::Item.create(name: 'Order')
|
62
64
|
|
@@ -108,8 +110,6 @@ You can store in conditions statements whatever conditions you like in plain rub
|
|
108
110
|
2. The same thing happens with the item: it will be defined both as `@item` and as the name of its class, so if you call `user.can? :read, @order` you will have `@item` and `@order` variable defined containing your `@order` object.
|
109
111
|
3. You can pass as third argument an hash of objects like this `user.can? :read, @order, time: Time.zone.now, whatever: @you_want` and you will have `@time` and `@whatever` variables defined.
|
110
112
|
|
111
|
-
WARNING: since the condition statement gets evaluated, i recommend not to allow anyone except project developers to create conditions, in order to prevent malicious code from being executed.
|
112
|
-
|
113
113
|
If one `Dynamican::Permission` is linked to many conditions, the model will be allowed to make that action only if all conditions are true. If you want to set alternative conditions, you should store the `or` conditions inside the same condition statement, like this:
|
114
114
|
|
115
115
|
condition.statement = '@user.nice? || @user.polite?'
|
@@ -121,6 +121,50 @@ There is a `for_item(item_name)` scope, which turns to string and then classifie
|
|
121
121
|
There is also a `without_item` scope to filter records that are not linked to any item.
|
122
122
|
As mentioned before, you can also use `conditional` and `unconditional` scopes to find objects with or without any condition attached.
|
123
123
|
|
124
|
-
### Controller
|
124
|
+
### Controller helpers
|
125
125
|
|
126
126
|
Your controllers now all have the `authorize!` method, which accepts one or two arguments: the first is the action, and the second (optional) is the item (or list of items) you want to check permissions for. As you can see, the usage is similar to the `can?` method. The reason is that is actually calls that method on the instance of `@current_user` and, whether permissions are evaluated as false, it raises an exception which is rescued by an `unauthorized` response rendered.
|
127
|
+
|
128
|
+
## Filters
|
129
|
+
|
130
|
+
In each model you want to have filters, put the following.
|
131
|
+
|
132
|
+
include Dynamican::Skimmable
|
133
|
+
|
134
|
+
### Use skimming filters of another model
|
135
|
+
|
136
|
+
If you want, you can also allow a related model (for example User) to use Role's skimming rules. In that case the in the User model you should do as follows.
|
137
|
+
|
138
|
+
skim_throught :roles
|
139
|
+
|
140
|
+
### Configuration and Usage
|
141
|
+
|
142
|
+
Make sure you have one `Dynamican::Item` for each class you want your skimmable models to skim. The name of the item has to be PascalCase.
|
143
|
+
|
144
|
+
item = Skimming::Item.create(name: 'Order')
|
145
|
+
|
146
|
+
Create one `Dynamican::Rule` for each condition you want to evaluate when you decide if the skimmable should keep the items of a certain collection.
|
147
|
+
|
148
|
+
rule = Skimming::Rule.create(statement: '@order.created_at < 1.month.ago')
|
149
|
+
|
150
|
+
This, for example, hides all orders older than 1 month from the collection.
|
151
|
+
|
152
|
+
Create one `Dynamican::Filter` for each item your model needs to skim and assign the rule.
|
153
|
+
|
154
|
+
role.create_filter(item: item, rule: rule)
|
155
|
+
|
156
|
+
If now you call `role.skim orders_collection` only orders newer than 1 month ago will be returned.
|
157
|
+
|
158
|
+
Also user can do that if it has the role assigned and you have specified `skim_through :roles` in User model.
|
159
|
+
|
160
|
+
### Rules
|
161
|
+
|
162
|
+
You can store in filters rules whatever conditions you like in plain ruby and the string will be evaulated. Inside the rules, object have to be called as instance variables. These instance variables indeed need to be present and can be declared in a few ways.
|
163
|
+
|
164
|
+
1. The model name of the instance you called `skim` from, like the user, will be defined automatically based on model name, so if you call `user.skim rooms_collection` you will have `@user` variable defined.
|
165
|
+
2. Based on the skimmed collection, instance variables are defined. In the case above, `@order` variable will be present for each of the orders to be evaluated in. The collection name is calculated based on the class of the first object in the collection and will raise an error if is not the same for all collection elements. You can override this calculation specifying the `item_name` of the collection (the skimming will look for collection_filters with that `item_name`). This can be necessary if you are filtering throught different classes instances having STI.
|
166
|
+
3. You can pass as third argument an hash of objects like this `user.skim rooms_collection, time: Time.zone.now, whatever: @you_want` and you will have `@time` and `@whatever` variables defined for rule evaluation.
|
167
|
+
|
168
|
+
|
169
|
+
## Security
|
170
|
+
Since the conditions and rules statements get evaluated, it's highly recommended not to allow anyone except project developers to create them, in order to prevent unsafe code from being executed.
|
data/dynamican.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'dynamican'
|
3
|
-
s.version = '
|
4
|
-
s.date = '
|
3
|
+
s.version = '2.0.0'
|
4
|
+
s.date = '2024-03-05'
|
5
5
|
s.summary = "Dynamic permissions"
|
6
6
|
s.description = "Dynamic and flexible database configurable permissions for your application"
|
7
7
|
s.authors = ["Valerio Bellaveglia"]
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Dynamican
|
2
|
-
module
|
2
|
+
module Permittable
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
@@ -10,7 +10,7 @@ module Dynamican
|
|
10
10
|
if item.respond_to? :each
|
11
11
|
item.all? { |single_item| can? action, single_item }
|
12
12
|
else
|
13
|
-
Dynamican::
|
13
|
+
Dynamican::Authorizer.new(self, action, item, conditions_instances).evaluate
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dynamican
|
2
|
+
module Skimmable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_many :filters, class_name: 'Dynamican::Filter', as: :skimmable
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def skim_through(association)
|
11
|
+
has_many :filters, through: association, class_name: 'Dynamican::Filter', source: :filters
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def skim(collection, item_name: nil, **skimming_instances)
|
16
|
+
Dynamican::Skimmer.new(self, collection, item_name, skimming_instances).skim
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Dynamican
|
2
|
+
class Skimmer
|
3
|
+
attr_reader :subject, :collection, :item_name, :skimming_instances
|
4
|
+
|
5
|
+
def initialize(subject, collection, item_name, skimming_instances)
|
6
|
+
@subject = subject
|
7
|
+
@collection = collection
|
8
|
+
@item_name = calculate_item_name
|
9
|
+
@skimming_instances = skimming_instances
|
10
|
+
end
|
11
|
+
|
12
|
+
def skim
|
13
|
+
set_instance_variables
|
14
|
+
|
15
|
+
filters_rules = subject.filters.for_item(item_name).map(&:rules).flatten.map(&:statement)
|
16
|
+
|
17
|
+
return collection if filters_rules.empty?
|
18
|
+
|
19
|
+
skimming_result = collection.select do |collection_item|
|
20
|
+
instance_variable_set("@#{item_name.downcase}", collection_item)
|
21
|
+
|
22
|
+
filters_rules.all? { |rule| eval rule }
|
23
|
+
end
|
24
|
+
|
25
|
+
skimming_result
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def calculate_item_name
|
31
|
+
return item_name.to_s.classify if item_name
|
32
|
+
|
33
|
+
items_classes = collection.map(&:class).uniq
|
34
|
+
|
35
|
+
raise 'Invalid collection: contains items with different classes (#{items.classes.join(', ')}). Use same class items or specify their item_name.' unless items_classes.count == 1
|
36
|
+
|
37
|
+
items_classes.first.name.demodulize
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_instance_variables
|
41
|
+
instance_variable_set("@#{subject.class.name.downcase}", subject)
|
42
|
+
|
43
|
+
skimming_instances.each do |instance_name, instance_object|
|
44
|
+
instance_variable_set("@#{instance_name}", instance_object)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
data/lib/dynamican.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
require 'dynamican/authorization'
|
2
|
-
require 'dynamican/
|
3
|
-
require 'dynamican/
|
2
|
+
require 'dynamican/authorizer'
|
3
|
+
require 'dynamican/permittable'
|
4
|
+
require 'dynamican/skimmer'
|
5
|
+
require 'dynamican/skimmable'
|
4
6
|
require 'models/dynamican/permission'
|
5
7
|
require 'models/dynamican/action'
|
6
8
|
require 'models/dynamican/item'
|
7
9
|
require 'models/dynamican/condition'
|
10
|
+
require 'models/dynamican/filter'
|
11
|
+
require 'models/dynamican/rule'
|
8
12
|
require 'generators/dynamican_migration_generator'
|
@@ -10,44 +10,58 @@ class DynamicanMigrationGenerator < Rails::Generators::Base
|
|
10
10
|
|
11
11
|
def migration_data
|
12
12
|
<<MIGRATION
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
13
|
+
class DynamicanMigration < ActiveRecord::Migration[5.2]
|
14
|
+
def change
|
15
|
+
create_table :dynamican_permissions do |t|
|
16
|
+
t.bigint :permittable_id
|
17
|
+
t.string :permittable_type
|
18
|
+
t.bigint :action_id
|
19
|
+
|
20
|
+
t.timestamps
|
21
|
+
end
|
22
|
+
|
23
|
+
create_table :dynamican_actions do |t|
|
24
|
+
t.string :name
|
25
|
+
|
26
|
+
t.timestamps
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table :dynamican_items do |t|
|
30
|
+
t.string :name
|
31
|
+
|
32
|
+
t.timestamps
|
33
|
+
end
|
34
|
+
|
35
|
+
create_table :dynamican_conditions do |t|
|
36
|
+
t.bigint :permission_id
|
37
|
+
t.string :statement
|
38
|
+
t.string :description
|
39
|
+
|
40
|
+
t.timestamps
|
41
|
+
end
|
42
|
+
|
43
|
+
create_table :dynamican_filters do |t|
|
44
|
+
t.bigint :item_id
|
45
|
+
t.bigint :skimmable_id
|
46
|
+
t.string :skimmable_type
|
47
|
+
|
48
|
+
t.timestamps
|
49
|
+
end
|
50
|
+
|
51
|
+
create_table :dynamican_rules do |t|
|
52
|
+
t.bigint :filter_id
|
53
|
+
t.string :statement
|
54
|
+
t.string :name
|
55
|
+
|
56
|
+
t.timestamps
|
57
|
+
end
|
58
|
+
|
59
|
+
create_table :dynamican_items_dynamican_permissions do |t|
|
60
|
+
t.bigint :item_id
|
61
|
+
t.bigint :permission_id
|
49
62
|
end
|
50
63
|
end
|
64
|
+
end
|
51
65
|
MIGRATION
|
52
66
|
end
|
53
67
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Dynamican
|
2
2
|
class Action < ActiveRecord::Base
|
3
|
+
self.table_name = 'dynamican_actions'
|
4
|
+
|
3
5
|
has_many :permissions, class_name: 'Dynamican::Permission', inverse_of: :action, foreign_key: :action_id, dependent: :destroy
|
4
6
|
|
5
7
|
validates :name, presence: true, uniqueness: true
|
6
|
-
|
7
|
-
attr_readonly :name
|
8
8
|
end
|
9
9
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Dynamican
|
2
|
+
class Filter < ActiveRecord::Base
|
3
|
+
self.table_name = 'dynamican_filters'
|
4
|
+
|
5
|
+
belongs_to :skimmable, polymorphic: true
|
6
|
+
belongs_to :item, class_name: 'Dynamican::Item', inverse_of: :filters, foreign_key: :item_id
|
7
|
+
has_many :rules
|
8
|
+
|
9
|
+
validates_presence_of :rules
|
10
|
+
|
11
|
+
scope :for_item, -> (item_name) { joins(:item).where(items: { name: item_name }) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module Dynamican
|
2
2
|
class Item < ActiveRecord::Base
|
3
|
+
self.table_name = 'dynamican_items'
|
4
|
+
|
3
5
|
has_and_belongs_to_many :permissions, class_name: 'Dynamican::Permission', inverse_of: :items, foreign_key: :item_id, dependent: :destroy
|
6
|
+
has_many :filters, class_name: 'Dynamican::Filter', inverse_of: :item, foreign_key: :item_id
|
4
7
|
|
5
8
|
validates :name, presence: true, uniqueness: true
|
6
9
|
|
7
|
-
attr_readonly :name
|
8
|
-
|
9
10
|
before_validation :classify_name
|
10
11
|
|
11
12
|
def classify_name
|
@@ -1,11 +1,13 @@
|
|
1
1
|
module Dynamican
|
2
2
|
class Permission < ActiveRecord::Base
|
3
|
+
self.table_name = 'dynamican_permissions'
|
4
|
+
|
3
5
|
belongs_to :permittable, polymorphic: true
|
4
6
|
belongs_to :action, class_name: 'Dynamican::Action', inverse_of: :permissions, foreign_key: :action_id
|
5
|
-
has_and_belongs_to_many :items, class_name: 'Dynamican::Item', inverse_of: :permissions, foreign_key: :permission_id
|
7
|
+
has_and_belongs_to_many :items, class_name: 'Dynamican::Item', inverse_of: :permissions, foreign_key: :permission_id, join_table: :dynamican_items_dynamican_permissions
|
6
8
|
has_many :conditions, class_name: 'Dynamican::Condition', inverse_of: :permission, foreign_key: :permission_id
|
7
9
|
|
8
|
-
scope :for_action, -> (action_name) { joins(:action).where(
|
10
|
+
scope :for_action, -> (action_name) { joins(:action).where(action: { name: action_name }) }
|
9
11
|
scope :for_item, -> (item_name) { joins(:items).where(items: { name: item_name.to_s.classify }) }
|
10
12
|
scope :without_item, -> { left_outer_joins(:items).where(items: { id: nil }) }
|
11
13
|
scope :conditional, -> { joins(:conditions) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamican
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Valerio Bellaveglia
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Dynamic and flexible database configurable permissions for your application
|
14
14
|
email:
|
@@ -21,13 +21,17 @@ files:
|
|
21
21
|
- dynamican.gemspec
|
22
22
|
- lib/dynamican.rb
|
23
23
|
- lib/dynamican/authorization.rb
|
24
|
-
- lib/dynamican/
|
25
|
-
- lib/dynamican/
|
24
|
+
- lib/dynamican/authorizer.rb
|
25
|
+
- lib/dynamican/permittable.rb
|
26
|
+
- lib/dynamican/skimmable.rb
|
27
|
+
- lib/dynamican/skimmer.rb
|
26
28
|
- lib/generators/dynamican_migration_generator.rb
|
27
29
|
- lib/models/dynamican/action.rb
|
28
30
|
- lib/models/dynamican/condition.rb
|
31
|
+
- lib/models/dynamican/filter.rb
|
29
32
|
- lib/models/dynamican/item.rb
|
30
33
|
- lib/models/dynamican/permission.rb
|
34
|
+
- lib/models/dynamican/rule.rb
|
31
35
|
homepage: https://github.com/ValerioBellaveglia/Dynamican
|
32
36
|
licenses:
|
33
37
|
- MIT
|
@@ -47,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
51
|
- !ruby/object:Gem::Version
|
48
52
|
version: '0'
|
49
53
|
requirements: []
|
50
|
-
rubygems_version: 3.
|
54
|
+
rubygems_version: 3.5.3
|
51
55
|
signing_key:
|
52
56
|
specification_version: 4
|
53
57
|
summary: Dynamic permissions
|