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 /app/models/user.rb | |
| 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.
Diffstat (limited to 'app/models/user.rb')
| -rw-r--r-- | app/models/user.rb | 31 |
1 files changed, 31 insertions, 0 deletions
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 |
