blob: 43a90d80bf0130293f212a1ba1be53f036e230cf [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "kerberos/account_manager.h"
#include <map>
#include <memory>
#include <utility>
#include <base/bind.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/memory/ref_counted.h>
#include <base/test/test_mock_time_task_runner.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <libpasswordprovider/fake_password_provider.h>
#include <libpasswordprovider/password_provider_test_utils.h>
#include "kerberos/fake_krb5_interface.h"
#include "kerberos/kerberos_metrics.h"
#include "kerberos/krb5_jail_wrapper.h"
using testing::_;
using testing::Mock;
using testing::NiceMock;
using testing::Return;
namespace kerberos {
namespace {
constexpr char kUser[] = "user@REALM.COM";
constexpr char kUser2[] = "user2@REALM2.COM";
constexpr char kUser3[] = "user3@REALM3.COM";
constexpr char kPassword[] = "i<3k3R8e5Oz";
constexpr char kPassword2[] = "ih4zf00d";
constexpr char kKrb5Conf[] = R"(
[libdefaults]
forwardable = true)";
constexpr char kStrongKrb5Conf[] = R"(
[libdefaults]
forwardable = true
default_tkt_enctypes = aes256-cts-hmac-sha1-96
default_tgs_enctypes = aes256-cts-hmac-sha1-96
permitted_enctypes = aes256-cts-hmac-sha1-96)";
constexpr char kLegacyKrb5Conf[] = R"(
[libdefaults]
forwardable = true
default_tkt_enctypes = arcfour-hmac
default_tgs_enctypes = arcfour-hmac
permitted_enctypes = arcfour-hmac)";
constexpr char kInvalidKrb5Conf[] = R"(
[libdefaults]
stonkskew = 123)";
constexpr Krb5Interface::TgtStatus kValidTgt(3600, 3600);
constexpr Krb5Interface::TgtStatus kExpiredTgt(0, 0);
// Convenience defines to make code more readable
constexpr bool kManaged = true;
constexpr bool kUnmanaged = false;
constexpr bool kRememberPassword = true;
constexpr bool kDontRememberPassword = false;
constexpr bool kUseLoginPassword = true;
constexpr bool kDontUseLoginPassword = false;
constexpr char kEmptyPassword[] = "";
class MockMetrics : public KerberosMetrics {
public:
explicit MockMetrics(const base::FilePath& storage_dir)
: KerberosMetrics(storage_dir) {}
MockMetrics(const MockMetrics&) = delete;
MockMetrics& operator=(const MockMetrics&) = delete;
~MockMetrics() override = default;
MOCK_METHOD(bool, ShouldReportDailyUsageStats, (), (override));
MOCK_METHOD(void,
ReportDailyUsageStats,
(int, int, int, int, int),
(override));
MOCK_METHOD(void,
ReportKerberosEncryptionTypes,
(KerberosEncryptionTypes),
(override));
};
} // namespace
class AccountManagerTest : public ::testing::Test {
public:
AccountManagerTest()
: kerberos_files_changed_(
base::BindRepeating(&AccountManagerTest::OnKerberosFilesChanged,
base::Unretained(this))),
kerberos_ticket_expiring_(
base::BindRepeating(&AccountManagerTest::OnKerberosTicketExpiring,
base::Unretained(this))) {}
AccountManagerTest(const AccountManagerTest&) = delete;
AccountManagerTest& operator=(const AccountManagerTest&) = delete;
~AccountManagerTest() override = default;
void SetUp() override {
::testing::Test::SetUp();
// Create temp directory for files written during tests.
CHECK(storage_dir_.CreateUniqueTempDir());
accounts_path_ = storage_dir_.GetPath().Append("accounts");
account_dir_ = storage_dir_.GetPath().Append(
AccountManager::GetSafeFilenameForTesting(kUser));
krb5cc_path_ = account_dir_.Append("krb5cc");
krb5conf_path_ = account_dir_.Append("krb5.conf");
password_path_ = account_dir_.Append("password");
// Create the manager with a fake krb5 interface.
auto krb5 = std::make_unique<FakeKrb5Interface>();
auto password_provider =
std::make_unique<password_provider::FakePasswordProvider>();
metrics_ = std::make_unique<NiceMock<MockMetrics>>(storage_dir_.GetPath());
krb5_ = krb5.get();
password_provider_ = password_provider.get();
manager_ = std::make_unique<AccountManager>(
storage_dir_.GetPath(), kerberos_files_changed_,
kerberos_ticket_expiring_, std::move(krb5),
std::move(password_provider), metrics_.get());
}
void TearDown() override {
// Make sure the file stored on disk contains the same accounts as the
// manager instance. This catches cases where AccountManager forgets to save
// accounts on some change.
if (base::PathExists(accounts_path_)) {
std::vector<Account> accounts = manager_->ListAccounts();
AccountManager other_manager(
storage_dir_.GetPath(), kerberos_files_changed_,
kerberos_ticket_expiring_, std::make_unique<FakeKrb5Interface>(),
std::make_unique<password_provider::FakePasswordProvider>(),
metrics_.get());
other_manager.LoadAccounts();
std::vector<Account> other_accounts = other_manager.ListAccounts();
ASSERT_NO_FATAL_FAILURE(ExpectAccountsEqual(accounts, other_accounts));
}
::testing::Test::TearDown();
}
// Add account with default settings.
ErrorType AddAccount() { return manager_->AddAccount(kUser, kUnmanaged); }
// Sets some default Kerberos configuration.
ErrorType SetConfig() { return SetConfig(kKrb5Conf); }
// Sets Kerberos configuration.
ErrorType SetConfig(const std::string& config) {
return manager_->SetConfig(kUser, config);
}
// Acquire Kerberos ticket with default credentials and settings.
ErrorType AcquireTgt() {
return manager_->AcquireTgt(kUser, kPassword, kDontRememberPassword,
kDontUseLoginPassword);
}
void SaveLoginPassword(const char* password) {
auto password_ptr = password_provider::test::CreatePassword(password);
password_provider_->SavePassword(*password_ptr);
}
// Fast forwards to the next scheduled task (assumed to be the renewal task)
// and verifies expectation that |krb5_->RenewTgt() was called|.
void RunScheduledRenewalTask() {
int initial_count = krb5_->renew_tgt_call_count();
EXPECT_EQ(1, task_runner_->GetPendingTaskCount());
task_runner_->FastForwardBy(task_runner_->NextPendingTaskDelay());
EXPECT_EQ(initial_count + 1, krb5_->renew_tgt_call_count());
}
protected:
void OnKerberosFilesChanged(const std::string& principal_name) {
kerberos_files_changed_count_[principal_name]++;
}
void OnKerberosTicketExpiring(const std::string& principal_name) {
kerberos_ticket_expiring_count_[principal_name]++;
}
void ExpectAccountsEqual(const std::vector<Account>& account_list_1,
const std::vector<Account>& account_list_2) {
ASSERT_EQ(account_list_1.size(), account_list_2.size());
for (size_t n = 0; n < account_list_1.size(); ++n) {
const Account& account1 = account_list_1[n];
const Account& account2 = account_list_2[n];
EXPECT_EQ(account1.principal_name(), account2.principal_name());
EXPECT_EQ(account1.is_managed(), account2.is_managed());
EXPECT_EQ(account1.use_login_password(), account2.use_login_password());
// TODO(https://crbug.com/952239): Check additional properties.
}
}
std::unique_ptr<AccountManager> manager_;
// Fake Kerberos interface used by |manager_|. Not owned.
FakeKrb5Interface* krb5_;
// Fake password provider to get the login password. Not owned.
password_provider::FakePasswordProvider* password_provider_;
// Mock metrics for testing UMA stat recording.
std::unique_ptr<NiceMock<MockMetrics>> metrics_;
// Paths of files stored by |manager_|.
base::ScopedTempDir storage_dir_;
base::FilePath accounts_path_;
base::FilePath account_dir_;
base::FilePath krb5conf_path_;
base::FilePath krb5cc_path_;
base::FilePath password_path_;
AccountManager::KerberosFilesChangedCallback kerberos_files_changed_;
AccountManager::KerberosTicketExpiringCallback kerberos_ticket_expiring_;
std::map<std::string, int> kerberos_files_changed_count_;
std::map<std::string, int> kerberos_ticket_expiring_count_;
scoped_refptr<base::TestMockTimeTaskRunner> task_runner_{
new base::TestMockTimeTaskRunner()};
base::TestMockTimeTaskRunner::ScopedContext scoped_context_{task_runner_};
};
// Adding an account succeeds and serializes the file on disk.
TEST_F(AccountManagerTest, AddAccountSuccess) {
EXPECT_FALSE(base::PathExists(accounts_path_));
EXPECT_EQ(ERROR_NONE, AddAccount());
EXPECT_TRUE(base::PathExists(accounts_path_));
}
// AddAccount() fails if the same account is added twice.
TEST_F(AccountManagerTest, AddDuplicateAccountFail) {
ignore_result(AddAccount());
EXPECT_TRUE(base::DeleteFile(accounts_path_));
EXPECT_EQ(ERROR_DUPLICATE_PRINCIPAL_NAME, AddAccount());
EXPECT_FALSE(base::PathExists(accounts_path_));
}
// Adding a managed account overwrites an existing unmanaged account.
TEST_F(AccountManagerTest, ManagedOverridesUnmanaged) {
ignore_result(manager_->AddAccount(kUser, kUnmanaged));
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_TRUE(base::PathExists(krb5cc_path_));
// Overwriting with a managed account should wipe existing files and make the
// account managed.
EXPECT_EQ(ERROR_DUPLICATE_PRINCIPAL_NAME,
manager_->AddAccount(kUser, kManaged));
EXPECT_FALSE(base::PathExists(krb5cc_path_));
std::vector<Account> accounts = manager_->ListAccounts();
ASSERT_EQ(1u, accounts.size());
EXPECT_TRUE(accounts[0].is_managed());
}
// Adding an unmanaged account does not overwrite an existing managed account.
TEST_F(AccountManagerTest, UnmanagedDoesNotOverrideManaged) {
ignore_result(manager_->AddAccount(kUser, kManaged));
EXPECT_EQ(ERROR_DUPLICATE_PRINCIPAL_NAME,
manager_->AddAccount(kUser, kUnmanaged));
std::vector<Account> accounts = manager_->ListAccounts();
ASSERT_EQ(1u, accounts.size());
EXPECT_TRUE(accounts[0].is_managed());
}
// RemoveAccount() succeeds if the account exists and serializes the file on
// disk.
TEST_F(AccountManagerTest, RemoveAccountSuccess) {
ignore_result(AddAccount());
EXPECT_TRUE(base::DeleteFile(accounts_path_));
EXPECT_EQ(ERROR_NONE, manager_->RemoveAccount(kUser));
EXPECT_TRUE(base::PathExists(accounts_path_));
}
// RemoveAccount() fails if the account does not exist.
TEST_F(AccountManagerTest, RemoveUnknownAccountFail) {
EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME, manager_->RemoveAccount(kUser));
EXPECT_FALSE(base::PathExists(accounts_path_));
}
// RemoveAccount() does not trigger KerberosFilesChanged if the credential cache
// does not exists.
TEST_F(AccountManagerTest, RemoveAccountTriggersKFCIfCCExists) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, manager_->RemoveAccount(kUser));
EXPECT_EQ(0, kerberos_files_changed_count_[kUser]);
}
// RemoveAccount() triggers KerberosFilesChanged if the credential cache exists.
TEST_F(AccountManagerTest, RemoveAccountDoesNotTriggerKFCIfCCDoesNotExist) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_EQ(1, kerberos_files_changed_count_[kUser]);
EXPECT_EQ(ERROR_NONE, manager_->RemoveAccount(kUser));
EXPECT_EQ(2, kerberos_files_changed_count_[kUser]);
}
// Repeatedly calling AddAccount() and RemoveAccount() succeeds.
TEST_F(AccountManagerTest, RepeatedAddRemoveSuccess) {
ignore_result(AddAccount());
ignore_result(manager_->RemoveAccount(kUser));
EXPECT_EQ(ERROR_NONE, AddAccount());
EXPECT_EQ(ERROR_NONE, manager_->RemoveAccount(kUser));
}
// ClearAccounts(CLEAR_ALL) clears all accounts.
TEST_F(AccountManagerTest, ClearAccountsSuccess) {
ignore_result(manager_->AddAccount(kUser, kUnmanaged));
ignore_result(manager_->AddAccount(kUser2, kManaged));
EXPECT_EQ(ERROR_NONE, manager_->ClearAccounts(CLEAR_ALL, {}));
std::vector<Account> accounts = manager_->ListAccounts();
EXPECT_EQ(0u, accounts.size());
}
// ClearAccounts(CLEAR_ALL) wipes Kerberos configuration and credential cache.
TEST_F(AccountManagerTest, ClearAccountsRemovesKerberosFiles) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, SetConfig());
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_TRUE(base::PathExists(krb5conf_path_));
EXPECT_TRUE(base::PathExists(krb5cc_path_));
EXPECT_EQ(ERROR_NONE, manager_->ClearAccounts(CLEAR_ALL, {}));
EXPECT_FALSE(base::PathExists(krb5conf_path_));
EXPECT_FALSE(base::PathExists(krb5cc_path_));
}
// ClearAccounts(CLEAR_ALL) triggers KerberosFilesChanged if the credential
// cache exists.
TEST_F(AccountManagerTest, ClearAccountsTriggersKFCIfCCExists) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_EQ(1, kerberos_files_changed_count_[kUser]);
EXPECT_EQ(ERROR_NONE, manager_->ClearAccounts(CLEAR_ALL, {}));
EXPECT_EQ(2, kerberos_files_changed_count_[kUser]);
}
// ClearAccounts(CLEAR_ALL) does not trigger KerberosFilesChanged if the
// credential cache does not exist.
TEST_F(AccountManagerTest, ClearAccountsDoesNotTriggerKFCIfDoesNotCCExist) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, manager_->ClearAccounts(CLEAR_ALL, {}));
EXPECT_EQ(0, kerberos_files_changed_count_[kUser]);
}
// ClearAccounts(CLEAR_ONLY_UNMANAGED_ACCOUNTS) clears only unmanaged accounts.
TEST_F(AccountManagerTest, ClearUnmanagedAccountsSuccess) {
ignore_result(manager_->AddAccount(kUser, kUnmanaged));
ignore_result(manager_->AddAccount(kUser2, kManaged));
EXPECT_EQ(ERROR_NONE,
manager_->ClearAccounts(CLEAR_ONLY_UNMANAGED_ACCOUNTS, {}));
std::vector<Account> accounts = manager_->ListAccounts();
ASSERT_EQ(1u, accounts.size());
EXPECT_EQ(kUser2, accounts[0].principal_name());
}
// ClearAccounts(CLEAR_ONLY_UNMANAGED_REMEMBERED_PASSWORDS) clears only
// passwords of unmanaged accounts.
TEST_F(AccountManagerTest, ClearUnmanagedPasswordsSuccess) {
// kUser is unmanaged, kUser2 is managed.
ignore_result(manager_->AddAccount(kUser, kUnmanaged));
ignore_result(manager_->AddAccount(kUser2, kManaged));
ignore_result(manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
ignore_result(manager_->AcquireTgt(kUser2, kPassword, kRememberPassword,
kDontUseLoginPassword));
base::FilePath password_path_2 =
storage_dir_.GetPath()
.Append(AccountManager::GetSafeFilenameForTesting(kUser2))
.Append("password");
EXPECT_TRUE(base::PathExists(password_path_));
EXPECT_TRUE(base::PathExists(password_path_2));
EXPECT_EQ(ERROR_NONE, manager_->ClearAccounts(
CLEAR_ONLY_UNMANAGED_REMEMBERED_PASSWORDS, {}));
std::vector<Account> accounts = manager_->ListAccounts();
ASSERT_EQ(2u, accounts.size());
EXPECT_FALSE(base::PathExists(password_path_));
EXPECT_TRUE(base::PathExists(password_path_2));
}
// ClearAccounts(CLEAR_ONLY_MANAGED_ACCOUNTS) clears only managed accounts that
// are not on the keep list.
TEST_F(AccountManagerTest, ClearManagedPasswordsWithKeepListSuccess) {
ignore_result(manager_->AddAccount(kUser, kManaged));
ignore_result(manager_->AddAccount(kUser2, kManaged));
ignore_result(manager_->AddAccount(kUser3, kUnmanaged));
// Keep the managed kUser-account.
EXPECT_EQ(ERROR_NONE,
manager_->ClearAccounts(CLEAR_ONLY_MANAGED_ACCOUNTS, {kUser}));
std::vector<Account> accounts = manager_->ListAccounts();
ASSERT_EQ(2u, accounts.size());
EXPECT_EQ(kUser, accounts[0].principal_name());
EXPECT_EQ(kUser3, accounts[1].principal_name());
}
// SetConfig() succeeds and writes the config to |krb5conf_path_|.
TEST_F(AccountManagerTest, SetConfigSuccess) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, SetConfig());
std::string krb5_conf;
EXPECT_TRUE(base::ReadFileToString(krb5conf_path_, &krb5_conf));
EXPECT_EQ(krb5_conf, kKrb5Conf);
}
// SetConfig() calls ValidateConfig on the Kerberos interface.
TEST_F(AccountManagerTest, SetConfigValidatesConfig) {
ignore_result(AddAccount());
krb5_->set_validate_config_error(ERROR_BAD_CONFIG);
EXPECT_EQ(ERROR_BAD_CONFIG, SetConfig());
}
// SetConfig() triggers KerberosFilesChanged if the credential cache exists.
TEST_F(AccountManagerTest, SetConfigTriggersKFCIfCCExists) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_EQ(1, kerberos_files_changed_count_[kUser]);
EXPECT_EQ(ERROR_NONE, SetConfig());
EXPECT_EQ(2, kerberos_files_changed_count_[kUser]);
}
// SetConfig() does not trigger KerberosFilesChanged if the credential cache
// does not exist.
TEST_F(AccountManagerTest, SetConfigDoesNotTriggerKFCIfDoesNotCCExist) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, SetConfig());
EXPECT_EQ(0, kerberos_files_changed_count_[kUser]);
}
// RemoveAccount() removes the config file.
TEST_F(AccountManagerTest, RemoveAccountRemovesConfig) {
ignore_result(AddAccount());
ignore_result(SetConfig());
EXPECT_TRUE(base::PathExists(krb5conf_path_));
EXPECT_EQ(ERROR_NONE, manager_->RemoveAccount(kUser));
EXPECT_FALSE(base::PathExists(krb5conf_path_));
}
// ValidateConfig() validates a good config successfully.
TEST_F(AccountManagerTest, ValidateConfigSuccess) {
constexpr char kValidKrb5Conf[] = "";
ConfigErrorInfo error_info;
EXPECT_EQ(ERROR_NONE, manager_->ValidateConfig(kValidKrb5Conf, &error_info));
EXPECT_EQ(CONFIG_ERROR_NONE, error_info.code());
}
// ValidateConfig() returns the correct error for a bad config.
TEST_F(AccountManagerTest, ValidateConfigFailure) {
ConfigErrorInfo expected_error_info;
expected_error_info.set_code(CONFIG_ERROR_SECTION_SYNTAX);
krb5_->set_config_error_info(expected_error_info);
krb5_->set_validate_config_error(ERROR_BAD_CONFIG);
constexpr char kBadKrb5Conf[] =
"[libdefaults]'); DROP TABLE KerberosTickets;--";
ConfigErrorInfo error_info;
EXPECT_EQ(ERROR_BAD_CONFIG,
manager_->ValidateConfig(kBadKrb5Conf, &error_info));
EXPECT_EQ(expected_error_info.SerializeAsString(),
error_info.SerializeAsString());
}
// AcquireTgt() succeeds and writes a credential cache file.
TEST_F(AccountManagerTest, AcquireTgtSuccess) {
ignore_result(AddAccount());
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_TRUE(base::PathExists(krb5cc_path_));
}
// AcquireTgt() triggers KerberosFilesChanged on success.
TEST_F(AccountManagerTest, AcquireTgtTriggersKFCOnSuccess) {
ignore_result(AddAccount());
EXPECT_EQ(0, kerberos_files_changed_count_[kUser]);
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_EQ(1, kerberos_files_changed_count_[kUser]);
}
// AcquireTgt() does not trigger KerberosFilesChanged on failure.
TEST_F(AccountManagerTest, AcquireTgtDoesNotTriggerKFCOnFailure) {
ignore_result(AddAccount());
krb5_->set_acquire_tgt_error(ERROR_UNKNOWN);
EXPECT_EQ(ERROR_UNKNOWN, AcquireTgt());
EXPECT_EQ(0, kerberos_files_changed_count_[kUser]);
}
// AcquireTgt() saves password to disk if |remember_password| is true and
// removes the file again if |remember_password| is false.
TEST_F(AccountManagerTest, AcquireTgtRemembersPasswordsIfWanted) {
ignore_result(AddAccount());
EXPECT_FALSE(base::PathExists(password_path_));
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
EXPECT_TRUE(base::PathExists(password_path_));
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kPassword, kDontRememberPassword,
kDontUseLoginPassword));
EXPECT_FALSE(base::PathExists(password_path_));
}
// AcquireTgt() uses saved password if none is given, no matter if it should be
// remembered again or not.
TEST_F(AccountManagerTest, AcquireTgtLoadsRememberedPassword) {
ignore_result(AddAccount());
ignore_result(manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
// This should load stored password and keep it.
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kEmptyPassword, kRememberPassword,
kDontUseLoginPassword));
EXPECT_TRUE(base::PathExists(password_path_));
// This should load stored password, but erase it afterwards.
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kEmptyPassword, kDontRememberPassword,
kDontUseLoginPassword));
EXPECT_FALSE(base::PathExists(password_path_));
// Check that the fake krb5 interface returns an error for a missing password.
// This verifies that the above AcquireTgt() call actually loaded the
// password from disk.
EXPECT_EQ(ERROR_BAD_PASSWORD,
manager_->AcquireTgt(kUser, kEmptyPassword, kDontRememberPassword,
kDontUseLoginPassword));
}
// AcquireTgt() uses the login password if saved.
TEST_F(AccountManagerTest, AcquireTgtUsesLoginPassword) {
ignore_result(AddAccount());
// Shouldn't explode if the login password not set yet.
EXPECT_EQ(ERROR_BAD_PASSWORD,
manager_->AcquireTgt(kUser, kEmptyPassword, kDontRememberPassword,
kUseLoginPassword));
SaveLoginPassword(kPassword);
krb5_->set_expected_password(kPassword);
// Uses the login password.
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kEmptyPassword, kDontRememberPassword,
kUseLoginPassword));
// Check if auth fails without kUseLoginPassword.
EXPECT_EQ(ERROR_BAD_PASSWORD,
manager_->AcquireTgt(kUser, kEmptyPassword, kDontRememberPassword,
kDontUseLoginPassword));
}
// AcquireTgt() wipes a saved password if the login password is used.
TEST_F(AccountManagerTest, AcquireTgtWipesStoredPasswordOnUsesLoginPassword) {
ignore_result(AddAccount());
ignore_result(manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
EXPECT_TRUE(base::PathExists(password_path_));
SaveLoginPassword(kPassword);
// Note: kRememberPassword gets ignored if kUseLoginPassword is passed.
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kEmptyPassword, kRememberPassword,
kUseLoginPassword));
EXPECT_FALSE(base::PathExists(password_path_));
}
// AcquireTgt() ignores the passed password if the login password is used.
TEST_F(AccountManagerTest, AcquireTgtIgnoresPassedPasswordOnUsesLoginPassword) {
ignore_result(AddAccount());
SaveLoginPassword(kPassword);
krb5_->set_expected_password(kPassword);
// Auth works despite passed kPassword2 != expected kPassword because the
// login kPassword is used.
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kPassword2, kDontRememberPassword,
kUseLoginPassword));
}
// AcquireTgt() records all encryption types UMA stats on success.
TEST_F(AccountManagerTest, AcquireTgtEnctypesMetricsAll) {
ignore_result(AddAccount());
ignore_result(SetConfig());
// The expected encryption type should be reported through |metric_|.
EXPECT_CALL(*metrics_,
ReportKerberosEncryptionTypes(KerberosEncryptionTypes::kAll));
EXPECT_EQ(ERROR_NONE, AcquireTgt());
}
// AcquireTgt() records strong encryption types UMA stats on success.
TEST_F(AccountManagerTest, AcquireTgtEnctypesMetricsStrong) {
ignore_result(AddAccount());
ignore_result(SetConfig(kStrongKrb5Conf));
// The expected encryption type should be reported through |metric_|.
EXPECT_CALL(*metrics_,
ReportKerberosEncryptionTypes(KerberosEncryptionTypes::kStrong));
EXPECT_EQ(ERROR_NONE, AcquireTgt());
}
// AcquireTgt() records legacy encryption types UMA stats on success.
TEST_F(AccountManagerTest, AcquireTgtEnctypesMetricsLegacy) {
ignore_result(AddAccount());
ignore_result(SetConfig(kLegacyKrb5Conf));
// The expected encryption type should be reported through |metric_|.
EXPECT_CALL(*metrics_,
ReportKerberosEncryptionTypes(KerberosEncryptionTypes::kLegacy));
EXPECT_EQ(ERROR_NONE, AcquireTgt());
}
// AcquireTgt() doesn't record encryption types UMA stats on failure.
TEST_F(AccountManagerTest, AcquireTgtEnctypesMetricsFailure) {
ignore_result(AddAccount());
ignore_result(SetConfig());
// No encryption type should be reported through |metric_|.
EXPECT_CALL(*metrics_, ReportKerberosEncryptionTypes(_)).Times(0);
krb5_->set_acquire_tgt_error(ERROR_UNKNOWN);
EXPECT_EQ(ERROR_UNKNOWN, AcquireTgt());
}
// AcquireTgt() doesn't record encryption types UMA stats if no config is
// available.
TEST_F(AccountManagerTest, AcquireTgtEnctypesMetricsNoConfig) {
ignore_result(AddAccount());
// No encryption type should be reported through |metric_|.
EXPECT_CALL(*metrics_, ReportKerberosEncryptionTypes(_)).Times(0);
EXPECT_EQ(ERROR_NONE, AcquireTgt());
}
// AcquireTgt() doesn't record encryption types UMA stats if config is invalid.
TEST_F(AccountManagerTest, AcquireTgtEnctypesMetricsInvalidConfig) {
ignore_result(AddAccount());
ignore_result(SetConfig(kInvalidKrb5Conf));
// No encryption type should be reported through |metric_|.
EXPECT_CALL(*metrics_, ReportKerberosEncryptionTypes(_)).Times(0);
EXPECT_EQ(ERROR_NONE, AcquireTgt());
}
// RemoveAccount() removes the credential cache file.
TEST_F(AccountManagerTest, RemoveAccountRemovesCC) {
ignore_result(AddAccount());
ignore_result(AcquireTgt());
EXPECT_TRUE(base::PathExists(krb5cc_path_));
EXPECT_EQ(ERROR_NONE, manager_->RemoveAccount(kUser));
EXPECT_FALSE(base::PathExists(krb5cc_path_));
}
// RemoveAccount() removes saved passwords.
TEST_F(AccountManagerTest, RemoveAccountRemovesPassword) {
ignore_result(AddAccount());
ignore_result(manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
EXPECT_TRUE(base::PathExists(password_path_));
EXPECT_EQ(ERROR_NONE, manager_->RemoveAccount(kUser));
EXPECT_FALSE(base::PathExists(password_path_));
}
// ListAccounts() succeeds and contains the expected data.
TEST_F(AccountManagerTest, ListAccountsSuccess) {
ignore_result(manager_->AddAccount(kUser, kManaged));
ignore_result(SetConfig());
ignore_result(manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
SaveLoginPassword(kPassword);
ignore_result(manager_->AddAccount(kUser2, kUnmanaged));
// Note: kRememberPassword should be ignored here, see below.
ignore_result(manager_->AcquireTgt(kUser2, kPassword, kRememberPassword,
kUseLoginPassword));
EXPECT_TRUE(base::PathExists(krb5cc_path_));
// Set a fake tgt status.
constexpr int kRenewalSeconds = 10;
constexpr int kValiditySeconds = 90;
krb5_->set_tgt_status(
Krb5Interface::TgtStatus(kValiditySeconds, kRenewalSeconds));
// Verify that ListAccounts returns the expected account.
std::vector<Account> accounts = manager_->ListAccounts();
ASSERT_EQ(2u, accounts.size());
EXPECT_EQ(kUser, accounts[0].principal_name());
EXPECT_EQ(kKrb5Conf, accounts[0].krb5conf());
EXPECT_EQ(kRenewalSeconds, accounts[0].tgt_renewal_seconds());
EXPECT_EQ(kValiditySeconds, accounts[0].tgt_validity_seconds());
EXPECT_TRUE(accounts[0].is_managed());
EXPECT_TRUE(accounts[0].password_was_remembered());
EXPECT_EQ(kUser2, accounts[1].principal_name());
EXPECT_FALSE(accounts[1].password_was_remembered());
EXPECT_TRUE(accounts[1].use_login_password());
}
// ListAccounts() ignores failures in GetTgtStatus() and loading the config.
TEST_F(AccountManagerTest, ListAccountsIgnoresFailures) {
ignore_result(AddAccount());
ignore_result(SetConfig());
ignore_result(AcquireTgt());
EXPECT_TRUE(base::PathExists(krb5cc_path_));
// Make reading the config fail.
EXPECT_TRUE(base::SetPosixFilePermissions(krb5conf_path_, 0));
// Make GetTgtStatus() fail.
krb5_->set_get_tgt_status_error(ERROR_UNKNOWN);
// ListAccounts() should still work, despite the errors.
std::vector<Account> accounts = manager_->ListAccounts();
ASSERT_EQ(1u, accounts.size());
EXPECT_EQ(kUser, accounts[0].principal_name());
// The config should not be set since we made reading the file fail.
EXPECT_FALSE(accounts[0].has_krb5conf());
// tgt_*_seconds should not be set since we made GetTgtStatus() fail.
EXPECT_FALSE(accounts[0].has_tgt_renewal_seconds());
EXPECT_FALSE(accounts[0].has_tgt_validity_seconds());
}
// GetKerberosFiles returns empty KerberosFiles if there is no credential cache,
// even if there is a config.
TEST_F(AccountManagerTest, GetKerberosFilesSucceedsWithoutCC) {
ignore_result(AddAccount());
ignore_result(SetConfig());
KerberosFiles files;
EXPECT_EQ(ERROR_NONE, manager_->GetKerberosFiles(kUser, &files));
EXPECT_FALSE(files.has_krb5cc());
EXPECT_FALSE(files.has_krb5conf());
}
// GetKerberosFiles returns the expected KerberosFiles if there is a credential
// cache.
TEST_F(AccountManagerTest, GetKerberosFilesSucceedsWithCC) {
ignore_result(AddAccount());
ignore_result(SetConfig());
ignore_result(AcquireTgt());
KerberosFiles files;
EXPECT_EQ(ERROR_NONE, manager_->GetKerberosFiles(kUser, &files));
EXPECT_FALSE(files.krb5cc().empty());
EXPECT_EQ(kKrb5Conf, files.krb5conf());
}
// Most methods return ERROR_UNKNOWN_PRINCIPAL if called with such a principal.
TEST_F(AccountManagerTest, MethodsReturnUnknownPrincipal) {
KerberosFiles files;
EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME, manager_->RemoveAccount(kUser));
EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME, SetConfig());
EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME, AcquireTgt());
EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME,
manager_->GetKerberosFiles(kUser, &files));
}
// Accounts can be saved to disk and loaded from disk.
TEST_F(AccountManagerTest, SerializationSuccess) {
ignore_result(manager_->AddAccount(kUser, kManaged));
ignore_result(manager_->AcquireTgt(kUser, kPassword, kDontRememberPassword,
kUseLoginPassword));
ignore_result(manager_->AddAccount(kUser2, kUnmanaged));
ignore_result(manager_->AcquireTgt(kUser2, kPassword, kDontRememberPassword,
kDontUseLoginPassword));
EXPECT_EQ(ERROR_NONE, manager_->SaveAccounts());
AccountManager other_manager(
storage_dir_.GetPath(), kerberos_files_changed_,
kerberos_ticket_expiring_, std::make_unique<FakeKrb5Interface>(),
std::make_unique<password_provider::FakePasswordProvider>(),
metrics_.get());
other_manager.LoadAccounts();
std::vector<Account> accounts = other_manager.ListAccounts();
ASSERT_EQ(2u, accounts.size());
EXPECT_EQ(kUser, accounts[0].principal_name());
EXPECT_EQ(kUser2, accounts[1].principal_name());
EXPECT_TRUE(accounts[0].is_managed());
EXPECT_FALSE(accounts[1].is_managed());
EXPECT_TRUE(accounts[0].use_login_password());
EXPECT_FALSE(accounts[1].use_login_password());
// TODO(https://crbug.com/952239): Check additional Account properties.
}
// The StartObservingTickets() method triggers KerberosTicketExpiring for
// expired signals and starts observing valid tickets.
TEST_F(AccountManagerTest, StartObservingTickets) {
krb5_->set_tgt_status(kValidTgt);
ignore_result(AddAccount());
ignore_result(SetConfig());
ignore_result(AcquireTgt());
EXPECT_EQ(0, kerberos_ticket_expiring_count_[kUser]);
task_runner_->ClearPendingTasks();
// Fake an expired ticket. Check that KerberosTicketExpiring is triggered, but
// no renewal task is scheduled.
krb5_->set_tgt_status(kExpiredTgt);
manager_->StartObservingTickets();
EXPECT_EQ(1, kerberos_ticket_expiring_count_[kUser]);
EXPECT_EQ(0, task_runner_->GetPendingTaskCount());
// Fake a valid ticket. Check that KerberosTicketExpiring is NOT triggered,
// but a renewal task is scheduled.
krb5_->set_tgt_status(kValidTgt);
EXPECT_EQ(0, task_runner_->GetPendingTaskCount());
manager_->StartObservingTickets();
EXPECT_EQ(1, task_runner_->GetPendingTaskCount());
EXPECT_EQ(1, kerberos_ticket_expiring_count_[kUser]);
EXPECT_EQ(0, krb5_->renew_tgt_call_count());
task_runner_->FastForwardBy(task_runner_->NextPendingTaskDelay());
EXPECT_EQ(1, krb5_->renew_tgt_call_count());
}
// When a TGT is acquired successfully, automatic renewal is scheduled.
TEST_F(AccountManagerTest, AcquireTgtSchedulesRenewalOnSuccess) {
ignore_result(AddAccount());
krb5_->set_tgt_status(kValidTgt);
EXPECT_EQ(0, task_runner_->GetPendingTaskCount());
EXPECT_EQ(ERROR_NONE, AcquireTgt());
EXPECT_EQ(1, task_runner_->GetPendingTaskCount());
}
// When a TGT fails to be acquired, no automatic renewal is scheduled.
TEST_F(AccountManagerTest, AcquireTgtDoesNotScheduleRenewalOnFailure) {
ignore_result(AddAccount());
krb5_->set_tgt_status(kValidTgt);
krb5_->set_acquire_tgt_error(ERROR_UNKNOWN);
EXPECT_EQ(0, task_runner_->GetPendingTaskCount());
EXPECT_EQ(ERROR_UNKNOWN, AcquireTgt());
EXPECT_EQ(0, task_runner_->GetPendingTaskCount());
}
// A scheduled TGT renewal task calls |krb5_->RenewTgt()|.
TEST_F(AccountManagerTest, AutoRenewalCallsRenewTgt) {
krb5_->set_tgt_status(kValidTgt);
ignore_result(AddAccount());
ignore_result(AcquireTgt());
int initial_acquire_tgt_call_count = krb5_->acquire_tgt_call_count();
// Set some return value for the RenewTgt() call and fast forward to scheduled
// renewal task.
const ErrorType expected_error = ERROR_UNKNOWN;
krb5_->set_renew_tgt_error(expected_error);
RunScheduledRenewalTask();
EXPECT_EQ(initial_acquire_tgt_call_count, krb5_->acquire_tgt_call_count());
EXPECT_EQ(expected_error, manager_->last_renew_tgt_error_for_testing());
}
// A scheduled TGT renewal task calls |krb5_->AcquireTgt()| using the login
// password if the call to |krb5_->RenewTgt()| fails and the login password was
// used for the initial AcquireTgt() call.
TEST_F(AccountManagerTest, AutoRenewalUsesLoginPasswordIfRenewalFails) {
krb5_->set_tgt_status(kValidTgt);
ignore_result(AddAccount());
// Acquire TGT with login password.
SaveLoginPassword(kPassword);
krb5_->set_expected_password(kPassword);
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, std::string(), kDontRememberPassword,
kUseLoginPassword));
int initial_acquire_tgt_call_count = krb5_->acquire_tgt_call_count();
krb5_->set_renew_tgt_error(ERROR_UNKNOWN);
RunScheduledRenewalTask();
// The scheduled renewal task should have called AcquireTgt() with the login
// password and succeeded.
EXPECT_EQ(initial_acquire_tgt_call_count + 1,
krb5_->acquire_tgt_call_count());
EXPECT_EQ(ERROR_NONE, manager_->last_renew_tgt_error_for_testing());
}
// A scheduled TGT renewal task calls |krb5_->AcquireTgt()| using the remembered
// password if the call to |krb5_->RenewTgt()| fails and the password was
// remembered for the initial AcquireTgt() call.
TEST_F(AccountManagerTest, AutoRenewalUsesRememberedPasswordIfRenewalFails) {
krb5_->set_tgt_status(kValidTgt);
ignore_result(AddAccount());
// Acquire TGT and remember password.
krb5_->set_expected_password(kPassword);
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
int initial_acquire_tgt_call_count = krb5_->acquire_tgt_call_count();
krb5_->set_renew_tgt_error(ERROR_UNKNOWN);
RunScheduledRenewalTask();
// The scheduled renewal task should have called AcquireTgt() with the
// remembered password and succeeded.
EXPECT_EQ(initial_acquire_tgt_call_count + 1,
krb5_->acquire_tgt_call_count());
EXPECT_EQ(ERROR_NONE, manager_->last_renew_tgt_error_for_testing());
}
// A scheduled TGT renewal task does not call |krb5_->AcquireTgt()| using the
// remembered password if the call to |krb5_->RenewTgt()| succeeds and the
// password was remembered for the initial AcquireTgt() call (similar for login
// password, but we don't test that).
TEST_F(AccountManagerTest, AutoRenewalDoesNotCallAcquireTgtIfRenewalSucceeds) {
krb5_->set_tgt_status(kValidTgt);
ignore_result(AddAccount());
// Acquire TGT and remember password.
krb5_->set_expected_password(kPassword);
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
int initial_acquire_tgt_call_count = krb5_->acquire_tgt_call_count();
krb5_->set_renew_tgt_error(ERROR_NONE);
RunScheduledRenewalTask();
// The scheduled renewal task should NOT have called AcquireTgt() again since
// |krb5_->RenewTgt()|.
EXPECT_EQ(initial_acquire_tgt_call_count, krb5_->acquire_tgt_call_count());
EXPECT_EQ(ERROR_NONE, manager_->last_renew_tgt_error_for_testing());
}
// Verifies that all files written have the expected access permissions.
// Unfortunately, file ownership can't be tested as the test won't run as
// kerberosd user nor can it switch to it.
TEST_F(AccountManagerTest, FilePermissions) {
constexpr int kFileMode_rw =
base::FILE_PERMISSION_READ_BY_USER | base::FILE_PERMISSION_WRITE_BY_USER;
constexpr int kFileMode_rw_r =
kFileMode_rw | base::FILE_PERMISSION_READ_BY_GROUP;
constexpr int kFileMode_rw_r__r =
kFileMode_rw_r | base::FILE_PERMISSION_READ_BY_OTHERS;
constexpr int kFileMode_rwxrwx =
base::FILE_PERMISSION_USER_MASK | base::FILE_PERMISSION_GROUP_MASK;
// Wrap the fake krb5 in a jail wrapper to get the file permissions of krb5cc
// right. Note that we can't use a Krb5JailWrapper for the whole test since
// that would break the counters in FakeKrb5Interface (they would be inc'ed in
// another process!).
manager_->WrapKrb5ForTesting();
// Can't set user in this test.
Krb5JailWrapper::DisableChangeUserForTesting(true);
EXPECT_EQ(ERROR_NONE, AddAccount());
EXPECT_EQ(ERROR_NONE, SetConfig());
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kPassword, kRememberPassword,
kDontUseLoginPassword));
int mode;
EXPECT_TRUE(GetPosixFilePermissions(accounts_path_, &mode));
EXPECT_EQ(kFileMode_rw, mode);
EXPECT_TRUE(GetPosixFilePermissions(account_dir_, &mode));
EXPECT_EQ(kFileMode_rwxrwx, mode);
EXPECT_TRUE(GetPosixFilePermissions(krb5cc_path_, &mode));
EXPECT_EQ(kFileMode_rw_r, mode);
EXPECT_TRUE(GetPosixFilePermissions(krb5conf_path_, &mode));
EXPECT_EQ(kFileMode_rw_r__r, mode);
EXPECT_TRUE(GetPosixFilePermissions(password_path_, &mode));
EXPECT_EQ(kFileMode_rw, mode);
}
// Tests that [Should]ReportDailyUsageStats is called as advertised.
TEST_F(AccountManagerTest, ReportDailyUsageStats) {
// ShouldReportDailyUsageStats() should be called by GetKerberosFiles() even
// if there is no account, and if that returns true, ReportDailyUsageStats()
// should be called as well.
EXPECT_CALL(*metrics_, ShouldReportDailyUsageStats()).WillOnce(Return(true));
EXPECT_CALL(*metrics_, ReportDailyUsageStats(0, 0, 0, 0, 0));
KerberosFiles files;
EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME,
manager_->GetKerberosFiles(kUser, &files));
Mock::VerifyAndClearExpectations(metrics_.get());
AddAccount();
// ShouldReportDailyUsageStats() should be called by AcquireTgt(), but if that
// returns false, ReportDailyUsageStats() should NOT be called.
EXPECT_CALL(*metrics_, ShouldReportDailyUsageStats()).WillOnce(Return(false));
EXPECT_CALL(*metrics_, ReportDailyUsageStats(_, _, _, _, _)).Times(0);
AcquireTgt();
Mock::VerifyAndClearExpectations(metrics_.get());
// ShouldReportDailyUsageStats() should be called by AcquireTgt(), and if that
// returns true, ReportDailyUsageStats() should be called as well.
EXPECT_CALL(*metrics_, ShouldReportDailyUsageStats()).WillOnce(Return(true));
EXPECT_CALL(*metrics_, ReportDailyUsageStats(_, _, _, _, _));
AcquireTgt();
Mock::VerifyAndClearExpectations(metrics_.get());
}
TEST_F(AccountManagerTest, AccountStats) {
SaveLoginPassword(kPassword);
EXPECT_EQ(ERROR_NONE, manager_->AddAccount(kUser, kManaged));
EXPECT_EQ(ERROR_NONE, manager_->AddAccount(kUser2, kManaged));
EXPECT_EQ(ERROR_NONE, manager_->AddAccount(kUser3, kUnmanaged));
// Set metrics up so that the stats are reported on the last call.
EXPECT_CALL(*metrics_, ShouldReportDailyUsageStats())
.WillOnce(Return(false))
.WillOnce(Return(false))
.WillOnce(Return(true));
// 3 accounts total, 2 managed, 1 unmanaged, 1 remembering the password, 1
// using the login password.
EXPECT_CALL(*metrics_, ReportDailyUsageStats(3, 2, 1, 1, 1));
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser, kPassword, kDontRememberPassword,
kDontUseLoginPassword));
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser2, kPassword, kRememberPassword,
kDontUseLoginPassword));
EXPECT_EQ(ERROR_NONE,
manager_->AcquireTgt(kUser3, kEmptyPassword, kDontRememberPassword,
kUseLoginPassword));
}
} // namespace kerberos