blob: 7209838dc58949ccd82c500eb39ab22b365ba62f [file] [log] [blame]
// 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_