privacycg / storage-access

The Storage Access API
https://privacycg.github.io/storage-access/
199 stars 27 forks source link

Means to observe availability of storage access #55

Open jeremyroman opened 3 years ago

jeremyroman commented 3 years ago

The storage access API provides for querying the storage access state (hasStorageAccess) and requesting changes (requestStorageAccess), but not observing changes. Authors could poll hasStorageAccess, but this is inefficient and unergonomic.

A frame might have multiple libraries or widgets which need to be updated when the user approves storage access. Currently, the solutions available to the author are:

  1. manually identify all libraries/widgets which need to be notified and all widgets which can trigger a request, and route notifications between them
  2. replace requestStorageAccess with a wrapper
  3. poll hasStorageAccess (wasting CPU cycles)

The first of these gets even harder if the flag set changes due to a change outside the document. For instance, a storage access request originating from a same-origin frame elsewhere on the page causes a change with no notification in the current CG draft (since these share a partitioned storage key and thus flag set).

The simplest solution would seem to be:

partial interface Document {
    readonly attribute Promise<void> storageAccessAvailable;
};

which resolves whenever hasStorageAccess would start resolving to true (i.e., has storage access flag for the flag set corresponding to the document is set and the was expressly denied storage flag is not set).

Sample usage:

let loggedInWidget = document.getElementById('logged-in');
loggedInWidget.textContent = 'Unknown';
document.storageAccessAvailable.then(() => {
    // Runs immediately if storage access is already available.
    // Otherwise runs when it is granted, if ever.
    loggedInWidget.textContent = localStorage.getItem('user') ?? 'Logged out';
});
jeremyroman commented 3 years ago

@johnwilander @annevk Would you mind taking a look at this idea, or advising me on how to properly ask for feedback? I'm not very clear on the etiquette for contributing here.

johnwilander commented 3 years ago

Hi! Sorry for the delay. This was talked about in the context of per-frame vs per-page storage access. We eventually agreed to per-page storage access which introduces the problem you're trying to solve.

Looking at real world use cases though, the user has just interacted with iframeA from thirdParty.example, the frame called the Storage Access API, and the frame was granted access. What immediately needs to happen in sibling frames? The user is not interacting with them. Are you thinking of speculatively fetching resources in case the user interacts with iframeB from thirdParty.example?

jeremyroman commented 3 years ago

A few cases:

  1. Like you describe: iframes iframeA and iframeB both from thirdparty.example, say a social embed which allows the user to like a piece of embedded content. The user interacts with iframeA, and approves a storage access prompt. When the user then scrolls and looks at iframeB, it still does not reflect whether the user has already liked the associated content (or any other personal state), even though the user has already approved access. But when the user does anything that causes iframeB to check, it finds that it already has access. thirdparty.example can fix this behavior by polling hasStorageAccess periodically, but this wakes up the CPU frequently and wastes resources.

  2. A single iframe from thirdparty.example uses multiple libraries with different authors. When the user interacts with any component in the iframe that requires storage access and it is granted, the other components have storage access but don't know it, so they appear to the user not to be responding to the prompt approval (which, from their perspective, applies to the page as a whole) correctly. Fixing this either requires that the author to tightly couple and coordinate this across these various libraries (i.e., building this exact API themselves, and trying to make sure every call site to requestStorageAccess hooks into it), or the library authors can each poll hasStorageAccess.

I will note that I'm particularly motivated by another use case that we're considering using Storage Access API for, which is uncredentialed prerendering (in which a prerendered page would be denied storage access in much the same way as a third-party frame might, and storage access would be granted only if the user chose to navigate to the prerendered page). In this case, the grant of storage access is also a result of an action outside the affected frame, and so it's necessary for the page to be able to observe it, preferably without polling.

jakearchibald commented 3 years ago

2. Fixing this either requires that the author to tightly couple and coordinate this across these various libraries (i.e., building this exact API themselves, and trying to make sure every call site to requestStorageAccess hooks into it), or the library authors can each poll hasStorageAccess.

+1. Say we had three components:

On page load, the login box knows it doesn't have storage access, so it puts itself into an indeterminate state. It wants to update this if storage becomes available, but it doesn't want to request storage access.

The visual preferences component may want to change page appearance once storage access is gained.

The button is the thing that calls requestStorageAccess when clicked. If storage access is gained by some other means, this button should go away, as it's no longer needed.

In this example, if we don't have something like storageAccessAvailable, this app will have to reinvent it using something like BroadcastChannel, and components which aren't built by the same team may end up using a different system which can get out of sync. The only reliable system is polling.

There's a similar issues with the storage event on localstorage. If storage changes, the event is fired on other window instances. This means that components on the same page won't hear about the change unless a custom pub/sub is created, or components resort to hacks like each creating an iframe to get another window object for the event. It'd be good to avoid the need for more hacks 😄.

annevk commented 3 years ago

As I mentioned in #62 this seems reasonable to Mozilla. Did you also consider giving a signal before the switch happens to allow for any migration?

jeremyroman commented 3 years ago

I admit to not being completely current on the state of pre-storage-access (i.e., document.hasStorageAccess() resolves to false) behavior of storage APIs (e.g. this doesn't seem to correspond with the current notion).

I think a migration signal might be interesting (though I think this issue makes sense independently from it). If I had to sketch one out, I would imagine it wouldn't affect the shape of this idea, because you could do something like:

document.addEventListener('beforestorageaccess', async e => {
  let readyToMigrate = (async function() {
    // open a database, fetch some rows, etc
    // exactly what must be pulled out depends on whether
    // database connections, transactions, etc. can persist
    // across the change
  })();
  e.waitUntil(readyToMigrate);
  let dataToMigrate = await readyToMigrate;
  await document.storageAccessAvailable;
  // write into the newly available database
});

The semantics of such a beforestorageaccess event would be entwined with the semantics of the storage partitioning (which I think are still evolving -- and raise questions like which frame is responsible for migrating data, if they share a partition before), but I think in all likely scenarios fit naturally with document.storageAccessAvailable.

annevk commented 3 years ago

Yeah, no disagreement there. (FWIW, MDN matches what we currently ship on release, Firefox Nightly has where we want things to go. Which is approximately that third parties have partitioned storage by default and can get to non-partitioned storage for all same-origin documents within an agent.)

johannhof commented 2 years ago

This is generally a reasonable idea and it doesn't look problematic from a privacy perspective. We still consider this something to have in the next version of the spec, but not for immediate graduation. It would be interesting to see if there are real-world problems that we could solve with such a feature right now.

lghall commented 1 year ago

Adding a +1 to this request - For Google Workspace, there are scenarios with multiple embeds in the same top-level page (for example, a third-party site that embeds multiple Google Docs, or a Google Doc + a Google Calendar appointment booking page). Having some way for embeds to automatically know when storage access has been granted is key to providing a smooth user experience.

johannhof commented 1 year ago

Note that with https://github.com/privacycg/storage-access/pull/138 we have added formal integration with permissions, and I'll try to verify and test that storage access availability can be observed through the Permissions API. I'll close this issue once that's done.

annevk commented 1 year ago

I'm not sure that addresses all cases from OP. You can observe that you can now call requestStorageAccess() in your document, but you can't observe that someone else has done so. So depending on the use case you might still want some kind of signal that requestStorageAccess() has been successfully invoked for your document.

cfredric commented 1 year ago

I believe you ought to be able to use the permissions API to observe changes which might be caused by other documents:

navigator.permissions.query({ name: 'storage-access' }).then(async (status) => {
 if (status.state === 'granted') {
   await document.requestStorageAccess(); // should auto-resolve due to the existing grant
   doSomethingWithThirdPartyCookies();
 } else {
  status.addEventListenever('change', () => reactToPermissionChange(status));
 }
});
annevk commented 1 year ago

Correct. I'm talking about observing a successful requestStorageAccess() invocation in your document, no other documents involved. That's one of the scenarios OP discusses, where you have multiple libraries interested in the outcome of that call. Of course all these libraries could call requestStorageAccess() themselves, but that might also have inadvertent side effects.

johannhof commented 1 year ago

I don't see why @cfredric's code doesn't work for these libraries. Through the permissions API they can ensure that they won't prompt the user and will only call rSA when the main UI code has done so. What might be missing from the example is a hasStorageAccess call to avoid setting up the permissions part if it's not necessary, but otherwise it seems sufficient to me.

annevk commented 1 year ago

That pattern encourages swapping Storage Access as soon as possible by all code that wants to do something once Storage Access is there. It doesn't allow for code reacting to Storage Access becoming available and leaving a single piece of code in charge of when to actually make the swap.

johannhof commented 1 year ago

Yeah, that's fair, though I'd personally consider that a lower priority until we have evidence that this is an issue. It feels like allowing cross-frame observation of grants is more impactful but I could be wrong. Certainly don't want to block adding that kind of ability.

annevk commented 1 year ago

I still think @jeremyroman's suggestion makes sense and as far as I can tell he specifically made it for the single-document case, only later elaborating on multiple documents.

I agree it's not quite a priority and #154 might also change things around, but I'd like to keep this open for now.