dynamican 1.0.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57e35df3eaa7632f3e703de8ee81fa03205baf0f34678cc6f1cb7e4d1de30519
4
- data.tar.gz: 6a0f272dba7e3c6fb4575c1b491cd4aa1b7c82c074a28cbde012e6233120d4bf
3
+ metadata.gz: 07765d12ff4c1d8450e780bc29414d1db951b7ae2645876de19b723f92a9d688
4
+ data.tar.gz: 784a582fab983684cd45d41205e9a2a050036e13bad45d70ebd3f88a15be4d66
5
5
  SHA512:
6
- metadata.gz: a8302dc81c027a3256e4d943f2633c5d4a311dfebcc274c69c4f831607bd6ee68d4ab69615d8e66b2923e9fef1c472f16208855f07b30e5e5ac080300b78267a
7
- data.tar.gz: '09ef56baf0b012223b1c562bfc1f57d2559bb9009ccb311322eab8750baa894c922f21f32b5e540a162e360e4b714b71a14f5ac764c52d6d7535f56b299c0f51'
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
- In each model you want to have the feature, just put the following.
17
+ ## Permissions
18
18
 
19
- include Dynamican::Model
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 personally 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.
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
- ## Usage
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
- Then create 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.
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 usage
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 = '1.0.2'
4
- s.date = '2021-06-30'
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
- class Evaluator
2
+ class Authorizer
3
3
  attr_reader :subject, :action, :item, :item_name, :conditions_instances
4
4
 
5
5
  def initialize(subject, action, item, conditions_instances = {})
@@ -32,15 +32,11 @@ module Dynamican
32
32
  end
33
33
 
34
34
  def calculate_item_name
35
- class_name = if item.class.in? [Symbol, String, Class]
35
+ if item.class.in? [Symbol, String, Class]
36
36
  item.to_s.classify
37
37
  else
38
38
  item.class.name.demodulize
39
39
  end
40
-
41
- given_item_name = item.class.try(:dynamican_item_name)
42
-
43
- given_item_name || class_name
44
40
  end
45
41
  end
46
42
  end
@@ -1,5 +1,5 @@
1
1
  module Dynamican
2
- module Model
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::Evaluator.new(self, action, item, conditions_instances).evaluate
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/evaluator'
3
- require 'dynamican/model'
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
- class DynamicanMigration < ActiveRecord::Migration[5.2]
14
- def change
15
- unless table_exists? :permissions
16
- create_table :permissions do |t|
17
- t.bigint :permittable_id
18
- t.string :permittable_type
19
- t.bigint :action_id
20
-
21
- t.timestamps
22
- end
23
-
24
- create_table :actions do |t|
25
- t.string :name
26
-
27
- t.timestamps
28
- end
29
-
30
- create_table :items do |t|
31
- t.string :name
32
-
33
- t.timestamps
34
- end
35
-
36
- create_table :conditions do |t|
37
- t.bigint :permission_id
38
- t.string :statement
39
- t.string :description
40
-
41
- t.timestamps
42
- end
43
-
44
- create_table :items_permissions do |t|
45
- t.bigint :item_id
46
- t.bigint :permission_id
47
- end
48
- end
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
@@ -1,5 +1,7 @@
1
1
  module Dynamican
2
2
  class Condition < ActiveRecord::Base
3
+ self.table_name = 'dynamican_conditions'
4
+
3
5
  belongs_to :permission, class_name: 'Dynamican::Permission', inverse_of: :conditions, foreign_key: :permission_id
4
6
 
5
7
  validates_presence_of :statement
@@ -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(actions: { name: action_name }) }
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) }
@@ -0,0 +1,10 @@
1
+ module Dynamican
2
+ class Rule < ActiveRecord::Base
3
+ self.table_name = 'dynamican_rules'
4
+
5
+ belongs_to :filter, class_name: 'Dynamican::Filter', inverse_of: :rule, foreign_key: :filter_id
6
+
7
+ validates_presence_of :statement
8
+ end
9
+ end
10
+
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: 1.0.2
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: 2021-06-30 00:00:00.000000000 Z
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/evaluator.rb
25
- - lib/dynamican/model.rb
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.2.3
54
+ rubygems_version: 3.5.3
51
55
  signing_key:
52
56
  specification_version: 4
53
57
  summary: Dynamic permissions