blob: dfe2c874a904ef117127d9736498bf6ddee97fb1 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/permissions/unused_site_permissions_service.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/run_loop.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/content_settings/core/browser/content_settings_info.h"
#include "components/content_settings/core/browser/content_settings_registry.h"
#include "components/content_settings/core/browser/content_settings_utils.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_pattern.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/content_settings/core/common/features.h"
#include "components/permissions/constants.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"
#include "url/origin.h"
constexpr base::TimeDelta kRevocationThresholdNoDelayForTesting = base::Days(0);
constexpr base::TimeDelta kRevocationThresholdWithDelayForTesting =
base::Minutes(5);
constexpr base::TimeDelta kRevocationCleanUpThresholdWithDelayForTesting =
base::Minutes(30);
namespace permissions {
namespace {
// Called on a background thread.
UnusedSitePermissionsService::UnusedPermissionMap GetUnusedPermissionsMap(
base::Clock* clock,
scoped_refptr<HostContentSettingsMap> hcsm) {
UnusedSitePermissionsService::UnusedPermissionMap recently_unused;
base::Time threshold =
clock->Now() - content_settings::GetCoarseVisitedTimePrecision();
auto* registry = content_settings::ContentSettingsRegistry::GetInstance();
for (const content_settings::ContentSettingsInfo* info : *registry) {
ContentSettingsType type = info->website_settings_info()->type();
if (!content_settings::CanTrackLastVisit(type)) {
continue;
}
ContentSettingsForOneType settings;
hcsm->GetSettingsForOneType(type, &settings);
for (const auto& setting : settings) {
// Skip wildcard patterns that don't belong to a single origin. These
// shouldn't track visit timestamps.
if (!setting.primary_pattern.MatchesSingleOrigin()) {
continue;
}
if (setting.metadata.last_visited != base::Time() &&
setting.metadata.last_visited < threshold) {
GURL url = GURL(setting.primary_pattern.ToString());
// Converting URL to a origin is normally an anti-pattern but here it is
// ok since the URL belongs to a single origin. Therefore, it has a
// fully defined URL+scheme+port which makes converting URL to origin
// successful.
url::Origin origin = url::Origin::Create(url);
recently_unused[origin.Serialize()].push_back(
{type, std::move(setting)});
}
}
}
return recently_unused;
}
base::TimeDelta GetRevocationThreshold() {
// TODO(crbug.com/1401701): Clean up no delay revocation after the feature is
// ready. Today, no delay revocation is necessary to enable manual testing.
if (content_settings::features::kSafetyCheckUnusedSitePermissionsNoDelay
.Get()) {
return kRevocationThresholdNoDelayForTesting;
} else if (content_settings::features::
kSafetyCheckUnusedSitePermissionsWithDelay.Get()) {
return kRevocationThresholdWithDelayForTesting;
}
return content_settings::features::
kSafetyCheckUnusedSitePermissionsRevocationThreshold.Get();
}
base::TimeDelta GetCleanUpThreshold() {
// TODO(crbug.com/1401701): Clean up delayed clean up logic after the feature
// is ready. Today, this is necessary to enable manual testing.
if (content_settings::features::kSafetyCheckUnusedSitePermissionsWithDelay
.Get()) {
return kRevocationCleanUpThresholdWithDelayForTesting;
}
return content_settings::features::
kSafetyCheckUnusedSitePermissionsRevocationCleanUpThreshold.Get();
}
} // namespace
UnusedSitePermissionsService::TabHelper::TabHelper(
content::WebContents* web_contents,
UnusedSitePermissionsService* unused_site_permission_service)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<TabHelper>(*web_contents),
unused_site_permission_service_(
unused_site_permission_service->AsWeakPtr()) {}
UnusedSitePermissionsService::TabHelper::~TabHelper() = default;
void UnusedSitePermissionsService::TabHelper::PrimaryPageChanged(
content::Page& page) {
if (unused_site_permission_service_) {
unused_site_permission_service_->OnPageVisited(
page.GetMainDocument().GetLastCommittedOrigin());
}
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(UnusedSitePermissionsService::TabHelper);
UnusedSitePermissionsService::UnusedSitePermissionsService(
HostContentSettingsMap* hcsm)
: hcsm_(hcsm), clock_(base::DefaultClock::GetInstance()) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
UnusedSitePermissionsService::~UnusedSitePermissionsService() = default;
void UnusedSitePermissionsService::Shutdown() {
update_timer_.Stop();
}
void UnusedSitePermissionsService::StartRepeatedUpdates() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
UpdateUnusedPermissionsAsync(base::NullCallback());
base::TimeDelta repeated_update_interval =
content_settings::features::
kSafetyCheckUnusedSitePermissionsRepeatedUpdateInterval.Get();
update_timer_.Start(
FROM_HERE, repeated_update_interval,
base::BindRepeating(
&UnusedSitePermissionsService::UpdateUnusedPermissionsAsync,
base::Unretained(this), base::NullCallback()));
}
void UnusedSitePermissionsService::RegrantPermissionsForOrigin(
const url::Origin& origin) {
content_settings::SettingInfo info;
base::Value stored_value(hcsm_->GetWebsiteSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, &info));
if (!stored_value.is_dict()) {
return;
}
base::Value::List* permission_type_list =
stored_value.GetDict().FindList(kRevokedKey);
// Check the format of the returned value since it is coming from disk.
if (!permission_type_list) {
NOTREACHED();
return;
}
// Re-grant the permissions that are revoked. This service only auto-revokes
// permissions that were ALLOW, so re-granting will switch those permissions
// back to ALLOW again.
for (auto& permission_type : *permission_type_list) {
// Check if stored permission type is valid.
auto type_int = permission_type.GetIfInt();
if (!type_int.has_value()) {
continue;
}
hcsm_->SetContentSettingCustomScope(
info.primary_pattern, info.secondary_pattern,
static_cast<ContentSettingsType>(type_int.value()),
ContentSetting::CONTENT_SETTING_ALLOW);
}
// Ignore origin from future auto-revocations.
IgnoreOriginForAutoRevocation(origin);
// Remove origin from revoked permissions list.
hcsm_->SetWebsiteSettingCustomScope(
info.primary_pattern, info.secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, {});
}
void UnusedSitePermissionsService::UndoRegrantPermissionsForOrigin(
const std::set<ContentSettingsType> permissions,
const absl::optional<content_settings::ContentSettingConstraints>
constraint,
const url::Origin origin) {
for (const auto& permission : permissions) {
hcsm_->SetContentSettingCustomScope(
ContentSettingsPattern::FromURLNoWildcard(origin.GetURL()),
ContentSettingsPattern::Wildcard(), permission,
ContentSetting::CONTENT_SETTING_DEFAULT);
}
StorePermissionInRevokedPermissionSetting(permissions, constraint, origin);
}
void UnusedSitePermissionsService::ClearRevokedPermissionsList() {
ContentSettingsForOneType settings;
hcsm_->GetSettingsForOneType(
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, &settings);
for (const auto& revoked_permissions : settings) {
hcsm_->SetWebsiteSettingCustomScope(
revoked_permissions.primary_pattern,
revoked_permissions.secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, {});
}
}
void UnusedSitePermissionsService::UpdateUnusedPermissionsAsync(
base::RepeatingClosure callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::BEST_EFFORT},
base::BindOnce(&GetUnusedPermissionsMap, clock_, hcsm_),
base::BindOnce(
&UnusedSitePermissionsService::OnUnusedPermissionsMapRetrieved,
AsWeakPtr(), std::move(callback)));
}
void UnusedSitePermissionsService::IgnoreOriginForAutoRevocation(
const url::Origin& origin) {
auto* registry = content_settings::ContentSettingsRegistry::GetInstance();
for (const content_settings::ContentSettingsInfo* info : *registry) {
ContentSettingsType type = info->website_settings_info()->type();
ContentSettingsForOneType settings;
hcsm_->GetSettingsForOneType(type, &settings);
for (const auto& setting : settings) {
if (setting.metadata.last_visited != base::Time() &&
setting.primary_pattern.MatchesSingleOrigin() &&
setting.primary_pattern.Matches(origin.GetURL())) {
hcsm_->ResetLastVisitedTime(setting.primary_pattern,
setting.secondary_pattern, type);
break;
}
}
}
}
// Called by TabHelper when a URL was visited.
void UnusedSitePermissionsService::OnPageVisited(const url::Origin& origin) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Check if this origin has unused permissions.
auto origin_entry = recently_unused_permissions_.find(origin.Serialize());
if (origin_entry == recently_unused_permissions_.end()) {
return;
}
// See which permissions of the origin actually match the URL and update them.
auto& site_permissions = origin_entry->second;
for (auto it = site_permissions.begin(); it != site_permissions.end();) {
if (it->source.primary_pattern.Matches(origin.GetURL())) {
hcsm_->UpdateLastVisitedTime(it->source.primary_pattern,
it->source.secondary_pattern, it->type);
site_permissions.erase(it++);
} else {
it++;
}
}
// Remove origin entry if all permissions were updated.
if (site_permissions.empty()) {
recently_unused_permissions_.erase(origin_entry);
}
}
void UnusedSitePermissionsService::OnUnusedPermissionsMapRetrieved(
base::OnceClosure callback,
UnusedPermissionMap map) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
recently_unused_permissions_ = map;
RevokeUnusedPermissions();
if (callback) {
std::move(callback).Run();
}
}
void UnusedSitePermissionsService::RevokeUnusedPermissions() {
if (!base::FeatureList::IsEnabled(
content_settings::features::kSafetyCheckUnusedSitePermissions)) {
return;
}
base::Time threshold = clock_->Now() - GetRevocationThreshold();
for (auto itr = recently_unused_permissions_.begin();
itr != recently_unused_permissions_.end();) {
std::list<ContentSettingEntry>& unused_site_permissions = itr->second;
// All |primary_pattern|s are equal across list items, the same is true for
// |secondary_pattern|s. This property is needed later and checked in the
// loop.
ContentSettingsPattern primary_pattern =
unused_site_permissions.front().source.primary_pattern;
ContentSettingsPattern secondary_pattern =
unused_site_permissions.front().source.secondary_pattern;
std::set<ContentSettingsType> revoked_permissions;
for (auto permission_itr = unused_site_permissions.begin();
permission_itr != unused_site_permissions.end();) {
const ContentSettingEntry& entry = *permission_itr;
// Check if the current permission can be auto revoked.
ContentSetting setting =
content_settings::ValueToContentSetting(entry.source.setting_value);
if (!content_settings::CanBeAutoRevoked(entry.type, setting)) {
permission_itr++;
continue;
}
DCHECK_EQ(entry.source.primary_pattern, primary_pattern);
DCHECK_EQ(entry.source.secondary_pattern, secondary_pattern);
// Reset the permission to default if the site is visited before
// threshold. Also, the secondary pattern should be wildcard.
DCHECK(entry.source.metadata.last_visited != base::Time());
DCHECK(entry.type != ContentSettingsType::NOTIFICATIONS);
if (entry.source.metadata.last_visited < threshold &&
entry.source.secondary_pattern ==
ContentSettingsPattern::Wildcard()) {
revoked_permissions.insert(entry.type);
hcsm_->SetContentSettingCustomScope(
entry.source.primary_pattern, entry.source.secondary_pattern,
entry.type, ContentSetting::CONTENT_SETTING_DEFAULT);
unused_site_permissions.erase(permission_itr++);
} else {
permission_itr++;
}
}
// Store revoked permissions on HCSM.
if (!revoked_permissions.empty()) {
StorePermissionInRevokedPermissionSetting(revoked_permissions,
absl::nullopt, primary_pattern,
secondary_pattern);
}
// Handle clean up of recently_unused_permissions_ map after revocation.
if (unused_site_permissions.empty()) {
// Since all unused permissions are revoked, the map should be cleared.
recently_unused_permissions_.erase(itr++);
} else {
// Since there are some permissions that are not revoked, the tracked
// unused permissions should be set to those permissions.
// Note that, currently all permissions belong to a single domain will
// revoked all together, since triggering permission prompt requires a
// page visit. So the timestamp of all granted permissions of the origin
// will be updated. However, this logic will prevent edge cases like
// permission prompt stays open long time, also will provide support for
// revoking permissions separately in the future.
itr++;
}
}
}
void UnusedSitePermissionsService::StorePermissionInRevokedPermissionSetting(
const std::set<ContentSettingsType> permissions,
const absl::optional<content_settings::ContentSettingConstraints>
constraint,
const url::Origin origin) {
// The |secondary_pattern| for
// |ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS| is always wildcard.
StorePermissionInRevokedPermissionSetting(
permissions, constraint,
ContentSettingsPattern::FromURLNoWildcard(origin.GetURL()),
ContentSettingsPattern::Wildcard());
}
void UnusedSitePermissionsService::StorePermissionInRevokedPermissionSetting(
const std::set<ContentSettingsType> permissions,
const absl::optional<content_settings::ContentSettingConstraints>
constraint,
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern) {
GURL url = GURL(primary_pattern.ToString());
// The url should be valid as it is checked that the pattern represents a
// single origin.
DCHECK(url.is_valid());
// Get the current value of the setting to append the recently revoked
// permissions.
base::Value cur_value(hcsm_->GetWebsiteSetting(
url, url, ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, nullptr));
base::Value::Dict dict = cur_value.is_dict() ? std::move(cur_value.GetDict())
: base::Value::Dict();
base::Value::List permission_type_list =
dict.FindList(permissions::kRevokedKey)
? std::move(*dict.FindList(permissions::kRevokedKey))
: base::Value::List();
for (const auto& permission : permissions) {
permission_type_list.Append(static_cast<int32_t>(permission));
}
dict.Set(kRevokedKey, base::Value::List(std::move(permission_type_list)));
const content_settings::ContentSettingConstraints default_constraint = {
.expiration = clock_->Now() + GetCleanUpThreshold()};
// Set website setting for the list of recently revoked permissions and
// previously revoked permissions, if exists.
hcsm_->SetWebsiteSettingCustomScope(
primary_pattern, secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS,
base::Value(std::move(dict)),
constraint.has_value() ? constraint.value() : default_constraint);
}
void UnusedSitePermissionsService::UpdateUnusedPermissionsForTesting() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::RunLoop loop;
UpdateUnusedPermissionsAsync(loop.QuitClosure());
loop.Run();
}
std::vector<UnusedSitePermissionsService::ContentSettingEntry>
UnusedSitePermissionsService::GetTrackedUnusedPermissionsForTesting() {
std::vector<ContentSettingEntry> result;
for (const auto& list : recently_unused_permissions_) {
for (const auto& entry : list.second) {
result.push_back(entry);
}
}
return result;
}
void UnusedSitePermissionsService::SetClockForTesting(base::Clock* clock) {
clock_ = clock;
}
} // namespace permissions