| // 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. |
| |
| #ifndef CHROME_BROWSER_POLICY_EXTENSION_FORCE_INSTALL_MIXIN_H_ |
| #define CHROME_BROWSER_POLICY_EXTENSION_FORCE_INSTALL_MIXIN_H_ |
| |
| #include <atomic> |
| #include <map> |
| #include <optional> |
| #include <string> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/memory/raw_ptr.h" |
| #include "build/build_config.h" |
| #include "chrome/test/base/mixin_based_in_process_browser_test.h" |
| #include "components/policy/core/common/cloud/test/policy_builder.h" |
| #include "extensions/common/extension_id.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| |
| class GURL; |
| class Profile; |
| |
| namespace base { |
| class Version; |
| } // namespace base |
| |
| namespace extensions { |
| class Extension; |
| } // namespace extensions |
| |
| namespace policy { |
| class MockConfigurationPolicyProvider; |
| } // namespace policy |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| |
| namespace ash { |
| class DeviceStateMixin; |
| class EmbeddedPolicyTestServerMixin; |
| } // namespace ash |
| |
| namespace policy { |
| class DevicePolicyCrosTestHelper; |
| } // namespace policy |
| |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // A mixin that allows to force-install an extension/app via user or device |
| // policy. |
| // |
| // Encapsulates the following operations: |
| // * generating a CRX file, |
| // * generating an update manifest, |
| // * hosting the update manifest and the CRX via an embedded test server, |
| // * configuring the force installation in the user/device policy. |
| // |
| // Example usage (for force-installing using the user-level policy): |
| // |
| // class MyTestFixture : ... { |
| // protected: |
| // void SetUpOnMainThread() override { |
| // ... |
| // force_install_mixin_.InitWithMockPolicyProvider(...); |
| // } |
| // ExtensionForceInstallMixin force_install_mixin_{&mixin_host_}; |
| // }; |
| // IN_PROC_BROWSER_TEST_F(...) { |
| // EXPECT_TRUE(force_install_mixin_.ForceInstallFromCrx(...)); |
| // } |
| // |
| // Internally, the mixin owns an embedded test server that hosts files needed |
| // for the forced installation: |
| // * "/<extension_id>.xml" - update manifests referred to by policies, |
| // * "/<extension_id>-<version>.crx" - CRX packages referred to by the update |
| // manifests. |
| class ExtensionForceInstallMixin final : public InProcessBrowserTestMixin { |
| public: |
| // The type of the waiting mode for the force installation operation. |
| enum class WaitMode { |
| // Don't wait, and return immediately. |
| kNone, |
| // Wait until the force-installation pref is updated. |
| kPrefSet, |
| // Wait until the extension is loaded. |
| kLoad, |
| // Wait until the extension's background page is loaded for the first time. |
| kBackgroundPageFirstLoad, |
| // Wait until the extension is loaded and its (presumably javascript |
| // typescript) code sends the hard-coded message 'ready'. The extension |
| // needs to send a message via `chrome.test.sendMessage('ready')`. |
| kReadyMessageReceived, |
| }; |
| |
| // The type of the waiting mode for the force-installed extension update. |
| enum class UpdateWaitMode { |
| // Don't wait, and return immediately. |
| kNone, |
| // TODO(crbug.com/40697472): Add other wait modes as necessary. |
| }; |
| |
| // The type of the server error that should be simulated. |
| enum class ServerErrorMode { |
| // No error - network requests will succeed. |
| kNone, |
| // Don't respond to any network request at all (a rough equivalent of an |
| // absent network). |
| kHung, |
| // Respond with the HTTP 500 Internal Server Error. |
| kInternalError, |
| }; |
| |
| explicit ExtensionForceInstallMixin(InProcessBrowserTestMixinHost* host); |
| ExtensionForceInstallMixin(const ExtensionForceInstallMixin&) = delete; |
| ExtensionForceInstallMixin& operator=(const ExtensionForceInstallMixin&) = |
| delete; |
| ~ExtensionForceInstallMixin() override; |
| |
| // Use one of the Init*() methods below to initialize the object before |
| // calling any other method. |
| // Note: The |profile| argument is optional; if it's null, only the `kNone` |
| // waiting mode is allowed, and Get...Extension() methods are fobidden. |
| |
| void InitWithMockPolicyProvider( |
| Profile* profile, |
| policy::MockConfigurationPolicyProvider* mock_policy_provider); |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| void InitWithDeviceStateMixin(Profile* profile, |
| ash::DeviceStateMixin* device_state_mixin); |
| void InitWithDevicePolicyCrosTestHelper( |
| Profile* profile, |
| policy::DevicePolicyCrosTestHelper* device_policy_cros_test_helper); |
| void InitWithEmbeddedPolicyMixin( |
| Profile* profile, |
| ash::EmbeddedPolicyTestServerMixin* policy_test_server_mixin, |
| policy::UserPolicyBuilder* user_policy_builder, |
| const std::string& account_id, |
| const std::string& policy_type); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // Force-installs the CRX file |crx_path|; under the hood, generates an update |
| // manifest and serves it and the CRX file by the embedded test server. |
| // |extension_id| - if non-null, will be set to the installed extension ID. |
| // |extension_version| - if non-null, will be set to the installed extension |
| // version. |
| [[nodiscard]] bool ForceInstallFromCrx( |
| const base::FilePath& crx_path, |
| WaitMode wait_mode, |
| extensions::ExtensionId* extension_id = nullptr, |
| base::Version* extension_version = nullptr); |
| // Force-installs the extension from the given source directory (which should |
| // contain the manifest.json file and all other files of the extension). |
| // Under the hood, packs the directory into a CRX file and serves it like |
| // ForceInstallFromCrx(). |
| // |pem_path| - if non-empty, will be used to load the private key for packing |
| // the extension; when empty, a random key will be generated. |
| // |extension_id| - if non-null, will be set to the installed extension ID. |
| // |extension_version| - if non-null, will be set to the installed extension |
| // version. |
| [[nodiscard]] bool ForceInstallFromSourceDir( |
| const base::FilePath& extension_dir_path, |
| const std::optional<base::FilePath>& pem_path, |
| WaitMode wait_mode, |
| extensions::ExtensionId* extension_id = nullptr, |
| base::Version* extension_version = nullptr); |
| |
| // Updates the served extension to the new version from |crx_path|. It's |
| // expected that a ForceInstallFromCrx() call was done previously for this |
| // extension. |
| // |extension_version| - if non-null, will be set to the CRX'es version. |
| [[nodiscard]] bool UpdateFromCrx(const base::FilePath& crx_path, |
| UpdateWaitMode wait_mode, |
| base::Version* extension_version = nullptr); |
| // Updates the served |extension_id| extension to the new version from |
| // |extension_dir_path|. It's expected that a ForceInstallFromSourceDir() call |
| // was done previously for this extension. |
| // |extension_version| - if non-null, will be set to the extension's version. |
| [[nodiscard]] bool UpdateFromSourceDir( |
| const base::FilePath& extension_dir_path, |
| const extensions::ExtensionId& extension_id, |
| UpdateWaitMode wait_mode, |
| base::Version* extension_version = nullptr); |
| |
| // Returns the extension, or null if it's not installed yet. |
| const extensions::Extension* GetInstalledExtension( |
| const extensions::ExtensionId& extension_id) const; |
| // Returns the extension, or null if it's not installed or not enabled yet. |
| const extensions::Extension* GetEnabledExtension( |
| const extensions::ExtensionId& extension_id) const; |
| |
| // Changes the embedded test server's error mode, allowing to simulate |
| // network unavailability and errors. |
| void SetServerErrorMode(ServerErrorMode server_error_mode); |
| |
| // InProcessBrowserTestMixin: |
| void SetUpOnMainThread() override; |
| |
| bool initialized() const { return initialized_; } |
| |
| private: |
| // Returns the path to the file that is served by the embedded test server |
| // under the given name. |
| base::FilePath GetPathInServedDir(const std::string& file_name) const; |
| // Returns the URL of the update manifest pointing to the embedded test |
| // server. |
| GURL GetServedUpdateManifestUrl( |
| const extensions::ExtensionId& extension_id) const; |
| // Returns the URL of the CRX file pointing to the embedded test server. |
| GURL GetServedCrxUrl(const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version) const; |
| // Makes the given |source_crx_path| file served by the embedded test server. |
| bool ServeExistingCrx(const base::FilePath& source_crx_path, |
| const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version); |
| // Packs the given |extension_dir_path| (using the |pem_path| if provided or a |
| // random key otherwise) and makes the produced CRX file served by the |
| // embedded test server. |
| bool CreateAndServeCrx(const base::FilePath& extension_dir_path, |
| const std::optional<base::FilePath>& pem_path, |
| const base::Version& extension_version, |
| extensions::ExtensionId* extension_id); |
| // Force-installs the CRX file served by the embedded test server. |
| bool ForceInstallFromServedCrx(const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version, |
| WaitMode wait_mode); |
| // Creates an update manifest with the CRX URL pointing to the embedded test |
| // server. |
| bool CreateAndServeUpdateManifestFile( |
| const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version); |
| // Sets the policy to force-install the given extension from the given update |
| // manifest URL. |
| bool UpdatePolicy(const extensions::ExtensionId& extension_id, |
| const GURL& update_manifest_url); |
| // Waits until the extension's given version is installed and gets into the |
| // requested mode. Does nothing if |wait_mode| is |kNone|. |
| bool WaitForExtensionUpdate(const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version, |
| UpdateWaitMode wait_mode); |
| |
| base::ScopedTempDir temp_dir_; |
| net::EmbeddedTestServer embedded_test_server_; |
| bool initialized_ = false; |
| raw_ptr<Profile, DanglingUntriaged> profile_ = nullptr; |
| raw_ptr<policy::MockConfigurationPolicyProvider> mock_policy_provider_ = |
| nullptr; |
| #if BUILDFLAG(IS_CHROMEOS) |
| raw_ptr<ash::DeviceStateMixin> device_state_mixin_ = nullptr; |
| raw_ptr<policy::DevicePolicyCrosTestHelper> device_policy_cros_test_helper_ = |
| nullptr; |
| raw_ptr<ash::EmbeddedPolicyTestServerMixin> policy_test_server_mixin_ = |
| nullptr; |
| raw_ptr<policy::UserPolicyBuilder> user_policy_builder_ = nullptr; |
| // |account_id_| and |policy_type_| are only used with |
| // |policy_test_server_mixin_|. |
| std::string account_id_; |
| std::string policy_type_; |
| #endif |
| // Mapping from the extension ID to the PEM file (the supplied or a randomly |
| // generated one). It's not populated for extensions installed from CRX files, |
| // since there's no PEM file available in that case. |
| std::map<extensions::ExtensionId, base::FilePath> extension_id_to_pem_path_; |
| // The current error mode. Stored in an atomic variable, as the server's |
| // request handlers are reading from it on IO thread. |
| std::atomic<ServerErrorMode> server_error_mode_{ServerErrorMode::kNone}; |
| }; |
| |
| #endif // CHROME_BROWSER_POLICY_EXTENSION_FORCE_INSTALL_MIXIN_H_ |