chromium/chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_manager.cc

// Copyright 2022 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/web_applications/isolated_web_apps/policy/isolated_web_app_policy_manager.h"

#include <algorithm>
#include <functional>
#include <memory>
#include <optional>
#include <ostream>
#include <string>

#include "base/barrier_callback.h"
#include "base/barrier_closure.h"
#include "base/check_deref.h"
#include "base/containers/map_util.h"
#include "base/containers/to_value_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_file.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/functional/overloaded.h"
#include "base/i18n/time_formatting.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/strings/to_string.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/types/cxx23_to_underlying.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/version.h"
#include "chrome/browser/web_applications/callback_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/install_isolated_web_app_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_downloader.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_constants.h"
#include "chrome/browser/web_applications/isolated_web_apps/update_manifest/update_manifest.h"
#include "chrome/browser/web_applications/isolated_web_apps/update_manifest/update_manifest_fetcher.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chromeos/components/mgs/managed_guest_session_utils.h"
#include "components/prefs/pref_service.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/isolated_web_apps_policy.h"
#include "net/base/backoff_entry.h"
#include "net/base/net_errors.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "url/gurl.h"

namespace web_app {

namespace {

constexpr net::BackoffEntry::Policy kInstallRetryBackoffPolicy = {
    /*num_errors_to_ignore=*/0,
    /*initial_delay_ms=*/60 * 1000,
    /*multiply_factor=*/2.0,
    /*jitter_factor=*/0.0,
    /*maximum_backoff_ms=*/5 * 60 * 60 * 1000,
    /*entry_lifetime_ms=*/-1,
    /*always_use_initial_delay=*/false,
};

constexpr int kIsolatedWebAppForceInstallMaxRetryTreshold = 2;
constexpr base::TimeDelta kIsolatedWebAppForceInstallEmergencyDelay =
    base::Hours(5);

constexpr auto kUpdateManifestFetchTrafficAnnotation =
    net::DefinePartialNetworkTrafficAnnotation("iwa_policy_update_manifest",
                                               "iwa_update_manifest_fetcher",
                                               R"(
  semantics {
    sender: "Isolated Web App Policy Manager"
    description:
      "Downloads the update manifest of an Isolated Web App that is provided "
      "in an enterprise policy by the administrator. The update manifest "
      "contains at least the list of the available versions of the IWA "
      "and the URL to the Signed Web Bundles that correspond to each version."
    trigger:
      "Installation/update of a IWA from the enterprise policy requires "
      "fetching of a IWA Update Manifest"
  }
  policy {
    setting: "This feature cannot be disabled in settings."
    chrome_policy {
      IsolatedWebAppInstallForceList {
        IsolatedWebAppInstallForceList: ""
      }
    }
  })");

constexpr auto kWebBundleDownloadTrafficAnnotation =
    net::DefinePartialNetworkTrafficAnnotation("iwa_policy_signed_web_bundle",
                                               "iwa_bundle_downloader",
                                               R"(
  semantics {
    sender: "Isolated Web App Policy Manager"
    description:
      "Downloads the Signed Web Bundle of an Isolated Web App (IWA) from the "
      "URL read from an Update Manifest that is provided in an enterprise "
      "policy by the administrator. The Signed Web Bundle contains code and "
      "other resources of the IWA."
    trigger:
      "An Isolated Web App is installed from an enterprise policy."
  }
  policy {
    setting: "This feature cannot be disabled in settings."
    chrome_policy {
      IsolatedWebAppInstallForceList {
        IsolatedWebAppInstallForceList: ""
      }
    }
  })");

std::vector<IsolatedWebAppExternalInstallOptions> ParseIwaPolicyValues(
    const base::Value::List& iwa_policy_values) {
  std::vector<IsolatedWebAppExternalInstallOptions> iwa_install_options;
  iwa_install_options.reserve(iwa_policy_values.size());
  for (const auto& policy_entry : iwa_policy_values) {
    const base::expected<IsolatedWebAppExternalInstallOptions, std::string>
        options = IsolatedWebAppExternalInstallOptions::FromPolicyPrefValue(
            policy_entry);
    if (options.has_value()) {
      iwa_install_options.push_back(options.value());
    } else {
      LOG(ERROR) << "Could not interpret IWA force-install policy: "
                 << options.error();
    }
  }

  return iwa_install_options;
}

// Add the install source to the already installed app.
struct AppActionAddPolicyInstallSource {
  AppActionAddPolicyInstallSource() {}

  base::Value::Dict GetDebugValue() const {
    return base::Value::Dict().Set("type", "AppActionAddPolicyInstallSource");
  }
};

// Remove the install source from the already installed app, possibly
// uninstalling it if no more sources are remaining.
struct AppActionRemoveInstallSource {
  explicit AppActionRemoveInstallSource(WebAppManagement::Type source)
      : source(source) {}

  base::Value::Dict GetDebugValue() const {
    return base::Value::Dict()
        .Set("type", "AppActionRemoveInstallSource")
        .Set("source", base::ToString(source));
  }

  WebAppManagement::Type source;
};

// Install the app. Will error if it is already installed.
struct AppActionInstall {
  explicit AppActionInstall(IsolatedWebAppExternalInstallOptions options)
      : options(std::move(options)) {}

  base::Value::Dict GetDebugValue() const {
    return base::Value::Dict()
        .Set("type", "AppActionInstall")
        .Set("update_manifest_url",
             options.update_manifest_url().possibly_invalid_spec());
  }

  IsolatedWebAppExternalInstallOptions options;
};

using AppAction = absl::variant<AppActionRemoveInstallSource,
                                AppActionAddPolicyInstallSource,
                                AppActionInstall>;
using AppActions = base::flat_map<web_package::SignedWebBundleId, AppAction>;

}  // namespace

namespace internal {

IwaInstaller::IwaInstallCommandWrapperImpl::IwaInstallCommandWrapperImpl(
    web_app::WebAppProvider* provider)
    : provider_(provider) {}

void IwaInstaller::IwaInstallCommandWrapperImpl::Install(
    const IsolatedWebAppInstallSource& install_source,
    const IsolatedWebAppUrlInfo& url_info,
    const base::Version& expected_version,
    WebAppCommandScheduler::InstallIsolatedWebAppCallback callback) {
  // There is no need to keep the browser or profile alive when
  // policy-installing an IWA. If the browser or profile shut down, installation
  // will be re-attempted the next time they start, assuming that the policy is
  // still set.
  provider_->scheduler().InstallIsolatedWebApp(
      url_info, install_source, expected_version,
      /*optional_keep_alive=*/nullptr,
      /*optional_profile_keep_alive=*/nullptr, std::move(callback));
}

IwaInstallerResult::IwaInstallerResult(Type type, std::string message)
    : type_(type), message_(std::move(message)) {}

base::Value::Dict IwaInstallerResult::ToDebugValue() const {
  return base::Value::Dict()
      .Set("type", base::ToString(type_))
      .Set("message", message_);
}

IwaInstaller::IwaInstaller(
    IsolatedWebAppExternalInstallOptions install_options,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    std::unique_ptr<IwaInstallCommandWrapper> install_command_wrapper,
    base::Value::List& log,
    ResultCallback callback)
    : install_options_(std::move(install_options)),
      url_loader_factory_(std::move(url_loader_factory)),
      install_command_wrapper_(std::move(install_command_wrapper)),
      log_(log),
      callback_(std::move(callback)) {}

IwaInstaller::~IwaInstaller() = default;

void IwaInstaller::Start() {
  if (chromeos::IsManagedGuestSession() &&
      !base::FeatureList::IsEnabled(
          features::kIsolatedWebAppManagedGuestSessionInstall)) {
    LOG(ERROR) << "IWA installation in managed guest sessions is disabled.";
    Finish(Result(Result::Type::kErrorManagedGuestSessionInstallDisabled));
    return;
  }

  auto weak_ptr = weak_factory_.GetWeakPtr();
  RunChainedCallbacks(
      base::BindOnce(&IwaInstaller::CreateTempFile, weak_ptr),
      base::BindOnce(&IwaInstaller::DownloadUpdateManifest, weak_ptr),
      base::BindOnce(&IwaInstaller::DownloadWebBundle, weak_ptr),
      base::BindOnce(&IwaInstaller::RunInstallCommand, weak_ptr));
}

void IwaInstaller::CreateTempFile(base::OnceClosure next_step_callback) {
  log_->Append(base::Value("creating temp file"));
  ScopedTempWebBundleFile::Create(base::BindOnce(
      &IwaInstaller::OnTempFileCreated, weak_factory_.GetWeakPtr(),
      std::move(next_step_callback)));
}

void IwaInstaller::OnTempFileCreated(base::OnceClosure next_step_callback,
                                     ScopedTempWebBundleFile bundle) {
  if (!bundle) {
    Finish(Result(Result::Type::kErrorCantCreateTempFile));
    return;
  }
  bundle_ = std::move(bundle);
  log_->Append(
      base::Value(u"created temp file: " + bundle_.path().LossyDisplayName()));
  std::move(next_step_callback).Run();
}

void IwaInstaller::DownloadUpdateManifest(
    base::OnceCallback<void(GURL, base::Version)> next_step_callback) {
  log_->Append(base::Value(
      "Downloading Update Manifest from " +
      install_options_.update_manifest_url().possibly_invalid_spec()));

  update_manifest_fetcher_ = std::make_unique<UpdateManifestFetcher>(
      install_options_.update_manifest_url(),
      kUpdateManifestFetchTrafficAnnotation, url_loader_factory_);
  update_manifest_fetcher_->FetchUpdateManifest(base::BindOnce(
      &IwaInstaller::OnUpdateManifestParsed, weak_factory_.GetWeakPtr(),
      std::move(next_step_callback)));
}

void IwaInstaller::OnUpdateManifestParsed(
    base::OnceCallback<void(GURL, base::Version)> next_step_callback,
    base::expected<UpdateManifest, UpdateManifestFetcher::Error> fetch_result) {
  update_manifest_fetcher_.reset();
  ASSIGN_OR_RETURN(
      UpdateManifest update_manifest, fetch_result,
      [&](UpdateManifestFetcher::Error error) {
        switch (error) {
          case UpdateManifestFetcher::Error::kDownloadFailed:
            Finish(Result(Result::Type::kErrorUpdateManifestDownloadFailed));
            break;
          case UpdateManifestFetcher::Error::kInvalidJson:
          case UpdateManifestFetcher::Error::kInvalidManifest:
            Finish(Result(Result::Type::kErrorUpdateManifestParsingFailed));
            break;
        }
      });

  std::optional<UpdateManifest::VersionEntry> latest_version =
      update_manifest.GetLatestVersion(
          // TODO(b/294481776): In the future, we will support channel selection
          // via policy. For now, we always use the "default" channel.
          UpdateChannelId::default_id());
  if (!latest_version.has_value()) {
    Finish(Result(Result::Type::kErrorWebBundleUrlCantBeDetermined));
    return;
  }

  log_->Append(base::Value("Downloaded Update Manifest. Latest version: " +
                           latest_version->version().GetString() + " from " +
                           latest_version->src().possibly_invalid_spec()));
  std::move(next_step_callback)
      .Run(latest_version->src(), latest_version->version());
}

void IwaInstaller::DownloadWebBundle(
    base::OnceCallback<void(base::Version)> next_step_callback,
    GURL web_bundle_url,
    base::Version expected_version) {
  log_->Append(base::Value("Downloading Web Bundle from " +
                           web_bundle_url.possibly_invalid_spec()));

  bundle_downloader_ = IsolatedWebAppDownloader::CreateAndStartDownloading(
      std::move(web_bundle_url), bundle_.path(),
      kWebBundleDownloadTrafficAnnotation, url_loader_factory_,
      base::BindOnce(&IwaInstaller::OnWebBundleDownloaded,
                     // If `this` is deleted, `bundle_downloader_` is deleted
                     // as well, and thus the callback will never run.
                     base::Unretained(this),
                     base::BindOnce(std::move(next_step_callback),
                                    std::move(expected_version))));
}

void IwaInstaller::OnWebBundleDownloaded(base::OnceClosure next_step_callback,
                                         int32_t net_error) {
  bundle_downloader_.reset();

  if (net_error != net::OK) {
    Finish(Result(Result::Type::kErrorCantDownloadWebBundle,
                  net::ErrorToString(net_error)));
    return;
  }
  log_->Append(base::Value("Downloaded Web Bundle"));
  std::move(next_step_callback).Run();
}

void IwaInstaller::RunInstallCommand(base::Version expected_version) {
  log_->Append(base::Value("Running install command, expected version: " +
                           expected_version.GetString()));
  IsolatedWebAppUrlInfo url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
          install_options_.web_bundle_id());

  // TODO: crbug.com/306638108 - In the time it took to download everything, the
  // app might have already been installed by other means.

  install_command_wrapper_->Install(
      IsolatedWebAppInstallSource::FromExternalPolicy(
          IwaSourceBundleProdModeWithFileOp(bundle_.path(),
                                            IwaSourceBundleProdFileOp::kMove)),
      url_info, std::move(expected_version),
      base::BindOnce(&IwaInstaller::OnIwaInstalled,
                     weak_factory_.GetWeakPtr()));
}

void IwaInstaller::OnIwaInstalled(
    base::expected<InstallIsolatedWebAppCommandSuccess,
                   InstallIsolatedWebAppCommandError> result) {
  if (!result.has_value()) {
    LOG(ERROR) << "Could not install the IWA "
               << install_options_.web_bundle_id();
    Finish(Result(Result::Type::kErrorCantInstallFromWebBundle,
                  base::ToString(result.error())));
  } else {
    Finish(Result(Result::Type::kSuccess));
  }
}

void IwaInstaller::Finish(Result result) {
  std::move(callback_).Run(std::move(result));
}

std::unique_ptr<IwaInstaller> IwaInstallerFactory::Create(
    IsolatedWebAppExternalInstallOptions install_options,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    base::Value::List& log,
    WebAppProvider* provider,
    IwaInstaller::ResultCallback callback) {
  return GetIwaInstallerFactory().Run(std::move(install_options),
                                      std::move(url_loader_factory), log,
                                      provider, std::move(callback));
}

IwaInstallerFactory::IwaInstallerFactoryCallback&
IwaInstallerFactory::GetIwaInstallerFactory() {
  static base::LazyInstance<IwaInstallerFactoryCallback>::Leaky
      iwa_installer_factory = LAZY_INSTANCE_INITIALIZER;
  if (!iwa_installer_factory.Get()) {
    iwa_installer_factory.Get() = base::BindRepeating(
        [](IsolatedWebAppExternalInstallOptions install_options,
           scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
           base::Value::List& log, WebAppProvider* provider,
           IwaInstaller::ResultCallback callback) {
          return std::make_unique<internal::IwaInstaller>(
              std::move(install_options), std::move(url_loader_factory),
              std::make_unique<IwaInstaller::IwaInstallCommandWrapperImpl>(
                  provider),
              log, std::move(callback));
        });
  }
  return iwa_installer_factory.Get();
}

std::ostream& operator<<(std::ostream& os,
                         IwaInstallerResultType install_result_type) {
  using Type = IwaInstallerResultType;

  switch (install_result_type) {
    case Type::kSuccess:
      return os << "kSuccess";
    case Type::kErrorCantCreateTempFile:
      return os << "kErrorCantCreateTempFile";
    case Type::kErrorUpdateManifestDownloadFailed:
      return os << "kErrorUpdateManifestDownloadFailed";
    case Type::kErrorUpdateManifestParsingFailed:
      return os << "kErrorUpdateManifestParsingFailed";
    case Type::kErrorWebBundleUrlCantBeDetermined:
      return os << "kErrorWebBundleUrlCantBeDetermined";
    case Type::kErrorCantDownloadWebBundle:
      return os << "kErrorCantDownloadWebBundle";
    case Type::kErrorCantInstallFromWebBundle:
      return os << "kErrorCantInstallFromWebBundle";
    case Type::kErrorManagedGuestSessionInstallDisabled:
      return os << "kErrorManagedGuestSessionInstallDisabled";
  }
}

}  // namespace internal

// static
void IsolatedWebAppPolicyManager::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterListPref(prefs::kIsolatedWebAppInstallForceList);
  registry->RegisterIntegerPref(
      prefs::kIsolatedWebAppPendingInitializationCount, 0);
}

IsolatedWebAppPolicyManager::IsolatedWebAppPolicyManager(Profile* profile)
    : profile_(profile),
      install_retry_backoff_entry_(&kInstallRetryBackoffPolicy) {}

IsolatedWebAppPolicyManager::~IsolatedWebAppPolicyManager() = default;

void IsolatedWebAppPolicyManager::Start(base::OnceClosure on_started_callback) {
  CHECK(on_started_callback_.is_null());
  on_started_callback_ = std::move(on_started_callback);

  pref_change_registrar_.Init(profile_->GetPrefs());
  pref_change_registrar_.Add(
      prefs::kIsolatedWebAppInstallForceList,
      base::BindRepeating(&IsolatedWebAppPolicyManager::ProcessPolicy,
                          weak_ptr_factory_.GetWeakPtr(),
                          /*finished_closure=*/base::DoNothing()));

  const int pending_inits_count = GetPendingInitCount();
  SetPendingInitCount(pending_inits_count + 1);
  if (pending_inits_count <= kIsolatedWebAppForceInstallMaxRetryTreshold) {
    CleanupAndProcessPolicyOnSessionStart();
  } else {
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &IsolatedWebAppPolicyManager::CleanupAndProcessPolicyOnSessionStart,
            weak_ptr_factory_.GetWeakPtr()),
        kIsolatedWebAppForceInstallEmergencyDelay);
  }

  if (!on_started_callback_.is_null()) {
    std::move(on_started_callback_).Run();
  }
}

void IsolatedWebAppPolicyManager::SetProvider(base::PassKey<WebAppProvider>,
                                              WebAppProvider& provider) {
  provider_ = &provider;
}

#if !BUILDFLAG(IS_CHROMEOS)
static_assert(
    false,
    "Make sure to update `WebAppInternalsHandler` to call "
    "`IsolatedWebAppPolicyManager::GetDebugValue` on non-ChromeOS when "
    "`IsolatedWebAppPolicyManager` is no longer ChromeOS-exclusive.");
#endif

base::Value IsolatedWebAppPolicyManager::GetDebugValue() const {
  return base::Value(
      base::Value::Dict()
          .Set("policy_is_being_processed",
               policy_is_being_processed_
                   ? base::Value(current_process_log_.Clone())
                   : base::Value(false))
          .Set("policy_reprocessing_is_queued", reprocess_policy_needed_)
          .Set("process_logs", process_logs_.ToDebugValue()));
}

void IsolatedWebAppPolicyManager::ProcessPolicy(
    base::OnceClosure finished_closure) {
  CHECK(provider_);
  base::Value::Dict process_log;
  process_log.Set("start_time",
                  base::TimeFormatFriendlyDateAndTime(base::Time::Now()));

  // Ensure that only one policy resolution can happen at one time.
  if (policy_is_being_processed_) {
    reprocess_policy_needed_ = true;
    process_log.Set("warning",
                    "policy is already being processed - waiting for "
                    "processing to finish.");
    process_logs_.AppendCompletedStep(std::move(process_log));
    std::move(finished_closure).Run();
    return;
  }

  policy_is_being_processed_ = true;
  current_process_log_ = std::move(process_log);

  if(!content::IsolatedWebAppsPolicy::AreIsolatedWebAppsEnabled(profile_)) {
    current_process_log_.Set(
        "error",
        "policy is ignored because isolated web apps are not enabled.");
    OnPolicyProcessed();
    std::move(finished_closure).Run();
    return;
  }

  if (chromeos::IsManagedGuestSession() &&
      !base::FeatureList::IsEnabled(
          features::kIsolatedWebAppManagedGuestSessionInstall)) {
    current_process_log_.Set(
        "error", "IWA installation in managed guest sessions is disabled.");
    OnPolicyProcessed();
    return;
  }

  provider_->scheduler().ScheduleCallback<AllAppsLock>(
      "IsolatedWebAppPolicyManager::ProcessPolicy", AllAppsLockDescription(),
      base::BindOnce(&IsolatedWebAppPolicyManager::DoProcessPolicy,
                     weak_ptr_factory_.GetWeakPtr()),
      /*on_complete=*/
      std::move(finished_closure));
}

void IsolatedWebAppPolicyManager::CleanupAndProcessPolicyOnSessionStart() {
  base::RepeatingClosure finished_barrier = base::BarrierClosure(
      /*num_closures=*/2u,
      base::BindOnce(&IsolatedWebAppPolicyManager::SetPendingInitCount,
                     weak_ptr_factory_.GetWeakPtr(),
                     /*pending_count=*/0));

  CleanupOrphanedBundles(/*finished_closure=*/finished_barrier);
  ProcessPolicy(/*finished_closure=*/finished_barrier);
}

int IsolatedWebAppPolicyManager::GetPendingInitCount() {
  PrefService& pref_service = CHECK_DEREF(profile_->GetPrefs());
  if (!pref_service.HasPrefPath(
          prefs::kIsolatedWebAppPendingInitializationCount)) {
    pref_service.SetInteger(prefs::kIsolatedWebAppPendingInitializationCount,
                            0);
  }
  return CHECK_DEREF(profile_->GetPrefs())
      .GetUserPrefValue(prefs::kIsolatedWebAppPendingInitializationCount)
      ->GetIfInt()
      .value_or(0);
}

void IsolatedWebAppPolicyManager::SetPendingInitCount(int pending_count) {
  profile_->GetPrefs()->SetInteger(
      prefs::kIsolatedWebAppPendingInitializationCount, pending_count);
}

void IsolatedWebAppPolicyManager::DoProcessPolicy(
    AllAppsLock& lock,
    base::Value::Dict& debug_info) {
  CHECK(provider_);
  CHECK(install_tasks_.empty());

  std::vector<IsolatedWebAppExternalInstallOptions> apps_in_policy =
      ParseIwaPolicyValues(profile_->GetPrefs()->GetList(
          prefs::kIsolatedWebAppInstallForceList));
  base::flat_map<web_package::SignedWebBundleId,
                 std::reference_wrapper<const WebApp>>
      installed_iwas = GetInstalledIwas(lock.registrar());

  AppActions app_actions;
  size_t number_of_install_tasks = 0;
  for (const IsolatedWebAppExternalInstallOptions& install_options :
       apps_in_policy) {
    std::reference_wrapper<const WebApp>* maybe_installed_app =
        base::FindOrNull(installed_iwas, install_options.web_bundle_id());
    if (!maybe_installed_app) {
      app_actions.emplace(install_options.web_bundle_id(),
                          AppActionInstall(install_options));
      ++number_of_install_tasks;
      continue;
    }
    const WebApp& installed_app = maybe_installed_app->get();

    static_assert(base::ranges::is_sorted(
        std::vector{WebAppManagement::Type::kIwaShimlessRma,
                    // Add further higher priority IWA sources here and make
                    // sure that the `case` statements below are sorted
                    // appropriately...
                    WebAppManagement::Type::kIwaPolicy,
                    // Add further lower priority IWA sources here and make sure
                    // that the `case` statements below are sorted
                    // appropriately...
                    WebAppManagement::Type::kIwaUserInstalled}));
    switch (installed_app.GetHighestPrioritySource()) {
      case WebAppManagement::kSystem:
      case WebAppManagement::kKiosk:
      case WebAppManagement::kPolicy:
      case WebAppManagement::kOem:
      case WebAppManagement::kSubApp:
      case WebAppManagement::kWebAppStore:
      case WebAppManagement::kOneDriveIntegration:
      case WebAppManagement::kSync:
      case WebAppManagement::kApsDefault:
      case WebAppManagement::kDefault: {
        NOTREACHED();
      }

      // Do not touch installed apps if they are managed by a higher priority (=
      // lower numerical value) or by the IWA policy source.
      case WebAppManagement::kIwaShimlessRma:
      case WebAppManagement::kIwaPolicy:
        break;

      case WebAppManagement::kIwaUserInstalled:
        if (!installed_app.isolation_data().has_value() ||
            installed_app.isolation_data()->location.dev_mode()) {
          // Always fully uninstall and then reinstall dev mode apps.
          app_actions.emplace(install_options.web_bundle_id(),
                              AppActionRemoveInstallSource(
                                  WebAppManagement::kIwaUserInstalled));

          // We need to reprocess the policy immediately after, so that the then
          // uninstalled app is re-installed.
          reprocess_policy_needed_ = true;
        } else {
          // For non-dev-mode apps, just add the IWA policy install source. In
          // the future, we might force-downgrade the app here if the version in
          // the policy is lower than the version of the app that is already
          // installed.
          app_actions.emplace(install_options.web_bundle_id(),
                              AppActionAddPolicyInstallSource());
        }
        break;
    }
  }

  for (const auto& [web_bundle_id, _] : installed_iwas) {
    if (!base::Contains(apps_in_policy, web_bundle_id,
                        &IsolatedWebAppExternalInstallOptions::web_bundle_id)) {
      app_actions.emplace(web_bundle_id, AppActionRemoveInstallSource(
                                             WebAppManagement::kIwaPolicy));
    }
  }

  debug_info.Set("apps_in_policy",
                 base::ToValueList(apps_in_policy, [](const auto& options) {
                   return base::ToString(options.web_bundle_id());
                 }));
  debug_info.Set(
      "installed_iwas",
      base::ToValueList(installed_iwas, [](const auto& installed_iwa) {
        const auto& [web_bundle_id, _] = installed_iwa;
        return base::ToString(web_bundle_id);
      }));
  debug_info.Set(
      "app_actions", base::ToValueList(app_actions, [](const auto& entry) {
        const auto& [web_bundle_id, app_action] = entry;
        return base::Value::Dict()
            .Set("web_bundle_id", base::ToString(web_bundle_id))
            .Set("action", absl::visit(base::Overloaded{[](const auto& action) {
                                         return action.GetDebugValue();
                                       }},
                                       app_action));
      }));
  current_process_log_.Merge(debug_info.Clone());

  auto action_done_callback = base::BarrierClosure(
      app_actions.size(),
      // Always asynchronously exit this method so that `lock` is released
      // before the next method is called.
      base::BindOnce(
          [](base::WeakPtr<IsolatedWebAppPolicyManager> weak_ptr) {
            base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
                FROM_HERE,
                base::BindOnce(&IsolatedWebAppPolicyManager::OnPolicyProcessed,
                               std::move(weak_ptr)));
          },
          weak_ptr_factory_.GetWeakPtr()));
  auto install_task_done_callback =
      base::BarrierCallback<internal::IwaInstaller::Result>(
          number_of_install_tasks,
          base::BindOnce(
              &IsolatedWebAppPolicyManager::OnAllInstallTasksCompleted,
              weak_ptr_factory_.GetWeakPtr()));

  for (const auto& [web_bundle_id, app_action] : app_actions) {
    auto url_info =
        IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(web_bundle_id);
    absl::visit(
        base::Overloaded{
            [&](const AppActionAddPolicyInstallSource& action) {
              auto callback = [&]() {
                LogAddPolicyInstallSourceResult(web_bundle_id);
                action_done_callback.Run();
              };

              {
                ScopedRegistryUpdate update = lock.sync_bridge().BeginUpdate();
                WebApp* app_to_update = update->UpdateApp(url_info.app_id());
                app_to_update->AddSource(WebAppManagement::Type::kIwaPolicy);
              }
              // Trigger update discovery here, because the Update Manifest URL
              // might have changed.
              provider_->iwa_update_manager().MaybeDiscoverUpdatesForApp(
                  url_info.app_id());

              callback();
            },
            [&](const AppActionRemoveInstallSource& action) {
              auto callback = base::BindOnce(&IsolatedWebAppPolicyManager::
                                                 LogRemoveInstallSourceResult,
                                             weak_ptr_factory_.GetWeakPtr(),
                                             web_bundle_id, action.source)
                                  .Then(action_done_callback);

              provider_->scheduler().RemoveInstallManagementMaybeUninstall(
                  url_info.app_id(), action.source,
                  webapps::WebappUninstallSource::kIwaEnterprisePolicy,
                  std::move(callback));
            },
            [&](const AppActionInstall& action) {
              auto weak_ptr = weak_ptr_factory_.GetWeakPtr();

              auto callback =
                  base::BindOnce(
                      &IsolatedWebAppPolicyManager::OnInstallTaskCompleted,
                      weak_ptr, web_bundle_id, install_task_done_callback)
                      .Then(base::BindOnce(&IsolatedWebAppPolicyManager::
                                               MaybeStartNextInstallTask,
                                           weak_ptr))
                      .Then(action_done_callback);

              auto installer = internal::IwaInstallerFactory::Create(
                  action.options, profile_->GetURLLoaderFactory(),
                  *current_process_log_.EnsureDict("install_progress")
                       ->EnsureList(base::ToString(web_bundle_id)),
                  provider_, std::move(callback));
              install_tasks_.push(std::move(installer));
            },
        },
        app_action);
  }

  MaybeStartNextInstallTask();
}

void IsolatedWebAppPolicyManager::LogAddPolicyInstallSourceResult(
    web_package::SignedWebBundleId web_bundle_id) {
  current_process_log_.EnsureDict("add_install_source_results")
      ->Set(base::ToString(web_bundle_id), "success");
}

void IsolatedWebAppPolicyManager::LogRemoveInstallSourceResult(
    web_package::SignedWebBundleId web_bundle_id,
    WebAppManagement::Type source,
    webapps::UninstallResultCode uninstall_code) {
  if (!webapps::UninstallSucceeded(uninstall_code)) {
    DLOG(WARNING) << "Could not remove install source " << source
                  << " from IWA " << web_bundle_id
                  << ". Error: " << uninstall_code;
  }
  current_process_log_.EnsureDict("remove_install_source_results")
      ->EnsureDict(base::ToString(web_bundle_id))
      ->Set(base::ToString(source), base::ToString(uninstall_code));
}

void IsolatedWebAppPolicyManager::OnInstallTaskCompleted(
    web_package::SignedWebBundleId web_bundle_id,
    base::RepeatingCallback<void(internal::IwaInstaller::Result)> callback,
    internal::IwaInstaller::Result install_result) {
  // Remove the completed task from the queue.
  install_tasks_.pop();

  if (install_result.type() != internal::IwaInstallerResultType::kSuccess) {
    DLOG(WARNING) << "Could not force-install IWA " << web_bundle_id
                  << ". Error: " << install_result.ToDebugValue();
  }
  current_process_log_.EnsureDict("install_results")
      ->Set(base::ToString(web_bundle_id), install_result.ToDebugValue());

  callback.Run(install_result);
}

void IsolatedWebAppPolicyManager::OnAllInstallTasksCompleted(
    std::vector<internal::IwaInstaller::Result> install_results) {
  if (install_results.empty()) {
    return;
  }

  const bool any_task_failed = base::ranges::any_of(
      install_results, [](const internal::IwaInstaller::Result& result) {
        return result.type() != internal::IwaInstallerResultType::kSuccess;
      });

  if (any_task_failed) {
    install_retry_backoff_entry_.InformOfRequest(/*succeeded=*/false);
    CleanupOrphanedBundles(/*finished_closure=*/base::DoNothing());
  } else {
    install_retry_backoff_entry_.Reset();
    return;
  }

  // No retry needed if it was already scheduled --> Exit early.
  if (reprocess_policy_needed_) {
    return;
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&IsolatedWebAppPolicyManager::ProcessPolicy,
                     weak_ptr_factory_.GetWeakPtr(),
                     /*finished_closure=*/base::DoNothing()),
      install_retry_backoff_entry_.GetTimeUntilRelease());
}

void IsolatedWebAppPolicyManager::MaybeStartNextInstallTask() {
  if (!install_tasks_.empty()) {
    install_tasks_.front()->Start();
  }
}

void IsolatedWebAppPolicyManager::OnPolicyProcessed() {
  process_logs_.AppendCompletedStep(
      std::exchange(current_process_log_, base::Value::Dict()));

  policy_is_being_processed_ = false;

  if (!on_started_callback_.is_null()) {
    std::move(on_started_callback_).Run();
  }

  if (reprocess_policy_needed_) {
    reprocess_policy_needed_ = false;
    ProcessPolicy(/*finished_closure=*/base::DoNothing());
  }
  // TODO (peletskyi): Check policy compliance here as in theory
  // more race conditions are possible.
}

void IsolatedWebAppPolicyManager::CleanupOrphanedBundles(
    base::OnceClosure finished_closure) {
  provider_->scheduler().CleanupOrphanedIsolatedApps(
      base::IgnoreArgs<
          base::expected<CleanupOrphanedIsolatedWebAppsCommandSuccess,
                         CleanupOrphanedIsolatedWebAppsCommandError>>(
          std::move(finished_closure)));
}

IsolatedWebAppPolicyManager::ProcessLogs::ProcessLogs() = default;
IsolatedWebAppPolicyManager::ProcessLogs::~ProcessLogs() = default;

void IsolatedWebAppPolicyManager::ProcessLogs::AppendCompletedStep(
    base::Value::Dict log) {
  log.Set("end_time", base::TimeFormatFriendlyDateAndTime(base::Time::Now()));

  // Keep only the most recent `kMaxEntries`.
  logs_.emplace_front(std::move(log));
  if (logs_.size() > kMaxEntries) {
    logs_.pop_back();
  }
}

base::Value IsolatedWebAppPolicyManager::ProcessLogs::ToDebugValue() const {
  return base::Value(base::ToValueList(logs_, &base::Value::Dict::Clone));
}

}  // namespace web_app