...] * : User ID, login, or email. Without any, shows a summary. * * ## EXAMPLES * * # Summary, including whether the wp-config.php bypass is set. * $ wp ao 2fa status * * # One user: whether enrolled, and whether the secret can be read. * $ wp ao 2fa status alice * * @param string[] $args Positional arguments. * * @return void */ public function status( $args ) { $recovery = new Two_Factor_Recovery(); if ( empty( $args ) ) { $enrolled = 0; $after = 0; do { $ids = Two_Factor_Recovery::enrolled_user_ids( $after ); $enrolled += count( $ids ); $after = $ids ? max( $ids ) : $after; } while ( 200 === count( $ids ) ); \WP_CLI::line( sprintf( 'Users with two-factor data: %d', $enrolled ) ); $bypass = $recovery->bypass_status(); \WP_CLI::line( 'Emergency bypass (ADMINOPTIM_2FA_BYPASS): ' . ( 'off' === $bypass['state'] ? 'not set' : $bypass['state'] . ' for ' . implode( ', ', $bypass['users'] ) . ( $bypass['until'] ? ' until ' . gmdate( 'Y-m-d H:i:s', $bypass['until'] ) . ' UTC' : '' ) ) ); \WP_CLI::line( 'Email a one-time code when a secret cannot be read: ' . ( Two_Factor_Email_Otp::is_enabled( get_option( Two_Factor_Authentication::OPTION_NAME, [] ) ) ? 'on' : 'off' ) ); return; } foreach ( $args as $identifier ) { $user = Two_Factor_Recovery::find_user( (string) $identifier ); if ( ! $user ) { \WP_CLI::warning( sprintf( 'No user found for "%s".', $identifier ) ); continue; } $meta = get_user_meta( $user->ID, Two_Factor_Authentication::OPTION_NAME, true ); $meta = is_array( $meta ) ? $meta : []; if ( empty( $meta ) ) { \WP_CLI::line( sprintf( '%s (ID %d): no two-factor data', $user->user_login, $user->ID ) ); continue; } $secret = Two_Factor_Authentication::get_totp_secret( $meta ); $state = is_wp_error( $secret ) ? 'cannot be read (' . $secret->get_error_code() . ')' : ( null !== \Yipresser\AdminOptimizer\Support\AO_Secrets::name_from_reference( $meta['secret'] ?? '' ) ? 'stored in the secret store' : 'plain' ); \WP_CLI::line( sprintf( '%1$s (ID %2$d): %3$s; secret %4$s; %5$d recovery codes; %6$d trusted devices', $user->user_login, $user->ID, empty( $meta['enabled'] ) ? 'not enrolled' : 'enrolled', $state, is_array( $meta['recovery_codes'] ?? null ) ? count( $meta['recovery_codes'] ) : 0, is_array( get_user_meta( $user->ID, Two_Factor_Authentication::TRUSTED_TOKEN_NAME, true ) ) ? count( get_user_meta( $user->ID, Two_Factor_Authentication::TRUSTED_TOKEN_NAME, true ) ) : 0 ) ); } } /** * Reset one or more users' two-factor authentication. * * Removes the secret, recovery codes, trusted devices, and any pending emailed code. The * user signs in with their password and sets 2FA up again; for a compulsory role the * grace period starts over. The account owner is emailed unless --skip-email is given. * * ## OPTIONS * * ... * : User ID, login, or email. * * [--skip-email] * : Do not email the account owner. * * ## EXAMPLES * * $ wp ao 2fa reset alice * Success: Reset two-factor authentication for alice. * * @param string[] $args Positional arguments. * @param array $assoc_args Flags. * * @return void */ public function reset( $args, $assoc_args ) { $recovery = new Two_Factor_Recovery(); $done = 0; foreach ( $args as $identifier ) { $user = Two_Factor_Recovery::find_user( (string) $identifier ); if ( ! $user ) { \WP_CLI::warning( sprintf( 'No user found for "%s".', $identifier ) ); continue; } $had = $recovery->reset_user( (int) $user->ID ); if ( empty( $assoc_args['skip-email'] ) ) { $recovery->notify_reset( $user ); } \WP_CLI::success( sprintf( $had ? 'Reset two-factor authentication for %s.' : '%s had no two-factor data; cleared anyway.', $user->user_login ) ); ++$done; } if ( 0 === $done ) { \WP_CLI::error( 'No users were reset.' ); } } /** * Reset two-factor authentication for every user that has any. * * For a total lockout, for example after the encryption key changed. Requires --yes. * * ## OPTIONS * * [--yes] * : Confirm resetting every user. Required; without it nothing is changed. * * [--skip-email] * : Do not email the account owners. * * ## EXAMPLES * * $ wp ao 2fa reset-all --yes * * @subcommand reset-all * * @param string[] $args Positional arguments. * @param array $assoc_args Flags. * * @return void */ public function reset_all( $args, $assoc_args ) { if ( empty( $assoc_args['yes'] ) ) { \WP_CLI::error( 'This resets two-factor authentication for every user. Run it again with --yes to confirm.' ); } $recovery = new Two_Factor_Recovery(); $count = 0; $after = 0; do { $ids = Two_Factor_Recovery::enrolled_user_ids( $after ); foreach ( $ids as $id ) { $recovery->reset_user( $id ); $user = get_user_by( 'id', $id ); if ( $user && empty( $assoc_args['skip-email'] ) ) { $recovery->notify_reset( $user ); } ++$count; } $after = $ids ? max( $ids ) : $after; } while ( 200 === count( $ids ) ); \WP_CLI::success( sprintf( 'Reset two-factor authentication for %d user(s).', $count ) ); } } // Register `wp ao` once, then this module's subcommand under it. $adminoptim_cli_path = [ 'ao' ]; if ( ! \WP_CLI::get_root_command()->find_subcommand( $adminoptim_cli_path ) ) { \WP_CLI::add_command( 'ao', AO_CLI_Namespace::class ); } \WP_CLI::add_command( 'ao 2fa', Two_Factor_Authentication_CLI::class );