| // 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/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" |
| |
| 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), /*source=*/"", |
| /*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 |
| |
| struct WebTestPermissionManager::Subscription { |
| PermissionDescription permission; |
| base::RepeatingCallback<void(blink::mojom::PermissionStatus)> callback; |
| blink::mojom::PermissionStatus current_value; |
| }; |
| |
| WebTestPermissionManager::PermissionDescription::PermissionDescription( |
| blink::PermissionType type, |
| const GURL& origin, |
| const GURL& embedding_origin) |
| : type(type), origin(origin), embedding_origin(embedding_origin) {} |
| |
| bool WebTestPermissionManager::PermissionDescription::operator==( |
| const PermissionDescription& other) const { |
| if (type != other.type) { |
| return false; |
| } |
| |
| if (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!=( |
| const PermissionDescription& other) const { |
| return !this->operator==(other); |
| } |
| |
| size_t WebTestPermissionManager::PermissionDescription::Hash::operator()( |
| const PermissionDescription& description) const { |
| const int type_int = static_cast<int>(description.type); |
| |
| if (description.type == 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(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( |
| blink::PermissionType permission, |
| const GURL& requesting_origin, |
| const GURL& embedding_origin) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) || |
| BrowserThread::CurrentlyOn(BrowserThread::IO)); |
| |
| // The same-site auto-grant mechanism for STORAGE_ACCESS_GRANT currently only |
| // works when requesting permissions. |
| // TODO(crbug.com/1471209): maybe it should also work when querying |
| // permissions. |
| if (permission == 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, requesting_origin, embedding_origin); |
| } |
| |
| blink::mojom::PermissionStatus WebTestPermissionManager::GetPermissionStatus( |
| blink::PermissionType permission, |
| const GURL& requesting_origin, |
| const GURL& embedding_origin) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) || |
| BrowserThread::CurrentlyOn(BrowserThread::IO)); |
| |
| base::AutoLock lock(permissions_lock_); |
| |
| auto it = permissions_.find( |
| PermissionDescription(permission, requesting_origin, embedding_origin)); |
| if (it == permissions_.end()) { |
| auto default_state = default_permission_status_.find(permission); |
| 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 == 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) && |
| it->second == blink::mojom::PermissionStatus::DENIED) { |
| return blink::mojom::PermissionStatus::ASK; |
| } |
| |
| return it->second; |
| } |
| |
| PermissionResult |
| WebTestPermissionManager::GetPermissionResultForOriginWithoutContext( |
| blink::PermissionType permission, |
| const url::Origin& requesting_origin, |
| const url::Origin& embedding_origin) { |
| blink::mojom::PermissionStatus status = GetPermissionStatus( |
| permission, requesting_origin.GetURL(), embedding_origin.GetURL()); |
| |
| return PermissionResult(status, content::PermissionStatusSource::UNSPECIFIED); |
| } |
| |
| blink::mojom::PermissionStatus |
| WebTestPermissionManager::GetPermissionStatusForCurrentDocument( |
| blink::PermissionType permission, |
| RenderFrameHost* render_frame_host) { |
| if (render_frame_host->IsNestedWithinFencedFrame()) |
| return blink::mojom::PermissionStatus::DENIED; |
| return GetPermissionStatus( |
| permission, |
| PermissionUtil::GetLastCommittedOriginAsURL(render_frame_host), |
| PermissionUtil::GetLastCommittedOriginAsURL( |
| render_frame_host->GetMainFrame())); |
| } |
| |
| blink::mojom::PermissionStatus |
| WebTestPermissionManager::GetPermissionStatusForWorker( |
| blink::PermissionType permission, |
| RenderProcessHost* render_process_host, |
| const GURL& worker_origin) { |
| return GetPermissionStatus(permission, worker_origin, worker_origin); |
| } |
| |
| blink::mojom::PermissionStatus |
| WebTestPermissionManager::GetPermissionStatusForEmbeddedRequester( |
| blink::PermissionType permission, |
| content::RenderFrameHost* render_frame_host, |
| const url::Origin& overridden_origin) { |
| if (render_frame_host->IsNestedWithinFencedFrame()) { |
| return blink::mojom::PermissionStatus::DENIED; |
| } |
| return GetPermissionStatus(permission, overridden_origin.GetURL(), |
| PermissionUtil::GetLastCommittedOriginAsURL( |
| render_frame_host->GetMainFrame())); |
| } |
| |
| WebTestPermissionManager::SubscriptionId |
| WebTestPermissionManager::SubscribeToPermissionStatusChange( |
| blink::PermissionType permission, |
| RenderProcessHost* render_process_host, |
| RenderFrameHost* render_frame_host, |
| const GURL& requesting_origin, |
| base::RepeatingCallback<void(blink::mojom::PermissionStatus)> callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // If the request is from a worker, it won't have a RFH. |
| GURL embedding_origin = requesting_origin; |
| if (render_frame_host) { |
| embedding_origin = PermissionUtil::GetLastCommittedOriginAsURL( |
| render_frame_host->GetMainFrame()); |
| } |
| |
| auto subscription = std::make_unique<Subscription>(); |
| subscription->permission = |
| PermissionDescription(permission, requesting_origin, embedding_origin); |
| subscription->callback = std::move(callback); |
| subscription->current_value = |
| GetPermissionStatus(permission, subscription->permission.origin, |
| subscription->permission.embedding_origin); |
| |
| auto id = subscription_id_generator_.GenerateNextId(); |
| subscriptions_.AddWithID(std::move(subscription), id); |
| return id; |
| } |
| |
| void WebTestPermissionManager::UnsubscribeFromPermissionStatusChange( |
| SubscriptionId subscription_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (!subscriptions_.Lookup(subscription_id)) |
| return; |
| |
| subscriptions_.Remove(subscription_id); |
| } |
| |
| 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(permission, url.DeprecatedGetOriginAsURL(), |
| embedding_url.DeprecatedGetOriginAsURL()); |
| |
| { |
| base::AutoLock lock(permissions_lock_); |
| |
| auto it = permissions_.find(description); |
| if (it == permissions_.end()) { |
| permissions_.insert( |
| std::pair<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::PermissionDescriptorToPermissionType(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) { |
| std::vector<base::OnceClosure> callbacks; |
| callbacks.reserve(subscriptions_.size()); |
| |
| for (SubscriptionsMap::iterator iter(&subscriptions_); !iter.IsAtEnd(); |
| iter.Advance()) { |
| Subscription* subscription = iter.GetCurrentValue(); |
| if (subscription->permission != permission) |
| continue; |
| |
| if (subscription->current_value == status) |
| continue; |
| |
| subscription->current_value = status; |
| |
| // 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)); |
| } |
| |
| 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 (permission.type) { |
| 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(base::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 |