-
Notifications
You must be signed in to change notification settings - Fork 284
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
Request write scopes only when needed #1566
Comments
@felixarntz the IB looks good for the most part so far. I'm not sure we need to separate required scopes from extra granted scopes in separate options. It seems like this is only needed to ensure the extra granted scopes are requested again when the user re-authenticates since those will not be required. Why not just merge all granted scopes with the required scopes when calling I think we still need the concept of "additional scopes" i.e. "scopes to request" but given the above, I think we can limit their footprint to parameters rather than add additional user options. How about something like this? <?php
/**
* $this = OAuth_Client
* @param array $scopes Extra scopes to request.
* @param string $redirect_url Redirect URL after authentication.
*/
public function request_scopes( array $scopes, $redirect_url = '' ) {
if ( $this->need_reauthenticate( $scopes ) ) { // Update to accept optional $scopes.
return $this->get_authentication_url( $redirect_url, $scopes ); // Update to accept optional $scopes.
}
return false; // Scopes already granted.
} Aside: we should move I feel like one of the more complicated bits will be determining if one of the current granted scopes satisfies the requirement for read access. Also, not all modules have corresponding For this reason, I think we'll need a different strategy for determining if granted scopes are sufficient or not. Maybe I'll start working on some of the more straightforward details while we work on refining the more complicated details here. |
@felixarntz not sure what you mean by this:
Why would we not change the required scopes only for Tag Manager (possibly issue I mentioned above?). This isn't mentioned in the AC One of the benefits here is that we can request less scopes as it seems we were already requesting a few that we didn't need. Regarding extra scopes needed per-datapoint in the datapoint definitions, we might also want to allow for request method-specific definitions as some datapoints are available with both methods, but only require the extra scopes for Maybe the definition should have a structure like this: <?php
[ $datapoint => [
'service' => $service,
'GET' => [ "scopes" => $scopes ],
'POST' => [ "scopes" => $scopes ],
] |
@aaemnnosttv I've updated the current IB based on our conversation earlier. I've also checked all the Google API endpoints and included the correct list of scopes to cover in the IB (for Analytics you need two different scopes to cover the whole spectrum of |
I've added a clientside IB… apologies if it's too vague but I wanted to walk through the broad strokes here and not get too in-the-weeds about some of the "how" to move this along. Based on our earlier discussion, I think this should match up with what was proposed. Let me know if it sounds good, or if I'm missing anything 🙂 I'm marking this one as a 30 estimate because there's a lot of moving parts and code in this one. |
I messed up the IB because the settings requests are not (necessarily) where we make requests that demand write permissions. Gonna update it in a bit and then send it back to IB Review. Thanks for catching that @aaemnnosttv 🙂 |
Tweaked the IB—should be set for another review now. |
Can you clarify what you mean by "settings" store? The functionality where we need to listen to
I think doing this unconditionally (or any unconditional persistence FWIW) is out of scope here. We need to make sure we persist all currently relevant state (most importantly the current module store's one) only right before the user is redirected to the OAuth flow as a result of clicking the modal button that shows due to a
I would think this should be singular
See above, this is not particular to settings, but to module stores overall. To take an example, when you create a Google Analytics account via provisioning API, no settings are involved/affected. I think this part here of how we can check for the error would be good to define a bit more. We could put it in the
This is not entirely correct, the URL to point to will be the "connect URL" from
I'm not sure I agree. The user presented a clear intent when they previously initiated an action, and they even request scopes to do it. Having them require to click again afterwards is I feel one click too much. When you do something in an app and it asks you for some permissions, are you expecting it to then proceed or are you expecting it to ask you to do it again? However, since this is an additional piece though and could also be added afterwards, I'm okay leaving it out for now and focus on the other functionality first. Once we have that we can better assess what this feels like.
That sounds good to me, let's do that. Let's just make sure not to update the legacy code more than we need to for this (this issue is already quite big and we are on a tight schedule since this needs to go into 1.9.0). |
Awesome, IB ✅ Let's get to it! |
@cole10up Current state here, with all associated PRs merged, this is now ready to be QAd. Expected experience is:
Another general thing to test is that you shouldn't see the general warning that you need to grant additional scopes at any time (the one that e.g. would show on top of the dashboard). For that it will be especially good to test updating from e.g. 1.8.1 to the new version and ensure this does not happen. |
@cole10up Did you also specifically test the steps in #1566 (comment)? |
Feature Description
Site Kit currently requests scopes on a per-module basis. Whenever you activate a module, you have to grant access to all scopes that using this module may need. The "may" is an important word here: For example, you will only need Analytics write permissions if you're creating a new account, property, or profile. However, currently all users need to grant these permissions even if they'll never use this functionality. Using incremental scopes is a best practice with OAuth, and while we're doing a decent job at it handling it per module, we should improve that specifically for scopes that allow write access, so that these are really only requested when they're needed.
This is especially time-critical because of the Analytics provisioning workstream which introduced a new write scope (for the first time since original plugin release). With the current logic in place, every existing user with Analytics active would see a prompt to grant that scope, which would be very much out of context, and they may not want to use this feature at all.
As part of this issue, we will mainly focus on Analytics, since the implementation relies on the write-related client logic being refactored already (using datastores). We can also add the change to AdSense, although that is trivial here because we don't expose any write logic (the API only has a single route that writes data anyway). This issue will only partially address the issue for Tag Manager though, since we'll need to complete #1386 first, but it must not be a blocker for this one.
Two modules that should not be changed are Search Console and Site Verification. Site Verification doesn't differentiate between read and write scopes anyway, and for Search Console we can continue to always request write permissions, since they are already needed during setup. We can reconsider that separately in the future.
Do not alter or remove anything below. The following sections will be managed by moderators only.
Acceptance criteria
get_scopes
methods of the following modules should be modified to only include non-write scopes:https://www.googleapis.com/auth/adsense
should be replaced withhttps://www.googleapis.com/auth/adsense.readonly
.https://www.googleapis.com/auth/analytics.readonly
should be removed.Authentication::needs_reauthenticate()
method in PHP needs to be updated so that lower permission required scopes we request don't result intrue
if the user already has granted access to a higher permission scope (e.g. if we requestadsense.readonly
but the user already grantedadsense
). This is so that users from older versions are not unexpectedly and unnecessarily asked to re-authenticate.get_datapoint_services()
method, while not breaking backward-compatibility.Module
base class should implement a mechanism to check for such datapoints that the user has these scopes. If not, amissing_required_scopes
error should be returned, and the array of required scopes should be included in the error data.createModuleStore
, related: Add getAdminScreenURL and getAdminReauthURL selectors to base module store #1559) that handles incoming errors.missing_required_scopes
one, the client should open a modal dialog informing the user they need to grant additional permissions (exact wording TBD, let's just put something basic for now and review later).getReAuthURL
utility function), with the redirect URL set to the same URL they're currently on (pretty much like today).access_denied
error) for such a case of additional scopes being requested (non-default module write scopes), they should not end up on the dashboard as usual, but on the original screen they came from, like in the success case.Implementation Brief
Scopes
utility class with a map of scopes mapped to a list of other scopes that satisfy them (user needs to have all of these scopes), and introduce a static methodhas_scopes( $required_scopes, $granted_scopes )
that returnstrue|false
. If a required scope is in the scope map, it should be considered covered if$granted_scopes
includes all of these scopes or the scope itself; otherwise the scope itself needs to be actually included in$granted_scopes
. The scopes covered should be:adsense.readonly
-->adsense
analytics.readonly
-->analytics.edit
andanalytics
analytics.manage.users.readonly
-->analytics.manage.users
tagmanager.readonly
-->tagmanager.edit.containers
webmasters.readonly
-->webmasters
Authentication::need_reauthenticate()
toOAuth_Client
as a public method, and modify it to only retrieve the option values and then pass them to the aboveScopes::has_scopes
method (the currentarray_intersect
logic will be replaced with logic from there).Module::get_datapoints_definition()
method, which should return a map of$datapoint_method:$datapoint => $datapoint_definition
pairs, with the latter being an associative array. For now, the only thing required in that array should be aservice
key, and it should optionally have ascopes
key and arequest_scopes_message
key. This should eventually replace the existingModule::get_datapoint_services()
, but we should maintain backward-compatibility here. Let's makeModule::get_datapoint_services()
non-abstract and return an empty array by default. The default implementation ofModule::get_datapoints_definition()
should callModule::get_datapoint_services()
and transform the return value so that it has the expected format (values should be associative arrays withservice
slug, instead of just the service slug). The currentget_datapoint_services()
does not differentiate betweenGET
andPOST
on a code-level basis, so it should for this BC cases be assumed to cover both.Module
logic for handling datapoint requests to see if the datapoint has ascopes
entry, and if so, ensure the user has granted all these scopes (viaScopes::has_scopes( $datapoint_scopes, OAuth_Client::get_granted_scopes() )
). If not, return amissing_required_scopes
error, and include the array of required scopes indata.requiredScopes
. If the datapoint also has arequest_scopes_message
(see above), including that in the error asdata.requestScopesMessage
. Use a specific scopes exception that implements a newWP_Errorable
contract, taking care of the additional error data.Module::get_datapoint_services()
withModule::get_datapoints_definition()
in Analytics and Tag Manager module, annotating datapoints that require additional scopes with a list of them and addrequest_scopes_message
annotations on a case-by-case basis (TBD). Even though we will for now keep the regular list of Tag Manager scopes as immediately requested, the datapoints can already be updated to have the annotations (the user will just never run into the "error" here because they will already have granted scopes).get_datapoints_definition()
implementations that replace the currentget_datapoint_services()
implementations. All these other modules do not need to request any additional scopes currently, so they should just return the correct$datapoint_method:$datapoint
entries with the respectiveservice
key.OAuth_Client::get_granted_additional_scopes()
method that looks at a new optiongooglesitekit_additional_auth_scopes
and returns that array.OAuth_Client::get_granted_scopes()
should be modified to include these scopes too (see below why we need this extra separation).?oauth2callback=1
handler, separate granted scopes by whether they are inget_required_scopes()
(the generally required scopes) or not. If not, put them into thegooglesitekit_additional_auth_scopes
option, otherwise into the regulargooglesitekit_auth_scopes
option.OAuth_Client::get_authentication_url()
to support an additional optional parameter$additional_scopes
. The method should callsetScopes
with not onlyget_required_scopes()
, but the combination ofget_required_scopes()
,get_granted_additional_scopes()
, and$additional_scopes
. This is why we need the additional scopes handling. We also need to keep requesting the additional scopes from elsewhere that the user already granted, otherwise they will be "un-granted" again.Authentication::handle_oauth()
should be modified to support anadditional_scopes
query parameter for thegooglesitekit_connect
bit, to be passed on toOAuth_Client::get_authentication_url()
. The client will need to pass the additional scopes needed for the specific API datapoint in this parameter.Client Implementation
setPermissionScopeError()
to thecore/user
data store that would allow missing scopes to be set by any component/datastore when a permissions error is encountered.getPermissionScopeError()
that checks for missing permissions/scopes for this user based on any attempted actions on the page. This would be used in thePermissionsModal
component, prompting users to navigate to an OAuth page that requests the missing scopes for their account.create-account
in Analytics), check for a themissing_required_scopes
error and dispatch a notification todispatch( 'core/user' ).setPermissionScopeError()
. An example would be in https://github.com/google/site-kit-wp/blob/developer/assets/js/modules/analytics/datastore/accounts.js#L171. If the WP REST API returnsmissing_required_scopes
, we would set the error in the data store.PermissionsModal
component that conditionally renders a modal whengetPermissionScopeError()
returns an error. This will show some kind of modal/error message/etc. that prompts the user to request more permissions with text/a link from the WP REST API, based on the data set itgetPermissionScopeError()
. This should be inside a common,Root
component going forward (see: Add RegistryProvider to all React apps #1530), but for now can wrap Analytics along with our<RestoreSnapshots>
component wrapper.Error modal
The permissions error is important enough to be a blocking UX and warrants a modal window. We should add a
PermissionsModal
component that displays:The last prop is required because there's no generic URL to send a user to update their scopes. This would be provided by the PHP function
Authentication::get_connect_url()
with the scopes needed and should be sent by the API in amissing_required_scopes
error.For the non-optional props, the modal error component can have generic, but non-module-specific messages about missing permissions. If the API wants to specify what the permissions are and why they're being requested, they'll be returned in the error set by
dispatch( 'core/user' ).setPermissionScopeError()
.State restoration
takeSnapshot()
action available onData.commonActions
to use that will save a snapshot of the entire data store's state to ourCache
API (site-kit-wp/assets/js/googlesitekit/api/cache.js
Line 17 in b5b4f28
restoreSnapshot( { clearAfterRestore = true } )
). It should check for cached data, set it using aoverwriteState(newState)
action that entirely overwrites state, and then clear the cache so the data is not kept. In the future we'll likely want to extend this action to support the argumentclearAfterRestore: false
, but that can be marked as TODO: and left unsupported for now. If this action is attached to a data store: it supports state restoration.getStoresWithSnapshots( registry = googlesitekit.data )
that iterates through all data stores (usingregistry.namespaces
) and checks each one for an action with the namerestoreSnapshot
. This action should return all data store names that support state restoration with the above action and list them in an array.restoreSnapshotsForStores( registry, ...storeNames )
that waits for all calls torestoreSnapshot()
on every store ingetStoresWithSnapshots()
to complete. This will be used to block rendering of components beneath<Root>
until completed.<RestoreSnapshots>
component that blocks rendering of children component (all datastore-reliant components) until all state snapshots (restoreSnapshotsForStores()
) are restored.Potential issues
For edit views in settings this might be problematic though: we don't currently have a way to open a particular screen on the settings view to my knowledge. So if you were editing a module, needing write scopes, but were on this screen:
we wouldn't have a good way to send you back there right now. The editing state isn't hooked up to our data store so restoring it via state isn't possible (though would be my preferred approach).
isEditing
prop, and then clear it after the first time it's checked.QA Brief
Changelog entry
The text was updated successfully, but these errors were encountered: