diff --git a/app/Console/Commands/ResetMfa.php b/app/Console/Commands/ResetMfa.php new file mode 100644 index 000000000..feb477943 --- /dev/null +++ b/app/Console/Commands/ResetMfa.php @@ -0,0 +1,74 @@ +<?php + +namespace BookStack\Console\Commands; + +use BookStack\Auth\User; +use Illuminate\Console\Command; + +class ResetMfa extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'bookstack:reset-mfa + {--id= : Numeric ID of the user to reset MFA for} + {--email= : Email address of the user to reset MFA for} + '; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Reset & Clear any configured MFA methods for the given user'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $id = $this->option('id'); + $email = $this->option('email'); + if (!$id && !$email) { + $this->error('Either a --id=<number> or --email=<email> option must be provided.'); + return 1; + } + + /** @var User $user */ + $field = $id ? 'id' : 'email'; + $value = $id ?: $email; + $user = User::query() + ->where($field, '=', $value) + ->first(); + + if (!$user) { + $this->error("A user where {$field}={$value} could not be found."); + return 1; + } + + $this->info("This will delete any configure multi-factor authentication methods for user: \n- ID: {$user->id}\n- Name: {$user->name}\n- Email: {$user->email}\n"); + $this->info('If multi-factor authentication is required for this user they will be asked to reconfigure their methods on next login.'); + $confirm = $this->confirm('Are you sure you want to proceed?'); + if ($confirm) { + $user->mfaValues()->delete(); + $this->info('User MFA methods have been reset.'); + return 0; + } + + return 1; + } +} diff --git a/tests/Commands/ResetMfaCommandTest.php b/tests/Commands/ResetMfaCommandTest.php new file mode 100644 index 000000000..550f6d390 --- /dev/null +++ b/tests/Commands/ResetMfaCommandTest.php @@ -0,0 +1,65 @@ +<?php + +namespace Tests\Commands; + +use BookStack\Auth\Access\Mfa\MfaValue; +use BookStack\Auth\User; +use Tests\TestCase; + +class ResetMfaCommandTest extends TestCase +{ + public function test_command_requires_email_or_id_option() + { + $this->artisan('bookstack:reset-mfa') + ->expectsOutput('Either a --id=<number> or --email=<email> option must be provided.') + ->assertExitCode(1); + } + + public function test_command_runs_with_provided_email() + { + /** @var User $user */ + $user = User::query()->first(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); + + $this->assertEquals(1, $user->mfaValues()->count()); + $this->artisan("bookstack:reset-mfa --email={$user->email}") + ->expectsQuestion('Are you sure you want to proceed?', true) + ->expectsOutput('User MFA methods have been reset.') + ->assertExitCode(0); + $this->assertEquals(0, $user->mfaValues()->count()); + } + + public function test_command_runs_with_provided_id() + { + /** @var User $user */ + $user = User::query()->first(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); + + $this->assertEquals(1, $user->mfaValues()->count()); + $this->artisan("bookstack:reset-mfa --id={$user->id}") + ->expectsQuestion('Are you sure you want to proceed?', true) + ->expectsOutput('User MFA methods have been reset.') + ->assertExitCode(0); + $this->assertEquals(0, $user->mfaValues()->count()); + } + + public function test_saying_no_to_confirmation_does_not_reset_mfa() + { + /** @var User $user */ + $user = User::query()->first(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); + + $this->assertEquals(1, $user->mfaValues()->count()); + $this->artisan("bookstack:reset-mfa --id={$user->id}") + ->expectsQuestion('Are you sure you want to proceed?', false) + ->assertExitCode(1); + $this->assertEquals(1, $user->mfaValues()->count()); + } + + public function test_giving_non_existing_user_shows_error_message() + { + $this->artisan("bookstack:reset-mfa --email=donkeys@example.com") + ->expectsOutput('A user where email=donkeys@example.com could not be found.') + ->assertExitCode(1); + } +}