-
-
Notifications
You must be signed in to change notification settings - Fork 127
Expand file tree
/
Copy pathrole.rb
More file actions
271 lines (225 loc) · 7.5 KB
/
Copy pathrole.rb
File metadata and controls
271 lines (225 loc) · 7.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# frozen_string_literal: true
class Role < ApplicationRecord
include Keygen::EE::ProtectedMethods[:permissions=, entitlements: %i[permissions]]
include Keygen::PortableClass
include Denormalizable::Model
include Accountable
include Dirtyable
USER_ROLES = %w[user admin developer read_only sales_agent support_agent].freeze
ENVIRONMENT_ROLES = %w[environment].freeze
PRODUCT_ROLES = %w[product].freeze
LICENSE_ROLES = %w[license].freeze
ROLE_RANK = {
admin: 7,
environment: 6,
developer: 5,
product: 4,
sales_agent: 3,
support_agent: 2,
read_only: 1,
license: 0,
user: 0,
}.with_indifferent_access
.freeze
belongs_to :resource,
polymorphic: true,
inverse_of: :role
has_many :role_permissions,
dependent: :delete_all,
inverse_of: :role,
autosave: true
has_many :permissions, through: :role_permissions do
def actions = loaded? ? collect(&:action) : super
end
# NB(ezekg) we're using account over account_id here because it may not be persisted
has_account default: -> { resource&.account }
# NB(ezekg) an explicit :inverse_of is required because Environment#tokens is
# scoped to the environment, not to the environment as a bearer
denormalizes :name,
to: :tokens, through: :resource, inverse_of: :bearer, as: :bearer_role
# FIXME(ezekg) replace with accountable concern i.e. an association
delegate :default_permissions, :default_permission_ids,
:allowed_permissions, :allowed_permission_ids,
allow_nil: true,
to: :resource
accepts_nested_attributes_for :role_permissions
tracks_nested_attributes_for :role_permissions
# Set default permissions unless already set
before_create :set_default_permissions,
unless: :role_permissions_attributes_assigned?
# NOTE(ezekg) Sanity checks
validates :resource_type,
inclusion: { in: [User.name, Environment.name, Product.name, License.name] }
validates :name,
inclusion: { in: USER_ROLES, message: 'must be a valid user role' },
if: -> {
resource.is_a?(User)
}
validates :name,
inclusion: { in: ENVIRONMENT_ROLES, message: 'must be a valid environment role' },
if: -> {
resource.is_a?(Environment)
}
validates :name,
inclusion: { in: PRODUCT_ROLES, message: 'must be a valid product role' },
if: -> {
resource.is_a?(Product)
}
validates :name,
inclusion: { in: LICENSE_ROLES, message: 'must be a valid license role' },
if: -> {
resource.is_a?(License)
}
validates :permission_ids,
inclusion: {
in: -> role { role.allowed_permission_ids },
message: 'unsupported permissions',
}
##
# permissions= sets the role's permissions. It does not save automatically.
#
# Instead of doing a has_many(through:), we're doing this so that we can
# allow permissions to be attached by action via the resource, rather than
# by ID. We don't expose permission IDs to the world. This also allows
# us to insert in bulk, rather than serially.
def permissions=(*ids)
return if
ids == [nil]
assign_attributes(
role_permissions_attributes: ids.flatten
.compact
.map {{ permission_id: it }},
)
end
##
# permissions overrides association reader to include pending permission changes
def permissions
return pending_permissions if
role_permissions_attributes_assigned? || new_record?
super
end
##
# pending_permissions permissions returns the role's pending permissions,
# via the :role_permissions nested attributes.
def pending_permissions
unless role_permissions_attributes_assigned?
return Permission.where(id: default_permission_ids) if
new_record?
return Permission.none
end
Permission.where(
id: role_permissions_attributes.collect { it[:permission_id] },
)
end
##
# permission_ids returns an array of the role's permission IDs,
# including pending changes.
def permission_ids
case
when role_permissions_attributes_assigned?
role_permissions_attributes.collect { it[:permission_id] }
when role_permissions.loaded?
role_permissions.collect(&:permission_id)
else
# NB(ezekg) avoid loading the association just to read ids, since a
# loaded through-association will result in n+1 queries on
# subsequent permission reads
role_permissions.pluck(:permission_id)
end
end
##
# reset_permissions! resets the role's permissions to defaults.
def reset_permissions!
update!(permissions: default_permission_ids)
end
##
# reset_permissions resets the role's permission attributes to defaults.
def reset_permissions
self.permissions = default_permission_ids
end
##
# name= overloads role assignment so we can reset permissions
# on role change.
def name=(...)
super(...)
# Reset permissions on role change by using the intersection of our
# current role's permissions and the new role's default permisisons.
# This helps prevent prevents accidental privilege escalation, e.g.
# for user => admin => user.
#
# Only run when role is persisted, i.e. on updates.
return unless
persisted?
self.permissions = permission_ids & (default_permission_ids << Permission.wildcard_id)
end
##
# deconstruct allows pattern pattern matching like:
#
# role in Role(:admin | :user)
#
def deconstruct = [name.to_sym]
def rank
ROLE_RANK.fetch(name) { -1 }
end
def ===(comparison_role)
comparison_role.equal?(self) ||
comparison_role.instance_of?(self.class) &&
comparison_role.id == id
end
def ==(comparison_role)
rank == comparison_role.rank &&
name == comparison_role.name
end
def <=(comparison_role)
rank <= comparison_role.rank
end
def <(comparison_role)
rank < comparison_role.rank
end
def >=(comparison_role)
rank >= comparison_role.rank
end
def >(comparison_role)
rank > comparison_role.rank
end
def user? = name.to_sym == :user
def admin? = name.to_sym == :admin
def environment? = name.to_sym == :environment
def product? = name.to_sym == :product
def license? = name.to_sym == :license
def changed_for_autosave?
super || role_permissions_attributes_assigned?
end
def changed?
super || role_permissions_attributes_assigned?
end
private
def set_default_permissions
assign_attributes(
role_permissions_attributes: default_permission_ids.map {{ permission_id: it }},
)
end
##
# autosave_associated_records_for_role_permissions bulk inserts role permissions instead
# of saving them sequentially, which is incredibly slow with 100+ permissions.
def autosave_associated_records_for_role_permissions
return if
role_permissions_attributes.nil?
transaction do
role_permissions.delete_all
if role_permissions_attributes.any?
# FIXME(ezekg) Can't use role_permissions.upsert_all at this point, because for
# some reason role_id ends up being nil. Instead, we'll use the
# class method and then reset the stale associations.
RolePermission.upsert_all(
role_permissions_attributes.map { it.merge(role_id: id) },
record_timestamps: true,
on_duplicate: :skip,
)
end
# reset stale associations after the bulk upsert
role_permissions.reset
permissions.reset
end
end
end