| @@ -14,9 +14,9 @@ | ||
| 14 | 14 | |
| 15 | 15 | /** |
| 16 | 16 | * Implements the authorization (roles and permissions) features of the plugin. |
| 17 | 17 | */ |
| 18 | -class Authorization extends Static_Instance { | |
| 18 | +class Authorization extends Singleton { | |
| 19 | 19 | |
| 20 | 20 | /** |
| 21 | 21 | * This function will fail with a wp_die() message to the user if they |
| 22 | 22 | * don't have access. |
| @@ -23,9 +23,10 @@ | ||
| 23 | 23 | * |
| 24 | 24 | * @param WP_User $user User to check. |
| 25 | 25 | * @param array $user_emails Array of user's plaintext emails (in case current user doesn't have a WP account). |
| 26 | 26 | * @param array $user_data Array of keys for email, username, first_name, last_name, |
| 27 | - * authenticated_by, google_attributes, cas_attributes, ldap_attributes. | |
| 27 | + * authenticated_by, google_attributes, cas_attributes, ldap_attributes, | |
| 28 | + * oauth2_attributes. | |
| 28 | 29 | * @return WP_Error|WP_User |
| 29 | 30 | * WP_Error if there was an error on user creation / adding user to blog. |
| 30 | 31 | * WP_Error / wp_die() if user does not have access. |
| 31 | 32 | * WP_User if user has access. |
| @@ -45,8 +46,40 @@ | ||
| 45 | 46 | $auth_settings_access_users_approved_multi |
| 46 | 47 | ) |
| 47 | 48 | ); |
| 48 | 49 | |
| 50 | + // Detect whether this user's first and last name should be updated below | |
| 51 | + // (if the external CAS/LDAP service provides a different value, the option | |
| 52 | + // is set to update it, and it's empty if the option to only set it if empty | |
| 53 | + // is enabled). | |
| 54 | + $should_update_first_name = | |
| 55 | + $user && ! empty( $user_data['first_name'] ) && $user_data['first_name'] !== $user->first_name && | |
| 56 | + ( | |
| 57 | + ( | |
| 58 | + ! empty( $user_data['authenticated_by'] ) && 'cas' === $user_data['authenticated_by'] && | |
| 59 | + ! empty( $auth_settings['cas_attr_update_on_login'] ) && | |
| 60 | + ( '1' === $auth_settings['cas_attr_update_on_login'] || ( 'update-if-empty' === $auth_settings['cas_attr_update_on_login'] && empty( $user->first_name ) ) ) | |
| 61 | + ) || ( | |
| 62 | + ! empty( $user_data['authenticated_by'] ) && 'ldap' === $user_data['authenticated_by'] && | |
| 63 | + ! empty( $auth_settings['ldap_attr_update_on_login'] ) && | |
| 64 | + ( '1' === $auth_settings['ldap_attr_update_on_login'] || ( 'update-if-empty' === $auth_settings['ldap_attr_update_on_login'] && empty( $user->first_name ) ) ) | |
| 65 | + ) | |
| 66 | + ); | |
| 67 | + | |
| 68 | + $should_update_last_name = | |
| 69 | + $user && ! empty( $user_data['last_name'] ) && $user_data['last_name'] !== $user->last_name && | |
| 70 | + ( | |
| 71 | + ( | |
| 72 | + ! empty( $user_data['authenticated_by'] ) && 'cas' === $user_data['authenticated_by'] && | |
| 73 | + ! empty( $auth_settings['cas_attr_update_on_login'] ) && | |
| 74 | + ( '1' === $auth_settings['cas_attr_update_on_login'] || ( 'update-if-empty' === $auth_settings['cas_attr_update_on_login'] && empty( $user->last_name ) ) ) | |
| 75 | + ) || ( | |
| 76 | + ! empty( $user_data['authenticated_by'] ) && 'ldap' === $user_data['authenticated_by'] && | |
| 77 | + ! empty( $auth_settings['ldap_attr_update_on_login'] ) && | |
| 78 | + ( '1' === $auth_settings['ldap_attr_update_on_login'] || ( 'update-if-empty' === $auth_settings['ldap_attr_update_on_login'] && empty( $user->last_name ) ) ) | |
| 79 | + ) | |
| 80 | + ); | |
| 81 | + | |
| 49 | 82 | /** |
| 50 | 83 | * Filter whether to block the currently logging in user based on any of |
| 51 | 84 | * their user attributes. |
| 52 | 85 | * |
| @@ -70,9 +103,9 @@ | ||
| 70 | 103 | array_push( |
| 71 | 104 | $auth_settings_access_users_blocked, |
| 72 | 105 | array( |
| 73 | 106 | 'email' => Helper::lowercase( $user_email ), |
| 74 | - 'date_added' => date( 'M Y' ), | |
| 107 | + 'date_added' => wp_date( 'M Y' ), | |
| 75 | 108 | ) |
| 76 | 109 | ); |
| 77 | 110 | update_option( 'auth_settings_access_users_blocked', $auth_settings_access_users_blocked ); |
| 78 | 111 | } |
| @@ -103,8 +136,22 @@ | ||
| 103 | 136 | return new \WP_Error( 'invalid_login', __( 'Invalid login attempted.', 'authorizer' ) ); |
| 104 | 137 | } |
| 105 | 138 | } |
| 106 | 139 | |
| 140 | + // If this externally-authenticated user is an existing administrator (admin | |
| 141 | + // in single site mode, or super admin in network mode), and isn't blocked, | |
| 142 | + // let them in. Update their first/last name if needed (CAS/LDAP). | |
| 143 | + if ( $user && is_super_admin( $user->ID ) ) { | |
| 144 | + if ( $should_update_first_name ) { | |
| 145 | + update_user_meta( $user->ID, 'first_name', $user_data['first_name'] ); | |
| 146 | + } | |
| 147 | + if ( $should_update_last_name ) { | |
| 148 | + update_user_meta( $user->ID, 'last_name', $user_data['last_name'] ); | |
| 149 | + } | |
| 150 | + | |
| 151 | + return $user; | |
| 152 | + } | |
| 153 | + | |
| 107 | 154 | // Get the default role for this user (or their current role, if they |
| 108 | 155 | // already have an account). |
| 109 | 156 | $default_role = $user && is_array( $user->roles ) && count( $user->roles ) > 0 ? $user->roles[0] : $auth_settings['access_default_role']; |
| 110 | 157 | /** |
| @@ -134,15 +181,8 @@ | ||
| 134 | 181 | reset( $user_emails ); |
| 135 | 182 | foreach ( $user_emails as $user_email ) { |
| 136 | 183 | $is_newly_approved_user = false; |
| 137 | 184 | |
| 138 | - // If this externally authenticated user is an existing administrator | |
| 139 | - // (administrator in single site mode, or super admin in network mode), | |
| 140 | - // and is not in the blocked list, let them in. | |
| 141 | - if ( $user && is_super_admin( $user->ID ) ) { | |
| 142 | - return $user; | |
| 143 | - } | |
| 144 | - | |
| 145 | 185 | // If this externally authenticated user isn't in the approved list |
| 146 | 186 | // and login access is set to "All authenticated users," or if they were |
| 147 | 187 | // automatically approved in the "authorizer_approve_login" filter |
| 148 | 188 | // above, then add them to the approved list (they'll get an account |
| @@ -168,9 +208,9 @@ | ||
| 168 | 208 | // Add this user to the approved list. |
| 169 | 209 | $approved_user = array( |
| 170 | 210 | 'email' => Helper::lowercase( $user_email ), |
| 171 | 211 | 'role' => $approved_role, |
| 172 | - 'date_added' => date( 'Y-m-d H:i:s' ), | |
| 212 | + 'date_added' => wp_date( 'Y-m-d H:i:s' ), | |
| 173 | 213 | ); |
| 174 | 214 | array_push( $auth_settings_access_users_approved, $approved_user ); |
| 175 | 215 | array_push( $auth_settings_access_users_approved_single, $approved_user ); |
| 176 | 216 | update_option( 'auth_settings_access_users_approved', $auth_settings_access_users_approved_single ); |
| @@ -210,9 +250,9 @@ | ||
| 210 | 250 | 'user_pass' => wp_generate_password(), // random password. |
| 211 | 251 | 'first_name' => array_key_exists( 'first_name', $user_data ) ? $user_data['first_name'] : '', |
| 212 | 252 | 'last_name' => array_key_exists( 'last_name', $user_data ) ? $user_data['last_name'] : '', |
| 213 | 253 | 'user_email' => Helper::lowercase( $user_info['email'] ), |
| 214 | - 'user_registered' => date( 'Y-m-d H:i:s' ), | |
| 254 | + 'user_registered' => wp_date( 'Y-m-d H:i:s' ), | |
| 215 | 255 | 'role' => $user_info['role'], |
| 216 | 256 | ) |
| 217 | 257 | ); |
| 218 | 258 | |
| @@ -320,43 +360,22 @@ | ||
| 320 | 360 | } |
| 321 | 361 | } |
| 322 | 362 | } |
| 323 | 363 | } else { |
| 324 | - // Update first/last names of WordPress user from external | |
| 325 | - // service if that option is set. | |
| 326 | - if ( ( array_key_exists( 'authenticated_by', $user_data ) && 'cas' === $user_data['authenticated_by'] && array_key_exists( 'cas_attr_update_on_login', $auth_settings ) && 1 === intval( $auth_settings['cas_attr_update_on_login'] ) ) || ( array_key_exists( 'authenticated_by', $user_data ) && 'ldap' === $user_data['authenticated_by'] && array_key_exists( 'ldap_attr_update_on_login', $auth_settings ) && 1 === intval( $auth_settings['ldap_attr_update_on_login'] ) ) ) { | |
| 327 | - if ( array_key_exists( 'first_name', $user_data ) && 0 < strlen( $user_data['first_name'] ) ) { | |
| 328 | - wp_update_user( | |
| 329 | - array( | |
| 330 | - 'ID' => $user->ID, | |
| 331 | - 'first_name' => $user_data['first_name'], | |
| 332 | - ) | |
| 333 | - ); | |
| 334 | - } | |
| 335 | - if ( array_key_exists( 'last_name', $user_data ) && strlen( $user_data['last_name'] ) > 0 ) { | |
| 336 | - wp_update_user( | |
| 337 | - array( | |
| 338 | - 'ID' => $user->ID, | |
| 339 | - 'last_name' => $user_data['last_name'], | |
| 340 | - ) | |
| 341 | - ); | |
| 342 | - } | |
| 364 | + // Update first/last name from CAS/LDAP if needed. | |
| 365 | + if ( $should_update_first_name ) { | |
| 366 | + update_user_meta( $user->ID, 'first_name', $user_data['first_name'] ); | |
| 343 | 367 | } |
| 368 | + if ( $should_update_last_name ) { | |
| 369 | + update_user_meta( $user->ID, 'last_name', $user_data['last_name'] ); | |
| 370 | + } | |
| 344 | 371 | |
| 345 | 372 | // Update this user's role if it was modified in the |
| 346 | 373 | // authorizer_custom_role filter. |
| 347 | 374 | if ( $default_role !== $approved_role ) { |
| 348 | - // Update user's role in WordPress. | |
| 375 | + // Update user's role in WordPress. Note: User's role will be changed | |
| 376 | + // in the approved list via hook `set_user_role` in WP_User::set_role(). | |
| 349 | 377 | $user->set_role( $approved_role ); |
| 350 | - | |
| 351 | - // Update user's role in this site's approved list and save. | |
| 352 | - foreach ( $auth_settings_access_users_approved_single as $key => $existing_user ) { | |
| 353 | - if ( 0 === strcasecmp( $user->user_email, $existing_user['email'] ) ) { | |
| 354 | - $auth_settings_access_users_approved_single[ $key ]['role'] = $approved_role; | |
| 355 | - break; | |
| 356 | - } | |
| 357 | - } | |
| 358 | - update_option( 'auth_settings_access_users_approved', $auth_settings_access_users_approved_single ); | |
| 359 | 378 | } |
| 360 | 379 | } |
| 361 | 380 | |
| 362 | 381 | // If this is multisite, add new user to current blog. |
| @@ -420,8 +439,15 @@ | ||
| 420 | 439 | } |
| 421 | 440 | } |
| 422 | 441 | } |
| 423 | 442 | |
| 443 | + // Fetch the external service this user authenticated with, and append | |
| 444 | + // it to the logout URL below (so we can fire custom logout routines in | |
| 445 | + // custom_logout() based on their external service. This is necessary | |
| 446 | + // because a pending user does not have a WP_User, and thus no | |
| 447 | + // "authenticated_by" usermeta that is normally used to do this. | |
| 448 | + $external_param = isset( $user_data['authenticated_by'] ) ? '&external=' . $user_data['authenticated_by'] : ''; | |
| 449 | + | |
| 424 | 450 | // Notify user about pending status and return without authenticating them. |
| 425 | 451 | // phpcs:ignore WordPress.Security.NonceVerification |
| 426 | 452 | $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : home_url(); |
| 427 | 453 | $page_title = get_bloginfo( 'name' ) . ' - Access Pending'; |
| @@ -428,9 +454,9 @@ | ||
| 428 | 454 | $error_message = |
| 429 | 455 | apply_filters( 'the_content', $auth_settings['access_pending_redirect_to_message'] ) . |
| 430 | 456 | '<hr />' . |
| 431 | 457 | '<p style="text-align: center;">' . |
| 432 | - '<a class="button" href="' . wp_logout_url( $redirect_to ) . '">' . | |
| 458 | + '<a class="button" href="' . wp_logout_url( $redirect_to ) . $external_param . '">' . | |
| 433 | 459 | __( 'Back', 'authorizer' ) . |
| 434 | 460 | '</a></p>'; |
| 435 | 461 | update_option( 'auth_settings_advanced_login_error', $error_message ); |
| 436 | 462 | wp_die( wp_kses( $error_message, Helper::$allowed_html ), esc_html( $page_title ) ); |
| @@ -447,9 +473,9 @@ | ||
| 447 | 473 | * Restrict access to WordPress site based on settings (everyone, logged_in_users). |
| 448 | 474 | * |
| 449 | 475 | * Action: parse_request |
| 450 | 476 | * |
| 451 | - * @param array $wp WordPress object. | |
| 477 | + * @param WP $wp WordPress object. | |
| 452 | 478 | * @return WP|void WP object when passing through to WordPress authentication, or void. |
| 453 | 479 | */ |
| 454 | 480 | public function restrict_access( $wp ) { |
| 455 | 481 | // Grab plugin settings. |
| @@ -468,15 +494,11 @@ | ||
| 468 | 494 | // Allow access if option is set to 'everyone'. |
| 469 | 495 | ( 'everyone' === $auth_settings['access_who_can_view'] ) || |
| 470 | 496 | // Allow access to approved external users and logged in users if option is set to 'logged_in_users'. |
| 471 | 497 | ( 'logged_in_users' === $auth_settings['access_who_can_view'] && Helper::is_user_logged_in_and_blog_user() && $this->is_email_in_list( $current_user->user_email, 'approved' ) ) || |
| 472 | - // Allow access for requests to /wp-json/oauth1 so oauth clients can authenticate to use the REST API. | |
| 473 | - ( property_exists( $wp, 'matched_query' ) && stripos( $wp->matched_query, 'rest_oauth1=' ) === 0 ) || | |
| 474 | - // Allow access for non-GET requests to /wp-json/*, since REST API authentication already covers them. | |
| 475 | - ( property_exists( $wp, 'matched_query' ) && 0 === stripos( $wp->matched_query, 'rest_route=' ) && isset( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== $_SERVER['REQUEST_METHOD'] ) || | |
| 476 | - // Allow access for GET requests to /wp-json/ (root), since REST API discovery calls rely on this. | |
| 477 | - ( property_exists( $wp, 'matched_query' ) && 'rest_route=/' === $wp->matched_query ) | |
| 478 | - // Note that GET requests to a rest endpoint will be restricted by authorizer. In that case, error messages will be returned as JSON. | |
| 498 | + // Allow REST API requests (access is determined later in the rest_authentication_errors hook). | |
| 499 | + // See: https://github.com/WordPress/WordPress/blob/8e41746cb11271d063608a63e3f6091a8685e677/wp-includes/rest-api.php#L131-L133. | |
| 500 | + ( ! empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) | |
| 479 | 501 | ); |
| 480 | 502 | |
| 481 | 503 | /** |
| 482 | 504 | * Developers can use the `authorizer_has_access` filter to override |
| @@ -505,9 +527,9 @@ | ||
| 505 | 527 | return $wp; |
| 506 | 528 | } |
| 507 | 529 | |
| 508 | 530 | // Allow HEAD requests to the root (usually discovery from a REST client). |
| 509 | - if ( 'HEAD' === $_SERVER['REQUEST_METHOD'] && empty( $wp->request ) && empty( $wp->matched_query ) ) { | |
| 531 | + if ( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'HEAD' === $_SERVER['REQUEST_METHOD'] && empty( $wp->request ) && empty( $wp->matched_query ) ) { | |
| 510 | 532 | return $wp; |
| 511 | 533 | } |
| 512 | 534 | |
| 513 | 535 | /* We've determined that the current user doesn't have access, so we deal with them now. */ |
| @@ -568,9 +590,10 @@ | ||
| 568 | 590 | |
| 569 | 591 | // Check to see if the requested category is public. If so, show it. |
| 570 | 592 | $current_category_name = property_exists( $wp, 'query_vars' ) && array_key_exists( 'category_name', $wp->query_vars ) && strlen( $wp->query_vars['category_name'] ) > 0 ? $wp->query_vars['category_name'] : ''; |
| 571 | 593 | if ( $current_category_name ) { |
| 572 | - $current_category_name = end( explode( '/', $current_category_name ) ); | |
| 594 | + $current_category_name_pieces = explode( '/', $current_category_name ); | |
| 595 | + $current_category_name = end( $current_category_name_pieces ); | |
| 573 | 596 | if ( in_array( 'cat_' . $current_category_name, $auth_settings['access_public_pages'], true ) ) { |
| 574 | 597 | if ( 'no_warning' === $auth_settings['access_public_warning'] ) { |
| 575 | 598 | update_option( 'auth_settings_advanced_public_notice', false ); |
| 576 | 599 | } else { |
| @@ -618,8 +641,47 @@ | ||
| 618 | 641 | } |
| 619 | 642 | |
| 620 | 643 | |
| 621 | 644 | /** |
| 645 | + * Prevent REST API access if user isn't authenticated and "only logged in | |
| 646 | + * users can see the site" is enabled. | |
| 647 | + * | |
| 648 | + * Filter: rest_authentication_errors | |
| 649 | + * | |
| 650 | + * @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication method wasn't used, true if authentication succeeded. | |
| 651 | + * @return WP_Error|null|true WP_Error if not logged in and "only logged in users can see the site" is enabled. | |
| 652 | + */ | |
| 653 | + public function restrict_rest_api( $errors ) { | |
| 654 | + // If there is already an error, just return that. | |
| 655 | + if ( ! empty( $errors ) ) { | |
| 656 | + return $errors; | |
| 657 | + } | |
| 658 | + | |
| 659 | + // If user isn't logged in, check for "only logged in users can see the site". | |
| 660 | + if ( ! is_user_logged_in() ) { | |
| 661 | + // Grab plugin settings. | |
| 662 | + $options = Options::get_instance(); | |
| 663 | + $auth_settings = $options->get_all( Helper::SINGLE_CONTEXT, 'allow override' ); | |
| 664 | + | |
| 665 | + if ( | |
| 666 | + 'logged_in_users' === $auth_settings['access_who_can_view'] && | |
| 667 | + false === apply_filters( 'authorizer_has_access', false, $GLOBALS['wp'] ) | |
| 668 | + ) { | |
| 669 | + return new \WP_Error( | |
| 670 | + 'rest_cannot_view', | |
| 671 | + wp_strip_all_tags( $auth_settings['access_redirect_to_message'] ), | |
| 672 | + array( | |
| 673 | + 'status' => 401, | |
| 674 | + ) | |
| 675 | + ); | |
| 676 | + } | |
| 677 | + } | |
| 678 | + | |
| 679 | + return $errors; | |
| 680 | + } | |
| 681 | + | |
| 682 | + | |
| 683 | + /** | |
| 622 | 684 | * Helper function to determine whether a given email is in one of |
| 623 | 685 | * the lists (pending, approved, blocked). Defaults to the list of |
| 624 | 686 | * approved users. |
| 625 | 687 | * |
| @@ -665,10 +727,10 @@ | ||
| 665 | 727 | default: |
| 666 | 728 | if ( 'single' !== $multisite_mode ) { |
| 667 | 729 | // Get multisite users only. |
| 668 | 730 | $auth_settings_access_users_approved = $options->get( 'access_users_approved', Helper::NETWORK_CONTEXT ); |
| 669 | - } elseif ( is_multisite() && 1 === intval( $options->get( 'advanced_override_multisite' ) ) ) { | |
| 670 | - // This site has overridden any multisite settings, so only get its users. | |
| 731 | + } elseif ( is_multisite() && 1 === intval( $options->get( 'advanced_override_multisite' ) ) && empty( $options->get( 'prevent_override_multisite', Helper::NETWORK_CONTEXT ) ) ) { | |
| 732 | + // This site has overridden any multisite settings (and is not prevented from doing so), so only get its users. | |
| 671 | 733 | $auth_settings_access_users_approved = $options->get( 'access_users_approved', Helper::SINGLE_CONTEXT ); |
| 672 | 734 | } else { |
| 673 | 735 | // Get all site users and all multisite users. |
| 674 | 736 | $auth_settings_access_users_approved = array_merge( |