def create
authorize @user, :reset_password?
- @user.send_reset_password_instructions
+ @user.reset_password!
log_action :reset_password, @user
- redirect_to admin_accounts_path
+ redirect_to admin_account_path(@user.account_id)
end
end
end
--- /dev/null
+# frozen_string_literal: true
+
+module Admin
+ class SignInTokenAuthenticationsController < BaseController
+ before_action :set_target_user
+
+ def create
+ authorize @user, :enable_sign_in_token_auth?
+ @user.update(skip_sign_in_token: false)
+ log_action :enable_sign_in_token_auth, @user
+ redirect_to admin_account_path(@user.account_id)
+ end
+
+ def destroy
+ authorize @user, :disable_sign_in_token_auth?
+ @user.update(skip_sign_in_token: true)
+ log_action :disable_sign_in_token_auth, @user
+ redirect_to admin_account_path(@user.account_id)
+ end
+
+ private
+
+ def set_target_user
+ @user = User.find(params[:user_id])
+ end
+ end
+end
@user.disable_two_factor!
log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later!
- redirect_to admin_accounts_path
+ redirect_to admin_account_path(@user.account_id)
end
private
# sign_in_token_sent_at :datetime
# webauthn_id :string
# sign_up_ip :inet
+# skip_sign_in_token :boolean
#
class User < ApplicationRecord
end
def suspicious_sign_in?(ip)
- !otp_required_for_login? && current_sign_in_at.present? && current_sign_in_at < 2.weeks.ago && !recent_ip?(ip)
+ !otp_required_for_login? && !skip_sign_in_token? && current_sign_in_at.present? && !recent_ip?(ip)
end
def functional?
super
end
- def reset_password!(new_password, new_password_confirmation)
+ def reset_password(new_password, new_password_confirmation)
return false if encrypted_password.blank?
super
end
+ def reset_password!
+ # First, change password to something random, invalidate the remember-me token,
+ # and deactivate all sessions
+ transaction do
+ update(remember_token: nil, remember_created_at: nil, password: SecureRandom.hex)
+ session_activations.destroy_all
+ end
+
+ # Then, remove all authorized applications and connected push subscriptions
+ Doorkeeper::AccessGrant.by_resource_owner(self).in_batches.update_all(revoked_at: Time.now.utc)
+
+ Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
+ batch.update_all(revoked_at: Time.now.utc)
+ Web::PushSubscription.where(access_token_id: batch).delete_all
+ end
+
+ # Finally, send a reset password prompt to the user
+ send_reset_password_instructions
+ end
+
def show_all_media?
setting_display_media == 'show_all'
end
admin? && !record.staff?
end
+ def disable_sign_in_token_auth?
+ staff?
+ end
+
+ def enable_sign_in_token_auth?
+ staff?
+ end
+
def confirm?
staff? && !record.confirmed?
end
- else
= t('admin.accounts.confirming')
%td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user)
+ %tr
+ %th{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 }= t('admin.accounts.security')
+ %td{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 }
+ - if @account.user&.two_factor_enabled?
+ = t 'admin.accounts.security_measures.password_and_2fa'
+ - elsif @account.user&.skip_sign_in_token?
+ = t 'admin.accounts.security_measures.only_password'
+ - else
+ = t 'admin.accounts.security_measures.password_and_sign_in_token'
+ %td
+ - if @account.user&.two_factor_enabled?
+ = table_link_to 'unlock', t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete if can?(:disable_2fa, @account.user)
+ - elsif @account.user&.skip_sign_in_token?
+ = table_link_to 'lock', t('admin.accounts.enable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :post if can?(:enable_sign_in_token_auth, @account.user)
+ - else
+ = table_link_to 'unlock', t('admin.accounts.disable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :delete if can?(:disable_sign_in_token_auth, @account.user)
+
+ - if can?(:reset_password, @account.user)
+ %tr
+ %td
+ = table_link_to 'key', t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, data: { confirm: t('admin.accounts.are_you_sure') }
%tr
%th= t('simple_form.labels.defaults.locale')
%div
- if @account.local?
- = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- - if @account.user&.otp_required_for_login?
- = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- if !@account.memorial? && @account.user_approved?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
- else
rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:'
rejecting_media_title: Filtered media
silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:'
- silenced_title: Silenced servers
+ silenced_title: Limited servers
suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:'
suspended_title: Suspended servers
unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.
demote: Demote
destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
disable: Freeze
+ disable_sign_in_token_auth: Disable e-mail token authentication
disable_two_factor_authentication: Disable 2FA
disabled: Frozen
display_name: Display name
email: Email
email_status: Email status
enable: Unfreeze
+ enable_sign_in_token_auth: Enable e-mail token authentication
enabled: Enabled
enabled_msg: Successfully unfroze %{username}'s account
followers: Followers
active: Active
all: All
pending: Pending
- silenced: Silenced
+ silenced: Limited
suspended: Suspended
title: Moderation
moderation_notes: Moderation notes
search: Search
search_same_email_domain: Other users with the same e-mail domain
search_same_ip: Other users with the same IP
- sensitive: Sensitive
- sensitized: marked as sensitive
+ security_measures:
+ only_password: Only password
+ password_and_2fa: Password and 2FA
+ password_and_sign_in_token: Password and e-mail token
+ sensitive: Force-sensitive
+ sensitized: Marked as sensitive
shared_inbox_url: Shared inbox URL
show:
created_reports: Made reports
time_in_queue: Waiting in queue %{time}
title: Accounts
unconfirmed_email: Unconfirmed email
- undo_sensitized: Undo sensitive
- undo_silenced: Undo silence
+ undo_sensitized: Undo force-sensitive
+ undo_silenced: Undo limit
undo_suspension: Undo suspension
- unsilenced_msg: Successfully unlimited %{username}'s account
+ unsilenced_msg: Successfully undid limit of %{username}'s account
unsubscribe: Unsubscribe
unsuspended_msg: Successfully unsuspended %{username}'s account
username: Username
destroy_custom_emoji: Delete Custom Emoji
destroy_domain_allow: Delete Domain Allow
destroy_domain_block: Delete Domain Block
- destroy_email_domain_block: Delete e-mail domain block
+ destroy_email_domain_block: Delete E-mail Domain Block
destroy_ip_block: Delete IP rule
destroy_status: Delete Post
destroy_unavailable_domain: Delete Unavailable Domain
disable_2fa_user: Disable 2FA
disable_custom_emoji: Disable Custom Emoji
+ disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User
disable_user: Disable User
enable_custom_emoji: Enable Custom Emoji
+ enable_sign_in_token_auth_user: Enable E-mail Token Authentication for User
enable_user: Enable User
memorialize_account: Memorialize Account
promote_user: Promote User
reopen_report: Reopen Report
reset_password_user: Reset Password
resolve_report: Resolve Report
- sensitive_account: Mark the media in your account as sensitive
- silence_account: Silence Account
+ sensitive_account: Force-Sensitive Account
+ silence_account: Limit Account
suspend_account: Suspend Account
unassigned_report: Unassign Report
- unsensitive_account: Unmark the media in your account as sensitive
- unsilence_account: Unsilence Account
+ unsensitive_account: Undo Force-Sensitive Account
+ unsilence_account: Undo Limit Account
unsuspend_account: Unsuspend Account
update_announcement: Update Announcement
update_custom_emoji: Update Custom Emoji
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
disable_custom_emoji_html: "%{name} disabled emoji %{target}"
+ disable_sign_in_token_auth_user_html: "%{name} disabled e-mail token authentication for %{target}"
disable_user_html: "%{name} disabled login for user %{target}"
enable_custom_emoji_html: "%{name} enabled emoji %{target}"
+ enable_sign_in_token_auth_user_html: "%{name} enabled e-mail token authentication for %{target}"
enable_user_html: "%{name} enabled login for user %{target}"
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
promote_user_html: "%{name} promoted user %{target}"
reset_password_user_html: "%{name} reset password of user %{target}"
resolve_report_html: "%{name} resolved report %{target}"
sensitive_account_html: "%{name} marked %{target}'s media as sensitive"
- silence_account_html: "%{name} silenced %{target}'s account"
+ silence_account_html: "%{name} limited %{target}'s account"
suspend_account_html: "%{name} suspended %{target}'s account"
unassigned_report_html: "%{name} unassigned report %{target}"
unsensitive_account_html: "%{name} unmarked %{target}'s media as sensitive"
- unsilence_account_html: "%{name} unsilenced %{target}'s account"
+ unsilence_account_html: "%{name} undid limit of %{target}'s account"
unsuspend_account_html: "%{name} unsuspended %{target}'s account"
update_announcement_html: "%{name} updated announcement %{target}"
update_custom_emoji_html: "%{name} updated emoji %{target}"
rejecting_media: rejecting media files
rejecting_reports: rejecting reports
severity:
- silence: silenced
+ silence: limited
suspend: suspended
show:
affected_accounts:
one: One account in the database affected
other: "%{count} accounts in the database affected"
retroactive:
- silence: Unsilence existing affected accounts from this domain
+ silence: Undo limit of existing affected accounts from this domain
suspend: Unsuspend existing affected accounts from this domain
title: Undo domain block for %{domain}
undo: Undo
resources :users, only: [] do
resource :two_factor_authentication, only: [:destroy]
+ resource :sign_in_token_authentication, only: [:create, :destroy]
end
resources :custom_emojis, only: [:index, :new, :create] do
--- /dev/null
+class AddSkipSignInTokenToUsers < ActiveRecord::Migration[6.1]
+ def change
+ add_column :users, :skip_sign_in_token, :boolean
+ end
+end
t.datetime "sign_in_token_sent_at"
t.string "webauthn_id"
t.inet "sign_up_ip"
+ t.boolean "skip_sign_in_token"
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
option :email, required: true
option :confirmed, type: :boolean
- option :role, default: 'user'
+ option :role, default: 'user', enum: %w(user moderator admin)
+ option :skip_sign_in_token, type: :boolean
option :reattach, type: :boolean
option :force, type: :boolean
desc 'create USERNAME', 'Create a new user'
With the --role option one of "user", "admin" or "moderator"
can be supplied. Defaults to "user"
+ With the --skip-sign-in-token option, you can ensure that
+ the user is never asked for an e-mailed security code.
+
With the --reattach option, the new user will be reattached
to a given existing username of an old account. If the old
account is still in use by someone else, you can supply
def create(username)
account = Account.new(username: username)
password = SecureRandom.hex
- user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
+ user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true, skip_sign_in_token: options[:skip_sign_in_token])
if options[:reattach]
account = Account.find_local(username) || Account.new(username: username)
end
end
- option :role
+ option :role, enum: %w(user moderator admin)
option :email
option :confirm, type: :boolean
option :enable, type: :boolean
option :disable_2fa, type: :boolean
option :approve, type: :boolean
option :reset_password, type: :boolean
+ option :skip_sign_in_token, type: :boolean
desc 'modify USERNAME', 'Modify a user'
long_desc <<-LONG_DESC
Modify a user account.
With the --reset-password option, the user's password is replaced by
a randomly-generated one, printed in the output.
+
+ With the --skip-sign-in-token option, you can ensure that
+ the user is never asked for an e-mailed security code.
LONG_DESC
def modify(username)
user = Account.find_local(username)&.user
user.disabled = true if options[:disable]
user.approved = true if options[:approve]
user.otp_required_for_login = false if options[:disable_2fa]
+ user.skip_sign_in_token = options[:skip_sign_in_token] unless options[:skip_sign_in_token].nil?
user.confirm if options[:confirm]
if user.save
post :create, params: { account_id: account.id }
- expect(response).to redirect_to(admin_accounts_path)
+ expect(response).to redirect_to(admin_account_path(account.id))
end
end
end
user.update(otp_required_for_login: true)
end
- it 'redirects to admin accounts page' do
+ it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id }
user.reload
expect(user.otp_enabled?).to eq false
- expect(response).to redirect_to(admin_accounts_path)
+ expect(response).to redirect_to(admin_account_path(user.account_id))
end
end
nickname: 'Security Key')
end
- it 'redirects to admin accounts page' do
+ it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id }
user.reload
expect(user.otp_enabled?).to eq false
expect(user.webauthn_enabled?).to eq false
- expect(response).to redirect_to(admin_accounts_path)
+ expect(response).to redirect_to(admin_account_path(user.account_id))
end
end
end
end
end
+ describe '#reset_password!' do
+ subject(:user) { Fabricate(:user, password: 'foobar12345') }
+
+ let!(:session_activation) { Fabricate(:session_activation, user: user) }
+ let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
+ let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
+
+ before do
+ user.reset_password!
+ end
+
+ it 'changes the password immediately' do
+ expect(user.external_or_valid_password?('foobar12345')).to be false
+ end
+
+ it 'deactivates all sessions' do
+ expect(user.session_activations.count).to eq 0
+ end
+
+ it 'revokes all access tokens' do
+ expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
+ end
+
+ it 'removes push subscriptions' do
+ expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
+ end
+ end
+
describe '#confirm!' do
subject(:user) { Fabricate(:user, confirmed_at: confirmed_at) }