// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chromeos/ash/components/dbus/kerberos/fake_kerberos_client.h"
#include <utility>
#include "base/containers/contains.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_split.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "third_party/cros_system_api/dbus/kerberos/dbus-constants.h"
namespace ash {
namespace {
// Fake validity lifetime for TGTs.
constexpr base::TimeDelta kTgtValidity = base::Hours(10);
// Fake renewal lifetime for TGTs.
constexpr base::TimeDelta kTgtRenewal = base::Hours(24);
// Blocklist for fake config validation.
const char* const kBlocklistedConfigOptions[] = {
"allow_weak_crypto",
"ap_req_checksum_type",
"ccache_type",
"default_ccache_name ",
"default_client_keytab_name",
"default_keytab_name",
"default_realm",
"k5login_authoritative",
"k5login_directory",
"kdc_req_checksum_type",
"plugin_base_dir",
"realm_try_domains",
"safe_checksum_type",
"verify_ap_req_nofail",
"default_domain",
"v4_instance_convert",
"v4_realm",
"[appdefaults]",
"[plugins]",
};
// Performs a fake validation of a config line by just checking for some
// non-allowlisted keywords. Returns true if no blocklisted items are contained.
bool ValidateConfigLine(const std::string& line) {
for (const char* option : kBlocklistedConfigOptions) {
if (base::Contains(line, option)) {
return false;
}
}
return true;
}
// Runs ValidateConfigLine() on every line of |krb5_config|. Returns a
// ConfigErrorInfo object that indicates the first line where validation fails,
// if any.
kerberos::ConfigErrorInfo ValidateConfigLines(const std::string& krb5_config) {
std::vector<std::string> lines = base::SplitString(
krb5_config, "\r\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
for (size_t line_index = 0; line_index < lines.size(); ++line_index) {
if (!ValidateConfigLine(lines[line_index])) {
kerberos::ConfigErrorInfo error_info;
error_info.set_code(kerberos::CONFIG_ERROR_KEY_NOT_SUPPORTED);
error_info.set_line_index(static_cast<int>(line_index));
return error_info;
}
}
kerberos::ConfigErrorInfo error_info;
error_info.set_code(kerberos::CONFIG_ERROR_NONE);
return error_info;
}
// Posts |callback| on the current thread's task runner, passing it the
// |response| message.
template <class TProto>
void PostProtoResponse(base::OnceCallback<void(const TProto&)> callback,
const TProto& response,
base::TimeDelta delay) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, base::BindOnce(std::move(callback), response), delay);
}
// Similar to PostProtoResponse(), but posts |callback| with a proto containing
// only the given |error|.
template <class TProto>
void PostResponse(base::OnceCallback<void(const TProto&)> callback,
kerberos::ErrorType error,
base::TimeDelta delay) {
TProto response;
response.set_error(error);
PostProtoResponse(std::move(callback), response, delay);
}
// Reads the password from the file descriptor |password_fd|.
// Not very efficient, but simple!
std::string ReadPassword(int password_fd) {
std::string password;
char c;
while (base::ReadFromFD(password_fd, base::span_from_ref(c))) {
password.push_back(c);
}
return password;
}
} // namespace
FakeKerberosClient::FakeKerberosClient() = default;
FakeKerberosClient::~FakeKerberosClient() = default;
void FakeKerberosClient::AddAccount(const kerberos::AddAccountRequest& request,
AddAccountCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
auto it =
base::ranges::find(accounts_, AccountData(request.principal_name()));
if (it != accounts_.end()) {
it->is_managed |= request.is_managed();
PostResponse(std::move(callback), kerberos::ERROR_DUPLICATE_PRINCIPAL_NAME,
task_delay_);
return;
}
AccountData data(request.principal_name());
data.is_managed = request.is_managed();
accounts_.push_back(data);
PostResponse(std::move(callback), kerberos::ERROR_NONE, task_delay_);
}
void FakeKerberosClient::RemoveAccount(
const kerberos::RemoveAccountRequest& request,
RemoveAccountCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
kerberos::RemoveAccountResponse response;
auto it =
base::ranges::find(accounts_, AccountData(request.principal_name()));
if (it == accounts_.end()) {
response.set_error(kerberos::ERROR_UNKNOWN_PRINCIPAL_NAME);
} else {
accounts_.erase(it);
response.set_error(kerberos::ERROR_NONE);
}
MapAccountData(response.mutable_accounts());
PostProtoResponse(std::move(callback), response, task_delay_);
}
void FakeKerberosClient::ClearAccounts(
const kerberos::ClearAccountsRequest& request,
ClearAccountsCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
std::unordered_set<std::string> keep_list(
request.principal_names_to_ignore_size());
for (int n = 0; n < request.principal_names_to_ignore_size(); ++n)
keep_list.insert(request.principal_names_to_ignore(n));
for (auto it = accounts_.begin(); it != accounts_.end(); /* empty */) {
if (base::Contains(keep_list, it->principal_name)) {
++it;
continue;
}
switch (DetermineWhatToRemove(request.mode(), *it)) {
case WhatToRemove::kNothing:
++it;
continue;
case WhatToRemove::kPassword:
it->password.clear();
++it;
continue;
case WhatToRemove::kAccount:
it = accounts_.erase(it);
continue;
}
}
kerberos::ClearAccountsResponse response;
MapAccountData(response.mutable_accounts());
response.set_error(kerberos::ERROR_NONE);
PostProtoResponse(std::move(callback), response, task_delay_);
}
void FakeKerberosClient::ListAccounts(
const kerberos::ListAccountsRequest& request,
ListAccountsCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
kerberos::ListAccountsResponse response;
MapAccountData(response.mutable_accounts());
response.set_error(kerberos::ERROR_NONE);
PostProtoResponse(std::move(callback), response, task_delay_);
}
void FakeKerberosClient::SetConfig(const kerberos::SetConfigRequest& request,
SetConfigCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
AccountData* data = GetAccountData(request.principal_name());
if (!data) {
PostResponse(std::move(callback), kerberos::ERROR_UNKNOWN_PRINCIPAL_NAME,
task_delay_);
return;
}
kerberos::ConfigErrorInfo error_info =
ValidateConfigLines(request.krb5conf());
if (error_info.code() != kerberos::CONFIG_ERROR_NONE) {
PostResponse(std::move(callback), kerberos::ERROR_BAD_CONFIG, task_delay_);
return;
}
data->krb5conf = request.krb5conf();
PostResponse(std::move(callback), kerberos::ERROR_NONE, task_delay_);
}
void FakeKerberosClient::ValidateConfig(
const kerberos::ValidateConfigRequest& request,
ValidateConfigCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
kerberos::ConfigErrorInfo error_info =
ValidateConfigLines(request.krb5conf());
kerberos::ValidateConfigResponse response;
response.set_error(error_info.code() != kerberos::CONFIG_ERROR_NONE
? kerberos::ERROR_BAD_CONFIG
: kerberos::ERROR_NONE);
*response.mutable_error_info() = std::move(error_info);
PostProtoResponse(std::move(callback), response, task_delay_);
}
void FakeKerberosClient::AcquireKerberosTgt(
const kerberos::AcquireKerberosTgtRequest& request,
int password_fd,
AcquireKerberosTgtCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
AccountData* data = GetAccountData(request.principal_name());
if (!data) {
PostResponse(std::move(callback), kerberos::ERROR_UNKNOWN_PRINCIPAL_NAME,
task_delay_);
return;
}
// Remember whether to use the login password.
data->use_login_password = request.use_login_password();
std::string password;
if (request.use_login_password()) {
// "Retrieve" login password.
password = "fake_login_password";
// Erase a previously remembered password.
data->password.clear();
} else {
// Remember password.
password = ReadPassword(password_fd);
if (!password.empty() && request.remember_password())
data->password = password;
// Use remembered password.
if (password.empty())
password = data->password;
// Erase a previously remembered password.
if (!request.remember_password())
data->password.clear();
}
// Reject empty passwords.
if (password.empty()) {
PostResponse(std::move(callback), kerberos::ERROR_BAD_PASSWORD,
task_delay_);
return;
}
if (simulated_number_of_network_failures_ > 0) {
simulated_number_of_network_failures_--;
PostResponse(std::move(callback), kerberos::ERROR_NETWORK_PROBLEM,
task_delay_);
return;
}
// It worked! Magic!
data->has_tgt = true;
PostResponse(std::move(callback), kerberos::ERROR_NONE, task_delay_);
}
void FakeKerberosClient::GetKerberosFiles(
const kerberos::GetKerberosFilesRequest& request,
GetKerberosFilesCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
AccountData* data = GetAccountData(request.principal_name());
if (!data) {
PostResponse(std::move(callback), kerberos::ERROR_UNKNOWN_PRINCIPAL_NAME,
task_delay_);
return;
}
kerberos::GetKerberosFilesResponse response;
if (data->has_tgt) {
response.mutable_files()->set_krb5cc("Fake Kerberos credential cache");
response.mutable_files()->set_krb5conf("Fake Kerberos configuration");
}
response.set_error(kerberos::ERROR_NONE);
PostProtoResponse(std::move(callback), response, task_delay_);
}
base::CallbackListSubscription
FakeKerberosClient::SubscribeToKerberosFileChangedSignal(
KerberosFilesChangedCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
DCHECK(callback);
return base::CallbackListSubscription();
}
base::CallbackListSubscription
FakeKerberosClient::SubscribeToKerberosTicketExpiringSignal(
KerberosTicketExpiringCallback callback) {
MaybeRecordFunctionCallForTesting(__FUNCTION__);
DCHECK(callback);
return base::CallbackListSubscription();
}
void FakeKerberosClient::SetTaskDelay(base::TimeDelta delay) {
task_delay_ = delay;
}
void FakeKerberosClient::StartRecordingFunctionCalls() {
DCHECK(!recorded_function_calls_);
recorded_function_calls_ = std::string();
}
std::string FakeKerberosClient::StopRecordingAndGetRecordedFunctionCalls() {
DCHECK(recorded_function_calls_);
std::string result;
recorded_function_calls_->swap(result);
recorded_function_calls_.reset();
return result;
}
std::size_t FakeKerberosClient::GetNumberOfAccounts() const {
return accounts_.size();
}
void FakeKerberosClient::SetSimulatedNumberOfNetworkFailures(
int number_of_failures) {
simulated_number_of_network_failures_ = number_of_failures;
}
void FakeKerberosClient::MaybeRecordFunctionCallForTesting(
const char* function_name) {
if (!recorded_function_calls_)
return;
if (!recorded_function_calls_->empty())
recorded_function_calls_->append(",");
recorded_function_calls_->append(function_name);
}
KerberosClient::TestInterface* FakeKerberosClient::GetTestInterface() {
return this;
}
FakeKerberosClient::AccountData* FakeKerberosClient::GetAccountData(
const std::string& principal_name) {
auto it = base::ranges::find(accounts_, AccountData(principal_name));
return it != accounts_.end() ? &*it : nullptr;
}
FakeKerberosClient::AccountData::AccountData(const std::string& principal_name)
: principal_name(principal_name) {}
FakeKerberosClient::AccountData::AccountData(const AccountData& other) =
default;
FakeKerberosClient::AccountData& FakeKerberosClient::AccountData::operator=(
const AccountData& other) = default;
bool FakeKerberosClient::AccountData::operator==(
const AccountData& other) const {
return principal_name == other.principal_name;
}
bool FakeKerberosClient::AccountData::operator!=(
const AccountData& other) const {
return !(*this == other);
}
void FakeKerberosClient::MapAccountData(RepeatedAccountField* accounts) {
for (const AccountData& data : accounts_) {
kerberos::Account* account = accounts->Add();
account->set_principal_name(data.principal_name);
account->set_krb5conf(data.krb5conf);
account->set_tgt_validity_seconds(data.has_tgt ? kTgtValidity.InSeconds()
: 0);
account->set_tgt_renewal_seconds(data.has_tgt ? kTgtRenewal.InSeconds()
: 0);
account->set_is_managed(data.is_managed);
account->set_password_was_remembered(!data.password.empty());
account->set_use_login_password(data.use_login_password);
}
}
// static
FakeKerberosClient::WhatToRemove FakeKerberosClient::DetermineWhatToRemove(
kerberos::ClearMode mode,
const AccountData& data) {
switch (mode) {
case kerberos::CLEAR_ALL:
return WhatToRemove::kAccount;
case kerberos::CLEAR_ONLY_MANAGED_ACCOUNTS:
return data.is_managed ? WhatToRemove::kAccount : WhatToRemove::kNothing;
case kerberos::CLEAR_ONLY_UNMANAGED_ACCOUNTS:
return !data.is_managed ? WhatToRemove::kAccount : WhatToRemove::kNothing;
case kerberos::CLEAR_ONLY_UNMANAGED_REMEMBERED_PASSWORDS:
return !data.is_managed ? WhatToRemove::kPassword
: WhatToRemove::kNothing;
}
return WhatToRemove::kNothing;
}
} // namespace ash