From 67328d217336c80f5fbd9f08cf0f8abb503865a7 Mon Sep 17 00:00:00 2001 From: Joachim Jensen Date: Sun, 18 Feb 2024 23:34:26 -0800 Subject: [PATCH] [RUA-48] Setting - REST API Content Protection (#48) * [RUA-48] settings screen cleanup. new setting type * [RUA-48] setting: rest api content protection * [RUA-48] intercept rest api call * [RUA-48] label --- admin/settings.php | 135 +++++++++++++++++++++++++++++++++++---------- app.php | 76 +++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 30 deletions(-) diff --git a/admin/settings.php b/admin/settings.php index a693420..126df24 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -10,6 +10,7 @@ final class RUA_Settings_Page extends RUA_Admin { + const PREFIX = 'rua_'; /** * Settings slug * @var string @@ -25,6 +26,7 @@ final class RUA_Settings_Page extends RUA_Admin /** * Settings prefix * @var string + * @deprecated */ private $prefix = 'rua-'; @@ -152,6 +154,7 @@ public function render_screen() */ public function add_scripts_styles() { + WPCACore::enqueue_scripts_styles(RUA_App::TYPE_RESTRICT); } public function init_settings() @@ -162,44 +165,68 @@ public function init_settings() 'title' => __('General', 'restrict-user-access'), 'callback' => '', 'fields' => [] + ], + 'security' => [ + 'name' => 'security', + 'title' => __('Security', 'restrict-user-access'), + 'callback' => '', + 'fields' => [] ] ]; + $levels = [ + 0 => __('-- None --') + ]; + foreach (RUA_App::instance()->get_levels() as $id => $level) { + $levels[$level->ID] = $level->post_title; + } + $this->settings['general']['fields'][] = [ - 'name' => 'registration-level', + 'name' => 'rua-registration-level', 'title' => __('New User Default Level', 'restrict-user-access'), - 'callback' => [$this,'dropdown_levels'], + 'callback' => [$this,'dropdown'], 'args' => [ - 'label_for' => $this->prefix . 'registration-level' + 'options' => $levels ] ]; $default_role = get_option('default_role'); $roles = get_editable_roles(); $this->settings['general']['fields'][] = [ - 'name' => 'registration-role', + 'name' => 'rua-registration-role', 'title' => __('New User Default Role'), 'callback' => [$this,'setting_moved'], 'args' => [ - 'option' => !empty($roles[$default_role]) ? $roles[$default_role]['name'] : $default_role, - 'title' => __('General Settings'), - 'url' => 'options-general.php' + 'option' => !empty($roles[$default_role]) ? $roles[$default_role]['name'] : $default_role, + 'wp_title' => __('General Settings'), + 'url' => 'options-general.php' ], 'register' => false ]; $this->settings['general']['fields'][] = [ - 'name' => 'registration', + 'name' => 'rua-registration', 'title' => __('Enable Registration', 'restrict-user-access'), 'callback' => [$this,'setting_moved'], 'args' => [ - 'option' => get_option('users_can_register') ? __('Yes') : __('No'), - 'title' => __('General Settings'), - 'url' => 'options-general.php' + 'option' => get_option('users_can_register') ? __('Yes') : __('No'), + 'wp_title' => __('General Settings'), + 'url' => 'options-general.php' ], 'register' => false ]; + $this->settings['security']['fields'][] = [ + 'name' => self::PREFIX . 'rest_api_access', + 'title' => __('REST API Content Protection', 'restrict-user-access'), + 'callback' => [$this,'checkbox'], + 'args' => [ + 'default_value' => 1, + 'recommended' => __('Enabled'), + 'description' => __('Deny access to content in REST API for users without legitimate a purpose.', 'restrict-user-access') . + ' ' . __('Learn more') . '' + ], + ]; foreach ($this->settings as $section) { add_settings_section( $this->prefix . $section['name'], @@ -208,8 +235,10 @@ public function init_settings() $this->slug ); foreach ($section['fields'] as $field) { + $field['args']['title'] = $field['title']; + $field['args']['label_for'] = $field['name']; add_settings_field( - $this->prefix . $field['name'], + $field['name'], $field['title'], $field['callback'], $this->slug, @@ -217,41 +246,77 @@ public function init_settings() $field['args'] ); if (!isset($field['register']) || $field['register']) { - register_setting($this->option_group, $this->prefix . $field['name']); + register_setting($this->option_group, $field['name']); } } } } /** - * Render levels dropdown - * Skip synchronized levels + * Render checkbox * - * @since 0.17 + * @since 0.10 * @param array $args * @return void */ - public function dropdown_levels($args) + public function checkbox($args) { - echo ''; + echo '
'; + echo ''; + if (isset($args['description'])) { + echo '

' . $args['description'] . '

'; + } + if (isset($args['recommended'])) { + echo '

Recommended: ' . $args['recommended'] . '

'; } - echo ''; } /** - * Render checkbox - * - * @since 0.10 - * @param array $args + * @param $args * @return void */ - public function checkbox($args) + public function radio($args) { - $option = get_option($args['label_for']); - echo ''; + $current_value = $this->get_setting_value($args); + + echo '
'; + echo '' . $args['title'] . ''; + echo '

'; + foreach ($this->get_options($args) as $option_value => $label) { + echo '
'; + } + echo '

'; + if (isset($args['description'])) { + echo '

' . $args['description'] . '

'; + } + if (isset($args['recommended'])) { + echo '

Recommended: ' . $args['recommended'] . '

'; + } + echo '
'; + } + + /** + * @param $args + * @return void + */ + public function dropdown($args) + { + $current_value = $this->get_setting_value($args); + + echo ''; + if (isset($args['recommended'])) { + echo '

Recommended: ' . $args['recommended'] . '

'; + } } /** @@ -266,7 +331,17 @@ public function setting_moved($args) echo $args['option']; echo '

' . sprintf( __('Setting can be changed in %s', 'restrict-user-access'), - '' . $args['title'] . '' + '' . $args['wp_title'] . '' ) . '

'; } + + private function get_setting_value($args) + { + return get_option($args['label_for'], isset($args['default_value']) ? $args['default_value'] : false); + } + + private function get_options($args) + { + return isset($args['options']) ? $args['options'] : []; + } } diff --git a/app.php b/app.php index e55bc9f..d4befc1 100644 --- a/app.php +++ b/app.php @@ -153,6 +153,11 @@ public function __construct() 'cas/user_visibility', [$this,'sidebars_check_levels'] ); + + add_filter( + 'rest_authentication_errors', + [$this, 'rest_api_access'] + ); } public function ensure_wpca_loaded() @@ -664,4 +669,75 @@ public function process_level_automators() } } } + + public function rest_api_access($result) + { + //bail if auth has been handled elsewhere + if ($result === true || is_wp_error($result)) { + return $result; + } + + if (rua_get_user()->has_global_access()) { + return $result; + } + + if (!get_option('rua_rest_api_access', 1)) { + return $result; + } + + //Contributor is the lowest role that should have access, + //since they can see content in admin area + if (current_user_can('edit_posts')) { + return $result; + } + + $restricted = [ + '/wp/v2/search' => true, + '/wp/v2/users' => true + ]; + + $ignored_post_types = [ + 'nav_menu_item' => true, + 'wp_block' => true, + 'wp_template' => true, + 'wp_template_part' => true, + 'wp_navigation' => true + ]; + foreach (get_post_types(['show_in_rest' => true], 'objects') as $post_type) { + if (empty($post_type->rest_base)) { + continue; + } + if (isset($ignored_post_types[$post_type->name])) { + continue; + } + $restricted['/' . $post_type->rest_namespace . '/' . $post_type->rest_base] = true; + } + $ignored_taxonomies = [ + 'menu' => true, + ]; + foreach (get_taxonomies(['show_in_rest' => true], 'objects') as $taxonomy) { + if (empty($taxonomy->rest_base)) { + continue; + } + if (isset($ignored_taxonomies[$post_type->name])) { + continue; + } + $restricted['/' . $taxonomy->rest_namespace . '/' . $taxonomy->rest_base] = true; + } + + global $wp; + + $route = $wp->query_vars['rest_route']; + $route = preg_replace('/(\/\d+)$/', '', $route, 1); + + if (!isset($restricted[$route])) { + return $result; + } + + return new WP_Error( + 'rest_forbidden', + __('Sorry, you are not allowed to do that.'), + ['status' => rest_authorization_required_code()] + ); + } }