blob: 2089898a79a133924d094a003d7fca5cf793c4a1 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h"
#include <memory>
#include <string>
#include <utility>
#include "base/base_paths.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/util/values/values_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/file_system_access/file_system_access_permission_context_factory.h"
#include "chrome/browser/file_system_access/file_system_access_permission_request_manager.h"
#include "chrome/browser/installable/installable_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#include "chrome/browser/ui/file_system_access_dialogs.h"
#include "chrome/common/chrome_paths.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/safe_browsing/buildflags.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/origin.h"
#if !defined(OS_ANDROID)
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#endif
namespace features {
const base::Feature kFileSystemAccessPersistentPermissions{
"kFileSystemAccessPersistentPermissions",
base::FEATURE_DISABLED_BY_DEFAULT};
} // namespace features
namespace {
using HandleType = content::FileSystemAccessPermissionContext::HandleType;
using GrantType = ChromeFileSystemAccessPermissionContext::GrantType;
using blink::mojom::PermissionStatus;
using permissions::PermissionAction;
// This long after the last top-level tab or window for an origin is closed (or
// is navigated to another origin), all the permissions for that origin will be
// revoked.
constexpr base::TimeDelta kPermissionRevocationTimeout =
base::TimeDelta::FromSeconds(5);
// Interval at which to periodically sweep persisted permissions to revoke
// expired grants and renew those with corresponding active grants.
constexpr base::TimeDelta kPersistentPermissionSweepInterval =
base::TimeDelta::FromHours(3);
// Dictionary keys for the FILE_SYSTEM_ACCESS_CHOOSER_DATA setting.
const char kPermissionPathKey[] = "path";
const char kPermissionIsDirectoryKey[] = "is-directory";
const char kPermissionWritableKey[] = "writable";
const char kPermissionReadableKey[] = "readable";
const char kPermissionLastUsedTimeKey[] = "time";
// Dictionary keys for the FILE_SYSTEM_LAST_PICKED_DIRECTORY website setting.
// Schema (per origin):
// {
// ...
// {
// "default-id" : { "path" : <path> , "path-type" : <type>}
// "custom-id-fruit" : { "path" : <path> , "path-type" : <type> }
// "custom-id-flower" : { "path" : <path> , "path-type" : <type> }
// ...
// }
// ...
// }
const char kDefaultLastPickedDirectoryKey[] = "default-id";
const char kCustomLastPickedDirectoryKey[] = "custom-id";
const char kPathKey[] = "path";
const char kPathTypeKey[] = "path-type";
const char kTimestampKey[] = "timestamp";
// TODO(https://crbug.com/1177334): Remove migration logic.
// Deprecated 2/2021. Former schema (per origin):
// {
// ...
// "default-path" : <path>,
// "default-path-type" : <type>,
// ...
// }
const char kDeprecatedLastPickedDirectoryKey[] = "default-path";
const char kDeprecatedLastPickedDirectoryTypeKey[] = "default-path-type";
void ShowFileSystemAccessRestrictedDirectoryDialogOnUIThread(
content::GlobalRenderFrameHostId frame_id,
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
base::OnceCallback<
void(ChromeFileSystemAccessPermissionContext::SensitiveDirectoryResult)>
callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
if (!rfh || !rfh->IsActive()) {
// Requested from a no longer valid render frame host.
std::move(callback).Run(ChromeFileSystemAccessPermissionContext::
SensitiveDirectoryResult::kAbort);
return;
}
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(rfh);
if (!web_contents) {
// Requested from a worker, or a no longer existing tab.
std::move(callback).Run(ChromeFileSystemAccessPermissionContext::
SensitiveDirectoryResult::kAbort);
return;
}
ShowFileSystemAccessRestrictedDirectoryDialog(
origin, path, handle_type, std::move(callback), web_contents);
}
// Sentinel used to indicate that no PathService key is specified for a path in
// the struct below.
constexpr const int kNoBasePathKey = -1;
enum BlockType {
kBlockAllChildren,
kBlockNestedDirectories,
kDontBlockChildren
};
const struct {
// base::BasePathKey value (or one of the platform specific extensions to it)
// for a path that should be blocked. Specify kNoBasePathKey if |path| should
// be used instead.
int base_path_key;
// Explicit path to block instead of using |base_path_key|. Set to nullptr to
// use |base_path_key| on its own. If both |base_path_key| and |path| are set,
// |path| is treated relative to the path |base_path_key| resolves to.
const base::FilePath::CharType* path;
// If this is set to kDontBlockChildren, only the given path and its parents
// are blocked. If this is set to kBlockAllChildren, all children of the given
// path are blocked as well. Finally if this is set to kBlockNestedDirectories
// access is allowed to individual files in the directory, but nested
// directories are still blocked.
// The BlockType of the nearest ancestor of a path to check is what ultimately
// determines if a path is blocked or not. If a blocked path is a descendent
// of another blocked path, then it may override the child-blocking policy of
// its ancestor. For example, if /home blocks all children, but
// /home/downloads does not, then /home/downloads/file.ext will *not* be
// blocked.
BlockType type;
} kBlockedPaths[] = {
// Don't allow users to share their entire home directory, entire desktop or
// entire documents folder, but do allow sharing anything inside those
// directories not otherwise blocked.
{base::DIR_HOME, nullptr, kDontBlockChildren},
{base::DIR_USER_DESKTOP, nullptr, kDontBlockChildren},
{chrome::DIR_USER_DOCUMENTS, nullptr, kDontBlockChildren},
// Similar restrictions for the downloads directory.
{chrome::DIR_DEFAULT_DOWNLOADS, nullptr, kDontBlockChildren},
{chrome::DIR_DEFAULT_DOWNLOADS_SAFE, nullptr, kDontBlockChildren},
// The Chrome installation itself should not be modified by the web.
{chrome::DIR_APP, nullptr, kBlockAllChildren},
// And neither should the configuration of at least the currently running
// Chrome instance (note that this does not take --user-data-dir command
// line overrides into account).
{chrome::DIR_USER_DATA, nullptr, kBlockAllChildren},
// ~/.ssh is pretty sensitive on all platforms, so block access to that.
{base::DIR_HOME, FILE_PATH_LITERAL(".ssh"), kBlockAllChildren},
// And limit access to ~/.gnupg as well.
{base::DIR_HOME, FILE_PATH_LITERAL(".gnupg"), kBlockAllChildren},
#if defined(OS_WIN)
// Some Windows specific directories to block, basically all apps, the
// operating system itself, as well as configuration data for apps.
{base::DIR_PROGRAM_FILES, nullptr, kBlockAllChildren},
{base::DIR_PROGRAM_FILESX86, nullptr, kBlockAllChildren},
{base::DIR_PROGRAM_FILES6432, nullptr, kBlockAllChildren},
{base::DIR_WINDOWS, nullptr, kBlockAllChildren},
{base::DIR_APP_DATA, nullptr, kBlockAllChildren},
{base::DIR_LOCAL_APP_DATA, nullptr, kBlockAllChildren},
{base::DIR_COMMON_APP_DATA, nullptr, kBlockAllChildren},
// Opening a file from an MTP device, such as a smartphone or a camera, is
// implemented by Windows as opening a file in the temporary internet files
// directory. To support that, allow opening files in that directory, but
// not whole directories.
{base::DIR_IE_INTERNET_CACHE, nullptr, kBlockNestedDirectories},
#endif
#if defined(OS_MAC)
// Similar Mac specific blocks.
{base::DIR_APP_DATA, nullptr, kBlockAllChildren},
{base::DIR_HOME, FILE_PATH_LITERAL("Library"), kBlockAllChildren},
// Allow access to iCloud files.
{base::DIR_HOME, FILE_PATH_LITERAL("Library/Mobile Documents"),
kDontBlockChildren},
#endif
#if defined(OS_LINUX) || defined(OS_CHROMEOS)
// On Linux also block access to devices via /dev, as well as security
// sensitive data in /sys and /proc.
{kNoBasePathKey, FILE_PATH_LITERAL("/dev"), kBlockAllChildren},
{kNoBasePathKey, FILE_PATH_LITERAL("/sys"), kBlockAllChildren},
{kNoBasePathKey, FILE_PATH_LITERAL("/proc"), kBlockAllChildren},
// And block all of ~/.config, matching the similar restrictions on mac
// and windows.
{base::DIR_HOME, FILE_PATH_LITERAL(".config"), kBlockAllChildren},
// Block ~/.dbus as well, just in case, although there probably isn't much a
// website can do with access to that directory and its contents.
{base::DIR_HOME, FILE_PATH_LITERAL(".dbus"), kBlockAllChildren},
#endif
// TODO(https://crbug.com/984641): Refine this list, for example add
// XDG_CONFIG_HOME when it is not set ~/.config?
};
bool ShouldBlockAccessToPath(const base::FilePath& check_path,
HandleType handle_type) {
DCHECK(!check_path.empty());
DCHECK(check_path.IsAbsolute());
base::FilePath nearest_ancestor;
int nearest_ancestor_path_key = kNoBasePathKey;
BlockType nearest_ancestor_block_type = kDontBlockChildren;
for (const auto& block : kBlockedPaths) {
base::FilePath blocked_path;
if (block.base_path_key != kNoBasePathKey) {
if (!base::PathService::Get(block.base_path_key, &blocked_path))
continue;
if (block.path)
blocked_path = blocked_path.Append(block.path);
} else {
DCHECK(block.path);
blocked_path = base::FilePath(block.path);
}
if (check_path == blocked_path || check_path.IsParent(blocked_path)) {
VLOG(1) << "Blocking access to " << check_path
<< " because it is a parent of " << blocked_path << " ("
<< block.base_path_key << ")";
return true;
}
if (blocked_path.IsParent(check_path) &&
(nearest_ancestor.empty() || nearest_ancestor.IsParent(blocked_path))) {
nearest_ancestor = blocked_path;
nearest_ancestor_path_key = block.base_path_key;
nearest_ancestor_block_type = block.type;
}
}
// The path we're checking is not in a potentially blocked directory, or the
// nearest ancestor does not block access to its children. Grant access.
if (nearest_ancestor.empty() ||
nearest_ancestor_block_type == kDontBlockChildren) {
return false;
}
// The path we're checking is a file, and the nearest ancestor only blocks
// access to directories. Grant access.
if (handle_type == HandleType::kFile &&
nearest_ancestor_block_type == kBlockNestedDirectories) {
return false;
}
// The nearest ancestor blocks access to its children, so block access.
VLOG(1) << "Blocking access to " << check_path << " because it is inside "
<< nearest_ancestor << " (" << nearest_ancestor_path_key << ")";
return true;
}
// Returns a callback that calls the passed in |callback| by posting a task to
// the current sequenced task runner.
template <typename... ResultTypes>
base::OnceCallback<void(ResultTypes... results)>
BindResultCallbackToCurrentSequence(
base::OnceCallback<void(ResultTypes... results)> callback) {
return base::BindOnce(
[](scoped_refptr<base::TaskRunner> task_runner,
base::OnceCallback<void(ResultTypes... results)> callback,
ResultTypes... results) {
task_runner->PostTask(FROM_HERE,
base::BindOnce(std::move(callback), results...));
},
base::SequencedTaskRunnerHandle::Get(), std::move(callback));
}
void DoSafeBrowsingCheckOnUIThread(
content::GlobalRenderFrameHostId frame_id,
std::unique_ptr<content::FileSystemAccessWriteItem> item,
safe_browsing::CheckDownloadCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Download Protection Service is not supported on Android.
#if BUILDFLAG(FULL_SAFE_BROWSING)
safe_browsing::SafeBrowsingService* sb_service =
g_browser_process->safe_browsing_service();
if (!sb_service || !sb_service->download_protection_service() ||
!sb_service->download_protection_service()->enabled()) {
std::move(callback).Run(safe_browsing::DownloadCheckResult::UNKNOWN);
return;
}
if (!item->browser_context) {
content::RenderProcessHost* rph =
content::RenderProcessHost::FromID(frame_id.child_id);
if (!rph) {
std::move(callback).Run(safe_browsing::DownloadCheckResult::UNKNOWN);
return;
}
item->browser_context = rph->GetBrowserContext();
}
if (!item->web_contents) {
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
if (rfh)
item->web_contents = content::WebContents::FromRenderFrameHost(rfh);
}
sb_service->download_protection_service()->CheckFileSystemAccessWrite(
std::move(item), std::move(callback));
#endif
}
ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult
InterpretSafeBrowsingResult(safe_browsing::DownloadCheckResult result) {
using Result = safe_browsing::DownloadCheckResult;
switch (result) {
// Only allow downloads that are marked as SAFE or UNKNOWN by SafeBrowsing.
// All other types are going to be blocked. UNKNOWN could be the result of a
// failed safe browsing ping.
case Result::UNKNOWN:
case Result::SAFE:
case Result::ALLOWLISTED_BY_POLICY:
return ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult::
kAllow;
case Result::DANGEROUS:
case Result::UNCOMMON:
case Result::DANGEROUS_HOST:
case Result::POTENTIALLY_UNWANTED:
case Result::BLOCKED_PASSWORD_PROTECTED:
case Result::BLOCKED_TOO_LARGE:
case Result::BLOCKED_UNSUPPORTED_FILE_TYPE:
case Result::DANGEROUS_ACCOUNT_COMPROMISE:
return ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult::
kBlock;
// This shouldn't be returned for File System Access write checks.
case Result::ASYNC_SCANNING:
case Result::SENSITIVE_CONTENT_WARNING:
case Result::SENSITIVE_CONTENT_BLOCK:
case Result::DEEP_SCANNED_SAFE:
case Result::PROMPT_FOR_SCANNING:
NOTREACHED();
return ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult::
kAllow;
}
NOTREACHED();
return ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult::kBlock;
}
std::string GenerateLastPickedDirectoryKey(const std::string& id) {
return id.empty() ? kDefaultLastPickedDirectoryKey
: base::StrCat({kCustomLastPickedDirectoryKey, "-", id});
}
base::StringPiece PathAsPermissionKey(const base::FilePath& path) {
return base::StringPiece(
reinterpret_cast<const char*>(path.value().data()),
path.value().size() * sizeof(base::FilePath::CharType));
}
base::StringPiece GetGrantKeyFromGrantType(GrantType type) {
return type == GrantType::kWrite ? kPermissionWritableKey
: kPermissionReadableKey;
}
} // namespace
ChromeFileSystemAccessPermissionContext::Grants::Grants() = default;
ChromeFileSystemAccessPermissionContext::Grants::~Grants() = default;
ChromeFileSystemAccessPermissionContext::Grants::Grants(Grants&&) = default;
ChromeFileSystemAccessPermissionContext::Grants&
ChromeFileSystemAccessPermissionContext::Grants::operator=(Grants&&) = default;
class ChromeFileSystemAccessPermissionContext::PermissionGrantImpl
: public content::FileSystemAccessPermissionGrant {
public:
PermissionGrantImpl(
base::WeakPtr<ChromeFileSystemAccessPermissionContext> context,
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
GrantType type)
: context_(std::move(context)),
origin_(origin),
path_(path),
handle_type_(handle_type),
type_(type) {}
// FileSystemAccessPermissionGrant:
PermissionStatus GetStatus() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return status_;
}
base::FilePath GetPath() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return path_;
}
void RequestPermission(
content::GlobalRenderFrameHostId frame_id,
UserActivationState user_activation_state,
base::OnceCallback<void(PermissionRequestOutcome)> callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Check if a permission request has already been processed previously. This
// check is done first because we don't want to reset the status of a
// permission if it has already been granted.
if (GetStatus() != PermissionStatus::ASK || !context_) {
if (GetStatus() == PermissionStatus::GRANTED) {
SetStatus(PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
}
std::move(callback).Run(PermissionRequestOutcome::kRequestAborted);
return;
}
if (HasPersistedPermission(MetricsOptions::kRecord)) {
SetStatus(PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback),
PermissionRequestOutcome::kGrantedByPersistentPermission);
return;
}
if (AncestorHasPersistedPermission()) {
SetStatus(PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback),
PermissionRequestOutcome::kGrantedByAncestorPersistentPermission);
return;
}
if (type_ == GrantType::kWrite) {
ContentSetting content_setting =
context_->GetWriteGuardContentSetting(origin_);
// Content setting grants write permission without asking.
if (content_setting == CONTENT_SETTING_ALLOW) {
SetStatus(PermissionStatus::GRANTED,
PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback),
PermissionRequestOutcome::kGrantedByContentSetting);
return;
}
// Content setting blocks write permission.
if (content_setting == CONTENT_SETTING_BLOCK) {
SetStatus(PermissionStatus::DENIED,
PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback),
PermissionRequestOutcome::kBlockedByContentSetting);
return;
}
}
// Otherwise, perform checks and ask the user for permission.
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
if (!rfh || !rfh->IsActive()) {
// Requested from a no longer valid render frame host.
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kInvalidFrame);
return;
}
if (user_activation_state == UserActivationState::kRequired &&
!rfh->HasTransientUserActivation()) {
// No permission prompts without user activation.
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kNoUserActivation);
return;
}
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(rfh);
if (!web_contents) {
// Requested from a worker, or a no longer existing tab.
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kInvalidFrame);
return;
}
url::Origin embedding_origin =
url::Origin::Create(web_contents->GetLastCommittedURL());
if (embedding_origin != origin_) {
// Third party iframes are not allowed to request more permissions.
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kThirdPartyContext);
return;
}
auto* request_manager =
FileSystemAccessPermissionRequestManager::FromWebContents(web_contents);
if (!request_manager) {
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kRequestAborted);
return;
}
// Drop fullscreen mode so that the user sees the URL bar.
base::ScopedClosureRunner fullscreen_block =
web_contents->ForSecurityDropFullscreen();
FileSystemAccessPermissionRequestManager::Access access =
type_ == GrantType::kRead
? FileSystemAccessPermissionRequestManager::Access::kRead
: FileSystemAccessPermissionRequestManager::Access::kWrite;
// If a website wants both read and write access, code in content will
// request those as two separate requests. The |request_manager| will then
// detect this and combine the two requests into one prompt. As such this
// code does not have to have any way to request Access::kReadWrite.
request_manager->AddRequest(
{origin_, path_, handle_type_, access},
base::BindOnce(&PermissionGrantImpl::OnPermissionRequestResult, this,
std::move(callback)),
std::move(fullscreen_block));
}
bool HasPersistedPermission(MetricsOptions options) const {
return context_->HasPersistedPermission(origin_, path_, handle_type_, type_,
options);
}
bool AncestorHasPersistedPermission() const {
for (base::FilePath parent = path_.DirName(); parent != parent.DirName();
parent = parent.DirName()) {
if (context_->HasPersistedPermission(origin_, parent,
HandleType::kDirectory, type_,
MetricsOptions::kDoNotRecord))
return true;
}
return false;
}
const url::Origin& origin() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return origin_;
}
HandleType handle_type() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return handle_type_;
}
const base::FilePath& path() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return path_;
}
GrantType type() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return type_;
}
void SetStatus(PermissionStatus status,
PersistedPermissionOptions persisted_status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
bool should_notify = status_ != status;
status_ = status;
if (context_ &&
persisted_status ==
PersistedPermissionOptions::kUpdatePersistedPermission &&
base::FeatureList::IsEnabled(
features::kFileSystemAccessPersistentPermissions)) {
// PersistedPermissionOptions only applies to this grant's type. If the
// other type is persisted, only revoke permission to this grant's type.
base::Value grant = AsValue();
if (grant.FindBoolKey(kPermissionWritableKey).value_or(false) ||
grant.FindBoolKey(kPermissionReadableKey).value_or(false)) {
context_->GrantObjectPermission(origin_, std::move(grant));
} else {
context_->RevokeObjectPermission(origin_, GetKey());
}
}
if (should_notify)
NotifyPermissionStatusChanged();
}
static void CollectGrants(
const std::map<base::FilePath, PermissionGrantImpl*>& grants,
std::vector<base::FilePath>* directory_grants,
std::vector<base::FilePath>* file_grants) {
for (const auto& entry : grants) {
if (entry.second->GetStatus() != PermissionStatus::GRANTED)
continue;
if (entry.second->handle_type() == HandleType::kDirectory) {
directory_grants->push_back(entry.second->path());
} else {
file_grants->push_back(entry.second->path());
}
}
}
protected:
~PermissionGrantImpl() override {
if (context_)
context_->PermissionGrantDestroyed(this);
}
private:
void OnPermissionRequestResult(
base::OnceCallback<void(PermissionRequestOutcome)> callback,
PermissionAction result) {
switch (result) {
case PermissionAction::GRANTED:
SetStatus(PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kUserGranted);
if (context_)
context_->ScheduleUsageIconUpdate();
break;
case PermissionAction::DENIED:
SetStatus(PermissionStatus::DENIED,
PersistedPermissionOptions::kUpdatePersistedPermission);
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kUserDenied);
break;
case PermissionAction::DISMISSED:
case PermissionAction::IGNORED:
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kUserDismissed);
break;
case PermissionAction::REVOKED:
case PermissionAction::GRANTED_ONCE:
case PermissionAction::NUM:
NOTREACHED();
break;
}
}
void RunCallbackAndRecordPermissionRequestOutcome(
base::OnceCallback<void(PermissionRequestOutcome)> callback,
PermissionRequestOutcome outcome) {
if (type_ == GrantType::kWrite) {
base::UmaHistogramEnumeration(
"NativeFileSystemAPI.WritePermissionRequestOutcome", outcome);
if (handle_type_ == HandleType::kDirectory) {
base::UmaHistogramEnumeration(
"NativeFileSystemAPI.WritePermissionRequestOutcome.Directory",
outcome);
} else {
base::UmaHistogramEnumeration(
"NativeFileSystemAPI.WritePermissionRequestOutcome.File", outcome);
}
} else {
base::UmaHistogramEnumeration(
"NativeFileSystemAPI.ReadPermissionRequestOutcome", outcome);
if (handle_type_ == HandleType::kDirectory) {
base::UmaHistogramEnumeration(
"NativeFileSystemAPI.ReadPermissionRequestOutcome.Directory",
outcome);
} else {
base::UmaHistogramEnumeration(
"NativeFileSystemAPI.ReadPermissionRequestOutcome.File", outcome);
}
}
std::move(callback).Run(outcome);
}
base::StringPiece GetKey() const { return PathAsPermissionKey(path_); }
base::Value AsValue() const {
base::Value value(base::Value::Type::DICTIONARY);
value.SetKey(kPermissionPathKey, util::FilePathToValue(path_));
value.SetBoolKey(kPermissionIsDirectoryKey,
handle_type_ == HandleType::kDirectory);
value.SetBoolKey(GetGrantKeyFromGrantType(type_),
status_ == PermissionStatus::GRANTED);
// Persisted permissions include both read and write information in one
// object. Figure out if the other grant type is already persisted.
auto opposite_type =
type_ == GrantType::kRead ? GrantType::kWrite : GrantType::kRead;
if (context_->HasPersistedPermission(origin_, path_, handle_type_,
opposite_type,
MetricsOptions::kDoNotRecord))
value.SetBoolKey(GetGrantKeyFromGrantType(opposite_type), true);
value.SetKey(kPermissionLastUsedTimeKey,
util::TimeToValue(context_->clock_->Now()));
return value;
}
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtr<ChromeFileSystemAccessPermissionContext> const context_;
const url::Origin origin_;
const base::FilePath path_;
const HandleType handle_type_;
const GrantType type_;
base::Time last_used_time_;
// This member should only be updated via SetStatus(), to make sure
// observers are properly notified about any change in status.
PermissionStatus status_ = PermissionStatus::ASK;
};
struct ChromeFileSystemAccessPermissionContext::OriginState {
// Raw pointers, owned collectively by all the handles that reference this
// grant. When last reference goes away this state is cleared as well by
// PermissionGrantDestroyed().
std::map<base::FilePath, PermissionGrantImpl*> read_grants;
std::map<base::FilePath, PermissionGrantImpl*> write_grants;
// Timer that is triggered whenever the user navigates away from this origin.
// This is used to give a website a little bit of time for background work
// before revoking all permissions for the origin.
std::unique_ptr<base::RetainingOneShotTimer> cleanup_timer;
};
constexpr base::TimeDelta ChromeFileSystemAccessPermissionContext::
kPersistentPermissionExpirationTimeoutNonPWA;
constexpr base::TimeDelta ChromeFileSystemAccessPermissionContext::
kPersistentPermissionExpirationTimeoutPWA;
constexpr base::TimeDelta
ChromeFileSystemAccessPermissionContext::kPersistentPermissionGracePeriod;
ChromeFileSystemAccessPermissionContext::
ChromeFileSystemAccessPermissionContext(content::BrowserContext* context,
const base::Clock* clock)
: ObjectPermissionContextBase(
ContentSettingsType::FILE_SYSTEM_WRITE_GUARD,
ContentSettingsType::FILE_SYSTEM_ACCESS_CHOOSER_DATA,
HostContentSettingsMapFactory::GetForProfile(context)),
profile_(context),
clock_(clock) {
DETACH_FROM_SEQUENCE(sequence_checker_);
content_settings_ = base::WrapRefCounted(
HostContentSettingsMapFactory::GetForProfile(profile_));
if (base::FeatureList::IsEnabled(
features::kFileSystemAccessPersistentPermissions)) {
// Revoke expired persisted permissions.
content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
->PostTask(FROM_HERE,
base::BindOnce(&ChromeFileSystemAccessPermissionContext::
UpdatePersistedPermissions,
weak_factory_.GetWeakPtr()));
// Periodically sweep persisted permissions to revoke expired
// permissions and renew those with corresponding active grants.
periodic_sweep_persisted_permissions_timer_.SetTaskRunner(
content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT}));
periodic_sweep_persisted_permissions_timer_.Start(
FROM_HERE, kPersistentPermissionSweepInterval, this,
&ChromeFileSystemAccessPermissionContext::UpdatePersistedPermissions);
}
}
ChromeFileSystemAccessPermissionContext::
~ChromeFileSystemAccessPermissionContext() = default;
scoped_refptr<content::FileSystemAccessPermissionGrant>
ChromeFileSystemAccessPermissionContext::GetReadPermissionGrant(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// operator[] might insert a new OriginState in |origins_|, but that
// is exactly what we want.
auto& origin_state = origins_[origin];
auto*& existing_grant = origin_state.read_grants[path];
scoped_refptr<PermissionGrantImpl> new_grant;
if (existing_grant && existing_grant->handle_type() != handle_type) {
// |path| changed from being a directory to being a file or vice versa,
// don't just re-use the existing grant but revoke the old grant before
// creating a new grant.
existing_grant->SetStatus(
PermissionStatus::DENIED,
PersistedPermissionOptions::kUpdatePersistedPermission);
existing_grant = nullptr;
}
if (!existing_grant) {
new_grant = base::MakeRefCounted<PermissionGrantImpl>(
weak_factory_.GetWeakPtr(), origin, path, handle_type,
GrantType::kRead);
existing_grant = new_grant.get();
}
const ContentSetting content_setting = GetReadGuardContentSetting(origin);
switch (content_setting) {
case CONTENT_SETTING_ALLOW:
// Don't persist permissions when the origin is allowlisted.
existing_grant->SetStatus(
PermissionStatus::GRANTED,
PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
break;
case CONTENT_SETTING_ASK:
// If a parent directory is already readable this new grant should also be
// readable.
if (new_grant &&
AncestorHasActivePermission(origin, path, GrantType::kRead)) {
existing_grant->SetStatus(
PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
break;
}
switch (user_action) {
case UserAction::kOpen:
case UserAction::kSave:
// Open and Save dialog only grant read access for individual files.
if (handle_type == HandleType::kDirectory)
break;
FALLTHROUGH;
case UserAction::kDragAndDrop:
// Drag&drop grants read access for all handles.
existing_grant->SetStatus(
PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
break;
case UserAction::kLoadFromStorage:
break;
}
break;
case CONTENT_SETTING_BLOCK:
// Don't bother revoking persisted permissions. If the permissions have
// not yet expired when the ContentSettingValue is changed, they will
// effectively be reinstated.
if (new_grant) {
existing_grant->SetStatus(
PermissionStatus::DENIED,
PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
} else {
// We won't revoke permission to an existing grant.
}
break;
default:
NOTREACHED();
break;
}
if (existing_grant->GetStatus() == PermissionStatus::GRANTED)
ScheduleUsageIconUpdate();
return existing_grant;
}
scoped_refptr<content::FileSystemAccessPermissionGrant>
ChromeFileSystemAccessPermissionContext::GetWritePermissionGrant(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// operator[] might insert a new OriginState in |origins_|, but that
// is exactly what we want.
auto& origin_state = origins_[origin];
auto*& existing_grant = origin_state.write_grants[path];
scoped_refptr<PermissionGrantImpl> new_grant;
if (existing_grant && existing_grant->handle_type() != handle_type) {
// |path| changed from being a directory to being a file or vice versa,
// don't just re-use the existing grant but revoke the old grant before
// creating a new grant.
existing_grant->SetStatus(
PermissionStatus::DENIED,
PersistedPermissionOptions::kUpdatePersistedPermission);
existing_grant = nullptr;
}
if (!existing_grant) {
new_grant = base::MakeRefCounted<PermissionGrantImpl>(
weak_factory_.GetWeakPtr(), origin, path, handle_type,
GrantType::kWrite);
existing_grant = new_grant.get();
}
const ContentSetting content_setting = GetWriteGuardContentSetting(origin);
switch (content_setting) {
case CONTENT_SETTING_ALLOW:
// Don't persist permissions when the origin is allowlisted.
existing_grant->SetStatus(
PermissionStatus::GRANTED,
PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
break;
case CONTENT_SETTING_ASK:
// If a parent directory is already writable this new grant should also be
// writable.
if (new_grant &&
AncestorHasActivePermission(origin, path, GrantType::kWrite)) {
existing_grant->SetStatus(
PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
break;
}
switch (user_action) {
case UserAction::kSave:
// Only automatically grant write access for save dialogs.
existing_grant->SetStatus(
PermissionStatus::GRANTED,
PersistedPermissionOptions::kUpdatePersistedPermission);
break;
case UserAction::kOpen:
case UserAction::kDragAndDrop:
case UserAction::kLoadFromStorage:
break;
}
break;
case CONTENT_SETTING_BLOCK:
// Don't bother revoking persisted permissions. If the permissions have
// not yet expired when the ContentSettingValue is changed, they will
// effectively be reinstated.
if (new_grant) {
existing_grant->SetStatus(
PermissionStatus::DENIED,
PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
} else {
// We won't revoke permission to an existing grant.
}
break;
default:
NOTREACHED();
break;
}
if (existing_grant->GetStatus() == PermissionStatus::GRANTED)
ScheduleUsageIconUpdate();
return existing_grant;
}
// Returns non-expired persisted permissions. Active grants are ignored here,
// since persisted permissions are almost always a superset of active grants.
// The exception is when CONTENT_SETTING_ALLOW for an origin, which is only
// possible for System Web Apps.
std::vector<std::unique_ptr<permissions::ObjectPermissionContextBase::Object>>
ChromeFileSystemAccessPermissionContext::GetGrantedObjects(
const url::Origin& origin) {
std::vector<std::unique_ptr<Object>> objects =
ObjectPermissionContextBase::GetGrantedObjects(origin);
bool is_installed_pwa = OriginIsInstalledPWA(origin);
// Filter out expired permissions.
objects.erase(
base::ranges::remove_if(
objects,
[this, &is_installed_pwa](const std::unique_ptr<Object>& object) {
auto last_activity_time =
util::ValueToTime(
object->value.FindKey(kPermissionLastUsedTimeKey))
.value_or(base::Time::Min());
return this->PersistentPermissionIsExpired(last_activity_time,
is_installed_pwa);
}),
objects.end());
return objects;
}
// Returns non-expired persisted permissions. Active grants are ignored here,
// since persisted permissions are almost always a superset of active grants.
// The exception is when CONTENT_SETTING_ALLOW for an origin, which is only
// possible for System Web Apps.
std::vector<std::unique_ptr<permissions::ObjectPermissionContextBase::Object>>
ChromeFileSystemAccessPermissionContext::GetAllGrantedObjects() {
std::vector<std::unique_ptr<Object>> objects =
GetAllGrantedOrExpiredObjects();
url::Origin origin;
GURL origin_as_url;
bool is_installed_pwa = false;
// Filter out expired permissions.
// Checking whether an origin has an installed PWA may be expensive.
// GetAllGrantedObjects() returns objects grouped by origin, so this should
// only check once per origin.
objects.erase(base::ranges::remove_if(
objects,
[this, &is_installed_pwa, &origin,
&origin_as_url](const std::unique_ptr<Object>& object) {
if (object->origin != origin_as_url) {
origin_as_url = object->origin;
origin = url::Origin::Create(object->origin);
is_installed_pwa = OriginIsInstalledPWA(origin);
}
auto last_activity_time =
util::ValueToTime(
object->value.FindKey(kPermissionLastUsedTimeKey))
.value_or(base::Time::Min());
return this->PersistentPermissionIsExpired(
last_activity_time, is_installed_pwa);
}),
objects.end());
return objects;
}
std::string ChromeFileSystemAccessPermissionContext::GetKeyForObject(
const base::Value& object) {
const auto optional_path =
util::ValueToFilePath(object.FindKey(kPermissionPathKey));
DCHECK(optional_path);
return std::string(PathAsPermissionKey(optional_path.value()));
}
bool ChromeFileSystemAccessPermissionContext::IsValidObject(
const base::Value& object) {
// At least one of the readable/writable keys needs to be set.
if (!object.is_dict() || (object.DictSize() != 4 && object.DictSize() != 5) ||
!object.FindKey(kPermissionPathKey) ||
!object.FindBoolKey(kPermissionIsDirectoryKey) ||
(!object.FindBoolKey(kPermissionWritableKey) &&
!object.FindBoolKey(kPermissionReadableKey)) ||
!object.FindKey(kPermissionLastUsedTimeKey)) {
return false;
}
return true;
}
std::u16string ChromeFileSystemAccessPermissionContext::GetObjectDisplayName(
const base::Value& object) {
const auto optional_path =
util::ValueToFilePath(object.FindKey(kPermissionPathKey));
DCHECK(optional_path);
return optional_path->LossyDisplayName();
}
ContentSetting
ChromeFileSystemAccessPermissionContext::GetWriteGuardContentSetting(
const url::Origin& origin) {
return content_settings()->GetContentSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_WRITE_GUARD);
}
ContentSetting
ChromeFileSystemAccessPermissionContext::GetReadGuardContentSetting(
const url::Origin& origin) {
return content_settings()->GetContentSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_READ_GUARD);
}
bool ChromeFileSystemAccessPermissionContext::CanObtainReadPermission(
const url::Origin& origin) {
return GetReadGuardContentSetting(origin) == CONTENT_SETTING_ASK ||
GetReadGuardContentSetting(origin) == CONTENT_SETTING_ALLOW;
}
bool ChromeFileSystemAccessPermissionContext::CanObtainWritePermission(
const url::Origin& origin) {
return GetWriteGuardContentSetting(origin) == CONTENT_SETTING_ASK ||
GetWriteGuardContentSetting(origin) == CONTENT_SETTING_ALLOW;
}
void ChromeFileSystemAccessPermissionContext::ConfirmSensitiveDirectoryAccess(
const url::Origin& origin,
PathType path_type,
const base::FilePath& path,
HandleType handle_type,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(SensitiveDirectoryResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(https://crbug.com/1009970): Figure out what external paths should be
// blocked. We could resolve the external path to a local path, and check for
// blocked directories based on that, but that doesn't work well. Instead we
// should have a separate Chrome OS only code path to block for example the
// root of certain external file systems.
if (path_type == PathType::kExternal) {
std::move(callback).Run(SensitiveDirectoryResult::kAllowed);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&ShouldBlockAccessToPath, path, handle_type),
base::BindOnce(&ChromeFileSystemAccessPermissionContext::
DidConfirmSensitiveDirectoryAccess,
GetWeakPtr(), origin, path, handle_type, frame_id,
std::move(callback)));
}
void ChromeFileSystemAccessPermissionContext::PerformAfterWriteChecks(
std::unique_ptr<content::FileSystemAccessWriteItem> item,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(AfterWriteCheckResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&DoSafeBrowsingCheckOnUIThread, frame_id, std::move(item),
base::BindOnce(
[](scoped_refptr<base::TaskRunner> task_runner,
base::OnceCallback<void(AfterWriteCheckResult result)>
callback,
safe_browsing::DownloadCheckResult result) {
task_runner->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
InterpretSafeBrowsingResult(result)));
},
base::SequencedTaskRunnerHandle::Get(), std::move(callback))));
}
void ChromeFileSystemAccessPermissionContext::
DidConfirmSensitiveDirectoryAccess(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(SensitiveDirectoryResult)> callback,
bool should_block) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!should_block) {
std::move(callback).Run(SensitiveDirectoryResult::kAllowed);
return;
}
auto result_callback =
BindResultCallbackToCurrentSequence(std::move(callback));
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&ShowFileSystemAccessRestrictedDirectoryDialogOnUIThread,
frame_id, origin, path, handle_type,
std::move(result_callback)));
}
// TODO(https://crbug.com/1177334): Remove migration logic.
void ChromeFileSystemAccessPermissionContext::MaybeMigrateOriginToNewSchema(
const url::Origin& origin) {
std::unique_ptr<base::Value> value = content_settings()->GetWebsiteSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY, /*info=*/nullptr);
if (!value)
return;
auto* default_path_value = value->FindKey(kDeprecatedLastPickedDirectoryKey);
if (!default_path_value)
return;
auto default_path =
util::ValueToFilePath(default_path_value).value_or(base::FilePath());
auto default_type =
value->FindIntKey(kDeprecatedLastPickedDirectoryTypeKey) ==
static_cast<int>(PathType::kExternal)
? PathType::kExternal
: PathType::kLocal;
// Remove old keys.
value->RemoveKey(kDeprecatedLastPickedDirectoryKey);
value->RemoveKey(kDeprecatedLastPickedDirectoryTypeKey);
// Set this information as the default.
base::Value entry(base::Value::Type::DICTIONARY);
entry.SetKey(kPathKey, util::FilePathToValue(default_path));
entry.SetIntKey(kPathTypeKey, static_cast<int>(default_type));
value->SetKey(GenerateLastPickedDirectoryKey(std::string()),
std::move(entry));
content_settings_->SetWebsiteSettingDefaultScope(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY, std::move(value));
}
void ChromeFileSystemAccessPermissionContext::MaybeEvictEntries(
std::unique_ptr<base::Value>& value) {
if (!value->is_dict()) {
value = std::make_unique<base::Value>(base::Value::Type::DICTIONARY);
return;
}
std::vector<std::pair<base::Time, std::string>> entries;
entries.reserve(value->DictSize());
for (auto entry : value->DictItems()) {
// Don't evict the default ID.
if (entry.first == kDefaultLastPickedDirectoryKey)
continue;
entries.emplace_back(util::ValueToTime(entry.second.FindKey(kTimestampKey))
.value_or(base::Time::Min()),
entry.first);
}
if (entries.size() <= max_ids_per_origin_)
return;
base::ranges::sort(entries);
size_t entries_to_remove = entries.size() - max_ids_per_origin_;
for (size_t i = 0; i < entries_to_remove; ++i) {
bool did_remove_entry = value->RemoveKey(entries[i].second);
DCHECK(did_remove_entry);
}
}
void ChromeFileSystemAccessPermissionContext::SetLastPickedDirectory(
const url::Origin& origin,
const std::string& id,
const base::FilePath& path,
const PathType type) {
MaybeMigrateOriginToNewSchema(origin);
std::unique_ptr<base::Value> value = content_settings()->GetWebsiteSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY, /*info=*/nullptr);
if (!value)
value = std::make_unique<base::Value>(base::Value::Type::DICTIONARY);
// Create an entry into the nested dictionary.
base::Value entry(base::Value::Type::DICTIONARY);
entry.SetKey(kPathKey, util::FilePathToValue(path));
entry.SetIntKey(kPathTypeKey, static_cast<int>(type));
entry.SetKey(kTimestampKey, util::TimeToValue(clock_->Now()));
value->SetKey(GenerateLastPickedDirectoryKey(id), std::move(entry));
MaybeEvictEntries(value);
content_settings_->SetWebsiteSettingDefaultScope(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY, std::move(value));
}
ChromeFileSystemAccessPermissionContext::PathInfo
ChromeFileSystemAccessPermissionContext::GetLastPickedDirectory(
const url::Origin& origin,
const std::string& id) {
MaybeMigrateOriginToNewSchema(origin);
std::unique_ptr<base::Value> value = content_settings()->GetWebsiteSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY, /*info=*/nullptr);
PathInfo path_info;
if (!value)
return path_info;
auto* entry = value->FindDictKey(GenerateLastPickedDirectoryKey(id));
if (!entry)
return path_info;
auto type_int = entry->FindIntKey(kPathTypeKey)
.value_or(static_cast<int>(PathType::kLocal));
path_info.type = type_int == static_cast<int>(PathType::kExternal)
? PathType::kExternal
: PathType::kLocal;
path_info.path = util::ValueToFilePath(entry->FindKey(kPathKey))
.value_or(base::FilePath());
return path_info;
}
base::FilePath
ChromeFileSystemAccessPermissionContext::GetWellKnownDirectoryPath(
blink::mojom::WellKnownDirectory directory) {
int key = base::PATH_START;
switch (directory) {
case blink::mojom::WellKnownDirectory::kDefault:
key = chrome::DIR_USER_DOCUMENTS;
break;
case blink::mojom::WellKnownDirectory::kDirDesktop:
key = base::DIR_USER_DESKTOP;
break;
case blink::mojom::WellKnownDirectory::kDirDocuments:
key = chrome::DIR_USER_DOCUMENTS;
break;
case blink::mojom::WellKnownDirectory::kDirDownloads:
key = chrome::DIR_DEFAULT_DOWNLOADS;
break;
case blink::mojom::WellKnownDirectory::kDirMusic:
key = chrome::DIR_USER_MUSIC;
break;
case blink::mojom::WellKnownDirectory::kDirPictures:
key = chrome::DIR_USER_PICTURES;
break;
case blink::mojom::WellKnownDirectory::kDirVideos:
key = chrome::DIR_USER_VIDEOS;
break;
}
base::FilePath directory_path;
base::PathService::Get(key, &directory_path);
return directory_path;
}
ChromeFileSystemAccessPermissionContext::Grants
ChromeFileSystemAccessPermissionContext::GetPermissionGrants(
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = origins_.find(origin);
if (it == origins_.end())
return {};
Grants grants;
PermissionGrantImpl::CollectGrants(it->second.read_grants,
&grants.directory_read_grants,
&grants.file_read_grants);
PermissionGrantImpl::CollectGrants(it->second.write_grants,
&grants.directory_write_grants,
&grants.file_write_grants);
return grants;
}
void ChromeFileSystemAccessPermissionContext::RevokeGrants(
const url::Origin& origin,
PersistedPermissionOptions persisted_status) {
auto origin_it = origins_.find(origin);
if (origin_it == origins_.end())
return;
OriginState& origin_state = origin_it->second;
for (auto& grant : origin_state.read_grants)
grant.second->SetStatus(PermissionStatus::ASK, persisted_status);
for (auto& grant : origin_state.write_grants)
grant.second->SetStatus(PermissionStatus::ASK, persisted_status);
ScheduleUsageIconUpdate();
}
bool ChromeFileSystemAccessPermissionContext::OriginHasReadAccess(
const url::Origin& origin) {
auto it = origins_.find(origin);
if (it == origins_.end())
return false;
if (it->second.read_grants.empty())
return false;
for (const auto& grant : it->second.read_grants) {
if (grant.second->GetStatus() == PermissionStatus::GRANTED)
return true;
}
return false;
}
bool ChromeFileSystemAccessPermissionContext::OriginHasWriteAccess(
const url::Origin& origin) {
auto it = origins_.find(origin);
if (it == origins_.end())
return false;
if (it->second.write_grants.empty())
return false;
for (const auto& grant : it->second.write_grants) {
if (grant.second->GetStatus() == PermissionStatus::GRANTED)
return true;
}
return false;
}
void ChromeFileSystemAccessPermissionContext::NavigatedAwayFromOrigin(
const url::Origin& origin) {
auto it = origins_.find(origin);
// If we have no permissions for the origin, there is nothing to do.
if (it == origins_.end())
return;
// Start a timer to possibly clean up permissions for this origin.
if (!it->second.cleanup_timer) {
it->second.cleanup_timer = std::make_unique<base::RetainingOneShotTimer>(
FROM_HERE, kPermissionRevocationTimeout,
base::BindRepeating(&ChromeFileSystemAccessPermissionContext::
MaybeCleanupActivePermissions,
base::Unretained(this), origin));
}
it->second.cleanup_timer->Reset();
}
void ChromeFileSystemAccessPermissionContext::TriggerTimersForTesting() {
for (const auto& it : origins_) {
if (it.second.cleanup_timer) {
auto task = it.second.cleanup_timer->user_task();
it.second.cleanup_timer->Stop();
task.Run();
}
}
}
void ChromeFileSystemAccessPermissionContext::MaybeCleanupActivePermissions(
const url::Origin& origin) {
auto it = origins_.find(origin);
// If we have no permissions for the origin, there is nothing to do.
if (it == origins_.end())
return;
#if !defined(OS_ANDROID)
// Iterate over all top-level frames by iterating over all browsers, and all
// tabs within those browsers. This also counts PWAs in windows without
// tab strips, as those are still implemented as a Browser with a single tab.
for (Browser* browser : *BrowserList::GetInstance()) {
if (browser->profile() != profile())
continue;
TabStripModel* tabs = browser->tab_strip_model();
for (int i = 0; i < tabs->count(); ++i) {
content::WebContents* web_contents = tabs->GetWebContentsAt(i);
url::Origin tab_origin =
url::Origin::Create(web_contents->GetLastCommittedURL());
// Found a tab for this origin, so early exit and don't revoke grants.
if (tab_origin == origin)
return;
}
}
// No tabs found with the same origin, so renew persisted permissions before
// revoking all active permissions for the origin.
if (base::FeatureList::IsEnabled(
features::kFileSystemAccessPersistentPermissions)) {
UpdatePersistedPermissionsForOrigin(origin);
}
RevokeGrants(origin,
PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
#endif
}
bool ChromeFileSystemAccessPermissionContext::AncestorHasActivePermission(
const url::Origin& origin,
const base::FilePath& path,
GrantType grant_type) {
auto it = origins_.find(origin);
if (it == origins_.end())
return false;
const auto& relevant_grants = grant_type == GrantType::kWrite
? it->second.write_grants
: it->second.read_grants;
if (relevant_grants.empty())
return false;
// Permissions are inherited from the closest ancestor.
for (base::FilePath parent = path.DirName(); parent != parent.DirName();
parent = parent.DirName()) {
auto i = relevant_grants.find(parent);
if (i != relevant_grants.end() && i->second &&
i->second->GetStatus() == PermissionStatus::GRANTED) {
return true;
}
}
return false;
}
bool ChromeFileSystemAccessPermissionContext::OriginIsInstalledPWA(
const url::Origin& origin) {
return DoesOriginContainAnyInstalledWebApp(profile_, origin.GetURL());
}
void ChromeFileSystemAccessPermissionContext::
UpdatePersistedPermissionsForTesting() {
UpdatePersistedPermissions();
}
void ChromeFileSystemAccessPermissionContext::UpdatePersistedPermissions() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
SCOPED_UMA_HISTOGRAM_TIMER(
"Storage.FileSystemAccess.PersistedPermissions.SweepTime.All");
url::Origin origin;
GURL origin_as_url;
bool is_installed_pwa = false;
auto objects = GetAllGrantedOrExpiredObjects();
for (const auto& object : objects) {
// Checking whether an origin has an installed PWA may be expensive.
// GetAllGrantedObjects() returns objects grouped by origin, so this should
// only check once per origin.
if (object->origin != origin_as_url) {
origin_as_url = object->origin;
origin = url::Origin::Create(object->origin);
is_installed_pwa = OriginIsInstalledPWA(origin);
}
MaybeRenewOrRevokePersistedPermission(origin, std::move(object->value),
is_installed_pwa);
}
base::UmaHistogramCounts1000(
"Storage.FileSystemAccess.PersistedPermissions.Count", objects.size());
}
void ChromeFileSystemAccessPermissionContext::
UpdatePersistedPermissionsForOrigin(const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
SCOPED_UMA_HISTOGRAM_TIMER(
"Storage.FileSystemAccess.PersistedPermissions.SweepTime.Origin");
bool is_installed_pwa = OriginIsInstalledPWA(origin);
// Call the base class's version of this method, since this class overrides
// this method to filter out expired grants.
for (const auto& object :
ObjectPermissionContextBase::GetGrantedObjects(origin)) {
MaybeRenewOrRevokePersistedPermission(origin, std::move(object->value),
is_installed_pwa);
}
}
void ChromeFileSystemAccessPermissionContext::
MaybeRenewOrRevokePersistedPermission(const url::Origin& origin,
base::Value value,
bool is_installed_pwa) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = origins_.find(origin);
// Look for active read or write grants.
bool found = false;
if (it != origins_.end()) {
base::FilePath path =
util::ValueToFilePath(value.FindKey(kPermissionPathKey)).value();
HandleType handle_type =
value.FindBoolKey(kPermissionIsDirectoryKey).value()
? HandleType::kDirectory
: HandleType::kFile;
const OriginState& origin_state = it->second;
if (value.FindBoolKey(kPermissionReadableKey).value_or(false)) {
found = base::ranges::any_of(
origin_state.read_grants, [&path, &handle_type](auto& grant) {
return grant.first == path &&
grant.second->handle_type() == handle_type &&
grant.second->GetStatus() == PermissionStatus::GRANTED;
});
}
if (!found && value.FindBoolKey(kPermissionWritableKey).value_or(false)) {
found = base::ranges::any_of(
origin_state.write_grants, [&path, &handle_type](auto& grant) {
return grant.first == path &&
grant.second->handle_type() == handle_type &&
grant.second->GetStatus() == PermissionStatus::GRANTED;
});
}
}
if (found) {
value.SetKey(kPermissionLastUsedTimeKey, util::TimeToValue(clock_->Now()));
GrantObjectPermission(origin, std::move(value));
} else {
auto last_activity_time =
util::ValueToTime(value.FindKey(kPermissionLastUsedTimeKey))
.value_or(base::Time::Min());
// Allow a grace period before revoking permissions to allow for better
// metrics regarding permission timeouts.
if (PersistentPermissionIsExpired(
last_activity_time + kPersistentPermissionGracePeriod,
is_installed_pwa)) {
RevokeObjectPermission(origin, GetKeyForObject(value));
}
}
}
absl::optional<base::Value>
ChromeFileSystemAccessPermissionContext::GetPersistedPermission(
const url::Origin& origin,
const base::FilePath& path) {
if (!base::FeatureList::IsEnabled(
features::kFileSystemAccessPersistentPermissions)) {
return absl::nullopt;
}
// Don't persist permissions when the origin is allowlisted or blocked.
auto content_setting = GetWriteGuardContentSetting(origin);
if (content_setting == CONTENT_SETTING_ALLOW ||
content_setting == CONTENT_SETTING_BLOCK) {
return absl::nullopt;
}
// TODO(https://crbug.com/984772): If a parent directory has a persisted
// permission, we should return true here.
const std::unique_ptr<Object> object =
GetGrantedObject(origin, PathAsPermissionKey(path));
if (!object)
return absl::nullopt;
return std::move(object->value);
}
std::vector<std::unique_ptr<permissions::ObjectPermissionContextBase::Object>>
ChromeFileSystemAccessPermissionContext::GetAllGrantedOrExpiredObjects() {
return ObjectPermissionContextBase::GetAllGrantedObjects();
}
bool ChromeFileSystemAccessPermissionContext::HasPersistedPermissionForTesting(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
GrantType grant_type) {
return HasPersistedPermission(origin, path, handle_type, grant_type,
MetricsOptions::kDoNotRecord);
}
bool ChromeFileSystemAccessPermissionContext::HasPersistedPermission(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
GrantType grant_type,
MetricsOptions options) {
const auto& grant = GetPersistedPermission(origin, path);
if (!grant.has_value())
return false;
if (grant->FindBoolKey(kPermissionIsDirectoryKey).value() !=
(handle_type == HandleType::kDirectory)) {
return false;
}
if (!grant->FindBoolKey(GetGrantKeyFromGrantType(grant_type))
.value_or(false)) {
return false;
}
auto is_installed_pwa = OriginIsInstalledPWA(origin);
auto last_activity_time =
util::ValueToTime(grant->FindKey(kPermissionLastUsedTimeKey)).value();
if (options == MetricsOptions::kRecord) {
base::UmaHistogramCustomTimes(
base::StrCat({"Storage.FileSystemAccess.PersistedPermissions.Age.",
is_installed_pwa ? "PWA" : "NonPWA"}),
clock_->Now() - last_activity_time, base::TimeDelta::FromSeconds(1),
base::TimeDelta::FromDays(24), 60);
}
return !PersistentPermissionIsExpired(last_activity_time, is_installed_pwa);
}
bool ChromeFileSystemAccessPermissionContext::PersistentPermissionIsExpired(
const base::Time& last_used,
bool is_installed_pwa) {
base::TimeDelta duration = is_installed_pwa
? kPersistentPermissionExpirationTimeoutPWA
: kPersistentPermissionExpirationTimeoutNonPWA;
return (last_used + duration) < clock_->Now();
}
void ChromeFileSystemAccessPermissionContext::PermissionGrantDestroyed(
PermissionGrantImpl* grant) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = origins_.find(grant->origin());
if (it == origins_.end())
return;
auto& grants = grant->type() == GrantType::kRead ? it->second.read_grants
: it->second.write_grants;
auto grant_it = grants.find(grant->path());
// Any non-denied permission grants should have still been in our grants list.
// If this invariant is voilated we would have permissions that might be
// granted but won't be visible in any UI because the permission context isn't
// tracking them anymore.
if (grant_it == grants.end()) {
DCHECK_EQ(PermissionStatus::DENIED, grant->GetStatus());
return;
}
// The grant in |grants| for this path might have been replaced with a
// different grant. Only erase if it actually matches the grant that was
// destroyed.
if (grant_it->second == grant)
grants.erase(grant_it);
ScheduleUsageIconUpdate();
}
void ChromeFileSystemAccessPermissionContext::ScheduleUsageIconUpdate() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (usage_icon_update_scheduled_)
return;
usage_icon_update_scheduled_ = true;
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(
&ChromeFileSystemAccessPermissionContext::DoUsageIconUpdate,
weak_factory_.GetWeakPtr()));
}
void ChromeFileSystemAccessPermissionContext::DoUsageIconUpdate() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
usage_icon_update_scheduled_ = false;
#if !defined(OS_ANDROID)
for (Browser* browser : *BrowserList::GetInstance()) {
if (browser->profile() != profile())
continue;
browser->window()->UpdatePageActionIcon(
PageActionIconType::kFileSystemAccess);
}
#endif
}
base::WeakPtr<ChromeFileSystemAccessPermissionContext>
ChromeFileSystemAccessPermissionContext::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}