dynamican 0.1.7

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6dd7da02224c7b3e20e8b582f5929657255d33d4ddf81037a09e2c303df7182
4
+ data.tar.gz: 1c7f34c8b6335a89c21087243bdcaec746966ff76c7bc076204aefa736fce121
5
+ SHA512:
6
+ metadata.gz: ea646968f0358bada82722bda78c2895bd6763428823f02ab51866caedbd45e7af0f411b5e77850e2bb71f83751503922aed96cb39b906fd344ebcca453154d9
7
+ data.tar.gz: 3c29cc459e4aff80101d2ec436deba04c3600410e030cc4e3cf09ad575df8b3cd1f93602a8639937930733c82513a6ea8293eeddbb309cd9cb5afe7159bfd329
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ dynamican-*
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Dynamican
2
+ Dynamican is a flexible gem to introduce permissions on rails applications. It is very customizable because it stores all the rules inside the database and can be added to multiple models: for example you can add permissions to your Role model or to users. With a couple of overrides you can also add permissions to both Role and User and use role permissions through user.
3
+
4
+ ## Installation
5
+ Inside your Gemfile put
6
+
7
+ gem 'dynamican'
8
+
9
+ then run `bundle install`
10
+
11
+ Once you have the gem, open the source code and look for migrations folder. You need to create in your project those migrations and run them. Obviously if you already have the gem installed and you are only updating, you only need to create the migrations for the later versions of the gem.
12
+
13
+ In each model you want to have the feature, just put
14
+
15
+ include Dynamican::Model
16
+
17
+ Create `config/dynamican.yml` file and compile it as follows for each of the models you included the code above into (let's say for instance you included Dynamican::Model into User and Role models)
18
+
19
+ associations:
20
+ role:
21
+ class_name: 'Role'
22
+ user:
23
+ class_name: 'User'
24
+
25
+ ### Using permissions on one model through another model
26
+
27
+ I wanted to have the possibility to assign permissions both directly to my User model and my Role model, so that if the User had one permission and one of its Role had another, the User could benefit from both. So i assigned the feature (as explained above) to both models and then decorated my User model like this
28
+
29
+ module Decorators
30
+ module Models
31
+ module User
32
+ module DynamicanOverrides
33
+ def self.prepended(base)
34
+ base.class_eval do
35
+ has_many :user_permission_connectors, class_name: 'Dynamican::PermissionConnector'
36
+ has_many :user_permissions, class_name: 'Dynamican::Permission', through: :user_permission_connectors, source: :permission
37
+ has_many :role_permissions, through: :roles, class_name: 'Dynamican::Permission', source: :permissions
38
+ end
39
+ end
40
+
41
+ def permission_connectors
42
+ Dynamican::PermissionConnector.where(id: user_permission_connectors.ids + roles.map(&:permission_connectors).flatten.map(&:id))
43
+ end
44
+
45
+ def permissions
46
+ Dynamican::Permission.where(id: user_permissions.ids + role_permissions.ids)
47
+ end
48
+
49
+ ::User.prepend self
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ I personally have put this in `app/decorators/models/user/dynamican_overrides.rb` (you need to load the folder if you don't already have it) but you can make it work as you please. I recommend to keep it separate from the original User model though.
56
+
57
+ ## Usage
58
+
59
+ Now the hard part: the real configuration.
60
+
61
+ Create one `Dynamican::Permission` for each pair of action-object you need. For example i created CRUD permissions for my models. Let's say, for instance, you have the Order model.
62
+
63
+ p1 = Dynamican::Permission.create(action: 'create', object_name: 'order')
64
+ p2 = Dynamican::Permission.create(action: 'read', object_name: 'order')
65
+ p3 = Dynamican::Permission.create(action: 'update', object_name: 'order')
66
+ p4 = Dynamican::Permission.create(action: 'delete', object_name: 'order')
67
+
68
+ To assign one of these permissions to your Role or User, you need to create a `Dynamican::PermissionConnector` like this
69
+
70
+ user.permission_connectors.create(permission: p1)
71
+
72
+ Or simply
73
+
74
+ user.permissions << p1
75
+
76
+ Now your user is considered able to create orders and you can check the permissions:
77
+
78
+ user.can? :create, :order
79
+ # Returns true
80
+
81
+ user.can? :read, :order
82
+ # Returns false
83
+
84
+ You can pass as second argument (the object) a symbol, a string, the class itself and also the instance (instances are used for condition evaluations).
85
+ You can also pass an array of these elements and permissions for all elements will be evaluated. The `can?` method will return true only if all permissions are evaluated positively.
86
+ You can also create custom permissions which don't need an object, simply leaving the `object_name` empty. Let's say you want to give permission to a certain user to dance
87
+
88
+ p5 = Dynamican::Permission.create(action: 'dance')
89
+
90
+ user.permissions << p5
91
+
92
+ Call the `can?` method without any second argument
93
+
94
+ user.can? :dance
95
+
96
+ # Returns true
97
+
98
+
99
+ ## Conditions
100
+
101
+ You can link a `Dynamican::PermissionConnector` to as many conditions as you want.
102
+
103
+ Conditions are created like this
104
+
105
+ permission_connector.conditions.create(statement: '@user.orders.count < 5')
106
+ permission_connector.conditions.create(statement: '@object.field.present?')
107
+
108
+ You can store in conditions statements whatever conditions you like in plain ruby and the string will be evaulated. Inside the statements, object have to be called as instance variables. These instance variables need to be present and can be declared as follows:
109
+
110
+ 1. The model name of the instance you called `can?` from, like the user, will be defined automatically based on model name so if you call `user.can?` you will have `@user` variable defined.
111
+ 2. The object you pass as second argument (which should match the `object_name` of your permission) will be defined as `@object`, so if you call `user.can? :read, @order` you will have `@object` variable defined containing your `@order`.
112
+ 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.
113
+
114
+ WARNING: since the condition statement gets evaluated, i recommend not to allow anyone except project developers to create conditions to prevent malicious code from being executed.
115
+
116
+ If one `Dynamican::PermissionConnector` 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 statement, like this:
117
+
118
+ condition.statement = '@user.nice? || @user.polite?'
data/dynamican.gemspec ADDED
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'dynamican'
3
+ s.version = '0.1.7'
4
+ s.date = '2020-04-12'
5
+ s.summary = "Dynamic permissions"
6
+ s.description = "Dynamic and flexible database configurable permissions for your application"
7
+ s.authors = ["Valerio Bellaveglia"]
8
+ s.files = `git ls-files`.split("\n")
9
+ s.homepage = 'https://github.com/ValerioBellaveglia/Dynamican'
10
+ s.require_path = 'lib'
11
+ s.license = 'MIT'
12
+ end
@@ -0,0 +1,17 @@
1
+ module Dynamican
2
+ def self.configuration
3
+ @configuration ||= Configuration.new(configuration_hash)
4
+ end
5
+
6
+ def self.configuration_hash
7
+ @configuration_hash ||= HashWithIndifferentAccess.new(YAML.load_file('config/dynamican.yml'))
8
+ end
9
+
10
+ class Configuration
11
+ attr_accessor :associations
12
+
13
+ def initialize(configuration_hash)
14
+ @associations = configuration_hash[:associations]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ module Dynamican
2
+ class Evaluator
3
+ attr_reader :subject, :action, :object, :object_name, :conditions_instances
4
+
5
+ def initialize(subject, action, object, conditions_instances = {})
6
+ @subject = subject
7
+ @action = action
8
+ @object = object
9
+ @object_name = calculate_object_name
10
+ @conditions_instances = conditions_instances
11
+ end
12
+
13
+ def evaluate
14
+ set_instance_variables
15
+
16
+ matching_connectors = subject.permission_connectors.for_action(action).for_object(object_name)
17
+ matching_conditions_statements = matching_connectors.conditional.map(&:conditions).flatten.map(&:statement)
18
+
19
+ matching_connectors.unconditional.any? ||
20
+ matching_conditions_statements.any? && matching_conditions_statements.map { |statement| eval statement }.all?
21
+ end
22
+
23
+ private
24
+
25
+ def set_instance_variables
26
+ instance_variable_set("@#{subject.model_name.element}", subject)
27
+
28
+ conditions_instances.each do |instance_name, instance_object|
29
+ instance_variable_set("@#{instance_name}", instance_object)
30
+ end
31
+ end
32
+
33
+ def calculate_object_name
34
+ if object.class.in? [Symbol, String, Class]
35
+ object.to_s.downcase
36
+ elsif object.is_a?(NilClass)
37
+ nil
38
+ else
39
+ object.class.to_s.downcase
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ module Dynamican
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :permission_connectors, class_name: 'Dynamican::PermissionConnector'
7
+ has_many :permissions, class_name: 'Dynamican::Permission', through: :permission_connectors, source: :permission
8
+ end
9
+
10
+ def can?(action, object = nil, conditions_instances = {})
11
+ if object.respond_to? :each
12
+ object.all? { |single_object| can? action, single_object }
13
+ else
14
+ Dynamican::Evaluator.new(self, action, object, conditions_instances).evaluate
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/dynamican.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'dynamican/configuration'
2
+ require 'dynamican/evaluator'
3
+ require 'dynamican/model'
4
+ require 'models/dynamican/condition'
5
+ require 'models/dynamican/permission_connector'
6
+ require 'models/dynamican/permission'
@@ -0,0 +1,5 @@
1
+ module Dynamican
2
+ class Condition < ActiveRecord::Base
3
+ has_and_belongs_to_many :permission_connectors, class_name: 'Dynamican::PermissionConnector', foreign_key: :condition_id
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ module Dynamican
2
+ class Permission < ActiveRecord::Base
3
+ has_many :permission_connectors, class_name: 'Dynamican::PermissionConnector', inverse_of: :permission, foreign_key: :permission_id
4
+
5
+ validates_presence_of :action
6
+
7
+ scope :for_action, -> (actions) { where(action: actions) }
8
+ scope :for_object, -> (object_names) { where(object_name: object_names) }
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ module Dynamican
2
+ class PermissionConnector < ActiveRecord::Base
3
+ Dynamican.configuration.associations.each do |association_name, association_options|
4
+ belongs_to association_name.to_sym, class_name: association_options[:class_name], inverse_of: association_options[:inverse_of], foreign_key: "#{association_name}_id".to_sym, optional: true
5
+ end
6
+ has_and_belongs_to_many :conditions, class_name: 'Dynamican::Condition', inverse_of: :permission_connectors, foreign_key: :permission_connector_id
7
+ belongs_to :permission, class_name: 'Dynamican::Permission', inverse_of: :permission_connectors, foreign_key: :permission_id
8
+
9
+ scope :conditional, -> { where(conditional: true) }
10
+ scope :unconditional, -> { where(conditional: false) }
11
+ scope :for_action, -> (action) { joins(:permission).where(permissions: { action: action }) }
12
+ scope :for_object, -> (object_name) { joins(:permission).where(permissions: { object_name: object_name }) }
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ class DynamicanCreateDatabaseStructure < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :conditions do |t|
4
+ t.string :description
5
+ t.string :statement
6
+
7
+ t.timestamps
8
+ end
9
+
10
+ create_table :permissions do |t|
11
+ t.string :action
12
+ t.string :object_name
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ create_table :permission_connectors do |t|
18
+ t.references :user
19
+ t.references :role
20
+ t.references :permission
21
+ t.boolean :conditional, default: false
22
+
23
+ t.timestamps
24
+ end
25
+
26
+ create_table :conditions_permission_connectors do |t|
27
+ t.bigint :condition_id
28
+ t.bigint :permission_connector_id
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamican
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.7
5
+ platform: ruby
6
+ authors:
7
+ - Valerio Bellaveglia
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Dynamic and flexible database configurable permissions for your application
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".gitignore"
20
+ - README.md
21
+ - dynamican.gemspec
22
+ - lib/dynamican.rb
23
+ - lib/dynamican/configuration.rb
24
+ - lib/dynamican/evaluator.rb
25
+ - lib/dynamican/model.rb
26
+ - lib/models/dynamican/condition.rb
27
+ - lib/models/dynamican/permission.rb
28
+ - lib/models/dynamican/permission_connector.rb
29
+ - migrations/0.1.2/dynamican_create_database_structure.rb
30
+ homepage: https://github.com/ValerioBellaveglia/Dynamican
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.0.2
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: Dynamic permissions
53
+ test_files: []