slow_your_roles 2.0.2

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.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlowYourRoles
4
+ module Generators
5
+ # Generator class to add SlowYourRoles to an ActiveRecord model.
6
+ class SlowYourRolesGenerator < Rails::Generators::Base
7
+ namespace 'slow_your_roles'
8
+
9
+ argument :role_col, type: :string, required: false, default: 'roles', banner: 'role column'
10
+
11
+ class_option :use_bitmask_method, type: :boolean, required: false, default: false,
12
+ desc: 'Setup migration for Bitmask method'
13
+
14
+ desc 'Create ActiveRecord migration for slow_your_roles on NAME model using \
15
+ [ROLE] column -- defaults to \'roles\''
16
+
17
+ source_root File.expand_path('../templates', __dir__)
18
+
19
+ hook_for :orm
20
+
21
+ def show_readme
22
+ readme 'README' if behavior == :invoke
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+
2
+
3
+ ===============================================
4
+
5
+ Now run rake db:migrate
6
+
7
+ ===============================================
8
+
9
+
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlowYourRoles
4
+ # Bitmask support
5
+ class Bitmask
6
+ def initialize(base, column_name, _options)
7
+ base.send :define_method, :_roles= do |roles|
8
+ states = base.const_get(column_name.upcase.to_sym)
9
+ self[column_name.to_sym] = (roles & states).map { |r| 2**states.index(r) }.sum
10
+ end
11
+
12
+ base.send :define_method, :_roles do
13
+ states = base.const_get(column_name.upcase.to_sym)
14
+ masked_integer = self[column_name.to_sym] || 0
15
+ states.reject.with_index { |_r, i| masked_integer[i].zero? }
16
+ end
17
+
18
+ base.send :define_method, :has_role? do |role|
19
+ _roles.include?(role)
20
+ end
21
+
22
+ base.send :define_method, :add_role do |*roles|
23
+ roles.each do |role|
24
+ self._roles = _roles.push(role).uniq
25
+ end
26
+ end
27
+
28
+ base.send :define_method, :add_role! do |*roles|
29
+ roles.each do |role|
30
+ add_role(role)
31
+ end
32
+ save!
33
+ end
34
+
35
+ base.send :define_method, :remove_role do |role|
36
+ new_roles = _roles
37
+ new_roles.delete(role)
38
+ self._roles = new_roles
39
+ end
40
+
41
+ base.send :define_method, :remove_role! do |role|
42
+ remove_role(role)
43
+ save!
44
+ end
45
+
46
+ base.send :define_method, :clear_roles do
47
+ self[column_name.to_sym] = 0
48
+ end
49
+
50
+ base.send :define_method, :clear_roles! do
51
+ clear_roles
52
+ save!
53
+ end
54
+
55
+ base.class_eval do
56
+ alias_method :add_roles, :add_role
57
+ alias_method :add_roles!, :add_role
58
+
59
+ scope :with_role, (proc { |role|
60
+ states = base.const_get(column_name.upcase.to_sym)
61
+ raise ArgumentError unless states.include? role
62
+
63
+ role_bit_index = states.index(role)
64
+ valid_mask_integers = (0..2**states.count - 1).select { |i| i[role_bit_index] == 1 }
65
+ where(column_name => valid_mask_integers)
66
+ })
67
+ scope :without_role, (proc { |role|
68
+ states = base.const_get(column_name.upcase.to_sym)
69
+ raise ArgumentError unless states.include? role
70
+
71
+ role_bit_index = states.index(role)
72
+ valid_mask_integers = (0..2**states.count - 1).reject { |i| i[role_bit_index] == 1 }
73
+ where(column_name => valid_mask_integers)
74
+ })
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlowYourRoles
4
+ # Serialize support
5
+ class Serialize
6
+ def initialize(base, column_name, _options)
7
+ base.serialize column_name.to_sym, Array
8
+ base.before_validation(:make_default_roles, on: :create)
9
+ base.send :define_method, :has_role? do |role|
10
+ self[column_name.to_sym].include?(role)
11
+ end
12
+
13
+ base.send :define_method, :add_role do |*roles|
14
+ clear_roles if self[column_name.to_sym].blank?
15
+
16
+ roles.each do |role|
17
+ return false if !roles_marker.empty? && role.include?(roles_marker)
18
+ end
19
+
20
+ roles.each do |role|
21
+ next if has_role?(role)
22
+
23
+ self[column_name.to_sym] << role
24
+ end
25
+
26
+ self[column_name.to_sym]
27
+ end
28
+
29
+ base.send :define_method, :add_role! do |role|
30
+ return false unless add_role(role)
31
+
32
+ save!
33
+ end
34
+
35
+ base.send :define_method, :remove_role do |role|
36
+ self[column_name.to_sym].delete(role)
37
+ end
38
+
39
+ base.send :define_method, :remove_role! do |role|
40
+ remove_role(role)
41
+ save!
42
+ end
43
+
44
+ base.send :define_method, :clear_roles do
45
+ self[column_name.to_sym] = []
46
+ end
47
+
48
+ base.send :define_method, :make_default_roles do
49
+ clear_roles if self[column_name.to_sym].blank?
50
+ end
51
+
52
+ base.send :private, :make_default_roles
53
+
54
+ # Scopes:
55
+ # ---------
56
+ # For security, wrapping markers must be included in the LIKE search,
57
+ # otherwise a user with role 'administrator' would erroneously be included
58
+ # in `User.with_scope('admin')`.
59
+ #
60
+ # Rails uses YAML for serialization, so the markers are newlines.
61
+ # Unfortunately, sqlite can't match newlines reliably, and it doesn't
62
+ # natively support REGEXP. Therefore, hooks are currently being used to
63
+ # wrap roles in '!' markers when talking to the database. This is hacky,
64
+ # but unavoidable. The implication is that, for security, it must be
65
+ # actively enforced that role names cannot include the '!' character.
66
+ #
67
+ # An alternative would be to use JSON instead of YAML to serialize the
68
+ # data, but I've wrestled countless SerializationTypeMismatch errors
69
+ # trying to accomplish this, in vain. The real problem, of course, is even
70
+ # trying to query serialized data. I'm unsure how well this would work in
71
+ # different ruby versions or implementations, which may handle object
72
+ # dumping differently. Bitmasking seems to be a more reliable strategy.
73
+
74
+ base.class_eval do
75
+ alias_method :add_roles, :add_role
76
+ alias_method :add_roles!, :add_role
77
+
78
+ cattr_accessor :roles_marker
79
+ cattr_accessor :column
80
+
81
+ self.roles_marker = '!'
82
+ self.column = "#{table_name}.#{column_name}"
83
+
84
+ scope :with_role, (proc { |r|
85
+ where("#{column} LIKE '%#{roles_marker}#{r}#{roles_marker}%'")
86
+ })
87
+
88
+ scope :without_role, (proc { |r|
89
+ where("#{column} NOT LIKE '%#{roles_marker}#{r}#{roles_marker}%' OR #{column} IS NULL")
90
+ })
91
+
92
+ define_method :add_role_markers do
93
+ self[column_name.to_sym].map! { |r| [roles_marker, r, roles_marker].join }
94
+ end
95
+
96
+ define_method :strip_role_markers do
97
+ self[column_name.to_sym].map! { |r| r.gsub(roles_marker, '') }
98
+ end
99
+
100
+ private :add_role_markers, :strip_role_markers
101
+ before_save :add_role_markers
102
+ after_save :strip_role_markers
103
+ after_rollback :strip_role_markers
104
+ after_find :strip_role_markers
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ # Include this module in your ActiveRecord model for role support.
6
+ module SlowYourRoles
7
+ extend ActiveSupport::Concern
8
+
9
+ included do |base|
10
+ base.send :alias_method, :method_missing_without_roles, :method_missing
11
+ base.send :alias_method, :method_missing, :method_missing_with_roles
12
+
13
+ base.send :alias_method, :respond_to_without_roles?, :respond_to?
14
+ base.send :alias_method, :respond_to?, :respond_to_with_roles?
15
+ end
16
+
17
+ ALLOWED_METHODS = %i[serialize bitmask].freeze
18
+
19
+ ALLOWED_METHODS.each do |method|
20
+ autoload method.to_s.capitalize.to_sym, "methods/#{method}"
21
+ end
22
+
23
+ # Adding slow_your_roles to the model class.
24
+ module ClassMethods
25
+ def slow_your_roles(name, options = { method: :serialize })
26
+ begin
27
+ raise NameError unless ALLOWED_METHODS.include? options[:method]
28
+ rescue NameError
29
+ puts '[Slow Your Roles] Storage method does not exist reverting to Serialize'
30
+ options[:method] = :serialize
31
+ end
32
+ "SlowYourRoles::#{options[:method].to_s.camelize}".constantize.new(self, name, options)
33
+ end
34
+ end
35
+
36
+ def method_missing_with_roles(method_id, *args, &block)
37
+ match = method_id.to_s.match(/^is_(\w+)[?]$/)
38
+ if match && respond_to?('has_role?')
39
+ self.class.send(:define_method, "is_#{match[1]}?") do
40
+ send :has_role?, (match[1]).to_s
41
+ end
42
+ send "is_#{match[1]}?"
43
+ else
44
+ method_missing_without_roles(method_id, *args, &block)
45
+ end
46
+ end
47
+
48
+ def respond_to_with_roles?(method_id, _include_private = false)
49
+ match = method_id.to_s.match(/^is_(\w+)[?]$/)
50
+ if match && respond_to?('has_role?')
51
+ true
52
+ else
53
+ respond_to_without_roles?(method_id, false)
54
+ end
55
+ end
56
+ end
57
+
58
+ module ActiveRecord
59
+ # Include SlowYourRoles in ActiveRecord::Base
60
+ class Base
61
+ include SlowYourRoles
62
+ end
63
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'slow_your_roles'
5
+ s.version = '2.0.2'
6
+ s.authors = ['Aaron A']
7
+ s.date = '2020-04-21'
8
+ s.summary = 'Easy role authorization in Rails'
9
+ s.description = 'Easy role authorization in Rails ported from an unmaintained library'
10
+ s.email = '_aaron@tutanota.com'
11
+ s.extra_rdoc_files = ['CHANGELOG.md', 'README.md'] + Dir['lib/**/*']
12
+ s.files = [
13
+ 'CHANGELOG.md',
14
+ 'Gemfile',
15
+ 'Gemfile.lock',
16
+ 'README.md',
17
+ 'Rakefile',
18
+ 'slow_your_roles.gemspec',
19
+ 'init.rb'] + Dir['lib/**/*']
20
+ s.test_files = Dir['spec/**/*']
21
+ s.homepage = 'http://github.com/aarona/slow_your_roles'
22
+ s.rdoc_options = [
23
+ '--line-numbers',
24
+ '--inline-source',
25
+ '--title',
26
+ 'Slow_your_roles',
27
+ '--main',
28
+ 'README.md'
29
+ ]
30
+ s.require_paths = ['lib']
31
+
32
+ s.add_runtime_dependency('activesupport', ['>= 6.0.3.1'])
33
+ s.add_development_dependency('activerecord', ['>= 6.0.3.1'])
34
+ s.add_development_dependency('rspec', ['>= 0'])
35
+ s.add_development_dependency('rubocop', ['>= 0'])
36
+ s.add_development_dependency('sqlite3', ['>= 0'])
37
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SlowYourRoles do
6
+ describe 'bitmask method' do
7
+ it 'should allow me to set a users role' do
8
+ user = BitmaskUser.new
9
+ user.add_role 'admin'
10
+ expect(user._roles). to include 'admin'
11
+ end
12
+
13
+ it 'should allow me to set multiple roles at one time' do
14
+ user = BitmaskUser.new
15
+ user.add_roles 'admin', 'manager'
16
+ expect(user._roles).to include 'admin'
17
+ expect(user._roles).to include 'manager'
18
+ expect(user._roles.length).to eq 2
19
+ user.add_roles 'admin', 'manager','user'
20
+ expect(user._roles).to include 'user'
21
+ expect(user._roles.length).to eq 3
22
+ end
23
+
24
+ it 'should return true for is_admin? if the admin role is added to the user' do
25
+ user = BitmaskUser.new
26
+ user.add_role 'admin'
27
+ expect(user.is_admin?).to eq(true)
28
+ end
29
+
30
+ it "should return true for has_role? 'admin' if the admin role is added to the user" do
31
+ user = BitmaskUser.new
32
+ user.add_role 'admin'
33
+ expect(user.has_role?('admin')).to eq(true)
34
+ end
35
+
36
+ it "should turn false for has_role? 'manager' if manager role is not added to the user" do
37
+ user = BitmaskUser.new
38
+ expect(user.has_role?('manager')).to eq(false)
39
+ end
40
+
41
+ it 'should turn false for is_manager? if manager role is not added to the user' do
42
+ user = BitmaskUser.new
43
+ expect(user.is_manager?).to eq(false)
44
+ end
45
+
46
+ it 'should not allow you to add a role not in the array list of roles' do
47
+ user = BitmaskUser.new
48
+ user.add_role 'lolcat'
49
+ expect(user.is_lolcat?).to eq(false)
50
+ end
51
+
52
+ it 'should only add valid roles when adding multiple roles' do
53
+ user = BitmaskUser.new
54
+ user.add_roles 'admin', 'manager', 'lolcat'
55
+ expect(user._roles.length).to eq 2
56
+ expect(user._roles).to include 'admin'
57
+ expect(user._roles).to include 'manager'
58
+ end
59
+
60
+ describe 'normal methods' do
61
+ it 'should not save to the database if not implicitly saved' do
62
+ user = BitmaskUser.create(name: 'Ryan')
63
+ user.add_role 'admin'
64
+ expect(user.is_admin?).to eq(true)
65
+ user.reload
66
+
67
+ expect(user.is_admin?).to eq(false)
68
+ end
69
+
70
+ it 'should save to the database if implicity saved' do
71
+ user = BitmaskUser.create(name: 'Ryan')
72
+ user.add_role 'admin'
73
+ expect(user.is_admin?).to eq(true)
74
+ user.save
75
+ user.reload
76
+
77
+ expect(user.is_admin?).to eq(true)
78
+ end
79
+
80
+ it 'should clear all roles and not save if not implicitly saved' do
81
+ user = BitmaskUser.create(name: 'Ryan')
82
+ user.add_role 'admin'
83
+ expect(user.is_admin?).to eq(true)
84
+ user.save
85
+ user.reload
86
+ expect(user.is_admin?).to eq(true)
87
+
88
+ user.clear_roles
89
+ expect(user.is_admin?).to eq(false)
90
+ user.reload
91
+
92
+ expect(user.is_admin?).to eq(true)
93
+ end
94
+
95
+ it 'should clear all roles and save if implicitly saved' do
96
+ user = BitmaskUser.create(name: 'Ryan')
97
+ user.add_role 'admin'
98
+ expect(user.is_admin?).to eq(true)
99
+ user.save
100
+ user.reload
101
+ expect(user.is_admin?).to eq(true)
102
+
103
+ user.clear_roles
104
+ expect(user.is_admin?).to eq(false)
105
+ user.save
106
+ user.reload
107
+
108
+ expect(user.is_admin?).to eq(false)
109
+ end
110
+
111
+ it 'should remove a role and not save unless implicitly saved' do
112
+ user = BitmaskUser.create(name: 'Ryan')
113
+ user.add_role 'admin'
114
+ expect(user.is_admin?).to eq(true)
115
+ user.save
116
+ user.reload
117
+
118
+ expect(user.is_admin?).to eq(true)
119
+ user.remove_role 'admin'
120
+ expect(user.is_admin?).to eq(false)
121
+ user.reload
122
+
123
+ expect(user.is_admin?).to eq(true)
124
+ end
125
+
126
+ it 'should remove a role and save if implicitly saved' do
127
+ user = BitmaskUser.create(name: 'Ryan')
128
+ user.add_role 'admin'
129
+ expect(user.is_admin?).to eq(true)
130
+ user.save
131
+ user.reload
132
+
133
+ expect(user.is_admin?).to eq(true)
134
+ user.remove_role 'admin'
135
+ expect(user.is_admin?).to eq(false)
136
+ user.save
137
+ user.reload
138
+
139
+ expect(user.is_admin?).to eq(false)
140
+ end
141
+ end
142
+
143
+ describe 'bang method' do
144
+ it 'should save to the database if the bang method is used' do
145
+ user = BitmaskUser.create(name: 'Ryan')
146
+ user.add_role! 'admin'
147
+ expect(user.is_admin?).to eq(true)
148
+ user.reload
149
+
150
+ expect(user.is_admin?).to eq(true)
151
+ end
152
+
153
+ it 'should allow me to set multiple roles at one time' do
154
+ user = BitmaskUser.new
155
+ user.add_roles! 'admin', 'manager'
156
+ expect(user._roles).to include 'admin'
157
+ expect(user._roles).to include 'manager'
158
+ expect(user._roles.length).to eq 2
159
+ user.add_roles! 'admin', 'manager','user'
160
+ expect(user._roles).to include 'user'
161
+ expect(user._roles.length).to eq 3
162
+ end
163
+
164
+ it 'should remove a role and save' do
165
+ user = BitmaskUser.create(name: 'Ryan')
166
+ user.add_role 'admin'
167
+ expect(user.is_admin?).to eq(true)
168
+ user.save
169
+ user.reload
170
+
171
+ expect(user.is_admin?).to eq(true)
172
+ user.remove_role! 'admin'
173
+ expect(user.is_admin?).to eq(false)
174
+ user.reload
175
+
176
+ expect(user.is_admin?).to eq(false)
177
+ end
178
+
179
+ it 'should clear all roles and save' do
180
+ user = BitmaskUser.create(name: 'Ryan')
181
+ user.add_role 'admin'
182
+ expect(user.is_admin?).to eq(true)
183
+ user.save
184
+ user.reload
185
+ expect(user.is_admin?).to eq(true)
186
+
187
+ user.clear_roles!
188
+ expect(user.is_admin?).to eq(false)
189
+ user.reload
190
+
191
+ expect(user.is_admin?).to eq(false)
192
+ end
193
+ end
194
+
195
+ describe 'scopes' do
196
+ describe 'with_role' do
197
+ it 'should implement the `with_role` scope' do
198
+ expect(BitmaskUser).to respond_to :with_role
199
+ end
200
+
201
+ it 'should return an ActiveRecord::Relation' do
202
+ expect(BitmaskUser.with_role('admin').class).to eq(
203
+ "BitmaskUser::ActiveRecord_Relation".constantize
204
+ )
205
+ end
206
+
207
+ it 'should raise an ArgumentError for undefined roles' do
208
+ expect { BitmaskUser.with_role('your_mom') }.to raise_error(ArgumentError)
209
+ end
210
+
211
+ it 'should match records with a given role' do
212
+ user = BitmaskUser.create(name: 'Daniel')
213
+ expect(BitmaskUser.with_role('admin')).not_to include user
214
+ user.add_role! 'admin'
215
+ expect(BitmaskUser.with_role('admin')).to include user
216
+ end
217
+
218
+ it 'should be chainable' do
219
+ (daniel = BitmaskUser.create(name: 'Daniel')).add_role! 'user'
220
+ (ryan = BitmaskUser.create(name: 'Ryan')).add_role! 'user'
221
+ ryan.add_role! 'admin'
222
+ admin_users = BitmaskUser.with_role('user').with_role('admin')
223
+ expect(admin_users).to include ryan
224
+ expect(admin_users).not_to include daniel
225
+ end
226
+ end
227
+
228
+ describe 'without_role' do
229
+ it 'should implement the `without_role` scope' do
230
+ expect(BitmaskUser).to respond_to :without_role
231
+ end
232
+
233
+ it 'should return an ActiveRecord::Relation' do
234
+ expect(BitmaskUser.without_role('admin').class).to eq(
235
+ "BitmaskUser::ActiveRecord_Relation".constantize
236
+ )
237
+ end
238
+
239
+ it 'should raise an ArgumentError for undefined roles' do
240
+ expect { BitmaskUser.without_role('your_mom') }.to raise_error(ArgumentError)
241
+ end
242
+
243
+ it 'should match records with a given role' do
244
+ user = BitmaskUser.create(name: 'Daniel')
245
+ expect(BitmaskUser.without_role('admin')).to include user
246
+ user.add_role! 'admin'
247
+ expect(BitmaskUser.without_role('admin')).not_to include user
248
+ end
249
+
250
+ it 'should be chainable' do
251
+ (daniel = BitmaskUser.create(name: 'Daniel')).add_role! 'user'
252
+ (ryan = BitmaskUser.create(name: 'Ryan')).add_role! 'user'
253
+ ryan.add_role! 'admin'
254
+ non_admin_users = BitmaskUser.with_role('user').without_role('admin')
255
+ expect(non_admin_users).not_to include ryan
256
+ expect(non_admin_users).to include daniel
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end