From 344aba0838d7ebdc648d0c160fff0ae095db8ac7 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 10 Apr 2024 22:06:47 +0200 Subject: [PATCH] feat: implement File System API support (#41419) --- chromium_src/BUILD.gn | 2 + docs/api/session.md | 9 +- .../filesystem-permission-request.md | 5 + .../media-access-permission-request.md | 5 + .../open-external-permission-request.md | 3 + docs/api/structures/permission-request.md | 4 + filenames.auto.gni | 4 + filenames.gni | 4 + patches/chromium/.patches | 1 + ..._expose_file_system_access_blocklist.patch | 303 +++++++ shell/browser/electron_browser_context.cc | 6 + shell/browser/electron_browser_context.h | 2 + .../file_system_access_permission_context.cc | 799 ++++++++++++++++++ .../file_system_access_permission_context.h | 154 ++++ ...ystem_access_permission_context_factory.cc | 51 ++ ...system_access_permission_context_factory.h | 42 + .../browser/web_contents_permission_helper.h | 4 +- shell/common/api/electron_api_clipboard.cc | 14 + shell/common/api/electron_api_clipboard.h | 3 + .../gin_converters/content_converter.cc | 2 + spec/chromium-spec.ts | 127 ++- spec/fixtures/file-system/test-writable.html | 26 + spec/fixtures/file-system/test.txt | 1 + 23 files changed, 1562 insertions(+), 9 deletions(-) create mode 100644 docs/api/structures/filesystem-permission-request.md create mode 100644 docs/api/structures/media-access-permission-request.md create mode 100644 docs/api/structures/open-external-permission-request.md create mode 100644 docs/api/structures/permission-request.md create mode 100644 patches/chromium/refactor_expose_file_system_access_blocklist.patch create mode 100644 shell/browser/file_system_access/file_system_access_permission_context.cc create mode 100644 shell/browser/file_system_access/file_system_access_permission_context.h create mode 100644 shell/browser/file_system_access/file_system_access_permission_context_factory.cc create mode 100644 shell/browser/file_system_access/file_system_access_permission_context_factory.h create mode 100644 spec/fixtures/file-system/test-writable.html create mode 100644 spec/fixtures/file-system/test.txt diff --git a/chromium_src/BUILD.gn b/chromium_src/BUILD.gn index 3ce8735e56..0f23d78198 100644 --- a/chromium_src/BUILD.gn +++ b/chromium_src/BUILD.gn @@ -33,6 +33,8 @@ static_library("chrome") { "//chrome/browser/devtools/visual_logging.h", "//chrome/browser/extensions/global_shortcut_listener.cc", "//chrome/browser/extensions/global_shortcut_listener.h", + "//chrome/browser/file_system_access/file_system_access_features.cc", + "//chrome/browser/file_system_access/file_system_access_features.h", "//chrome/browser/icon_loader.cc", "//chrome/browser/icon_loader.h", "//chrome/browser/icon_manager.cc", diff --git a/docs/api/session.md b/docs/api/session.md index 7b24a22724..d0acb12181 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -818,15 +818,10 @@ win.webContents.session.setCertificateVerifyProc((request, callback) => { * `top-level-storage-access` - Allow top-level sites to request third-party cookie access on behalf of embedded content originating from another site in the same related website set using the [Storage Access API](https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API). * `window-management` - Request access to enumerate screens using the [`getScreenDetails`](https://developer.chrome.com/en/articles/multi-screen-window-placement/) API. * `unknown` - An unrecognized permission request. + * `fileSystem` - Request access to read, write, and file management capabilities using the [File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API). * `callback` Function * `permissionGranted` boolean - Allow or deny the permission. - * `details` Object - Some properties are only available on certain permission types. - * `externalURL` string (optional) - The url of the `openExternal` request. - * `securityOrigin` string (optional) - The security origin of the `media` request. - * `mediaTypes` string[] (optional) - The types of media access being requested, elements can be `video` - or `audio` - * `requestingUrl` string - The last URL the requesting frame loaded - * `isMainFrame` boolean - Whether the frame making the request is the main frame + * `details` [PermissionRequest](structures/permission-request.md) | [FilesystemPermissionRequest](structures/filesystem-permission-request.md) | [MediaAccessPermissionRequest](structures/media-access-permission-request.md) | [OpenExternalPermissionRequest](structures/open-external-permission-request.md) - Additional information about the permission being requested. Sets the handler which can be used to respond to permission requests for the `session`. Calling `callback(true)` will allow the permission and `callback(false)` will reject it. diff --git a/docs/api/structures/filesystem-permission-request.md b/docs/api/structures/filesystem-permission-request.md new file mode 100644 index 0000000000..8d813708ca --- /dev/null +++ b/docs/api/structures/filesystem-permission-request.md @@ -0,0 +1,5 @@ +# FilesystemPermissionRequest Object extends `PermissionRequest` + +* `filePath` string (optional) - The path of the `fileSystem` request. +* `isDirectory` boolean (optional) - Whether the `fileSystem` request is a directory. +* `fileAccessType` string (optional) - The access type of the `fileSystem` request. Can be `writable` or `readable`. diff --git a/docs/api/structures/media-access-permission-request.md b/docs/api/structures/media-access-permission-request.md new file mode 100644 index 0000000000..6a9cca661a --- /dev/null +++ b/docs/api/structures/media-access-permission-request.md @@ -0,0 +1,5 @@ +# MediaAccessPermissionRequest Object extends `PermissionRequest` + +* `securityOrigin` string (optional) - The security origin of the request. +* `mediaTypes` string[] (optional) - The types of media access being requested - elements can be `video` + or `audio`. diff --git a/docs/api/structures/open-external-permission-request.md b/docs/api/structures/open-external-permission-request.md new file mode 100644 index 0000000000..d7b7f68fc9 --- /dev/null +++ b/docs/api/structures/open-external-permission-request.md @@ -0,0 +1,3 @@ +# OpenExternalPermissionRequest Object extends `PermissionRequest` + +* `externalURL` string (optional) - The url of the `openExternal` request. diff --git a/docs/api/structures/permission-request.md b/docs/api/structures/permission-request.md new file mode 100644 index 0000000000..4858f84a32 --- /dev/null +++ b/docs/api/structures/permission-request.md @@ -0,0 +1,4 @@ +# PermissionRequest Object + +* `requestingUrl` string - The last URL the requesting frame loaded. +* `isMainFrame` boolean - Whether the frame making the request is the main frame. diff --git a/filenames.auto.gni b/filenames.auto.gni index 000573868c..3c32cc4161 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -89,6 +89,7 @@ auto_filenames = { "docs/api/structures/extension.md", "docs/api/structures/file-filter.md", "docs/api/structures/file-path-with-headers.md", + "docs/api/structures/filesystem-permission-request.md", "docs/api/structures/gpu-feature-status.md", "docs/api/structures/hid-device.md", "docs/api/structures/input-event.md", @@ -99,6 +100,7 @@ auto_filenames = { "docs/api/structures/jump-list-item.md", "docs/api/structures/keyboard-event.md", "docs/api/structures/keyboard-input-event.md", + "docs/api/structures/media-access-permission-request.md", "docs/api/structures/memory-info.md", "docs/api/structures/memory-usage-details.md", "docs/api/structures/mime-typed-buffer.md", @@ -106,7 +108,9 @@ auto_filenames = { "docs/api/structures/mouse-wheel-input-event.md", "docs/api/structures/notification-action.md", "docs/api/structures/notification-response.md", + "docs/api/structures/open-external-permission-request.md", "docs/api/structures/payment-discount.md", + "docs/api/structures/permission-request.md", "docs/api/structures/point.md", "docs/api/structures/post-body.md", "docs/api/structures/printer-info.md", diff --git a/filenames.gni b/filenames.gni index c7bb2f8389..dbc912b97e 100644 --- a/filenames.gni +++ b/filenames.gni @@ -380,6 +380,10 @@ filenames = { "shell/browser/file_select_helper.cc", "shell/browser/file_select_helper.h", "shell/browser/file_select_helper_mac.mm", + "shell/browser/file_system_access/file_system_access_permission_context.cc", + "shell/browser/file_system_access/file_system_access_permission_context.h", + "shell/browser/file_system_access/file_system_access_permission_context_factory.cc", + "shell/browser/file_system_access/file_system_access_permission_context_factory.h", "shell/browser/font_defaults.cc", "shell/browser/font_defaults.h", "shell/browser/hid/electron_hid_delegate.cc", diff --git a/patches/chromium/.patches b/patches/chromium/.patches index ee985721f7..3a3bd94ad1 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -129,3 +129,4 @@ build_run_reclient_cfg_generator_after_chrome.patch fix_suppress_clang_-wimplicit-const-int-float-conversion_in.patch fix_getcursorscreenpoint_wrongly_returns_0_0.patch fix_add_support_for_skipping_first_2_no-op_refreshes_in_thumb_cap.patch +refactor_expose_file_system_access_blocklist.patch diff --git a/patches/chromium/refactor_expose_file_system_access_blocklist.patch b/patches/chromium/refactor_expose_file_system_access_blocklist.patch new file mode 100644 index 0000000000..eaedc5b778 --- /dev/null +++ b/patches/chromium/refactor_expose_file_system_access_blocklist.patch @@ -0,0 +1,303 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shelley Vohr +Date: Wed, 27 Mar 2024 10:47:48 +0100 +Subject: refactor: expose file system access blocklist + +This CL exposes the file system access blocklist publicly so that we can leverage +it in Electron and prevent drift from Chrome's blocklist. We should look for a way +to upstream this change to Chrome. + +diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc +index 9c644d678d6d811ae5679594c0574fc0d8607f62..792cd62da17239ca6933930880af23754e4ab3d3 100644 +--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc ++++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc +@@ -38,7 +38,6 @@ + #include "chrome/browser/profiles/profile_manager.h" + #include "chrome/browser/safe_browsing/download_protection/download_protection_util.h" + #include "chrome/browser/ui/file_system_access_dialogs.h" +-#include "chrome/common/chrome_paths.h" + #include "chrome/common/pdf_util.h" + #include "chrome/grit/generated_resources.h" + #include "components/content_settings/core/browser/host_content_settings_map.h" +@@ -222,121 +221,6 @@ bool MaybeIsLocalUNCPath(const base::FilePath& path) { + } + #endif + +-// 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. +- {base::DIR_EXE, nullptr, kBlockAllChildren}, +-#if !BUILDFLAG(IS_FUCHSIA) +- {base::DIR_MODULE, nullptr, kBlockAllChildren}, +-#endif +- {base::DIR_ASSETS, 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 BUILDFLAG(IS_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_ROAMING_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 BUILDFLAG(IS_MAC) +- // Similar Mac specific blocks. +- {base::DIR_APP_DATA, nullptr, kBlockAllChildren}, +- {base::DIR_HOME, FILE_PATH_LITERAL("Library"), kBlockAllChildren}, +- // Allow access to other cloud files, such as Google Drive. +- {base::DIR_HOME, FILE_PATH_LITERAL("Library/CloudStorage"), +- kDontBlockChildren}, +- // Allow the site to interact with data from its corresponding natively +- // installed (sandboxed) application. It would be nice to limit a site to +- // access only _its_ corresponding natively installed application, +- // but unfortunately there's no straightforward way to do that. See +- // https://crbug.com/984641#c22. +- {base::DIR_HOME, FILE_PATH_LITERAL("Library/Containers"), +- kDontBlockChildren}, +- // Allow access to iCloud files... +- {base::DIR_HOME, FILE_PATH_LITERAL("Library/Mobile Documents"), +- kDontBlockChildren}, +- // ... which may also appear at this directory. +- {base::DIR_HOME, +- FILE_PATH_LITERAL("Library/Mobile Documents/com~apple~CloudDocs"), +- kDontBlockChildren}, +-#endif +-#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) +- // On Linux also block access to devices via /dev. +- {kNoBasePathKey, FILE_PATH_LITERAL("/dev"), kBlockAllChildren}, +- // And security sensitive data in /proc and /sys. +- {kNoBasePathKey, FILE_PATH_LITERAL("/proc"), kBlockAllChildren}, +- {kNoBasePathKey, FILE_PATH_LITERAL("/sys"), kBlockAllChildren}, +- // And system files in /boot and /etc. +- {kNoBasePathKey, FILE_PATH_LITERAL("/boot"), kBlockAllChildren}, +- {kNoBasePathKey, FILE_PATH_LITERAL("/etc"), 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? +-}; +- + // Describes a rule for blocking a directory, which can be constructed + // dynamically (based on state) or statically (from kBlockedPaths). + struct BlockPathRule { +diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h +index 8bc8257b603a88e56f77dcf7d72aa9dad45880db..484f98c68b0dc860a6482e923df2379133c57749 100644 +--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h ++++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h +@@ -17,12 +17,13 @@ + #include "base/time/default_clock.h" + #include "chrome/browser/file_system_access/file_system_access_features.h" + #include "chrome/browser/file_system_access/file_system_access_permission_request_manager.h" +-#include "components/enterprise/buildflags/buildflags.h" ++#include "chrome/common/chrome_paths.h" + #include "components/permissions/features.h" + #include "components/permissions/object_permission_context_base.h" + #include "content/public/browser/file_system_access_permission_context.h" + #include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom-forward.h" + ++ + #if !BUILDFLAG(IS_ANDROID) + #include "chrome/browser/permissions/one_time_permissions_tracker.h" + #include "chrome/browser/permissions/one_time_permissions_tracker_observer.h" +@@ -30,7 +31,8 @@ + #include "chrome/browser/web_applications/web_app_install_manager_observer.h" + #endif + +-#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS) ++#if 0 ++#include "components/enterprise/buildflags/buildflags.h" + #include "chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h" + #include "components/enterprise/common/files_scan_data.h" + #endif +@@ -331,6 +333,121 @@ class ChromeFileSystemAccessPermissionContext + // chrome://settings/content/filesystem UI. + static constexpr char kPermissionPathKey[] = "path"; + ++ // Sentinel used to indicate that no PathService key is specified for a path in ++ // the struct below. ++ static constexpr int kNoBasePathKey = -1; ++ ++ enum BlockType { ++ kBlockAllChildren, ++ kBlockNestedDirectories, ++ kDontBlockChildren ++ }; ++ ++ static constexpr 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. ++ {base::DIR_EXE, nullptr, kBlockAllChildren}, ++ #if !BUILDFLAG(IS_FUCHSIA) ++ {base::DIR_MODULE, nullptr, kBlockAllChildren}, ++ #endif ++ {base::DIR_ASSETS, 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 BUILDFLAG(IS_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_ROAMING_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 BUILDFLAG(IS_MAC) ++ // Similar Mac specific blocks. ++ {base::DIR_APP_DATA, nullptr, kBlockAllChildren}, ++ {base::DIR_HOME, FILE_PATH_LITERAL("Library"), kBlockAllChildren}, ++ // Allow access to other cloud files, such as Google Drive. ++ {base::DIR_HOME, FILE_PATH_LITERAL("Library/CloudStorage"), ++ kDontBlockChildren}, ++ // Allow the site to interact with data from its corresponding natively ++ // installed (sandboxed) application. It would be nice to limit a site to ++ // access only _its_ corresponding natively installed application, ++ // but unfortunately there's no straightforward way to do that. See ++ // https://crbug.com/984641#c22. ++ {base::DIR_HOME, FILE_PATH_LITERAL("Library/Containers"), ++ kDontBlockChildren}, ++ // Allow access to iCloud files... ++ {base::DIR_HOME, FILE_PATH_LITERAL("Library/Mobile Documents"), ++ kDontBlockChildren}, ++ // ... which may also appear at this directory. ++ {base::DIR_HOME, ++ FILE_PATH_LITERAL("Library/Mobile Documents/com~apple~CloudDocs"), ++ kDontBlockChildren}, ++ #endif ++ #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) ++ // On Linux also block access to devices via /dev. ++ {kNoBasePathKey, FILE_PATH_LITERAL("/dev"), kBlockAllChildren}, ++ // And security sensitive data in /proc and /sys. ++ {kNoBasePathKey, FILE_PATH_LITERAL("/proc"), kBlockAllChildren}, ++ {kNoBasePathKey, FILE_PATH_LITERAL("/sys"), kBlockAllChildren}, ++ // And system files in /boot and /etc. ++ {kNoBasePathKey, FILE_PATH_LITERAL("/boot"), kBlockAllChildren}, ++ {kNoBasePathKey, FILE_PATH_LITERAL("/etc"), 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? ++ }; ++ + protected: + SEQUENCE_CHECKER(sequence_checker_); + +@@ -350,7 +467,7 @@ class ChromeFileSystemAccessPermissionContext + + void PermissionGrantDestroyed(PermissionGrantImpl* grant); + +-#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS) ++#if 0 + void OnContentAnalysisComplete( + std::vector entries, + EntriesAllowedByEnterprisePolicyCallback callback, diff --git a/shell/browser/electron_browser_context.cc b/shell/browser/electron_browser_context.cc index 4045070de1..e9d124f9e1 100644 --- a/shell/browser/electron_browser_context.cc +++ b/shell/browser/electron_browser_context.cc @@ -45,6 +45,7 @@ #include "shell/browser/electron_browser_main_parts.h" #include "shell/browser/electron_download_manager_delegate.h" #include "shell/browser/electron_permission_manager.h" +#include "shell/browser/file_system_access/file_system_access_permission_context_factory.h" #include "shell/browser/net/resolve_proxy_helper.h" #include "shell/browser/protocol_registry.h" #include "shell/browser/special_storage_policy.h" @@ -533,6 +534,11 @@ ElectronBrowserContext::GetReduceAcceptLanguageControllerDelegate() { return nullptr; } +content::FileSystemAccessPermissionContext* +ElectronBrowserContext::GetFileSystemAccessPermissionContext() { + return FileSystemAccessPermissionContextFactory::GetForBrowserContext(this); +} + ResolveProxyHelper* ElectronBrowserContext::GetResolveProxyHelper() { if (!resolve_proxy_helper_) { resolve_proxy_helper_ = base::MakeRefCounted( diff --git a/shell/browser/electron_browser_context.h b/shell/browser/electron_browser_context.h index 0a47519402..3002ebeb4c 100644 --- a/shell/browser/electron_browser_context.h +++ b/shell/browser/electron_browser_context.h @@ -150,6 +150,8 @@ class ElectronBrowserContext : public content::BrowserContext { content::StorageNotificationService* GetStorageNotificationService() override; content::ReduceAcceptLanguageControllerDelegate* GetReduceAcceptLanguageControllerDelegate() override; + content::FileSystemAccessPermissionContext* + GetFileSystemAccessPermissionContext() override; CookieChangeNotifier* cookie_change_notifier() const { return cookie_change_notifier_.get(); diff --git a/shell/browser/file_system_access/file_system_access_permission_context.cc b/shell/browser/file_system_access/file_system_access_permission_context.cc new file mode 100644 index 0000000000..42bd41c1bd --- /dev/null +++ b/shell/browser/file_system_access/file_system_access_permission_context.cc @@ -0,0 +1,799 @@ +// Copyright (c) 2024 Microsoft, GmbH +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/file_system_access/file_system_access_permission_context.h" + +#include +#include + +#include "base/base_paths.h" +#include "base/files/file_path.h" +#include "base/json/values_util.h" +#include "base/path_service.h" +#include "base/task/thread_pool.h" +#include "base/values.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h" // nogncheck +#include "chrome/browser/file_system_access/file_system_access_features.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/disallow_activation_reason.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 "shell/browser/electron_permission_manager.h" +#include "shell/browser/web_contents_permission_helper.h" +#include "shell/common/gin_converters/file_path_converter.h" +#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/origin.h" + +namespace { + +using BlockType = ChromeFileSystemAccessPermissionContext::BlockType; +using HandleType = content::FileSystemAccessPermissionContext::HandleType; +using GrantType = electron::FileSystemAccessPermissionContext::GrantType; +using blink::mojom::PermissionStatus; + +#if BUILDFLAG(IS_WIN) +[[nodiscard]] constexpr bool ContainsInvalidDNSCharacter( + base::FilePath::StringType hostname) { + return !base::ranges::all_of(hostname, [](base::FilePath::CharType c) { + return (c >= L'A' && c <= L'Z') || (c >= L'a' && c <= L'z') || + (c >= L'0' && c <= L'9') || (c == L'.') || (c == L'-'); + }); +} + +bool MaybeIsLocalUNCPath(const base::FilePath& path) { + if (!path.IsNetwork()) { + return false; + } + + const std::vector components = + path.GetComponents(); + + // Check for server name that could represent a local system. We only + // check for a very short list, as it is impossible to cover all different + // variants on Windows. + if (components.size() >= 2 && + (base::FilePath::CompareEqualIgnoreCase(components[1], + FILE_PATH_LITERAL("localhost")) || + components[1] == FILE_PATH_LITERAL("127.0.0.1") || + components[1] == FILE_PATH_LITERAL(".") || + components[1] == FILE_PATH_LITERAL("?") || + ContainsInvalidDNSCharacter(components[1]))) { + return true; + } + + // In case we missed the server name check above, we also check for shares + // ending with '$' as they represent pre-defined shares, including the local + // drives. + for (size_t i = 2; i < components.size(); ++i) { + if (components[i].back() == L'$') { + return true; + } + } + + return false; +} +#endif + +// Describes a rule for blocking a directory, which can be constructed +// dynamically (based on state) or statically (from kBlockedPaths). +struct BlockPathRule { + base::FilePath path; + BlockType type; +}; + +bool ShouldBlockAccessToPath(const base::FilePath& path, + HandleType handle_type, + std::vector rules) { + DCHECK(!path.empty()); + DCHECK(path.IsAbsolute()); + +#if BUILDFLAG(IS_WIN) + // On Windows, local UNC paths are rejected, as UNC path can be written in a + // way that can bypass the blocklist. + if (base::FeatureList::IsEnabled( + features::kFileSystemAccessLocalUNCPathBlock) && + MaybeIsLocalUNCPath(path)) { + return true; + } +#endif + + // Add the hard-coded rules to the dynamic rules. + for (auto const& [key, rule_path, type] : + ChromeFileSystemAccessPermissionContext::kBlockedPaths) { + if (key == ChromeFileSystemAccessPermissionContext::kNoBasePathKey) { + rules.emplace_back(base::FilePath{rule_path}, type); + } else if (base::FilePath path; base::PathService::Get(key, &path)) { + rules.emplace_back(rule_path ? path.Append(rule_path) : path, type); + } + } + + base::FilePath nearest_ancestor; + BlockType nearest_ancestor_block_type = BlockType::kDontBlockChildren; + for (const auto& block : rules) { + if (path == block.path || path.IsParent(block.path)) { + DLOG(INFO) << "Blocking access to " << path + << " because it is a parent of " << block.path; + return true; + } + + if (block.path.IsParent(path) && + (nearest_ancestor.empty() || nearest_ancestor.IsParent(block.path))) { + nearest_ancestor = block.path; + 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 == BlockType::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 == BlockType::kBlockNestedDirectories) { + return false; + } + + // The nearest ancestor blocks access to its children, so block access. + DLOG(INFO) << "Blocking access to " << path << " because it is inside " + << nearest_ancestor; + return true; +} + +} // namespace + +namespace electron { + +class FileSystemAccessPermissionContext::PermissionGrantImpl + : public content::FileSystemAccessPermissionGrant { + public: + PermissionGrantImpl(base::WeakPtr context, + const url::Origin& origin, + const base::FilePath& path, + HandleType handle_type, + GrantType type, + UserAction user_action) + : context_{std::move(context)}, + origin_{origin}, + handle_type_{handle_type}, + type_{type}, + path_{path} {} + + // 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 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); + } + std::move(callback).Run(PermissionRequestOutcome::kRequestAborted); + return; + } + + content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id); + if (!rfh) { + // Requested from a no longer valid RenderFrameHost. + std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame); + return; + } + + // Don't request permission for an inactive RenderFrameHost as the + // page might not distinguish properly between user denying the permission + // and automatic rejection. + if (rfh->IsInactiveAndDisallowActivation( + content::DisallowActivationReasonId:: + kFileSystemAccessPermissionRequest)) { + std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame); + return; + } + + // We don't allow file system access from fenced frames. + if (rfh->IsNestedWithinFencedFrame()) { + std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame); + return; + } + + if (user_activation_state == UserActivationState::kRequired && + !rfh->HasTransientUserActivation()) { + // No permission prompts without user activation. + std::move(callback).Run(PermissionRequestOutcome::kNoUserActivation); + return; + } + + if (content::WebContents::FromRenderFrameHost(rfh) == nullptr) { + std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame); + return; + } + + auto origin = rfh->GetLastCommittedOrigin().GetURL(); + if (url::Origin::Create(origin) != origin_) { + // Third party iframes are not allowed to request more permissions. + std::move(callback).Run(PermissionRequestOutcome::kThirdPartyContext); + return; + } + + auto* permission_manager = + static_cast( + context_->browser_context()->GetPermissionControllerDelegate()); + if (!permission_manager) { + std::move(callback).Run(PermissionRequestOutcome::kRequestAborted); + return; + } + + blink::PermissionType type = static_cast( + electron::WebContentsPermissionHelper::PermissionType::FILE_SYSTEM); + + base::Value::Dict details; + details.Set("filePath", base::FilePathToValue(path_)); + details.Set("isDirectory", handle_type_ == HandleType::kDirectory); + details.Set("fileAccessType", + type_ == GrantType::kWrite ? "writable" : "readable"); + + permission_manager->RequestPermissionWithDetails( + type, rfh, origin, false, std::move(details), + base::BindOnce(&PermissionGrantImpl::OnPermissionRequestResult, this, + std::move(callback))); + } + + 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_; + } + + GrantType type() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return type_; + } + + void SetStatus(PermissionStatus new_status) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + auto permission_changed = status_ != new_status; + status_ = new_status; + + if (permission_changed) { + NotifyPermissionStatusChanged(); + } + } + + static void UpdateGrantPath( + std::map& grants, + const base::FilePath& old_path, + const base::FilePath& new_path) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + auto entry_it = base::ranges::find_if( + grants, + [&old_path](const auto& entry) { return entry.first == old_path; }); + + if (entry_it == grants.end()) { + // There must be an entry for an ancestor of this entry. Nothing to do + // here. + return; + } + + DCHECK_EQ(entry_it->second->GetStatus(), PermissionStatus::GRANTED); + + auto* const grant_impl = entry_it->second; + grant_impl->SetPath(new_path); + + // Update the permission grant's key in the map of active permissions. + grants.erase(entry_it); + grants.emplace(new_path, grant_impl); + } + + protected: + ~PermissionGrantImpl() override { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (context_) { + context_->PermissionGrantDestroyed(this); + } + } + + private: + void OnPermissionRequestResult( + base::OnceCallback callback, + blink::mojom::PermissionStatus status) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (status == blink::mojom::PermissionStatus::GRANTED) { + SetStatus(PermissionStatus::GRANTED); + std::move(callback).Run(PermissionRequestOutcome::kUserGranted); + } else { + SetStatus(PermissionStatus::DENIED); + std::move(callback).Run(PermissionRequestOutcome::kUserDenied); + } + } + + void SetPath(const base::FilePath& new_path) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (path_ == new_path) + return; + + path_ = new_path; + NotifyPermissionStatusChanged(); + } + + SEQUENCE_CHECKER(sequence_checker_); + + base::WeakPtr const context_; + const url::Origin origin_; + const HandleType handle_type_; + const GrantType type_; + base::FilePath path_; + + // This member should only be updated via SetStatus(). + PermissionStatus status_ = PermissionStatus::ASK; +}; + +struct FileSystemAccessPermissionContext::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 read_grants; + std::map write_grants; +}; + +FileSystemAccessPermissionContext::FileSystemAccessPermissionContext( + content::BrowserContext* browser_context) + : browser_context_(browser_context) { + DETACH_FROM_SEQUENCE(sequence_checker_); +} + +FileSystemAccessPermissionContext::~FileSystemAccessPermissionContext() = + default; + +scoped_refptr +FileSystemAccessPermissionContext::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 |active_permissions_map_|, + // but that is exactly what we want. + auto& origin_state = active_permissions_map_[origin]; + auto*& existing_grant = origin_state.read_grants[path]; + scoped_refptr 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); + existing_grant = nullptr; + } + + if (!existing_grant) { + new_grant = base::MakeRefCounted( + weak_factory_.GetWeakPtr(), origin, path, handle_type, GrantType::kRead, + user_action); + existing_grant = new_grant.get(); + } + + // 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); + } else { + 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); + break; + case UserAction::kLoadFromStorage: + case UserAction::kNone: + break; + } + } + + return existing_grant; +} + +scoped_refptr +FileSystemAccessPermissionContext::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 |active_permissions_map_|, + // but that is exactly what we want. + auto& origin_state = active_permissions_map_[origin]; + auto*& existing_grant = origin_state.write_grants[path]; + scoped_refptr 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); + existing_grant = nullptr; + } + + if (!existing_grant) { + new_grant = base::MakeRefCounted( + weak_factory_.GetWeakPtr(), origin, path, handle_type, + GrantType::kWrite, user_action); + existing_grant = new_grant.get(); + } + + // 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); + } else { + switch (user_action) { + case UserAction::kSave: + // Only automatically grant write access for save dialogs. + existing_grant->SetStatus(PermissionStatus::GRANTED); + break; + case UserAction::kOpen: + case UserAction::kDragAndDrop: + case UserAction::kLoadFromStorage: + case UserAction::kNone: + break; + } + } + + return existing_grant; +} + +bool FileSystemAccessPermissionContext::CanObtainReadPermission( + const url::Origin& origin) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return true; +} + +bool FileSystemAccessPermissionContext::CanObtainWritePermission( + const url::Origin& origin) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return true; +} + +void FileSystemAccessPermissionContext::ConfirmSensitiveEntryAccess( + const url::Origin& origin, + PathType path_type, + const base::FilePath& path, + HandleType handle_type, + UserAction user_action, + content::GlobalRenderFrameHostId frame_id, + base::OnceCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + auto after_blocklist_check_callback = base::BindOnce( + &FileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist, + GetWeakPtr(), origin, path, handle_type, user_action, frame_id, + std::move(callback)); + CheckPathAgainstBlocklist(path_type, path, handle_type, + std::move(after_blocklist_check_callback)); +} + +void FileSystemAccessPermissionContext::CheckPathAgainstBlocklist( + PathType path_type, + const base::FilePath& path, + HandleType handle_type, + base::OnceCallback 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(/*should_block=*/false); + return; + } + + std::vector extra_rules; + extra_rules.emplace_back(browser_context_->GetPath().DirName(), + BlockType::kBlockAllChildren); + + base::ThreadPool::PostTaskAndReplyWithResult( + FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE}, + base::BindOnce(&ShouldBlockAccessToPath, path, handle_type, extra_rules), + std::move(callback)); +} + +void FileSystemAccessPermissionContext::PerformAfterWriteChecks( + std::unique_ptr item, + content::GlobalRenderFrameHostId frame_id, + base::OnceCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + std::move(callback).Run(AfterWriteCheckResult::kAllow); +} + +void FileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist( + const url::Origin& origin, + const base::FilePath& path, + HandleType handle_type, + UserAction user_action, + content::GlobalRenderFrameHostId frame_id, + base::OnceCallback callback, + bool should_block) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (user_action == UserAction::kNone) { + std::move(callback).Run(should_block ? SensitiveEntryResult::kAbort + : SensitiveEntryResult::kAllowed); + return; + } + + // Chromium opens a dialog here, but in Electron's case we log and abort. + if (should_block) { + LOG(INFO) << path.value() + << " is blocked by the blocklis and cannot be accessed"; + std::move(callback).Run(SensitiveEntryResult::kAbort); + return; + } + + std::move(callback).Run(SensitiveEntryResult::kAllowed); +} + +void FileSystemAccessPermissionContext::SetLastPickedDirectory( + const url::Origin& origin, + const std::string& id, + const base::FilePath& path, + const PathType type) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + LOG(INFO) << "NOTIMPLEMENTED SetLastPickedDirectory: " << path.value(); +} + +FileSystemAccessPermissionContext::PathInfo +FileSystemAccessPermissionContext::GetLastPickedDirectory( + const url::Origin& origin, + const std::string& id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + LOG(INFO) << "NOTIMPLEMENTED GetLastPickedDirectory"; + return PathInfo(); +} + +base::FilePath FileSystemAccessPermissionContext::GetWellKnownDirectoryPath( + blink::mojom::WellKnownDirectory directory, + const url::Origin& origin) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + int key = base::PATH_START; + switch (directory) { + 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; +} + +std::u16string FileSystemAccessPermissionContext::GetPickerTitle( + const blink::mojom::FilePickerOptionsPtr& options) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // TODO(asully): Consider adding custom strings for invocations of the file + // picker, as well. Returning the empty string will fall back to the platform + // default for the given picker type. + std::u16string title; + switch (options->type_specific_options->which()) { + case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag:: + kDirectoryPickerOptions: + title = l10n_util::GetStringUTF16( + options->type_specific_options->get_directory_picker_options() + ->request_writable + ? IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_WRITABLE_DIRECTORY_TITLE + : IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_READABLE_DIRECTORY_TITLE); + break; + case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag:: + kSaveFilePickerOptions: + title = l10n_util::GetStringUTF16( + IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_SAVE_FILE_TITLE); + break; + case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag:: + kOpenFilePickerOptions: + break; + } + return title; +} + +void FileSystemAccessPermissionContext::NotifyEntryMoved( + const url::Origin& origin, + const base::FilePath& old_path, + const base::FilePath& new_path) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (old_path == new_path) { + return; + } + + auto it = active_permissions_map_.find(origin); + if (it != active_permissions_map_.end()) { + PermissionGrantImpl::UpdateGrantPath(it->second.write_grants, old_path, + new_path); + PermissionGrantImpl::UpdateGrantPath(it->second.read_grants, old_path, + new_path); + } +} + +void FileSystemAccessPermissionContext::OnFileCreatedFromShowSaveFilePicker( + const GURL& file_picker_binding_context, + const storage::FileSystemURL& url) {} + +void FileSystemAccessPermissionContext::CheckPathsAgainstEnterprisePolicy( + std::vector entries, + content::GlobalRenderFrameHostId frame_id, + EntriesAllowedByEnterprisePolicyCallback callback) { + std::move(callback).Run(std::move(entries)); +} + +void FileSystemAccessPermissionContext::RevokeGrant( + const url::Origin& origin, + const base::FilePath& file_path) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + auto origin_it = active_permissions_map_.find(origin); + if (origin_it != active_permissions_map_.end()) { + OriginState& origin_state = origin_it->second; + for (auto& grant : origin_state.read_grants) { + if (file_path.empty() || grant.first == file_path) { + grant.second->SetStatus(PermissionStatus::ASK); + } + } + for (auto& grant : origin_state.write_grants) { + if (file_path.empty() || grant.first == file_path) { + grant.second->SetStatus(PermissionStatus::ASK); + } + } + } +} + +bool FileSystemAccessPermissionContext::OriginHasReadAccess( + const url::Origin& origin) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + auto it = active_permissions_map_.find(origin); + if (it != active_permissions_map_.end()) { + return base::ranges::any_of(it->second.read_grants, [&](const auto& grant) { + return grant.second->GetStatus() == PermissionStatus::GRANTED; + }); + } + + return false; +} + +bool FileSystemAccessPermissionContext::OriginHasWriteAccess( + const url::Origin& origin) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + auto it = active_permissions_map_.find(origin); + if (it != active_permissions_map_.end()) { + return base::ranges::any_of( + it->second.write_grants, [&](const auto& grant) { + return grant.second->GetStatus() == PermissionStatus::GRANTED; + }); + } + + return false; +} + +void FileSystemAccessPermissionContext::CleanupPermissions( + const url::Origin& origin) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + RevokeGrant(origin); +} + +bool FileSystemAccessPermissionContext::AncestorHasActivePermission( + const url::Origin& origin, + const base::FilePath& path, + GrantType grant_type) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + auto it = active_permissions_map_.find(origin); + if (it == active_permissions_map_.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; +} + +void FileSystemAccessPermissionContext::PermissionGrantDestroyed( + PermissionGrantImpl* grant) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + auto it = active_permissions_map_.find(grant->origin()); + if (it == active_permissions_map_.end()) { + return; + } + + auto& grants = grant->type() == GrantType::kRead ? it->second.read_grants + : it->second.write_grants; + auto grant_it = grants.find(grant->GetPath()); + // Any non-denied permission grants should have still been in our grants + // list. If this invariant is violated 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); + } +} + +base::WeakPtr +FileSystemAccessPermissionContext::GetWeakPtr() { + return weak_factory_.GetWeakPtr(); +} + +} // namespace electron diff --git a/shell/browser/file_system_access/file_system_access_permission_context.h b/shell/browser/file_system_access/file_system_access_permission_context.h new file mode 100644 index 0000000000..cd19bd1f70 --- /dev/null +++ b/shell/browser/file_system_access/file_system_access_permission_context.h @@ -0,0 +1,154 @@ +// Copyright (c) 2024 Microsoft, GmbH +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_ELECTRON_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_H_ +#define ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_ELECTRON_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_H_ + +#include "shell/browser/file_system_access/file_system_access_permission_context.h" + +#include +#include +#include + +#include "base/functional/callback.h" +#include "base/memory/weak_ptr.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/file_system_access_permission_context.h" + +class GURL; + +namespace base { +class FilePath; +} // namespace base + +namespace storage { +class FileSystemURL; +} // namespace storage + +namespace electron { + +class FileSystemAccessPermissionContext + : public KeyedService, + public content::FileSystemAccessPermissionContext { + public: + enum class GrantType { kRead, kWrite }; + + explicit FileSystemAccessPermissionContext( + content::BrowserContext* browser_context); + FileSystemAccessPermissionContext(const FileSystemAccessPermissionContext&) = + delete; + FileSystemAccessPermissionContext& operator=( + const FileSystemAccessPermissionContext&) = delete; + ~FileSystemAccessPermissionContext() override; + + // content::FileSystemAccessPermissionContext: + scoped_refptr + GetReadPermissionGrant(const url::Origin& origin, + const base::FilePath& path, + HandleType handle_type, + UserAction user_action) override; + + scoped_refptr + GetWritePermissionGrant(const url::Origin& origin, + const base::FilePath& path, + HandleType handle_type, + UserAction user_action) override; + + void ConfirmSensitiveEntryAccess( + const url::Origin& origin, + PathType path_type, + const base::FilePath& path, + HandleType handle_type, + UserAction user_action, + content::GlobalRenderFrameHostId frame_id, + base::OnceCallback callback) override; + + void PerformAfterWriteChecks( + std::unique_ptr item, + content::GlobalRenderFrameHostId frame_id, + base::OnceCallback callback) override; + + bool CanObtainReadPermission(const url::Origin& origin) override; + bool CanObtainWritePermission(const url::Origin& origin) override; + + void SetLastPickedDirectory(const url::Origin& origin, + const std::string& id, + const base::FilePath& path, + const PathType type) override; + + PathInfo GetLastPickedDirectory(const url::Origin& origin, + const std::string& id) override; + + base::FilePath GetWellKnownDirectoryPath( + blink::mojom::WellKnownDirectory directory, + const url::Origin& origin) override; + + std::u16string GetPickerTitle( + const blink::mojom::FilePickerOptionsPtr& options) override; + + void NotifyEntryMoved(const url::Origin& origin, + const base::FilePath& old_path, + const base::FilePath& new_path) override; + + void OnFileCreatedFromShowSaveFilePicker( + const GURL& file_picker_binding_context, + const storage::FileSystemURL& url) override; + + void CheckPathsAgainstEnterprisePolicy( + std::vector entries, + content::GlobalRenderFrameHostId frame_id, + EntriesAllowedByEnterprisePolicyCallback callback) override; + + enum class Access { kRead, kWrite, kReadWrite }; + + enum class RequestType { kNewPermission, kRestorePermissions }; + + void RevokeGrant(const url::Origin& origin, + const base::FilePath& file_path = base::FilePath()); + + bool OriginHasReadAccess(const url::Origin& origin); + bool OriginHasWriteAccess(const url::Origin& origin); + + content::BrowserContext* browser_context() const { return browser_context_; } + + protected: + SEQUENCE_CHECKER(sequence_checker_); + + private: + class PermissionGrantImpl; + + void PermissionGrantDestroyed(PermissionGrantImpl* grant); + + void CheckPathAgainstBlocklist(PathType path_type, + const base::FilePath& path, + HandleType handle_type, + base::OnceCallback callback); + void DidCheckPathAgainstBlocklist( + const url::Origin& origin, + const base::FilePath& path, + HandleType handle_type, + UserAction user_action, + content::GlobalRenderFrameHostId frame_id, + base::OnceCallback callback, + bool should_block); + + void CleanupPermissions(const url::Origin& origin); + + bool AncestorHasActivePermission(const url::Origin& origin, + const base::FilePath& path, + GrantType grant_type) const; + + base::WeakPtr GetWeakPtr(); + + const raw_ptr browser_context_; + + struct OriginState; + std::map active_permissions_map_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_H_ diff --git a/shell/browser/file_system_access/file_system_access_permission_context_factory.cc b/shell/browser/file_system_access/file_system_access_permission_context_factory.cc new file mode 100644 index 0000000000..ff553fc2dc --- /dev/null +++ b/shell/browser/file_system_access/file_system_access_permission_context_factory.cc @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Microsoft, GmbH +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/file_system_access/file_system_access_permission_context_factory.h" + +#include "base/functional/bind.h" +#include "base/memory/ptr_util.h" +#include "base/no_destructor.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "shell/browser/file_system_access/file_system_access_permission_context.h" + +namespace electron { + +// static +electron::FileSystemAccessPermissionContext* +FileSystemAccessPermissionContextFactory::GetForBrowserContext( + content::BrowserContext* context) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +// static +FileSystemAccessPermissionContextFactory* +FileSystemAccessPermissionContextFactory::GetInstance() { + static base::NoDestructor instance; + return instance.get(); +} + +FileSystemAccessPermissionContextFactory:: + FileSystemAccessPermissionContextFactory() + : BrowserContextKeyedServiceFactory( + "FileSystemAccessPermissionContext", + BrowserContextDependencyManager::GetInstance()) {} + +FileSystemAccessPermissionContextFactory:: + ~FileSystemAccessPermissionContextFactory() = default; + +// static +KeyedService* FileSystemAccessPermissionContextFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return BuildInstanceFor(context).release(); +} + +std::unique_ptr +FileSystemAccessPermissionContextFactory::BuildInstanceFor( + content::BrowserContext* context) { + return std::make_unique(context); +} + +} // namespace electron diff --git a/shell/browser/file_system_access/file_system_access_permission_context_factory.h b/shell/browser/file_system_access/file_system_access_permission_context_factory.h new file mode 100644 index 0000000000..b08fe99d02 --- /dev/null +++ b/shell/browser/file_system_access/file_system_access_permission_context_factory.h @@ -0,0 +1,42 @@ +// Copyright (c) 2024 Microsoft, GmbH +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_FACTORY_H_ +#define ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_FACTORY_H_ + +#include "base/no_destructor.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "shell/browser/file_system_access/file_system_access_permission_context.h" + +namespace electron { + +class FileSystemAccessPermissionContextFactory + : public BrowserContextKeyedServiceFactory { + public: + static FileSystemAccessPermissionContext* GetForBrowserContext( + content::BrowserContext* context); + static FileSystemAccessPermissionContextFactory* GetInstance(); + + static std::unique_ptr BuildInstanceFor( + content::BrowserContext* context); + + FileSystemAccessPermissionContextFactory( + const FileSystemAccessPermissionContextFactory&) = delete; + FileSystemAccessPermissionContextFactory& operator=( + const FileSystemAccessPermissionContextFactory&) = delete; + + private: + friend class base::NoDestructor; + + FileSystemAccessPermissionContextFactory(); + ~FileSystemAccessPermissionContextFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_FACTORY_H_ diff --git a/shell/browser/web_contents_permission_helper.h b/shell/browser/web_contents_permission_helper.h index 3e7571a79e..4dfd6a53f7 100644 --- a/shell/browser/web_contents_permission_helper.h +++ b/shell/browser/web_contents_permission_helper.h @@ -30,7 +30,9 @@ class WebContentsPermissionHelper OPEN_EXTERNAL, SERIAL, HID, - USB + USB, + KEYBOARD_LOCK, + FILE_SYSTEM }; // Asynchronous Requests diff --git a/shell/common/api/electron_api_clipboard.cc b/shell/common/api/electron_api_clipboard.cc index 9168559566..af03e7e99a 100644 --- a/shell/common/api/electron_api_clipboard.cc +++ b/shell/common/api/electron_api_clipboard.cc @@ -17,6 +17,7 @@ #include "third_party/skia/include/core/SkImageInfo.h" #include "third_party/skia/include/core/SkPixmap.h" #include "ui/base/clipboard/clipboard_format_type.h" +#include "ui/base/clipboard/file_info.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/gfx/codec/png_codec.h" @@ -274,6 +275,17 @@ void Clipboard::Clear(gin_helper::Arguments* args) { ui::Clipboard::GetForCurrentThread()->Clear(GetClipboardBuffer(args)); } +// This exists for testing purposes ONLY. +void Clipboard::WriteFilesForTesting(const std::vector& files) { + std::vector file_infos; + for (const auto& file : files) { + file_infos.emplace_back(ui::FileInfo(ui::FileInfo(file, file.BaseName()))); + } + + ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste); + writer.WriteFilenames(ui::FileInfosToURIList(file_infos)); +} + } // namespace electron::api namespace { @@ -302,6 +314,8 @@ void Initialize(v8::Local exports, dict.SetMethod("writeFindText", &electron::api::Clipboard::WriteFindText); dict.SetMethod("readBuffer", &electron::api::Clipboard::ReadBuffer); dict.SetMethod("writeBuffer", &electron::api::Clipboard::WriteBuffer); + dict.SetMethod("_writeFilesForTesting", + &electron::api::Clipboard::WriteFilesForTesting); dict.SetMethod("clear", &electron::api::Clipboard::Clear); } diff --git a/shell/common/api/electron_api_clipboard.h b/shell/common/api/electron_api_clipboard.h index b0fa62c3b6..3abe0895a9 100644 --- a/shell/common/api/electron_api_clipboard.h +++ b/shell/common/api/electron_api_clipboard.h @@ -8,6 +8,7 @@ #include #include +#include "shell/common/gin_converters/file_path_converter.h" #include "ui/base/clipboard/clipboard.h" #include "ui/gfx/image/image.h" #include "v8/include/v8.h" @@ -63,6 +64,8 @@ class Clipboard { static void WriteBuffer(const std::string& format_string, const v8::Local buffer, gin_helper::Arguments* args); + + static void WriteFilesForTesting(const std::vector& files); }; } // namespace electron::api diff --git a/shell/common/gin_converters/content_converter.cc b/shell/common/gin_converters/content_converter.cc index 46499cf016..774696a196 100644 --- a/shell/common/gin_converters/content_converter.cc +++ b/shell/common/gin_converters/content_converter.cc @@ -229,6 +229,8 @@ v8::Local Converter::ToV8( return StringToV8(isolate, "hid"); case PermissionType::USB: return StringToV8(isolate, "usb"); + case PermissionType::FILE_SYSTEM: + return StringToV8(isolate, "fileSystem"); default: return StringToV8(isolate, "unknown"); } diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts index 8cfc735e1d..7a549f764a 100644 --- a/spec/chromium-spec.ts +++ b/spec/chromium-spec.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents, dialog, MessageBoxOptions } from 'electron/main'; +import { clipboard } from 'electron/common'; import { closeAllWindows } from './lib/window-helpers'; import * as https from 'node:https'; import * as http from 'node:http'; @@ -13,6 +14,7 @@ import { PipeTransport } from './pipe-transport'; import * as ws from 'ws'; import { setTimeout } from 'node:timers/promises'; import { AddressInfo } from 'node:net'; +import { MediaAccessPermissionRequest } from 'electron'; const features = process._linkedBinding('electron_common_features'); @@ -846,6 +848,129 @@ describe('chromium features', () => { }); }); + describe('File System API,', () => { + afterEach(closeAllWindows); + afterEach(() => { + session.defaultSession.setPermissionRequestHandler(null); + }); + + it('allows access by default to reading an OPFS file', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + partition: 'file-system-spec', + contextIsolation: false + } + }); + + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const result = await w.webContents.executeJavaScript(` + new Promise(async (resolve, reject) => { + const root = await navigator.storage.getDirectory(); + const fileHandle = await root.getFileHandle('test', { create: true }); + const { name, size } = await fileHandle.getFile(); + resolve({ name, size }); + } + )`, true); + expect(result).to.deep.equal({ name: 'test', size: 0 }); + }); + + it('fileHandle.queryPermission by default has permission to read and write to OPFS files', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + partition: 'file-system-spec', + contextIsolation: false + } + }); + + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const status = await w.webContents.executeJavaScript(` + new Promise(async (resolve, reject) => { + const root = await navigator.storage.getDirectory(); + const fileHandle = await root.getFileHandle('test', { create: true }); + const status = await fileHandle.queryPermission({ mode: 'readwrite' }); + resolve(status); + } + )`, true); + expect(status).to.equal('granted'); + }); + + it('fileHandle.requestPermission automatically grants permission to read and write to OPFS files', async () => { + const w = new BrowserWindow({ + webPreferences: { + nodeIntegration: true, + partition: 'file-system-spec', + contextIsolation: false + } + }); + + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const status = await w.webContents.executeJavaScript(` + new Promise(async (resolve, reject) => { + const root = await navigator.storage.getDirectory(); + const fileHandle = await root.getFileHandle('test', { create: true }); + const status = await fileHandle.requestPermission({ mode: 'readwrite' }); + resolve(status); + } + )`, true); + expect(status).to.equal('granted'); + }); + + it('requests permission when trying to create a writable file handle', (done) => { + const writablePath = path.join(fixturesPath, 'file-system', 'test-writable.html'); + const testFile = path.join(fixturesPath, 'file-system', 'test.txt'); + + const w = new BrowserWindow({ + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + sandbox: false + } + }); + + w.webContents.session.setPermissionRequestHandler((wc, permission, callback, details) => { + expect(permission).to.equal('fileSystem'); + + const { href } = url.pathToFileURL(writablePath); + expect(details).to.deep.equal({ + fileAccessType: 'writable', + isDirectory: false, + isMainFrame: true, + filePath: testFile, + requestingUrl: href + }); + + callback(true); + }); + + ipcMain.once('did-create-file-handle', async () => { + const result = await w.webContents.executeJavaScript(` + new Promise((resolve, reject) => { + try { + const writable = fileHandle.createWritable(); + resolve(true); + } catch { + resolve(false); + } + }) + `, true); + expect(result).to.be.true(); + done(); + }); + + w.loadFile(writablePath); + + w.webContents.once('did-finish-load', () => { + // @ts-expect-error Undocumented testing method. + clipboard._writeFilesForTesting([testFile]); + w.webContents.paste(); + }); + }); + }); + describe('web workers', () => { let appProcess: ChildProcess.ChildProcessWithoutNullStreams | undefined; @@ -1663,7 +1788,7 @@ describe('chromium features', () => { it('provides a securityOrigin to the request handler', async () => { session.defaultSession.setPermissionRequestHandler( (wc, permission, callback, details) => { - if (details.securityOrigin !== undefined) { + if ((details as MediaAccessPermissionRequest).securityOrigin !== undefined) { callback(true); } else { callback(false); diff --git a/spec/fixtures/file-system/test-writable.html b/spec/fixtures/file-system/test-writable.html new file mode 100644 index 0000000000..6d7012192b --- /dev/null +++ b/spec/fixtures/file-system/test-writable.html @@ -0,0 +1,26 @@ + + + + + + Hello World! + + + + + + + \ No newline at end of file diff --git a/spec/fixtures/file-system/test.txt b/spec/fixtures/file-system/test.txt new file mode 100644 index 0000000000..95d09f2b10 --- /dev/null +++ b/spec/fixtures/file-system/test.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file