blob: 019330db1c14a564866a51a14664a1418bff34d6 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/storage_access_api/storage_access_grant_permission_context.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/content_settings/cookie_settings_factory.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/dips/dips_service.h"
#include "chrome/browser/first_party_sets/first_party_sets_policy_service.h"
#include "chrome/browser/first_party_sets/first_party_sets_policy_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/webid/federated_identity_permission_context.h"
#include "chrome/browser/webid/federated_identity_permission_context_factory.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_constraints.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/permissions/constants.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_request_id.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/runtime_feature_state/runtime_feature_state_document_data.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_features.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/cookie_setting_override.h"
#include "net/cookies/site_for_cookies.h"
#include "net/first_party_sets/first_party_set_entry.h"
#include "net/first_party_sets/first_party_set_metadata.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/runtime_feature_state/runtime_feature_state_read_context.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom-shared.h"
#include "third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
namespace {
// This is mutable for testing purposes.
static int implicit_grant_limit = 0;
// How far back to look when requiring top-level user interaction on the
// requesting site for Storage Access API permission grants. If this value is an
// empty duration (e.g. "0s"), then no top-level user interaction is required.
constexpr base::TimeDelta kStorageAccessAPITopLevelUserInteractionBound =
base::Days(30);
// Returns true if the request wasn't answered by the user explicitly. Note that
// this is only called when persisting a permission grant.
bool IsImplicitOutcome(RequestOutcome outcome) {
switch (outcome) {
case RequestOutcome::kGrantedByFirstPartySet:
case RequestOutcome::kGrantedByAllowance:
case RequestOutcome::kDismissedByUser:
case RequestOutcome::kReusedPreviousDecision:
case RequestOutcome::kReusedImplicitGrant:
return true;
case RequestOutcome::kGrantedByUser:
case RequestOutcome::kDeniedByUser:
return false;
case RequestOutcome::kDeniedByPrerequisites:
case RequestOutcome::kDeniedByTopLevelInteractionHeuristic:
case RequestOutcome::kAllowedByCookieSettings:
case RequestOutcome::kDeniedByCookieSettings:
case RequestOutcome::kAllowedBySameSite:
case RequestOutcome::kDeniedAborted:
case RequestOutcome::kAllowedByFedCM:
NOTREACHED_NORETURN();
}
}
// Returns true if the request outcome should be displayed in the omnibox.
bool ShouldDisplayOutcomeInOmnibox(RequestOutcome outcome) {
switch (outcome) {
case RequestOutcome::kGrantedByUser:
case RequestOutcome::kDeniedByUser:
case RequestOutcome::kDismissedByUser:
case RequestOutcome::kReusedPreviousDecision:
return true;
case RequestOutcome::kGrantedByFirstPartySet:
case RequestOutcome::kGrantedByAllowance:
case RequestOutcome::kDeniedByTopLevelInteractionHeuristic:
case RequestOutcome::kReusedImplicitGrant:
return false;
case RequestOutcome::kDeniedByPrerequisites:
case RequestOutcome::kAllowedByCookieSettings:
case RequestOutcome::kDeniedByCookieSettings:
case RequestOutcome::kAllowedBySameSite:
case RequestOutcome::kDeniedAborted:
case RequestOutcome::kAllowedByFedCM:
NOTREACHED_NORETURN();
}
}
// Converts a ContentSetting to the corresponding RequestOutcome. This assumes
// that the request was not answered implicitly; i.e., that a prompt was shown
// to the user (at some point - not necessarily for this request).
RequestOutcome RequestOutcomeFromPrompt(ContentSetting content_setting,
bool persist) {
switch (content_setting) {
case CONTENT_SETTING_DEFAULT:
return RequestOutcome::kDismissedByUser;
case CONTENT_SETTING_ALLOW:
return persist ? RequestOutcome::kGrantedByUser
: RequestOutcome::kReusedPreviousDecision;
case CONTENT_SETTING_BLOCK:
return persist ? RequestOutcome::kDeniedByUser
: RequestOutcome::kReusedPreviousDecision;
default:
NOTREACHED_NORETURN();
}
}
void RecordOutcomeSample(RequestOutcome outcome) {
base::UmaHistogramEnumeration("API.StorageAccess.RequestOutcome", outcome);
}
content_settings::ContentSettingConstraints ComputeConstraints(
RequestOutcome outcome) {
content_settings::ContentSettingConstraints constraints;
switch (outcome) {
case RequestOutcome::kGrantedByFirstPartySet:
constraints.set_lifetime(
permissions::kStorageAccessAPIRelatedWebsiteSetsLifetime);
constraints.set_session_model(
content_settings::mojom::SessionModel::NON_RESTORABLE_USER_SESSION);
return constraints;
case RequestOutcome::kGrantedByAllowance:
constraints.set_lifetime(
permissions::kStorageAccessAPIImplicitPermissionLifetime);
constraints.set_session_model(
content_settings::mojom::SessionModel::USER_SESSION);
return constraints;
case RequestOutcome::kGrantedByUser:
case RequestOutcome::kDeniedByUser:
constraints.set_lifetime(
permissions::kStorageAccessAPIExplicitPermissionLifetime);
constraints.set_session_model(
content_settings::mojom::SessionModel::DURABLE);
return constraints;
case RequestOutcome::kDeniedByPrerequisites:
case RequestOutcome::kDismissedByUser:
case RequestOutcome::kReusedPreviousDecision:
case RequestOutcome::kDeniedByTopLevelInteractionHeuristic:
case RequestOutcome::kAllowedByCookieSettings:
case RequestOutcome::kReusedImplicitGrant:
case RequestOutcome::kDeniedByCookieSettings:
case RequestOutcome::kAllowedBySameSite:
case RequestOutcome::kDeniedAborted:
case RequestOutcome::kAllowedByFedCM:
NOTREACHED_NORETURN();
}
}
bool ShouldPersistSetting(bool permission_allowed,
RequestOutcome outcome,
bool persist) {
// Regardless of how the result was obtained, the permissions code determined
// the result should not be persisted; respect that determination.
if (!persist) {
return false;
}
// Explicit responses to a prompt should be persisted to avoid user annoyance
// or prompt spam.
if (!IsImplicitOutcome(outcome)) {
return true;
}
// Implicit denials are not persisted, since they can be re-derived easily and
// don't have any user-facing concerns, so persistence just adds complexity.
// Grants, however, should be persisted to ensure the associated behavioral
// changes stick.
return permission_allowed;
}
// Returns true if the user/field trials have enabled FedCM/SAA autogrants
// globally via the flag/Feature, or "locally" via the origin trial.
//
// Feature state overrides take precedence over origin trial state.
bool AreFedCmAutograntsEnabled(content::RenderFrameHost* rfh) {
if (std::optional<bool> state = base::FeatureList::GetStateIfOverridden(
blink::features::kFedCmWithStorageAccessAPI);
state.has_value()) {
return state.value();
}
content::RuntimeFeatureStateDocumentData* document_data =
content::RuntimeFeatureStateDocumentData::GetForCurrentDocument(rfh);
CHECK(document_data);
return document_data->runtime_feature_state_read_context()
.IsFedCmWithStorageAccessAPIEnabled();
}
} // namespace
// static
int StorageAccessGrantPermissionContext::GetImplicitGrantLimitForTesting() {
return implicit_grant_limit;
}
// static
void StorageAccessGrantPermissionContext::SetImplicitGrantLimitForTesting(
int limit) {
implicit_grant_limit = limit;
}
StorageAccessGrantPermissionContext::StorageAccessGrantPermissionContext(
content::BrowserContext* browser_context)
: PermissionContextBase(
browser_context,
ContentSettingsType::STORAGE_ACCESS,
blink::mojom::PermissionsPolicyFeature::kStorageAccessAPI) {}
StorageAccessGrantPermissionContext::~StorageAccessGrantPermissionContext() =
default;
void StorageAccessGrantPermissionContext::DecidePermissionForTesting(
permissions::PermissionRequestData request_data,
permissions::BrowserPermissionCallback callback) {
DecidePermission(std::move(request_data), std::move(callback));
}
void StorageAccessGrantPermissionContext::DecidePermission(
permissions::PermissionRequestData request_data,
permissions::BrowserPermissionCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(request_data.requesting_origin.is_valid());
CHECK(request_data.embedding_origin.is_valid());
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(
request_data.id.global_render_frame_host_id());
CHECK(rfh);
if (rfh->GetLastCommittedOrigin().opaque() || rfh->IsCredentialless() ||
rfh->IsNestedWithinFencedFrame() ||
rfh->IsSandboxed(
network::mojom::WebSandboxFlags::kStorageAccessByUserActivation)) {
// No need to log anything here, since well-behaved renderers have already
// done these checks and have logged to the console. This block is to handle
// compromised renderers.
RecordOutcomeSample(RequestOutcome::kDeniedByPrerequisites);
mojo::ReportBadMessage(
"requestStorageAccess: Must not be called by a fenced frame, iframe "
"with an opaque origin, credentialless iframe, or sandboxed iframe");
std::move(callback).Run(CONTENT_SETTING_BLOCK);
return;
}
// Return early without letting SAA override any explicit user settings to
// block 3p cookies.
HostContentSettingsMap* settings_map =
HostContentSettingsMapFactory::GetForProfile(browser_context());
CHECK(settings_map);
ContentSetting setting = settings_map->GetContentSetting(
request_data.requesting_origin, request_data.embedding_origin,
ContentSettingsType::COOKIES);
if (setting == CONTENT_SETTING_BLOCK) {
RecordOutcomeSample(RequestOutcome::kDeniedByCookieSettings);
std::move(callback).Run(CONTENT_SETTING_BLOCK);
return;
}
// Return early without prompting users if cookie access is already allowed.
// This does not take previously granted SAA permission into account.
scoped_refptr<content_settings::CookieSettings> cookie_settings =
CookieSettingsFactory::GetForProfile(
Profile::FromBrowserContext(browser_context()));
net::CookieSettingOverrides overrides = rfh->GetCookieSettingOverrides();
overrides.Remove(net::CookieSettingOverride::kStorageAccessGrantEligible);
if (cookie_settings->IsFullCookieAccessAllowed(
request_data.requesting_origin, net::SiteForCookies(),
url::Origin::Create(request_data.embedding_origin), overrides)) {
RecordOutcomeSample(RequestOutcome::kAllowedByCookieSettings);
std::move(callback).Run(CONTENT_SETTING_ALLOW);
return;
}
net::SchemefulSite requesting_site(request_data.requesting_origin);
net::SchemefulSite embedding_site(request_data.embedding_origin);
// Return early without prompting users if the requesting frame is same-site
// with the top-level frame.
if (requesting_site == embedding_site) {
RecordOutcomeSample(RequestOutcome::kAllowedBySameSite);
std::move(callback).Run(CONTENT_SETTING_ALLOW);
return;
}
{
// Normally a previous prompt rejection would already be filtered before
// reaching `StorageAccessGrantPermissionContext::DecidePermission`, but the
// requirement not to surface the user's denial back to the caller means
// this code is reachable even after permission has been blocked.
// Accordingly, check the default implementation, and if a denial has been
// persisted, respect that decision.
ContentSetting existing_setting =
PermissionContextBase::GetPermissionStatusInternal(
rfh, request_data.requesting_origin, request_data.embedding_origin);
// ALLOW grants are handled by PermissionContextBase so they never reach
// this point.
CHECK_NE(existing_setting, CONTENT_SETTING_ALLOW);
if (existing_setting == CONTENT_SETTING_BLOCK) {
NotifyPermissionSetInternal(
request_data.id, request_data.requesting_origin,
request_data.embedding_origin, std::move(callback),
/*persist=*/false, existing_setting,
RequestOutcome::kReusedPreviousDecision);
return;
}
CHECK_EQ(existing_setting, CONTENT_SETTING_ASK);
}
// FedCM grants (and the appropriate permissions policy) may allow the call to
// auto-resolve (without granting a new permission).
if (AreFedCmAutograntsEnabled(rfh) &&
rfh->IsFeatureEnabled(
blink::mojom::PermissionsPolicyFeature::kIdentityCredentialsGet)) {
FederatedIdentityPermissionContext* fedcm_context =
FederatedIdentityPermissionContextFactory::GetForProfile(
browser_context());
if (fedcm_context && fedcm_context->HasSharingPermission(
/*relying_party_embedder=*/embedding_site,
/*identity_provider=*/requesting_site)) {
RecordOutcomeSample(RequestOutcome::kAllowedByFedCM);
fedcm_context->MarkStorageAccessEligible(
/*relying_party_embedder=*/embedding_site,
/*identity_provider=*/requesting_site,
base::BindOnce(std::move(callback), CONTENT_SETTING_ALLOW));
return;
}
}
if (!request_data.user_gesture) {
rfh->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
"requestStorageAccess: Must be handling a user gesture to use.");
RecordOutcomeSample(RequestOutcome::kDeniedByPrerequisites);
std::move(callback).Run(CONTENT_SETTING_BLOCK);
return;
}
first_party_sets::FirstPartySetsPolicyServiceFactory::GetForBrowserContext(
browser_context())
->ComputeFirstPartySetMetadata(
requesting_site, &embedding_site,
base::BindOnce(&StorageAccessGrantPermissionContext::
CheckForAutoGrantOrAutoDenial,
weak_factory_.GetWeakPtr(), std::move(request_data),
std::move(callback)));
}
void StorageAccessGrantPermissionContext::CheckForAutoGrantOrAutoDenial(
permissions::PermissionRequestData request_data,
permissions::BrowserPermissionCallback callback,
net::FirstPartySetMetadata metadata) {
if (metadata.AreSitesInSameFirstPartySet()) {
switch (metadata.top_frame_entry()->site_type()) {
case net::SiteType::kPrimary:
case net::SiteType::kAssociated:
// Since the sites are in the same First-Party Set, risk of abuse due
// to allowing access is considered to be low.
NotifyPermissionSetInternal(
request_data.id, request_data.requesting_origin,
request_data.embedding_origin, std::move(callback),
/*persist=*/true, CONTENT_SETTING_ALLOW,
RequestOutcome::kGrantedByFirstPartySet);
return;
case net::SiteType::kService:
break;
}
}
// Get all of our implicit grants and see which ones apply to our
// |requesting_origin|.
if (implicit_grant_limit > 0) {
HostContentSettingsMap* settings_map =
HostContentSettingsMapFactory::GetForProfile(browser_context());
CHECK(settings_map);
ContentSettingsForOneType implicit_grants =
settings_map->GetSettingsForOneType(
ContentSettingsType::STORAGE_ACCESS,
content_settings::mojom::SessionModel::USER_SESSION);
const int existing_implicit_grants = base::ranges::count_if(
implicit_grants, [&request_data](const auto& entry) {
return entry.primary_pattern.Matches(request_data.requesting_origin);
});
// If we have fewer grants than our limit, we can just set an implicit grant
// now and skip prompting the user.
if (existing_implicit_grants < implicit_grant_limit) {
NotifyPermissionSetInternal(
request_data.id, request_data.requesting_origin,
request_data.embedding_origin, std::move(callback),
/*persist=*/true, CONTENT_SETTING_ALLOW,
RequestOutcome::kGrantedByAllowance);
return;
}
}
// We haven't found a reason to auto-grant permission, but before we prompt
// there's one more hurdle: the user must have interacted with the requesting
// site in a top-level context recently.
DIPSService* dips_service = DIPSService::Get(browser_context());
if (!dips_service ||
kStorageAccessAPITopLevelUserInteractionBound == base::TimeDelta()) {
// If we don't have access to this kind of historical info or the time bound
// is empty, we waive the requirement, and show the prompt.
PermissionContextBase::DecidePermission(std::move(request_data),
std::move(callback));
return;
}
GURL site(request_data.requesting_origin);
dips_service->DidSiteHaveInteractionSince(
site, base::Time::Now() - kStorageAccessAPITopLevelUserInteractionBound,
base::BindOnce(&StorageAccessGrantPermissionContext::
OnCheckedUserInteractionHeuristic,
weak_factory_.GetWeakPtr(), std::move(request_data),
std::move(callback)));
}
void StorageAccessGrantPermissionContext::OnCheckedUserInteractionHeuristic(
permissions::PermissionRequestData request_data,
permissions::BrowserPermissionCallback callback,
bool had_top_level_user_interaction) {
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(
request_data.id.global_render_frame_host_id());
if (!rfh) {
// After async steps, the RenderFrameHost is not guaranteed to still be
// alive.
RecordOutcomeSample(RequestOutcome::kDeniedAborted);
std::move(callback).Run(CONTENT_SETTING_BLOCK);
return;
}
if (!had_top_level_user_interaction) {
rfh->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
"requestStorageAccess: Request denied because the embedded site has "
"never been interacted with as a top-level context");
NotifyPermissionSetInternal(
request_data.id, request_data.requesting_origin,
request_data.embedding_origin, std::move(callback),
/*persist=*/false, CONTENT_SETTING_BLOCK,
RequestOutcome::kDeniedByTopLevelInteractionHeuristic);
return;
}
// PermissionContextBase::DecidePermission requires that the RenderFrameHost
// is still alive.
CHECK(rfh);
// Show prompt.
PermissionContextBase::DecidePermission(std::move(request_data),
std::move(callback));
}
ContentSetting StorageAccessGrantPermissionContext::GetPermissionStatusInternal(
content::RenderFrameHost* render_frame_host,
const GURL& requesting_origin,
const GURL& embedding_origin) const {
// Permission query from top-level frame should be "granted" by default.
if (render_frame_host && render_frame_host->IsInPrimaryMainFrame()) {
return CONTENT_SETTING_ALLOW;
}
ContentSetting setting = PermissionContextBase::GetPermissionStatusInternal(
render_frame_host, requesting_origin, embedding_origin);
// The spec calls for avoiding exposure of rejections to prevent any attempt
// at retaliating against users who would reject a prompt.
if (setting == CONTENT_SETTING_BLOCK) {
return CONTENT_SETTING_ASK;
}
return setting;
}
void StorageAccessGrantPermissionContext::NotifyPermissionSet(
const permissions::PermissionRequestID& id,
const GURL& requesting_origin,
const GURL& embedding_origin,
permissions::BrowserPermissionCallback callback,
bool persist,
ContentSetting content_setting,
bool is_one_time,
bool is_final_decision) {
CHECK(!is_one_time);
CHECK(is_final_decision);
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
RequestOutcome outcome = RequestOutcomeFromPrompt(content_setting, persist);
if (outcome == RequestOutcome::kReusedPreviousDecision) {
// This could be an implicit, e.g. FPS or allowance based permission. Check
// if the exception has an ephemeral session model.
content_settings::SettingInfo info;
HostContentSettingsMapFactory::GetForProfile(browser_context())
->GetContentSetting(requesting_origin, embedding_origin,
ContentSettingsType::STORAGE_ACCESS, &info);
switch (info.metadata.session_model()) {
case content_settings::mojom::SessionModel::NON_RESTORABLE_USER_SESSION:
case content_settings::mojom::SessionModel::USER_SESSION:
outcome = RequestOutcome::kReusedImplicitGrant;
break;
case content_settings::mojom::SessionModel::DURABLE:
case content_settings::mojom::SessionModel::ONE_TIME:
break;
}
}
NotifyPermissionSetInternal(id, requesting_origin, embedding_origin,
std::move(callback), persist, content_setting,
outcome);
}
void StorageAccessGrantPermissionContext::NotifyPermissionSetInternal(
const permissions::PermissionRequestID& id,
const GURL& requesting_origin,
const GURL& embedding_origin,
permissions::BrowserPermissionCallback callback,
bool persist,
ContentSetting content_setting,
RequestOutcome outcome) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
RecordOutcomeSample(outcome);
const bool permission_allowed = (content_setting == CONTENT_SETTING_ALLOW);
UpdateTabContext(id, requesting_origin, permission_allowed);
if (ShouldDisplayOutcomeInOmnibox(outcome)) {
auto* content_settings =
content_settings::PageSpecificContentSettings::GetForFrame(
id.global_render_frame_host_id());
if (content_settings) {
content_settings->OnTwoSitePermissionChanged(
ContentSettingsType::STORAGE_ACCESS,
net::SchemefulSite(requesting_origin), content_setting);
}
}
if (!ShouldPersistSetting(permission_allowed, outcome, persist)) {
if (content_setting == CONTENT_SETTING_DEFAULT) {
content_setting = CONTENT_SETTING_ASK;
}
std::move(callback).Run(content_setting);
return;
}
// Our failure cases are tracked by the prompt outcomes in the
// `Permissions.Action.StorageAccess` histogram. Because implicitly denied
// results return early, in practice this means that an implicit result at
// this point means a grant was generated.
CHECK(!IsImplicitOutcome(outcome) || permission_allowed);
if (permission_allowed) {
base::UmaHistogramBoolean("API.StorageAccess.GrantIsImplicit",
IsImplicitOutcome(outcome));
}
HostContentSettingsMap* settings_map =
HostContentSettingsMapFactory::GetForProfile(browser_context());
CHECK(settings_map);
CHECK(persist);
settings_map->SetContentSettingDefaultScope(
requesting_origin, embedding_origin, ContentSettingsType::STORAGE_ACCESS,
content_setting, ComputeConstraints(outcome));
ContentSettingsForOneType grants =
settings_map->GetSettingsForOneType(ContentSettingsType::STORAGE_ACCESS);
// TODO(crbug.com/40638427): Ensure that this update of settings doesn't
// cause a double update with
// ProfileNetworkContextService::OnContentSettingChanged.
// We only want to signal the renderer process once the default storage
// partition has updated and ack'd the update. This prevents a race where
// the renderer could initiate a network request based on the response to this
// request before the access grants have updated in the network service.
browser_context()
->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess()
->SetContentSettings(
ContentSettingsType::STORAGE_ACCESS, grants,
base::BindOnce(std::move(callback), content_setting));
}
void StorageAccessGrantPermissionContext::UpdateContentSetting(
const GURL& requesting_origin,
const GURL& embedding_origin,
ContentSetting content_setting,
bool is_one_time) {
CHECK(!is_one_time);
// We need to notify the network service of content setting updates before we
// run our callback. As a result we do our updates when we're notified of a
// permission being set and should not be called here.
NOTREACHED_NORETURN();
}