diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-01 04:14:00 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-01 04:14:00 +0200 |
| commit | abd7ee1fc2ecc15b50944db30c59bedc26ec41b6 (patch) | |
| tree | 821bb8ca610664c57d9065bf62285166243500a4 | |
| parent | 6df48c1413a14516e7ee8919f33fbc13f0141966 (diff) | |
Let Redaktion grant and revoke its own role
Any holder may add or remove another account, witnessed as
redaktion_grant/revoke so the vouching is legible. Not behind elevation:
onboarding must not wait for a keyholder, and a compromised Redaktion
account can already publish.
| -rw-r--r-- | app/controllers/users_controller.rb | 30 | ||||
| -rw-r--r-- | app/helpers/node_actions_helper.rb | 14 | ||||
| -rw-r--r-- | app/models/user.rb | 31 | ||||
| -rw-r--r-- | app/views/users/_user.html.erb | 17 | ||||
| -rw-r--r-- | app/views/users/index.html.erb | 1 | ||||
| -rw-r--r-- | config/locales/de.yml | 13 | ||||
| -rw-r--r-- | config/locales/en.yml | 15 | ||||
| -rw-r--r-- | config/routes.rb | 2 | ||||
| -rw-r--r-- | test/controllers/users_controller_test.rb | 20 |
9 files changed, 134 insertions, 9 deletions
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 583ebac0..9b9d64e5 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb | |||
| @@ -5,17 +5,17 @@ class UsersController < ApplicationController | |||
| 5 | # Private | 5 | # Private |
| 6 | 6 | ||
| 7 | before_action :login_required | 7 | before_action :login_required |
| 8 | before_action :find_user, :only => [:show, :edit, :update, :reset_otp, :deactivate, :reactivate] | 8 | before_action :find_user, :only => [:show, :edit, :update, :reset_otp, :deactivate, :reactivate, :grant_redaktion, :revoke_redaktion] |
| 9 | before_action :require_admin, :only => [:index, :new, :create, :reset_otp, :deactivate, :reactivate] | 9 | before_action :require_redaktion, :only => [:index] |
| 10 | before_action :require_admin, :only => [:new, :create, :reset_otp, :deactivate, :reactivate] | ||
| 10 | before_action :require_elevation, :only => [:new, :create, :reset_otp, :deactivate, :reactivate] | 11 | before_action :require_elevation, :only => [:new, :create, :reset_otp, :deactivate, :reactivate] |
| 11 | before_action :verify_status, :except => [:index] | 12 | before_action :verify_status, :except => [:index, :grant_redaktion, :revoke_redaktion] |
| 12 | 13 | ||
| 13 | layout 'admin' | 14 | layout 'admin' |
| 14 | 15 | ||
| 15 | ROLE_PRESETS = { | 16 | ROLE_PRESETS = { |
| 16 | "editor" => [], | 17 | "editor" => [], |
| 17 | "redaktion" => ["redaktion"], | 18 | "redaktion" => ["redaktion"], |
| 18 | "admin" => ["admin", "redaktion"] | ||
| 19 | }.freeze | 19 | }.freeze |
| 20 | 20 | ||
| 21 | GROUP_ORDER = [:admin, :redaktion, :editor, :alumni].freeze | 21 | GROUP_ORDER = [:admin, :redaktion, :editor, :alumni].freeze |
| @@ -74,6 +74,28 @@ class UsersController < ApplicationController | |||
| 74 | redirect_to users_path | 74 | redirect_to users_path |
| 75 | end | 75 | end |
| 76 | 76 | ||
| 77 | def grant_redaktion | ||
| 78 | return deny_role_access(:redaktion_required) unless current_user.redaktion? | ||
| 79 | |||
| 80 | case @user.grant_redaktion!(:actor => current_user) | ||
| 81 | when :granted then flash[:notice] = t("flash.users.redaktion_granted", :login => @user.login) | ||
| 82 | when :no_second_factor then flash[:error] = t("flash.users.redaktion_needs_otp", :login => @user.login) | ||
| 83 | end | ||
| 84 | |||
| 85 | redirect_to users_path | ||
| 86 | end | ||
| 87 | |||
| 88 | def revoke_redaktion | ||
| 89 | return deny_role_access(:redaktion_required) unless current_user.redaktion? | ||
| 90 | |||
| 91 | case @user.revoke_redaktion!(:actor => current_user) | ||
| 92 | when :revoked then flash[:notice] = t("flash.users.redaktion_revoked", :login => @user.login) | ||
| 93 | when :self then flash[:error] = t("flash.users.redaktion_not_self") | ||
| 94 | end | ||
| 95 | |||
| 96 | redirect_to users_path | ||
| 97 | end | ||
| 98 | |||
| 77 | def reset_otp | 99 | def reset_otp |
| 78 | @user.disable_otp!(:actor => current_user) | 100 | @user.disable_otp!(:actor => current_user) |
| 79 | flash[:notice] = t("flash.users.otp_reset", :login => @user.login) | 101 | flash[:notice] = t("flash.users.otp_reset", :login => @user.login) |
diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb index f57ef84f..53f6ecd0 100644 --- a/app/helpers/node_actions_helper.rb +++ b/app/helpers/node_actions_helper.rb | |||
| @@ -20,7 +20,9 @@ module NodeActionsHelper | |||
| 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", | 22 | "user_deactivate" => "user-off", |
| 23 | "user_reactivate" => "user-check" | 23 | "user_reactivate" => "user-check", |
| 24 | "redaktion_grant" => "user-plus", | ||
| 25 | "redaktion_revoke" => "user-minus" | ||
| 24 | }.freeze | 26 | }.freeze |
| 25 | 27 | ||
| 26 | def verb_icon action | 28 | def verb_icon action |
| @@ -283,4 +285,14 @@ module NodeActionsHelper | |||
| 283 | t("node_actions.user_reactivate", :actor => actor_ref(action), | 285 | t("node_actions.user_reactivate", :actor => actor_ref(action), |
| 284 | :target => user_participant_ref(action)).html_safe | 286 | :target => user_participant_ref(action)).html_safe |
| 285 | end | 287 | end |
| 288 | |||
| 289 | def summarize_redaktion_grant action | ||
| 290 | t("node_actions.redaktion_grant", :actor => actor_ref(action), | ||
| 291 | :target => user_participant_ref(action)).html_safe | ||
| 292 | end | ||
| 293 | |||
| 294 | def summarize_redaktion_revoke action | ||
| 295 | t("node_actions.redaktion_revoke", :actor => actor_ref(action), | ||
| 296 | :target => user_participant_ref(action)).html_safe | ||
| 297 | end | ||
| 286 | end | 298 | end |
diff --git a/app/models/user.rb b/app/models/user.rb index bf0f40ee..c3035a02 100644 --- a/app/models/user.rb +++ b/app/models/user.rb | |||
| @@ -25,6 +25,7 @@ class User < ApplicationRecord | |||
| 25 | :message => Authentication.bad_email_message | 25 | :message => Authentication.bad_email_message |
| 26 | 26 | ||
| 27 | validate :roles_are_known | 27 | validate :roles_are_known |
| 28 | validate :admin_needs_second_factor | ||
| 28 | 29 | ||
| 29 | # Authenticates a user by their login name and unencrypted password. Returns the user or nil. | 30 | # Authenticates a user by their login name and unencrypted password. Returns the user or nil. |
| 30 | def self.authenticate(login, password) | 31 | def self.authenticate(login, password) |
| @@ -136,6 +137,30 @@ class User < ApplicationRecord | |||
| 136 | true | 137 | true |
| 137 | end | 138 | end |
| 138 | 139 | ||
| 140 | def grant_redaktion!(actor:) | ||
| 141 | return :already if redaktion? | ||
| 142 | return :no_second_factor unless otp_enrolled? | ||
| 143 | |||
| 144 | transaction do | ||
| 145 | update_column(:roles, (roles | ["redaktion"]).sort) | ||
| 146 | NodeAction.record!(:participants => [self], :user => actor, | ||
| 147 | :action => "redaktion_grant", :target_login => login) | ||
| 148 | end | ||
| 149 | :granted | ||
| 150 | end | ||
| 151 | |||
| 152 | def revoke_redaktion!(actor:) | ||
| 153 | return :already unless redaktion? | ||
| 154 | return :self unless actor != self | ||
| 155 | |||
| 156 | transaction do | ||
| 157 | update_column(:roles, (roles - ["redaktion"]).sort) | ||
| 158 | NodeAction.record!(:participants => [self], :user => actor, | ||
| 159 | :action => "redaktion_revoke", :target_login => login) | ||
| 160 | end | ||
| 161 | :revoked | ||
| 162 | end | ||
| 163 | |||
| 139 | # otp_secret present == enrolled. otp_pending_secret holds the secret | 164 | # otp_secret present == enrolled. otp_pending_secret holds the secret |
| 140 | # between QR display and first-code confirmation. otp_consumed_timestep | 165 | # between QR display and first-code confirmation. otp_consumed_timestep |
| 141 | # makes every accepted code single-use (replay guard within the drift | 166 | # makes every accepted code single-use (replay guard within the drift |
| @@ -213,4 +238,10 @@ class User < ApplicationRecord | |||
| 213 | unknown = roles.to_a - ROLES | 238 | unknown = roles.to_a - ROLES |
| 214 | errors.add(:roles, :unknown, :list => unknown.join(", ")) if unknown.any? | 239 | errors.add(:roles, :unknown, :list => unknown.join(", ")) if unknown.any? |
| 215 | end | 240 | end |
| 241 | |||
| 242 | def admin_needs_second_factor | ||
| 243 | return unless roles.include?("admin") | ||
| 244 | return if otp_secret.present? | ||
| 245 | errors.add(:roles, :admin_needs_otp) | ||
| 246 | end | ||
| 216 | end | 247 | end |
diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb index ff9d4e37..ba82375d 100644 --- a/app/views/users/_user.html.erb +++ b/app/views/users/_user.html.erb | |||
| @@ -26,5 +26,22 @@ | |||
| 26 | <% end %> | 26 | <% end %> |
| 27 | <% end %> | 27 | <% end %> |
| 28 | </td> | 28 | </td> |
| 29 | <td> | ||
| 30 | <% if current_user.redaktion? && !user.alumni? %> | ||
| 31 | <% if user.redaktion? %> | ||
| 32 | <% unless user == current_user %> | ||
| 33 | <%= button_to t(".revoke_redaktion"), revoke_redaktion_user_path(user), method: :put, | ||
| 34 | form: { data: { confirm: t(".confirm_revoke_redaktion", :login => user.login) }, | ||
| 35 | class: 'button_to destructive' } %> | ||
| 36 | <% end %> | ||
| 37 | <% elsif user.otp_enrolled? %> | ||
| 38 | <%= button_to t(".grant_redaktion"), grant_redaktion_user_path(user), method: :put, | ||
| 39 | form: { data: { confirm: t(".confirm_grant_redaktion", :login => user.login) }, | ||
| 40 | class: 'button_to state_changing' } %> | ||
| 41 | <% else %> | ||
| 42 | <span class="field_hint"><%= t(".needs_otp") %></span> | ||
| 43 | <% end %> | ||
| 44 | <% end %> | ||
| 45 | </td> | ||
| 29 | </tr> | 46 | </tr> |
| 30 | <% end %> | 47 | <% end %> |
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 854811a2..2936bbea 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb | |||
| @@ -8,6 +8,7 @@ | |||
| 8 | <% end %> | 8 | <% end %> |
| 9 | <% end %> | 9 | <% end %> |
| 10 | </p> | 10 | </p> |
| 11 | <p class="field_hint"><%= t(".admin_hint") %></p> | ||
| 11 | 12 | ||
| 12 | <% UsersController::GROUP_ORDER.each do |group| %> | 13 | <% UsersController::GROUP_ORDER.each do |group| %> |
| 13 | <% members = @users[group] || [] %> | 14 | <% members = @users[group] || [] %> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 6b5c97cc..67117945 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -150,6 +150,7 @@ de: | |||
| 150 | attributes: | 150 | attributes: |
| 151 | roles: | 151 | roles: |
| 152 | unknown: "enthält unbekannte Rollen: %{list}" | 152 | unknown: "enthält unbekannte Rollen: %{list}" |
| 153 | admin_needs_otp: "Die Administrator-Rolle setzt einen zweiten Faktor voraus. Der Nutzer muss diesen zuerst selbst unter »Mein Konto« einrichten" | ||
| 153 | related_asset: | 154 | related_asset: |
| 154 | attributes: | 155 | attributes: |
| 155 | headline: | 156 | headline: |
| @@ -226,6 +227,8 @@ de: | |||
| 226 | otp_reset: "%{actor} hat den zweiten Faktor von %{target} zurückgesetzt" | 227 | otp_reset: "%{actor} hat den zweiten Faktor von %{target} zurückgesetzt" |
| 227 | user_deactivate: "%{actor} hat %{target} deaktiviert" | 228 | user_deactivate: "%{actor} hat %{target} deaktiviert" |
| 228 | user_reactivate: "%{actor} hat %{target} reaktiviert" | 229 | user_reactivate: "%{actor} hat %{target} reaktiviert" |
| 230 | redaktion_grant: "%{actor} hat %{target} in die Redaktion aufgenommen" | ||
| 231 | redaktion_revoke: "%{actor} hat %{target} aus der Redaktion entfernt" | ||
| 229 | 232 | ||
| 230 | open_gallery: "Gallerie anzeigen" | 233 | open_gallery: "Gallerie anzeigen" |
| 231 | asset_licenses: | 234 | asset_licenses: |
| @@ -291,6 +294,11 @@ de: | |||
| 291 | deactivate: "Deaktivieren" | 294 | deactivate: "Deaktivieren" |
| 292 | reactivate: "Reaktivieren" | 295 | reactivate: "Reaktivieren" |
| 293 | confirm_deactivate: "%{login} deaktivieren? Die Anmeldung wird sofort verweigert, Zuschreibungen bleiben erhalten." | 296 | confirm_deactivate: "%{login} deaktivieren? Die Anmeldung wird sofort verweigert, Zuschreibungen bleiben erhalten." |
| 297 | grant_redaktion: "In die Redaktion aufnehmen" | ||
| 298 | revoke_redaktion: "Aus der Redaktion entfernen" | ||
| 299 | confirm_grant_redaktion: "%{login} in die Redaktion aufnehmen? Damit darf %{login} in den geschützten Bereichen veröffentlichen." | ||
| 300 | confirm_revoke_redaktion: "%{login} aus der Redaktion entfernen?" | ||
| 301 | needs_otp: "zweiter Faktor fehlt" | ||
| 294 | index: | 302 | index: |
| 295 | title: "Benutzerkonten" | 303 | title: "Benutzerkonten" |
| 296 | create_editor: "Editor-Konto anlegen" | 304 | create_editor: "Editor-Konto anlegen" |
| @@ -301,6 +309,7 @@ de: | |||
| 301 | group_editor: "Editors" | 309 | group_editor: "Editors" |
| 302 | group_alumni: "Ehemalige" | 310 | group_alumni: "Ehemalige" |
| 303 | group_empty: "— keine —" | 311 | group_empty: "— keine —" |
| 312 | admin_hint: "Administrative Rechte lassen sich erst vergeben, nachdem der Nutzer sich angemeldet und einen zweiten Faktor eingerichtet hat." | ||
| 304 | labels: | 313 | labels: |
| 305 | roles: "Rollen" | 314 | roles: "Rollen" |
| 306 | roles: | 315 | roles: |
| @@ -595,6 +604,10 @@ de: | |||
| 595 | deactivated: "%{login} ist jetzt alumni und kann sich nicht mehr anmelden." | 604 | deactivated: "%{login} ist jetzt alumni und kann sich nicht mehr anmelden." |
| 596 | reactivated: "%{login} kann sich wieder anmelden." | 605 | reactivated: "%{login} kann sich wieder anmelden." |
| 597 | cannot_deactivate_self: "Das eigene Konto kann nicht deaktiviert werden." | 606 | cannot_deactivate_self: "Das eigene Konto kann nicht deaktiviert werden." |
| 607 | redaktion_granted: "%{login} gehört jetzt zur Redaktion." | ||
| 608 | redaktion_revoked: "%{login} gehört nicht mehr zur Redaktion." | ||
| 609 | redaktion_needs_otp: "%{login} braucht zuerst einen zweiten Faktor." | ||
| 610 | redaktion_not_self: "Die eigene Redaktions-Rolle kann nicht abgegeben werden." | ||
| 598 | assets: | 611 | assets: |
| 599 | created: "Asset wurde angelegt." | 612 | created: "Asset wurde angelegt." |
| 600 | updated: "Asset wurde aktualisiert." | 613 | updated: "Asset wurde aktualisiert." |
diff --git a/config/locales/en.yml b/config/locales/en.yml index e4c7ecc4..95eb7e94 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -97,6 +97,7 @@ en: | |||
| 97 | attributes: | 97 | attributes: |
| 98 | roles: | 98 | roles: |
| 99 | unknown: "contains unknown roles: %{list}" | 99 | unknown: "contains unknown roles: %{list}" |
| 100 | admin_needs_otp: "Administrator role requires an enrolled second factor. The user has to set one up under My account first" | ||
| 100 | related_asset: | 101 | related_asset: |
| 101 | attributes: | 102 | attributes: |
| 102 | headline: | 103 | headline: |
| @@ -170,6 +171,8 @@ en: | |||
| 170 | otp_reset: "%{actor} reset the second factor of %{target}" | 171 | otp_reset: "%{actor} reset the second factor of %{target}" |
| 171 | user_deactivate: "%{actor} deactivated %{target}" | 172 | user_deactivate: "%{actor} deactivated %{target}" |
| 172 | user_reactivate: "%{actor} reactivated %{target}" | 173 | user_reactivate: "%{actor} reactivated %{target}" |
| 174 | redaktion_grant: "%{actor} added %{target} to Redaktion" | ||
| 175 | redaktion_revoke: "%{actor} removed %{target} from Redaktion" | ||
| 173 | 176 | ||
| 174 | open_gallery: "Open gallery" | 177 | open_gallery: "Open gallery" |
| 175 | asset_licenses: | 178 | asset_licenses: |
| @@ -235,6 +238,12 @@ en: | |||
| 235 | deactivate: "Deactivate" | 238 | deactivate: "Deactivate" |
| 236 | reactivate: "Reactivate" | 239 | reactivate: "Reactivate" |
| 237 | confirm_deactivate: "Deactivate %{login}? Sign-in is refused immediately; attributions are preserved." | 240 | confirm_deactivate: "Deactivate %{login}? Sign-in is refused immediately; attributions are preserved." |
| 241 | grant_redaktion: "Add to Redaktion" | ||
| 242 | revoke_redaktion: "Remove from Redaktion" | ||
| 243 | confirm_grant_redaktion: "Add %{login} to Redaktion? They will be able to publish in the protected sections." | ||
| 244 | confirm_revoke_redaktion: "Remove %{login} from Redaktion?" | ||
| 245 | needs_otp: "no second factor" | ||
| 246 | |||
| 238 | index: | 247 | index: |
| 239 | title: "User accounts" | 248 | title: "User accounts" |
| 240 | create_editor: "Create editor account" | 249 | create_editor: "Create editor account" |
| @@ -245,6 +254,7 @@ en: | |||
| 245 | group_editor: "Editors" | 254 | group_editor: "Editors" |
| 246 | group_alumni: "Alumni" | 255 | group_alumni: "Alumni" |
| 247 | group_empty: "— none —" | 256 | group_empty: "— none —" |
| 257 | admin_hint: "Administrative rights can only be granted after the account has signed in and set up a second factor." | ||
| 248 | labels: | 258 | labels: |
| 249 | roles: "Roles" | 259 | roles: "Roles" |
| 250 | roles: | 260 | roles: |
| @@ -554,6 +564,11 @@ en: | |||
| 554 | deactivated: "%{login} is now an alumnus and can no longer sign in." | 564 | deactivated: "%{login} is now an alumnus and can no longer sign in." |
| 555 | reactivated: "%{login} can sign in again." | 565 | reactivated: "%{login} can sign in again." |
| 556 | cannot_deactivate_self: "You cannot deactivate your own account." | 566 | cannot_deactivate_self: "You cannot deactivate your own account." |
| 567 | redaktion_granted: "%{login} is now part of Redaktion." | ||
| 568 | redaktion_revoked: "%{login} is no longer part of Redaktion." | ||
| 569 | redaktion_needs_otp: "%{login} needs a second factor first." | ||
| 570 | redaktion_not_self: "You cannot give up your own Redaktion role." | ||
| 571 | |||
| 557 | assets: | 572 | assets: |
| 558 | created: "Asset was successfully created." | 573 | created: "Asset was successfully created." |
| 559 | updated: "Asset was successfully updated." | 574 | updated: "Asset was successfully updated." |
diff --git a/config/routes.rb b/config/routes.rb index 58e1e632..d19ac25b 100644 --- a/config/routes.rb +++ b/config/routes.rb | |||
| @@ -102,6 +102,8 @@ Cccms::Application.routes.draw do | |||
| 102 | put :reset_otp | 102 | put :reset_otp |
| 103 | put :deactivate | 103 | put :deactivate |
| 104 | put :reactivate | 104 | put :reactivate |
| 105 | put :grant_redaktion | ||
| 106 | put :revoke_redaktion | ||
| 105 | end | 107 | end |
| 106 | end | 108 | end |
| 107 | resource :otp_enrollment, :only => [:show, :create, :update, :destroy] | 109 | 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 b6f0970d..d7d8b9a6 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb | |||
| @@ -6,7 +6,7 @@ class UsersControllerTest < ActionController::TestCase | |||
| 6 | login_as :quentin | 6 | login_as :quentin |
| 7 | get :index | 7 | get :index |
| 8 | assert_redirected_to admin_path | 8 | assert_redirected_to admin_path |
| 9 | assert_equal I18n.t("flash.common.admin_required"), flash[:error] | 9 | assert_equal I18n.t("flash.common.redaktion_required"), flash[:error] |
| 10 | end | 10 | end |
| 11 | 11 | ||
| 12 | test "get index as admin shows every group with per-row actions" do | 12 | test "get index as admin shows every group with per-row actions" do |
| @@ -53,7 +53,7 @@ class UsersControllerTest < ActionController::TestCase | |||
| 53 | assert !User.last.admin | 53 | assert !User.last.admin |
| 54 | end | 54 | end |
| 55 | 55 | ||
| 56 | test "creating new admin users being logged in as admin" do | 56 | test "creating a Redaktion account" do |
| 57 | login_as :aaron | 57 | login_as :aaron |
| 58 | elevate_session! | 58 | elevate_session! |
| 59 | assert_difference "User.count", +1 do | 59 | assert_difference "User.count", +1 do |
| @@ -63,13 +63,14 @@ class UsersControllerTest < ActionController::TestCase | |||
| 63 | :email => "foo@bar.com", | 63 | :email => "foo@bar.com", |
| 64 | :password => "xxxzzz", | 64 | :password => "xxxzzz", |
| 65 | :password_confirmation => "xxxzzz", | 65 | :password_confirmation => "xxxzzz", |
| 66 | :roles => ["admin", "redaktion"] | 66 | :roles => ["redaktion"] |
| 67 | } | 67 | } |
| 68 | } | 68 | } |
| 69 | end | 69 | end |
| 70 | 70 | ||
| 71 | assert_redirected_to user_path(User.last) | 71 | assert_redirected_to user_path(User.last) |
| 72 | assert User.last.admin | 72 | assert User.last.redaktion? |
| 73 | assert_not User.last.is_admin? | ||
| 73 | end | 74 | end |
| 74 | 75 | ||
| 75 | test "creating new users not being logged as regular user wont work" do | 76 | test "creating new users not being logged as regular user wont work" do |
| @@ -196,6 +197,7 @@ class UsersControllerTest < ActionController::TestCase | |||
| 196 | login_as :aaron | 197 | login_as :aaron |
| 197 | elevate_session! | 198 | elevate_session! |
| 198 | user = users(:quentin) | 199 | user = users(:quentin) |
| 200 | user.update_column(:otp_secret, ROTP::Base32.random) | ||
| 199 | put :update, params: { :id => user.id, :user => {:roles => ["admin", "redaktion"]} } | 201 | put :update, params: { :id => user.id, :user => {:roles => ["admin", "redaktion"]} } |
| 200 | 202 | ||
| 201 | assert_equal true, user.reload.is_admin? | 203 | assert_equal true, user.reload.is_admin? |
| @@ -261,4 +263,14 @@ class UsersControllerTest < ActionController::TestCase | |||
| 261 | 263 | ||
| 262 | assert_not user.reload.is_admin? | 264 | assert_not user.reload.is_admin? |
| 263 | end | 265 | end |
| 266 | |||
| 267 | test "an account without a second factor cannot be promoted to admin" do | ||
| 268 | login_as :aaron | ||
| 269 | elevate_session! | ||
| 270 | user = users(:quentin) | ||
| 271 | |||
| 272 | put :update, params: { :id => user.id, :user => { :roles => ["admin"] } } | ||
| 273 | |||
| 274 | assert_not user.reload.is_admin? | ||
| 275 | end | ||
| 264 | end | 276 | end |
