chromium/chrome/browser/ash/app_mode/load_profile.cc

// Copyright 2013 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/ash/app_mode/load_profile.h"

#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <variant>

#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/sequence_checker.h"
#include "base/syslog_logging.h"
#include "base/system/sys_info.h"
#include "base/thread_annotations.h"
#include "base/types/expected.h"
#include "chrome/browser/ash/app_mode/cancellable_job.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launch_error.h"
#include "chrome/browser/ash/app_mode/kiosk_app_types.h"
#include "chrome/browser/ash/app_mode/retry_runner.h"
#include "chrome/browser/ash/login/auth/chrome_login_performer.h"
#include "chrome/browser/ash/login/session/user_session_manager.h"
#include "chrome/browser/ash/login/ui/login_display_host.h"
#include "chromeos/ash/components/dbus/cryptohome/UserDataAuth.pb.h"
#include "chromeos/ash/components/dbus/userdataauth/userdataauth_client.h"
#include "chromeos/ash/components/login/auth/auth_events_recorder.h"
#include "chromeos/ash/components/login/auth/login_performer.h"
#include "chromeos/ash/components/login/auth/public/auth_failure.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user_names.h"

namespace ash::kiosk {

namespace {

bool IsTestOrLinuxChromeOS() {
  // This code should only run in Chrome OS, so not `IsRunningOnChromeOS()`
  // means it's either a test or linux-chromeos.
  return !base::SysInfo::IsRunningOnChromeOS();
}

KioskAppLaunchError::Error LoginFailureToKioskLaunchError(
    const AuthFailure& error) {
  switch (error.reason()) {
    case AuthFailure::COULD_NOT_MOUNT_TMPFS:
    case AuthFailure::COULD_NOT_MOUNT_CRYPTOHOME:
      return KioskAppLaunchError::Error::kUnableToMount;
    case AuthFailure::DATA_REMOVAL_FAILED:
      return KioskAppLaunchError::Error::kUnableToRemove;
    case AuthFailure::USERNAME_HASH_FAILED:
      return KioskAppLaunchError::Error::kUnableToRetrieveHash;
    default:
      LOG(ERROR) << "KIOSK launch error because of AuthFailure::FailureReason: "
                 << error.reason();
      return KioskAppLaunchError::Error::kUnableToMount;
  }
}

CryptohomeMountState ToResult(
    std::optional<user_data_auth::IsMountedReply> reply) {
  if (!reply.has_value()) {
    return CryptohomeMountState::kServiceUnavailable;
  }
  if (IsTestOrLinuxChromeOS()) {
    // In tests and in linux-chromeos there is no real cryptohome, and the fake
    // one always replies with `is_mounted()` true. We override the reply so
    // Kiosk login can proceed.
    return CryptohomeMountState::kNotMounted;
  }
  return reply->is_mounted() ? CryptohomeMountState::kMounted
                             : CryptohomeMountState::kNotMounted;
}

void CheckCryptohomeMountState(CryptohomeMountStateCallback on_done) {
  UserDataAuthClient::Get()->WaitForServiceToBeAvailable(base::BindOnce(
      [](CryptohomeMountStateCallback on_done, bool service_is_ready) {
        if (!service_is_ready || !UserDataAuthClient::Get()) {
          return std::move(on_done).Run(
              CryptohomeMountState::kServiceUnavailable);
        }

        UserDataAuthClient::Get()->IsMounted(
            user_data_auth::IsMountedRequest(),
            base::BindOnce(&ToResult).Then(std::move(on_done)));
      },
      std::move(on_done)));
}

// Checks the mount state of cryptohome and retries if the cryptohome service is
// not yet available.
std::unique_ptr<CancellableJob> CheckCryptohome(
    CryptohomeMountStateCallback on_done) {
  return RunUpToNTimes<CryptohomeMountState>(
      /*n=*/5,
      /*job=*/base::BindRepeating(&CheckCryptohomeMountState),
      /*should_retry=*/
      base::BindRepeating([](const CryptohomeMountState& result) {
        return result == CryptohomeMountState::kServiceUnavailable;
      }),
      std::move(on_done));
}

class SigninPerformer : public LoginPerformer::Delegate, public CancellableJob {
 public:
  static std::unique_ptr<CancellableJob> Run(
      KioskAppType app_type,
      AccountId account_id,
      PerformSigninResultCallback on_done) {
    auto handle = base::WrapUnique(new SigninPerformer(std::move(on_done)));

    switch (app_type) {
      case KioskAppType::kChromeApp:
        handle->login_performer_->LoginAsKioskAccount(account_id);
        break;
      case KioskAppType::kWebApp:
        handle->login_performer_->LoginAsWebKioskAccount(account_id);
        break;
      case KioskAppType::kIsolatedWebApp:
        // TODO(crbug.com/361019018): add profile load and login.
        NOTIMPLEMENTED();
        break;
    }

    return handle;
  }

  SigninPerformer(const SigninPerformer&) = delete;
  SigninPerformer& operator=(const SigninPerformer&) = delete;
  ~SigninPerformer() override = default;

 private:
  explicit SigninPerformer(PerformSigninResultCallback on_done)
      : on_done_(std::move(on_done)),
        login_performer_(std::make_unique<ChromeLoginPerformer>(
            this,
            AuthEventsRecorder::Get())) {}

  // LoginPerformer::Delegate overrides:
  void OnAuthSuccess(const UserContext& user_context) override {
    // `LoginPerformer` manages its own lifecycle on success, release ownership.
    login_performer_->set_delegate(nullptr);
    std::ignore = login_performer_.release();

    std::move(on_done_).Run(user_context);
  }
  void OnAuthFailure(const AuthFailure& auth_error) override {
    KioskAppLaunchError::SaveCryptohomeFailure(auth_error);
    std::move(on_done_).Run(base::unexpected(auth_error));
  }
  void PolicyLoadFailed() override {
    std::move(on_done_).Run(
        base::unexpected(PerformSigninError::kPolicyLoadFailed));
  }
  void AllowlistCheckFailed(const std::string& email) override {
    std::move(on_done_).Run(
        base::unexpected(PerformSigninError::kAllowlistCheckFailed));
  }
  void OnOldEncryptionDetected(std::unique_ptr<UserContext> user_context,
                               bool has_incomplete_migration) override {
    NOTREACHED();
  }

  PerformSigninResultCallback on_done_;
  std::unique_ptr<LoginPerformer> login_performer_;
};

bool IsRetriableError(const PerformSigninResult& result) {
  if (!result.has_value() &&
      std::holds_alternative<AuthFailure>(result.error())) {
    // Signal a retriable error if the cryptohome mount failed due to
    // corruption of the on-disk state. We always ask to "create" cryptohome
    // and the corrupted one was deleted under the hood.
    return std::get<AuthFailure>(result.error()).reason() ==
           AuthFailure::UNRECOVERABLE_CRYPTOHOME;
  }
  return false;
}

std::unique_ptr<CancellableJob> Signin(KioskAppType app_type,
                                       AccountId account_id,
                                       PerformSigninResultCallback on_done) {
  return RunUpToNTimes<PerformSigninResult>(
      /*n=*/3,
      /*job=*/
      base::BindRepeating(
          [](KioskAppType app_type, AccountId account_id,
             RetryResultCallback<PerformSigninResult> on_result) {
            return SigninPerformer::Run(app_type, account_id,
                                        std::move(on_result));
          },
          app_type, account_id),
      /*should_retry=*/base::BindRepeating(&IsRetriableError),
      /*on_done=*/
      std::move(on_done));
}

KioskAppLaunchError::Error SigninErrorToKioskLaunchError(
    PerformSigninError error) {
  switch (error) {
    case PerformSigninError::kPolicyLoadFailed:
      return KioskAppLaunchError::Error::kPolicyLoadFailed;
    case PerformSigninError::kAllowlistCheckFailed:
      return KioskAppLaunchError::Error::kUserNotAllowlisted;
  }
}

class SessionStarter : public CancellableJob,
                       public UserSessionManagerDelegate {
 public:
  static std::unique_ptr<CancellableJob> Run(
      const UserContext& user_context,
      StartSessionResultCallback on_done) {
    auto handle = base::WrapUnique(new SessionStarter(std::move(on_done)));
    UserSessionManager::GetInstance()->StartSession(
        user_context, UserSessionManager::StartSessionType::kPrimary,
        /*has_auth_cookies=*/false,
        /*has_active_session=*/false,
        /*delegate=*/handle->weak_ptr_factory_.GetWeakPtr());
    return handle;
  }

  SessionStarter(const SessionStarter&) = delete;
  SessionStarter& operator=(const SessionStarter&) = delete;
  ~SessionStarter() override = default;

 private:
  explicit SessionStarter(StartSessionResultCallback on_done)
      : on_done_(std::move(on_done)) {}

  // UserSessionManagerDelegate implementation:
  void OnProfilePrepared(Profile* profile, bool browser_launched) override {
    std::move(on_done_).Run(CHECK_DEREF(profile));
  }

  base::WeakPtr<UserSessionManagerDelegate> AsWeakPtr() override {
    return weak_ptr_factory_.GetWeakPtr();
  }

  StartSessionResultCallback on_done_;
  base::WeakPtrFactory<SessionStarter> weak_ptr_factory_{this};
};

void LogErrorToSyslog(KioskAppLaunchError::Error error) {
  switch (error) {
    case KioskAppLaunchError::Error::kCryptohomedNotRunning:
      SYSLOG(ERROR) << "Cryptohome not available when loading Kiosk profile.";
      break;
    case KioskAppLaunchError::Error::kAlreadyMounted:
      SYSLOG(ERROR) << "Cryptohome already mounted when loading Kiosk profile.";
      break;
    case KioskAppLaunchError::Error::kUserNotAllowlisted:
      SYSLOG(ERROR) << "LoginPerformer disallowed Kiosk user sign in.";
      break;
    default:
      SYSLOG(ERROR) << "Unexpected error " << (int)error;
  }
}

// Helper class that implements the functionality of `LoadProfile`.
// See docs on that function for more information.
class ProfileLoader : public CancellableJob {
 public:
  static std::unique_ptr<CancellableJob> Run(
      const AccountId& app_account_id,
      KioskAppType app_type,
      CheckCryptohomeCallback check_cryptohome,
      PerformSigninCallback perform_signin,
      StartSessionCallback start_session,
      LoadProfileResultCallback on_done);

  ProfileLoader(const ProfileLoader&) = delete;
  ProfileLoader& operator=(const ProfileLoader&) = delete;
  ~ProfileLoader() override;

 private:
  ProfileLoader(const AccountId& app_account_id,
                KioskAppType app_type,
                CheckCryptohomeCallback check_cryptohome,
                PerformSigninCallback perform_signin,
                StartSessionCallback start_session,
                LoadProfileResultCallback on_done);

  void CheckCryptohomeIsNotMounted();
  void LoginAsKioskAccount();
  void PrepareProfile(const UserContext& user_context);
  void ReturnSuccess(Profile& profile);
  void ReturnError(KioskAppLaunchError::Error result);

  const AccountId account_id_;
  const KioskAppType app_type_;

  // `current_step_` is a handle to the job currently being executed. The
  // possible steps are listed in the callbacks below.
  std::unique_ptr<CancellableJob> current_step_
      GUARDED_BY_CONTEXT(sequence_checker_);
  CheckCryptohomeCallback check_cryptohome_;
  PerformSigninCallback perform_signin_;
  StartSessionCallback start_session_;

  LoadProfileResultCallback on_done_ GUARDED_BY_CONTEXT(sequence_checker_);

  SEQUENCE_CHECKER(sequence_checker_);
};

std::unique_ptr<CancellableJob> ProfileLoader::Run(
    const AccountId& app_account_id,
    KioskAppType app_type,
    CheckCryptohomeCallback check_cryptohome,
    PerformSigninCallback perform_signin,
    StartSessionCallback start_session,
    LoadProfileResultCallback on_done) {
  auto loader = base::WrapUnique(new ProfileLoader(
      app_account_id, app_type, std::move(check_cryptohome),
      std::move(perform_signin), std::move(start_session), std::move(on_done)));
  loader->CheckCryptohomeIsNotMounted();
  return loader;
}

ProfileLoader::ProfileLoader(const AccountId& app_account_id,
                             KioskAppType app_type,
                             CheckCryptohomeCallback check_cryptohome,
                             PerformSigninCallback perform_signin,
                             StartSessionCallback start_session,
                             LoadProfileResultCallback on_done)
    : account_id_(app_account_id),
      app_type_(app_type),
      check_cryptohome_(std::move(check_cryptohome)),
      perform_signin_(std::move(perform_signin)),
      start_session_(std::move(start_session)),
      on_done_(std::move(on_done)) {}

ProfileLoader::~ProfileLoader() = default;

void ProfileLoader::CheckCryptohomeIsNotMounted() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  current_step_ =
      std::move(check_cryptohome_)
          .Run(base::BindOnce(
              [](ProfileLoader* self, CryptohomeMountState result) {
                switch (result) {
                  case CryptohomeMountState::kNotMounted:
                    return self->LoginAsKioskAccount();
                  case CryptohomeMountState::kMounted:
                    return self->ReturnError(
                        KioskAppLaunchError::Error::kAlreadyMounted);
                  case CryptohomeMountState::kServiceUnavailable:
                    return self->ReturnError(
                        KioskAppLaunchError::Error::kCryptohomedNotRunning);
                }
              },
              // Safe because `this` owns `current_step_`
              base::Unretained(this)));
}

void ProfileLoader::LoginAsKioskAccount() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  current_step_ =
      std::move(perform_signin_)
          .Run(app_type_, account_id_,
               /*on_done=*/
               base::BindOnce(
                   [](ProfileLoader* self, PerformSigninResult result) {
                     if (result.has_value()) {
                       return self->PrepareProfile(result.value());
                     } else if (auto* error = std::get_if<PerformSigninError>(
                                    &result.error())) {
                       return self->ReturnError(
                           SigninErrorToKioskLaunchError(*error));
                     } else if (auto* auth_failure =
                                    std::get_if<AuthFailure>(&result.error())) {
                       return self->ReturnError(
                           LoginFailureToKioskLaunchError(*auth_failure));
                     }
                     NOTREACHED();
                   },
                   // Safe because `this` owns `current_step_`
                   base::Unretained(this)));
}

void ProfileLoader::PrepareProfile(const UserContext& user_context) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  current_step_ =
      std::move(start_session_)
          .Run(user_context,
               base::BindOnce(&ProfileLoader::ReturnSuccess,
                              // Safe because `this` owns `current_step_`
                              base::Unretained(this)));
}

void ProfileLoader::ReturnSuccess(Profile& profile) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  current_step_.reset();
  std::move(on_done_).Run(&profile);
}

void ProfileLoader::ReturnError(KioskAppLaunchError::Error result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  current_step_.reset();
  LogErrorToSyslog(result);
  std::move(on_done_).Run(base::unexpected(std::move(result)));
}

}  // namespace

std::unique_ptr<CancellableJob> LoadProfile(const AccountId& app_account_id,
                                            KioskAppType app_type,
                                            LoadProfileResultCallback on_done) {
  return LoadProfileWithCallbacks(
      app_account_id, app_type, base::BindOnce(&CheckCryptohome),
      base::BindOnce(&Signin), base::BindOnce(&SessionStarter::Run),
      std::move(on_done));
}

std::unique_ptr<CancellableJob> LoadProfileWithCallbacks(
    const AccountId& app_account_id,
    KioskAppType app_type,
    CheckCryptohomeCallback check_cryptohome,
    PerformSigninCallback perform_signin,
    StartSessionCallback start_session,
    LoadProfileResultCallback on_done) {
  return ProfileLoader::Run(
      app_account_id, app_type, std::move(check_cryptohome),
      std::move(perform_signin), std::move(start_session), std::move(on_done));
}

}  // namespace ash::kiosk