Repository navigation
Premium Analytics: support view_stats-level dashboard access #50889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
18d80a2
28645ee
e454232
c721e5e
35ff0c9
4033653
763d071
cffc00c
bf87793
66833cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: minor | ||
| Type: added | ||
|
|
||
| Allow users with the view_stats capability to open the Analytics dashboard, and shop managers to read the store reports. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| <?php | ||
| /** | ||
| * Who may see the Premium Analytics dashboard. | ||
| * | ||
| * Jetpack Stats lets a site grant non-administrators access through the | ||
| * `view_stats` meta capability, and this dashboard replaces that UI, so it has | ||
| * to honour the same grant. `add_menu_page()` takes a single capability string, | ||
| * so the "manage_options OR view_stats" rule lives in a meta capability of our | ||
| * own rather than being spelled out at each call site. | ||
| * | ||
| * A class rather than a function file so every consumer reaches it through the | ||
| * autoloader: gating lives in files loaded on several different paths, and a | ||
| * `require_once` in each of them is a dependency to keep in sync (and an | ||
| * unmeasurable line of coverage) for no benefit. | ||
| * | ||
| * @package automattic/jetpack-premium-analytics | ||
| */ | ||
|
|
||
| namespace Automattic\Jetpack\PremiumAnalytics; | ||
|
|
||
| /** | ||
| * The dashboard's capability rules. | ||
| * | ||
| * @since $$next-version$$ | ||
| */ | ||
| class Capabilities { | ||
|
|
||
| /** | ||
| * Meta capability for reading the dashboard. | ||
| */ | ||
| const VIEW_ANALYTICS = 'jetpack_view_analytics'; | ||
|
|
||
| /** | ||
| * Hooks the dashboard's meta capability mapping. | ||
| * | ||
| * Called from WordPress-aware entry points, never at load time: this class is | ||
| * autoloaded in contexts where WordPress — and add_filter() — isn't there. | ||
| * Idempotent, so overlapping callers are free to call it. | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function register() { | ||
| add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10, 3 ); | ||
| } | ||
|
|
||
| /** | ||
| * Unhooks the mapping registered by register(). | ||
| * | ||
| * Test tear-down needs this to drop the one filter: remove_all_filters( | ||
| * 'map_meta_cap' ) would also take out Stats' own `view_stats` mapping. | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function unregister() { | ||
| remove_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_caps' ), 10 ); | ||
| } | ||
|
|
||
| /** | ||
| * Maps the dashboard capability to the primitives that grant it. | ||
| * | ||
| * `view_stats` alone would track Stats more closely, but it only means | ||
| * anything once the Stats package has hooked its own `map_meta_cap` — which | ||
| * `Analytics::init_wpcom_simple()` never does. Without the `manage_options` | ||
| * arm, an unmapped `view_stats` would take the dashboard away from | ||
| * administrators too, which is worse than the gap this closes. | ||
| * | ||
| * @param string[] $caps Primitive capabilities required of the user. | ||
| * @param string $cap Capability being checked. | ||
| * @param int $user_id User being checked. | ||
| * @return string[] Primitives for the dashboard capability; anything else untouched. | ||
| */ | ||
| public static function map_meta_caps( $caps, $cap, $user_id ) { | ||
| if ( self::VIEW_ANALYTICS !== $cap ) { | ||
| return $caps; | ||
| } | ||
|
|
||
| if ( user_can( $user_id, 'manage_options' ) || user_can( $user_id, 'view_stats' ) ) { | ||
| return array( 'read' ); | ||
| } | ||
|
|
||
| return array( 'do_not_allow' ); | ||
| } | ||
|
|
||
| /** | ||
| * Whether the current user may read the dashboard. | ||
| * | ||
| * @return bool | ||
| */ | ||
| public static function current_user_can_view_analytics() { | ||
| return current_user_can( self::VIEW_ANALYTICS ); | ||
| } | ||
|
|
||
| /** | ||
| * Whether the current user may read the store reports. | ||
| * | ||
| * "Store reports" is everything the proxy serves from its `analytics` prefix — | ||
| * WooCommerce's own reporting data. Mirrors the capability | ||
| * {@see \Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller} enforces | ||
| * there (Capabilities_Test pins the two together); surfaces backed by the prefix | ||
| * are hidden from readers who fail it, since all they could collect is 403s. | ||
| * | ||
| * @return bool | ||
| */ | ||
| public static function current_user_can_view_store_reports() { | ||
| // The proxy accepts manage_options for every prefix. | ||
| return current_user_can( 'manage_options' ) || current_user_can( 'view_woocommerce_reports' ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,9 +94,7 @@ function register_dashboard_default_layout_route() { | |
| array( | ||
| 'methods' => \WP_REST_Server::READABLE, | ||
| 'callback' => __NAMESPACE__ . '\\get_dashboard_default_layout_response', | ||
| 'permission_callback' => static function () { | ||
| return current_user_can( 'manage_options' ); | ||
| }, | ||
| 'permission_callback' => array( Capabilities::class, 'current_user_can_view_analytics' ), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This route's permission check was widened from A Impact is limited — only default widget-type identifiers leak (strings already public in the plugin source/JS bundles), not real store data, since the proxy call for actual data stays correctly gated on Suggest gating on
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 66833cd |
||
| 'args' => array( | ||
| 'name' => array( | ||
| 'description' => __( 'Dashboard identifier as produced by the build pipeline.', 'jetpack-premium-analytics-pkg' ), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love it how we are centralizing the capabilities check 👍