| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string> |
| #include <string_view> |
| #include <tuple> |
| |
| #include "base/notreached.h" |
| #include "base/strings/to_string.h" |
| #include "chrome/browser/extensions/chrome_test_extension_loader.h" |
| #include "chrome/browser/extensions/extension_keybinding_registry.h" |
| #include "chrome/browser/extensions/install_verifier.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/supervised_user/supervised_user_test_util.h" |
| #include "chrome/browser/ui/extensions/extensions_dialogs.h" |
| #include "chrome/browser/ui/supervised_user/parent_permission_dialog.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model_observer.h" |
| #include "chrome/test/interaction/interactive_browser_test.h" |
| #include "chrome/test/supervised_user/browser_user.h" |
| #include "chrome/test/supervised_user/family_live_test.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/supervised_user/core/common/pref_names.h" |
| #include "components/supervised_user/test_support/family_link_settings_state_management.h" |
| #include "content/public/test/browser_test.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/interaction/element_identifier.h" |
| |
| namespace supervised_user { |
| namespace { |
| |
| static constexpr std::string_view kChromeManageExternsionsUrl = |
| "chrome://extensions/"; |
| static constexpr std::string_view kExtensionSiteSettingsUrl = |
| "chrome://settings/content/siteDetails?site=chrome-extension://"; |
| static constexpr std::string_view kExtensionName = "An Extension"; |
| |
| // TODO(b/321242366): Consider moving to helper class. |
| // Checks if a page title matches the given regexp in ecma script dialect. |
| InteractiveBrowserTestApi::StateChange PageWithMatchingTitle( |
| std::string_view title_regexp) { |
| DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kStateChange); |
| InteractiveBrowserTestApi::StateChange state_change; |
| state_change.type = |
| InteractiveBrowserTestApi::StateChange::Type::kConditionTrue; |
| state_change.event = kStateChange; |
| state_change.test_function = base::StringPrintf(R"js( |
| () => /%s/.test(document.title) |
| )js", |
| title_regexp.data()); |
| state_change.continue_across_navigation = true; |
| return state_change; |
| } |
| |
| // Test the behavior of handling extensions for supervised users. |
| class SupervisedUserExtensionsParentalControlsUiTest |
| : public InteractiveFamilyLiveTest, |
| public testing::WithParamInterface< |
| std::tuple<FamilyLiveTest::RpcMode, |
| /*permissions_switch_state=*/FamilyLinkToggleState, |
| /*extensions_switch_state=*/FamilyLinkToggleState>> { |
| public: |
| SupervisedUserExtensionsParentalControlsUiTest() |
| : InteractiveFamilyLiveTest(GetRpcMode()) {} |
| |
| protected: |
| // Child tries to enable a disabled extension (which is pending parent |
| // approval) by clicking at the extension's toggle. |
| // When the Extensions toggle is ON and used to manage the extensions, |
| // the extension should be already enabled. |
| // In that case the method only verifies the enabled state. |
| auto ChildClicksEnableExtensionIfExtensionDisabled( |
| ui::ElementIdentifier kChildTab, |
| bool expected_extension_enabled) { |
| return Steps( |
| ExecuteJs(kChildTab, base::StringPrintf( |
| R"js( |
| () => { |
| const view_manager = |
| document.querySelector("extensions-manager").shadowRoot |
| .querySelector("#container").querySelector("#viewManager"); |
| if (!view_manager) { |
| throw Error("Path to view_manager element is invalid."); |
| } |
| const container = view_manager.querySelector("#items-list") |
| .shadowRoot.querySelector("#container"); |
| if (!container) { |
| throw Error("Path to container element is invalid."); |
| } |
| const count = container.querySelectorAll("extensions-item").length; |
| if (count !== 1) { |
| throw Error("Encountered unexpected number of extensions: " + count); |
| } |
| const extn = container.querySelectorAll("extensions-item")[0]; |
| if (!extn) { |
| throw Error("Path to extension element is invalid."); |
| } |
| const toggle = extn.shadowRoot.querySelector("#enableToggle"); |
| if (!toggle) { |
| throw Error("Path to extension toggle is invalid."); |
| } |
| if (toggle.ariaPressed != "%s") { |
| throw Error("Extension toggle in unexpected state: " + toggle.ariaPressed); |
| } |
| if (toggle.ariaPressed == "false") { |
| toggle.click(); |
| } |
| } |
| )js", |
| base::ToString(expected_extension_enabled))), |
| Log("Child inspected extension toggle.")); |
| } |
| |
| // Installs programmatically (not through the UI) an extension for the given |
| // user. |
| void InstallExtension(Profile* profile) { |
| std::string extension_manifest = base::StringPrintf( |
| R"({ |
| "name": "%s", |
| "manifest_version": 3, |
| "version": "0.1", |
| "host_permissions": ["<all_urls>"], |
| "permissions": [ "geolocation" ] |
| })", |
| kExtensionName.data()); |
| extensions::TestExtensionDir extension_dir; |
| extension_dir.WriteManifest(extension_manifest); |
| |
| extensions::ChromeTestExtensionLoader extension_loader(profile); |
| extension_loader.set_ignore_manifest_warnings(true); |
| extension_loader.LoadExtension(extension_dir.Pack()); |
| } |
| |
| ui::ElementIdentifier GetTargetUIElement() { |
| CHECK(GetExtensionsSwitchTargetState() == FamilyLinkToggleState::kDisabled); |
| // Parent approval dialog should appear. |
| return ParentPermissionDialog::kDialogViewIdForTesting; |
| } |
| |
| // Navigates to the `Settings` page for the installed extension under test |
| // and inspects the permissions granted to the `Location` setting. |
| // Checks if the `Locations` attribute is editable or not (html attribute |
| // should be disabled), respecting the configuration of the "Permissions" |
| // switch in Family Link. |
| auto CheckExtensionLocationPermissions(ui::ElementIdentifier kChildElementId, |
| Profile* profile) { |
| extensions::ExtensionId installed_extension_id; |
| const auto& installed_extensions = |
| extensions::ExtensionRegistry::Get(profile) |
| ->GenerateInstalledExtensionsSet(); |
| for (const auto& extension : installed_extensions) { |
| if (extension->name() == kExtensionName) { |
| installed_extension_id = extension->id(); |
| break; |
| } |
| } |
| CHECK(installed_extension_id.size() > 0) |
| << "There must be an installed extension."; |
| |
| // When the Permissions FL switch is Off, the Location permissions button |
| // should be disabled (unmodifiable). |
| bool permissions_button_greyed_out = |
| GetPermissionsSwitchTargetState() == FamilyLinkToggleState::kDisabled; |
| return Steps( |
| Log("With installed extension : " + installed_extension_id), |
| NavigateWebContents(kChildElementId, |
| GURL(std::string(kExtensionSiteSettingsUrl) + |
| std::string(installed_extension_id))), |
| WaitForStateChange(kChildElementId, PageWithMatchingTitle("Settings")), |
| Log("With extension settings page open."), |
| // Detect the Location permission and check whether it's user |
| // modifiable. |
| ExecuteJs(kChildElementId, |
| base::StringPrintf( |
| R"js( |
| () => { const location_permission = document.querySelector("body > settings-ui") |
| .shadowRoot.querySelector("#main") |
| .shadowRoot.querySelector("settings-basic-page") |
| .shadowRoot.querySelector("#basicPage > settings-section.expanded > settings-privacy-page") |
| .shadowRoot.querySelector("#pages > settings-subpage > site-details") |
| .shadowRoot.querySelector('[label="Location"]') |
| .shadowRoot.querySelector("#permission"); |
| if (!location_permission) { |
| throw Error('No location permission menu was found.'); |
| } |
| if (location_permission.disabled === "%s") { |
| throw Error('Unexpected Location Permission state: ' + permission_drop.disabled); |
| } |
| } |
| )js", |
| base::ToString(permissions_button_greyed_out))), |
| Log("Child inspected Location Permission button.")); |
| } |
| |
| auto CheckForParentDialogIfExtensionDisabled( |
| bool is_expected_extension_enabled) { |
| if (is_expected_extension_enabled) { |
| // No dialog appears in this case. |
| return Steps(Log("No dialog check is done, the extension is enabled.")); |
| } |
| auto target_ui_element_id = GetTargetUIElement(); |
| return Steps( |
| Log(base::StringPrintf("Waiting for the %s to appear.", |
| (target_ui_element_id == |
| ParentPermissionDialog::kDialogViewIdForTesting) |
| ? "parent approval dialog" |
| : "blocked extension message")), |
| WaitForShow(target_ui_element_id), |
| Log(base::StringPrintf("The %s appears.", |
| (target_ui_element_id == |
| ParentPermissionDialog::kDialogViewIdForTesting) |
| ? "parent approval dialog" |
| : "blocked extension message"))); |
| } |
| |
| static FamilyLiveTest::RpcMode GetRpcMode() { |
| return std::get<0>(GetParam()); |
| } |
| |
| static FamilyLinkToggleState GetPermissionsSwitchTargetState() { |
| return std::get<1>(GetParam()); |
| } |
| |
| static FamilyLinkToggleState GetExtensionsSwitchTargetState() { |
| return std::get<2>(GetParam()); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SupervisedUserExtensionsParentalControlsUiTest, |
| ChildTogglesExtensionMissingParentApproval) { |
| extensions::ScopedInstallVerifierBypassForTest install_verifier_bypass; |
| |
| DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kChildElementId); |
| DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(InIntendedStateObserver, |
| kDefineStateObserverId); |
| DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(InIntendedStateObserver, |
| kResetStateObserverId); |
| const int child_tab_index = 0; |
| |
| // The extensions should be disabled (pending parent approval) in all cases, |
| // expect when the new "Extensions" FL switch is ON. |
| const bool should_be_enabled = |
| GetExtensionsSwitchTargetState() == FamilyLinkToggleState::kEnabled; |
| |
| TurnOnSync(); |
| |
| // Set the FL switch in the value that require parent approvals for |
| // extension installation. |
| RunTestSequence(Log("Set config that requires parental approvals."), |
| WaitForStateSeeding( |
| kResetStateObserverId, child(), |
| FamilyLinkSettingsState::SetAdvancedSettingsDefault())); |
| |
| InstallExtension(&child().profile()); |
| |
| RunTestSequence(InAnyContext( |
| Log("Given an installed disabled extension."), |
| // Parent sets both the FL Permissions and Extensions switches. |
| // Only one of them impacts the handling of supervised user extensions. |
| WaitForStateSeeding( |
| kDefineStateObserverId, child(), |
| FamilyLinkSettingsState::AdvancedSettingsToggles( |
| {FamilyLinkToggleConfiguration( |
| {.type = FamilyLinkToggleType::kExtensionsToggle, |
| .state = GetExtensionsSwitchTargetState()}), |
| FamilyLinkToggleConfiguration( |
| {.type = FamilyLinkToggleType::kPermissionsToggle, |
| .state = GetPermissionsSwitchTargetState()})})), |
| // Child navigates to the extensions page and tries to enable the |
| // extension, if it is disabled. |
| Log("When child visits the extensions management page."), |
| InstrumentTab(kChildElementId, child_tab_index, &child().browser()), |
| NavigateWebContents(kChildElementId, GURL(kChromeManageExternsionsUrl)), |
| WaitForStateChange(kChildElementId, PageWithMatchingTitle("Extensions")), |
| Log("When child tries to enable the extension."), |
| ChildClicksEnableExtensionIfExtensionDisabled(kChildElementId, |
| should_be_enabled), |
| // If the extension is not already enabled, check that the expect UI |
| // dialog appears. |
| CheckForParentDialogIfExtensionDisabled(should_be_enabled), |
| CheckExtensionLocationPermissions(kChildElementId, &child().profile()))); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| SupervisedUserExtensionsParentalControlsUiTest, |
| testing::Combine( |
| testing::Values(FamilyLiveTest::RpcMode::kProd, |
| FamilyLiveTest::RpcMode::kTestImpersonation), |
| /*permissions_switch_target_value=*/ |
| testing::Values(FamilyLinkToggleState::kEnabled, |
| FamilyLinkToggleState::kDisabled), |
| /*extensions_switch_target_value==*/ |
| testing::Values(FamilyLinkToggleState::kEnabled, |
| FamilyLinkToggleState::kDisabled)), |
| [](const auto& info) { |
| return ToString(std::get<0>(info.param)) + |
| std::string( |
| (std::get<1>(info.param) == FamilyLinkToggleState::kEnabled |
| ? "WithPermissionsOn" |
| : "WithPermissionsOff")) + |
| std::string( |
| (std::get<2>(info.param) == FamilyLinkToggleState::kEnabled |
| ? "WithExtensionsOn" |
| : "WithExtensionsOff")); |
| }); |
| |
| } // namespace |
| } // namespace supervised_user |