w3c / permissions

Permissions API
https://www.w3.org/TR/permissions/
Other
106 stars 51 forks source link

Add another permission state "always-ask" (from one-time grants)? #414

Closed jan-ivar closed 7 months ago

jan-ivar commented 1 year ago

In browsers that support non-persisted permissions, or maybe doesn't support persisted ones, "granted" isn't always attainable.

In these browsers, the strongest available positive signal is that the user granted access last time. But the permissions API doesn't seem to care about past non-persisted "granted" permissions. Maybe it should?

A "positive" or "granted last time" value might help a site optimize its UX for returning users, or think about the problem.

An example from https://github.com/w3c/mediacapture-main/issues/928:

Most video conferencing sites offer a smoother user experience to returning Chrome users than to returning users in other browsers, because they basically ignore past non-persisted permissions entirely. IOW, that users in Safari or Firefox granted camera last time they used the site, and the time before that, counts for nothing ... Anything short of "granted" persisted permission is ignored, and seems treated as a user retraining problem.

As mentioned in that issue, mediacapture-main might be able to work around this with a MAY return "granted" in place of "prompt" for web compat with persisted-permission browsers, if the site isn't passing in a deviceId. But a cleaner solution would be nicer long-term.

annevk commented 1 year ago

It seems to me that websites can solve this already through a variety of storage APIs so until we change the API (which seems like it would not be backwards compatible) we should first figure out why they do what they do.

jan-ivar commented 1 year ago

We've suggested this to sites in the past, but they've refused storage as a solution, claiming too many edge cases to worry about.

The challenge seems to be to abstract this information in a manner sites can respond to directly, without requiring them to write extra code to accommodate specific user agents, which they're not doing.

I don't think this impacts web compat much, as sites seem to chase "granted" as the happy path:

jan-ivar commented 1 year ago

The premise here is that onetime-permissions represent a level of user buy-in worth persisting and informing apps about:

  1. "denied" — user has blocked permission and there will be no prompt
  2. "prompt" — no indication of user trust and there will be a prompt
  3. "always-ask" — user granted permission last time but there may be a prompt
  4. "granted" — user has granted permission and there will be no prompt

Here's an example using a shim in Chrome Canary 116 which is running a user experiment I appear to be included in: image I click the top choice, "Allow this time", and navigator.permissions.query now says "granted" (I'm examining my choice from the URL bar which is why you don't see my face): image If I ↻ it's still "granted", but once I close the tab and reopen it, I'll be prompted again. This is where my shim kicks in: image Instead of "prompt" the shim returns "always-ask" to reflect that the user's last interaction with the prompt was positive.

This gives websites extra information they can use to decide whether to accept the current level of permission as satisfactory, or continue to train/push the user to escalate permission to every visit in order to avoid the training.

In short, I think this is the right abstraction to capture the current diversity in user agents and UX.

Disclaimer: The shim is imperfect¹, but highlights that there's more state to tackle here than websites want to deal with just to work reliably in all user agents.


1. The shim relies on localStorage, and thus cannot detect user changes when the page is closed. It's also hampered by Chrome not notifying the page for some reason when the user chooses to "Reset permissions" even while the page is up (a bug?). Still, it tries to detect denied permissions as well as permission reset as best it is able.

fippo commented 1 year ago

the chromium experiment can be tested out by --enable-features=OneTimePermission

jan-ivar commented 1 year ago

Chrome Canary 116 which is running a user experiment I appear to be included in

For those curious this appears to be a Chrome field trial that can be enabled with chrome://flags/#one-time-permission.

jan-ivar commented 1 year ago

After some feedback, "always-ask" seems like a less confusing name for this new permission (I've edited above).

tomayac commented 1 year ago

Firefox's recent Intent to Ship is interesting in this context. Reproduced below:

Since Firefox grants one-time permissions by default, the values returned are as follows:

  • "granted" — the user granted persistent or one-time permission last time, and hasn't explicitly revoked it. This positive signal satisfies the spec which says the caller "can use the feature possibly without having the user agent asking the user's permission".
  • "denied" —The user has blocked permission (or the iframe is missing allow= policy)
  • "prompt" — Neither of the above apply (this is the initial state)

This is designed to support one-time permissions as the norm. We appreciate the need to pursue "granted" in other browsers without worrying about differences in Firefox, hence this design.

marcoscaceres commented 1 year ago

@miketaylr and I reviewed this (as Editors), and I'm still not sure why it isn't the website that is remembering that they have shown the UI.

Sure, the API is returning "prompt" in WebKit, but the site should be remembering that they have shown the training UI to the user.

So even though these are "one time permissions" (and they persist in Chrome per browsing session, and it returns "granted"), it's still appropriate for the API to return "prompt" because it will prompt.

marcoscaceres commented 1 year ago

Another way of considering this, is that the permission model for an API could change over time for whatever reason... it would be presumptuous to deliver "always-ask" for any API, when always ask is just "prompt".

Father, the spec for the corresponding API (e.g., geolocation) could mandated "always ask" (because the permission lifetime being bound to a browser session or some timer), but not have that explicitly in reflected through the Permissions API.

marcoscaceres commented 1 year ago

This is not to shut down discussion here in any way. We are really trying to understand the value that this adds!

fjacky commented 1 year ago

Best permission UX practice for developers is to prompt in context, i.e. always provide sufficient rationale (whether implicitly or explicitly). Websites already have at least two ways to determine whether they’ve previously primed a user, or alternatively infer the permission action the user took in the last visit:

  1. They can use cookies or local storage to persist whether a user has previously been successfully primed. This approach is sufficient to streamline the process of, for example, joining a meeting in the context of a returning user who grants ephemerally (e.g. https://github.com/w3c/mediacapture-main/issues/928)
  2. They can use cookies or local storage to persist whether the site has a grant. If the site also employs change listeners, they can correctly deduce whether a user has previously granted ephemerally in the vast majority of cases.

This comment says that approach 1) has previously been proposed to developers here but was rejected due to the number of edge cases. Approach 2) was also mentioned in that discussion. The following summarizes the concerns developers raised:

  1. There is no way to account for, and detect all of the ways in which users can change permissions after granting them without access to this API.

    • This concern only applies to approach 2), since approach 1) does not aim to detect types of grants & permission changes.
    • However, adding the state “always-ask” to the permission query API does not address this concern. For example, resetting a permission should also remove the always-ask state. Thus, this is a case that can’t be handled by either API version.
  2. There are a myriad of edge cases that can cause the localStorage value to not be reliable

    • If multiple tabs to the same site are open, all share the same localStorage. While all localStorage operations are synchronous, if improperly used, it’s easy to imagine scenarios where one open tab to an application ‘pollutes’ values that are read and processed by the same application open in other tabs. This is an implementation concern, and not an inherent lack of reliability of localStorage.
    • Considering the site can use the query API to determine whether a permission is currently in state “granted”, “prompt” or “denied”, approach 1) is sufficient to offer good permission UX, since it provides the user’s previous permission action/state, as well as their current permission state.
  3. The storage solution does not allow the site to distinguish a permanent grant from a one time grant

    • Since the site has the grant now, it can provide the functionality it wants to provide. We don’t want to give developers information about whether the current grant is one-time or a persistent grant since this would give developers the power to reject one-time grants. This would go against the very goal of one-time grants, which is giving the user more control and thereby improving privacy.

In fact, combining a query API response with a simple localStorage tag is already equivalent to adding “always-ask” to the API (the state “prompt” + tag implies “always-ask”). In Firefox’s permission model, the storage solution could even be made superior to the proposed API: by using change listeners and recording grants as well as revocations, the site can distinguish between “granted ephemerally last time” and “granted ephemerally in another tab”, which would both be considered “always-ask” in the proposed API.

For these reasons, we don’t see any benefit in extending the current query API with "always-ask".

jan-ivar commented 1 year ago

Hi, sorry for the delay, I just got back from vacation! Let me try to address each point.

I'm still not sure why it isn't the website that is remembering that they have shown the UI.

The pragmatic answer is: they just aren't.

Sites have had years to solve this, and haven't. They're not going to put in the effort for minority permission models, or their incentives are poor (sites understandably prefer more permission), or both. Whatever the reason, It seems time to declare defeat on the idea that sites will remember one-time permissions on their own, as there are no signs they ever will.

They can use cookies or local storage to persist whether a user has previously been successfully primed.

It's not that simple in practice. Permissions outlive local storage in most browsers. They're user settings separate from "site data" in most browsers. E.g. Safari can share them across devices:

image

Additionally, a subset of privacy sensitive users run with settings like this one in Firefox:

image

...which defeats any localStorage-based solution, making it unattractive to web devs, who understandably prefer permissions as a more reliable signal.

it's still appropriate for the API to return "prompt" because it will prompt.

Let me address this semantic challenge. Years ago I recall an internal all-hands meeting at Mozilla where we concluded it was an unfortunate mistake that "prompt" was the default value in this spec (I believe @annevk was there). We felt it would have been better if something like "default" was the default, leaving the other states "granted", "denied" and "prompt" to always represent actual user choices. Here's Tools / Page Info in Firefox which I think illustrates this well (always ask = prompt):

image

Of course it's clearly too late to undo that mistake now, hence this request for a new "always-ask" value.

I think "always-ask" has a nice ring to it because it represents a user choice. Of course it doesn't mean the user won't ever change their mind later, but that's the setting right now: always ask, until I change this setting.

fjacky commented 1 year ago

The pragmatic answer is: they just aren't.

Since implementation of such functionality is reasonably straightforward, I'm not convinced of this. A more likely reason for sites not using such solutions may be that there is no real need for them. However, how did you arrive at the conclusion that sites don’t employ such approaches?

.. a subset of privacy sensitive users run with settings like this one in Firefox: [Delete cookies and site data when Firefox is closed]

Assuming the always-allow state would be added to the API, you're implying that even though all cookies and site data are cleared, the site can still learn about a previous, ephemeral (!) permission grant that happened before the clearing. This would negatively impact privacy. I would be surprised if privacy-sensitive users would expect this behavior. It would also introduce a new bit of information that can be used for fingerprinting.

Permissions outlive local storage in most browsers.

That's correct. There seem to be two cases

  1. The user clears cookies & site data: In this case, information about previous priming would be lost because localStorage would be cleared. However, as mentioned above, I don’t think sites should have information about past ephemeral grants after the user cleared cookies and site data.

  2. Cross-device synchronization of permissions: this is user-agent specific implementation that is not part of the permission spec. The spec neither says that user settings can or should be synchronized, nor does it say that site data should not be synchronized. For that reason, user agent specific implementations of cross device synchronization shouldn’t be the motivation for this API change. Additionally, in the cross-device scenario, the worst case is that the user would be primed once per device, instead of once in total. The impact this would have on user experience seems negligible. By requiring authentication, the site can also fully circumvent this concern by storing information about grants server-side instead of client-side.

We felt it would have been better if something like "default" was the default, leaving the other states "granted", "denied" and "prompt" to always represent actual user choices.

The permission state “prompt” is defined as:

"The user has not given express permission to use the feature (i.e., it's the same as denied). It also means that if a caller attempts to use the feature, the user agent will either be prompting the user for permission or access to the feature will be denied."

If the user agent should prompt on permission access, it doesn’t matter whether the user has previously granted ephemerally or not, hence “default” and “prompt” would be equivalent from a permission state perspective.

Exposing this permission state through site settings certainly makes sense, however independent of whether this setting option is labeled “Ask”, “Always Ask”, “Always prompt” or similar variations, the underlying permission state can be accurately represented by “prompt”.

In other words, “prompt”, “granted” and “denied” don’t represent user actions, but permission states. There is a clear distinction: a user action can change the permission state, but it doesn’t correspond to a permission state. Incidentally, a grant user action leads to the permission state “granted” and a deny user action leads to the permission state “denied”; that doesn’t turn permission states into user actions. For example, a persistently granted permission has permission state “granted” while there exists no user action. Having the query API return permission state instead of user action seems like a good approach.

Of course it's clearly too late to undo that mistake now, hence this request for a new "always-ask" value.

I disagree with the assumption that a mistake was made, and don’t see how adding "always-ask" is an improvement. “always-ask” introduces ambiguity, since “always-ask” and “prompt” both behave the same. Additionally, never resetting "always-ask” doesn’t seem like a good path forward due to the negative privacy impact.

fjacky commented 1 year ago

There’s a lot here, and it might be worth finding a forum to discuss in more detail. Perhaps @mikewest and/or @dveditz could help us find time to discuss the API in more detail in WebAppSec?

mikewest commented 1 year ago

We're planning our TPAC agenda in today's meeting (https://github.com/w3c/webappsec/blob/main/meetings/2023/2023-08-16-agenda.md). I'm pretty sure we'll be able to make room for the Permissions API generally, and this topic specifically.

jan-ivar commented 1 year ago

I think we're getting side-tracked. Both the local storage argument and the privacy argument can be levied against permissions query() as a whole: none of it is needed (if local storage is sufficient), and all of it is a fingerprinting surface.

A more likely reason for sites not using such solutions may be that there is no real need for them.

See https://github.com/w3c/mediacapture-main/issues/928 for the "slow-lane" problem. But as https://github.com/w3c/permissions/issues/414#issuecomment-1652264633 points out, Firefox plans to return "granted" instead of "always-ask", and therefore doesn't need anything from this group to solve its problem. I was merely trying to answer @marcoscaceres question, but this seems to be turning into a side-track.

... you're implying that even though all cookies and site data are cleared, the site can still learn about a previous, ephemeral (!) permission grant that happened before the clearing. This would negatively impact privacy. I would be surprised if privacy-sensitive users would expect this behavior. It would also introduce a new bit of information that can be used for fingerprinting.

All query values are fingerprinting bits. Please read our Intent to ship for how Firefox intends to mitigate privacy concerns inherent in this spec. We welcome feedback on the intent to ship. But again, that's not what this issue is about.

This issue is about adding an arguably missing value to the recognized permission states as argued in the OP.

The merits of this new value can hopefully be judged by the same standards applied to existing values (which I think disqualify arguments such as: value can be achieved using local-storage, and value is a fingerprinting bit).

The audience for this new value would be apps that object to Firefox's implementation (and maybe future versions of Chrome?), and want to discern "always-ask" as a discrete value from Firefox's "granted".

If this WG feels this new value is not necessary (hopefully using arguments unique to this new value), then so be it. We'll have this issue to direct complaints from app makers to.

miketaylr commented 12 months ago

If we fix https://github.com/w3c/permissions/issues/388, it makes sense (to me, anyways) for UAs that implement ephemeral grants to have the following model:

"user grants" => permission state is "granted" "ephemeral grant (via 'grant this time' UI or UA policy) => "granted" for the lifetime of the document, then "prompt" after "denied" => maybe "denied", maybe "prompt", depending on UA policy or API

The middle case would solve for Firefox's compat issues, and Chrome's "one time grant" experiment, right?

npdoty commented 12 months ago

.query() was always a mistake, especially when it could be used for a script to ensure that the user won't be prompted -- it means that the script can safely access the user's information without the user knowing.

So it seems a strict improvement to me that "granted" might sometimes mean there will be a prompt. And if the spec doesn't allow that (I think it does, but we could make it more explicit), then we should just clarify that it does. Providing even more detail about what the user has seen in the past doesn't seem good: it just introduces fingerprinting surface and would be better as a cookie value.

npdoty commented 12 months ago

Specifications should typically not return results that make a promise or assertion on the user's behalf. I don't think the UA needs to be allowed to "lie", but rather we should just define the response as "willing to have the permission requested".

jan-ivar commented 12 months ago

Thanks for the link to https://github.com/w3c/permissions/issues/388, I've commented there.

The middle case would solve for Firefox's compat issues, and Chrome's "one time grant" experiment, right?

No, the middle case describes status quo. What Firefox intends to ship is:

"ephemeral grant (via 'grant this time' UI or UA policy) => "granted" until the user denies a future prompt or revokes

I.e. one-time permission as the norm. The Firefox permission prompt is mostly harmless: if a user denies a prompt, the site gets "blocked" for the duration of that document only, then "prompt" after that. ↻ → 👍

We therefore don't think apps need a guarantee of a prompt-free experience in Firefox (with the fingerprinting side-effects @npdoty mentions), but if you do, I suggest supporting this issue.

jan-ivar commented 12 months ago

.query() was always a mistake, especially when it could be used for a script to ensure that the user won't be prompted -- it means that the script can safely access the user's information without the user knowing.

I agree. Though with camera and microphone it would be user observable (indicators for "at least 3 seconds"). This might not discourage bolder tracking libraries however, so I find this point convincing.

So it seems a strict improvement to me that "granted" might sometimes mean there will be a prompt. And if the spec doesn't allow that (I think it does, but we could make it more explicit), then we should just clarify that it does.

It says: "The caller will can use [SIC] the feature possibly without having the user agent asking the user's permission."

With Safari, Firefox (and now Chrome experimenting) with one-time permission, it would be great to try to standardize what to return for one-time grants. Happy to discuss that here or close this and open a new issue.

Providing even more detail about what the user has seen in the past doesn't seem good: it just introduces fingerprinting surface and would be better as a cookie value.

Note Firefox plans to clear this always-ask state (whatever value we end up exposing it as) with web storage anyway, as a mitigation. Just wanted to add that for any remaining proponents of a discrete value. But it sounds like we're leaning toward not adding a new value.

jan-ivar commented 7 months ago

Closing due to lack of interest.