chromium/chromeos/ash/components/osauth/impl/auth_hub_vector_lifecycle.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/osauth/impl/auth_hub_vector_lifecycle.h"

#include <optional>

#include "base/check.h"
#include "base/check_op.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chromeos/ash/components/osauth/impl/auth_hub_common.h"
#include "chromeos/ash/components/osauth/public/auth_factor_engine.h"
#include "chromeos/ash/components/osauth/public/auth_factor_engine_factory.h"
#include "chromeos/ash/components/osauth/public/common_types.h"
#include "chromeos/ash/components/osauth/public/string_utils.h"

namespace ash {

namespace {

// TODO (b/271248265): Review timeout values.
#if !defined(NDEBUG)
constexpr base::TimeDelta kWatchdogTimeout = base::Seconds(5);
#else
constexpr base::TimeDelta kWatchdogTimeout = base::Seconds(10);
#endif

enum class EngineAttemptStatus {
  kIdle,
  kStarting,
  kNoFactor,
  kStarted,
  kFailed,
  kCriticalError,
  kCleaningUp,
  kFinishing,
};

}  // namespace

struct AuthHubVectorLifecycle::FactorAttemptState {
  raw_ptr<AuthFactorEngine, DanglingUntriaged> engine;
  EngineAttemptStatus status;
};

AuthHubVectorLifecycle::AuthHubVectorLifecycle(Owner* owner,
                                               AuthHubMode mode,
                                               const AuthEnginesMap& engines)
    : available_engines_(engines), owner_(owner) {
  CHECK(!engines.empty());
}

AuthHubVectorLifecycle::~AuthHubVectorLifecycle() = default;

AuthHubVectorLifecycle::Owner::~Owner() = default;

void AuthHubVectorLifecycle::StartAttempt(const AuthAttemptVector& attempt) {
  target_attempt_ = attempt;
  switch (stage_) {
    case Stage::kIdle:
      StartForTargetAttempt();
      break;
    case Stage::kStarted:
      ShutdownAttempt(base::BindOnce(&AuthHubVectorLifecycle::OnCancelAttempt,
                                     weak_factory_.GetWeakPtr()));
      break;
    case Stage::kStartingAttempt:
      // Set up new target mode, but do not modify initializing_for_,
      // `ProceedIfAllFactorsStarted` would trigger re-initialization.
      break;
    case Stage::kCleaningUpAttempt:
    case Stage::kFinishingAttempt:
      // Just update the target mode, `ProceedIfAllFactorsFinished` would
      // trigger new attempt.
      break;
  }
}

void AuthHubVectorLifecycle::CancelAttempt() {
  switch (stage_) {
    case Stage::kIdle:
      LOG(WARNING) << "Request to cancel attempt without actual attempt";
      break;
    case Stage::kStarted:
      target_attempt_ = std::nullopt;
      ShutdownAttempt(base::BindOnce(&AuthHubVectorLifecycle::OnCancelAttempt,
                                     weak_factory_.GetWeakPtr()));
      break;
    case Stage::kStartingAttempt:
    case Stage::kCleaningUpAttempt:
    case Stage::kFinishingAttempt:
      target_attempt_ = std::nullopt;
      break;
  }
}

void AuthHubVectorLifecycle::FinishAttempt() {
  if (stage_ != Stage::kStarted) {
    LOG(ERROR) << "Trying to finish attempt when none is started";
    base::debug::DumpWithoutCrashing();
    return;
  }

  target_attempt_ = std::nullopt;
  ShutdownAttempt(base::BindOnce(&AuthHubVectorLifecycle::OnFinishAttempt,
                                 weak_factory_.GetWeakPtr()));
}

void AuthHubVectorLifecycle::OnCancelAttempt(
    const AuthAttemptVector& current_attempt) {
  owner_->OnAttemptCancelled(current_attempt);
}

void AuthHubVectorLifecycle::OnFinishAttempt(
    const AuthAttemptVector& current_attempt) {
  owner_->OnAttemptFinished(current_attempt);
}

bool AuthHubVectorLifecycle::IsIdle() const {
  return stage_ == Stage::kIdle;
}

void AuthHubVectorLifecycle::StartForTargetAttempt() {
  CHECK_EQ(stage_, Stage::kIdle);
  CHECK(target_attempt_.has_value());

  stage_ = Stage::kStartingAttempt;
  initializing_for_ = target_attempt_;

  CHECK(engines_.empty());
  for (const auto& [factor, engine] : available_engines_) {
    engines_[factor] =
        FactorAttemptState{engine, EngineAttemptStatus::kStarting};
  }

  watchdog_.Stop();
  watchdog_.Start(
      FROM_HERE, kWatchdogTimeout,
      base::BindOnce(&AuthHubVectorLifecycle::OnAttemptStartWatchdog,
                     weak_factory_.GetWeakPtr()));
  for (auto& [unused, state] : engines_) {
    state.engine->StartAuthFlow(target_attempt_->account,
                                target_attempt_->purpose, this);
  }
}

void AuthHubVectorLifecycle::OnAttemptStartWatchdog() {
  LOG(ERROR) << "Attempt start watchdog triggered";
  CHECK_EQ(stage_, Stage::kStartingAttempt);

  for (auto& [factor, state] : engines_) {
    if (state.status == EngineAttemptStatus::kStarting) {
      state.status = EngineAttemptStatus::kFailed;
      LOG(ERROR) << "Factor " << factor << " did not start in time";
      state.engine->StartFlowTimedOut();
    }
  }
  ProceedIfAllFactorsStarted();
}

void AuthHubVectorLifecycle::ProceedIfAllFactorsStarted() {
  CHECK_EQ(stage_, Stage::kStartingAttempt);
  for (const auto& [unused, state] : engines_) {
    if (state.status == EngineAttemptStatus::kStarting) {
      return;
    }
  }
  watchdog_.Stop();
  stage_ = Stage::kStarted;
  if (initializing_for_ != target_attempt_) {
    // Not notifying owner, just restart for new target.
    initializing_for_ = std::nullopt;
    ShutdownAttempt(base::BindOnce(&AuthHubVectorLifecycle::OnCancelAttempt,
                                   weak_factory_.GetWeakPtr()));
    return;
  }
  CHECK(target_attempt_.has_value());

  current_attempt_ = target_attempt_;

  AuthFactorsSet present;
  AuthFactorsSet failed;

  for (const auto& [factor, state] : engines_) {
    switch (state.status) {
      case EngineAttemptStatus::kStarted:
        present.Put(factor);
        state.engine->UpdateObserver(owner_->AsEngineObserver());
        break;
      case EngineAttemptStatus::kNoFactor:
        // Just ignore them
        break;
      case EngineAttemptStatus::kCriticalError:
      case EngineAttemptStatus::kFailed:
        failed.Put(factor);
        break;
      case EngineAttemptStatus::kStarting:
      case EngineAttemptStatus::kIdle:
      case EngineAttemptStatus::kCleaningUp:
      case EngineAttemptStatus::kFinishing:
        NOTREACHED();
    }
  }
  owner_->OnAttemptStarted(*current_attempt_, present, failed);
}

void AuthHubVectorLifecycle::ShutdownAttempt(
    OnShutdownAttemptNotifyOwner on_shutdown_attempt) {
  CHECK_EQ(stage_, Stage::kStarted);
  CHECK(!on_shutdown_attempt_);

  on_shutdown_attempt_ = std::move(on_shutdown_attempt);

  stage_ = Stage::kCleaningUpAttempt;
  for (auto& [unused, state] : engines_) {
    state.status = EngineAttemptStatus::kCleaningUp;
  }
  watchdog_.Stop();
  watchdog_.Start(
      FROM_HERE, kWatchdogTimeout,
      base::BindOnce(&AuthHubVectorLifecycle::OnAttemptCleanedUpWatchdog,
                     weak_factory_.GetWeakPtr()));

  for (auto& [unused, state] : engines_) {
    state.engine->UpdateObserver(this);
    state.engine->CleanUp(
        base::BindOnce(&AuthHubVectorLifecycle::OnFactorCleanedUp,
                       weak_factory_.GetWeakPtr()));
  }
}

void AuthHubVectorLifecycle::OnFactorCleanedUp(AshAuthFactor factor) {
  CHECK_EQ(stage_, Stage::kCleaningUpAttempt);
  CHECK(engines_.contains(factor));
  engines_[factor].status = EngineAttemptStatus::kFinishing;
  FinishIfAllFactorsCleanedUp();
}

void AuthHubVectorLifecycle::OnAttemptCleanedUpWatchdog() {
  LOG(ERROR) << "Attempt cleanup watchdog triggered";
  CHECK_EQ(stage_, Stage::kCleaningUpAttempt);

  for (auto& [factor, state] : engines_) {
    if (state.status == EngineAttemptStatus::kCleaningUp) {
      state.status = EngineAttemptStatus::kFailed;
      LOG(ERROR) << "Factor " << factor << " did not clean up in time";
      state.engine->StopFlowTimedOut();
      state.engine->UpdateObserver(nullptr);
    }
  }
  FinishIfAllFactorsCleanedUp();
}

void AuthHubVectorLifecycle::FinishIfAllFactorsCleanedUp() {
  CHECK_EQ(stage_, Stage::kCleaningUpAttempt);
  for (const auto& [unused, state] : engines_) {
    if (state.status == EngineAttemptStatus::kCleaningUp) {
      return;
    }
  }
  watchdog_.Stop();
  if (current_attempt_.has_value()) {
    // we need to notify about cleanup first.
    owner_->OnAttemptCleanedUp(*current_attempt_);
  }
  stage_ = Stage::kFinishingAttempt;
  for (auto& [unused, state] : engines_) {
    state.status = EngineAttemptStatus::kFinishing;
  }
  watchdog_.Start(
      FROM_HERE, kWatchdogTimeout,
      base::BindOnce(&AuthHubVectorLifecycle::OnAttemptFinishWatchdog,
                     weak_factory_.GetWeakPtr()));

  // TODO(b/277929602): metrics on initialization time.
  for (auto& [unused, state] : engines_) {
    state.engine->UpdateObserver(this);
    state.engine->StopAuthFlow(base::BindOnce(
        &AuthHubVectorLifecycle::OnFactorFinished, weak_factory_.GetWeakPtr()));
  }
}

void AuthHubVectorLifecycle::OnFactorFinished(AshAuthFactor factor) {
  CHECK_EQ(stage_, Stage::kFinishingAttempt);
  CHECK(engines_.contains(factor));
  engines_[factor].status = EngineAttemptStatus::kIdle;
  engines_[factor].engine->UpdateObserver(nullptr);
  ProceedIfAllFactorsFinished();
}

void AuthHubVectorLifecycle::OnAttemptFinishWatchdog() {
  LOG(ERROR) << "Attempt finish watchdog triggered";
  CHECK_EQ(stage_, Stage::kFinishingAttempt);

  for (auto& [factor, state] : engines_) {
    if (state.status == EngineAttemptStatus::kFinishing) {
      state.status = EngineAttemptStatus::kFailed;
      LOG(ERROR) << "Factor " << factor << " did not finish in time";
      state.engine->StopFlowTimedOut();
      state.engine->UpdateObserver(nullptr);
    }
  }
  ProceedIfAllFactorsFinished();
}

void AuthHubVectorLifecycle::ProceedIfAllFactorsFinished() {
  CHECK_EQ(stage_, Stage::kFinishingAttempt);
  for (const auto& [unused, state] : engines_) {
    if (state.status == EngineAttemptStatus::kFinishing) {
      return;
    }
  }
  engines_.clear();

  watchdog_.Stop();
  stage_ = Stage::kIdle;

  if (current_attempt_.has_value()) {
    // We have notified owner about attempt start, so
    // we need to notify about finish.
    CHECK(on_shutdown_attempt_);
    std::move(on_shutdown_attempt_).Run(*current_attempt_);
  }
  current_attempt_ = std::nullopt;

  if (target_attempt_.has_value()) {
    StartForTargetAttempt();
  } else {
    owner_->OnIdle();
  }
}

// AuthFactorEngine::FactorEngineObserver:
void AuthHubVectorLifecycle::OnFactorPresenceChecked(AshAuthFactor factor,
                                                     bool factor_present) {
  CHECK_EQ(stage_, Stage::kStartingAttempt);
  CHECK(engines_.contains(factor));

  engines_[factor].status = factor_present ? EngineAttemptStatus::kStarted
                                           : EngineAttemptStatus::kNoFactor;
  ProceedIfAllFactorsStarted();
}

void AuthHubVectorLifecycle::OnFactorAttempt(AshAuthFactor factor) {
  // Ignored
  // Should not happen, as factors start in disabled state.
  base::debug::DumpWithoutCrashing();
}
void AuthHubVectorLifecycle::OnFactorAttemptResult(AshAuthFactor factor,
                                                   bool success) {
  // Ignored
  // Should not happen, as factors start in disabled state.
  base::debug::DumpWithoutCrashing();
}

void AuthHubVectorLifecycle::OnPolicyChanged(AshAuthFactor factor) {
  // Ignored
}
void AuthHubVectorLifecycle::OnLockoutChanged(AshAuthFactor factor) {
  // Ignored
}
void AuthHubVectorLifecycle::OnFactorSpecificRestrictionsChanged(
    AshAuthFactor factor) {
  // Ignored
}
void AuthHubVectorLifecycle::OnFactorCustomSignal(AshAuthFactor factor) {
  // Ignored
}

void AuthHubVectorLifecycle::OnCriticalError(AshAuthFactor factor) {
  CHECK(stage_ == Stage::kStartingAttempt ||
        stage_ == Stage::kFinishingAttempt);

  CHECK(engines_.contains(factor));
  engines_[factor].status = EngineAttemptStatus::kCriticalError;

  if (stage_ == Stage::kStartingAttempt) {
    ProceedIfAllFactorsStarted();
  } else {
    ProceedIfAllFactorsFinished();
  }
}

}  // namespace ash