blob: f63f50f0adcbf387518c4edefa6f7f1f5957f75e [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/web_test/browser/web_test_permission_manager.h"
#include <functional>
#include <list>
#include <memory>
#include <utility>
#include "base/barrier_callback.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/hash/hash.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "content/browser/permissions/permission_util.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/web_test/browser/web_test_content_browser_client.h"
#include "net/base/schemeful_site.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom-forward.h"
namespace content {
namespace {
std::vector<ContentSettingPatternSource> GetContentSettings(
const ContentSettingsPattern& permission_pattern,
const ContentSettingsPattern& embedding_pattern,
blink::mojom::PermissionStatus status) {
std::optional<ContentSetting> setting;
switch (status) {
case blink::mojom::PermissionStatus::GRANTED:
setting = ContentSetting::CONTENT_SETTING_ALLOW;
break;
case blink::mojom::PermissionStatus::DENIED:
setting = ContentSetting::CONTENT_SETTING_BLOCK;
break;
case blink::mojom::PermissionStatus::ASK:
break;
}
std::vector<ContentSettingPatternSource> patterns;
if (setting) {
patterns.emplace_back(permission_pattern, embedding_pattern,
base::Value(*setting),
content_settings::ProviderType::kNone,
/*incognito=*/false);
}
return patterns;
}
bool ShouldHideDeniedState(blink::PermissionType permission_type) {
return permission_type == blink::PermissionType::STORAGE_ACCESS_GRANT ||
permission_type == blink::PermissionType::TOP_LEVEL_STORAGE_ACCESS;
}
} // namespace
WebTestPermissionManager::PermissionDescription::PermissionDescription() =
default;
WebTestPermissionManager::PermissionDescription::PermissionDescription(
blink::mojom::PermissionDescriptorPtr permission_descriptor,
const GURL& origin,
const GURL& embedding_origin)
: permission_descriptor(std::move(permission_descriptor)),
origin(origin),
embedding_origin(embedding_origin) {}
WebTestPermissionManager::PermissionDescription::PermissionDescription(
const PermissionDescription& other) {
permission_descriptor = other.permission_descriptor->Clone();
origin = other.origin;
embedding_origin = other.embedding_origin;
}
WebTestPermissionManager::PermissionDescription::~PermissionDescription() =
default;
bool WebTestPermissionManager::PermissionDescription::operator==(
const PermissionDescription& other) const {
blink::PermissionType permission_type =
blink::PermissionDescriptorToPermissionType(permission_descriptor);
if (permission_type != blink::PermissionDescriptorToPermissionType(
other.permission_descriptor)) {
return false;
}
if (permission_type == blink::PermissionType::STORAGE_ACCESS_GRANT) {
const net::SchemefulSite requesting_site(origin);
const net::SchemefulSite other_requesting_site(other.origin);
const net::SchemefulSite embedding_site(embedding_origin);
const net::SchemefulSite other_embedding_site(other.embedding_origin);
return requesting_site == other_requesting_site &&
embedding_site == other_embedding_site;
}
return origin == other.origin && embedding_origin == other.embedding_origin;
}
bool WebTestPermissionManager::PermissionDescription::operator==(
PermissionStatusSubscription* other) const {
// TODO(crbug.com/408965890): Add support for multi-state permissions. The
// following won't work for detecting changes in permission options.
if (blink::PermissionDescriptorToPermissionType(permission_descriptor) !=
other->permission) {
return false;
}
if (blink::PermissionDescriptorToPermissionType(permission_descriptor) ==
blink::PermissionType::STORAGE_ACCESS_GRANT) {
const net::SchemefulSite requesting_site(origin);
const net::SchemefulSite other_requesting_site(
other->requesting_origin_delegation);
const net::SchemefulSite embedding_site(embedding_origin);
const net::SchemefulSite other_embedding_site(other->embedding_origin);
return requesting_site == other_requesting_site &&
embedding_site == other_embedding_site;
}
return origin == other->requesting_origin_delegation &&
embedding_origin == other->embedding_origin;
}
size_t WebTestPermissionManager::PermissionDescription::Hash::operator()(
const PermissionDescription& description) const {
const int type_int =
static_cast<int>(blink::PermissionDescriptorToPermissionType(
description.permission_descriptor));
if (blink::PermissionDescriptorToPermissionType(
description.permission_descriptor) ==
blink::PermissionType::STORAGE_ACCESS_GRANT) {
const net::SchemefulSite requesting_site(description.origin);
const net::SchemefulSite embedding_site(description.embedding_origin);
const size_t hash =
base::HashInts(type_int, base::FastHash(embedding_site.Serialize()));
return base::HashInts(hash, base::FastHash(requesting_site.Serialize()));
}
const size_t hash = base::HashInts(
type_int, base::FastHash(description.embedding_origin.spec()));
return base::HashInts(hash, base::FastHash(description.origin.spec()));
}
WebTestPermissionManager::WebTestPermissionManager(
BrowserContext& browser_context)
: PermissionControllerDelegate(), browser_context_(browser_context) {}
WebTestPermissionManager::~WebTestPermissionManager() = default;
void WebTestPermissionManager::RequestPermissions(
content::RenderFrameHost* render_frame_host,
const content::PermissionRequestDescription& request_description,
base::OnceCallback<void(const std::vector<blink::mojom::PermissionStatus>&)>
callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (render_frame_host->IsNestedWithinFencedFrame()) {
std::move(callback).Run(std::vector<blink::mojom::PermissionStatus>(
request_description.permissions.size(),
blink::mojom::PermissionStatus::DENIED));
return;
}
std::vector<blink::mojom::PermissionStatus> result;
result.reserve(request_description.permissions.size());
const GURL& embedding_origin = PermissionUtil::GetLastCommittedOriginAsURL(
render_frame_host->GetMainFrame());
for (const auto& permission : request_description.permissions) {
result.push_back(GetPermissionStatusForRequestPermission(
permission, request_description.requesting_origin, embedding_origin));
}
std::move(callback).Run(result);
}
void WebTestPermissionManager::ResetPermission(blink::PermissionType permission,
const GURL& requesting_origin,
const GURL& embedding_origin) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::AutoLock lock(permissions_lock_);
const auto key = PermissionDescription(
PermissionDescriptorUtil::CreatePermissionDescriptorForPermissionType(
permission),
requesting_origin, embedding_origin);
if (!base::Contains(permissions_, key)) {
return;
}
permissions_.erase(key);
}
void WebTestPermissionManager::RequestPermissionsFromCurrentDocument(
content::RenderFrameHost* render_frame_host,
const content::PermissionRequestDescription& request_description,
base::OnceCallback<void(const std::vector<blink::mojom::PermissionStatus>&)>
callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (render_frame_host->IsNestedWithinFencedFrame()) {
std::move(callback).Run(std::vector<blink::mojom::PermissionStatus>(
request_description.permissions.size(),
blink::mojom::PermissionStatus::DENIED));
return;
}
std::vector<blink::mojom::PermissionStatus> result;
result.reserve(request_description.permissions.size());
const GURL& embedding_origin = PermissionUtil::GetLastCommittedOriginAsURL(
render_frame_host->GetMainFrame());
for (const auto& permission : request_description.permissions) {
result.push_back(GetPermissionStatusForRequestPermission(
permission, request_description.requesting_origin, embedding_origin));
}
std::move(callback).Run(result);
}
blink::mojom::PermissionStatus
WebTestPermissionManager::GetPermissionStatusForRequestPermission(
const blink::mojom::PermissionDescriptorPtr& permission_descriptor,
const GURL& requesting_origin,
const GURL& embedding_origin) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
BrowserThread::CurrentlyOn(BrowserThread::IO));
const auto permission_type =
blink::PermissionDescriptorToPermissionType(permission_descriptor);
// The same-site auto-grant mechanism for STORAGE_ACCESS_GRANT currently only
// works when requesting permissions.
// TODO(crbug.com/40278136): maybe it should also work when querying
// permissions.
if (permission_type == blink::PermissionType::STORAGE_ACCESS_GRANT &&
(requesting_origin == embedding_origin ||
net::SchemefulSite(requesting_origin) ==
net::SchemefulSite(embedding_origin))) {
return blink::mojom::PermissionStatus::GRANTED;
}
return GetPermissionStatus(permission_descriptor, requesting_origin,
embedding_origin);
}
blink::mojom::PermissionStatus WebTestPermissionManager::GetPermissionStatus(
const blink::mojom::PermissionDescriptorPtr& permission_descriptor,
const GURL& requesting_origin,
const GURL& embedding_origin) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
BrowserThread::CurrentlyOn(BrowserThread::IO));
base::AutoLock lock(permissions_lock_);
const auto permission_type =
blink::PermissionDescriptorToPermissionType(permission_descriptor);
auto it = permissions_.find(PermissionDescription(
permission_descriptor->Clone(), requesting_origin, embedding_origin));
if (it == permissions_.end()) {
auto default_state = default_permission_status_.find(permission_type);
if (default_state != default_permission_status_.end()) {
return default_state->second;
}
return blink::mojom::PermissionStatus::DENIED;
}
// Immitates the behaviour of the NotificationPermissionContext in that
// permission cannot be requested from cross-origin iframes, which the current
// permission status should reflect when it's status is ASK.
if (permission_type == blink::PermissionType::NOTIFICATIONS) {
if (requesting_origin != embedding_origin &&
it->second == blink::mojom::PermissionStatus::ASK) {
return blink::mojom::PermissionStatus::DENIED;
}
}
// Some permissions (currently storage access related) do not expose the
// denied state to avoid exposing potentially private user choices to
// developers.
if (ShouldHideDeniedState(permission_type) &&
it->second == blink::mojom::PermissionStatus::DENIED) {
return blink::mojom::PermissionStatus::ASK;
}
return it->second;
}
PermissionResult
WebTestPermissionManager::GetPermissionResultForOriginWithoutContext(
const blink::mojom::PermissionDescriptorPtr& permission_descriptor,
const url::Origin& requesting_origin,
const url::Origin& embedding_origin) {
blink::mojom::PermissionStatus status =
GetPermissionStatus(permission_descriptor, requesting_origin.GetURL(),
embedding_origin.GetURL());
return PermissionResult(status, content::PermissionStatusSource::UNSPECIFIED);
}
blink::mojom::PermissionStatus
WebTestPermissionManager::GetPermissionStatusForCurrentDocument(
const blink::mojom::PermissionDescriptorPtr& permission_descriptor,
RenderFrameHost* render_frame_host,
bool should_include_device_status) {
if (render_frame_host->IsNestedWithinFencedFrame())
return blink::mojom::PermissionStatus::DENIED;
return GetPermissionStatus(
permission_descriptor,
PermissionUtil::GetLastCommittedOriginAsURL(render_frame_host),
PermissionUtil::GetLastCommittedOriginAsURL(
render_frame_host->GetMainFrame()));
}
blink::mojom::PermissionStatus
WebTestPermissionManager::GetPermissionStatusForWorker(
const blink::mojom::PermissionDescriptorPtr& permission_descriptor,
RenderProcessHost* render_process_host,
const GURL& worker_origin) {
return GetPermissionStatus(permission_descriptor, worker_origin,
worker_origin);
}
blink::mojom::PermissionStatus
WebTestPermissionManager::GetPermissionStatusForEmbeddedRequester(
const blink::mojom::PermissionDescriptorPtr& permission_descriptor,
content::RenderFrameHost* render_frame_host,
const url::Origin& overridden_origin) {
if (render_frame_host->IsNestedWithinFencedFrame()) {
return blink::mojom::PermissionStatus::DENIED;
}
return GetPermissionStatus(permission_descriptor, overridden_origin.GetURL(),
PermissionUtil::GetLastCommittedOriginAsURL(
render_frame_host->GetMainFrame()));
}
void WebTestPermissionManager::OnPermissionStatusChangeSubscriptionAdded(
content::PermissionController::SubscriptionId subscription_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!subscriptions() || subscriptions()->IsEmpty()) {
return;
}
content::PermissionStatusSubscription* subscription =
subscriptions()->Lookup(subscription_id);
if (!subscription) {
return;
}
// If the request is from a worker, it won't have a RFH.
GURL embedding_origin = subscription->requesting_origin;
if (subscription->render_frame_id != -1) {
subscription->embedding_origin = embedding_origin =
PermissionUtil::GetLastCommittedOriginAsURL(
content::RenderFrameHost::FromID(subscription->render_process_id,
subscription->render_frame_id)
->GetMainFrame());
}
subscription->requesting_origin_delegation = subscription->requesting_origin;
// TODO(crbug.com/408965890): Add support for multi-state permissions. The
// following won't work for detecting changes in permission options.
subscription->permission_result = PermissionResult(
GetPermissionStatus(
PermissionDescriptorUtil::CreatePermissionDescriptorForPermissionType(
subscription->permission),
subscription->requesting_origin, subscription->embedding_origin),
content::PermissionStatusSource::UNSPECIFIED);
}
void WebTestPermissionManager::UnsubscribeFromPermissionStatusChange(
content::PermissionController::SubscriptionId subscription_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
void WebTestPermissionManager::SetPermission(
blink::PermissionType permission,
blink::mojom::PermissionStatus status,
const GURL& url,
const GURL& embedding_url,
blink::test::mojom::PermissionAutomation::SetPermissionCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
PermissionDescription description(
PermissionDescriptorUtil::CreatePermissionDescriptorForPermissionType(
permission),
url.DeprecatedGetOriginAsURL(), embedding_url.DeprecatedGetOriginAsURL());
{
base::AutoLock lock(permissions_lock_);
auto it = permissions_.find(description);
if (it == permissions_.end()) {
permissions_.insert(
std::pair<const PermissionDescription,
blink::mojom::PermissionStatus>(description, status));
} else {
it->second = status;
}
}
OnPermissionChanged(description, status, std::move(callback));
}
void WebTestPermissionManager::SetPermission(
blink::mojom::PermissionDescriptorPtr descriptor,
blink::mojom::PermissionStatus status,
const GURL& url,
const GURL& embedding_url,
blink::test::mojom::PermissionAutomation::SetPermissionCallback callback) {
auto type = blink::MaybePermissionDescriptorToPermissionType(descriptor);
if (!type) {
std::move(callback).Run(false);
return;
}
GURL applicable_permission_url = url;
if (PermissionUtil::IsDomainOverride(descriptor)) {
const auto overridden_origin =
PermissionUtil::ExtractDomainOverride(descriptor);
applicable_permission_url = overridden_origin.GetURL();
}
SetPermission(*type, status, applicable_permission_url, embedding_url,
std::move(callback));
}
void WebTestPermissionManager::ResetPermissions() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::AutoLock lock(permissions_lock_);
permissions_.clear();
}
void WebTestPermissionManager::Bind(
mojo::PendingReceiver<blink::test::mojom::PermissionAutomation> receiver) {
receivers_.Add(this, std::move(receiver));
}
void WebTestPermissionManager::OnPermissionChanged(
const PermissionDescription& permission,
blink::mojom::PermissionStatus status,
blink::test::mojom::PermissionAutomation::SetPermissionCallback
permission_callback) {
if (!subscriptions()) {
return;
}
std::vector<base::OnceClosure> callbacks;
callbacks.reserve(subscriptions()->size());
for (content::PermissionController::SubscriptionsMap::iterator iter(
subscriptions());
!iter.IsAtEnd(); iter.Advance()) {
PermissionStatusSubscription* subscription = iter.GetCurrentValue();
if (permission != subscription) {
continue;
}
if (subscription->permission_result &&
subscription->permission_result->status == status) {
continue;
}
subscription->permission_result =
PermissionResult(status, PermissionStatusSource::UNSPECIFIED);
// Add the callback to |callbacks| which will be run after the loop to
// prevent re-entrance issues.
callbacks.push_back(base::BindOnce(subscription->callback, status,
/*ignore_status_override=*/false));
}
for (auto& callback : callbacks)
std::move(callback).Run();
// The network service expects to hear about any new storage-access permission
// grants, so we have to inform it. This is true for "regular" or top-level
// storage access permission changes.
switch (blink::PermissionDescriptorToPermissionType(
permission.permission_descriptor)) {
case blink::PermissionType::STORAGE_ACCESS_GRANT:
browser_context_->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess()
->SetContentSettings(
ContentSettingsType::STORAGE_ACCESS,
GetContentSettings(
ContentSettingsPattern::FromURLToSchemefulSitePattern(
permission.origin),
ContentSettingsPattern::FromURLToSchemefulSitePattern(
permission.embedding_origin),
status),
base::BindOnce(std::move(permission_callback), /*success=*/true));
break;
case blink::PermissionType::TOP_LEVEL_STORAGE_ACCESS: {
// We dual-write `TOP_LEVEL_STORAGE_ACCESS` and `STORAGE_ACCESS_GRANT` due
// to the former granting a superset of the latter. Accordingly, we wait
// until both permissions have been written, including the notification to
// the network service, to run the permission callback. This could happen
// in either order without issue, so a barrier callback is used to ensure
// whichever finishes last then runs the callback. The asynchronicity
// comes in the form of the updates to the network service.
auto barrier_callback = base::BarrierCallback<bool>(
/*num_callbacks=*/3,
base::BindOnce(
[](blink::test::mojom::PermissionAutomation::SetPermissionCallback
permission_callback,
const std::vector<bool>& successes) {
std::move(permission_callback)
.Run(std::ranges::all_of(successes, std::identity()));
},
std::move(permission_callback)));
SetPermission(blink::PermissionType::STORAGE_ACCESS_GRANT,
blink::mojom::PermissionStatus::GRANTED, permission.origin,
permission.embedding_origin, barrier_callback);
auto* cookie_manager = browser_context_->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess();
cookie_manager->SetContentSettings(
ContentSettingsType::STORAGE_ACCESS,
GetContentSettings(
ContentSettingsPattern::FromURL(permission.origin),
ContentSettingsPattern::FromURL(permission.embedding_origin),
status),
base::BindOnce(barrier_callback, true));
cookie_manager->SetContentSettings(
ContentSettingsType::TOP_LEVEL_STORAGE_ACCESS,
GetContentSettings(
ContentSettingsPattern::FromURL(permission.origin),
ContentSettingsPattern::FromURL(permission.embedding_origin),
status),
base::BindOnce(barrier_callback, true));
break;
}
default:
std::move(permission_callback).Run(true);
break;
}
}
} // namespace content