| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/api/permissions/permissions_api.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/auto_reset.h" |
| #include "base/check_is_test.h" |
| #include "base/functional/bind.h" |
| #include "base/notimplemented.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "chrome/browser/extensions/api/permissions/permissions_api_helpers.h" |
| #include "chrome/browser/extensions/extension_install_prompt.h" |
| #include "chrome/browser/extensions/extension_management.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/permissions/permissions_updater.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/extensions/api/permissions.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_api_frame_id_map.h" |
| #include "extensions/browser/extension_function.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/permissions_manager.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/manifest_handlers/permissions_parser.h" |
| #include "extensions/common/permissions/permission_message_provider.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/permissions/permissions_info.h" |
| #include "extensions/common/url_pattern.h" |
| #include "extensions/common/url_pattern_set.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "chrome/browser/extensions/chrome_extension_function_details.h" |
| #endif |
| |
| namespace extensions { |
| |
| using api::permissions::Permissions; |
| using permissions_api_helpers::UnpackPermissionSetResult; |
| |
| namespace { |
| |
| const char kBlockedByEnterprisePolicy[] = |
| "Permissions are blocked by enterprise policy."; |
| const char kCantRemoveRequiredPermissionsError[] = |
| "You cannot remove required permissions."; |
| const char kNotInManifestPermissionsError[] = |
| "Only permissions specified in the manifest may be requested."; |
| const char kUserGestureRequiredError[] = |
| "This function must be called during a user gesture"; |
| constexpr char kMustSpecifyDocumentIdOrTabIdError[] = |
| "Must specify either 'documentId' or 'tabId'."; |
| constexpr char kTabNotFoundError[] = "No tab with ID '*'."; |
| constexpr char kInvalidDocumentIdError[] = "No document with ID '*'."; |
| constexpr char kExtensionHasSiteAccessError[] = |
| "Extension cannot add a host access request for a host it already has " |
| "access to."; |
| constexpr char kExtensionHasNoHostPermissionsError[] = |
| "Extension cannot add a host access request when it does not have any host " |
| "permissions."; |
| constexpr char kExtensionHasNoHostPermissionsForPatternError[] = |
| "Extension cannot add a host access request with a pattern that does match " |
| "any of its host permissions."; |
| constexpr char kExtensionRequestCannotBeRemovedError[] = |
| "Extension cannot remove a host access request that doesn't exist."; |
| constexpr char kAddRequestInvalidPatternError[] = |
| "Extension cannot add a request with an invalid value for 'pattern'."; |
| constexpr char kRemoveRequestInvalidPatternError[] = |
| "Extension cannot remove a request with an invalid value for 'pattern'."; |
| |
| PermissionsRequestFunction::DialogAction g_dialog_action = |
| PermissionsRequestFunction::DialogAction::kDefault; |
| PermissionsRequestFunction::ShowDialogCallback* g_show_dialog_callback = |
| nullptr; |
| PermissionsRequestFunction* g_pending_request_function = nullptr; |
| bool ignore_user_gesture_for_tests = false; |
| |
| // Returns whether `tab_id` is a valid tab. Populates `web_contents` with the |
| // ones belonging to the tab , and `error` if tab is invalid. |
| bool ValidateTab(int tab_id, |
| bool include_incognito_information, |
| content::BrowserContext* browser_context, |
| content::WebContents** web_contents, |
| std::string* error) { |
| bool is_valid = ExtensionTabUtil::GetTabById( |
| tab_id, browser_context, include_incognito_information, web_contents); |
| if (!is_valid) { |
| *error = ErrorUtils::FormatErrorMessage(kTabNotFoundError, |
| base::NumberToString(tab_id)); |
| } |
| |
| return is_valid; |
| } |
| |
| // Returns whether `document_id` is a valid document. Populates `web_contents` |
| // with the ones belonging to the document attached frame, and `error` if |
| // document is invalid. |
| bool ValidateDocument(const std::string& document_id, |
| bool include_incognito_information, |
| content::BrowserContext* browser_context, |
| content::WebContents** web_contents, |
| std::string* error) { |
| // Document is invalid if its id doesn't exist. |
| ExtensionApiFrameIdMap::DocumentId frame_document_id = |
| ExtensionApiFrameIdMap::DocumentIdFromString(document_id); |
| if (!frame_document_id) { |
| *error = |
| ErrorUtils::FormatErrorMessage(kInvalidDocumentIdError, document_id); |
| return false; |
| } |
| |
| // Document is invalid if there it has no frame attached. |
| content::RenderFrameHost* frame = |
| ExtensionApiFrameIdMap::Get()->GetRenderFrameHostByDocumentId( |
| frame_document_id); |
| if (!frame) { |
| *error = |
| ErrorUtils::FormatErrorMessage(kInvalidDocumentIdError, document_id); |
| return false; |
| } |
| |
| // Document is invalid if the web contents doesn't exist in our |
| // BrowserContext. We check for this since we found the RenderFrameHost |
| // through a generic lookup. |
| *web_contents = content::WebContents::FromRenderFrameHost(frame); |
| if (!ExtensionTabUtil::IsWebContentsInContext( |
| *web_contents, browser_context, include_incognito_information)) { |
| *error = |
| ErrorUtils::FormatErrorMessage(kInvalidDocumentIdError, document_id); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Returns whether `pattern` was successfully parsed into `parsed_pattern`. |
| bool ParsePattern(const std::string& pattern, URLPattern& parsed_pattern) { |
| parsed_pattern.SetValidSchemes(Extension::kValidHostPermissionSchemes); |
| return parsed_pattern.Parse(pattern) == URLPattern::ParseResult::kSuccess; |
| } |
| |
| } // namespace |
| |
| ExtensionFunction::ResponseAction PermissionsContainsFunction::Run() { |
| std::optional<api::permissions::Contains::Params> params = |
| api::permissions::Contains::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| std::string error; |
| std::unique_ptr<UnpackPermissionSetResult> unpack_result = |
| permissions_api_helpers::UnpackPermissionSet( |
| params->permissions, |
| PermissionsParser::GetRequiredPermissions(extension()), |
| PermissionsParser::GetOptionalPermissions(extension()), |
| ExtensionPrefs::Get(browser_context()) |
| ->AllowFileAccess(extension()->id()), |
| &error); |
| |
| if (!unpack_result) |
| return RespondNow(Error(std::move(error))); |
| |
| const PermissionSet& active_permissions = |
| extension()->permissions_data()->active_permissions(); |
| |
| bool has_all_permissions = |
| // An extension can never have an active permission that wasn't listed in |
| // the manifest, so we know it won't contain all permissions in |
| // |unpack_result| if there are any unlisted. |
| unpack_result->unlisted_apis.empty() && |
| unpack_result->unlisted_hosts.is_empty() && |
| // Restricted file scheme patterns cannot be active on the extension, |
| // since it doesn't have file access in that case. |
| unpack_result->restricted_file_scheme_patterns.is_empty() && |
| // Otherwise, check each expected location for whether it contains the |
| // relevant permissions. |
| active_permissions.apis().Contains(unpack_result->optional_apis) && |
| active_permissions.apis().Contains(unpack_result->required_apis) && |
| active_permissions.explicit_hosts().Contains( |
| unpack_result->optional_explicit_hosts) && |
| active_permissions.explicit_hosts().Contains( |
| unpack_result->required_explicit_hosts) && |
| active_permissions.scriptable_hosts().Contains( |
| unpack_result->required_scriptable_hosts); |
| |
| return RespondNow(ArgumentList( |
| api::permissions::Contains::Results::Create(has_all_permissions))); |
| } |
| |
| ExtensionFunction::ResponseAction PermissionsGetAllFunction::Run() { |
| // TODO(devlin): We should filter out file:-scheme patterns if the extension |
| // doesn't have file access here, so that they don't show up when the |
| // extension calls getAll(). This can either be solved by filtering here or |
| // by not adding file:-scheme patterns to |active_permissions| without file |
| // access (the former is easier, the latter is probably better overall but may |
| // require some investigation). |
| std::unique_ptr<Permissions> permissions = |
| permissions_api_helpers::PackPermissionSet( |
| extension()->permissions_data()->active_permissions()); |
| return RespondNow( |
| ArgumentList(api::permissions::GetAll::Results::Create(*permissions))); |
| } |
| |
| ExtensionFunction::ResponseAction PermissionsRemoveFunction::Run() { |
| std::optional<api::permissions::Remove::Params> params = |
| api::permissions::Remove::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| std::string error; |
| std::unique_ptr<UnpackPermissionSetResult> unpack_result = |
| permissions_api_helpers::UnpackPermissionSet( |
| params->permissions, |
| PermissionsParser::GetRequiredPermissions(extension()), |
| PermissionsParser::GetOptionalPermissions(extension()), |
| ExtensionPrefs::Get(browser_context()) |
| ->AllowFileAccess(extension_->id()), |
| &error); |
| |
| if (!unpack_result) |
| return RespondNow(Error(std::move(error))); |
| |
| // We can't remove any permissions that weren't specified in the manifest. |
| if (!unpack_result->unlisted_apis.empty() || |
| !unpack_result->unlisted_hosts.is_empty()) { |
| return RespondNow(Error(kNotInManifestPermissionsError)); |
| } |
| |
| // Make sure we only remove optional permissions, and not required |
| // permissions. Sadly, for some reason we support having a permission be both |
| // optional and required (and should assume its required), so we need both of |
| // these checks. |
| // TODO(devlin): *Why* do we support that? Should be a load error. |
| // NOTE(devlin): This won't support removal of required permissions that can |
| // withheld. I don't think that will be a common use case, and so is probably |
| // fine. |
| if (!unpack_result->required_apis.empty() || |
| !unpack_result->required_explicit_hosts.is_empty() || |
| !unpack_result->required_scriptable_hosts.is_empty()) { |
| return RespondNow(Error(kCantRemoveRequiredPermissionsError)); |
| } |
| |
| // Note: We don't check |restricted_file_scheme_patterns| here. If there are |
| // any, it means that the extension didn't have file access, but it also means |
| // that it doesn't, effectively, currently have that permission granted (i.e., |
| // it doesn't actually have access to any file:-scheme URL). |
| |
| PermissionSet permissions( |
| std::move(unpack_result->optional_apis), ManifestPermissionSet(), |
| std::move(unpack_result->optional_explicit_hosts), URLPatternSet()); |
| |
| // Only try and remove those permissions that are active on the extension. |
| // For backwards compatability with behavior before this check was added, just |
| // silently remove any that aren't present. |
| std::unique_ptr<const PermissionSet> permissions_to_revoke = |
| PermissionSet::CreateIntersection( |
| permissions, extension()->permissions_data()->active_permissions()); |
| |
| PermissionsUpdater(browser_context()) |
| .RevokeOptionalPermissions( |
| *extension(), *permissions_to_revoke, PermissionsUpdater::REMOVE_SOFT, |
| base::BindOnce( |
| &PermissionsRemoveFunction::Respond, this, |
| ArgumentList(api::permissions::Remove::Results::Create(true)))); |
| return did_respond() ? AlreadyResponded() : RespondLater(); |
| } |
| |
| // static |
| base::AutoReset<PermissionsRequestFunction::DialogAction> |
| PermissionsRequestFunction::SetDialogActionForTests( |
| DialogAction dialog_action) { |
| return base::AutoReset<PermissionsRequestFunction::DialogAction>( |
| &g_dialog_action, dialog_action); |
| } |
| |
| // static |
| base::AutoReset<PermissionsRequestFunction::ShowDialogCallback*> |
| PermissionsRequestFunction::SetShowDialogCallbackForTests( |
| ShowDialogCallback* callback) { |
| return base::AutoReset<ShowDialogCallback*>(&g_show_dialog_callback, |
| callback); |
| } |
| |
| // static |
| void PermissionsRequestFunction::ResolvePendingDialogForTests( |
| bool accept_dialog) { |
| CHECK(g_pending_request_function); |
| PermissionsRequestFunction* pending_function = g_pending_request_function; |
| // Clear out the pending function now. After Release() below, it's unsafe to |
| // use. |
| g_pending_request_function = nullptr; |
| |
| ExtensionInstallPrompt::DoneCallbackPayload result( |
| accept_dialog ? ExtensionInstallPrompt::Result::ACCEPTED |
| : ExtensionInstallPrompt::Result::USER_CANCELED); |
| pending_function->OnInstallPromptDone(result); |
| pending_function->Release(); // Balanced in Run(). |
| } |
| |
| // static |
| void PermissionsRequestFunction::SetIgnoreUserGestureForTests( |
| bool ignore) { |
| ignore_user_gesture_for_tests = ignore; |
| } |
| |
| PermissionsRequestFunction::PermissionsRequestFunction() = default; |
| |
| PermissionsRequestFunction::~PermissionsRequestFunction() { |
| CHECK_NE(g_pending_request_function, this) |
| << "Pending request function was never resolved!"; |
| } |
| |
| ExtensionFunction::ResponseAction PermissionsRequestFunction::Run() { |
| if (!user_gesture() && !ignore_user_gesture_for_tests && |
| extension_->location() != mojom::ManifestLocation::kComponent) { |
| return RespondNow(Error(kUserGestureRequiredError)); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| gfx::NativeWindow native_window = |
| ChromeExtensionFunctionDetails(this).GetNativeWindowForUI(); |
| if (!native_window && g_dialog_action == DialogAction::kDefault) { |
| return RespondNow(Error("Could not find an active window.")); |
| } |
| #else |
| // TODO(crbug.com/419057482): Once we have a cross-platform interface for |
| // browser windows that works on desktop Android, check for an active window. |
| NOTIMPLEMENTED() << "Skipping active window check"; |
| gfx::NativeWindow native_window = nullptr; |
| #endif |
| |
| std::optional<api::permissions::Request::Params> params = |
| api::permissions::Request::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| std::string error; |
| std::unique_ptr<UnpackPermissionSetResult> unpack_result = |
| permissions_api_helpers::UnpackPermissionSet( |
| params->permissions, |
| PermissionsParser::GetRequiredPermissions(extension()), |
| PermissionsParser::GetOptionalPermissions(extension()), |
| ExtensionPrefs::Get(browser_context()) |
| ->AllowFileAccess(extension_->id()), |
| &error); |
| |
| if (!unpack_result) |
| return RespondNow(Error(std::move(error))); |
| |
| // Don't allow the extension to request any permissions that weren't specified |
| // in the manifest. |
| if (!unpack_result->unlisted_apis.empty() || |
| !unpack_result->unlisted_hosts.is_empty()) { |
| return RespondNow(Error(kNotInManifestPermissionsError)); |
| } |
| |
| if (!unpack_result->restricted_file_scheme_patterns.is_empty()) { |
| return RespondNow(Error( |
| "Extension must have file access enabled to request '*'.", |
| unpack_result->restricted_file_scheme_patterns.begin()->GetAsString())); |
| } |
| |
| const PermissionSet& active_permissions = |
| extension()->permissions_data()->active_permissions(); |
| |
| // Determine which of the requested permissions are optional permissions that |
| // are "new", i.e. aren't already active on the extension. |
| requested_optional_ = std::make_unique<const PermissionSet>( |
| std::move(unpack_result->optional_apis), ManifestPermissionSet(), |
| std::move(unpack_result->optional_explicit_hosts), URLPatternSet()); |
| requested_optional_ = |
| PermissionSet::CreateDifference(*requested_optional_, active_permissions); |
| |
| // Determine which of the requested permissions are withheld host permissions. |
| // Since hosts are not always exact matches, we cannot take a set difference. |
| // Thus we only consider requested permissions that are not already active on |
| // the extension. |
| URLPatternSet explicit_hosts; |
| for (const auto& host : unpack_result->required_explicit_hosts) { |
| if (!active_permissions.explicit_hosts().ContainsPattern(host)) { |
| explicit_hosts.AddPattern(host); |
| } |
| } |
| URLPatternSet scriptable_hosts; |
| for (const auto& host : unpack_result->required_scriptable_hosts) { |
| if (!active_permissions.scriptable_hosts().ContainsPattern(host)) { |
| scriptable_hosts.AddPattern(host); |
| } |
| } |
| |
| requested_withheld_ = std::make_unique<const PermissionSet>( |
| APIPermissionSet(), ManifestPermissionSet(), std::move(explicit_hosts), |
| std::move(scriptable_hosts)); |
| |
| // Determine the total "new" permissions; this is the set of all permissions |
| // that aren't currently active on the extension. |
| std::unique_ptr<const PermissionSet> total_new_permissions = |
| PermissionSet::CreateUnion(*requested_withheld_, *requested_optional_); |
| |
| // If all permissions are already active, nothing left to do. |
| if (total_new_permissions->IsEmpty()) { |
| constexpr bool granted = true; |
| return RespondNow(WithArguments(granted)); |
| } |
| |
| // Automatically declines api permissions requests, which are blocked by |
| // enterprise policy. |
| if (!ExtensionManagementFactory::GetForBrowserContext(browser_context()) |
| ->IsPermissionSetAllowed(extension(), *total_new_permissions)) { |
| return RespondNow(Error(kBlockedByEnterprisePolicy)); |
| } |
| |
| // At this point, all permissions in |requested_withheld_| should be within |
| // the |withheld_permissions| section of the PermissionsData. |
| DCHECK(extension()->permissions_data()->withheld_permissions().Contains( |
| *requested_withheld_)); |
| |
| // Prompt the user for any new permissions that aren't contained within the |
| // already-granted permissions. We don't prompt for already-granted |
| // permissions since these were either granted to an earlier extension version |
| // or removed by the extension itself (using the permissions.remove() method). |
| std::unique_ptr<const PermissionSet> granted_permissions = |
| ExtensionPrefs::Get(browser_context()) |
| ->GetRuntimeGrantedPermissions(extension()->id()); |
| std::unique_ptr<const PermissionSet> already_granted_permissions = |
| PermissionSet::CreateIntersection(*granted_permissions, |
| *requested_optional_); |
| total_new_permissions = PermissionSet::CreateDifference( |
| *total_new_permissions, *already_granted_permissions); |
| |
| // We don't need to show the prompt if there are no new warnings, or if |
| // we're skipping the confirmation UI. COMPONENT extensions are allowed to |
| // silently increase their permission level. |
| const PermissionMessageProvider* message_provider = |
| PermissionMessageProvider::Get(); |
| // TODO(devlin): We should probably use the same logic we do for permissions |
| // increases here, where we check if there are *new* warnings (e.g., so we |
| // don't warn about the tabs permission if history is already granted). |
| bool has_no_warnings = |
| message_provider |
| ->GetPermissionMessages(message_provider->GetAllPermissionIDs( |
| *total_new_permissions, extension()->GetType())) |
| .empty(); |
| if (has_no_warnings || |
| extension_->location() == mojom::ManifestLocation::kComponent) { |
| OnInstallPromptDone(ExtensionInstallPrompt::DoneCallbackPayload( |
| ExtensionInstallPrompt::Result::ACCEPTED)); |
| return did_respond() ? AlreadyResponded() : RespondLater(); |
| } |
| |
| // Otherwise, we have to prompt the user (though we might "autoconfirm" for a |
| // test. |
| if (g_dialog_action != DialogAction::kDefault) { |
| prompted_permissions_for_testing_ = total_new_permissions->Clone(); |
| if (g_dialog_action == DialogAction::kAutoConfirm) { |
| OnInstallPromptDone(ExtensionInstallPrompt::DoneCallbackPayload( |
| ExtensionInstallPrompt::Result::ACCEPTED)); |
| } else if (g_dialog_action == DialogAction::kAutoReject) { |
| OnInstallPromptDone(ExtensionInstallPrompt::DoneCallbackPayload( |
| ExtensionInstallPrompt::Result::USER_CANCELED)); |
| } else { |
| CHECK_EQ(g_dialog_action, DialogAction::kProgrammatic); |
| // A test will let us know when to resolve the prompt. Add a reference to |
| // wait. |
| AddRef(); // Balanced in ResolvePendingDialogForTests(). |
| if (g_show_dialog_callback) { |
| g_show_dialog_callback->Run(native_window); |
| } |
| g_pending_request_function = this; |
| } |
| return did_respond() ? AlreadyResponded() : RespondLater(); |
| } |
| |
| install_ui_ = std::make_unique<ExtensionInstallPrompt>( |
| Profile::FromBrowserContext(browser_context()), native_window); |
| install_ui_->ShowDialog( |
| base::BindOnce(&PermissionsRequestFunction::OnInstallPromptDone, this), |
| extension(), nullptr, |
| std::make_unique<ExtensionInstallPrompt::Prompt>( |
| ExtensionInstallPrompt::PERMISSIONS_PROMPT), |
| std::move(total_new_permissions), |
| ExtensionInstallPrompt::GetDefaultShowDialogCallback()); |
| |
| // ExtensionInstallPrompt::ShowDialog() can call the response synchronously. |
| return did_respond() ? AlreadyResponded() : RespondLater(); |
| } |
| |
| bool PermissionsRequestFunction::ShouldKeepWorkerAliveIndefinitely() { |
| // `permissions.request()` may trigger a user prompt. In this case, we allow |
| // the extension service worker to be kept alive past the typical 5 minute |
| // limit per-task, since it may be blocked on user action. |
| return true; |
| } |
| |
| void PermissionsRequestFunction::OnInstallPromptDone( |
| ExtensionInstallPrompt::DoneCallbackPayload payload) { |
| // This dialog doesn't support the "withhold permissions" checkbox. |
| DCHECK_NE(payload.result, |
| ExtensionInstallPrompt::Result::ACCEPTED_WITH_WITHHELD_PERMISSIONS); |
| if (payload.result != ExtensionInstallPrompt::Result::ACCEPTED) { |
| Respond(ArgumentList(api::permissions::Request::Results::Create(false))); |
| return; |
| } |
| PermissionsUpdater permissions_updater(browser_context()); |
| requesting_withheld_permissions_ = !requested_withheld_->IsEmpty(); |
| requesting_optional_permissions_ = !requested_optional_->IsEmpty(); |
| if (requesting_withheld_permissions_) { |
| permissions_updater.GrantRuntimePermissions( |
| *extension(), *requested_withheld_, |
| base::BindOnce(&PermissionsRequestFunction::OnRuntimePermissionsGranted, |
| this)); |
| } |
| if (requesting_optional_permissions_) { |
| permissions_updater.GrantOptionalPermissions( |
| *extension(), *requested_optional_, |
| base::BindOnce( |
| &PermissionsRequestFunction::OnOptionalPermissionsGranted, this)); |
| } |
| |
| // Grant{Runtime|Optional}Permissions calls above can finish synchronously. |
| if (!did_respond()) |
| RespondIfRequestsFinished(); |
| } |
| |
| void PermissionsRequestFunction::OnRuntimePermissionsGranted() { |
| requesting_withheld_permissions_ = false; |
| RespondIfRequestsFinished(); |
| } |
| |
| void PermissionsRequestFunction::OnOptionalPermissionsGranted() { |
| requesting_optional_permissions_ = false; |
| RespondIfRequestsFinished(); |
| } |
| |
| void PermissionsRequestFunction::RespondIfRequestsFinished() { |
| if (requesting_withheld_permissions_ || requesting_optional_permissions_) |
| return; |
| |
| Respond(ArgumentList(api::permissions::Request::Results::Create(true))); |
| } |
| |
| std::unique_ptr<const PermissionSet> |
| PermissionsRequestFunction::TakePromptedPermissionsForTesting() { |
| return std::move(prompted_permissions_for_testing_); |
| } |
| |
| ExtensionFunction::ResponseAction |
| PermissionsAddHostAccessRequestFunction::Run() { |
| CHECK(base::FeatureList::IsEnabled( |
| extensions_features::kApiPermissionsHostAccessRequests)); |
| std::optional<api::permissions::AddHostAccessRequest::Params> params = |
| api::permissions::AddHostAccessRequest::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| // Validate request has only one of document or tab id, and its value is |
| // valid. |
| const std::optional<std::string>& document_id_param = |
| params->request.document_id; |
| std::optional<int> tab_id_param = params->request.tab_id; |
| if ((!document_id_param && !tab_id_param) || |
| (document_id_param && tab_id_param)) { |
| return RespondNow(Error(kMustSpecifyDocumentIdOrTabIdError)); |
| } |
| |
| content::WebContents* web_contents = nullptr; |
| int tab_id = -1; |
| bool is_valid = false; |
| std::string error; |
| if (tab_id_param) { |
| is_valid = |
| ValidateTab(tab_id_param.value(), include_incognito_information(), |
| browser_context(), &web_contents, &error); |
| tab_id = tab_id_param.value(); |
| } else { |
| // document_id_param. |
| is_valid = ValidateDocument(document_id_param.value(), |
| include_incognito_information(), |
| browser_context(), &web_contents, &error); |
| tab_id = is_valid ? ExtensionTabUtil::GetTabId(web_contents) : -1; |
| } |
| |
| if (!is_valid) { |
| CHECK(!error.empty()); |
| return RespondNow(Error(error)); |
| } |
| |
| // Validate request has a valid pattern, if given. |
| std::optional<std::string> pattern_param = params->request.pattern; |
| std::optional<URLPattern> pattern; |
| if (pattern_param) { |
| URLPattern parsed_pattern; |
| if (!ParsePattern(*pattern_param, parsed_pattern)) { |
| return RespondNow(Error(kAddRequestInvalidPatternError)); |
| } |
| pattern = parsed_pattern; |
| } |
| |
| // Verify we properly retrieved the necessary information. |
| DCHECK(web_contents); |
| DCHECK_NE(tab_id, -1); |
| |
| const GURL& url = web_contents->GetLastCommittedURL(); |
| auto* permissions_manager = PermissionsManager::Get(browser_context()); |
| |
| // Request is invalid if extension didn't request any host permissions. |
| if (!permissions_manager->HasRequestedHostPermissions(*extension())) { |
| return RespondNow(Error(kExtensionHasNoHostPermissionsError)); |
| } |
| |
| // Request is invalid if extension has access to the tab's current web |
| // contents. |
| PermissionsManager::ExtensionSiteAccess site_access = |
| permissions_manager->GetSiteAccess(*extension(), url); |
| if (site_access.has_site_access || |
| extension()->permissions_data()->HasTabPermissionsForSecurityOrigin( |
| tab_id, url)) { |
| return RespondNow(Error(kExtensionHasSiteAccessError)); |
| } |
| |
| // Request is invalid if pattern provided does not match the extension's host |
| // permissions. |
| if (pattern) { |
| const PermissionSet& required_permissions = |
| PermissionsParser::GetRequiredPermissions(extension()); |
| const PermissionSet& optional_permissions = |
| PermissionsParser::GetOptionalPermissions(extension()); |
| URLPatternSet pattern_list; |
| pattern_list.AddPattern(*pattern); |
| |
| if (!required_permissions.effective_hosts().OverlapsWith(pattern_list) && |
| !optional_permissions.effective_hosts().OverlapsWith(pattern_list)) { |
| return RespondNow(Error(kExtensionHasNoHostPermissionsForPatternError)); |
| } |
| } |
| |
| permissions_manager->AddHostAccessRequest(web_contents, tab_id, *extension(), |
| pattern); |
| return RespondNow(NoArguments()); |
| } |
| |
| ExtensionFunction::ResponseAction |
| PermissionsRemoveHostAccessRequestFunction::Run() { |
| CHECK(base::FeatureList::IsEnabled( |
| extensions_features::kApiPermissionsHostAccessRequests)); |
| std::optional<api::permissions::RemoveHostAccessRequest::Params> params = |
| api::permissions::RemoveHostAccessRequest::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| const std::optional<std::string>& document_id_param = |
| params->request.document_id; |
| std::optional<int> tab_id_param = params->request.tab_id; |
| |
| // Removal is invalid if it has both document and tab id. |
| if ((!document_id_param && !tab_id_param) || |
| (document_id_param && tab_id_param)) { |
| return RespondNow(Error(kMustSpecifyDocumentIdOrTabIdError)); |
| } |
| |
| content::WebContents* web_contents = nullptr; |
| int tab_id = -1; |
| |
| // Removal is invalid if document or tab id are not valid. |
| bool is_valid = false; |
| std::string error; |
| if (tab_id_param) { |
| is_valid = |
| ValidateTab(tab_id_param.value(), include_incognito_information(), |
| browser_context(), &web_contents, &error); |
| tab_id = tab_id_param.value(); |
| } else { |
| // document_id_param. |
| is_valid = ValidateDocument(document_id_param.value(), |
| include_incognito_information(), |
| browser_context(), &web_contents, &error); |
| tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| } |
| |
| if (!is_valid) { |
| CHECK(!error.empty()); |
| return RespondNow(Error(error)); |
| } |
| |
| // Removal is invalid if pattern provided cannot be parsed. |
| std::optional<std::string> pattern_param = params->request.pattern; |
| std::optional<URLPattern> pattern; |
| if (pattern_param) { |
| URLPattern parsed_pattern; |
| if (!ParsePattern(*pattern_param, parsed_pattern)) { |
| return RespondNow(Error(kRemoveRequestInvalidPatternError)); |
| } |
| pattern = parsed_pattern; |
| } |
| |
| // Verify we properly retrieved the necessary information. |
| DCHECK(web_contents); |
| DCHECK_NE(tab_id, -1); |
| |
| bool is_removed = |
| PermissionsManager::Get(browser_context()) |
| ->RemoveHostAccessRequest(tab_id, extension()->id(), pattern); |
| if (!is_removed) { |
| return RespondNow(Error(kExtensionRequestCannotBeRemovedError)); |
| } |
| |
| return RespondNow(NoArguments()); |
| } |
| |
| } // namespace extensions |