blob: 9b53e4be453dfc52f31fd58184cd840e884c1613 [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 <limits>
#include <utility>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/sha1.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include "kerberos/krb5_interface.h"
namespace kerberos {
namespace {
constexpr int kInvalidIndex = -1;
// Kerberos config files are stored as storage_dir + principal hash + this.
constexpr char kKrb5ConfFilePart[] = "_krb5.conf";
// Kerberos credential caches are stored as storage_dir + principal hash + this.
constexpr char kKrb5CCFilePart[] = "_krb5cc";
// Account data is stored as storage_dir + this.
constexpr char kAccountsFile[] = "accounts";
// Size limit for GetKerberosFiles (1 MB).
constexpr size_t kKrb5FileSizeLimit = 1024 * 1024;
// Returns the SHA1 hash of |principal_name| as hex string. This is used to
// generate (almost guaranteed) unique filenames for account data.
std::string HashPrincipal(const std::string& principal_name) {
std::string hash = base::SHA1HashString(principal_name);
return base::ToLowerASCII(base::HexEncode(hash.data(), hash.size()));
}
// Reads the file at |path| into |data|. Returns |ERROR_LOCAL_IO| if the file
// could not be read.
WARN_UNUSED_RESULT ErrorType LoadFile(const base::FilePath& path,
std::string* data) {
data->clear();
if (!base::ReadFileToStringWithMaxSize(path, data, kKrb5FileSizeLimit)) {
PLOG(ERROR) << "Failed to read " << path.value();
data->clear();
return ERROR_LOCAL_IO;
}
return ERROR_NONE;
}
// Writes |data| to the file at |path|. Returns |ERROR_LOCAL_IO| if the file
// could not be written.
ErrorType SaveFile(const base::FilePath& path, const std::string& data) {
const int data_size = static_cast<int>(data.size());
if (base::WriteFile(path, data.data(), data_size) != data_size) {
LOG(ERROR) << "Failed to write '" << path.value() << "'";
return ERROR_LOCAL_IO;
}
return ERROR_NONE;
}
} // namespace
AccountManager::AccountManager(
base::FilePath storage_dir,
KerberosFilesChangedCallback kerberos_files_changed,
std::unique_ptr<Krb5Interface> krb5)
: storage_dir_(std::move(storage_dir)),
kerberos_files_changed_(std::move(kerberos_files_changed)),
krb5_(std::move(krb5)) {}
AccountManager::~AccountManager() = default;
ErrorType AccountManager::SaveAccounts() const {
// Copy |accounts_| into proto message.
AccountDataList storage_accounts;
for (const auto& account : accounts_)
*storage_accounts.add_accounts() = account;
// Store serialized proto message on disk.
std::string accounts_blob;
if (!storage_accounts.SerializeToString(&accounts_blob)) {
LOG(ERROR) << "Failed to serialize accounts list to string";
return ERROR_LOCAL_IO;
}
return SaveFile(GetAccountsPath(), accounts_blob);
}
ErrorType AccountManager::LoadAccounts() {
accounts_.clear();
// A missing file counts as a file with empty data.
const base::FilePath accounts_path = GetAccountsPath();
if (!base::PathExists(accounts_path))
return ERROR_NONE;
// Load serialized proto blob.
std::string accounts_blob;
ErrorType error = LoadFile(accounts_path, &accounts_blob);
if (error != ERROR_NONE)
return error;
// Parse blob into proto message.
AccountDataList storage_accounts;
if (!storage_accounts.ParseFromString(accounts_blob)) {
LOG(ERROR) << "Failed to parse accounts list from string";
return ERROR_LOCAL_IO;
}
// Copy data into |accounts_|.
accounts_.reserve(storage_accounts.accounts_size());
for (int n = 0; n < storage_accounts.accounts_size(); ++n)
accounts_.push_back(storage_accounts.accounts(n));
return ERROR_NONE;
}
ErrorType AccountManager::AddAccount(const std::string& principal_name,
bool is_managed) {
int index = GetAccountIndex(principal_name);
if (index != kInvalidIndex) {
// Policy should overwrite user-added accounts, but user-added accounts
// should not overwrite policy accounts.
if (!accounts_[index].is_managed() && is_managed) {
DeleteKerberosFiles(principal_name);
accounts_[index].set_is_managed(is_managed);
SaveAccounts();
}
return ERROR_DUPLICATE_PRINCIPAL_NAME;
}
AccountData data;
data.set_principal_name(principal_name);
data.set_is_managed(is_managed);
accounts_.push_back(std::move(data));
SaveAccounts();
return ERROR_NONE;
}
ErrorType AccountManager::RemoveAccount(const std::string& principal_name) {
int index = GetAccountIndex(principal_name);
if (index == kInvalidIndex)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
DeleteKerberosFiles(principal_name);
accounts_.erase(accounts_.begin() + index);
SaveAccounts();
return ERROR_NONE;
}
void AccountManager::DeleteKerberosFiles(const std::string& principal_name) {
base::DeleteFile(GetKrb5ConfPath(principal_name), false /* recursive */);
const base::FilePath krb5cc_path = GetKrb5CCPath(principal_name);
if (base::PathExists(krb5cc_path)) {
base::DeleteFile(krb5cc_path, false /* recursive */);
TriggerKerberosFilesChanged(principal_name);
}
}
ErrorType AccountManager::ClearAccounts() {
// Early out.
if (accounts_.size() == 0)
return ERROR_NONE;
// Delete all teh dataz.
for (const auto& account : accounts_)
DeleteKerberosFiles(account.principal_name());
accounts_.clear();
SaveAccounts();
return ERROR_NONE;
}
ErrorType AccountManager::ListAccounts(std::vector<Account>* accounts) const {
for (const auto& it : accounts_) {
Account account;
account.set_principal_name(it.principal_name());
account.set_is_managed(it.is_managed());
// TODO(https://crbug.com/952239): Set additional properties.
// Do a best effort reporting results, don't bail on the first error. If
// there's a broken account, the user is able to recover the situation
// this way (reauthenticate or remove account and add back).
// Check PathExists, so that no error is printed if the file doesn't
// exist.
std::string krb5conf;
const base::FilePath krb5conf_path = GetKrb5ConfPath(it.principal_name());
if (base::PathExists(krb5conf_path) &&
LoadFile(krb5conf_path, &krb5conf) == ERROR_NONE) {
account.set_krb5conf(krb5conf);
}
// A missing krb5cc file just translates to an invalid ticket (lifetime
// 0).
Krb5Interface::TgtStatus tgt_status;
const base::FilePath krb5cc_path = GetKrb5CCPath(it.principal_name());
if (base::PathExists(krb5cc_path) &&
krb5_->GetTgtStatus(krb5cc_path, &tgt_status) == ERROR_NONE) {
account.set_tgt_validity_seconds(tgt_status.validity_seconds);
account.set_tgt_renewal_seconds(tgt_status.renewal_seconds);
}
accounts->push_back(std::move(account));
}
return ERROR_NONE;
}
ErrorType AccountManager::SetConfig(const std::string& principal_name,
const std::string& krb5conf) const {
const AccountData* data = GetAccountData(principal_name);
if (!data)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
ErrorType error = SaveFile(GetKrb5ConfPath(principal_name), krb5conf);
// Triggering the signal is only necessary if the credential cache exists.
if (error == ERROR_NONE && base::PathExists(GetKrb5CCPath(principal_name)))
TriggerKerberosFilesChanged(principal_name);
return error;
}
ErrorType AccountManager::AcquireTgt(const std::string& principal_name,
const std::string& password) const {
const AccountData* data = GetAccountData(principal_name);
if (!data)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
ErrorType error =
krb5_->AcquireTgt(principal_name, password, GetKrb5CCPath(principal_name),
GetKrb5ConfPath(principal_name));
// Assume the ticket changed if AcquireTgt() was successful.
if (error == ERROR_NONE)
TriggerKerberosFilesChanged(principal_name);
return error;
}
ErrorType AccountManager::GetKerberosFiles(const std::string& principal_name,
KerberosFiles* files) const {
files->clear_krb5cc();
files->clear_krb5conf();
const AccountData* data = GetAccountData(principal_name);
if (!data)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
// By convention, no credential cache means no error.
const base::FilePath krb5cc_path = GetKrb5CCPath(principal_name);
if (!base::PathExists(krb5cc_path))
return ERROR_NONE;
std::string krb5cc;
ErrorType error = LoadFile(krb5cc_path, &krb5cc);
if (error != ERROR_NONE)
return error;
std::string krb5conf;
error = LoadFile(GetKrb5ConfPath(principal_name), &krb5conf);
if (error != ERROR_NONE)
return error;
files->mutable_krb5cc()->assign(krb5cc.begin(), krb5cc.end());
files->mutable_krb5conf()->assign(krb5conf.begin(), krb5conf.end());
return ERROR_NONE;
}
// static
std::string AccountManager::HashPrincipalForTesting(
const std::string& principal_name) {
return HashPrincipal(principal_name);
}
void AccountManager::TriggerKerberosFilesChanged(
const std::string& principal_name) const {
if (!kerberos_files_changed_.is_null())
kerberos_files_changed_.Run(principal_name);
}
base::FilePath AccountManager::GetKrb5ConfPath(
const std::string& principal_name) const {
return storage_dir_.Append(HashPrincipal(principal_name) + kKrb5ConfFilePart);
}
base::FilePath AccountManager::GetKrb5CCPath(
const std::string& principal_name) const {
return storage_dir_.Append(HashPrincipal(principal_name) + kKrb5CCFilePart);
}
base::FilePath AccountManager::GetAccountsPath() const {
return storage_dir_.Append(kAccountsFile);
}
int AccountManager::GetAccountIndex(const std::string& principal_name) const {
for (size_t n = 0; n < accounts_.size(); ++n) {
if (accounts_[n].principal_name() == principal_name) {
CHECK(n <= std::numeric_limits<int>::max());
return static_cast<int>(n);
}
}
return kInvalidIndex;
}
const AccountData* AccountManager::GetAccountData(
const std::string& principal_name) const {
int index = GetAccountIndex(principal_name);
return index != kInvalidIndex ? &accounts_[index] : nullptr;
}
} // namespace kerberos