chromium/chromeos/ash/components/osauth/impl/auth_hub_attempt_handler.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_attempt_handler.h"

#include <optional>
#include <utility>

#include "base/check.h"
#include "base/containers/enum_set.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.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_status_consumer.h"
#include "chromeos/ash/components/osauth/public/common_types.h"

namespace ash {

AuthHubAttemptHandler::AuthHubAttemptHandler(
    AuthHubAttemptHandler::Owner* owner,
    const AuthAttemptVector& attempt,
    const AuthEnginesMap& engines,
    AuthFactorsSet expected_factors)
    : owner_(owner),
      attempt_(attempt),
      engines_(engines),
      initial_factors_(expected_factors) {}

AuthHubAttemptHandler::~AuthHubAttemptHandler() = default;

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

AuthHubConnector* AuthHubAttemptHandler::GetConnector() {
  return this;
}

void AuthHubAttemptHandler::SetConsumer(
    raw_ptr<AuthFactorStatusConsumer> consumer) {
  status_consumer_ = std::move(consumer);
  status_consumer_->InitializeUi(initial_factors_, this);
}

bool AuthHubAttemptHandler::HasOngoingAttempt() const {
  // It is safe to proceed with shutdown if we have
  // successfully authenticated.
  if (authenticated_) {
    return false;
  }
  return ongoing_attempt_factor_.has_value();
}

void AuthHubAttemptHandler::PrepareForShutdown(base::OnceClosure callback) {
  CHECK(!callback.is_null());
  if (shutting_down_) {
    shutdown_callbacks_.AddUnsafe(std::move(callback));
    return;
  }
  if (!ongoing_attempt_factor_.has_value() || authenticated_) {
    shutting_down_ = true;
    UpdateAllFactorStates();
    std::move(callback).Run();
    return;
  }
  shutting_down_ = true;
  shutdown_callbacks_.AddUnsafe(std::move(callback));
}

void AuthHubAttemptHandler::OnFactorsChecked(AuthFactorsSet available_factors,
                                             AuthFactorsSet failed_factors) {
  DCHECK(Intersection(available_factors, failed_factors).empty());
  // TODO(b/286814076): Refine this strategy.
  // All factors can be split into 4 groups, according to initial_factors_ and
  // two parameters passed:
  //  * New factors, that are in `available_factors` but were not listed in
  //  `initial_factors_`;
  //  * Available factors, that present both  in `available_factors` and
  //  `initial_factors_`;
  //  * Removed factors, that were listed in `initial_factors_` but do not
  //  present neither in `available_factors` nor in `failed_factors`;
  //  * Failed factors;

  AuthFactorsSet potentially_present = Union(available_factors, failed_factors);
  AuthFactorsSet new_factors = Difference(available_factors, initial_factors_);
  AuthFactorsSet removed_factors =
      Difference(initial_factors_, potentially_present);

  // If some factor engines failed, but there are no new/removed factors,
  // report those factors in error state. Otherwise, consider them removed.

  AuthFactorsSet failed_initial =
      Intersection(initial_factors_, failed_factors);
  bool same_factor_list = new_factors.empty() && removed_factors.empty();

  if (same_factor_list) {
    for (AshAuthFactor f : failed_initial) {
      // Create entry, but mark as failed.
      factor_state_[f].engine_failed = true;
      CalculateFactorState(f, factor_state_[f]);
    }
  }

  // Retain only necessary engines, fill status for them:
  for (auto it = engines_.begin(); it != engines_.end();) {
    if (!available_factors.Has(it->first)) {
      it = engines_.erase(it);
      continue;
    }
    FillAllStatusValues(it->first, factor_state_[it->first]);
    it++;
  }

  FactorsStatusMap update;
  for (auto& state : factor_state_) {
    update[state.first] = state.second.internal_state;
    state.second.reported_state = state.second.internal_state;
  }

  if (same_factor_list) {
    status_consumer_->OnFactorStatusesChanged(update);
  } else {
    owner_->UpdateFactorUiCache(attempt_, available_factors);
    status_consumer_->OnFactorListChanged(update);
  }
  PropagateEnginesEnabledStatus();
}

void AuthHubAttemptHandler::PropagateStatusUpdates() {
  FactorsStatusMap update;
  for (auto& state : factor_state_) {
    if (state.second.internal_state != state.second.reported_state) {
      update[state.first] = state.second.internal_state;
      state.second.reported_state = state.second.internal_state;
    }
  }
  if (!update.empty()) {
    status_consumer_->OnFactorStatusesChanged(update);
  }
  PropagateEnginesEnabledStatus();
}

void AuthHubAttemptHandler::PropagateEnginesEnabledStatus() {
  for (auto& state : factor_state_) {
    if (state.second.intended_usage != state.second.engine_usage) {
      state.second.engine_usage = state.second.intended_usage;
      engines_[state.first]->SetUsageAllowed(state.second.engine_usage);
    }
  }
}

void AuthHubAttemptHandler::OnFactorPresenceChecked(AshAuthFactor factor,
                                                    bool factor_present) {
  // No-op, this method is implemented and handled by AuthHubVectorLifecycle.
  // Result would be provided to this class via `OnFactorsChecked` call.
}

void AuthHubAttemptHandler::OnFactorAttempt(AshAuthFactor factor) {
  ongoing_attempt_factor_ = factor;
  UpdateAllFactorStates();
}

void AuthHubAttemptHandler::UpdateAllFactorStates() {
  for (auto& state : factor_state_) {
    CalculateFactorState(state.first, state.second);
  }
  PropagateStatusUpdates();
}

void AuthHubAttemptHandler::OnFactorAttemptResult(AshAuthFactor factor,
                                                  bool success) {
  CHECK(ongoing_attempt_factor_.has_value());
  CHECK(factor == *ongoing_attempt_factor_);

  if (shutting_down_) {
    shutdown_callbacks_.Notify();
    return;
  }

  if (success) {
    status_consumer_->OnFactorAuthSuccess(factor);
    authenticated_ = true;
    // Keep an `ongoing_attempt_factor_` to prevent
    // factors from being re-enabled.
    status_consumer_->OnEndAuthentication();

    // Calling `OnEndAuthentication` signals the end of interaction with UI for
    // this particular attempt, which would eventually destroy UI, so we reset
    // the pointer here to avoid calling into a danling pointer.
    status_consumer_ = nullptr;

    // Signal the successful auth to every auth engine.
    for (const auto& [unused, engine] : engines_) {
      engine->OnSuccessfulAuthentiation();
    }

    owner_->OnAuthenticationSuccess(attempt_, factor);
    return;
  } else {
    status_consumer_->OnFactorAuthFailure(factor);
    owner_->OnFactorAttemptFailed(attempt_, factor);
    ongoing_attempt_factor_.reset();
  }
  UpdateAllFactorStates();
}

void AuthHubAttemptHandler::OnPolicyChanged(AshAuthFactor factor) {
  CHECK(factor_state_.contains(factor));
  auto& state = factor_state_[factor];
  if (state.engine_failed) {
    return;
  }
  CHECK(engines_.contains(factor));
  auto* engine = engines_[factor].get();
  state.disabled_by_policy = engine->IsDisabledByPolicy();
  CalculateFactorState(factor, state);
  PropagateStatusUpdates();
}

void AuthHubAttemptHandler::OnLockoutChanged(AshAuthFactor factor) {
  CHECK(factor_state_.contains(factor));
  auto& state = factor_state_[factor];
  if (state.engine_failed) {
    return;
  }
  CHECK(engines_.contains(factor));
  auto* engine = engines_[factor].get();
  state.locked_out = engine->IsLockedOut();
  CalculateFactorState(factor, state);
  PropagateStatusUpdates();
}

void AuthHubAttemptHandler::OnFactorSpecificRestrictionsChanged(
    AshAuthFactor factor) {
  CHECK(factor_state_.contains(factor));
  auto& state = factor_state_[factor];
  if (state.engine_failed) {
    return;
  }
  CHECK(engines_.contains(factor));
  auto* engine = engines_[factor].get();
  state.factor_specific_restricted = engine->IsFactorSpecificRestricted();
  CalculateFactorState(factor, state);
  PropagateStatusUpdates();
}

void AuthHubAttemptHandler::OnCriticalError(AshAuthFactor factor) {
  CHECK(factor_state_.contains(factor));
  factor_state_[factor].engine_failed = true;
  CalculateFactorState(factor, factor_state_[factor]);
  PropagateStatusUpdates();
}

void AuthHubAttemptHandler::OnFactorCustomSignal(AshAuthFactor factor) {
  CHECK(engines_.contains(factor));
  status_consumer_->OnFactorCustomSignal(factor);
}

void AuthHubAttemptHandler::FillAllStatusValues(AshAuthFactor factor,
                                                FactorAttemptState& state) {
  CHECK(engines_.contains(factor));
  auto* engine = engines_[factor].get();
  state.disabled_by_policy = engine->IsDisabledByPolicy();
  state.locked_out = engine->IsLockedOut();
  state.factor_specific_restricted = engine->IsFactorSpecificRestricted();
  CalculateFactorState(factor, state);
}

void AuthHubAttemptHandler::CalculateFactorState(AshAuthFactor factor,
                                                 FactorAttemptState& state) {
  state.internal_state = AuthFactorState::kFactorReady;
  if (state.engine_failed) {
    state.internal_state = AuthFactorState::kEngineError;
    // Factor is marked as Failed if it's engine did not start.
    // We do not modify `intended_usage` here as we can not propagate
    // it to the engine.
    return;
  }
  if (shutting_down_) {
    // We need to set some disabled state here to prevent factors from
    // being used, does not matter which one.
    state.internal_state = AuthFactorState::kDisabledParallelAttempt;
    // As code is in the shutdown sequence, engine will not be re-enabled
    // again, so use kDisabled instead of kDisabledParallelAttempt here,
    // to prevent engine from queueing any attempts.
    state.intended_usage = AuthFactorEngine::UsageAllowed::kDisabled;
    return;
  }
  if (state.disabled_by_policy) {
    state.internal_state = AuthFactorState::kDisabledByPolicy;
    state.intended_usage = AuthFactorEngine::UsageAllowed::kDisabled;
    return;
  }
  if (state.factor_specific_restricted) {
    state.internal_state = AuthFactorState::kDisabledFactorSpecific;
    state.intended_usage = AuthFactorEngine::UsageAllowed::kDisabled;
    return;
  }
  if (state.locked_out) {
    state.internal_state = AuthFactorState::kLockedOutIndefinite;
    state.intended_usage = AuthFactorEngine::UsageAllowed::kDisabled;
    return;
  }
  if (ongoing_attempt_factor_.has_value()) {
    if (*ongoing_attempt_factor_ == factor) {
      state.internal_state = AuthFactorState::kOngoingAttempt;
    } else {
      state.internal_state = AuthFactorState::kDisabledParallelAttempt;
    }
    // While there is an ongoing attempt, keep all factors disabled to
    // prevent double authentication.
    state.intended_usage =
        AuthFactorEngine::UsageAllowed::kDisabledParallelAttempt;
    return;
  }
  state.internal_state = AuthFactorState::kFactorReady;
  state.intended_usage = AuthFactorEngine::UsageAllowed::kEnabled;
}

AuthFactorEngine* AuthHubAttemptHandler::GetEngine(AshAuthFactor factor) {
  CHECK(engines_.contains(factor));
  return engines_[factor];
}

}  // namespace ash