chromium/chrome/browser/policy/test/system_features_policy_browsertest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/constants/ash_features.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/crostini/fake_crostini_features.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/policy/system_features_disable_list_policy_handler.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/policy/policy_constants.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/constants.h"
#include "ui/base/l10n/l10n_util.h"

namespace policy {

namespace {
const char kCanvasAppURL[] = "https://canvas.apps.chrome/";
const char kCanvasAppTitle[] = "canvas.apps.chrome";
const char kWebStoreExtensionURL[] = "https://chrome.google.com/webstore/";
const char kWebStoreExtensionTitle[] = "chrome.google.com";

struct VisibilityFlags {
  bool show_in_search;
  bool show_in_launcher;
  bool show_in_shelf;
};

}  // namespace

class SystemFeaturesPolicyTest : public PolicyTest {
 public:
  SystemFeaturesPolicyTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{ash::features::kEcheSWA},
        /*disabled_features=*/{});
    fake_crostini_features_.set_is_allowed_now(true);
  }

 protected:
  std::u16string GetWebUITitle(const GURL& url,
                               bool using_navigation_throttle) {
    content::WebContents* web_contents =
        browser()->tab_strip_model()->GetActiveWebContents();
    EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
    if (using_navigation_throttle) {
      content::WaitForLoadStopWithoutSuccessCheck(web_contents);
    } else {
      EXPECT_TRUE(content::WaitForLoadStop(web_contents));
    }
    return web_contents->GetTitle();
  }

  void EnableExtensions(bool skip_session_components) {
    auto* profile = browser()->profile();
    extensions::ComponentLoader::EnableBackgroundExtensionsForTesting();
    extensions::ExtensionSystem::Get(profile)
        ->extension_service()
        ->component_loader()
        ->AddDefaultComponentExtensions(skip_session_components);
    base::RunLoop().RunUntilIdle();
  }

  // Disables specified system features or enables all if system_features is
  // empty. Updates disabled mode for disabled system features.
  void UpdateSystemFeaturesDisableList(base::Value system_features,
                                       const char* disabled_mode) {
    PolicyMap policies;
    policies.Set(key::kSystemFeaturesDisableList, POLICY_LEVEL_MANDATORY,
                 POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
                 std::move(system_features), nullptr);
    if (disabled_mode) {
      policies.Set(key::kSystemFeaturesDisableMode, POLICY_LEVEL_MANDATORY,
                   POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
                   base::Value(disabled_mode), nullptr);
    }
    // TODO(b/280518509): Remove this workaround once multidevice code
    // supports runtime policy updates.
    policies.Set(key::kPhoneHubAllowed, POLICY_LEVEL_MANDATORY,
                 POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD, base::Value(true),
                 nullptr);
    UpdateProviderPolicy(policies);
  }

  // Convenience overload of UpdateSystemFeaturesDisableList() that allows
  // callers to provide a base::Value::List instead of a base::Value.
  void UpdateSystemFeaturesDisableList(base::Value::List system_features,
                                       const char* disabled_mode) {
    UpdateSystemFeaturesDisableList(base::Value(std::move(system_features)),
                                    disabled_mode);
  }

  void VerifyExtensionAppState(const char* app_id,
                               apps::Readiness expected_readiness,
                               bool blocked_icon,
                               const VisibilityFlags& expected_visibility) {
    auto* profile = browser()->profile();
    extensions::ExtensionRegistry* registry =
        extensions::ExtensionRegistry::Get(profile);
    ASSERT_TRUE(registry->enabled_extensions().GetByID(app_id));
    VerifyAppState(app_id, expected_readiness, blocked_icon,
                   expected_visibility);
  }

  void VerifyAppState(const char* app_id,
                      apps::Readiness expected_readiness,
                      bool blocked_icon,
                      const VisibilityFlags& expected_visibility) {
    auto* profile = browser()->profile();
    auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);

    bool exist = proxy->AppRegistryCache().ForOneApp(
        app_id, [&expected_readiness, &blocked_icon,
                 &expected_visibility](const apps::AppUpdate& update) {
          EXPECT_EQ(expected_readiness, update.Readiness());
          if (blocked_icon) {
            EXPECT_TRUE(apps::IconEffects::kBlocked &
                        update.IconKey()->icon_effects);
          } else {
            EXPECT_FALSE(apps::IconEffects::kBlocked &
                         update.IconKey()->icon_effects);
          }
          EXPECT_EQ(expected_visibility.show_in_launcher,
                    update.ShowInLauncher());
          EXPECT_EQ(expected_visibility.show_in_search, update.ShowInSearch());
          EXPECT_EQ(expected_visibility.show_in_shelf, update.ShowInShelf());
        });
    EXPECT_TRUE(exist);
  }

  void InstallSWAs() {
    ash::SystemWebAppManager::GetForTest(browser()->profile())
        ->InstallSystemAppsForTesting();
  }

  void InstallPWA(const GURL& app_url, const char* app_id) {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(app_url);
    web_app_info->scope = app_url.GetWithoutFilename();
    webapps::AppId installed_app_id = web_app::test::InstallWebApp(
        browser()->profile(), std::move(web_app_info));
    EXPECT_EQ(app_id, installed_app_id);
  }

  VisibilityFlags GetVisibilityFlags(bool is_hidden) {
    VisibilityFlags flags;
    if (is_hidden) {
      flags.show_in_launcher = false;
      flags.show_in_search = false;
      flags.show_in_shelf = false;
      return flags;
    }
    flags.show_in_launcher = true;
    flags.show_in_search = true;
    flags.show_in_shelf = true;
    return flags;
  }

  void VerifyAppDisableMode(const char* app_id, const char* feature) {
    base::Value::List system_features;
    system_features.Append(feature);
    VisibilityFlags expected_visibility =
        GetVisibilityFlags(false /* is_hidden */);
    // Disable app with default mode (blocked).
    UpdateSystemFeaturesDisableList(system_features.Clone(), nullptr);
    VerifyAppState(app_id, apps::Readiness::kDisabledByPolicy, true,
                   expected_visibility);
    // Disable and hide app.
    expected_visibility = GetVisibilityFlags(true /* is_hidden */);
    UpdateSystemFeaturesDisableList(system_features.Clone(),
                                    kHiddenDisableMode);
    VerifyAppState(app_id, apps::Readiness::kDisabledByPolicy, true,
                   expected_visibility);
    // Disable and block app.
    expected_visibility = GetVisibilityFlags(false /* is_hidden */);
    UpdateSystemFeaturesDisableList(system_features.Clone(),
                                    kBlockedDisableMode);
    VerifyAppState(app_id, apps::Readiness::kDisabledByPolicy, true,
                   expected_visibility);
    // Enable app.
    UpdateSystemFeaturesDisableList(base::Value(), nullptr);
    VerifyAppState(app_id, apps::Readiness::kReady, false, expected_visibility);
  }

  // Used for non-link-capturing PWAs.
  void VerifyIsAppURLDisabled(const char* app_id,
                              const char* feature,
                              const char* url,
                              const char* app_title) {
    const GURL& app_url = GURL(url);

    // The URL navigation is still allowed because the app is not installed,
    // though it is disabled by policy.
    base::Value::List system_features;
    system_features.Append(feature);
    UpdateSystemFeaturesDisableList(std::move(system_features), nullptr);
    EXPECT_EQ(base::UTF8ToUTF16(app_title), GetWebUITitle(app_url, true));

    // Install the app.
    InstallPWA(app_url, app_id);
    EXPECT_EQ(l10n_util::GetStringUTF16(IDS_CHROME_URLS_DISABLED_PAGE_HEADER),
              GetWebUITitle(app_url, true));

    // Enable the app by removing it from the policy of disabled apps.
    UpdateSystemFeaturesDisableList(base::Value(), nullptr);
    EXPECT_EQ(base::UTF8ToUTF16(app_title), GetWebUITitle(app_url, true));
  }

  void VerifyIsExtensionAppURLAccessible(const char* url,
                                         const char* app_title) {
    const GURL& app_url = GURL(url);
    EXPECT_EQ(base::UTF8ToUTF16(app_title), GetWebUITitle(app_url, true));
  }

  base::test::ScopedFeatureList scoped_feature_list_;

  // Fake the Crostini feature to have the Terminal app icon show in the
  // launcher when installed.
  crostini::FakeCrostiniFeatures fake_crostini_features_;
};

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest, DisableWebStoreBeforeInstall) {
  base::Value::List system_features;
  system_features.Append(kWebStoreFeature);
  VisibilityFlags expected_visibility =
      GetVisibilityFlags(false /* is_hidden */);
  UpdateSystemFeaturesDisableList(std::move(system_features), nullptr);
  EnableExtensions(true);
  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  // The URL navigation should still be possible
  // even if the app is disabled by policy.
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);

  UpdateSystemFeaturesDisableList(base::Value(), nullptr);
  VerifyExtensionAppState(extensions::kWebStoreAppId, apps::Readiness::kReady,
                          false, expected_visibility);
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest, DisableWebStoreAfterInstall) {
  EnableExtensions(false);
  base::Value::List system_features;
  system_features.Append(kWebStoreFeature);
  VisibilityFlags expected_visibility =
      GetVisibilityFlags(false /* is_hidden */);
  UpdateSystemFeaturesDisableList(std::move(system_features), nullptr);

  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  // The URL navigation should still be possible
  // even if the app is disabled by policy.
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);

  UpdateSystemFeaturesDisableList(base::Value(), nullptr);
  VerifyExtensionAppState(extensions::kWebStoreAppId, apps::Readiness::kReady,
                          false, expected_visibility);
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest,
                       DisableWebStoreAfterInstallWithModes) {
  EnableExtensions(false);
  base::Value::List system_features;
  system_features.Append(kWebStoreFeature);
  VisibilityFlags expected_visibility =
      GetVisibilityFlags(false /* is_hidden */);
  // Disable app with default mode (blocked).
  // The URL navigation should still be possible
  // even if the app is disabled by policy.
  UpdateSystemFeaturesDisableList(system_features.Clone(), nullptr);
  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);
  // Disable and hide app.
  expected_visibility = GetVisibilityFlags(true /* is_hidden */);
  UpdateSystemFeaturesDisableList(system_features.Clone(), kHiddenDisableMode);
  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);
  // Disable and block app.
  expected_visibility = GetVisibilityFlags(false /* is_hidden */);
  UpdateSystemFeaturesDisableList(system_features.Clone(), kBlockedDisableMode);
  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);
  // Enable app
  UpdateSystemFeaturesDisableList(base::Value(), nullptr);
  VerifyExtensionAppState(extensions::kWebStoreAppId, apps::Readiness::kReady,
                          false, expected_visibility);
  VerifyIsExtensionAppURLAccessible(kWebStoreExtensionURL,
                                    kWebStoreExtensionTitle);
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest, DisableSWAs) {
  InstallSWAs();

  // Disable Camera app.
  VerifyAppDisableMode(web_app::kCameraAppId, kCameraFeature);

  // Disable Explore app.
  VerifyAppDisableMode(web_app::kHelpAppId, kExploreFeature);

  // Disable Gallery app.
  VerifyAppDisableMode(web_app::kMediaAppId, kGalleryFeature);

  // Disable Terminal app.
  VerifyAppDisableMode(guest_os::kTerminalSystemAppId, kTerminalFeature);

  // Disable Print Jobs app.
  VerifyAppDisableMode(web_app::kPrintManagementAppId, kPrintJobsFeature);

  // Disable Key Shortcuts app.
  VerifyAppDisableMode(web_app::kShortcutCustomizationAppId,
                       kKeyShortcutsFeature);
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest,
                       DisableMultipleAppsWithHiddenModeAfterInstall) {
  InstallSWAs();
  InstallPWA(GURL(kCanvasAppURL), web_app::kCanvasAppId);

  // Disable app with hidden mode.
  const base::Value::List system_features = base::Value::List()
                                                .Append(kCameraFeature)
                                                .Append(kScanningFeature)
                                                .Append(kWebStoreFeature)
                                                .Append(kCanvasFeature)
                                                .Append(kCroshFeature)
                                                .Append(kGalleryFeature)
                                                .Append(kTerminalFeature)
                                                .Append(kPrintJobsFeature)
                                                .Append(kKeyShortcutsFeature);
  UpdateSystemFeaturesDisableList(system_features.Clone(), kHiddenDisableMode);

  VisibilityFlags expected_visibility =
      GetVisibilityFlags(true /* is_hidden */);
  VerifyAppState(web_app::kCameraAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyAppState(web_app::kScanningAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  VerifyAppState(web_app::kCanvasAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyAppState(web_app::kCroshAppId, apps::Readiness::kDisabledByPolicy, true,
                 expected_visibility);
  VerifyAppState(web_app::kMediaAppId, apps::Readiness::kDisabledByPolicy, true,
                 expected_visibility);
  VerifyAppState(guest_os::kTerminalSystemAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);
  VerifyAppState(web_app::kPrintManagementAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);
  VerifyAppState(web_app::kShortcutCustomizationAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);

  // Disable and block apps.
  expected_visibility = GetVisibilityFlags(false /* is_hidden */);
  // Crosh is never shown.
  VisibilityFlags crosh_expected_visibility =
      GetVisibilityFlags(true /* is_hidden */);
  UpdateSystemFeaturesDisableList(system_features.Clone(), kBlockedDisableMode);

  VerifyAppState(web_app::kCameraAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyAppState(web_app::kScanningAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  VerifyAppState(web_app::kCanvasAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyAppState(web_app::kCroshAppId, apps::Readiness::kDisabledByPolicy, true,
                 crosh_expected_visibility);
  VerifyAppState(web_app::kMediaAppId, apps::Readiness::kDisabledByPolicy, true,
                 expected_visibility);
  VerifyAppState(guest_os::kTerminalSystemAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);
  VerifyAppState(web_app::kPrintManagementAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);
  VerifyAppState(web_app::kShortcutCustomizationAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);

  // Enable apps.
  UpdateSystemFeaturesDisableList(base::Value(), nullptr);
  VerifyAppState(web_app::kCameraAppId, apps::Readiness::kReady, false,
                 expected_visibility);
  VerifyAppState(web_app::kScanningAppId, apps::Readiness::kReady, false,
                 expected_visibility);
  VerifyExtensionAppState(extensions::kWebStoreAppId, apps::Readiness::kReady,
                          false, expected_visibility);
  VerifyAppState(web_app::kCanvasAppId, apps::Readiness::kReady, false,
                 expected_visibility);
  VerifyAppState(web_app::kCroshAppId, apps::Readiness::kReady, false,
                 crosh_expected_visibility);
  VerifyAppState(web_app::kMediaAppId, apps::Readiness::kReady, false,
                 expected_visibility);
  VerifyAppState(guest_os::kTerminalSystemAppId, apps::Readiness::kReady, false,
                 expected_visibility);
  VerifyAppState(web_app::kPrintManagementAppId, apps::Readiness::kReady, false,
                 expected_visibility);
  VerifyAppState(web_app::kShortcutCustomizationAppId, apps::Readiness::kReady,
                 false, expected_visibility);
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest,
                       DisableMultipleAppsWithHiddenModeBeforeInstall) {
  const base::Value::List system_features = base::Value::List()
                                                .Append(kCameraFeature)
                                                .Append(kScanningFeature)
                                                .Append(kWebStoreFeature)
                                                .Append(kCanvasFeature)
                                                .Append(kCroshFeature)
                                                .Append(kGalleryFeature)
                                                .Append(kTerminalFeature)
                                                .Append(kPrintJobsFeature)
                                                .Append(kKeyShortcutsFeature);
  UpdateSystemFeaturesDisableList(system_features.Clone(), kHiddenDisableMode);

  InstallSWAs();
  InstallPWA(GURL(kCanvasAppURL), web_app::kCanvasAppId);

  VisibilityFlags expected_visibility =
      GetVisibilityFlags(true /* is_hidden */);

  // Disable app with hidden mode.
  VerifyAppState(web_app::kCameraAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyAppState(web_app::kScanningAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyExtensionAppState(extensions::kWebStoreAppId,
                          apps::Readiness::kDisabledByPolicy, true,
                          expected_visibility);
  VerifyAppState(web_app::kCanvasAppId, apps::Readiness::kDisabledByPolicy,
                 true, expected_visibility);
  VerifyAppState(web_app::kCroshAppId, apps::Readiness::kDisabledByPolicy, true,
                 expected_visibility);
  VerifyAppState(web_app::kMediaAppId, apps::Readiness::kDisabledByPolicy, true,
                 expected_visibility);
  VerifyAppState(guest_os::kTerminalSystemAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);
  VerifyAppState(web_app::kPrintManagementAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);
  VerifyAppState(web_app::kShortcutCustomizationAppId,
                 apps::Readiness::kDisabledByPolicy, true, expected_visibility);
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest, RedirectChromeSettingsURL) {
  base::Value::List system_features;
  system_features.Append(kBrowserSettingsFeature);
  UpdateSystemFeaturesDisableList(std::move(system_features), nullptr);

  GURL settings_url = GURL(chrome::kChromeUISettingsURL);
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_CHROME_URLS_DISABLED_PAGE_HEADER),
            GetWebUITitle(settings_url, false));

  UpdateSystemFeaturesDisableList(base::Value(), nullptr);
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_SETTINGS_SETTINGS),
            GetWebUITitle(settings_url, false));
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest, RedirectCroshURL) {
  base::Value::List system_features;
  system_features.Append(kCroshFeature);
  UpdateSystemFeaturesDisableList(std::move(system_features), nullptr);

  GURL crosh_url = GURL(chrome::kChromeUIUntrustedCroshURL);
  EXPECT_EQ(l10n_util::GetStringUTF16(IDS_CHROME_URLS_DISABLED_PAGE_HEADER),
            GetWebUITitle(crosh_url, false));

  UpdateSystemFeaturesDisableList(base::Value(), nullptr);
  // Title is empty for untrusted URLs.
  EXPECT_EQ(std::u16string(), GetWebUITitle(crosh_url, false));
}

IN_PROC_BROWSER_TEST_F(SystemFeaturesPolicyTest, DisablePWAs) {
  // Disable Canvas app.
  VerifyIsAppURLDisabled(web_app::kCanvasAppId, kCanvasFeature, kCanvasAppURL,
                         kCanvasAppTitle);
  VerifyAppDisableMode(web_app::kCanvasAppId, kCanvasFeature);
}

}  // namespace policy