diff --git a/README.md b/README.md index 51bc5130..5d6a8300 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ On a network the old screen keeps its row in the network admin, and the *Setting ## Setting up Reprint manually -`wp-admin/admin.php?page=wpcom-migration-manual` (`manage_options`, single-site only). Nothing links to it; support gives out the URL. It holds the export secret form, a *Remove secret* button (which also turns the exporter off), the exporter toggle, the export URL and the WordPress.com blog ID. Deactivating the plugin disconnects and discards the secret and the exporter state. +`wp-admin/admin.php?page=wpcom-migration-manual` (`administrator` role, single-site only). Nothing links to it; support gives out the URL. It holds the export secret form, a *Remove secret* button (which also turns the exporter off), the exporter toggle, the export URL and the WordPress.com blog ID. Deactivating the plugin disconnects and discards the secret and the exporter state. ## How WordPress.com provisions the exporter diff --git a/plugin/reprint/class-manual-page.php b/plugin/reprint/class-manual-page.php index a816394f..0e2ad1d1 100644 --- a/plugin/reprint/class-manual-page.php +++ b/plugin/reprint/class-manual-page.php @@ -249,8 +249,8 @@ private function authorize( $action ) { wp_die( esc_html__( 'The exporter is not supported on networks.', 'wpcom-migration' ) ); } - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( esc_html__( 'You are not allowed to manage the exporter.', 'wpcom-migration' ) ); + if ( ! in_array( 'administrator', wp_get_current_user()->roles, true ) ) { + wp_die( esc_html__( 'Only an administrator can manage the exporter.', 'wpcom-migration' ) ); } check_admin_referer( $action ); @@ -272,7 +272,7 @@ private function redirect_with_notice( $result ) { * once the secret is valid; the blog ID when connected. */ public function render_page() { - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! in_array( 'administrator', wp_get_current_user()->roles, true ) ) { return; } diff --git a/tests/e2e/blueprint-screen.json b/tests/e2e/blueprint-screen.json index 9a81c57b..55eb3e74 100644 --- a/tests/e2e/blueprint-screen.json +++ b/tests/e2e/blueprint-screen.json @@ -30,6 +30,10 @@ "step": "runPHP", "code": "getMessage(); throw $error; }" + }, { "step": "runPHP", "code": " true, + 'manage_options' => true, + ) + ); + $user_id = wp_insert_user( + array( + 'user_login' => 'migration-options-only', + 'user_pass' => wp_generate_password( 24, false ), + 'role' => 'migration-options-only', + ) + ); + if ( is_wp_error( $user_id ) ) { + throw new RuntimeException( 'Could not create the options-only user: ' . $user_id->get_error_message() ); + } + + wp_set_current_user( $user_id ); + if ( ! current_user_can( 'manage_options' ) || in_array( 'administrator', wp_get_current_user()->roles, true ) ) { + throw new RuntimeException( 'The test user must have manage_options without the administrator role.' ); + } + if ( '' !== wpcom_migration_e2e_render_manual( $manual ) ) { + throw new RuntimeException( 'A non-administrator can view the manual export screen.' ); + } + + $die_handler = static function () { + throw new RuntimeException( 'manual access denied' ); + }; + $die_filter = static function () use ( $die_handler ) { + return $die_handler; + }; + $redirect_filter = static function () { + throw new RuntimeException( 'A manual action reached its redirect.' ); + }; + add_filter( 'wp_die_handler', $die_filter ); + add_filter( 'wp_redirect', $redirect_filter ); + try { + $actions = array( + array( Manual_Page::SAVE_SECRET_ACTION, 'handle_save_secret', array( Manual_Page::SECRET_FIELD => 'attacker-secret' ) ), + array( Manual_Page::SAVE_ENABLED_ACTION, 'handle_save_enabled', array( Manual_Page::ENABLED_FIELD => '1' ) ), + array( Manual_Page::DISCARD_SECRET_ACTION, 'handle_discard_secret', array() ), + ); + foreach ( $actions as $action ) { + wpcom_migration_e2e_post( $action[0], $action[2] ); + try { + $manual->{$action[1]}(); + throw new RuntimeException( 'A manual action returned without denying access.' ); + } catch ( RuntimeException $exception ) { + if ( 'manual access denied' !== $exception->getMessage() ) { + throw $exception; + } + } + } + } finally { + remove_filter( 'wp_die_handler', $die_filter ); + remove_filter( 'wp_redirect', $redirect_filter ); + wp_set_current_user( 1 ); + } + + $state = Exporter::get_state(); + if ( 'smoke-secret-0123456789abcdef0123456789abcdef' !== get_option( Exporter::SECRET_OPTION ) || ! $state['secret_valid'] || $state['window_open'] ) { + throw new RuntimeException( 'Non-administrator access changed exporter credentials or state.' ); + } + break; + case 'invalidate-secret': delete_option( Exporter::SECRET_HASH_OPTION ); $state = Exporter::get_state();