Skip to content

Commit a584398

Browse files
authoredOct 4, 2023
Merge pull request #2401 from alphagov/tidy-invitations-controller
Tidy InvitationsController
2 parents 621d729 + cc50bed commit a584398

File tree

8 files changed

+558
-221
lines changed

8 files changed

+558
-221
lines changed
 
+51-91
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# https://raw.githubusercontent.com/scambra/devise_invitable/master/app/controllers/devise/invitations_controller.rb
22
class InvitationsController < Devise::InvitationsController
3-
before_action :authenticate_user!
4-
after_action :verify_authorized, except: %i[edit update] # rubocop:disable Rails/LexicallyScopedActionFilter
3+
before_action :authenticate_inviter!, only: %i[new create resend]
4+
after_action :verify_authorized, only: %i[new create resend]
5+
6+
before_action :redirect_if_invitee_already_exists, only: :create
7+
before_action :configure_permitted_parameters, only: :create
8+
59
layout "admin_layout", only: %i[edit update]
610

711
include UserPermissionsControllerMethods
@@ -13,34 +17,37 @@ def new
1317
end
1418

1519
def create
16-
# Prevent an error when devise_invitable invites/updates an existing user,
17-
# and accepts_nested_attributes_for tries to create duplicate permissions.
18-
if (self.resource = User.find_by(email: params[:user][:email]))
19-
authorize resource
20-
flash[:alert] = "User already invited. If you want to, you can click 'Resend signup email'."
21-
respond_with resource, location: users_path
20+
authorize User
21+
22+
all_params = invite_params
23+
all_params[:require_2sv] = invitee_requires_2sv(all_params)
24+
25+
self.resource = resource_class.invite!(all_params, current_inviter)
26+
if resource.errors.empty?
27+
grant_default_permissions(resource)
28+
EventLog.record_account_invitation(resource, current_user)
29+
set_flash_message :notice, :send_instructions, email: resource.email
30+
respond_with resource, location: after_invite_path_for(resource)
2231
else
23-
# workaround for invitatable not providing a build_invitation which could be authorised before saving
24-
all_params = resource_params
25-
all_params[:require_2sv] = new_user_requires_2sv(all_params.symbolize_keys)
26-
27-
user = User.new(all_params)
28-
user.organisation_id = all_params[:organisation_id]
29-
authorize user
30-
31-
self.resource = resource_class.invite!(all_params, current_inviter)
32-
if resource.errors.empty?
33-
grant_default_permissions(resource)
34-
set_flash_message :notice, :send_instructions, email: resource.email
35-
respond_with resource, location: after_invite_path_for(resource)
36-
else
37-
respond_with_navigational(resource) { render :new }
38-
end
39-
40-
EventLog.record_account_invitation(@user, current_user)
32+
respond_with_navigational(resource) { render :new }
4133
end
4234
end
4335

36+
# rubocop:disable Lint/UselessMethodDefinition
37+
# Renders app/views/devise/invitations/edit.html.erb
38+
def edit
39+
super
40+
end
41+
42+
def update
43+
super
44+
end
45+
46+
def destroy
47+
super
48+
end
49+
# rubocop:enable Lint/UselessMethodDefinition
50+
4451
def resend
4552
user = User.find(params[:id])
4653
authorize user
@@ -52,86 +59,39 @@ def resend
5259

5360
private
5461

55-
def after_invite_path_for(_resource)
56-
if new_user_requires_2sv(resource)
62+
def after_invite_path_for(_)
63+
if invitee_requires_2sv(resource)
5764
users_path
5865
else
5966
require_2sv_user_path(resource)
6067
end
6168
end
6269

63-
# TODO: remove this method when we're on a version of devise_invitable which
64-
# no longer expects it to exist (v1.2.1 onwards)
65-
def build_resource
66-
self.resource = resource_class.new(resource_params)
67-
end
68-
69-
def resource_params
70-
sanitised_params = UserParameterSanitiser.new(
71-
user_params: unsanitised_user_params,
72-
current_user_role:,
73-
).sanitise
74-
75-
if params[:action] == "update"
76-
sanitised_params.to_h.merge(invitation_token:)
77-
else
78-
sanitised_params.to_h
70+
def grant_default_permissions(user)
71+
SupportedPermission.default.each do |default_permission|
72+
user.grant_permission(default_permission)
7973
end
8074
end
8175

82-
# TODO: once we've upgraded Devise and DeviseInvitable, `resource_params`
83-
# hopefully won't be being called for actions like `#new` anymore and we
84-
# can change the following `params.fetch(:user)` to
85-
# `params.require(:user)`. See
86-
# https://github.com/scambra/devise_invitable/blob/v1.1.5/app/controllers/devise/invitations_controller.rb#L10
87-
# and
88-
# https://github.com/plataformatec/devise/blob/v2.2/app/controllers/devise_controller.rb#L99
89-
# for details :)
90-
def unsanitised_user_params
91-
params.require(:user).permit(
92-
:name,
93-
:email,
94-
:organisation_id,
95-
:invitation_token,
96-
:password,
97-
:password_confirmation,
98-
:require_2sv,
99-
:role,
100-
supported_permission_ids: [],
101-
).to_h
102-
end
103-
104-
# NOTE: `current_user` doesn't exist for `#edit` and `#update` actions as
105-
# implemented in our current (out-of-date) versions of Devise
106-
# (https://github.com/plataformatec/devise/blob/v2.2/app/controllers/devise_controller.rb#L117)
107-
# and DeviseInvitable
108-
# (https://github.com/scambra/devise_invitable/blob/v1.1.5/app/controllers/devise/invitations_controller.rb#L5)
109-
#
110-
# With the old attr_accessible approach, this would fall back to the
111-
# default whitelist (i.e. equivalent to the `:normal` role) and this
112-
# this preserves that behaviour. In fact, a user accepting an invitation
113-
# only needs to modify `password` and `password_confirmation` so we could
114-
# only permit those two params for the `edit` and `update` actions.
115-
def current_user_role
116-
current_user.try(:role).try(:to_sym) || :normal
76+
def organisation(params)
77+
Organisation.find_by(id: params[:organisation_id])
11778
end
11879

119-
def invitation_token
120-
unsanitised_user_params.fetch(:invitation_token, {})
80+
def invitee_requires_2sv(params)
81+
organisation(params)&.require_2sv? || User.admin_roles.include?(params[:role])
12182
end
12283

123-
def update_resource_params
124-
resource_params
125-
end
126-
127-
def grant_default_permissions(user)
128-
SupportedPermission.default.each do |default_permission|
129-
user.grant_permission(default_permission)
84+
def redirect_if_invitee_already_exists
85+
if (resource = User.find_by(email: params[:user][:email]))
86+
authorize resource
87+
flash[:alert] = "User already invited. If you want to, you can click 'Resend signup email'."
88+
respond_with resource, location: users_path
13089
end
13190
end
13291

133-
def new_user_requires_2sv(params)
134-
(params[:organisation_id].present? && Organisation.find(params[:organisation_id]).require_2sv?) ||
135-
%w[superadmin admin organisation_admin super_organisation_admin].include?(params[:role])
92+
def configure_permitted_parameters
93+
keys = [:name, :organisation_id, { supported_permission_ids: [] }]
94+
keys << :role if policy(User).assign_role?
95+
devise_parameter_sanitizer.permit(:invite, keys:)
13696
end
13797
end

‎app/policies/user_policy.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ def new?
1010
alias_method :assign_organisations?, :new?
1111

1212
# invitations#create
13-
def create?
14-
current_user.superadmin? || (current_user.admin? && !record.superadmin?)
15-
end
13+
alias_method :create?, :new?
1614

1715
def edit?
1816
return false if current_user == record

‎app/views/devise/invitations/new.html.erb

+30-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,36 @@
55
<%= form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => {:method => :post, :class => 'well'} do |f| %>
66
<%= render "devise/shared/error_messages", resource: resource %>
77

8-
<%= render partial: "users/form_fields", locals: { f: f } %>
8+
<p class="form-group">
9+
<%= f.label :name %>
10+
<%= f.text_field :name, autofocus: true, autocomplete: "off", class: 'form-control input-md-6 ' %>
11+
</p>
12+
13+
<p class="form-group">
14+
<%= f.label :email %>
15+
<%= f.text_field :email, autocomplete: "off", class: 'form-control input-md-6 add-label-margin' %>
16+
</p>
17+
18+
<% if policy(User).assign_role? %>
19+
<p class="form-group">
20+
<%= f.label :role %><br />
21+
<%= f.select :role, options_for_select(assignable_user_roles.map(&:humanize).zip(assignable_user_roles), f.object.role), {}, class: "chosen-select form-control", 'data-module' => 'chosen' %>
22+
<span class="help-block">
23+
<strong>Superadmins</strong> can create and edit all user types and edit applications.<br />
24+
<strong>Admins</strong> can create and edit normal users.<br />
25+
<strong>Super Organisation Admins</strong> can unlock and unsuspend their organisation and related organisation accounts.<br />
26+
<strong>Organisation Admins</strong> can unlock and unsuspend their organisation accounts.
27+
</span>
28+
</p>
29+
<% end %>
30+
31+
<p class="form-group">
32+
<%= f.label :organisation_id, "Organisation" %><br />
33+
<%= f.select :organisation_id, organisation_options(f), organisation_select_options, { class: "chosen-select form-control", 'data-module' => 'chosen' } %>
34+
</p>
35+
36+
<h2 class="add-vertical-margins">Permissions</h2>
37+
<%= render partial: "shared/user_permissions", locals: { user_object: f.object }%>
938

1039
<%= f.submit "Create user and send email", :class => 'btn btn-success' %>
1140
<% end %>

‎app/views/users/_form_fields.html.erb

+19-19
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
<p class="form-group">
77
<%= f.label :email %>
88
<%= f.text_field :email, autocomplete: "off", class: 'form-control input-md-6 add-label-margin' %>
9-
<% if f.object.persisted? %>
10-
<% if f.object.invited_but_not_yet_accepted? %>
11-
<span class="help-block">Changes will trigger a new signup email.</span>
12-
<% end %>
9+
<% if f.object.invited_but_not_yet_accepted? %>
10+
<span class="help-block">Changes will trigger a new signup email.</span>
1311
<% end %>
1412
</p>
1513

@@ -25,22 +23,24 @@
2523
</p>
2624
<% end %>
2725

28-
<% if policy(User).assign_role? && @user.reason_for_2sv_exemption.blank? %>
29-
<p class="form-group">
30-
<%= f.label :role %><br />
31-
<%= f.select :role, options_for_select(assignable_user_roles.map(&:humanize).zip(assignable_user_roles), f.object.role), {}, class: "chosen-select form-control", 'data-module' => 'chosen' %>
32-
<span class="help-block">
33-
<strong>Superadmins</strong> can create and edit all user types and edit applications.<br />
34-
<strong>Admins</strong> can create and edit normal users.<br />
35-
<strong>Super Organisation Admins</strong> can unlock and unsuspend their organisation and related organisation accounts.<br />
36-
<strong>Organisation Admins</strong> can unlock and unsuspend their organisation accounts.
37-
</span>
38-
</p>
39-
<% elsif policy(User).assign_role? %>
40-
<p>This user's role is set to <%= @user.role %>. They are currently exempted from 2-step verification, meaning that their role cannot be changed as admins are required to have 2-step verification.</p>
26+
<% if policy(User).assign_role? %>
27+
<% if @user.reason_for_2sv_exemption.blank? %>
28+
<p class="form-group">
29+
<%= f.label :role %><br />
30+
<%= f.select :role, options_for_select(assignable_user_roles.map(&:humanize).zip(assignable_user_roles), f.object.role), {}, class: "chosen-select form-control", 'data-module' => 'chosen' %>
31+
<span class="help-block">
32+
<strong>Superadmins</strong> can create and edit all user types and edit applications.<br />
33+
<strong>Admins</strong> can create and edit normal users.<br />
34+
<strong>Super Organisation Admins</strong> can unlock and unsuspend their organisation and related organisation accounts.<br />
35+
<strong>Organisation Admins</strong> can unlock and unsuspend their organisation accounts.
36+
</span>
37+
</p>
38+
<% else %>
39+
<p>This user's role is set to <%= @user.role %>. They are currently exempted from 2-step verification, meaning that their role cannot be changed as admins are required to have 2-step verification.</p>
40+
<% end %>
4141
<% end %>
4242

43-
<% if policy(@user).mandate_2sv? && @user.persisted? %>
43+
<% if policy(@user).mandate_2sv? %>
4444
<dl>
4545
<dt>Account security</dt>
4646
<dd>
@@ -77,7 +77,7 @@
7777
<br/>
7878
User will be prompted to set up 2-step verification again the next time they sign in.</p>
7979
<% end %>
80-
<% if @user.persisted? && policy(@user).exempt_from_two_step_verification? && @user.reason_for_2sv_exemption.nil? %>
80+
<% if policy(@user).exempt_from_two_step_verification? && @user.reason_for_2sv_exemption.nil? %>
8181
<p>
8282
<%= link_to 'Exempt user from 2-step verification', edit_two_step_verification_exemption_path(@user) %>
8383
<br/>

‎lib/roles.rb

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def admin_role_classes
2828
role_classes - [Roles::Normal, Roles::Base]
2929
end
3030

31+
def admin_roles
32+
admin_role_classes.map(&:role_name)
33+
end
34+
3135
def roles
3236
role_classes.sort_by(&:level).map(&:role_name)
3337
end

‎test/controllers/invitations_controller_test.rb

+438-93
Large diffs are not rendered by default.

‎test/lib/roles_test.rb

+13
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ class Subject
3535
end
3636
end
3737

38+
context ".admin_roles" do
39+
should "only include admin role names" do
40+
expected_role_names = [
41+
Roles::Admin.role_name,
42+
Roles::Superadmin.role_name,
43+
Roles::OrganisationAdmin.role_name,
44+
Roles::SuperOrganisationAdmin.role_name,
45+
]
46+
47+
assert_same_elements expected_role_names, Subject.admin_roles
48+
end
49+
end
50+
3851
context ".roles" do
3952
should "order roles by their level" do
4053
expected_ordered_roles = %w[

‎test/policies/user_policy_test.rb

+2-14
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class UserPolicyTest < ActiveSupport::TestCase
1212
@gds = create(:organisation, name: "Government Digital Services", content_id: Organisation::GDS_ORG_CONTENT_ID)
1313
end
1414

15-
primary_management_actions = %i[new assign_organisations]
16-
user_management_actions = %i[edit create update unlock suspension cancel_email_change resend_email_change event_logs reset_2sv mandate_2sv]
15+
primary_management_actions = %i[new create assign_organisations]
16+
user_management_actions = %i[edit update unlock suspension cancel_email_change resend_email_change event_logs reset_2sv mandate_2sv]
1717
superadmin_actions = %i[assign_role]
1818
two_step_verification_exemption_actions = %i[exempt_from_two_step_verification]
1919

@@ -42,8 +42,6 @@ class UserPolicyTest < ActiveSupport::TestCase
4242
assert permit?(user, build(:superadmin_user), permission)
4343
end
4444

45-
next if permission == :create
46-
4745
should "not allow for #{permission} for the logged in user" do
4846
user = create(:superadmin_user)
4947

@@ -102,8 +100,6 @@ class UserPolicyTest < ActiveSupport::TestCase
102100
assert forbid?(user, build(:superadmin_user), permission)
103101
end
104102

105-
next if permission == :create
106-
107103
should "not allow for #{permission} for the logged in user" do
108104
user = create(:admin_user)
109105

@@ -140,10 +136,6 @@ class UserPolicyTest < ActiveSupport::TestCase
140136
assert permit?(build(:super_organisation_admin_user), User, :index)
141137
end
142138

143-
should "not allow for create" do
144-
assert forbid?(build(:super_organisation_admin_user), User, :create)
145-
end
146-
147139
primary_management_actions.each do |permission|
148140
should "not allow for #{permission}" do
149141
assert forbid?(build(:super_organisation_admin_user), User, permission)
@@ -201,10 +193,6 @@ class UserPolicyTest < ActiveSupport::TestCase
201193
assert permit?(build(:organisation_admin_user), User, :index)
202194
end
203195

204-
should "not allow for create" do
205-
assert forbid?(build(:organisation_admin_user), User, :create)
206-
end
207-
208196
primary_management_actions.each do |permission|
209197
should "not allow for #{permission}" do
210198
assert forbid?(build(:organisation_admin_user), User, permission)

0 commit comments

Comments
 (0)
Please sign in to comment.