bullet_train-roles 0.1.3 → 0.1.6

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: f41c9b86402531e17f5853d967530968368985d44a5130f011c8d6d0844691a7
4
- data.tar.gz: 102b0571926e9a698b2654b8d54128b226e8c67ffdd10f626a18baf23b35c754
3
+ metadata.gz: 93fd2729ae287aa0389216798de23663aa3703bed3f5f11e571371c4b55bd30a
4
+ data.tar.gz: e094e75bec9daf0073e1d658dd0113b4743dfb7a138cf2f2e4eb34851ef85d23
5
5
  SHA512:
6
- metadata.gz: 92706fc57f271f24cdcc2a6c204bbfeb21fe79e0a0b2cedd480c8ea51c62157e93f4665fe7de98f0ad0ff8b96fc04cd6448d408415faced1936e4d68a0521583
7
- data.tar.gz: 50c64184545ecfcdab68be3163148ae0d1ee3727f23fb415cfa741418d66b1aed71946f4e3902413d118bd22abe2e6715840d2548835f72d3d4f019412297807
6
+ metadata.gz: 5a905f35fd471b0dfd2e82355f0ede399ac7d212825509ca180732392418d6af9d5821ae46808c85b183e89515f0aacd029142e1d3f9b23ea1e360a0c754fce9
7
+ data.tar.gz: 2cbcb69b7244c96854ae4841978ded8b7466e04c13a534de8819e35fd28a65b38c03e28f5dbdad9e039bfff5630702196d691f3e1b64490bab464c433d69e982
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: .
11
11
  specs:
12
- bullet_train-roles (0.1.2)
12
+ bullet_train-roles (0.1.6)
13
13
  active_hash
14
14
  activesupport
15
15
  cancancan
@@ -106,6 +106,8 @@ GEM
106
106
  nio4r (2.5.8)
107
107
  nokogiri (1.12.5-arm64-darwin)
108
108
  racc (~> 1.4)
109
+ nokogiri (1.12.5-x86_64-darwin)
110
+ racc (~> 1.4)
109
111
  parallel (1.21.0)
110
112
  parser (3.0.3.2)
111
113
  ast (~> 2.4.1)
@@ -174,6 +176,7 @@ GEM
174
176
  PLATFORMS
175
177
  arm64-darwin-20
176
178
  arm64-darwin-21
179
+ x86_64-darwin-21
177
180
 
178
181
  DEPENDENCIES
179
182
  active_hash!
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,10 +52,6 @@ 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, the generator will 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`.
data/bin/setup CHANGED
@@ -8,7 +8,7 @@ bundle install
8
8
  # We need to cd into the dummy app because it uses a different Rakefile
9
9
  cd test/dummy
10
10
  bundle install
11
- rake db:test:prepare
11
+ rake db:reset
12
12
  cd ../..
13
13
 
14
14
  # Do any other automated setup that you need to do here
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Roles
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.6"
5
5
  end
@@ -58,11 +58,6 @@ module BulletTrain
58
58
  adapter_name
59
59
  end
60
60
 
61
- def db_adapter_supports_defaults?
62
- supported_db_adapters = %w[postgresql]
63
- supported_db_adapters.include?(db_adapter)
64
- end
65
-
66
61
  def find_json_data_type_identifier
67
62
  adapter_name = db_adapter
68
63
 
@@ -95,14 +90,6 @@ module BulletTrain
95
90
  File.write(file_location, update_file_content.join)
96
91
  end
97
92
 
98
- def add_default_value_to_migration(file_name, table_name)
99
- file_location = Dir["db/migrate/*_#{file_name}.rb"].last
100
- line_to_match = "add_column :#{table_name.downcase}, :role_ids"
101
- content_to_add = ", default: []\n"
102
-
103
- add_in_file(file_location, line_to_match, content_to_add)
104
- end
105
-
106
93
  def migration_file_exists?(file_name)
107
94
  file_location = Dir["db/migrate/*_#{file_name}.rb"].last
108
95
 
@@ -124,8 +111,6 @@ module BulletTrain
124
111
 
125
112
  generate "migration", "#{migration_file_name} role_ids:#{json_data_type_identifier}"
126
113
 
127
- add_default_value_to_migration(migration_file_name, top_level_model_table_name) if db_adapter_supports_defaults?
128
-
129
114
  puts("Success 🎉🎉\n\n")
130
115
  end
131
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.role_ids -= [role.key]
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,40 @@ 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
- @condition = {parent_association => {id: @parent_ids}}
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
+ # using method_defined? will break with ActiveRecord 7 because now models have team_id defined if they include
176
+ # has_one :team, through: :membership
177
+ if @model.column_names.include?("#{parent_association}_id")
178
+ @condition = {"#{parent_association}_id".to_sym => @parent_ids}
179
+ else
180
+ @condition = {parent_association => {id: @parent_ids}}
181
+ end
192
182
  end
193
183
  end
194
184
  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 records that have a role "included" in a different role they have.
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.select(&:present?)
60
+ self.role_ids = role_ids&.select(&:present?) || []
48
61
 
49
62
  return if @allowed_roles.nil?
50
63
 
@@ -58,7 +71,7 @@ module Roles
58
71
  end
59
72
 
60
73
  def roles=(roles)
61
- self.role_ids = roles.map(&:key)
74
+ self.update(role_ids: roles.map(&:key))
62
75
  end
63
76
 
64
77
  def assignable_roles
@@ -66,7 +79,7 @@ module Roles
66
79
  end
67
80
 
68
81
  def roles_without_defaults
69
- role_ids.map { |role_id| Role.find(role_id) }
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.3
4
+ version: 0.1.6
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-02-10 00:00:00.000000000 Z
12
+ date: 2022-03-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: byebug
@@ -215,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
215
  - !ruby/object:Gem::Version
216
216
  version: '0'
217
217
  requirements: []
218
- rubygems_version: 3.2.22
218
+ rubygems_version: 3.3.0
219
219
  signing_key:
220
220
  specification_version: 4
221
221
  summary: Yaml-backed ApplicationHash for CanCan Roles