Making WordPress.org

Changeset 14262


Ignore:
Timestamp:
12/11/2024 05:36:31 AM (21 months ago)
Author:
dd32
Message:

Plugin Directory: Require 2FA verification to confirm a plugin release.

This replaces the email access links.
All plugin committers are required to have 2FA enabled now.

Closes https://github.com/WordPress/wordpress.org/pull/344.
Fixes #7704.

Location:
sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory
Files:
1 deleted
3 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php

    r14218 r14262  
    77use WordPressdotorg\Plugin_Directory\Tools;
    88use WordPressdotorg\Plugin_Directory\Jobs\Plugin_Import;
    9 use WordPressdotorg\Plugin_Directory\Shortcodes\Release_Confirmation as Release_Confirmation_Shortcode;
    109use WordPressdotorg\Plugin_Directory\Email\Release_Confirmation_Enabled as Release_Confirmation_Enabled_Email;
    11 use WordPressdotorg\Plugin_Directory\Email\Release_Confirmation_Access as Release_Confirmation_Access_Email;
     10use Two_Factor_Core;
     11use function WordPressdotorg\Two_Factor\Revalidation\{
     12        get_status as get_revalidation_status,
     13        get_url as get_revalidation_url,
     14};
    1215
    1316/**
     
    8083                                return false;
    8184                        },
    82                 ] );
    83 
    84                 register_rest_route( 'plugins/v1', '/release-confirmation-access', [
    85                         'methods'             => \WP_REST_Server::READABLE,
    86                         'callback'            => [ $this, 'send_access_email' ],
    87                         'args'                => [
    88                         ],
    89                         'permission_callback' => 'is_user_logged_in',
    9085                ] );
    9186
     
    119114                $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
    120115
    121                 return (
    122                         Release_Confirmation_Shortcode::can_access() &&
    123                         current_user_can( 'plugin_manage_releases', $plugin )
    124                 );
     116                if ( ! $plugin || ! current_user_can( 'plugin_manage_releases', $plugin ) ) {
     117                        return false;
     118                }
     119
     120                // Check to see if they've confirmed their 2FA status recently..
     121                $status = get_revalidation_status();
     122                if ( $status && $status['can_save'] ) {
     123                        return true;
     124                }
     125
     126                // Before we say no, check if the user just needs to validate their 2FA.
     127                if ( $status && $status['needs_revalidate'] && 'GET' === $request->get_method() ) {
     128                        $current_rest_url = add_query_arg(
     129                                array(
     130                                        '_wpnonce'         => wp_create_nonce( 'wp_rest' ),
     131                                        '_wp_http_referer' => wp_get_referer(),
     132                                ),
     133                                get_rest_url( null, $request->get_route() )
     134                        );
     135
     136                        wp_safe_redirect( get_revalidation_url( $current_rest_url ) );
     137                        exit;
     138                }
     139
     140                return false;
    125141        }
    126142
     
    300316        }
    301317
    302         /**
    303          * Send a Access email
    304          */
    305         public function send_access_email( $request ) {
    306                 $result = [
    307                         'location' => wp_get_referer() ?: home_url( '/developers/releases/' ),
    308                 ];
    309                 $result['location'] = add_query_arg( 'send_access_email', '1', $result['location'] );
    310                 header( 'Location: ' . $result['location'] );
    311 
    312                 $email = new Release_Confirmation_Access_Email(
    313                         wp_get_current_user()
    314                 );
    315                 $result['sent'] = $email->send();
    316 
    317                 return $result;
    318         }
    319 
    320318        public function validate_plugin_tag_callback( $tag, $request ) {
    321319                $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php

    r14218 r14262  
    999999                        array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
    10001000                        $url
    1001                 );
    1002         }
    1003 
    1004         /**
    1005          * Generates a link to email the release confirmation link.
    1006          *
    1007          * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
    1008          * @return string URL to enable confirmations.
    1009          */
    1010         public static function get_release_confirmation_access_link() {
    1011                 return add_query_arg(
    1012                         array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
    1013                         home_url( 'wp-json/plugins/v1/release-confirmation-access' )
    10141001                );
    10151002        }
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-release-confirmation.php

    r14245 r14262  
    55use WordPressdotorg\Plugin_Directory\Template;
    66use WordPressdotorg\Plugin_Directory\Tools;
     7use Two_Factor_Core;
     8use function WordPressdotorg\Two_Factor\{
     9        Revalidation\get_status as get_revalidation_status,
     10        Revalidation\get_js_url as get_revalidation_js_url,
     11        get_onboarding_account_url as get_2fa_onboarding_url
     12};
    713
    814/**
     
    1420
    1521        const SHORTCODE = 'release-confirmation';
    16         const COOKIE    = 'release_confirmation_access_token';
    17         const META_KEY  = '_release_confirmation_access_token';
    1822        const URL_PARAM = 'access_token';
    1923
     
    5963                ob_start();
    6064
    61                 $should_show_access_notice = false;
    62                 foreach ( $plugins as $plugin ) {
    63                         if ( $plugin->release_confirmation ) {
    64                                 $should_show_access_notice = true;
    65                         }
    66                 }
    67 
    68                 if ( ! self::can_access() && $should_show_access_notice ) {
    69                         if ( isset( $_REQUEST['send_access_email'] ) ) {
    70                                 printf(
    71                                         '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>',
    72                                         __( 'Check your email for an access link to perform actions.', 'wporg-plugins')
    73                                 );
    74                         } else {
    75                                 printf(
    76                                         '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>',
    77                                         sprintf(
    78                                                 /* translators: %s: URL */
    79                                                 __( 'Check your email for an access link, or <a href="%s">request a new email</a> to perform actions.', 'wporg-plugins'),
    80                                                 Template::get_release_confirmation_access_link()
    81                                         )
    82                                 );
    83                         }
     65                // If the user is not using 2FA, show a notice.
     66                if ( ! Two_Factor_Core::is_user_using_two_factor( get_current_user_id() ) ) {
     67                        printf(
     68                                '<div class="plugin-notice notice notice-error notice-alt"><p>%s</p></div>',
     69                                sprintf(
     70                                        __( 'Your account has elevated privileges and requires extra security before you can manage plugin releases. Please <a href="%s">enable two-factor authentication now</a>.', 'wporg-plugins' ),
     71                                        get_2fa_onboarding_url()
     72                                )
     73                        );
    8474                }
    8575
     
    246236                $buttons = [];
    247237
    248                 if ( $data['confirmations_required'] && empty( $data['discarded'] ) ) {
     238                if (
     239                        ! is_user_logged_in() ||
     240                        ! Two_Factor_Core::is_user_using_two_factor( get_current_user_id() ) ||
     241                        ! current_user_can( 'plugin_manage_releases', $plugin  ) ||
     242
     243                        // No need to show actions if the release can't be confirmed, or is already confirmed
     244                        ! $data['confirmations_required'] ||
     245                        $data['confirmed']
     246                ) {
     247                        return '';
     248                }
     249
     250                if ( empty( $data['discarded'] ) ) {
    249251                        $current_user_confirmed = isset( $data['confirmations'][ wp_get_current_user()->user_login ] );
    250252
    251                         if ( ! $current_user_confirmed && ! $data['confirmed'] ) {
    252                                 if (
    253                                         self::can_access() &&
    254                                         current_user_can( 'plugin_manage_releases', $plugin  )
    255                                 ) {
    256                                         $buttons[] = sprintf(
    257                                                 '<a href="%s" class="wp-element-button button approve-release">%s</a>',
    258                                                 Template::get_release_confirmation_link( $data['tag'], $plugin ),
    259                                                 __( 'Confirm', 'wporg-plugins' )
    260                                         );
    261                                         $buttons[] = sprintf(
    262                                                 '<a href="%s" class="wp-element-button button approve-release">%s</a>',
    263                                                 Template::get_release_confirmation_link( $data['tag'], $plugin, 'discard' ),
    264                                                 __( 'Discard', 'wporg-plugins' )
    265                                         );
    266                                 } else {
    267                                         $buttons[] = sprintf(
    268                                                 '<a class="wp-element-button button approve-release disabled">%s</a>',
    269                                                 __( 'Confirm', 'wporg-plugins' )
    270                                         );
    271                                         $buttons[] = sprintf(
    272                                                 '<a class="wp-element-button button approve-release disabled">%s</a>',
    273                                                 __( 'Discard', 'wporg-plugins' )
    274                                         );
    275                                 }
    276 
    277                         } elseif ( $current_user_confirmed ) {
     253                        if ( ! $current_user_confirmed ) {
     254                                $confirm_link = Template::get_release_confirmation_link( $data['tag'], $plugin );
     255                                $discard_link = Template::get_release_confirmation_link( $data['tag'], $plugin, 'discard' );
     256
     257                                $confirm_link = get_revalidation_js_url( $confirm_link );
     258                                $discard_link = get_revalidation_js_url( $discard_link );
     259
    278260                                $buttons[] = sprintf(
    279                                         '<a class="wp-element-button button approve-release disabled">%s</a>',
    280                                         __( 'Confirmed', 'wporg-plugins' )
    281                                 );
     261                                        '<a href="%s" class="wp-element-button button approve-release" data-2fa-required data-2fa-message="%s">%s</a>',
     262                                        $confirm_link,
     263                                        esc_attr(
     264                                                sprintf(
     265                                                        /* translators: 1: Version number, 2: Plugin name. */
     266                                                        __( 'Confirm your Two-Factor Authentication to release version %1$s of %2$s.', 'wporg-plugins' ),
     267                                                        esc_html( $data['version'] ),
     268                                                        $plugin->post_title
     269                                                )
     270                                        ),
     271                                        __( 'Confirm', 'wporg-plugins' )
     272                                );
     273
     274                                $buttons[] = sprintf(
     275                                        '<a href="%s" class="wp-element-button button approve-release" data-2fa-required data-2fa-message="%s">%s</a>',
     276                                        $discard_link,
     277                                        esc_attr(
     278                                                sprintf(
     279                                                        /* translators: 1: Version number, 2: Plugin name. */
     280                                                        __( 'Confirm your Two-Factor Authentication to discard the release %1$s for %2$s.', 'wporg-plugins' ),
     281                                                        esc_html( $data['version'] ),
     282                                                        $plugin->post_title
     283                                                )
     284                                        ),
     285                                        __( 'Discard', 'wporg-plugins' )
     286                                );
     287
    282288                        }
    283289                } elseif (
     
    297303        }
    298304
    299         static function can_access() {
    300                 if ( ! is_user_logged_in() ) {
    301                         return false;
    302                 }
    303 
    304                 // Plugin reviewers can always access the release management functionality, in wp-admin.
    305                 if ( current_user_can( 'plugin_review' ) && ( is_admin() || wp_is_serving_rest_request() ) ) {
    306                         return true;
    307                 }
    308 
    309                 // Must have an access token..
    310                 if ( empty( $_COOKIE[ self::COOKIE ] ) ) {
    311                         return false;
    312                 }
    313 
    314                 // ...and it be valid..
    315                 $token = get_user_meta( get_current_user_id(), self::META_KEY, true );
    316                 if (
    317                         $token &&
    318                         $token['time'] > ( time() - DAY_IN_SECONDS ) &&
    319                         wp_check_password( $_COOKIE[ self::COOKIE ], $token['token'] )
    320                 ) {
    321                         return true;
    322                 }
    323 
    324                 return false;
    325         }
    326 
    327305        static function generate_access_url( $user = null ) {
    328                 if ( ! $user ) {
    329                         $user = wp_get_current_user();
    330                 }
    331                 if ( ! $user || ! $user->exists() ) {
    332                         return false;
    333                 }
    334 
    335                 $time      = time();
    336                 $plaintext = wp_generate_password( 24, false );
    337                 $token     = wp_hash_password( $plaintext );
    338                 update_user_meta( $user->ID, self::META_KEY, compact( 'token', 'time' ) );
    339 
    340                 $url = add_query_arg(
    341                         self::URL_PARAM,
    342                         urlencode( $plaintext ),
    343                         home_url( '/developers/releases/' )
    344                 );
    345 
    346                 return $url;
     306                return home_url( '/developers/releases/' );
    347307        }
    348308
     
    351311                if ( ! $post || ! is_page() || ! has_shortcode( $post->post_content, self::SHORTCODE ) ) {
    352312                        return;
    353                 }
    354 
    355                 // Migrate URL param to cookie.
    356                 if ( isset( $_REQUEST[ self::URL_PARAM ] ) ) {
    357                         setcookie( self::COOKIE, $_REQUEST[ self::URL_PARAM ], time() + DAY_IN_SECONDS, '/plugins/', 'wordpress.org', true, true );
    358                 }
    359 
    360                 // Expire the cookie when needed. This is not for security, only performance / cleanliness.
    361                 if ( isset( $_COOKIE[ self::COOKIE ] ) && ! self::can_access() ) {
    362                         unset( $_COOKIE[ self::COOKIE ] );
    363                         setcookie( self::COOKIE, false, time() - DAY_IN_SECONDS );
    364313                }
    365314
Note: See TracChangeset for help on using the changeset viewer.