Making WordPress.org

Changeset 14262


Ignore:
Timestamp:
12/11/2024 05:36:31 AM (4 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.