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 +7 -0
- data/.gitignore +1 -0
- data/README.md +118 -0
- data/dynamican.gemspec +12 -0
- data/lib/dynamican/configuration.rb +17 -0
- data/lib/dynamican/evaluator.rb +43 -0
- data/lib/dynamican/model.rb +18 -0
- data/lib/dynamican.rb +6 -0
- data/lib/models/dynamican/condition.rb +5 -0
- data/lib/models/dynamican/permission.rb +10 -0
- data/lib/models/dynamican/permission_connector.rb +14 -0
- data/migrations/0.1.2/dynamican_create_database_structure.rb +31 -0
- metadata +53 -0
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,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: []
|