chromium/chrome/browser/ash/policy/remote_commands/crd/device_command_start_crd_session_job.cc

// Copyright 2018 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/policy/remote_commands/crd/device_command_start_crd_session_job.h"

#include <iomanip>
#include <memory>
#include <optional>
#include <string_view>
#include <utility>

#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ash/policy/remote_commands/crd/crd_logging.h"
#include "chrome/browser/ash/policy/remote_commands/crd/crd_remote_command_utils.h"
#include "chrome/browser/ash/policy/remote_commands/crd/crd_uma_logger.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/device_identity/device_oauth2_token_service.h"
#include "chrome/browser/device_identity/device_oauth2_token_service_factory.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/pref_names.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "components/prefs/pref_service.h"
#include "remoting/host/chromeos/features.h"

namespace policy {

namespace {

using SessionParameters = StartCrdSessionJobDelegate::SessionParameters;
using ErrorCallback = StartCrdSessionJobDelegate::ErrorCallback;

// Job parameters fields:

// Job requires that UI was idle for at least this period of time
// to proceed. If absent / equal to 0, job will proceed regardless of user
// activity.
const char kIdlenessCutoffFieldName[] = "idlenessCutoffSec";

// True if the admin has confirmed that they want to start the CRD session
// while a user is currently using the device.
const char kAckedUserPresenceFieldName[] = "ackedUserPresence";

// The type of CRD session that the admin wants to start.
const char kCrdSessionTypeFieldName[] = "crdSessionType";

// The admin's email address.
const char kAdminEmailFieldName[] = "adminEmail";

// Result payload fields:

// Integer value containing DeviceCommandStartCrdSessionJob::ResultCode
const char kResultCodeFieldName[] = "resultCode";

// CRD Access Code if job was completed successfully
const char kResultAccessCodeFieldName[] = "accessCode";

// Optional detailed error message for error result codes.
const char kResultMessageFieldName[] = "message";

// Period in seconds since last user activity, if job finished with
// FAILURE_NOT_IDLE result code.
const char kResultLastActivityFieldName[] = "lastActivitySec";

std::optional<std::string> FindString(const base::Value::Dict& dict,
                                      std::string_view key) {
  if (!dict.contains(key)) {
    return std::nullopt;
  }
  return *dict.FindString(key);
}

void SendResultCodeToUma(CrdSessionType crd_session_type,
                         UserSessionType user_session_type,
                         ExtendedStartCrdSessionResultCode result_code) {
  base::UmaHistogramEnumeration("Enterprise.DeviceRemoteCommand.Crd.Result",
                                result_code);

  CrdUmaLogger(crd_session_type, user_session_type)
      .LogSessionLaunchResult(result_code);
}

std::string CreateSuccessPayload(const std::string& access_code) {
  return base::WriteJson(
             base::Value::Dict()
                 .Set(kResultCodeFieldName,
                      static_cast<int>(
                          StartCrdSessionResultCode::START_CRD_SESSION_SUCCESS))
                 .Set(kResultAccessCodeFieldName, access_code))
      .value();
}

std::string CreateNonIdlePayload(const base::TimeDelta& time_delta) {
  return base::WriteJson(
             base::Value::Dict()
                 .Set(kResultCodeFieldName,
                      static_cast<int>(
                          StartCrdSessionResultCode::FAILURE_NOT_IDLE))
                 .Set(kResultLastActivityFieldName,
                      static_cast<int>(time_delta.InSeconds())))
      .value();
}

std::string CreateErrorPayload(StartCrdSessionResultCode result_code,
                               const std::string& error_message) {
  CHECK_NE(result_code, StartCrdSessionResultCode::START_CRD_SESSION_SUCCESS);
  CHECK_NE(result_code, StartCrdSessionResultCode::FAILURE_NOT_IDLE);

  auto payload = base::Value::Dict()  //
                     .Set(kResultCodeFieldName, static_cast<int>(result_code));
  if (!error_message.empty()) {
    payload.Set(kResultMessageFieldName, error_message);
  }
  return base::WriteJson(payload).value();
}

DeviceOAuth2TokenService* GetOAuthService() {
  return DeviceOAuth2TokenServiceFactory::Get();
}

std::string GetRobotAccountUserName(const DeviceOAuth2TokenService* service) {
  CoreAccountId account_id = CHECK_DEREF(service).GetRobotAccountId();

  // TODO(msarda): This conversion will not be correct once account id is
  // migrated to be the Gaia ID on ChromeOS. Fix it.
  return account_id.ToString();
}

CrdSessionType ToCrdSessionTypeOrDefault(std::optional<int> int_value,
                                         CrdSessionType default_value) {
  if (!int_value.has_value() ||
      !enterprise_management::CrdSessionType_IsValid(int_value.value())) {
    return default_value;
  }
  return static_cast<CrdSessionType>(int_value.value());
}

void OnCrdSessionFinished(CrdSessionType crd_session_type,
                          UserSessionType user_session_type,
                          base::TimeDelta session_duration) {
  CrdUmaLogger(crd_session_type, user_session_type)
      .LogSessionDuration(session_duration);
}

bool IsKioskSession(UserSessionType session_type) {
  return session_type == UserSessionType::AUTO_LAUNCHED_KIOSK_SESSION ||
         session_type == UserSessionType::MANUALLY_LAUNCHED_KIOSK_SESSION;
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// DeviceCommandStartCrdSessionJob
////////////////////////////////////////////////////////////////////////////////

DeviceCommandStartCrdSessionJob::DeviceCommandStartCrdSessionJob(
    Delegate& delegate)
    : delegate_(delegate),
      robot_account_id_(GetRobotAccountUserName(GetOAuthService())) {}

DeviceCommandStartCrdSessionJob::DeviceCommandStartCrdSessionJob(
    Delegate& delegate,
    std::string_view robot_account_id)
    : delegate_(delegate), robot_account_id_(robot_account_id) {
  CHECK_IS_TEST();
}

DeviceCommandStartCrdSessionJob::~DeviceCommandStartCrdSessionJob() = default;

enterprise_management::RemoteCommand_Type
DeviceCommandStartCrdSessionJob::GetType() const {
  return enterprise_management::RemoteCommand::DEVICE_START_CRD_SESSION;
}

bool DeviceCommandStartCrdSessionJob::ParseCommandPayload(
    const std::string& command_payload) {
  std::optional<base::Value> root(base::JSONReader::Read(command_payload));
  if (!root || !root->is_dict()) {
    LOG(WARNING) << "Rejecting remote command with invalid payload: "
                 << std::quoted(command_payload);
    return false;
  }
  CRD_VLOG(1) << "Received remote command with payload "
              << std::quoted(command_payload);

  const base::Value::Dict& root_dict = root->GetDict();

  idleness_cutoff_ =
      base::Seconds(root_dict.FindInt(kIdlenessCutoffFieldName).value_or(0));

  acked_user_presence_ =
      root_dict.FindBool(kAckedUserPresenceFieldName).value_or(false);

  CrdSessionType crd_session_type =
      ToCrdSessionTypeOrDefault(root_dict.FindInt(kCrdSessionTypeFieldName),
                                CrdSessionType::REMOTE_SUPPORT_SESSION);

  admin_email_ = FindString(root_dict, kAdminEmailFieldName);

  curtain_local_user_session_ =
      (crd_session_type == CrdSessionType::REMOTE_ACCESS_SESSION);

  if (curtain_local_user_session_ &&
      !base::FeatureList::IsEnabled(
          remoting::features::kEnableCrdAdminRemoteAccess)) {
    LOG(WARNING) << "Rejecting CRD session type as CRD remote access feature "
                    "is not enabled";
    return false;
  }

  return true;
}

void DeviceCommandStartCrdSessionJob::RunImpl(
    CallbackWithResult result_callback) {
  CRD_LOG(INFO) << "Running start CRD session command";

  if (delegate_->HasActiveSession()) {
    CRD_VLOG(1) << "Terminating active session";
    delegate_->TerminateSession();
    CHECK(!delegate_->HasActiveSession());
  }

  result_callback_ = std::move(result_callback);

  if (!UserTypeSupportsCrd()) {
    return FinishWithError(
        ExtendedStartCrdSessionResultCode::kFailureUnsupportedUserType, "");
  }

  if (curtain_local_user_session_ && !IsRemoteAccessAllowedByPolicy(CHECK_DEREF(
                                         g_browser_process->local_state()))) {
    LOG(ERROR) << "Rejecting CRD session type as CRD remote access is disabled "
                  "by device policy.";
    return FinishWithError(
        ExtendedStartCrdSessionResultCode::kFailureDisabledByPolicy, "");
  }

  if (!IsDeviceIdle()) {
    FinishWithNotIdleError();
    return;
  }

  // First perform managed network check,
  CheckManagedNetworkASync(
      // Then start the CRD host.
      base::BindOnce(&DeviceCommandStartCrdSessionJob::StartCrdHostAndGetCode,
                     weak_factory_.GetWeakPtr()));
}

void DeviceCommandStartCrdSessionJob::CheckManagedNetworkASync(
    base::OnceClosure on_success) {
  if (!curtain_local_user_session_) {
    // No need to check for managed networks if we are not going to curtain
    // off the local session.
    std::move(on_success).Run();
    return;
  }

  CalculateIsInManagedEnvironmentAsync(base::BindOnce(
      [](base::OnceClosure on_success, ErrorCallback on_error,
         bool is_in_managed_environment) {
        if (is_in_managed_environment) {
          std::move(on_success).Run();
        } else {
          std::move(on_error).Run(
              ExtendedStartCrdSessionResultCode::kFailureUnmanagedEnvironment,
              /*error_messages=*/"");
        }
      },
      std::move(on_success), GetErrorCallback()));
}

void DeviceCommandStartCrdSessionJob::StartCrdHostAndGetCode() {
  CRD_VLOG(1) << "Starting CRD host and retrieving CRD access code";
  SessionParameters parameters;
  parameters.user_name = robot_account_id_;
  parameters.terminate_upon_input = ShouldTerminateUponInput();
  parameters.show_confirmation_dialog = ShouldShowConfirmationDialog();
  parameters.curtain_local_user_session = curtain_local_user_session_;
  parameters.admin_email = admin_email_;
  parameters.allow_troubleshooting_tools = ShouldAllowTroubleshootingTools();
  parameters.show_troubleshooting_tools = ShouldShowTroubleshootingTools();
  parameters.allow_reconnections = ShouldAllowReconnections();
  parameters.allow_file_transfer = ShouldAllowFileTransfer();

  delegate_->StartCrdHostAndGetCode(
      parameters,
      base::BindOnce(&DeviceCommandStartCrdSessionJob::FinishWithSuccess,
                     weak_factory_.GetWeakPtr()),
      base::BindOnce(&DeviceCommandStartCrdSessionJob::FinishWithError,
                     weak_factory_.GetWeakPtr()),
      base::BindOnce(&OnCrdSessionFinished, GetCrdSessionType(),
                     GetCurrentUserSessionType()));
}

void DeviceCommandStartCrdSessionJob::FinishWithSuccess(
    const std::string& access_code) {
  CRD_LOG(INFO) << "Successfully received CRD access code";
  if (!result_callback_) {
    return;  // Task was terminated.
  }

  SendResultCodeToUma(GetCrdSessionType(), GetCurrentUserSessionType(),
                      ExtendedStartCrdSessionResultCode::kSuccess);
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(result_callback_), ResultType::kSuccess,
                     CreateSuccessPayload(access_code)));
}

void DeviceCommandStartCrdSessionJob::FinishWithError(
    const ExtendedStartCrdSessionResultCode result_code,
    const std::string& message) {
  CHECK_NE(result_code, ExtendedStartCrdSessionResultCode::kSuccess);
  CRD_LOG(INFO) << "Not starting CRD session because of error (code "
                << static_cast<int>(result_code) << ", message '" << message
                << "')";
  if (!result_callback_) {
    return;  // Task was terminated.
  }

  SendResultCodeToUma(GetCrdSessionType(), GetCurrentUserSessionType(),
                      result_code);
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(result_callback_), ResultType::kFailure,
                     CreateErrorPayload(
                         ToStartCrdSessionResultCode(result_code), message)));
}

void DeviceCommandStartCrdSessionJob::FinishWithNotIdleError() {
  CRD_LOG(INFO) << "Not starting CRD session because device is not idle";
  if (!result_callback_) {
    return;  // Task was terminated.
  }

  SendResultCodeToUma(GetCrdSessionType(), GetCurrentUserSessionType(),
                      ExtendedStartCrdSessionResultCode::kFailureNotIdle);
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(result_callback_), ResultType::kFailure,
                     CreateNonIdlePayload(GetDeviceIdleTime())));
}

bool DeviceCommandStartCrdSessionJob::UserTypeSupportsCrd() const {
  CRD_VLOG(2) << "User is of type "
              << UserSessionTypeToString(GetCurrentUserSessionType());

  if (curtain_local_user_session_) {
    return UserSessionSupportsRemoteAccess(GetCurrentUserSessionType());
  } else {
    return UserSessionSupportsRemoteSupport(GetCurrentUserSessionType());
  }
}

CrdSessionType DeviceCommandStartCrdSessionJob::GetCrdSessionType() const {
  if (curtain_local_user_session_) {
    return CrdSessionType::REMOTE_ACCESS_SESSION;
  }
  return CrdSessionType::REMOTE_SUPPORT_SESSION;
}

bool DeviceCommandStartCrdSessionJob::IsDeviceIdle() const {
  return GetDeviceIdleTime() >= idleness_cutoff_;
}

bool DeviceCommandStartCrdSessionJob::ShouldShowConfirmationDialog() const {
  switch (GetCurrentUserSessionType()) {
    case UserSessionType::AUTO_LAUNCHED_KIOSK_SESSION:
    case UserSessionType::MANUALLY_LAUNCHED_KIOSK_SESSION:
    case UserSessionType::NO_SESSION:
      return false;

    case UserSessionType::AFFILIATED_USER_SESSION:
    case UserSessionType::MANAGED_GUEST_SESSION:
    case UserSessionType::UNAFFILIATED_USER_SESSION:
    case UserSessionType::GUEST_SESSION:
      return true;

    case UserSessionType::USER_SESSION_TYPE_UNKNOWN:
      NOTREACHED_IN_MIGRATION();
      return true;
  }
}

bool DeviceCommandStartCrdSessionJob::ShouldTerminateUponInput() const {
  if (curtain_local_user_session_) {
    return false;
  }

  switch (GetCurrentUserSessionType()) {
    case UserSessionType::AFFILIATED_USER_SESSION:
    case UserSessionType::MANAGED_GUEST_SESSION:
      // We never terminate upon input for the user-session scenarios, because:
      //   1. There is no risk of the admin spying on the users, as they need to
      //       explicitly accept the connection request.
      //   2. If we terminate upon input the session will immediately be
      //      terminated as soon as the user accepts the connection request,
      //      as pressing the button to accept the connection request counts as
      //      user input.
      return false;

    case UserSessionType::AUTO_LAUNCHED_KIOSK_SESSION:
    case UserSessionType::MANUALLY_LAUNCHED_KIOSK_SESSION:
      return !acked_user_presence_;

    case UserSessionType::NO_SESSION:
    case UserSessionType::UNAFFILIATED_USER_SESSION:
    case UserSessionType::GUEST_SESSION:
      return true;

    case UserSessionType::USER_SESSION_TYPE_UNKNOWN:
      NOTREACHED_IN_MIGRATION();
      return true;
  }
}

bool DeviceCommandStartCrdSessionJob::ShouldAllowReconnections() const {
  if (!base::FeatureList::IsEnabled(
          remoting::features::kEnableCrdAdminRemoteAccessV2)) {
    return false;
  }

  // Curtained off sessions support reconnections if Chrome restarts.
  return curtain_local_user_session_;
}

bool DeviceCommandStartCrdSessionJob::ShouldShowTroubleshootingTools() const {
  return IsKioskSession(GetCurrentUserSessionType());
}

bool DeviceCommandStartCrdSessionJob::ShouldAllowTroubleshootingTools() const {
  return IsKioskSession(GetCurrentUserSessionType()) &&
         CHECK_DEREF(ProfileManager::GetActiveUserProfile()->GetPrefs())
             .GetBoolean(prefs::kKioskTroubleshootingToolsEnabled);
}

bool DeviceCommandStartCrdSessionJob::ShouldAllowFileTransfer() const {
  return IsKioskSession(GetCurrentUserSessionType()) &&
         base::FeatureList::IsEnabled(
             remoting::features::kEnableCrdFileTransferForKiosk);
}

ErrorCallback DeviceCommandStartCrdSessionJob::GetErrorCallback() {
  return base::BindOnce(&DeviceCommandStartCrdSessionJob::FinishWithError,
                        weak_factory_.GetWeakPtr());
}

void DeviceCommandStartCrdSessionJob::TerminateImpl() {
  result_callback_.Reset();
  weak_factory_.InvalidateWeakPtrs();
  delegate_->TerminateSession();
}

}  // namespace policy