bullet_train-roles 0.1.1 → 0.1.5
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/Gemfile.lock +1 -1
- data/README.md +4 -8
- data/bin/setup +6 -0
- data/lib/bullet_train/roles/version.rb +1 -1
- data/lib/generators/bullet_train/roles/install/install_generator.rb +1 -11
- data/lib/models/role.rb +18 -30
- data/lib/roles/permit.rb +0 -8
- data/lib/roles/support.rb +23 -10
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51bbbee558714e45bdff2dadf3735cf8a2fce88d7e7e3a8144880c245bb8e384
|
4
|
+
data.tar.gz: 419402bc1f7560bc932c202275091456e4a68ddd271ddf7382df57b7f4b21b1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30bfee959c7c45e7dda23c7d4f71fa44a1efc9499e3bd5244e48e55895ff88e21fc26ade42def3591661c0aef1796762edae9a05625b1145822c5eb3a71cfc43
|
7
|
+
data.tar.gz: 6da43045a530531c8910f4c456f7c5b7211ac90f9cce69d05fc7a7456fedb664d099a951d0c5fdd131f4daa8f0784ac36add7837bfcc7760ff5193c22191c21c
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# Bullet Train Roles
|
2
2
|
|
3
|
-
Bullet Train Roles provides a Yaml-based configuration layer on top of [CanCanCan](https://github.com/CanCanCommunity/cancancan). You can use this configuration file to simplify the definition of many common permissions, while still implementing more complicated permissions in CanCanCan's traditional `app/model/ability.rb`.
|
3
|
+
Bullet Train Roles provides a Yaml-based configuration layer on top of [CanCanCan](https://github.com/CanCanCommunity/cancancan). You can use this configuration file to simplify the definition of many common permissions, while still implementing more complicated permissions in CanCanCan's traditional `app/model/ability.rb`.
|
4
4
|
|
5
5
|
Additionally, Bullet Train Roles makes it trivial to assign the same roles and associated permissions at different levels in your application. For example, you can assign someone administrative privileges at a team level, or only at a project level.
|
6
6
|
|
7
7
|
Bullet Train Roles was created by [Andrew Culver](http://twitter.com/andrewculver) and [Adam Pallozzi](https://twitter.com/adampallozzi).
|
8
8
|
|
9
|
-
## Example Domain Model
|
9
|
+
## Example Domain Model
|
10
10
|
|
11
11
|
For the sake of this document, we're going to assume the following example modeling around users and teams:
|
12
12
|
|
@@ -15,7 +15,7 @@ For the sake of this document, we're going to assume the following example model
|
|
15
15
|
- A `Membership` can have zero, one, or many `Role`s assigned.
|
16
16
|
- A `Membership` without a `Role` is just a default team member.
|
17
17
|
|
18
|
-
You don't have to name your models the same thing in order to use this Ruby Gem, but it does depend on having a similar structure.
|
18
|
+
You don't have to name your models the same thing in order to use this Ruby Gem, but it does depend on having a similar structure.
|
19
19
|
|
20
20
|
> If you're interested in reading more about how and why Bullet Train implements this structure, you can [read about it on our blog](https://blog.bullettrain.co/teams-should-be-an-mvp-feature/).
|
21
21
|
|
@@ -52,15 +52,11 @@ The installer will:
|
|
52
52
|
- add a basic `permit` call in `app/models/ability.rb`.
|
53
53
|
|
54
54
|
|
55
|
-
### Limitations
|
56
|
-
|
57
|
-
The generators currently assume you're using PostgreSQL and `jsonb` will be available when generating a `role_ids` column. If you're using MySQL, you can edit these migrations and use `json` instead, although you won't be able to set a default value and you'll need to take care of this in the model.
|
58
|
-
|
59
55
|
## Usage
|
60
56
|
|
61
57
|
The provided `Role` model is backed by a Yaml configuration in `config/models/roles.yml`.
|
62
58
|
|
63
|
-
To help explain this configuration and
|
59
|
+
To help explain this configuration and its options, we'll provide the following hypothetical example:
|
64
60
|
|
65
61
|
```
|
66
62
|
default:
|
data/bin/setup
CHANGED
@@ -47,7 +47,7 @@ module BulletTrain
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def db_adapter
|
50
|
-
allowed_adapter_types = %w[mysql sqlite postgresql]
|
50
|
+
allowed_adapter_types = %w[mysql mysql2 sqlite postgresql]
|
51
51
|
|
52
52
|
adapter_name = ActiveRecord::Base.connection.adapter_name.downcase
|
53
53
|
|
@@ -90,14 +90,6 @@ module BulletTrain
|
|
90
90
|
File.write(file_location, update_file_content.join)
|
91
91
|
end
|
92
92
|
|
93
|
-
def add_default_value_to_migration(file_name, table_name)
|
94
|
-
file_location = Dir["db/migrate/*_#{file_name}.rb"].last
|
95
|
-
line_to_match = "add_column :#{table_name.downcase}, :role_ids"
|
96
|
-
content_to_add = ", default: []\n"
|
97
|
-
|
98
|
-
add_in_file(file_location, line_to_match, content_to_add)
|
99
|
-
end
|
100
|
-
|
101
93
|
def migration_file_exists?(file_name)
|
102
94
|
file_location = Dir["db/migrate/*_#{file_name}.rb"].last
|
103
95
|
|
@@ -119,8 +111,6 @@ module BulletTrain
|
|
119
111
|
|
120
112
|
generate "migration", "#{migration_file_name} role_ids:#{json_data_type_identifier}"
|
121
113
|
|
122
|
-
add_default_value_to_migration(migration_file_name, top_level_model_table_name)
|
123
|
-
|
124
114
|
puts("Success 🎉🎉\n\n")
|
125
115
|
end
|
126
116
|
|
data/lib/models/role.rb
CHANGED
@@ -21,17 +21,12 @@ class Role < ActiveYaml::Base
|
|
21
21
|
|
22
22
|
def self.includes(role_or_key)
|
23
23
|
role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key
|
24
|
-
|
25
24
|
role = Role.find_by_key(role_key)
|
26
|
-
|
27
25
|
return Role.all.select(&:assignable?) if role.default?
|
28
|
-
|
29
26
|
result = []
|
30
|
-
|
31
27
|
all.each do |role|
|
32
28
|
result << role if role.includes.include?(role_key)
|
33
29
|
end
|
34
|
-
|
35
30
|
result
|
36
31
|
end
|
37
32
|
|
@@ -49,6 +44,10 @@ class Role < ActiveYaml::Base
|
|
49
44
|
key
|
50
45
|
end
|
51
46
|
|
47
|
+
def to_s
|
48
|
+
key
|
49
|
+
end
|
50
|
+
|
52
51
|
def included_by
|
53
52
|
Role.includes(self)
|
54
53
|
end
|
@@ -69,20 +68,15 @@ class Role < ActiveYaml::Base
|
|
69
68
|
|
70
69
|
def included_roles
|
71
70
|
default_roles = []
|
72
|
-
|
73
71
|
default_roles << Role.default unless default?
|
74
|
-
|
75
72
|
(default_roles + includes.map { |included_key| Role.find_by_key(included_key) }).uniq.compact
|
76
73
|
end
|
77
74
|
|
78
75
|
def manageable_by?(role_or_roles)
|
79
76
|
return true if default?
|
80
|
-
|
81
77
|
roles = role_or_roles.is_a?(Array) ? role_or_roles : [role_or_roles]
|
82
|
-
|
83
78
|
roles.each do |role|
|
84
79
|
return true if role.manageable_roles.include?(key)
|
85
|
-
|
86
80
|
role.included_roles.each do |included_role|
|
87
81
|
return true if manageable_by?([included_role])
|
88
82
|
end
|
@@ -109,23 +103,21 @@ class Role < ActiveYaml::Base
|
|
109
103
|
class Collection < Array
|
110
104
|
def initialize(model, ary)
|
111
105
|
@model = model
|
112
|
-
|
113
106
|
super(ary)
|
114
107
|
end
|
115
108
|
|
116
109
|
def <<(role)
|
117
110
|
return true if include?(role)
|
118
|
-
|
119
|
-
role_ids = @model.role_ids
|
120
|
-
|
111
|
+
role_ids = @model.role_ids || []
|
121
112
|
role_ids << role.id
|
122
|
-
|
123
113
|
@model.update(role_ids: role_ids)
|
124
114
|
end
|
125
115
|
|
126
116
|
def delete(role)
|
127
|
-
@model.
|
128
|
-
|
117
|
+
return @model.save unless include?(role)
|
118
|
+
current_role_ids = @model.role_ids || []
|
119
|
+
new_role_ids = current_role_ids - [role.key]
|
120
|
+
@model.role_ids = new_role_ids
|
129
121
|
@model.save
|
130
122
|
end
|
131
123
|
end
|
@@ -153,42 +145,38 @@ class Role < ActiveYaml::Base
|
|
153
145
|
|
154
146
|
def actions
|
155
147
|
return @actions if @actions
|
156
|
-
|
157
148
|
actions = (@ability_data["actions"] if @ability_data.is_a?(Hash)) || @ability_data
|
158
|
-
|
159
149
|
actions = [actions] unless actions.is_a?(Array)
|
160
|
-
|
161
150
|
@actions = actions.map!(&:to_sym)
|
162
151
|
end
|
163
152
|
|
164
153
|
def possible_parent_associations
|
165
154
|
ary = @parent.to_s.split("::").map(&:underscore)
|
166
|
-
|
167
155
|
possibilities = []
|
168
156
|
current = nil
|
169
|
-
|
170
157
|
until ary.empty?
|
171
158
|
current = "#{ary.pop}#{"_" unless current.nil?}#{current}"
|
172
159
|
possibilities << current
|
173
160
|
end
|
174
|
-
|
175
161
|
possibilities.map(&:to_sym)
|
176
162
|
end
|
177
163
|
|
178
164
|
def condition
|
179
165
|
return @condition if @condition
|
180
|
-
|
181
166
|
return nil unless @parent_ids
|
182
|
-
|
183
167
|
if @model == @parent
|
184
168
|
return @condition = {id: @parent_ids}
|
185
169
|
end
|
186
|
-
|
187
|
-
parent_association = possible_parent_associations.find { |association| @model.method_defined? association }
|
188
|
-
|
170
|
+
parent_association = possible_parent_associations.find { |association| @model.method_defined?(association) || @model.method_defined?("#{association}_id") }
|
189
171
|
return nil unless parent_association.present?
|
190
|
-
|
191
|
-
|
172
|
+
# If possible, use the team_id attribute because it saves us having to join all the way back to the sorce parent model
|
173
|
+
# In some scenarios this may be quicker, or if the parent model is in a different database shard, it may not even
|
174
|
+
# be possible to do the join
|
175
|
+
if @model.method_defined?("#{parent_association}_id")
|
176
|
+
@condition = {"#{parent_association}_id".to_sym => @parent_ids}
|
177
|
+
else
|
178
|
+
@condition = {parent_association => {id: @parent_ids}}
|
179
|
+
end
|
192
180
|
end
|
193
181
|
end
|
194
182
|
end
|
data/lib/roles/permit.rb
CHANGED
@@ -8,23 +8,18 @@ module Roles
|
|
8
8
|
|
9
9
|
# When changing permissions during development, you may also want to do this on each request:
|
10
10
|
# User.update_all ability_cache: nil if Rails.env.development?
|
11
|
-
|
12
11
|
output = []
|
13
12
|
added_roles = Set.new
|
14
|
-
|
15
13
|
user.send(through).map(&:roles).flatten.uniq.each do |role|
|
16
14
|
unless added_roles.include?(role)
|
17
15
|
output << "########### ROLE: #{role.key}"
|
18
|
-
|
19
16
|
output += add_abilities_for(role, user, through, parent)
|
20
|
-
|
21
17
|
added_roles << role
|
22
18
|
end
|
23
19
|
|
24
20
|
role.included_roles.each do |included_role|
|
25
21
|
unless added_roles.include?(included_role)
|
26
22
|
output << "############# INCLUDED ROLE: #{included_role.key}"
|
27
|
-
|
28
23
|
output += add_abilities_for(included_role, user, through, parent)
|
29
24
|
end
|
30
25
|
end
|
@@ -40,17 +35,14 @@ module Roles
|
|
40
35
|
|
41
36
|
def add_abilities_for(role, user, through, parent)
|
42
37
|
output = []
|
43
|
-
|
44
38
|
role.ability_generator(user, through, parent) do |ag|
|
45
39
|
if ag.valid?
|
46
40
|
output << "can #{ag.actions}, #{ag.model}, #{ag.condition}"
|
47
|
-
|
48
41
|
can(ag.actions, ag.model, ag.condition)
|
49
42
|
else
|
50
43
|
output << "# #{ag.model} does not respond to #{parent} so we're not going to add an ability for the #{through} context"
|
51
44
|
end
|
52
45
|
end
|
53
|
-
|
54
46
|
output
|
55
47
|
end
|
56
48
|
end
|
data/lib/roles/support.rb
CHANGED
@@ -13,16 +13,13 @@ module Roles
|
|
13
13
|
|
14
14
|
def assignable_roles
|
15
15
|
return Role.assignable if @allowed_roles.nil?
|
16
|
-
|
17
16
|
Role.assignable.select { |role| @allowed_roles.include?(role.key.to_sym) }
|
18
17
|
end
|
19
18
|
|
20
19
|
# Note default_role is an ActiveRecord core class method so we need to use something else here
|
21
20
|
def default_roles
|
22
21
|
default_role = Role.default
|
23
|
-
|
24
22
|
return [default_role] if @allowed_roles.nil?
|
25
|
-
|
26
23
|
@allowed_roles.include?(default_role.key.to_sym) ? [default_role] : []
|
27
24
|
end
|
28
25
|
end
|
@@ -30,11 +27,7 @@ module Roles
|
|
30
27
|
included do
|
31
28
|
validate :validate_roles
|
32
29
|
|
33
|
-
# This query will return
|
34
|
-
# For example, if you do with_roles(editor) it will return admin users if the admin role includes the editor role
|
35
|
-
scope :with_roles, ->(roles) { where("#{table_name}.role_ids ?| array[:keys]", keys: roles.map(&:key_plus_included_by_keys).flatten.uniq.map(&:to_s)) }
|
36
|
-
|
37
|
-
# This query will return roles that include the given role. See with_roles above for details
|
30
|
+
# This query will return roles that include the given role. See self.with_roles below for details
|
38
31
|
scope :with_role, ->(role) { role.nil? ? all : with_roles([role]) }
|
39
32
|
scope :viewers, -> { where("#{table_name}.role_ids = ?", [].to_json) }
|
40
33
|
scope :editors, -> { with_role(Role.find_by_key("editor")) }
|
@@ -43,8 +36,28 @@ module Roles
|
|
43
36
|
after_save :invalidate_cache
|
44
37
|
after_destroy :invalidate_cache
|
45
38
|
|
39
|
+
# This query will return records that have a role "included" in a different role they have.
|
40
|
+
# For example, if you do with_roles(editor) it will return admin users if the admin role includes the editor role
|
41
|
+
def self.with_roles(roles)
|
42
|
+
# Mysql and postgres have different syntax for searching json or jsonb columns so we need different queries depending on the database
|
43
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?("mysql") ? with_roles_mysql(roles) : with_roles_postgres(roles)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.with_roles_mysql(roles)
|
47
|
+
queries = []
|
48
|
+
roles.map(&:key_plus_included_by_keys).flatten.uniq.map(&:to_s).each do |role|
|
49
|
+
queries << "JSON_CONTAINS(#{table_name}.role_ids, '\"#{role}\"')"
|
50
|
+
end
|
51
|
+
query = queries.join(" OR ")
|
52
|
+
where(query)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.with_roles_postgres(roles)
|
56
|
+
where("#{table_name}.role_ids ?| array[:keys]", keys: roles.map(&:key_plus_included_by_keys).flatten.uniq.map(&:to_s))
|
57
|
+
end
|
58
|
+
|
46
59
|
def validate_roles
|
47
|
-
self.role_ids = role_ids
|
60
|
+
self.role_ids = role_ids&.select(&:present?) || []
|
48
61
|
|
49
62
|
return if @allowed_roles.nil?
|
50
63
|
|
@@ -66,7 +79,7 @@ module Roles
|
|
66
79
|
end
|
67
80
|
|
68
81
|
def roles_without_defaults
|
69
|
-
role_ids
|
82
|
+
role_ids&.map { |role_id| Role.find(role_id) } || []
|
70
83
|
end
|
71
84
|
|
72
85
|
def manageable_roles
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bullet_train-roles
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Prabin Poudel
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-02-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: byebug
|