summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/models/user.rb1
-rw-r--r--config/locales/de.yml2
-rw-r--r--config/locales/en.yml2
-rw-r--r--lib/authenticated_system.rb7
-rw-r--r--lib/authenticated_test_helper.rb6
-rw-r--r--test/controllers/users_controller_test.rb12
-rw-r--r--test/models/user_otp_test.rb32
7 files changed, 56 insertions, 6 deletions
diff --git a/app/models/user.rb b/app/models/user.rb
index c3035a02..4b70ebe5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -242,6 +242,7 @@ class User < ApplicationRecord
242 def admin_needs_second_factor 242 def admin_needs_second_factor
243 return unless roles.include?("admin") 243 return unless roles.include?("admin")
244 return if otp_secret.present? 244 return if otp_secret.present?
245 return if persisted? && roles_in_database.to_a.include?("admin")
245 errors.add(:roles, :admin_needs_otp) 246 errors.add(:roles, :admin_needs_otp)
246 end 247 end
247end 248end
diff --git a/config/locales/de.yml b/config/locales/de.yml
index fcceb023..93712574 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -598,7 +598,7 @@ de:
598 code_mismatch_rescan: "Der Code hat nicht gepasst. Neu scannen oder auf den nächsten Code warten." 598 code_mismatch_rescan: "Der Code hat nicht gepasst. Neu scannen oder auf den nächsten Code warten."
599 wrong_password: "Falsches Passwort." 599 wrong_password: "Falsches Passwort."
600 wrong_credentials: "Passwort oder Code falsch — zweiter Faktor unverändert." 600 wrong_credentials: "Passwort oder Code falsch — zweiter Faktor unverändert."
601 enabled: "Zweiter Faktor aktiviert. Der eben eingegebene Code ist verbraucht — für die Anmeldung auf den nächsten warten." 601 enabled: "Zweiter Faktor aktiviert. (Der eben eingegebene Code ist jetzt verbraucht.)"
602 disabled: "Zweiter Faktor entfernt." 602 disabled: "Zweiter Faktor entfernt."
603 users: 603 users:
604 created: "Benutzer %{login} angelegt" 604 created: "Benutzer %{login} angelegt"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 81d9f4fb..cd502b31 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -558,7 +558,7 @@ en:
558 code_mismatch_rescan: "That code did not match. Rescan or wait for the next code." 558 code_mismatch_rescan: "That code did not match. Rescan or wait for the next code."
559 wrong_password: "Wrong password." 559 wrong_password: "Wrong password."
560 wrong_credentials: "Password or code wrong -- second factor unchanged." 560 wrong_credentials: "Password or code wrong -- second factor unchanged."
561 enabled: "Second factor enabled. The code you just entered is spent -- wait for the next one before logging in with it." 561 enabled: "Second factor enabled. (The OTP code is spent now.)"
562 disabled: "Second factor disabled." 562 disabled: "Second factor disabled."
563 users: 563 users:
564 created: "User created %{login}" 564 created: "User created %{login}"
diff --git a/lib/authenticated_system.rb b/lib/authenticated_system.rb
index 04d8051f..668436b5 100644
--- a/lib/authenticated_system.rb
+++ b/lib/authenticated_system.rb
@@ -25,9 +25,10 @@ module AuthenticatedSystem
25 # Tied to is_admin? so losing the role closes the window at once, rather 25 # Tied to is_admin? so losing the role closes the window at once, rather
26 # than leaving a timestamp that would count again if the role returned. 26 # than leaving a timestamp that would count again if the role returned.
27 def elevated? 27 def elevated?
28 return false unless current_user&.is_admin? 28 return false unless current_user&.is_admin?
29 session[:elevated_at].to_i > ELEVATION_MAX_AGE.ago.to_i 29 return false unless current_user.otp_enrolled?
30 end 30 session[:elevated_at].to_i > ELEVATION_MAX_AGE.ago.to_i
31 end
31 32
32 def elevation_expires_at 33 def elevation_expires_at
33 return nil unless elevated? 34 return nil unless elevated?
diff --git a/lib/authenticated_test_helper.rb b/lib/authenticated_test_helper.rb
index 065a5f7d..483e6c88 100644
--- a/lib/authenticated_test_helper.rb
+++ b/lib/authenticated_test_helper.rb
@@ -6,6 +6,10 @@ module AuthenticatedTestHelper
6 end 6 end
7 7
8 def elevate_session! 8 def elevate_session!
9 session[:elevated_at] = Time.now.to_i 9 user = User.find_by(:id => @request.session[:user_id])
10 if user && !user.otp_enrolled?
11 user.update_column(:otp_secret, ROTP::Base32.random)
12 end
13 @request.session[:elevated_at] = Time.now.to_i
10 end 14 end
11end 15end
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index 2dd0759a..0c517647 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -273,4 +273,16 @@ class UsersControllerTest < ActionController::TestCase
273 273
274 assert_not user.reload.is_admin? 274 assert_not user.reload.is_admin?
275 end 275 end
276
277 test "clearing the factor closes an open elevation window" do
278 login_as :aaron
279 elevate_session!
280 get :new, params: { :locale => "de" }
281 assert_response :success
282
283 users(:aaron).update_column(:otp_secret, nil)
284
285 get :new, params: { :locale => "de" }
286 assert_redirected_to new_elevation_path
287 end
276end 288end
diff --git a/test/models/user_otp_test.rb b/test/models/user_otp_test.rb
index 81f25575..2044ac18 100644
--- a/test/models/user_otp_test.rb
+++ b/test/models/user_otp_test.rb
@@ -74,6 +74,38 @@ class UserOtpTest < ActiveSupport::TestCase
74 assert_equal @user.login, action.metadata["target_login"] 74 assert_equal @user.login, action.metadata["target_login"]
75 end 75 end
76 76
77 test "an admin without a factor can start enrollment" do
78 admin = users(:aaron)
79 assert admin.is_admin?
80 assert_not admin.otp_enrolled?
81
82 uri = admin.begin_otp_enrollment!
83
84 assert admin.reload.otp_pending_secret.present?
85 assert uri.present?
86 end
87
88 test "an admin disabling their factor keeps the role" do
89 admin = users(:aaron)
90 admin.update_column(:otp_secret, ROTP::Base32.random)
91
92 admin.disable_otp!(:actor => admin)
93
94 assert_not admin.reload.otp_enrolled?
95 assert admin.is_admin?
96 end
97
98 test "a fellow admin can reset an admin's factor" do
99 admin = users(:aaron)
100 admin.update_column(:otp_secret, ROTP::Base32.random)
101 actor = users(:redella)
102
103 admin.disable_otp!(:actor => actor)
104
105 assert_not admin.reload.otp_enrolled?
106 assert_equal "otp_reset", NodeAction.last.action
107 end
108
77 private 109 private
78 110
79 def enroll!(user) 111 def enroll!(user)