summaryrefslogtreecommitdiff
path: root/app/controllers/elevations_controller.rb
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-01 01:10:35 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-01 01:10:35 +0200
commitf6c1f0f08f031778a491465d35ac694bfcdc12b0 (patch)
tree99aec0f419b1122821124fdb7ae8bdf4bc5831db /app/controllers/elevations_controller.rb
parentdf90138fb55f7d3652d3d69d58325d7329920f51 (diff)
Require a fresh second factor for user management
Administrative actions are gated behind a 30-minute elevation window: creating and retiring accounts, editing roles, clearing a second factor. Reading the list is not gated, and content work is untouched. elevated? is tied to is_admin?, so losing the role closes the window at once. The window opens when the second factor verifies at login, so an admin heading straight for user management is already elevated, and closes on logout with the other session state. Five wrong codes end the session, mirroring the login challenge. users#update carries no elevation filter, since self-service reaches it; the role field is gated in user_params instead and fails closed.
Diffstat (limited to 'app/controllers/elevations_controller.rb')
-rw-r--r--app/controllers/elevations_controller.rb44
1 files changed, 44 insertions, 0 deletions
diff --git a/app/controllers/elevations_controller.rb b/app/controllers/elevations_controller.rb
new file mode 100644
index 00000000..804b8f95
--- /dev/null
+++ b/app/controllers/elevations_controller.rb
@@ -0,0 +1,44 @@
1# Step-up authentication for janitorial work. Mirrors the two-step login's
2# attempt cap: verify_otp! guards replay but not rate, and an attacker with a
3# stolen cookie would otherwise have unlimited tries at a six-digit code.
4class ElevationsController < ApplicationController
5 include RoleRequired
6
7 layout 'admin'
8
9 before_action :login_required
10 before_action :require_admin
11
12 MAX_ATTEMPTS = 5
13
14 def new
15 redirect_to admin_path if elevated?
16 end
17
18 def create
19 return render(:new) unless current_user.otp_enrolled?
20
21 session[:elevation_attempts] = session[:elevation_attempts].to_i + 1
22 if session[:elevation_attempts] > MAX_ATTEMPTS
23 logout_killing_session!
24 flash[:error] = t("flash.otp.too_many_attempts")
25 return redirect_to(login_path)
26 end
27
28 if current_user.verify_otp!(params[:code])
29 session.delete(:elevation_attempts)
30 elevate!
31 redirect_to safe_return_to(session.delete(:elevation_return_to),
32 :default => users_path)
33 else
34 flash.now[:error] = t("flash.otp.code_mismatch")
35 render :new
36 end
37 end
38
39 def destroy
40 drop_elevation!
41 flash[:notice] = t("flash.elevation.dropped")
42 redirect_to admin_path
43 end
44end