blob: a3487906453f6b8f509f9609f5d75505957674a2 [file] [log] [blame] [edit]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "secagentd/device_user.h"
#include <unistd.h>
#include <list>
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/uuid.h"
#include "bindings/device_management_backend.pb.h"
#include "brillo/errors/error.h"
#include "brillo/files/file_util.h"
#include "cryptohome/proto_bindings/UserDataAuth.pb.h"
#include "policy/device_local_account_policy_util.h"
#include "secagentd/common.h"
namespace secagentd {
DeviceUser::DeviceUser(
std::unique_ptr<org::chromium::SessionManagerInterfaceProxyInterface>
session_manager)
: DeviceUser(std::move(session_manager),
std::make_unique<org::chromium::UserDataAuthInterfaceProxy>(
common::GetDBus()),
base::FilePath("/")) {}
DeviceUser::DeviceUser(
std::unique_ptr<org::chromium::SessionManagerInterfaceProxyInterface>
session_manager,
std::unique_ptr<org::chromium::UserDataAuthInterfaceProxyInterface>
cryptohome_proxy,
const base::FilePath& root_path)
: weak_ptr_factory_(this),
session_manager_(std::move(session_manager)),
cryptohome_proxy_(std::move(cryptohome_proxy)),
root_path_(root_path) {}
void DeviceUser::RegisterSessionChangeHandler() {
session_manager_->GetObjectProxy()->WaitForServiceToBeAvailable(
base::BindOnce(
[](org::chromium::SessionManagerInterfaceProxyInterface*
session_manager,
base::WeakPtr<DeviceUser> weak_ptr, bool available) {
if (!available) {
LOG(ERROR) << "Failed to register for session_manager's session "
"change signal";
return;
}
session_manager->RegisterSessionStateChangedSignalHandler(
base::BindRepeating(&DeviceUser::OnSessionStateChange,
weak_ptr),
base::BindOnce(&DeviceUser::OnRegistrationResult, weak_ptr));
session_manager->GetObjectProxy()->SetNameOwnerChangedCallback(
base::BindRepeating(&DeviceUser::OnSessionManagerNameChange,
weak_ptr));
},
session_manager_.get(), weak_ptr_factory_.GetWeakPtr()));
}
void DeviceUser::RegisterRemoveCompletedHandler() {
cryptohome_proxy_->GetObjectProxy()->WaitForServiceToBeAvailable(
base::BindOnce(
[](org::chromium::UserDataAuthInterfaceProxyInterface*
cryptohome_proxy,
base::RepeatingCallback<void(
const user_data_auth::RemoveCompleted&)> signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback,
bool available) {
if (!available) {
LOG(ERROR) << "Failed to register for RemoveCompleted signal";
return;
}
cryptohome_proxy->RegisterRemoveCompletedSignalHandler(
std::move(signal_callback), std::move(on_connected_callback));
},
cryptohome_proxy_.get(),
base::BindRepeating(&DeviceUser::OnRemoveCompleted,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&DeviceUser::OnRegistrationResult,
weak_ptr_factory_.GetWeakPtr())));
}
void DeviceUser::RegisterScreenLockedHandler(
base::RepeatingClosure signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback) {
session_manager_->GetObjectProxy()->WaitForServiceToBeAvailable(
base::BindOnce(
[](org::chromium::SessionManagerInterfaceProxyInterface*
session_manager,
base::RepeatingClosure signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback,
bool available) {
if (!available) {
LOG(ERROR) << "Failed to register for session_manager's screen "
"locked signal";
return;
}
session_manager->RegisterScreenIsLockedSignalHandler(
std::move(signal_callback), std::move(on_connected_callback));
},
session_manager_.get(), std::move(signal_callback),
std::move(on_connected_callback)));
}
std::string DeviceUser::GetSanitizedUsername() {
return sanitized_username_;
}
void DeviceUser::RegisterScreenUnlockedHandler(
base::RepeatingClosure signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback) {
session_manager_->GetObjectProxy()->WaitForServiceToBeAvailable(
base::BindOnce(
[](org::chromium::SessionManagerInterfaceProxyInterface*
session_manager,
base::RepeatingClosure signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback,
bool available) {
if (!available) {
LOG(ERROR) << "Failed to register for session_manager's screen "
"unlocked signal";
return;
}
session_manager->RegisterScreenIsUnlockedSignalHandler(
std::move(signal_callback), std::move(on_connected_callback));
},
session_manager_.get(), std::move(signal_callback),
std::move(on_connected_callback)));
}
void DeviceUser::RegisterSessionChangeListener(
base::RepeatingCallback<void(const std::string&)> cb) {
session_change_listeners_.push_back(std::move(cb));
}
void DeviceUser::OnSessionManagerNameChange(const std::string& old_owner,
const std::string& new_owner) {
// When session manager crashes it logs user out.
device_user_ = device_user::kEmpty;
sanitized_username_ = device_user::kEmpty;
LOG(INFO) << "Session manager's name changed. Old: " << old_owner
<< " New: " << new_owner;
}
void DeviceUser::GetDeviceUserAsync(
base::OnceCallback<void(const std::string& device_user,
const std::string& device_userhash)> cb) {
if (device_user_ready_) {
std::move(cb).Run(device_user_, sanitized_username_);
} else {
on_device_user_ready_cbs_.push_back(std::move(cb));
}
}
std::list<std::string> DeviceUser::GetUsernamesForRedaction() {
return redacted_usernames_;
}
void DeviceUser::OnRegistrationResult(const std::string& interface,
const std::string& signal,
bool success) {
if (!success) {
LOG(ERROR) << "Callback registration failed for dbus signal: " << signal
<< " on interface: " << interface;
device_user_ = device_user::kUnknown;
sanitized_username_ = device_user::kUnknown;
} else if (interface == "org.chromium.SessionManagerInterface" &&
signal == "SessionStateChanged") {
OnSessionStateChange(kInit);
// [DO NOT REMOVE]: Removing or changing this log will break tast testing.
// Log message that signifies secagentd is listening for session_manager's
// session state change signal so testing can continue.
VLOG(1) << "[DO NOT REMOVE] Used for tast testing: Listening for session "
"state changes";
}
}
void DeviceUser::OnRemoveCompleted(
const user_data_auth::RemoveCompleted& remove_completed) {
if (remove_completed.sanitized_username().empty()) {
LOG(ERROR) << "RemoveCompleted signal has no username";
return;
}
auto remove_directory = root_path_.Append(kSecagentdDirectory)
.Append(remove_completed.sanitized_username());
if (!brillo::DeletePathRecursively(remove_directory)) {
LOG(ERROR) << "Failed to delete removed user's affiliation file";
}
}
void DeviceUser::OnSessionStateChange(const std::string& state) {
device_user_ready_ = false;
if (state == kStarted || state == kInit) {
flush_cb_.Run();
UpdateDeviceId();
if (!UpdateDeviceUser(state)) {
return;
}
}
device_user_ready_ = true;
for (auto& cb : on_device_user_ready_cbs_) {
std::move(cb).Run(device_user_, sanitized_username_);
}
on_device_user_ready_cbs_.clear();
for (auto cb : session_change_listeners_) {
cb.Run(state);
}
if (state == kStopping) {
device_user_ = device_user::kEmpty;
sanitized_username_ = device_user::kEmpty;
} else if (state == kStopped) {
device_user_ = device_user::kEmpty;
sanitized_username_ = device_user::kEmpty;
}
}
void DeviceUser::UpdateDeviceId() {
if (device_id_ != device_user::kEmpty) {
return;
}
auto response = RetrievePolicy(login_manager::ACCOUNT_TYPE_DEVICE, "");
if (!response.ok()) {
LOG(ERROR) << response.status();
return;
}
auto device_policy = response.value();
if (device_policy.device_affiliation_ids_size() >= 1) {
device_id_ = device_policy.device_affiliation_ids()[0];
if (device_policy.user_affiliation_ids_size() > 1) {
// There should only be 1 ID in the list.
LOG(ERROR) << "Greater than 1 Device ID. Count = "
<< device_policy.user_affiliation_ids_size();
}
}
}
bool DeviceUser::UpdateDeviceUser(const std::string& state) {
// Check if guest session is active.
bool is_guest = false;
brillo::ErrorPtr error;
if (!session_manager_->IsGuestSessionActive(&is_guest, &error) ||
error.get()) {
device_user_ = device_user::kUnknown;
sanitized_username_ = device_user::kUnknown;
// Do not exit method because possible that it is user session.
LOG(ERROR) << "Failed to deterimine if guest session "
<< error->GetMessage();
} else if (is_guest) {
device_user_ = device_user::kGuest;
sanitized_username_ = device_user::kGuest;
return true;
}
// Retrieve the device username.
std::string username;
std::string sanitized;
if (!session_manager_->RetrievePrimarySession(&username, &sanitized,
&error) ||
error.get()) {
device_user_ = device_user::kUnknown;
sanitized_username_ = device_user::kUnknown;
LOG(ERROR) << "Failed to retrieve primary session " << error->GetMessage();
return true;
} else {
// No active session.
if (username.empty()) {
// Only set as empty when Guest session retrieval succeeds.
if (device_user_ != device_user::kUnknown) {
device_user_ = device_user::kEmpty;
sanitized_username_ = device_user::kEmpty;
}
return true;
}
// Set the username for redaction.
if (std::find(redacted_usernames_.begin(), redacted_usernames_.end(),
username) == redacted_usernames_.end()) {
redacted_usernames_.push_front(username);
}
if (SetDeviceUserIfLocalAccount(username)) {
return true;
}
sanitized_username_ = sanitized;
// Check if sanitzed username directory exists.
base::FilePath directory_path =
root_path_.Append(kSecagentdDirectory).Append(sanitized);
if (base::DirectoryExists(directory_path)) {
std::string uuid;
if (base::PathExists(directory_path.Append("affiliated"))) {
device_user_ = username;
return true;
} else if (base::GetFileSize(directory_path.Append("unaffiliated"))
.value_or(0) == 0) {
LOG(ERROR)
<< "Failed to get username file size. Checking policy instead";
} else if (!base::ReadFileToString(directory_path.Append("unaffiliated"),
&uuid) ||
username.empty()) {
LOG(ERROR) << "Failed to read uuid. Checking policy instead";
} else {
device_user_ = uuid;
return true;
}
}
// When a user logs in for the first time there is a delay for their
// ID to be added. Add a slight delay so the ID can appear.
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DeviceUser::HandleUserPolicyAndNotifyListeners,
weak_ptr_factory_.GetWeakPtr(), state, username,
directory_path),
base::Seconds(2));
}
return false;
}
absl::StatusOr<enterprise_management::PolicyData> DeviceUser::RetrievePolicy(
login_manager::PolicyAccountType account_type,
const std::string& account_id) {
login_manager::PolicyDescriptor descriptor;
descriptor.set_account_type(account_type);
descriptor.set_account_id(account_id);
descriptor.set_domain(login_manager::POLICY_DOMAIN_CHROME);
std::string account_type_string =
account_type == login_manager::PolicyAccountType::ACCOUNT_TYPE_DEVICE
? "device"
: "user";
brillo::ErrorPtr error;
std::vector<uint8_t> out_blob;
std::string descriptor_string = descriptor.SerializeAsString();
if (!session_manager_->RetrievePolicyEx(
std::vector<uint8_t>(descriptor_string.begin(),
descriptor_string.end()),
&out_blob, &error) ||
error.get()) {
return absl::InternalError("Failed to retrieve " + account_type_string +
" policy " + error->GetMessage());
}
enterprise_management::PolicyFetchResponse response;
if (!response.ParseFromArray(out_blob.data(), out_blob.size())) {
return absl::InternalError("Failed to parse policy response for " +
account_type_string);
}
enterprise_management::PolicyData policy_data;
if (!policy_data.ParseFromArray(response.policy_data().data(),
response.policy_data().size())) {
return absl::InternalError("Failed to parse policy data for " +
account_type_string);
}
return policy_data;
}
bool DeviceUser::IsAffiliated(
const enterprise_management::PolicyData& user_policy) {
std::string user_id = "unset";
if (user_policy.user_affiliation_ids_size() >= 1) {
user_id = user_policy.user_affiliation_ids()[0];
if (user_policy.user_affiliation_ids_size() > 1) {
// There should only be 1 ID in the list.
LOG(ERROR) << "Greater than 1 User ID. Count = "
<< user_policy.user_affiliation_ids_size();
}
}
return user_id == device_id_;
}
bool DeviceUser::SetDeviceUserIfLocalAccount(std::string& username) {
base::expected<DeviceAccountType, policy::GetDeviceLocalAccountTypeError>
account_type = policy::GetDeviceLocalAccountType(&username);
if (!account_type.has_value()) {
return false;
}
auto it = local_account_map_.find(account_type.value());
if (it == local_account_map_.end()) {
LOG(ERROR) << "Unrecognized local account " << account_type.value();
device_user_ = device_user::kUnknown;
} else {
device_user_ = it->second;
}
return true;
}
void DeviceUser::HandleUserPolicyAndNotifyListeners(
const std::string& state,
std::string username,
base::FilePath user_directory) {
bool directory_exists = false;
if (base::DirectoryExists(user_directory) ||
base::CreateDirectory(user_directory)) {
directory_exists = true;
} else {
LOG(ERROR)
<< "Failed to create user directory. Not saving affiliation status.";
}
// Retrieve user policy information.
auto response = RetrievePolicy(login_manager::ACCOUNT_TYPE_USER, username);
if (!response.ok()) {
device_user_ = device_user::kUnknown;
LOG(ERROR) << response.status();
} else {
auto policy_data = response.value();
// Fill in device_user if user is affiliated.
if (IsAffiliated(policy_data)) {
device_user_ = username;
user_directory = user_directory.Append("affiliated");
// Do not store the real name on the device, just mark as affiliated.
if (directory_exists &&
!base::ImportantFileWriter::WriteFileAtomically(user_directory, "")) {
LOG(ERROR) << "Failed to write username to file";
}
} else {
device_user_ = device_user::kUnaffiliatedPrefix +
base::Uuid::GenerateRandomV4().AsLowercaseString();
user_directory = user_directory.Append("unaffiliated");
if (directory_exists && !base::ImportantFileWriter::WriteFileAtomically(
user_directory, device_user_)) {
LOG(ERROR) << "Failed to write username to file";
}
}
}
device_user_ready_ = true;
for (auto& cb : on_device_user_ready_cbs_) {
std::move(cb).Run(device_user_, sanitized_username_);
}
on_device_user_ready_cbs_.clear();
// Notify listeners.
for (auto cb : session_change_listeners_) {
cb.Run(state);
}
}
bool DeviceUser::GetIsUnaffiliated() {
// If there is no device user or it is one of the managed local accounts then
// it is considered affiliated.
const std::unordered_set<std::string> reporting_values = {
device_user::kEmpty, device_user::kManagedGuest, device_user::kKioskApp};
// If the user is unaffiliated their name will be a UUID. If they are
// affiliated it will be their email which contains the @ symbol.
return (!reporting_values.contains(device_user_) &&
device_user_.find("@") == std::string::npos);
}
std::string DeviceUser::GetUsernameBasedOnAffiliation(
const std::string& username, const std::string& sanitized_username) {
base::FilePath directory_path =
root_path_.Append(kSecagentdDirectory).Append(sanitized_username);
if (base::DirectoryExists(directory_path)) {
std::string uuid;
if (base::PathExists(directory_path.Append("affiliated"))) {
return username;
} else if (base::GetFileSize(directory_path.Append("unaffiliated"))
.value_or(0) == 0) {
LOG(ERROR) << "Failed to get username file size.";
return device_user::kUnknown;
} else if (!base::ReadFileToString(directory_path.Append("unaffiliated"),
&uuid) ||
username.empty()) {
LOG(ERROR) << "Failed to read uuid.";
return device_user::kUnknown;
} else {
return uuid;
}
} else {
// When a user has never signed into the device before their affiliation
// cannot be determined. User will be marked as unknown in that case.
LOG(INFO)
<< "Directory does not exist for user that failed authentication. "
"Marking as Unknown";
return device_user::kUnknown;
}
}
void DeviceUser::SetFlushCallback(base::RepeatingCallback<void()> cb) {
flush_cb_ = std::move(cb);
}
} // namespace secagentd