summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/users_controller.rb23
-rw-r--r--app/helpers/node_actions_helper.rb14
-rw-r--r--app/models/node_action.rb5
-rw-r--r--app/models/user.rb20
-rw-r--r--app/views/users/_user.html.erb22
-rw-r--r--config/locales/de.yml8
-rw-r--r--config/locales/en.yml8
-rw-r--r--config/routes.rb4
-rw-r--r--test/controllers/users_controller_test.rb40
9 files changed, 118 insertions, 26 deletions
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 95dff220..7bf23f17 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -4,7 +4,7 @@ class UsersController < ApplicationController
4 # Private 4 # Private
5 5
6 before_action :login_required 6 before_action :login_required
7 before_action :find_user, :only => [:show, :edit, :update, :destroy, :reset_otp] 7 before_action :find_user, :only => [:show, :edit, :update, :reset_otp, :deactivate, :reactivate]
8 before_action :verify_status, :except => [:index, :show] 8 before_action :verify_status, :except => [:index, :show]
9 9
10 layout 'admin' 10 layout 'admin'
@@ -53,8 +53,25 @@ class UsersController < ApplicationController
53 def show 53 def show
54 end 54 end
55 55
56 def destroy 56 def deactivate
57 @user.destroy if @user 57 return deny_user_access unless current_user.is_admin?
58
59 if @user == current_user
60 flash[:error] = t("flash.users.cannot_deactivate_self")
61 elsif @user.deactivate!(:actor => current_user)
62 flash[:notice] = t("flash.users.deactivated", :login => @user.login)
63 end
64
65 redirect_to users_path
66 end
67
68 def reactivate
69 return deny_user_access unless current_user.is_admin?
70
71 if @user.reactivate!(:actor => current_user)
72 flash[:notice] = t("flash.users.reactivated", :login => @user.login)
73 end
74
58 redirect_to users_path 75 redirect_to users_path
59 end 76 end
60 77
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb
index 4cbe741c..f57ef84f 100644
--- a/app/helpers/node_actions_helper.rb
+++ b/app/helpers/node_actions_helper.rb
@@ -18,7 +18,9 @@ module NodeActionsHelper
18 "asset_destroy" => "file-x", 18 "asset_destroy" => "file-x",
19 "otp_enroll" => "shield-lock", 19 "otp_enroll" => "shield-lock",
20 "otp_disable" => "shield-off", 20 "otp_disable" => "shield-off",
21 "otp_reset" => "shield-x" 21 "otp_reset" => "shield-x",
22 "user_deactivate" => "user-off",
23 "user_reactivate" => "user-check"
22 }.freeze 24 }.freeze
23 25
24 def verb_icon action 26 def verb_icon action
@@ -271,4 +273,14 @@ module NodeActionsHelper
271 t("node_actions.otp_reset", :actor => actor_ref(action), 273 t("node_actions.otp_reset", :actor => actor_ref(action),
272 :target => user_participant_ref(action)).html_safe 274 :target => user_participant_ref(action)).html_safe
273 end 275 end
276
277 def summarize_user_deactivate action
278 t("node_actions.user_deactivate", :actor => actor_ref(action),
279 :target => user_participant_ref(action)).html_safe
280 end
281
282 def summarize_user_reactivate action
283 t("node_actions.user_reactivate", :actor => actor_ref(action),
284 :target => user_participant_ref(action)).html_safe
285 end
274end 286end
diff --git a/app/models/node_action.rb b/app/models/node_action.rb
index fec5a062..d619aac5 100644
--- a/app/models/node_action.rb
+++ b/app/models/node_action.rb
@@ -98,8 +98,9 @@ class NodeAction < ApplicationRecord
98 # "detached_from" -- array of unique_names, only when any 98 # "detached_from" -- array of unique_names, only when any
99 # "headline_removed_from" -- array of unique_names, only when any 99 # "headline_removed_from" -- array of unique_names, only when any
100 # 100 #
101 # "otp_enroll" / "otp_disable" / "otp_reset" (second-factor lifecycle; 101 # "otp_enroll" / "otp_disable" / "otp_reset" / "user_deactivate" /
102 # node column nil; participants: the affected User -- the table's first 102 # "user_reactivate" (second-factor lifecycle; node column nil;
103 # participants: the affected User
103 # User-typed subject. otp_disable is self-service; otp_reset is an 104 # User-typed subject. otp_disable is self-service; otp_reset is an
104 # administrator clearing someone else's factor, where actor and 105 # administrator clearing someone else's factor, where actor and
105 # participant differ): 106 # participant differ):
diff --git a/app/models/user.rb b/app/models/user.rb
index 2e9da86c..1728521a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -105,6 +105,26 @@ class User < ApplicationRecord
105 roles.map { |r| I18n.t("users.roles.#{r}", :default => r) } 105 roles.map { |r| I18n.t("users.roles.#{r}", :default => r) }
106 end 106 end
107 107
108 def deactivate!(actor:)
109 return false if alumni?
110 transaction do
111 update_column(:roles, (roles | ["alumni"]).sort)
112 NodeAction.record!(:participants => [self], :user => actor,
113 :action => "user_deactivate", :target_login => login)
114 end
115 true
116 end
117
118 def reactivate!(actor:)
119 return false unless alumni?
120 transaction do
121 update_column(:roles, (roles - ["alumni"]).sort)
122 NodeAction.record!(:participants => [self], :user => actor,
123 :action => "user_reactivate", :target_login => login)
124 end
125 true
126 end
127
108 # otp_secret present == enrolled. otp_pending_secret holds the secret 128 # otp_secret present == enrolled. otp_pending_secret holds the secret
109 # between QR display and first-code confirmation. otp_consumed_timestep 129 # between QR display and first-code confirmation. otp_consumed_timestep
110 # makes every accepted code single-use (replay guard within the drift 130 # makes every accepted code single-use (replay guard within the drift
diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb
index 04884be8..ff9d4e37 100644
--- a/app/views/users/_user.html.erb
+++ b/app/views/users/_user.html.erb
@@ -9,14 +9,22 @@
9 <% end %> 9 <% end %>
10 </td> 10 </td>
11 <td><%= link_to t("admin.common.show"), user_path(user) %></td> 11 <td><%= link_to t("admin.common.show"), user_path(user) %></td>
12 <% if current_user.admin? || current_user == user %>
13 <td><%= link_to t("admin.common.edit"), edit_user_path(user) %></td>
14 <td> 12 <td>
15 <%= button_to user_path(user), method: :delete, 13 <% if current_user.admin? || current_user == user %>
16 form: { data: { confirm: t(".confirm_destroy", :login => user.login) }, class: 'button_to destructive' } do %> 14 <%= link_to t("admin.common.edit"), edit_user_path(user) %>
17 <%= icon("trash", library: "tabler", "aria-hidden": true) %> <%= t("admin.common.destroy") %> 15 <% end %>
18 <% end %> 16 </td>
17 <td>
18 <% if current_user.admin? && current_user != user %>
19 <% if user.alumni? %>
20 <%= button_to t(".reactivate"), reactivate_user_path(user), method: :put,
21 form: { class: 'button_to state_changing' } %>
22 <% else %>
23 <%= button_to t(".deactivate"), deactivate_user_path(user), method: :put,
24 form: { data: { confirm: t(".confirm_deactivate", :login => user.login) },
25 class: 'button_to destructive' } %>
26 <% end %>
27 <% end %>
19 </td> 28 </td>
20 <% end %>
21</tr> 29</tr>
22<% end %> 30<% end %>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index e15a08d2..8aae7ca5 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -219,6 +219,8 @@ de:
219 otp_enroll: "%{actor} hat einen zweiten Faktor eingerichtet" 219 otp_enroll: "%{actor} hat einen zweiten Faktor eingerichtet"
220 otp_disable: "%{actor} hat den zweiten Faktor entfernt" 220 otp_disable: "%{actor} hat den zweiten Faktor entfernt"
221 otp_reset: "%{actor} hat den zweiten Faktor von %{target} zurückgesetzt" 221 otp_reset: "%{actor} hat den zweiten Faktor von %{target} zurückgesetzt"
222 user_deactivate: "%{actor} hat %{target} deaktiviert"
223 user_reactivate: "%{actor} hat %{target} reaktiviert"
222 224
223 open_gallery: "Gallerie anzeigen" 225 open_gallery: "Gallerie anzeigen"
224 asset_licenses: 226 asset_licenses:
@@ -281,6 +283,9 @@ de:
281 user: 283 user:
282 confirm_destroy: "Benutzer %{login} wirklich löschen?" 284 confirm_destroy: "Benutzer %{login} wirklich löschen?"
283 no_roles: "—" 285 no_roles: "—"
286 deactivate: "Deaktivieren"
287 reactivate: "Reaktivieren"
288 confirm_deactivate: "%{login} deaktivieren? Die Anmeldung wird sofort verweigert, Zuschreibungen bleiben erhalten."
284 index: 289 index:
285 title: "Benutzerkonten" 290 title: "Benutzerkonten"
286 create_editor: "Editor-Konto anlegen" 291 create_editor: "Editor-Konto anlegen"
@@ -578,6 +583,9 @@ de:
578 created: "Benutzer %{login} angelegt" 583 created: "Benutzer %{login} angelegt"
579 updated: "Benutzer %{login} aktualisiert" 584 updated: "Benutzer %{login} aktualisiert"
580 otp_reset: "Zweiter Faktor von %{login} zurückgesetzt" 585 otp_reset: "Zweiter Faktor von %{login} zurückgesetzt"
586 deactivated: "%{login} ist jetzt alumni und kann sich nicht mehr anmelden."
587 reactivated: "%{login} kann sich wieder anmelden."
588 cannot_deactivate_self: "Das eigene Konto kann nicht deaktiviert werden."
581 assets: 589 assets:
582 created: "Asset wurde angelegt." 590 created: "Asset wurde angelegt."
583 updated: "Asset wurde aktualisiert." 591 updated: "Asset wurde aktualisiert."
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f8b94a00..e434538f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -167,6 +167,8 @@ en:
167 otp_enroll: "%{actor} set up a second factor" 167 otp_enroll: "%{actor} set up a second factor"
168 otp_disable: "%{actor} removed their second factor" 168 otp_disable: "%{actor} removed their second factor"
169 otp_reset: "%{actor} reset the second factor of %{target}" 169 otp_reset: "%{actor} reset the second factor of %{target}"
170 user_deactivate: "%{actor} deactivated %{target}"
171 user_reactivate: "%{actor} reactivated %{target}"
170 172
171 open_gallery: "Open gallery" 173 open_gallery: "Open gallery"
172 asset_licenses: 174 asset_licenses:
@@ -229,6 +231,9 @@ en:
229 user: 231 user:
230 confirm_destroy: "Do you really want to destroy user %{login}?" 232 confirm_destroy: "Do you really want to destroy user %{login}?"
231 no_roles: "—" 233 no_roles: "—"
234 deactivate: "Deactivate"
235 reactivate: "Reactivate"
236 confirm_deactivate: "Deactivate %{login}? Sign-in is refused immediately; attributions are preserved."
232 index: 237 index:
233 title: "User accounts" 238 title: "User accounts"
234 create_editor: "Create editor account" 239 create_editor: "Create editor account"
@@ -541,6 +546,9 @@ en:
541 created: "User created %{login}" 546 created: "User created %{login}"
542 updated: "Updated user %{login}" 547 updated: "Updated user %{login}"
543 otp_reset: "Second factor reset for %{login}" 548 otp_reset: "Second factor reset for %{login}"
549 deactivated: "%{login} is now an alumnus and can no longer sign in."
550 reactivated: "%{login} can sign in again."
551 cannot_deactivate_self: "You cannot deactivate your own account."
544 assets: 552 assets:
545 created: "Asset was successfully created." 553 created: "Asset was successfully created."
546 updated: "Asset was successfully updated." 554 updated: "Asset was successfully updated."
diff --git a/config/routes.rb b/config/routes.rb
index 4b5d15c9..1898dbbb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -97,9 +97,11 @@ Cccms::Application.routes.draw do
97 match '/login' => 'sessions#new', :as => :login, :via => :get 97 match '/login' => 'sessions#new', :as => :login, :via => :get
98 match 'search' => 'search#index', :as => :search, :via => :get 98 match 'search' => 'search#index', :as => :search, :via => :get
99 99
100 resources :users do 100 resources :users, :except => :destroy do
101 member do 101 member do
102 put :reset_otp 102 put :reset_otp
103 put :deactivate
104 put :reactivate
103 end 105 end
104 end 106 end
105 resource :otp_enrollment, :only => [:show, :create, :update, :destroy] 107 resource :otp_enrollment, :only => [:show, :create, :update, :destroy]
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index 1c5d16fc..14133029 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -13,7 +13,7 @@ class UsersControllerTest < ActionController::TestCase
13 login_as :aaron 13 login_as :aaron
14 get :index 14 get :index
15 assert_response :success 15 assert_response :success
16 assert_select "button[type=submit]", I18n.t("admin.common.destroy") 16 assert_select "button[type=submit]", I18n.t("users.user.deactivate")
17 assert_select "a", I18n.t("admin.common.show") 17 assert_select "a", I18n.t("admin.common.show")
18 end 18 end
19 19
@@ -146,22 +146,38 @@ class UsersControllerTest < ActionController::TestCase
146 146
147 test "destroying an user being logged in as regular user wont work" do 147 test "destroying an user being logged in as regular user wont work" do
148 login_as :quentin 148 login_as :quentin
149 assert_no_difference "User.count" do 149 put :deactivate, params: { :id => users(:quentin).id }
150 delete :destroy, params: { :id => User.find_by_login("aaron").id } 150
151 end
152 assert_redirected_to users_path 151 assert_redirected_to users_path
153 assert_equal( 152 assert_not users(:quentin).reload.alumni?
154 I18n.t("flash.common.admin_required"),
155 flash[:notice]
156 )
157 end 153 end
158 154
159 test "destroying an user being logged in as admin user" do 155 test "an admin deactivates another user, who can no longer sign in" do
160 login_as :aaron 156 login_as :aaron
161 assert_difference "User.count", -1 do 157 user = users(:quentin)
162 delete :destroy, params: { :id => User.find_by_login("quentin").id } 158
163 end 159 put :deactivate, params: { :id => user.id }
160
164 assert_redirected_to users_path 161 assert_redirected_to users_path
162 assert user.reload.alumni?
163 assert_equal ["admin", "alumni"].sort, user.roles.sort if user.is_admin?
164 end
165
166 test "reactivation restores the other roles untouched" do
167 login_as :aaron
168 user = users(:quentin)
169 user.update_column(:roles, ["alumni", "redaktion"])
170
171 put :reactivate, params: { :id => user.id }
172
173 assert_not user.reload.alumni?
174 assert_equal ["redaktion"], user.roles
175 end
176
177 test "an admin cannot deactivate their own account" do
178 login_as :aaron
179 put :deactivate, params: { :id => users(:aaron).id }
180 assert_not users(:aaron).reload.alumni?
165 end 181 end
166 182
167 test "enrolled user gets a working my account" do 183 test "enrolled user gets a working my account" do