organizations 0.5.0 → 0.7.0
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/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +7 -0
- data/.simplecov +17 -10
- data/Appraisals +2 -0
- data/CHANGELOG.md +112 -0
- data/README.md +714 -32
- data/app/controllers/organizations/invitations_controller.rb +27 -22
- data/app/controllers/organizations/memberships_controller.rb +37 -30
- data/app/controllers/organizations/organizations_controller.rb +8 -8
- data/app/controllers/organizations/public_invitations_controller.rb +1 -1
- data/app/mailers/organizations/invitation_mailer.rb +2 -2
- data/app/mailers/organizations/verification_mailer.rb +1 -1
- data/app/models/organizations/grant.rb +6 -0
- data/app/views/organizations/invitation_mailer/invitation_email.html.erb +11 -11
- data/app/views/organizations/invitation_mailer/invitation_email.text.erb +7 -7
- data/app/views/organizations/verification_mailer/code_email.html.erb +3 -3
- data/app/views/organizations/verification_mailer/code_email.text.erb +3 -3
- data/config/locales/en.yml +22 -0
- data/config/locales/es.yml +22 -0
- data/config/routes.rb +1 -2
- data/gemfiles/rails_7.2.gemfile +6 -1
- data/gemfiles/rails_8.1.gemfile +5 -1
- data/lib/generators/organizations/grants/grants_generator.rb +47 -0
- data/lib/generators/organizations/grants/templates/create_organizations_grants.rb.erb +59 -0
- data/lib/generators/organizations/install/install_generator.rb +1 -0
- data/lib/generators/organizations/install/templates/create_organizations_tables.rb.erb +15 -0
- data/lib/generators/organizations/join/join_generator.rb +113 -0
- data/lib/generators/organizations/join/templates/app/controllers/organization_access_controller.rb +146 -0
- data/lib/generators/organizations/join/templates/app/controllers/organization_joins_controller.rb +93 -0
- data/lib/generators/organizations/join/templates/app/views/organization_access/show.html.erb +142 -0
- data/lib/generators/organizations/join/templates/app/views/organization_joins/_disclosure.html.erb +8 -0
- data/lib/generators/organizations/join/templates/app/views/organization_joins/show.html.erb +113 -0
- data/lib/generators/organizations/join/templates/routes.rb +19 -0
- data/lib/generators/organizations/upgrade/templates/add_domain_verification.rb.erb +29 -0
- data/lib/generators/organizations/upgrade/templates/add_personal_organizations.rb.erb +22 -0
- data/lib/generators/organizations/upgrade/upgrade_generator.rb +79 -14
- data/lib/generators/organizations/views/templates/organizations/invitations/_form.html.erb +1 -1
- data/lib/generators/organizations/views/templates/organizations/invitations/index.html.erb +2 -2
- data/lib/generators/organizations/views/templates/organizations/invitations/show.html.erb +1 -1
- data/lib/generators/organizations/views/templates/organizations/memberships/index.html.erb +21 -13
- data/lib/generators/organizations/views/templates/organizations/organizations/index.html.erb +1 -1
- data/lib/generators/organizations/views/templates/organizations/organizations/show.html.erb +3 -3
- data/lib/generators/organizations/views/views_generator.rb +4 -6
- data/lib/organizations/account_deletion_plan.rb +118 -0
- data/lib/organizations/configuration.rb +155 -2
- data/lib/organizations/controller_helpers.rb +55 -15
- data/lib/organizations/grant_access.rb +75 -0
- data/lib/organizations/grant_holder.rb +152 -0
- data/lib/organizations/grantable.rb +28 -0
- data/lib/organizations/join_flow.rb +16 -12
- data/lib/organizations/models/concerns/has_organizations.rb +217 -9
- data/lib/organizations/models/domain.rb +283 -1
- data/lib/organizations/models/grant.rb +152 -0
- data/lib/organizations/models/invitation.rb +79 -8
- data/lib/organizations/models/join_request.rb +102 -43
- data/lib/organizations/models/membership.rb +96 -15
- data/lib/organizations/models/organization.rb +219 -48
- data/lib/organizations/organization_scoped.rb +16 -0
- data/lib/organizations/personal_owner_backfill.rb +122 -0
- data/lib/organizations/public_email_domains.rb +49 -0
- data/lib/organizations/resource.rb +11 -0
- data/lib/organizations/roles.rb +198 -36
- data/lib/organizations/version.rb +1 -1
- data/lib/organizations/view_helpers.rb +40 -9
- data/lib/organizations.rb +99 -4
- metadata +22 -2
|
@@ -32,6 +32,11 @@ module Organizations
|
|
|
32
32
|
# see Organizations::MetadataFlags.
|
|
33
33
|
extend Organizations::MetadataFlags
|
|
34
34
|
|
|
35
|
+
# grant!, revoke_grant!, grant_for, granted? (scoped resource grants)
|
|
36
|
+
include Organizations::GrantHolder
|
|
37
|
+
# limited_by_grants?, capabilities_for, can?, visible, granted_ids
|
|
38
|
+
include Organizations::GrantAccess
|
|
39
|
+
|
|
35
40
|
# Error raised when trying to demote below current role
|
|
36
41
|
class CannotDemoteOwner < Organizations::Error; end
|
|
37
42
|
class CannotPromoteToOwner < Organizations::Error; end
|
|
@@ -51,9 +56,19 @@ module Organizations
|
|
|
51
56
|
class_name: Organizations.user_class_name,
|
|
52
57
|
optional: true
|
|
53
58
|
|
|
59
|
+
# Scoped resource grants (rails g organizations:grants). Removed with the
|
|
60
|
+
# membership: a member who leaves keeps no access. (No dependent:
|
|
61
|
+
# option, because the table is optional: see GrantHolder.)
|
|
62
|
+
has_many :grants, class_name: "Organizations::Grant", inverse_of: :membership
|
|
63
|
+
before_destroy :delete_grants
|
|
64
|
+
|
|
54
65
|
# === Validations ===
|
|
55
66
|
|
|
56
|
-
validates :role, presence: true
|
|
67
|
+
validates :role, presence: true
|
|
68
|
+
# Checked when the role is set or changed, so a role retired from the
|
|
69
|
+
# configuration does not block unrelated updates of existing memberships.
|
|
70
|
+
validates :role, inclusion: { in: ->(_) { Roles.valid_roles.map(&:to_s) } },
|
|
71
|
+
if: -> { new_record? || will_save_change_to_role? }
|
|
57
72
|
# Proc message: resolved at VALIDATION time so it follows I18n.locale —
|
|
58
73
|
# a literal string here would be frozen in whatever locale loaded first.
|
|
59
74
|
validates :user_id, uniqueness: { scope: :organization_id,
|
|
@@ -77,17 +92,17 @@ module Organizations
|
|
|
77
92
|
# Memberships with viewer role
|
|
78
93
|
scope :viewers, -> { where(role: "viewer") }
|
|
79
94
|
|
|
95
|
+
# Memberships with a grant on a resource
|
|
96
|
+
scope :with_grant_on, lambda { |resource|
|
|
97
|
+
where(id: Organizations::Grant.active.for_resource(resource).select(:membership_id))
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
# Order by role hierarchy (owners first)
|
|
101
|
+
# (the configured hierarchy, custom roles included; unknown roles last)
|
|
81
102
|
scope :by_role_hierarchy, -> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
WHEN 'admin' THEN 1
|
|
86
|
-
WHEN 'member' THEN 2
|
|
87
|
-
WHEN 'viewer' THEN 3
|
|
88
|
-
ELSE 4
|
|
89
|
-
END
|
|
90
|
-
SQL
|
|
103
|
+
roles = Roles.hierarchy # one snapshot for the ranks and the fallback
|
|
104
|
+
ranks = roles.each_with_index.map { |role, index| "WHEN #{connection.quote(role.to_s)} THEN #{index}" }
|
|
105
|
+
order(Arel.sql("CASE role #{ranks.join(' ')} ELSE #{roles.length} END"))
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
# === Verified Joining (v0.5.0) ===
|
|
@@ -166,25 +181,87 @@ module Organizations
|
|
|
166
181
|
Roles.compare(role_sym, other.role_sym)
|
|
167
182
|
end
|
|
168
183
|
|
|
184
|
+
# Check if this member may give another member the specified role, or
|
|
185
|
+
# take it away from them: only roles that rank at or below this member's
|
|
186
|
+
# own role qualify. Useful to filter the role picker in an admin UI.
|
|
187
|
+
# @param role [Symbol, String] The role to give or take away
|
|
188
|
+
# @return [Boolean]
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# admin_membership.can_assign_role?(:admin) # => true
|
|
192
|
+
# member_membership.can_assign_role?(:admin) # => false
|
|
193
|
+
#
|
|
194
|
+
def can_assign_role?(role)
|
|
195
|
+
Roles.can_assign?(role_sym, role)
|
|
196
|
+
end
|
|
197
|
+
|
|
169
198
|
# === Role Change Methods ===
|
|
170
199
|
|
|
200
|
+
# Raise unless changed_by may move this member to new_role. A member who
|
|
201
|
+
# changes another member's role must rank at or above both the role being
|
|
202
|
+
# taken away and the role being given (see Roles.can_assign?), so a custom
|
|
203
|
+
# role that can edit member roles still cannot create or demote an admin.
|
|
204
|
+
# A changed_by who holds no membership in this organization, such as a
|
|
205
|
+
# console session or platform staff, is only recorded for the callbacks,
|
|
206
|
+
# as before; authorizing that account is the host's job. promote_to!,
|
|
207
|
+
# demote_to! and Organization#change_role_of! all call this.
|
|
208
|
+
# @param new_role [Symbol, String] The role being given
|
|
209
|
+
# @param changed_by [User, nil] Who is making the change
|
|
210
|
+
# @raise [Organizations::NotAuthorized] if either role ranks above changed_by's own
|
|
211
|
+
def authorize_role_change!(new_role, changed_by:)
|
|
212
|
+
self.class.authorize_rank!(organization, changed_by, [role_sym, new_role],
|
|
213
|
+
permission: :edit_member_roles,
|
|
214
|
+
message_key: :"errors.role_change_outranks_actor")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @api private
|
|
218
|
+
# The one check behind every action on a role: inviting to it, changing a
|
|
219
|
+
# member to or from it, removing a member who holds it, and revoking or
|
|
220
|
+
# resending an invitation for it. An actor may only act on roles that
|
|
221
|
+
# rank at or below its own and, below admin, whose permissions it holds
|
|
222
|
+
# (see Roles.assignment_refusal). An actor that is nil or holds no
|
|
223
|
+
# membership in the organization is not checked: the gem records it for
|
|
224
|
+
# the callbacks and the host authorizes it.
|
|
225
|
+
# @param message_key [Symbol] The message for a role that ranks above the actor's own
|
|
226
|
+
# @raise [Organizations::NotAuthorized] naming the first role the actor may not act on
|
|
227
|
+
def self.authorize_rank!(organization, actor, roles, permission:, message_key:)
|
|
228
|
+
actor_membership = actor && organization.memberships.find_by(user_id: actor.id)
|
|
229
|
+
return unless actor_membership
|
|
230
|
+
|
|
231
|
+
roles.map(&:to_sym).each do |role|
|
|
232
|
+
refusal = Roles.assignment_refusal(actor_membership.role_sym, role)
|
|
233
|
+
next unless refusal
|
|
234
|
+
|
|
235
|
+
key = refusal == :outranks ? message_key : :"errors.role_has_permissions_actor_lacks"
|
|
236
|
+
raise Organizations::NotAuthorized.new(
|
|
237
|
+
Organizations.t(key, organization: organization,
|
|
238
|
+
role: Organizations.t(:"roles.#{role}", organization: organization,
|
|
239
|
+
default: role.to_s.humanize)),
|
|
240
|
+
permission: permission,
|
|
241
|
+
organization: organization,
|
|
242
|
+
user: actor
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
171
247
|
# Promote to a higher role
|
|
172
248
|
# @param new_role [Symbol, String] The new role
|
|
173
249
|
# @param changed_by [User, nil] Who is making the change
|
|
174
250
|
# @return [self]
|
|
175
251
|
# @raise [InvalidRoleChange] if new role is not higher
|
|
176
252
|
# @raise [CannotPromoteToOwner] if trying to promote to owner (use transfer_ownership_to!)
|
|
253
|
+
# @raise [Organizations::NotAuthorized] if changed_by is a member whose role ranks below the new role
|
|
177
254
|
def promote_to!(new_role, changed_by: nil)
|
|
178
255
|
new_role_sym = new_role.to_sym
|
|
179
256
|
validate_role!(new_role_sym)
|
|
180
257
|
|
|
181
258
|
# Owner role is only assignable via transfer_ownership_to!
|
|
182
259
|
if new_role_sym == :owner
|
|
183
|
-
raise CannotPromoteToOwner, Organizations.t(:"errors.cannot_promote_to_owner")
|
|
260
|
+
raise CannotPromoteToOwner, Organizations.t(:"errors.cannot_promote_to_owner", organization: organization)
|
|
184
261
|
end
|
|
185
262
|
|
|
186
263
|
unless Roles.at_least?(new_role_sym, role_sym)
|
|
187
|
-
raise InvalidRoleChange, Organizations.t(:"errors.promote_not_higher", new_role: new_role, role: role)
|
|
264
|
+
raise InvalidRoleChange, Organizations.t(:"errors.promote_not_higher", organization: organization, new_role: new_role, role: role)
|
|
188
265
|
end
|
|
189
266
|
|
|
190
267
|
change_role_to!(new_role_sym, changed_by: changed_by)
|
|
@@ -196,16 +273,17 @@ module Organizations
|
|
|
196
273
|
# @return [self]
|
|
197
274
|
# @raise [CannotDemoteOwner] if trying to demote owner
|
|
198
275
|
# @raise [InvalidRoleChange] if new role is not lower
|
|
276
|
+
# @raise [Organizations::NotAuthorized] if changed_by is a member whose role ranks below the current role
|
|
199
277
|
def demote_to!(new_role, changed_by: nil)
|
|
200
278
|
new_role_sym = new_role.to_sym
|
|
201
279
|
validate_role!(new_role_sym)
|
|
202
280
|
|
|
203
281
|
if owner?
|
|
204
|
-
raise CannotDemoteOwner, Organizations.t(:"errors.cannot_demote_owner")
|
|
282
|
+
raise CannotDemoteOwner, Organizations.t(:"errors.cannot_demote_owner", organization: organization)
|
|
205
283
|
end
|
|
206
284
|
|
|
207
285
|
unless Roles.at_least?(role_sym, new_role_sym)
|
|
208
|
-
raise InvalidRoleChange, Organizations.t(:"errors.demote_not_lower", new_role: new_role, role: role)
|
|
286
|
+
raise InvalidRoleChange, Organizations.t(:"errors.demote_not_lower", organization: organization, new_role: new_role, role: role)
|
|
209
287
|
end
|
|
210
288
|
|
|
211
289
|
change_role_to!(new_role_sym, changed_by: changed_by)
|
|
@@ -221,7 +299,7 @@ module Organizations
|
|
|
221
299
|
|
|
222
300
|
return unless existing_owner.exists?
|
|
223
301
|
|
|
224
|
-
errors.add(:role, Organizations.t(:"attributes.owner_taken"))
|
|
302
|
+
errors.add(:role, Organizations.t(:"attributes.owner_taken", organization: organization))
|
|
225
303
|
end
|
|
226
304
|
|
|
227
305
|
def validate_role!(role)
|
|
@@ -238,6 +316,9 @@ module Organizations
|
|
|
238
316
|
ActiveRecord::Base.transaction do
|
|
239
317
|
# Lock row to prevent concurrent changes
|
|
240
318
|
lock!
|
|
319
|
+
# Checked on the locked, freshly read role, so a concurrent change
|
|
320
|
+
# cannot slip an admin past the check.
|
|
321
|
+
authorize_role_change!(new_role, changed_by: changed_by)
|
|
241
322
|
update!(role: new_role.to_s)
|
|
242
323
|
end
|
|
243
324
|
|