blob: a7da6844e03d684f102590e668100ea55f58d53f [file] [log] [blame]
// Copyright 2017 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 "authpolicy/authpolicy.h"
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <base/bind_helpers.h>
#include <base/callback.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/message_loop/message_loop.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/test/simple_test_clock.h>
#include <brillo/asan.h>
#include <brillo/dbus/dbus_method_invoker.h>
#include <brillo/file_utils.h>
#include <dbus/bus.h>
#include <dbus/cryptohome/dbus-constants.h>
#include <dbus/login_manager/dbus-constants.h>
#include <dbus/message.h>
#include <dbus/mock_bus.h>
#include <dbus/mock_exported_object.h>
#include <dbus/mock_object_proxy.h>
#include <dbus/object_path.h>
#include <dbus/scoped_dbus_error.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <login_manager/proto_bindings/policy_descriptor.pb.h>
#include <policy/device_policy_impl.h>
#include "authpolicy/anonymizer.h"
#include "authpolicy/path_service.h"
#include "authpolicy/policy/preg_policy_writer.h"
#include "authpolicy/proto_bindings/active_directory_info.pb.h"
#include "authpolicy/samba_interface.h"
#include "authpolicy/stub_common.h"
#include "bindings/chrome_device_policy.pb.h"
#include "bindings/cloud_policy.pb.h"
#include "bindings/device_management_backend.pb.h"
#include "bindings/policy_constants.h"
using brillo::dbus_utils::DBusObject;
using brillo::dbus_utils::ExtractMethodCallResults;
using dbus::MessageWriter;
using dbus::MockBus;
using dbus::MockExportedObject;
using dbus::MockObjectProxy;
using dbus::ObjectPath;
using dbus::ObjectProxy;
using dbus::Response;
using login_manager::PolicyDescriptor;
using testing::_;
using testing::AnyNumber;
using testing::Invoke;
using testing::NiceMock;
using testing::Return;
using testing::SaveArg;
namespace em = enterprise_management;
namespace authpolicy {
namespace {
// Some arbitrary D-Bus message serial number. Required for mocking D-Bus calls.
const int kDBusSerial = 123;
// Some constants for policy testing.
const bool kPolicyBool = true;
const int kPolicyInt = 321;
const bool kOtherPolicyBool = false;
const int kOtherPolicyInt = 234;
constexpr char kPolicyStr[] = "Str";
constexpr char kOtherPolicyStr[] = "OtherStr";
constexpr char kExtensionId[] = "abcdeFGHabcdefghAbcdefGhabcdEfgh";
constexpr char kOtherExtensionId[] = "ababababcdcdcdcdefefefefghghghgh";
constexpr char kExtensionPolicy1[] = "Policy1";
constexpr char kExtensionPolicy2[] = "Policy2";
constexpr char kMandatoryKey[] = "Policy";
constexpr char kRecommendedKey[] = "Recommended";
// Encryption types in krb5.conf.
constexpr char kKrb5EncTypesAll[] =
"aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 rc4-hmac";
constexpr char kKrb5EncTypesStrong[] =
"aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96";
// Error message when passing different account IDs to authpolicy.
constexpr char kMultiUserNotSupported[] = "Multi-user not supported";
// Stub user hash, returned from the stub Cryptohome proxy's
// GetSanitizedUsername call. Used as part of the user daemon store path.
constexpr char kSanitizedUsername[] = "user_hash";
// Stub daemon store directory used to back up auth state.
constexpr char kDaemonStoreDir[] = "daemon-store";
// SessionStateChanged signal payload we care about.
constexpr char kSessionStarted[] = "started";
constexpr char kSessionStopped[] = "stopped";
// Stub data for auth data cache testing.
constexpr char kCacheTestUserWorkgroup[] = "cache_test_user_workgroup";
constexpr char kCacheTestUserKdcIp[] = "cache_test_user_kdc_ip";
constexpr char kCacheTestUserDcName[] = "cache_test_user_dc_name";
constexpr char kCacheTestMachineWorkgroup[] = "cache_test_machine_workgroup";
constexpr char kCacheTestMachineDcName[] = "cache_test_machine_dc_name";
// See stub_net_main for the strings. Don't bother to make those constants.
constexpr char kDefaultWorkgroup[] = "WOKGROUP";
constexpr char kDefaultKdcIp[] = "111.222.33.2";
constexpr char kDefaultDcName[] = "DCNAME.EXAMPLE.COM";
// Policy values for cache testing.
const int kZeroHours = 0;
const int kNonZeroHours = 1;
struct SmbConf {
std::string machine_name;
std::string realm;
std::string kerberos_encryption_types;
std::string workgroup;
};
struct Krb5Conf {
std::string default_tgs_enctypes;
std::string default_tkt_enctypes;
std::string permitted_enctypes;
std::string allow_weak_crypto;
std::string kdc;
};
// Checks and casts an integer |error| to the corresponding ErrorType.
WARN_UNUSED_RESULT ErrorType CastError(int error) {
EXPECT_GE(error, 0);
EXPECT_LT(error, ERROR_COUNT);
return static_cast<ErrorType>(error);
}
// Create a file descriptor pointing to a pipe that contains the given data.
base::ScopedFD MakeFileDescriptor(const char* data) {
int fds[2];
EXPECT_TRUE(base::CreateLocalNonBlockingPipe(fds));
base::ScopedFD read_scoped_fd(fds[0]);
base::ScopedFD write_scoped_fd(fds[1]);
EXPECT_TRUE(
base::WriteFileDescriptor(write_scoped_fd.get(), data, strlen(data)));
return read_scoped_fd;
}
// Shortcut to create a file descriptor from a valid password (valid in the
// sense that the stub executables won't trigger any error behavior).
base::ScopedFD MakePasswordFd() {
return MakeFileDescriptor(kPassword);
}
// Stub completion callback for RegisterAsync().
void DoNothing(bool /* unused */) {}
// Creates a D-Bus response with the given |response_str| as message.
dbus::Response* RespondWithString(dbus::MethodCall* method_call,
const std::string& response_str) {
method_call->SetSerial(kDBusSerial);
std::unique_ptr<dbus::Response> response =
dbus::Response::FromMethodCall(method_call);
dbus::MessageWriter writer(response.get());
writer.AppendString(response_str);
return response.release();
}
// If |error| is ERROR_NONE, parses |proto_blob| into the |proto| if given.
// Otherwise, makes sure |proto_blob| is empty.
template <typename T>
void MaybeParseProto(int error,
const std::vector<uint8_t>& proto_blob,
T* proto) {
if (error != ERROR_NONE) {
EXPECT_TRUE(proto_blob.empty());
return;
}
if (proto) {
EXPECT_TRUE(proto->ParseFromArray(proto_blob.data(),
static_cast<int>(proto_blob.size())));
}
}
// Reads the smb.conf file at |smb_conf_path| and extracts some values.
void ReadSmbConf(const std::string& smb_conf_path, SmbConf* conf) {
std::string smb_conf;
EXPECT_TRUE(base::ReadFileToString(base::FilePath(smb_conf_path), &smb_conf));
FindToken(smb_conf, '=', "netbios name", &conf->machine_name);
EXPECT_TRUE(FindToken(smb_conf, '=', "realm", &conf->realm));
EXPECT_TRUE(FindToken(smb_conf, '=', "kerberos encryption types",
&conf->kerberos_encryption_types));
EXPECT_TRUE(FindToken(smb_conf, '=', "workgroup", &conf->workgroup));
}
// Checks whether the file at smb_conf_path is an smb.conf file and has the
// expected encryption types |expected_enc_types| set.
void CheckSmbEncTypes(const std::string& smb_conf_path,
const char* expected_enc_types) {
SmbConf conf;
ReadSmbConf(smb_conf_path, &conf);
EXPECT_EQ(expected_enc_types, conf.kerberos_encryption_types);
}
// Reads the krb5.conf file at |krb5_conf_path| and extracts some values.
void ReadKrb5Conf(const std::string& krb5_conf_path, Krb5Conf* conf) {
std::string krb5_conf;
EXPECT_TRUE(
base::ReadFileToString(base::FilePath(krb5_conf_path), &krb5_conf));
EXPECT_TRUE(FindToken(krb5_conf, '=', "default_tgs_enctypes",
&conf->default_tgs_enctypes));
EXPECT_TRUE(FindToken(krb5_conf, '=', "default_tkt_enctypes",
&conf->default_tkt_enctypes));
EXPECT_TRUE(FindToken(krb5_conf, '=', "permitted_enctypes",
&conf->permitted_enctypes));
EXPECT_TRUE(
FindToken(krb5_conf, '=', "allow_weak_crypto", &conf->allow_weak_crypto));
// KDC is optional.
if (!FindToken(krb5_conf, '=', "kdc", &conf->kdc))
conf->kdc.clear();
}
// Checks whether the file at krb5_conf_path is a krb5.conf file and has the
// expected encryption types |expected_enc_types| set.
void CheckKrb5EncTypes(const std::string& krb5_conf_path,
const char* expected_enc_types) {
Krb5Conf conf;
ReadKrb5Conf(krb5_conf_path, &conf);
EXPECT_EQ(expected_enc_types, conf.default_tgs_enctypes);
EXPECT_EQ(expected_enc_types, conf.default_tkt_enctypes);
EXPECT_EQ(expected_enc_types, conf.permitted_enctypes);
EXPECT_EQ("false", conf.allow_weak_crypto);
}
// Returns [|ip|].
std::string EmbraceIp(const std::string& ip) {
return "[" + ip + "]";
}
// Helper class that points some paths to convenient locations we can write to.
class TestPathService : public PathService {
public:
explicit TestPathService(const base::FilePath& base_path)
: PathService(false) {
// Stub binaries are in the OUT folder politely provided by the test script.
base::FilePath stub_path(getenv("OUT"));
CHECK(!stub_path.empty());
// Need to create the run dir since it is usually created by the ebuild.
base::FilePath run_dir = base_path.Append("run");
CHECK(base::CreateDirectory(run_dir));
// Override base directories.
Insert(Path::TEMP_DIR, base_path.Append("temp").value());
Insert(Path::STATE_DIR, base_path.Append("state").value());
Insert(Path::RUN_DIR, run_dir.value());
Insert(Path::DAEMON_STORE_DIR, base_path.Append(kDaemonStoreDir).value());
// Override binaries.
Insert(Path::KINIT, stub_path.Append("stub_kinit").value());
Insert(Path::KLIST, stub_path.Append("stub_klist").value());
Insert(Path::KPASSWD, stub_path.Append("stub_kpasswd").value());
Insert(Path::NET, stub_path.Append("stub_net").value());
Insert(Path::SMBCLIENT, stub_path.Append("stub_smbclient").value());
// Fill in the rest of the paths and build dependend paths.
Initialize();
}
};
// Metrics library that eats in particular timer errors.
class TestMetricsLibrary : public MetricsLibrary {
public:
bool SendToUMA(const std::string&, int, int, int, int) override {
return true;
}
};
// Version of AuthPolicyMetrics that just counts stats.
class TestMetrics : public AuthPolicyMetrics {
public:
TestMetrics() {
// Prevent some error messages from timers.
test_metrics_.Init();
chromeos_metrics::TimerReporter::set_metrics_lib(&test_metrics_);
}
~TestMetrics() override {
chromeos_metrics::TimerReporter::set_metrics_lib(nullptr);
}
void Report(MetricType metric_type, int sample) override {
last_metrics_sample_[metric_type] = sample;
metrics_report_count_[metric_type]++;
}
void ReportError(ErrorMetricType metric_type,
ErrorType /* error */) override {
error_report_count_[metric_type]++;
}
// Returns the most recently reported sample for the given |metric_type| or
// -1 if the metric has not been reported.
int GetLastMetricSample(MetricType metric_type) {
auto iter = last_metrics_sample_.find(metric_type);
return iter != last_metrics_sample_.end() ? iter->second : -1;
}
// Returns how often Report() was called with given |metric_type| and erases
// the count. Inefficient if metric_type isn't in the map, but shorter :)
int GetNumMetricReports(MetricType metric_type) {
const int count = metrics_report_count_[metric_type];
metrics_report_count_.erase(metric_type);
return count;
}
// Returns how often ReportDBusResult() was called with given |metric_type|
// and erases the count. Inefficient if |metric_type| isn't in the map, but
// shorter :)
int GetNumErrorReports(ErrorMetricType metric_type) {
const int count = error_report_count_[metric_type];
error_report_count_.erase(metric_type);
return count;
}
private:
TestMetricsLibrary test_metrics_;
std::map<MetricType, int> last_metrics_sample_;
std::map<MetricType, int> metrics_report_count_;
std::map<ErrorMetricType, int> error_report_count_;
};
// Helper to check the ErrorType value returned by authpolicy D-Bus calls.
// |was_called| is a marker used by the code that queues this callback to make
// sure that this callback was indeed called.
void CheckError(ErrorType expected_error,
bool* was_called,
std::unique_ptr<Response> response) {
EXPECT_TRUE(response.get());
dbus::MessageReader reader(response.get());
int32_t int_error;
EXPECT_TRUE(reader.PopInt32(&int_error));
ErrorType actual_error = CastError(int_error);
EXPECT_EQ(expected_error, actual_error);
EXPECT_TRUE(was_called);
EXPECT_FALSE(*was_called);
*was_called = true;
}
// Matcher for D-Bus method names to be used in CallMethod*().
MATCHER_P(IsMethod, method_name, "") {
return arg->GetMember() == method_name;
}
} // namespace
// Integration test for the authpolicyd D-Bus interface.
//
// Since the Active Directory protocols are a black box to us, a stub local
// server cannot be used. Instead, the Samba/Kerberos binaries are stubbed out.
//
// Error behavior is triggered by passing special user principals or passwords
// to the stub binaries. For instance, using |kNonExistingUserPrincipal| makes
// stub_kinit behave as if the requested account does not exist on the server.
// The same principle is used throughout this test.
//
// During policy fetch, authpolicy sends D-Bus messages to Session Manager. This
// communication is mocked out.
class AuthPolicyTest : public testing::Test {
public:
void SetUp() override {
// The message loop registers a task runner with the current thread, which
// is used by TgtManager to post automatic TGT renewal tasks.
message_loop_ = std::make_unique<base::MessageLoop>();
const ObjectPath object_path(std::string("/object/path"));
auto dbus_object =
std::make_unique<DBusObject>(nullptr, mock_bus_, object_path);
metrics_ = std::make_unique<TestMetrics>();
// Create path service with all paths pointing into a temp directory.
CHECK(base::CreateNewTempDirectory("" /* prefix (ignored) */, &base_path_));
paths_ = std::make_unique<TestPathService>(base_path_);
// Create the state directory since authpolicyd assumes its existence.
const base::FilePath state_path =
base::FilePath(paths_->Get(Path::STATE_DIR));
CHECK(base::CreateDirectory(state_path));
// Create daemon store directory where authpolicyd backs up auth state.
user_daemon_store_path_ =
base_path_.Append(kDaemonStoreDir).Append(kSanitizedUsername);
CHECK(base::CreateDirectory(user_daemon_store_path_));
// Stub path where the Kerberos ticket is backed up.
backup_path_ = user_daemon_store_path_.Append("user_backup_data");
// Set stub preg path. Since it is not trivial to pass the full path to the
// stub binaries, we simply use the directory from the krb5.conf file.
const base::FilePath gpo_dir =
base::FilePath(paths_->Get(Path::USER_KRB5_CONF)).DirName();
DCHECK(gpo_dir ==
base::FilePath(paths_->Get(Path::DEVICE_KRB5_CONF)).DirName());
stub_gpo1_path_ = gpo_dir.Append(kGpo1Filename);
stub_gpo2_path_ = gpo_dir.Append(kGpo2Filename);
// Mock out D-Bus initialization.
mock_exported_object_ =
new MockExportedObject(mock_bus_.get(), object_path);
EXPECT_CALL(*mock_bus_, GetExportedObject(object_path))
.Times(1)
.WillOnce(Return(mock_exported_object_.get()));
EXPECT_CALL(*mock_bus_, GetDBusTaskRunner())
.Times(1)
.WillOnce(Return(message_loop_->task_runner().get()));
EXPECT_CALL(*mock_exported_object_, ExportMethod(_, _, _, _))
.Times(AnyNumber());
EXPECT_CALL(*mock_exported_object_, SendSignal(_))
.WillRepeatedly(
Invoke(this, &AuthPolicyTest::HandleUserKerberosFilesChanged));
// Set up mock object proxy for session manager called from authpolicy.
mock_session_manager_proxy_ = new MockObjectProxy(
mock_bus_.get(), login_manager::kSessionManagerServiceName,
dbus::ObjectPath(login_manager::kSessionManagerServicePath));
EXPECT_CALL(*mock_bus_,
GetObjectProxy(login_manager::kSessionManagerServiceName, _))
.WillOnce(Return(mock_session_manager_proxy_.get()));
EXPECT_CALL(
*mock_session_manager_proxy_,
CallMethodWithErrorCallback(
IsMethod(login_manager::kSessionManagerStoreUnsignedPolicyEx), _, _,
_))
.WillRepeatedly(
Invoke(this, &AuthPolicyTest::StubCallStorePolicyMethod));
EXPECT_CALL(
*mock_session_manager_proxy_,
MockCallMethodAndBlockWithErrorDetails(
IsMethod(login_manager::kSessionManagerListStoredComponentPolicies),
_, _))
.WillRepeatedly(
Invoke(this, &AuthPolicyTest::StubListComponentIdsMethod));
EXPECT_CALL(
*mock_session_manager_proxy_,
ConnectToSignal(login_manager::kSessionManagerInterface,
login_manager::kSessionStateChangedSignal, _, _))
.WillOnce((SaveArg<2>(&session_state_changed_callback_)));
EXPECT_CALL(
*mock_session_manager_proxy_.get(),
MockCallMethodAndBlockWithErrorDetails(
IsMethod(login_manager::kSessionManagerRetrieveSessionState), _, _))
.WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout,
dbus::ScopedDBusError* error) {
return RespondWithString(method_call, kSessionStopped);
}));
// Set up mock object proxy for Cryptohome called from authpolicy.
mock_cryptohome_proxy_ = new NiceMock<MockObjectProxy>(
mock_bus_.get(), cryptohome::kCryptohomeServiceName,
dbus::ObjectPath(cryptohome::kCryptohomeServicePath));
EXPECT_CALL(*mock_bus_,
GetObjectProxy(cryptohome::kCryptohomeServiceName, _))
.WillOnce(Return(mock_cryptohome_proxy_.get()));
// Make Cryptohome's GetSanitizedUsername call return kSanitizedUsername.
ON_CALL(*mock_cryptohome_proxy_, MockCallMethodAndBlock(_, _))
.WillByDefault(Invoke([](dbus::MethodCall* method_call, int timeout) {
return RespondWithString(method_call, kSanitizedUsername);
}));
// Create AuthPolicy instance. Do this AFTER creating the proxy mocks since
// they might be accessed during initialization.
authpolicy_ = std::make_unique<AuthPolicy>(metrics_.get(), paths_.get());
EXPECT_EQ(ERROR_NONE, authpolicy_->Initialize(false /* expect_config */));
authpolicy_->RegisterAsync(std::move(dbus_object), base::Bind(&DoNothing));
// Don't sleep for kinit/smbclient retries, it just prolongs our tests.
samba().DisableRetrySleepForTesting();
// Unit tests usually run code that only exists in tests (like the
// framework), so disable the seccomp filters.
samba().DisableSeccompForTesting(true);
}
// Stub method called by the Session Manager mock to store policy. Validates
// the type of policy (user/device) contained in the |method_call|. If set by
// the individual unit tests, calls |validate_user_policy_| or
// |validate_device_policy_| to validate the contents of the policy proto.
void StubCallStorePolicyMethod(
dbus::MethodCall* method_call,
int /* timeout_ms */,
ObjectProxy::ResponseCallback callback,
dbus::ObjectProxy::ErrorCallback error_callback) {
// Safety check to make sure that old values are not carried along.
if (!store_policy_called_) {
EXPECT_FALSE(user_policy_validated_);
EXPECT_FALSE(device_policy_validated_);
EXPECT_EQ(0, validated_extension_ids_.size());
} else {
// The first policy stored is always user or device policy.
EXPECT_TRUE(user_policy_validated_ ^ device_policy_validated_);
}
store_policy_called_ = true;
// Based on the method name, check whether this is user or device policy.
EXPECT_TRUE(method_call);
EXPECT_TRUE(method_call->GetMember() ==
login_manager::kSessionManagerStoreUnsignedPolicyEx);
// Extract the policy blob from the method call.
std::vector<uint8_t> descriptor_blob;
std::vector<uint8_t> response_blob;
brillo::ErrorPtr error;
EXPECT_TRUE(ExtractMethodCallResults(method_call, &error, &descriptor_blob,
&response_blob));
// Unpack descriptor.
PolicyDescriptor descriptor;
const std::string descriptor_blob_str(descriptor_blob.begin(),
descriptor_blob.end());
EXPECT_TRUE(descriptor.ParseFromString(descriptor_blob_str));
// If policy is deleted, response_blob is an empty string.
if (response_blob.empty()) {
EXPECT_EQ(descriptor.domain(), login_manager::POLICY_DOMAIN_EXTENSIONS);
deleted_extension_ids_.insert(descriptor.component_id());
} else {
// Unwrap the three gazillion layers or policy.
const std::string response_blob_str(response_blob.begin(),
response_blob.end());
em::PolicyFetchResponse policy_response;
EXPECT_TRUE(policy_response.ParseFromString(response_blob_str));
em::PolicyData policy_data;
EXPECT_TRUE(policy_data.ParseFromString(policy_response.policy_data()));
// Run the policy through the appropriate policy validator.
ValidatePolicy(descriptor, policy_data);
}
// Answer authpolicy with an empty response to signal that policy has been
// stored.
EXPECT_FALSE(callback.is_null());
callback.Run(Response::CreateEmpty().get());
}
// Stub method called by the Session Manager mock to list stored component
// policy ids.
dbus::Response* StubListComponentIdsMethod(dbus::MethodCall* method_call,
int /* timeout_ms */,
dbus::ScopedDBusError* error) {
method_call->SetSerial(kDBusSerial);
auto response = dbus::Response::FromMethodCall(method_call);
dbus::MessageWriter writer(response.get());
writer.AppendArrayOfStrings(stored_extension_ids_);
// Note: The mock wraps this back into a std::unique_ptr.
return response.release();
}
void TearDown() override {
EXPECT_EQ(expected_error_reports[ERROR_OF_AUTHENTICATE_USER],
metrics_->GetNumErrorReports(ERROR_OF_AUTHENTICATE_USER));
EXPECT_EQ(expected_error_reports[ERROR_OF_GET_USER_STATUS],
metrics_->GetNumErrorReports(ERROR_OF_GET_USER_STATUS));
EXPECT_EQ(expected_error_reports[ERROR_OF_GET_USER_KERBEROS_FILES],
metrics_->GetNumErrorReports(ERROR_OF_GET_USER_KERBEROS_FILES));
EXPECT_EQ(expected_error_reports[ERROR_OF_JOIN_AD_DOMAIN],
metrics_->GetNumErrorReports(ERROR_OF_JOIN_AD_DOMAIN));
EXPECT_EQ(expected_error_reports[ERROR_OF_REFRESH_USER_POLICY],
metrics_->GetNumErrorReports(ERROR_OF_REFRESH_USER_POLICY));
EXPECT_EQ(expected_error_reports[ERROR_OF_REFRESH_DEVICE_POLICY],
metrics_->GetNumErrorReports(ERROR_OF_REFRESH_DEVICE_POLICY));
EXPECT_CALL(*mock_exported_object_, Unregister()).Times(1);
// Don't not leave no mess behind.
base::DeleteFile(base_path_, true /* recursive */);
}
protected:
// Joins a (stub) Active Directory domain. Returns the error code.
ErrorType Join(const std::string& machine_name,
const std::string& user_principal,
base::ScopedFD password_fd) WARN_UNUSED_RESULT {
JoinDomainRequest request;
request.set_machine_name(machine_name);
request.set_user_principal_name(user_principal);
return JoinEx(request, std::move(password_fd));
}
// Joins a (stub) Active Directory domain, locks the device and fetches empty
// device policy. Expects success.
void JoinAndFetchDevicePolicy(const std::string& machine_name) {
EXPECT_EQ(ERROR_NONE, Join(machine_name, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
}
// Extended Join() that takes a full JoinDomainRequest proto.
ErrorType JoinEx(const JoinDomainRequest& request,
base::ScopedFD password_fd,
std::string* joined_domain = nullptr) WARN_UNUSED_RESULT {
expected_error_reports[ERROR_OF_JOIN_AD_DOMAIN]++;
std::vector<uint8_t> blob(request.ByteSizeLong());
request.SerializeToArray(blob.data(), blob.size());
int error;
authpolicy_->JoinADDomain(blob, password_fd, &error, joined_domain);
return CastError(error);
}
// Authenticates to a (stub) Active Directory domain with the given
// credentials and returns the error code. Assigns the user account info to
// |account_info| if a non-nullptr is provided.
ErrorType Auth(const std::string& user_principal,
const std::string& account_id,
base::ScopedFD password_fd,
ActiveDirectoryAccountInfo* account_info = nullptr)
WARN_UNUSED_RESULT {
int32_t error = ERROR_NONE;
std::vector<uint8_t> account_info_blob;
expected_error_reports[ERROR_OF_AUTHENTICATE_USER]++;
int prev_files_changed_count = user_kerberos_files_changed_count_;
AuthenticateUserRequest request;
request.set_user_principal_name(user_principal);
request.set_account_id(account_id);
std::vector<uint8_t> blob(request.ByteSizeLong());
request.SerializeToArray(blob.data(), blob.size());
authpolicy_->AuthenticateUser(blob, password_fd, &error,
&account_info_blob);
MaybeParseProto(error, account_info_blob, account_info);
// At most one UserKerberosFilesChanged signal should have been fired.
EXPECT_LE(user_kerberos_files_changed_count_, prev_files_changed_count + 1);
return CastError(error);
}
// Gets a fake user status from a (stub) Active Directory service.
// |account_id| is the id (aka objectGUID) of the user. Assigns the user's
// status to |user_status| if a non-nullptr is given.
ErrorType GetUserStatus(const std::string& user_principal,
const std::string& account_id,
ActiveDirectoryUserStatus* user_status = nullptr)
WARN_UNUSED_RESULT {
int32_t error = ERROR_NONE;
std::vector<uint8_t> user_status_blob;
expected_error_reports[ERROR_OF_GET_USER_STATUS]++;
GetUserStatusRequest request;
request.set_user_principal_name(user_principal);
request.set_account_id(account_id);
std::vector<uint8_t> blob(request.ByteSizeLong());
request.SerializeToArray(blob.data(), blob.size());
authpolicy_->GetUserStatus(blob, &error, &user_status_blob);
MaybeParseProto(error, user_status_blob, user_status);
return CastError(error);
}
ErrorType GetUserKerberosFiles(const std::string& account_id,
KerberosFiles* kerberos_files = nullptr)
WARN_UNUSED_RESULT {
int32_t error = ERROR_NONE;
std::vector<uint8_t> kerberos_files_blob;
expected_error_reports[ERROR_OF_GET_USER_KERBEROS_FILES]++;
authpolicy_->GetUserKerberosFiles(account_id, &error, &kerberos_files_blob);
MaybeParseProto(error, kerberos_files_blob, kerberos_files);
return CastError(error);
}
// Authenticates to a (stub) Active Directory domain with default credentials.
// Returns the account id.
std::string DefaultAuth() {
ActiveDirectoryAccountInfo account_info;
EXPECT_EQ(ERROR_NONE,
Auth(kUserPrincipal, "", MakePasswordFd(), &account_info));
return account_info.account_id();
}
// Calls AuthPolicy::RefreshUserPolicy(). Verifies that
// StubCallStorePolicyMethod() and validate_user_policy_ are called as
// expected. These callbacks verify that the policy protobuf is valid and
// validate the contents.
void FetchAndValidateUserPolicy(const std::string& account_id,
ErrorType expected_error) {
dbus::MethodCall method_call(kAuthPolicyInterface,
kRefreshUserPolicyMethod);
method_call.SetSerial(kDBusSerial);
store_policy_called_ = false;
user_policy_validated_ = false;
device_policy_validated_ = false;
validated_extension_ids_.clear();
deleted_extension_ids_.clear();
bool callback_was_called = false;
AuthPolicy::PolicyResponseCallback callback =
std::make_unique<brillo::dbus_utils::DBusMethodResponse<int32_t>>(
&method_call,
base::Bind(&CheckError, expected_error, &callback_was_called));
expected_error_reports[ERROR_OF_REFRESH_USER_POLICY]++;
authpolicy_->RefreshUserPolicy(std::move(callback), account_id);
// If policy fetch succeeds, authpolicy_ makes a D-Bus call to Session
// Manager to store policy. We intercept this call and point it to
// StubCallStorePolicyMethod(), which validates policy and calls CheckError.
// If policy fetch fails, StubCallStorePolicyMethod() is not called, but
// authpolicy calls CheckError directly.
EXPECT_EQ(expected_error == ERROR_NONE, store_policy_called_);
EXPECT_EQ(expected_error == ERROR_NONE, user_policy_validated_);
EXPECT_FALSE(expected_error != ERROR_NONE &&
validated_extension_ids_.size() > 0);
EXPECT_FALSE(device_policy_validated_);
EXPECT_TRUE(callback_was_called); // Make sure CheckError() was called.
}
// Calls AuthPolicy::RefreshDevicePolicy(). Verifies that
// StubCallStorePolicyMethod() and validate_device_policy_ are called as
// expected. These callbacks verify that the policy protobuf is valid and
// validate the contents.
void FetchAndValidateDevicePolicy(ErrorType expected_error) {
dbus::MethodCall method_call(kAuthPolicyInterface,
kRefreshDevicePolicyMethod);
method_call.SetSerial(kDBusSerial);
store_policy_called_ = false;
user_policy_validated_ = false;
device_policy_validated_ = false;
validated_extension_ids_.clear();
deleted_extension_ids_.clear();
bool callback_was_called = false;
AuthPolicy::PolicyResponseCallback callback =
std::make_unique<brillo::dbus_utils::DBusMethodResponse<int32_t>>(
&method_call,
base::Bind(&CheckError, expected_error, &callback_was_called));
expected_error_reports[ERROR_OF_REFRESH_DEVICE_POLICY]++;
authpolicy_->RefreshDevicePolicy(std::move(callback));
// If policy fetch succeeds, authpolicy_ makes a D-Bus call to Session
// Manager to store policy. We intercept this call and point it to
// StubCallStorePolicyMethod(), which validates policy and calls CheckError.
// If policy fetch fails, StubCallStorePolicyMethod() is not called, but
// authpolicy calls CheckError directly.
EXPECT_EQ(expected_error == ERROR_NONE, store_policy_called_);
EXPECT_EQ(expected_error == ERROR_NONE, device_policy_validated_);
EXPECT_FALSE(expected_error != ERROR_NONE &&
validated_extension_ids_.size());
EXPECT_FALSE(user_policy_validated_);
EXPECT_TRUE(callback_was_called); // Make sure CheckError() was called.
}
// Runs the policy stored in |policy_data| through the validator function
// for the corresponding policy type.
void ValidatePolicy(const PolicyDescriptor& descriptor,
const em::PolicyData& policy_data) {
if (policy_data.policy_type() == kChromeUserPolicyType) {
EXPECT_EQ(descriptor.account_type(), login_manager::ACCOUNT_TYPE_USER);
EXPECT_FALSE(descriptor.account_id().empty());
EXPECT_EQ(descriptor.domain(), login_manager::POLICY_DOMAIN_CHROME);
EXPECT_TRUE(descriptor.component_id().empty());
em::CloudPolicySettings policy;
EXPECT_TRUE(policy.ParseFromString(policy_data.policy_value()));
if (validate_user_policy_) {
validate_user_policy_(policy);
user_policy_validated_ = true;
}
user_affiliation_marker_set_ =
policy_data.user_affiliation_ids_size() == 1 &&
policy_data.user_affiliation_ids(0) == kAffiliationMarker;
} else if (policy_data.policy_type() == kChromeDevicePolicyType) {
EXPECT_EQ(descriptor.account_type(), login_manager::ACCOUNT_TYPE_DEVICE);
EXPECT_TRUE(descriptor.account_id().empty());
EXPECT_EQ(descriptor.domain(), login_manager::POLICY_DOMAIN_CHROME);
EXPECT_TRUE(descriptor.component_id().empty());
em::ChromeDeviceSettingsProto policy;
EXPECT_TRUE(policy.ParseFromString(policy_data.policy_value()));
if (validate_device_policy_) {
validate_device_policy_(policy);
device_policy_validated_ = true;
}
EXPECT_EQ(1, policy_data.device_affiliation_ids_size());
EXPECT_EQ(kAffiliationMarker, policy_data.device_affiliation_ids(0));
} else if (policy_data.policy_type() == kChromeExtensionPolicyType) {
EXPECT_EQ(descriptor.domain(), login_manager::POLICY_DOMAIN_EXTENSIONS);
EXPECT_EQ(descriptor.component_id(), policy_data.settings_entity_id());
if (validate_extension_policy_) {
// policy_value() is the raw JSON string here.
validate_extension_policy_(descriptor.component_id(),
policy_data.policy_value());
validated_extension_ids_.insert(descriptor.component_id());
}
}
}
// Checks whether the user |policy| is empty.
static void CheckUserPolicyEmpty(const em::CloudPolicySettings& policy) {
em::CloudPolicySettings empty_policy;
EXPECT_EQ(policy.ByteSize(), empty_policy.ByteSize());
}
// Does not validate user policy. Use if you're testing something unrelated.
static void DontValidateUserPolicy(
const em::CloudPolicySettings& /* policy */) {}
// Checks whether the device |policy| is empty.
static void CheckDevicePolicyEmpty(
const em::ChromeDeviceSettingsProto& policy) {
em::ChromeDeviceSettingsProto empty_policy;
EXPECT_EQ(policy.ByteSize(), empty_policy.ByteSize());
}
// Does not validate device policy. Use if you're testing something unrelated.
static void DontValidateDevicePolicy(
const em::ChromeDeviceSettingsProto& /* policy */) {}
// Checks whether the extension |policy_json| is empty.
static void CheckExtensionPolicyEmpty(const std::string& /* extension_id */,
const std::string& policy_json) {
EXPECT_TRUE(policy_json.empty());
}
// Writes some default extension to the given writer.
static void WriteDefaultExtensionPolicy(policy::PRegPolicyWriter* writer) {
writer->SetKeysForExtensionPolicy(kExtensionId);
writer->AppendString(kExtensionPolicy1, kPolicyStr);
writer->SetKeysForExtensionPolicy(kOtherExtensionId);
writer->AppendBoolean(kExtensionPolicy2, kPolicyBool,
policy::POLICY_LEVEL_RECOMMENDED);
}
// Checks some default extension |policy_json| we're using for this test.
static void CheckDefaultExtensionPolicy(const std::string& extension_id,
const std::string& policy_json) {
std::string expected_policy_json;
if (extension_id == kExtensionId) {
expected_policy_json =
base::StringPrintf("{\"%s\":{\"%s\":\"%s\"}}", kMandatoryKey,
kExtensionPolicy1, kPolicyStr);
} else if (extension_id == kOtherExtensionId) {
expected_policy_json = base::StringPrintf(
"{\"%s\":{\"%s\":1}}", kRecommendedKey, kExtensionPolicy2);
} else {
FAIL() << "Unexpected extension id " << extension_id;
}
EXPECT_EQ(policy_json, expected_policy_json);
}
// Authpolicyd revokes write permissions on config.dat. Some tests perform two
// domain joins, though, and need to overwrite the previously generated config
// file.
bool MakeConfigWriteable() {
const base::FilePath config_path(paths_->Get(Path::CONFIG_DAT));
const int mode = base::FILE_PERMISSION_READ_BY_USER |
base::FILE_PERMISSION_WRITE_BY_USER;
return base::SetPosixFilePermissions(config_path, mode);
}
SambaInterface& samba() { return authpolicy_->GetSambaInterfaceForTesting(); }
void MarkDeviceAsLocked() { authpolicy_->SetDeviceIsLockedForTesting(); }
// Writes one file to |gpo_path| with a few policies. Sets up
// |validate_device_policy_| callback with corresponding expectations.
void SetupDeviceOneGpo(const base::FilePath& gpo_path) {
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kDeviceGuestModeEnabled, kPolicyBool);
writer.AppendInteger(policy::key::kDevicePolicyRefreshRate, kPolicyInt);
writer.AppendString(policy::key::kSystemTimezone, kPolicyStr);
const std::vector<std::string> str_list = {"str1", "str2"};
writer.AppendStringList(policy::key::kDeviceUserWhitelist, str_list);
writer.WriteToFile(gpo_path);
validate_device_policy_ = [str_list](
const em::ChromeDeviceSettingsProto& policy) {
EXPECT_EQ(kPolicyBool, policy.guest_mode_enabled().guest_mode_enabled());
EXPECT_EQ(
kPolicyInt,
policy.device_policy_refresh_rate().device_policy_refresh_rate());
EXPECT_EQ(kPolicyStr, policy.system_timezone().timezone());
const em::UserWhitelistProto& str_list_proto = policy.user_whitelist();
EXPECT_EQ(str_list_proto.user_whitelist_size(),
static_cast<int>(str_list.size()));
for (int n = 0; n < str_list_proto.user_whitelist_size(); ++n)
EXPECT_EQ(str_list_proto.user_whitelist(n), str_list.at(n));
};
}
// Writes a device policy file to |policy_path|. The file can be read with
// libpolicy.
void WriteDevicePolicyFile(const base::FilePath& policy_path,
const em::ChromeDeviceSettingsProto& policy) {
em::PolicyData policy_data;
policy_data.set_policy_value(policy.SerializeAsString());
em::PolicyFetchResponse policy_fetch_response;
policy_fetch_response.set_policy_data(policy_data.SerializeAsString());
std::string policy_blob = policy_fetch_response.SerializeAsString();
brillo::WriteStringToFile(policy_path, policy_blob);
}
// Writes |device_policy| to a file, points samba() to it and reinitializes
// samba(). This simulates a restart of authpolicyd with given device policy.
void WritePolicyAndRestartAuthpolicy(
const em::ChromeDeviceSettingsProto& device_policy) {
const base::FilePath policy_path = base_path_.Append("policy");
WriteDevicePolicyFile(policy_path, device_policy);
// Set up a device policy instance that reads from our fake file.
// Verification has to be disabled since MarkDeviceAsLocked() applies to
// authpolicy only, but doesn't actually set the real install attributes
// read by the impl.
auto policy_impl = std::make_unique<policy::DevicePolicyImpl>();
policy_impl->set_policy_path_for_testing(policy_path);
policy_impl->set_verify_policy_for_testing(false);
// Initialize again. This should load the device policy file.
samba().ResetForTesting();
samba().SetDevicePolicyImplForTesting(std::move(policy_impl));
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
}
// Returns the modification time of the file at |path|.
base::Time GetLastModified(const base::FilePath& path) {
base::File::Info file_info;
EXPECT_TRUE(GetFileInfo(path, &file_info));
return file_info.last_modified;
}
// Returns the modification time of the file at |path|.
base::Time GetLastModified(Path path) {
return GetLastModified(base::FilePath(paths_->Get(path)));
}
void SetLastModified(Path path, const base::Time& last_modified) {
const base::FilePath filepath(paths_->Get(path));
base::File::Info file_info;
EXPECT_TRUE(GetFileInfo(filepath, &file_info));
EXPECT_TRUE(
base::TouchFile(filepath, file_info.last_accessed, last_modified));
}
// Returns the contents of the file at |path|.
std::string ReadFile(Path path) {
std::string str;
EXPECT_TRUE(
base::ReadFileToString(base::FilePath(paths_->Get(path)), &str));
return str;
}
// Sends the session started signal to authpolicyd.
void NotifySessionStarted() {
// Tell authpolicyd that the session started.
dbus::Signal signal(login_manager::kSessionManagerInterface,
login_manager::kSessionStateChangedSignal);
dbus::MessageWriter writer(&signal);
writer.AppendString("started");
session_state_changed_callback_.Run(&signal);
}
std::unique_ptr<base::MessageLoop> message_loop_;
scoped_refptr<MockBus> mock_bus_ = new MockBus(dbus::Bus::Options());
scoped_refptr<MockExportedObject> mock_exported_object_;
scoped_refptr<MockObjectProxy> mock_session_manager_proxy_;
scoped_refptr<MockObjectProxy> mock_cryptohome_proxy_;
// Notifies authpolicy that the session state changed (e.g. "started").
base::Callback<void(dbus::Signal* signal)> session_state_changed_callback_;
// Keep this order! auth_policy_ must be last as it depends on the other two.
std::unique_ptr<TestMetrics> metrics_;
std::unique_ptr<TestPathService> paths_;
std::unique_ptr<AuthPolicy> authpolicy_;
base::FilePath base_path_;
base::FilePath stub_gpo1_path_;
base::FilePath stub_gpo2_path_;
base::FilePath user_daemon_store_path_;
base::FilePath backup_path_;
// Markers to check whether various callbacks are actually called.
bool store_policy_called_ = false; // StubCallStorePolicyMethod()
bool user_policy_validated_ = false; // Policy validation
bool device_policy_validated_ = false; // callbacks below.
// IDs of extensions for which policy was validated.
std::multiset<std::string> validated_extension_ids_;
// IDs of extensions for which policy was deleted.
std::multiset<std::string> deleted_extension_ids_;
// IDs returned from the stub Session Manager for ListStoredComponentPolicies.
std::vector<std::string> stored_extension_ids_;
// Set by ValidatePolicy during user policy validation if the affiliation
// marker is set.
bool user_affiliation_marker_set_ = false;
// How often the UserKerberosFilesChanged signal was fired.
int user_kerberos_files_changed_count_ = 0;
// Must be set in unit tests to validate policy protos which authpolicy_ sends
// to Session Manager via D-Bus (resp. to StubCallStorePolicyMethod() in these
// tests).
std::function<void(const em::CloudPolicySettings&)> validate_user_policy_;
std::function<void(const em::ChromeDeviceSettingsProto&)>
validate_device_policy_;
std::function<void(const std::string&, const std::string&)>
validate_extension_policy_;
private:
void HandleUserKerberosFilesChanged(dbus::Signal* signal) {
EXPECT_EQ(signal->GetInterface(), "org.chromium.AuthPolicy");
EXPECT_EQ(signal->GetMember(), "UserKerberosFilesChanged");
user_kerberos_files_changed_count_++;
}
// Expected calls of metrics reporting functions, set and checked internally.
std::map<ErrorMetricType, int> expected_error_reports;
};
// Can't fetch user policy if the user is not logged in.
TEST_F(AuthPolicyTest, UserPolicyFailsNotLoggedIn) {
FetchAndValidateUserPolicy("account_id", ERROR_NOT_LOGGED_IN);
}
// Can't fetch device policy if the device is not joined.
TEST_F(AuthPolicyTest, DevicePolicyFailsNotJoined) {
FetchAndValidateDevicePolicy(ERROR_NOT_JOINED);
}
// Authentication fails if the machine is not joined.
TEST_F(AuthPolicyTest, AuthFailsNotJoined) {
EXPECT_EQ(ERROR_NOT_JOINED, Auth(kUserPrincipal, "", MakePasswordFd()));
}
// Successful domain join. The machine should join the user's domain since
// Join() doesn't specify machine domain.
TEST_F(AuthPolicyTest, JoinSucceeds) {
JoinDomainRequest request;
request.set_machine_name(kMachineName);
request.set_user_principal_name(kUserPrincipal);
std::string joined_realm;
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd(), &joined_realm));
SmbConf conf;
ReadSmbConf(paths_->Get(Path::DEVICE_SMB_CONF), &conf);
EXPECT_EQ(base::ToUpperASCII(kMachineName), conf.machine_name);
EXPECT_EQ(kUserRealm, conf.realm);
EXPECT_EQ(kUserRealm, joined_realm);
EXPECT_EQ(kEncTypesStrong, conf.kerberos_encryption_types);
}
// Successful domain join with separate machine domain specified.
TEST_F(AuthPolicyTest, JoinSucceedsWithDifferentDomain) {
JoinDomainRequest request;
request.set_machine_name(kMachineName);
request.set_machine_domain(kMachineRealm);
request.set_user_principal_name(kUserPrincipal);
std::string joined_realm;
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd(), &joined_realm));
SmbConf conf;
ReadSmbConf(paths_->Get(Path::DEVICE_SMB_CONF), &conf);
EXPECT_EQ(base::ToUpperASCII(kMachineName), conf.machine_name);
EXPECT_EQ(kMachineRealm, conf.realm);
EXPECT_EQ(kMachineRealm, joined_realm);
}
// Successful domain join with organizational unit (OU).
TEST_F(AuthPolicyTest, JoinSucceedsWithOrganizationalUnit) {
JoinDomainRequest request;
request.set_machine_name(kMachineName);
request.set_user_principal_name(kExpectOuUserPrincipal);
for (size_t n = 0; n < kExpectedOuPartsSize; ++n)
*request.add_machine_ou() = kExpectedOuParts[n];
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
// Note: We can't test directly whether the computer was put into the right OU
// because there's no state for that in authpolicy. The only indicator is the
// 'createcomputer' parameter to net ads join, but that can only be tested in
// stub_net, see kExpectOuUserPrincipal.
}
// Encryption types are written properly to smb.conf.
TEST_F(AuthPolicyTest, JoinSetsProperEncTypes) {
std::pair<KerberosEncryptionTypes, const char*> enc_types_list[] = {
{ENC_TYPES_ALL, kEncTypesAll},
{ENC_TYPES_STRONG, kEncTypesStrong},
{ENC_TYPES_LEGACY, kEncTypesLegacy}};
for (const auto& enc_types : enc_types_list) {
JoinDomainRequest request;
request.set_machine_name(kMachineName);
request.set_user_principal_name(kUserPrincipal);
request.set_kerberos_encryption_types(enc_types.first);
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
CheckSmbEncTypes(paths_->Get(Path::DEVICE_SMB_CONF), enc_types.second);
EXPECT_TRUE(MakeConfigWriteable());
samba().ResetForTesting();
}
}
// The encryption types reset to strong after device policy fetch.
TEST_F(AuthPolicyTest, EncTypesResetAfterDevicePolicyFetch) {
// Disable machine password change, because the password check runs
// immediately and wipes smb.conf (to get server time) with ENC_TYPES_STRONG,
// so the check below for kEncTypesAll fails.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendInteger(policy::key::kDeviceMachinePasswordChangeRate, 0);
writer.WriteToFile(stub_gpo1_path_);
JoinDomainRequest request;
request.set_machine_name(kOneGpoMachineName);
request.set_user_principal_name(kUserPrincipal);
request.set_kerberos_encryption_types(ENC_TYPES_ALL);
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
MarkDeviceAsLocked();
// After the first device policy fetch, the enc types should be 'strong'
// internally, but the conf files used should still contain 'all' types.
validate_device_policy_ = &DontValidateDevicePolicy;
FetchAndValidateDevicePolicy(ERROR_NONE);
CheckSmbEncTypes(paths_->Get(Path::DEVICE_SMB_CONF), kEncTypesAll);
CheckKrb5EncTypes(paths_->Get(Path::DEVICE_KRB5_CONF), kKrb5EncTypesAll);
// After the second device policy fetch, the conf files should contain
// 'strong' enc types.
FetchAndValidateDevicePolicy(ERROR_NONE);
CheckSmbEncTypes(paths_->Get(Path::DEVICE_SMB_CONF), kEncTypesStrong);
CheckKrb5EncTypes(paths_->Get(Path::DEVICE_KRB5_CONF), kKrb5EncTypesStrong);
// Likewise, auth should only use 'strong' types.
validate_user_policy_ = &CheckUserPolicyEmpty;
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
CheckSmbEncTypes(paths_->Get(Path::USER_SMB_CONF), kEncTypesStrong);
CheckKrb5EncTypes(paths_->Get(Path::USER_KRB5_CONF), kKrb5EncTypesStrong);
}
// If The encryption types reset to strong after device policy fetch.
TEST_F(AuthPolicyTest, LoadsDevicePolicyOnStartup) {
// Join to bootstrap a config file.
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
// Set a device policy file with Kerberos encryption types set to 'all' and
// restart authpolicy, so that it loads this file on startup.
em::ChromeDeviceSettingsProto device_policy;
device_policy.mutable_device_kerberos_encryption_types()->set_types(
em::DeviceKerberosEncryptionTypesProto::ENC_TYPES_ALL);
WritePolicyAndRestartAuthpolicy(device_policy);
// Now an auth operation should use the loaded encryption types.
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
CheckKrb5EncTypes(paths_->Get(Path::USER_KRB5_CONF), kKrb5EncTypesAll);
}
// Both Samba commands (smb) and kinit (krb5) use the encryption types from the
// previous device policy fetch.
TEST_F(AuthPolicyTest, UsesEncTypesFromDevicePolicy) {
// Write a GPO with DeviceKerberosEncryptionTypes set to 'all'.
auto enc_types_all = em::DeviceKerberosEncryptionTypesProto::ENC_TYPES_ALL;
policy::PRegUserDevicePolicyWriter writer;
writer.AppendInteger(policy::key::kDeviceKerberosEncryptionTypes,
enc_types_all);
writer.WriteToFile(stub_gpo1_path_);
validate_device_policy_ = &DontValidateDevicePolicy;
// Join and fetch device policy. This should set encryption types to 'all' in
// Samba.
EXPECT_EQ(ERROR_NONE,
Join(kOneGpoMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
// Now subsequent calls should use encryption types 'all', both for stuff
// using smb.conf (policy fetch) as well as stuff using Kerberos tickets (user
// auth, device policy fetch).
validate_user_policy_ = &CheckUserPolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
// User and device smb.conf has enc types 'all'.
CheckSmbEncTypes(paths_->Get(Path::USER_SMB_CONF), kEncTypesAll);
CheckSmbEncTypes(paths_->Get(Path::DEVICE_SMB_CONF), kEncTypesAll);
// User and device krb5.conf has aes_* + rc4_hmac enc types.
CheckKrb5EncTypes(paths_->Get(Path::USER_KRB5_CONF), kKrb5EncTypesAll);
CheckKrb5EncTypes(paths_->Get(Path::DEVICE_KRB5_CONF), kKrb5EncTypesAll);
}
// By default, the user's and device's krb5.conf files only have strong crypto.
TEST_F(AuthPolicyTest, TgtsUseStrongEncTypesByDefault) {
JoinAndFetchDevicePolicy(kMachineName);
validate_user_policy_ = &CheckUserPolicyEmpty;
CheckKrb5EncTypes(paths_->Get(Path::DEVICE_KRB5_CONF), kKrb5EncTypesStrong);
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
CheckKrb5EncTypes(paths_->Get(Path::USER_KRB5_CONF), kKrb5EncTypesStrong);
}
// The password check runs when device policy is fetched.
TEST_F(AuthPolicyTest, ChecksMachinePasswordOnDevicePolicyFetch) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_FALSE(samba().DidPasswordChangeCheckRunForTesting());
MarkDeviceAsLocked();
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_TRUE(samba().DidPasswordChangeCheckRunForTesting());
}
// The password check can be toggled by fetched device policy.
TEST_F(AuthPolicyTest, FetchedPolicyTogglesMachinePassword) {
// Join domain.
EXPECT_EQ(ERROR_NONE,
Join(kOneGpoMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &DontValidateDevicePolicy;
// Turn off password change in policy.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendInteger(policy::key::kDeviceMachinePasswordChangeRate, 0);
writer.WriteToFile(stub_gpo1_path_);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_FALSE(samba().DidPasswordChangeCheckRunForTesting());
// Turn password change back on in policy.
policy::PRegUserDevicePolicyWriter writer2;
writer2.AppendInteger(policy::key::kDeviceMachinePasswordChangeRate, 1);
writer2.WriteToFile(stub_gpo1_path_);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_TRUE(samba().DidPasswordChangeCheckRunForTesting());
}
// The password check can be toggled by device policy loaded from disk.
TEST_F(AuthPolicyTest, PolicyOnDiskTogglesMachinePasswordChangeCheck) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
// Write a policy to disk that turns off password change and restart.
em::ChromeDeviceSettingsProto device_policy;
device_policy.mutable_device_machine_password_change_rate()->set_rate_days(0);
WritePolicyAndRestartAuthpolicy(device_policy);
EXPECT_FALSE(samba().DidPasswordChangeCheckRunForTesting());
// Write a policy to disk that turns password change back on and restart.
device_policy.mutable_device_machine_password_change_rate()->set_rate_days(1);
WritePolicyAndRestartAuthpolicy(device_policy);
EXPECT_TRUE(samba().DidPasswordChangeCheckRunForTesting());
}
// The password actually gets reset once it exceeds the max age.
TEST_F(AuthPolicyTest, MachinePasswordChangesWhenMaxAgeIsReached) {
EXPECT_EQ(ERROR_NONE,
Join(kChangePasswordMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
// Device policy fetch should trigger a password age check.
// kChangePasswordMachineName should trigger a server time that's bigger than
// initial_password_time + 30 days, so that the password should change.
const std::string initial_password = ReadFile(Path::MACHINE_PASS);
const base::Time initial_password_time = GetLastModified(Path::MACHINE_PASS);
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
const std::string current_password = ReadFile(Path::MACHINE_PASS);
const base::Time current_password_time = GetLastModified(Path::MACHINE_PASS);
EXPECT_NE(initial_password, current_password);
EXPECT_NE(initial_password_time, current_password_time);
EXPECT_GE(current_password_time - initial_password_time,
kDefaultMachinePasswordChangeRate);
// Authpolicy should also keep the prev password around.
const std::string previous_password = ReadFile(Path::PREV_MACHINE_PASS);
const base::Time previous_password_time =
GetLastModified(Path::PREV_MACHINE_PASS);
EXPECT_EQ(initial_password, previous_password);
EXPECT_EQ(initial_password_time, previous_password_time);
}
// If the current machine password has just been changed, it might not have
// propagated through Active Directory yet. In that case, kinit should fail and
// authpolicy should retry with the previous machine password.
TEST_F(AuthPolicyTest, DevicePolicyFetchUsesPrevMachinePassword) {
JoinAndFetchDevicePolicy(kMachineName);
// Create an expected password. stub_kinit will compare the expected password
// with the actual password and cause device policy fetch to fail.
const base::FilePath prev_password_path(paths_->Get(Path::PREV_MACHINE_PASS));
const base::FilePath expected_password_path =
base::FilePath(paths_->Get(Path::DEVICE_KRB5_CONF))
.DirName()
.Append(kExpectedMachinePassFilename);
const std::string expected_password = GenerateRandomMachinePassword();
brillo::WriteStringToFile(expected_password_path, expected_password);
EXPECT_FALSE(base::PathExists(prev_password_path));
FetchAndValidateDevicePolicy(ERROR_BAD_PASSWORD);
// Write the expected password at PREV_MACHINE_PASS and verify fetch works.
brillo::WriteStringToFile(prev_password_path, expected_password);
FetchAndValidateDevicePolicy(ERROR_NONE);
// kinit should be called 1x from 1st fetch, 1x from 2nd fetch and 2x from 3rd
// fetch (for current and prev machine password).
EXPECT_EQ(4, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
}
// The password check runs on startup on an enrolled device.
TEST_F(AuthPolicyTest, ChecksMachinePasswordOnStartup) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_FALSE(samba().DidPasswordChangeCheckRunForTesting());
// Make password old enough for a password change.
SetLastModified(Path::MACHINE_PASS, base::Time());
// Restart with empty device policy. This should trigger it as well.
samba().ResetForTesting();
EXPECT_FALSE(samba().DidPasswordChangeCheckRunForTesting());
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
EXPECT_TRUE(samba().DidPasswordChangeCheckRunForTesting());
// Check that SMB conf contains KDC IP (regression test for crbug.com/815139).
Krb5Conf conf;
ReadKrb5Conf(paths_->Get(Path::DEVICE_KRB5_CONF), &conf);
EXPECT_FALSE(conf.kdc.empty());
}
// Successful user authentication.
TEST_F(AuthPolicyTest, AuthSucceeds) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
}
// Successful user authentication with given account id.
TEST_F(AuthPolicyTest, AuthSucceedsWithKnownAccountId) {
ActiveDirectoryAccountInfo account_info;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
Auth(kUserPrincipal, kAccountId, MakePasswordFd(), &account_info));
EXPECT_EQ(kAccountId, account_info.account_id());
}
// Program should die if trying to auth with different account ids.
TEST_F(AuthPolicyTest, AuthFailsDifferentAccountIds) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, kAccountId, MakePasswordFd()));
EXPECT_DEATH((void)Auth(kUserPrincipal, kAltAccountId, MakePasswordFd()),
kMultiUserNotSupported);
}
// User authentication fails with bad (non-existent) account id.
TEST_F(AuthPolicyTest, AuthFailsWithBadAccountId) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_BAD_USER_NAME,
Auth(kUserPrincipal, kBadAccountId, MakePasswordFd()));
}
// Successful user authentication.
TEST_F(AuthPolicyTest, AuthSetsAccountInfo) {
ActiveDirectoryAccountInfo account_info;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
Auth(kUserPrincipal, "", MakePasswordFd(), &account_info));
EXPECT_EQ(kAccountId, account_info.account_id());
EXPECT_EQ(kDisplayName, account_info.display_name());
EXPECT_EQ(kGivenName, account_info.given_name());
EXPECT_EQ(kUserName, account_info.sam_account_name());
EXPECT_EQ(kCommonName, account_info.common_name());
EXPECT_EQ(kPwdLastSet, account_info.pwd_last_set());
EXPECT_EQ(kUserAccountControl, account_info.user_account_control());
}
// Authentication fails for badly formatted user principal name.
TEST_F(AuthPolicyTest, AuthFailsInvalidUpn) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_PARSE_UPN_FAILED,
Auth(kInvalidUserPrincipal, "", MakePasswordFd()));
}
// Authentication fails for non-existing user principal name.
TEST_F(AuthPolicyTest, AuthFailsBadUpn) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_BAD_USER_NAME,
Auth(kNonExistingUserPrincipal, "", MakePasswordFd()));
}
// Authentication fails for wrong password.
TEST_F(AuthPolicyTest, AuthFailsBadPassword) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_BAD_PASSWORD,
Auth(kUserPrincipal, "", MakeFileDescriptor(kWrongPassword)));
// During PODs auth it is possible that the user gets into a session despite
// the ERROR_BAD_PASSWORD error. This must mean that the password changed on
// the server and the user entered the old password, which successfully
// unlocked Cryptohome.
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, GetUserStatus(kUserPrincipal, kAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::PASSWORD_CHANGED,
status.password_status());
}
// Authentication fails for expired password.
TEST_F(AuthPolicyTest, AuthFailsExpiredPassword) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_PASSWORD_EXPIRED,
Auth(kUserPrincipal, "", MakeFileDescriptor(kExpiredPassword)));
}
// Authentication fails for rejected password.
TEST_F(AuthPolicyTest, AuthFailsRejectedPassword) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_PASSWORD_REJECTED,
Auth(kUserPrincipal, "", MakeFileDescriptor(kRejectedPassword)));
}
// Authentication succeeds if the "password will expire" warning is shown.
TEST_F(AuthPolicyTest, AuthSucceedsPasswordWillExpire) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
Auth(kUserPrincipal, "", MakeFileDescriptor(kWillExpirePassword)));
}
// Authentication fails if there's a network issue.
TEST_F(AuthPolicyTest, AuthFailsNetworkProblem) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NETWORK_PROBLEM,
Auth(kNetworkErrorUserPrincipal, "", MakePasswordFd()));
}
// Authentication fails with unsupported encryption type.
TEST_F(AuthPolicyTest, AuthFailsEncTypeNotSupported) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_KDC_DOES_NOT_SUPPORT_ENCRYPTION_TYPE,
Auth(kEncTypeNotSupportedUserPrincipal, "", MakePasswordFd()));
}
// Authentication retries without KDC if it fails the first time.
TEST_F(AuthPolicyTest, AuthSucceedsKdcRetry) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kKdcRetryUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(2, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
}
// Can't get user status before domain join.
TEST_F(AuthPolicyTest, GetUserStatusFailsNotJoined) {
EXPECT_EQ(ERROR_NOT_JOINED, GetUserStatus(kUserPrincipal, kAccountId));
EXPECT_EQ(0, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
}
// Program should die if trying to get user status with different account ids
// than what was used for auth.
TEST_F(AuthPolicyTest, GetUserStatusFailsDifferentAccountId) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, kAccountId, MakePasswordFd()));
EXPECT_DEATH((void)GetUserStatus(kUserPrincipal, kAltAccountId),
kMultiUserNotSupported);
}
// GetUserStatus succeeds without auth, reporting TGT_NOT_FOUND and lacking
// account info and password status.
TEST_F(AuthPolicyTest, GetUserStatusSucceedsTgtNotFound) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, GetUserStatus(kUserPrincipal, kAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::TGT_NOT_FOUND, status.tgt_status());
EXPECT_FALSE(status.has_account_info());
EXPECT_FALSE(status.has_password_status());
}
// GetUserStatus succeeds with join and auth, but with an expired TGT and
// available server.
TEST_F(AuthPolicyTest, GetUserStatusSucceedsTgtExpiredServerAvailable) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kExpiredTgtUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, GetUserStatus(kUserPrincipal, kAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::TGT_EXPIRED, status.tgt_status());
}
// GetUserStatus succeeds with join and auth, but with an expired TGT and
// unavailable server.
TEST_F(AuthPolicyTest, GetUserStatusFailsTgtExpiredServerUnavailable) {
EXPECT_EQ(ERROR_NONE,
Join(kPingServerFailMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kExpiredTgtUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(ERROR_NETWORK_PROBLEM, GetUserStatus(kUserPrincipal, kAccountId));
}
// GetUserStatus succeeds with join and auth.
TEST_F(AuthPolicyTest, GetUserStatusSucceeds) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_TRUE(authpolicy_->IsUserTgtAutoRenewalEnabledForTesting());
EXPECT_EQ(ERROR_NONE, GetUserStatus(kUserPrincipal, kAccountId, &status));
ActiveDirectoryUserStatus expected_status;
ActiveDirectoryAccountInfo& expected_account_info =
*expected_status.mutable_account_info();
expected_account_info.set_account_id(kAccountId);
expected_account_info.set_display_name(kDisplayName);
expected_account_info.set_given_name(kGivenName);
expected_account_info.set_sam_account_name(kUserName);
expected_account_info.set_common_name(kCommonName);
expected_account_info.set_pwd_last_set(kPwdLastSet);
expected_account_info.set_user_account_control(kUserAccountControl);
expected_status.set_tgt_status(ActiveDirectoryUserStatus::TGT_VALID);
expected_status.set_password_status(
ActiveDirectoryUserStatus::PASSWORD_VALID);
// Note that protobuf equality comparison is not supported.
std::string status_blob, expected_status_blob;
EXPECT_TRUE(status.SerializeToString(&status_blob));
EXPECT_TRUE(expected_status.SerializeToString(&expected_status_blob));
EXPECT_EQ(expected_status_blob, status_blob);
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
}
// GetUserStatus actually contains the last auth error.
TEST_F(AuthPolicyTest, GetUserStatusReportsLastAuthError) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_PASSWORD_EXPIRED,
Auth(kUserPrincipal, "", MakeFileDescriptor(kExpiredPassword)));
EXPECT_EQ(ERROR_NONE, GetUserStatus(kUserPrincipal, kAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::PASSWORD_EXPIRED,
status.password_status());
}
// GetUserStatus reports to expire the password.
TEST_F(AuthPolicyTest, GetUserStatusReportsExpiredPasswords) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
Auth(kUserPrincipal, kExpiredPasswordAccountId, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
GetUserStatus(kUserPrincipal, kExpiredPasswordAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::PASSWORD_EXPIRED,
status.password_status());
}
// GetUserStatus does not report expired passwords if UF_DONT_EXPIRE_PASSWD is
// set.
TEST_F(AuthPolicyTest, GetUserStatusDontReportNeverExpirePasswords) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, kNeverExpirePasswordAccountId,
MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, GetUserStatus(kUserPrincipal,
kNeverExpirePasswordAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::PASSWORD_VALID,
status.password_status());
}
// GetUserStatus reports password changes.
TEST_F(AuthPolicyTest, GetUserStatusReportChangedPasswords) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
Auth(kPasswordChangedUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
GetUserStatus(kUserPrincipal, kPasswordChangedAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::PASSWORD_CHANGED,
status.password_status());
}
// GetUserStatus reports valid password if the LDAP attributes pwdLastSet or
// userAccountControl are missing for some reason.
TEST_F(AuthPolicyTest, GetUserStatusReportValidPasswordsWithoutPwdFields) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kNoPwdFieldsUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(ERROR_NONE,
GetUserStatus(kUserPrincipal, kNoPwdFieldsAccountId, &status));
EXPECT_EQ(ActiveDirectoryUserStatus::PASSWORD_VALID,
status.password_status());
EXPECT_FALSE(status.account_info().has_pwd_last_set());
EXPECT_FALSE(status.account_info().has_user_account_control());
EXPECT_FALSE(authpolicy_->IsUserTgtAutoRenewalEnabledForTesting());
}
// GetUserKerberosFiles succeeds with empty files if not joined.
TEST_F(AuthPolicyTest, GetUserKerberosFilesEmptyNotJoined) {
KerberosFiles files;
EXPECT_EQ(ERROR_NONE, GetUserKerberosFiles(kAccountId, &files));
EXPECT_EQ(0, user_kerberos_files_changed_count_);
EXPECT_FALSE(files.has_krb5cc());
EXPECT_FALSE(files.has_krb5conf());
}
// GetUserKerberosFiles succeeds with empty files if not logged in.
TEST_F(AuthPolicyTest, GetUserKerberosFilesEmptyNotLoggedIn) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
KerberosFiles files;
EXPECT_EQ(ERROR_NONE, GetUserKerberosFiles(kAccountId, &files));
EXPECT_EQ(0, user_kerberos_files_changed_count_);
EXPECT_FALSE(files.has_krb5cc());
EXPECT_FALSE(files.has_krb5conf());
}
// Authenticating with different id after GetUserKerberosFiles dies.
TEST_F(AuthPolicyTest, GetUserKerberosFilesBeforeAuthWithAltIdDies) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, GetUserKerberosFiles(kAccountId));
EXPECT_DEATH((void)Auth(kUserPrincipal, kAltAccountId, MakePasswordFd()),
kMultiUserNotSupported);
}
// GetUserKerberosFiles succeeds with actual files if logged in.
TEST_F(AuthPolicyTest, GetUserKerberosFilesSucceeds) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
KerberosFiles files;
EXPECT_EQ(ERROR_NONE, GetUserKerberosFiles(kAccountId, &files));
EXPECT_EQ(1, user_kerberos_files_changed_count_);
EXPECT_TRUE(files.has_krb5cc());
EXPECT_TRUE(files.has_krb5conf());
EXPECT_EQ(kValidKrb5CCData, files.krb5cc());
EXPECT_NE(std::string::npos, files.krb5conf().find("allow_weak_crypto"));
}
// Changes of krb5.conf should trigger the UserKerberosFilesChanged signal. This
// is tested by retrying kinit without kdc ip causes a config change and should
// result in a UserKerberosFilesChanged signal.
TEST_F(AuthPolicyTest, ConfigChangeTriggersFilesChangedSignal) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
// Do a normal auth first to bootstrap Kerberos files, but generate an expired
// TGT, so that the last step won't change the TGT.
EXPECT_EQ(ERROR_NONE, Auth(kExpiredTgtUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(1, user_kerberos_files_changed_count_);
// 1x user TGT.
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
// Try to auth again, but trigger a KDC retry to change JUST the config.
EXPECT_EQ(ERROR_CONTACTING_KDC_FAILED,
Auth(kKdcRetryFailsUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(2, user_kerberos_files_changed_count_);
// 2x user TGT because of KDC retry.
EXPECT_EQ(2, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
// Once the config is changed, it shouldn't change again.
EXPECT_EQ(ERROR_KERBEROS_TICKET_EXPIRED, samba().RenewUserTgtForTesting());
EXPECT_EQ(2, user_kerberos_files_changed_count_);
// 1x user TGT.
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
}
// TGT renewal should trigger a KerberosFilesChanged signal.
TEST_F(AuthPolicyTest, RenewTriggersFilesChangedSignal) {
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_EQ(1, user_kerberos_files_changed_count_);
EXPECT_EQ(ERROR_NONE, samba().RenewUserTgtForTesting());
EXPECT_EQ(2, user_kerberos_files_changed_count_);
}
// Join fails if there's a network issue.
TEST_F(AuthPolicyTest, JoinFailsNetworkProblem) {
EXPECT_EQ(ERROR_NETWORK_PROBLEM,
Join(kMachineName, kNetworkErrorUserPrincipal, MakePasswordFd()));
}
// Join fails for badly formatted user principal name.
TEST_F(AuthPolicyTest, JoinFailsInvalidUpn) {
EXPECT_EQ(ERROR_PARSE_UPN_FAILED,
Join(kMachineName, kInvalidUserPrincipal, MakePasswordFd()));
}
// Join fails for non-existing user principal name, but the error message is the
// same as for wrong password.
TEST_F(AuthPolicyTest, JoinFailsBadUpn) {
EXPECT_EQ(ERROR_BAD_PASSWORD,
Join(kMachineName, kNonExistingUserPrincipal, MakePasswordFd()));
}
// Join fails for wrong password.
TEST_F(AuthPolicyTest, JoinFailsBadPassword) {
EXPECT_EQ(ERROR_BAD_PASSWORD, Join(kMachineName, kUserPrincipal,
MakeFileDescriptor(kWrongPassword)));
}
// Join fails with expired password.
TEST_F(AuthPolicyTest, JoinFailsPasswordExpired) {
EXPECT_EQ(ERROR_PASSWORD_EXPIRED, Join(kMachineName, kUserPrincipal,
MakeFileDescriptor(kExpiredPassword)));
}
// Join fails if user can't join a machine to the domain.
TEST_F(AuthPolicyTest, JoinFailsAccessDenied) {
EXPECT_EQ(ERROR_JOIN_ACCESS_DENIED,
Join(kMachineName, kAccessDeniedUserPrincipal, MakePasswordFd()));
}
// Join fails if the machine name is too long.
TEST_F(AuthPolicyTest, JoinFailsMachineNameTooLong) {
EXPECT_EQ(ERROR_MACHINE_NAME_TOO_LONG,
Join(kTooLongMachineName, kUserPrincipal, MakePasswordFd()));
}
// Join fails if the machine name contains invalid characters.
TEST_F(AuthPolicyTest, JoinFailsInvalidMachineName) {
EXPECT_EQ(ERROR_INVALID_MACHINE_NAME,
Join(kInvalidMachineName, kUserPrincipal, MakePasswordFd()));
}
// Join fails if the user can't join additional machines.
TEST_F(AuthPolicyTest, JoinFailsInsufficientQuota) {
EXPECT_EQ(
ERROR_USER_HIT_JOIN_QUOTA,
Join(kMachineName, kInsufficientQuotaUserPrincipal, MakePasswordFd()));
}
// Join fails with unsupported encryption type.
TEST_F(AuthPolicyTest, JoinFailsEncTypeNotSupported) {
EXPECT_EQ(
ERROR_KDC_DOES_NOT_SUPPORT_ENCRYPTION_TYPE,
Join(kMachineName, kEncTypeNotSupportedUserPrincipal, MakePasswordFd()));
}
// A second domain join is blocked.
TEST_F(AuthPolicyTest, JoinFailsAlreadyJoined) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_ALREADY_JOINED,
Join(kMachineName, kUserPrincipal, MakePasswordFd()));
}
// Successful user policy fetch with empty policy.
TEST_F(AuthPolicyTest, UserPolicyFetchSucceeds) {
validate_user_policy_ = &CheckUserPolicyEmpty;
JoinAndFetchDevicePolicy(kMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_EQ(2, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
EXPECT_EQ(2, metrics_->GetNumMetricReports(METRIC_DOWNLOAD_GPO_COUNT));
}
// For affiliated users, the affiliation marker should be set during user policy
// fetch.
TEST_F(AuthPolicyTest, AffiliationMarkerSetForAffiliatedUsers) {
validate_user_policy_ = &CheckUserPolicyEmpty;
JoinAndFetchDevicePolicy(kMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_TRUE(user_affiliation_marker_set_);
}
// For unaffiliated users, the affiliation marker should not be set during user
// policy fetch.
TEST_F(AuthPolicyTest, AffiliationMarkerNotSetForUnaffiliatedUsers) {
// Users in the same realm as the device are always affiliated, so pick a
// different realm.
JoinDomainRequest request;
request.set_machine_name(kUnaffiliatedMachineName);
request.set_machine_domain(kMachineRealm);
request.set_user_principal_name(kUserPrincipal);
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
validate_user_policy_ = &CheckUserPolicyEmpty;
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_FALSE(user_affiliation_marker_set_);
}
// Successful user policy fetch with actual data.
TEST_F(AuthPolicyTest, UserPolicyFetchSucceedsWithData) {
// Write a preg file with all basic data types. The file is picked up by
// stub_net and "downloaded" by stub_smbclient.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kSearchSuggestEnabled, kPolicyBool);
writer.AppendInteger(policy::key::kPolicyRefreshRate, kPolicyInt);
writer.AppendString(policy::key::kHomepageLocation, kPolicyStr);
const std::vector<std::string> apps = {"App1", "App2"};
writer.AppendStringList(policy::key::kPinnedLauncherApps, apps);
writer.WriteToFile(stub_gpo1_path_);
// Validate that the protobufs sent from authpolicy to Session Manager
// actually contain the policies set above. This validator is called by
// FetchAndValidateUserPolicy below.
validate_user_policy_ = [apps](const em::CloudPolicySettings& policy) {
EXPECT_EQ(kPolicyBool, policy.searchsuggestenabled().value());
EXPECT_EQ(kPolicyInt, policy.policyrefreshrate().value());
EXPECT_EQ(kPolicyStr, policy.homepagelocation().value());
const em::StringList& apps_proto = policy.pinnedlauncherapps().value();
EXPECT_EQ(apps_proto.entries_size(), static_cast<int>(apps.size()));
for (int n = 0; n < apps_proto.entries_size(); ++n)
EXPECT_EQ(apps_proto.entries(n), apps.at(n));
};
JoinAndFetchDevicePolicy(kOneGpoMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_EQ(2,
metrics_->GetNumMetricReports(METRIC_SMBCLIENT_FAILED_TRY_COUNT));
}
// Successful user policy fetch that also contains extension policy.
TEST_F(AuthPolicyTest, UserPolicyFetchSucceedsWithDataAndExtensions) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegPolicyWriter writer;
writer.SetKeysForUserDevicePolicy();
writer.AppendBoolean(policy::key::kSearchSuggestEnabled, kPolicyBool);
WriteDefaultExtensionPolicy(&writer);
writer.WriteToFile(stub_gpo1_path_);
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_EQ(kPolicyBool, policy.searchsuggestenabled().value());
};
validate_extension_policy_ = &CheckDefaultExtensionPolicy;
JoinAndFetchDevicePolicy(kOneGpoMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_EQ(2, validated_extension_ids_.size());
}
// Successful user policy fetch that also contains extension policy.
TEST_F(AuthPolicyTest, StaleExtensionPoliciesAreDeleted) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegPolicyWriter writer;
writer.SetKeysForExtensionPolicy(kExtensionId);
writer.AppendString(kExtensionPolicy1, kPolicyStr);
writer.WriteToFile(stub_gpo1_path_);
// Pretend that Session Manager has stored policy for these two extensions.
stored_extension_ids_.push_back(kExtensionId);
stored_extension_ids_.push_back(kOtherExtensionId);
// Fetch and validate. This should trigger policy kOtherExtensionId to be
// deleted because the GPO only contains kExtensionId.
validate_user_policy_ = &CheckUserPolicyEmpty;
validate_extension_policy_ = &CheckDefaultExtensionPolicy;
JoinAndFetchDevicePolicy(kOneGpoMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_EQ(validated_extension_ids_,
std::multiset<std::string>({kExtensionId}));
EXPECT_EQ(deleted_extension_ids_,
std::multiset<std::string>({kOtherExtensionId}));
}
// Verify that PolicyLevel is encoded properly.
TEST_F(AuthPolicyTest, UserPolicyFetchSucceedsWithPolicyLevel) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kSearchSuggestEnabled, kPolicyBool,
policy::POLICY_LEVEL_RECOMMENDED);
writer.AppendInteger(policy::key::kPolicyRefreshRate, kPolicyInt);
writer.WriteToFile(stub_gpo1_path_);
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_TRUE(policy.searchsuggestenabled().has_policy_options());
EXPECT_EQ(em::PolicyOptions_PolicyMode_RECOMMENDED,
policy.searchsuggestenabled().policy_options().mode());
EXPECT_TRUE(policy.policyrefreshrate().has_policy_options());
EXPECT_EQ(em::PolicyOptions_PolicyMode_MANDATORY,
policy.policyrefreshrate().policy_options().mode());
};
JoinAndFetchDevicePolicy(kOneGpoMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
}
// Verifies that a POLICY_LEVEL_MANDATORY policy is not overwritten by a
// POLICY_LEVEL_RECOMMENDED policy.
TEST_F(AuthPolicyTest, UserPolicyFetchMandatoryTakesPreference) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegUserDevicePolicyWriter writer1;
writer1.AppendBoolean(policy::key::kSearchSuggestEnabled, kPolicyBool,
policy::POLICY_LEVEL_MANDATORY);
writer1.WriteToFile(stub_gpo1_path_);
// Normally, the latter GPO file overrides the former
// (DevicePolicyFetchGposOverride), but POLICY_LEVEL_RECOMMENDED does not
// beat POLICY_LEVEL_MANDATORY.
policy::PRegUserDevicePolicyWriter writer2;
writer2.AppendBoolean(policy::key::kSearchSuggestEnabled, kOtherPolicyBool,
policy::POLICY_LEVEL_RECOMMENDED);
writer2.WriteToFile(stub_gpo2_path_);
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_TRUE(policy.searchsuggestenabled().has_value());
EXPECT_EQ(kPolicyBool, policy.searchsuggestenabled().value());
EXPECT_TRUE(policy.searchsuggestenabled().has_policy_options());
EXPECT_EQ(em::PolicyOptions_PolicyMode_MANDATORY,
policy.searchsuggestenabled().policy_options().mode());
};
JoinAndFetchDevicePolicy(kTwoGposMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
}
// Verify that GPO containing policies with the wrong data type are not set.
// An exception is bool and int. Internally, bools are handled like ints with
// value {0,1}, so that setting kPolicyRefreshRate to true is actually
// interpreted as int 1.
TEST_F(AuthPolicyTest, UserPolicyFetchIgnoreBadDataType) {
// Set policies with wrong data type, e.g. kPinnedLauncherApps is a string
// list, but it is set as a string. See UserPolicyFetchSucceedsWithData for
// the logic of policy testing.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kPolicyRefreshRate, kPolicyBool);
writer.AppendInteger(policy::key::kHomepageLocation, kPolicyInt);
writer.AppendString(policy::key::kPinnedLauncherApps, kPolicyStr);
const std::vector<std::string> apps = {"App1", "App2"};
writer.AppendStringList(policy::key::kSearchSuggestEnabled, apps);
writer.WriteToFile(stub_gpo1_path_);
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_FALSE(policy.has_searchsuggestenabled());
EXPECT_FALSE(policy.has_pinnedlauncherapps());
EXPECT_FALSE(policy.has_homepagelocation());
EXPECT_TRUE(policy.has_policyrefreshrate()); // Interpreted as int 1.
};
JoinAndFetchDevicePolicy(kOneGpoMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
}
// GPOs with version 0 should be ignored.
TEST_F(AuthPolicyTest, UserPolicyFetchIgnoreZeroVersion) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kSearchSuggestEnabled, kPolicyBool);
writer.WriteToFile(stub_gpo1_path_);
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_FALSE(policy.has_searchsuggestenabled());
};
JoinAndFetchDevicePolicy(kZeroUserVersionMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
// Validate the validation. GPO is actually taken if user version is > 0.
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_TRUE(policy.has_searchsuggestenabled());
};
EXPECT_TRUE(MakeConfigWriteable());
samba().ResetForTesting();
JoinAndFetchDevicePolicy(kOneGpoMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
}
// GPOs with an ignore flag set should be ignored. Sounds reasonable, hmm?
TEST_F(AuthPolicyTest, UserPolicyFetchIgnoreFlagSet) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kSearchSuggestEnabled, kPolicyBool);
writer.WriteToFile(stub_gpo1_path_);
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_FALSE(policy.has_searchsuggestenabled());
};
JoinAndFetchDevicePolicy(kDisableUserFlagMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
// Validate the validation. GPO is taken if the ignore flag is not set.
validate_user_policy_ = [](const em::CloudPolicySettings& policy) {
EXPECT_TRUE(policy.has_searchsuggestenabled());
};
EXPECT_TRUE(MakeConfigWriteable());
samba().ResetForTesting();
JoinAndFetchDevicePolicy(kOneGpoMachineName);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
}
// User policy fetch fails if there's no device policy (since the way user
// policy is fetched depends on device policy).
TEST_F(AuthPolicyTest, UserPolicyFetchFailsNoDevicePolicy) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NO_DEVICE_POLICY);
}
// User policy fetch works properly with loopback processing.
TEST_F(AuthPolicyTest, UserPolicyFetchObeysLoopbackProcessing) {
// Write 2 GPO files with 2 policies each:
//
// GPO Policy 1 2 3
// GPO1 x y
// GPO2 A B
//
// 'x' is the value of policy 1 in GPO1 etc. Now for the test,
// GPO1 is used as user GPO and
// GPO2 is used as device GPO.
//
// Depending on the loopback processing mode, this should result in
//
// Mode Policy 1 2 3
// Default x y <-- Only take user GPO1
// Merge A y B <-- Merge device GPO2 on top of user GPO1
// Replace A B <-- Only take device GPO2
const char* policy1 = policy::key::kSearchSuggestEnabled;
const char* policy2 = policy::key::kPolicyRefreshRate;
const char* policy3 = policy::key::kHomepageLocation;
const bool value_x = kPolicyBool;
const int value_y = kPolicyInt;
const bool value_A = kOtherPolicyBool;
const char* value_B = kOtherPolicyStr;
policy::PRegUserDevicePolicyWriter writer1;
writer1.AppendBoolean(policy1, value_x);
writer1.AppendInteger(policy2, value_y);
writer1.WriteToFile(stub_gpo1_path_);
policy::PRegUserDevicePolicyWriter writer2;
writer2.AppendBoolean(policy1, value_A);
writer2.AppendString(policy3, value_B);
writer2.WriteToFile(stub_gpo2_path_);
// |kLoopbackGpoMachineName| triggers stub_net to
// - return GPO1 for net ads gpo list <user_principal> and
// - return GPO2 for net ads gpo list <device_principal>.
JoinAndFetchDevicePolicy(kLoopbackGpoMachineName);
const int mode_min =
em::DeviceUserPolicyLoopbackProcessingModeProto::Mode_MIN;
const int mode_max =
em::DeviceUserPolicyLoopbackProcessingModeProto::Mode_MAX;
for (int int_mode = mode_min; int_mode <= mode_max; ++int_mode) {
const auto mode =
static_cast<em::DeviceUserPolicyLoopbackProcessingModeProto::Mode>(
int_mode);
samba().SetUserPolicyModeForTesting(mode);
validate_user_policy_ = [value_x, value_y, value_A, value_B,
mode](const em::CloudPolicySettings& policy) {
const bool has_policy1 = policy.has_searchsuggestenabled();
const bool has_policy2 = policy.has_policyrefreshrate();
const bool has_policy3 = policy.has_homepagelocation();
const bool policy1_value = policy.searchsuggestenabled().value();
const int policy2_value = policy.policyrefreshrate().value();
const std::string& policy3_value = policy.homepagelocation().value();
EXPECT_TRUE(has_policy1);
switch (mode) {
case em::DeviceUserPolicyLoopbackProcessingModeProto::
USER_POLICY_MODE_DEFAULT:
EXPECT_TRUE(has_policy2);
EXPECT_FALSE(has_policy3);
EXPECT_EQ(value_x, policy1_value);
EXPECT_EQ(value_y, policy2_value);
break;
case em::DeviceUserPolicyLoopbackProcessingModeProto::
USER_POLICY_MODE_MERGE:
EXPECT_TRUE(has_policy2);
EXPECT_TRUE(has_policy3);
EXPECT_EQ(value_A, policy1_value);
EXPECT_EQ(value_y, policy2_value);
EXPECT_EQ(value_B, policy3_value);
break;
case em::DeviceUserPolicyLoopbackProcessingModeProto::
USER_POLICY_MODE_REPLACE:
EXPECT_FALSE(has_policy2);
EXPECT_TRUE(has_policy3);
EXPECT_EQ(value_A, policy1_value);
EXPECT_EQ(value_B, policy3_value);
break;
}
};
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
}
// 3x for user TGT during auth, 1 for device policy fetch, 2x for device TGT
// for MERGE and REPLACE.
EXPECT_EQ(6, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
// 1x for device policy fetch, 1x for DEFAULT, 2x for MERGE, 1x for REPLACE.
EXPECT_EQ(5,
metrics_->GetNumMetricReports(METRIC_SMBCLIENT_FAILED_TRY_COUNT));
// 1x for device policy fetch, 1x for DEFAULT, 2x for MERGE, 1x for REPLACE.
EXPECT_EQ(5, metrics_->GetNumMetricReports(METRIC_DOWNLOAD_GPO_COUNT));
}
// Successful device policy fetch with empty policy.
TEST_F(AuthPolicyTest, DevicePolicyFetchSucceeds) {
validate_device_policy_ = &CheckDevicePolicyEmpty;
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_DOWNLOAD_GPO_COUNT));
}
// Device policy fetch fails if the machine account doesn't exist.
TEST_F(AuthPolicyTest, DevicePolicyFetchFailsBadMachineName) {
validate_device_policy_ = &CheckDevicePolicyEmpty;
EXPECT_EQ(ERROR_NONE,
Join(kNonExistingMachineName, kUserPrincipal, MakePasswordFd()));
FetchAndValidateDevicePolicy(ERROR_BAD_MACHINE_NAME);
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
EXPECT_EQ(0, metrics_->GetNumMetricReports(METRIC_DOWNLOAD_GPO_COUNT));
}
// Policy fetch should ignore GPO files that are missing on the server. This
// test looks for stub_gpo1_path_ and won't find it because we don't create it.
TEST_F(AuthPolicyTest, DevicePolicyFetchSucceedsMissingFile) {
validate_user_policy_ = &CheckUserPolicyEmpty;
JoinAndFetchDevicePolicy(kOneGpoMachineName);
}
// Successful device policy fetch with keytab file. This tests backwards
// compatibility with old clients that used a keytab file instead of a password.
TEST_F(AuthPolicyTest, DevicePolicyFetchSucceedsWithKeytab) {
validate_device_policy_ = &CheckDevicePolicyEmpty;
EXPECT_EQ(ERROR_NONE,
Join(kExpectKeytabMachineName, kUserPrincipal, MakePasswordFd()));
// Replace the machine password by a keytab file. Authpolicy should use that
// instead.
const base::FilePath password_path(paths_->Get(Path::MACHINE_PASS));
const base::FilePath keytab_path(paths_->Get(Path::MACHINE_KEYTAB));
EXPECT_TRUE(base::Move(password_path, keytab_path));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_KINIT_FAILED_TRY_COUNT));
EXPECT_EQ(1, metrics_->GetNumMetricReports(METRIC_DOWNLOAD_GPO_COUNT));
}
// Policy fetch fails if a file fails to download (unless it's missing, see
// DevicePolicyFetchSucceedsMissingFile).
TEST_F(AuthPolicyTest, DevicePolicyFetchFailsDownloadError) {
EXPECT_EQ(ERROR_NONE, Join(kGpoDownloadErrorMachineName, kUserPrincipal,
MakePasswordFd()));
FetchAndValidateDevicePolicy(ERROR_SMBCLIENT_FAILED);
}
// Successful device policy fetch with a few kinit retries because the machine
// account hasn't propagated yet.
TEST_F(AuthPolicyTest, DevicePolicyFetchSucceedsPropagationRetry) {
validate_device_policy_ = &CheckDevicePolicyEmpty;
EXPECT_EQ(ERROR_NONE, Join(kPropagationRetryMachineName, kUserPrincipal,
MakePasswordFd()));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(kNumPropagationRetries,
metrics_->GetLastMetricSample(METRIC_KINIT_FAILED_TRY_COUNT));
}
// Successful device policy fetch with actual data.
TEST_F(AuthPolicyTest, DevicePolicyFetchSucceedsWithData) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
SetupDeviceOneGpo(stub_gpo1_path_);
EXPECT_EQ(ERROR_NONE,
Join(kOneGpoMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
}
// Authpolicy caches device policy when device is not locked.
TEST_F(AuthPolicyTest, CachesDevicePolicyWhenDeviceIsNotLocked) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
SetupDeviceOneGpo(stub_gpo1_path_);
EXPECT_EQ(ERROR_NONE,
Join(kOneGpoMachineName, kUserPrincipal, MakePasswordFd()));
FetchAndValidateDevicePolicy(ERROR_DEVICE_POLICY_CACHED_BUT_NOT_SENT);
EXPECT_TRUE(base::DeleteFile(stub_gpo1_path_, /* recursive */ false));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
}
// Successful device policy fetch that also contains extension policy.
TEST_F(AuthPolicyTest, DevicePolicyFetchSucceedsWithDataAndExtensions) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegPolicyWriter writer;
writer.SetKeysForUserDevicePolicy();
writer.AppendBoolean(policy::key::kDeviceGuestModeEnabled, kPolicyBool);
WriteDefaultExtensionPolicy(&writer);
writer.WriteToFile(stub_gpo1_path_);
validate_device_policy_ = [](const em::ChromeDeviceSettingsProto& policy) {
EXPECT_EQ(kPolicyBool, policy.guest_mode_enabled().guest_mode_enabled());
};
validate_extension_policy_ = &CheckDefaultExtensionPolicy;
EXPECT_EQ(ERROR_NONE,
Join(kOneGpoMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(2, validated_extension_ids_.size());
}
// Completely empty GPO list fails. GPO lists should always contain at least
// a "local policy" created by the Samba tool, independently of the server.
TEST_F(AuthPolicyTest, DevicePolicyFetchFailsEmptyGpoList) {
EXPECT_EQ(ERROR_NONE,
Join(kEmptyGpoMachineName, kUserPrincipal, MakePasswordFd()));
FetchAndValidateDevicePolicy(ERROR_PARSE_FAILED);
}
// A GPO later in the list overrides prior GPOs.
TEST_F(AuthPolicyTest, DevicePolicyFetchGposOverride) {
// See UserPolicyFetchSucceedsWithData for the logic of policy testing.
policy::PRegUserDevicePolicyWriter writer1;
writer1.AppendBoolean(policy::key::kDeviceGuestModeEnabled, kOtherPolicyBool);
writer1.AppendInteger(policy::key::kDevicePolicyRefreshRate, kPolicyInt);
writer1.AppendString(policy::key::kSystemTimezone, kPolicyStr);
const std::vector<std::string> str_list1 = {"str1", "str2", "str3"};
writer1.AppendStringList(policy::key::kDeviceUserWhitelist, str_list1);
writer1.WriteToFile(stub_gpo1_path_);
policy::PRegUserDevicePolicyWriter writer2;
writer2.AppendBoolean(policy::key::kDeviceGuestModeEnabled, kPolicyBool);
writer2.AppendInteger(policy::key::kDevicePolicyRefreshRate, kOtherPolicyInt);
writer2.AppendString(policy::key::kSystemTimezone, kOtherPolicyStr);
const std::vector<std::string> str_list2 = {"str4", "str5"};
writer2.AppendStringList(policy::key::kDeviceUserWhitelist, str_list2);
writer2.WriteToFile(stub_gpo2_path_);
validate_device_policy_ = [str_list2](
const em::ChromeDeviceSettingsProto& policy) {
EXPECT_EQ(kPolicyBool, policy.guest_mode_enabled().guest_mode_enabled());
EXPECT_EQ(kOtherPolicyInt,
policy.device_policy_refresh_rate().device_policy_refresh_rate());
EXPECT_EQ(kOtherPolicyStr, policy.system_timezone().timezone());
const em::UserWhitelistProto& str_list_proto = policy.user_whitelist();
EXPECT_EQ(str_list_proto.user_whitelist_size(),
static_cast<int>(str_list2.size()));
for (int n = 0; n < str_list_proto.user_whitelist_size(); ++n)
EXPECT_EQ(str_list_proto.user_whitelist(n), str_list2.at(n));
};
EXPECT_EQ(ERROR_NONE,
Join(kTwoGposMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
FetchAndValidateDevicePolicy(ERROR_NONE);
}
// Make sure cleaning state works.
TEST_F(AuthPolicyTest, CleanStateDir) {
const base::FilePath state_path =
base::FilePath(paths_->Get(Path::STATE_DIR));
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_FALSE(base::IsDirectoryEmpty(state_path));
EXPECT_TRUE(AuthPolicy::CleanState(paths_.get()));
EXPECT_TRUE(base::IsDirectoryEmpty(state_path));
}
// Authentication doesn't back up auth state if Cryptohome is not mounted.
TEST_F(AuthPolicyTest, DoesNotBackUpOnAuthIfCryptohomeIsNotMounted) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_FALSE(base::PathExists(backup_path_));
}
// Authentication backs up auth state if Cryptohome is already mounted.
TEST_F(AuthPolicyTest, BacksUpOnAuthIfCryptohomeIsMounted) {
samba().OnSessionStateChanged(kSessionStarted);
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_FALSE(base::PathExists(backup_path_));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_TRUE(base::PathExists(backup_path_));
}
// The session state change signal handler triggers a backup of user auth state.
TEST_F(AuthPolicyTest, BacksUpOnSessionStarted) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_FALSE(base::PathExists(backup_path_));
NotifySessionStarted();
EXPECT_TRUE(base::PathExists(backup_path_));
}
// Kerberos ticket renewal triggers a backup of user auth state.
TEST_F(AuthPolicyTest, BacksUpOnTgtAutoRenewal) {
// Join and authenticate with Cryptohome mounted, so that a backup is written.
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
NotifySessionStarted();
// Trigger TGT renewal and check if the backup file got re-written.
base::Time orig_backup_time = GetLastModified(backup_path_);
EXPECT_EQ(ERROR_NONE, samba().RenewUserTgtForTesting());
base::Time new_backup_time = GetLastModified(backup_path_);
EXPECT_LT(orig_backup_time, new_backup_time);
}
// Restarting authpolicy reloads the backup data and user-specific calls work
// without another AuthenticateUser() call.
TEST_F(AuthPolicyTest, LoadsBackupAndRestoresState) {
// Join and authenticate with Cryptohome mounted, so that a backup is written.
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
NotifySessionStarted();
EXPECT_TRUE(base::PathExists(backup_path_));
// Restart authpolicyd.
samba().ResetForTesting();
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
// GetUserKerberosFiles should restore the backup including the Kerberos
// ticket, so the Kerberos files changed signal should be called.
KerberosFiles files;
EXPECT_EQ(1, user_kerberos_files_changed_count_);
EXPECT_EQ(ERROR_NONE, GetUserKerberosFiles(kAccountId, &files));
EXPECT_EQ(2, user_kerberos_files_changed_count_);
EXPECT_TRUE(files.has_krb5cc());
EXPECT_TRUE(files.has_krb5conf());
EXPECT_FALSE(files.krb5cc().empty());
EXPECT_FALSE(files.krb5conf().empty());
// The state should look like as if the user was logged in with valid TGT.
ActiveDirectoryUserStatus status;
EXPECT_EQ(ERROR_NONE, GetUserStatus(kUserPrincipal, kAccountId, &status));
EXPECT_TRUE(status.has_tgt_status());
EXPECT_EQ(ActiveDirectoryUserStatus::TGT_VALID, status.tgt_status());
EXPECT_TRUE(status.has_password_status());
EXPECT_EQ(ActiveDirectoryUserStatus::PASSWORD_VALID,
status.password_status());
EXPECT_TRUE(status.has_account_info());
EXPECT_TRUE(status.account_info().has_pwd_last_set());
EXPECT_TRUE(status.account_info().has_user_account_control());
// TGT renewal still works. Do this before auth to check all state for renewal
// got properly restored (auth overrides this).
EXPECT_EQ(ERROR_NONE, samba().RenewUserTgtForTesting());
// User policy fetch still works and that the affiliation state has been
// properly restored from backup.
validate_user_policy_ = &CheckUserPolicyEmpty;
EXPECT_FALSE(user_affiliation_marker_set_);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_TRUE(user_affiliation_marker_set_);
// Can also authenticate again to fetch a new TGT.
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, kAccountId, MakePasswordFd()));
}
// Policy fetch after a restart recovers successfully from backup (see
// https://crbug.com/908772).
TEST_F(AuthPolicyTest, LoadsBackupOnPolicyFetch) {
// Join and authenticate with Cryptohome mounted, so that a backup is written.
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
NotifySessionStarted();
// Restart authpolicyd.
samba().ResetForTesting();
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
// User policy fetch still works, even without auth.
validate_user_policy_ = &CheckUserPolicyEmpty;
FetchAndValidateUserPolicy(kAccountId, ERROR_NONE);
}
// By default, nothing should call the (expensive) anonymizer since no sensitive
// data is logged. Only if logging is enabled it should be called.
TEST_F(AuthPolicyTest, AnonymizerNotCalledWithoutLogging) {
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
validate_user_policy_ = &CheckUserPolicyEmpty;
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_NONE);
EXPECT_FALSE(samba().GetAnonymizerForTesting()->process_called_for_testing());
}
// If log output is requested, the logs should be anonymized.
TEST_F(AuthPolicyTest, AnonymizerCalledWithLogging) {
// Turn on max logging and trigger an error. This triggers debug logging
// which should be anonymized.
samba().SetDefaultLogLevel(AuthPolicyFlags::kMaxLevel);
ignore_result(Join(kTooLongMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_TRUE(samba().GetAnonymizerForTesting()->process_called_for_testing());
}
// Disable SeccompFiltersEnabled under ASAN. Minijail does not enable seccomp
// filtering when running under ASAN, so the test fails.
// https://crbug.com/908140
#ifndef BRILLO_ASAN_BUILD
// Re-enable seccomp filters and check that they are actually in effect.
TEST_F(AuthPolicyTest, SeccompFiltersEnabled) {
// Re-enable seccomp filtering and trigger it in net ads join.
samba().DisableSeccompForTesting(false);
EXPECT_EQ(ERROR_NET_FAILED,
Join(kSeccompMachineName, kUserPrincipal, MakePasswordFd()));
// Disable seccomp filtering again, make sure net ads join works this time.
samba().DisableSeccompForTesting(true);
EXPECT_EQ(ERROR_NONE,
Join(kSeccompMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
// Same with kinit. Check whether kinit can trigger seccomp failures.
samba().DisableSeccompForTesting(false);
EXPECT_EQ(ERROR_KINIT_FAILED,
Auth(kSeccompUserPrincipal, "", MakePasswordFd()));
samba().DisableSeccompForTesting(true);
EXPECT_EQ(ERROR_NONE, Auth(kSeccompUserPrincipal, "", MakePasswordFd()));
// Finally, check whether smbclient can trigger seccomp failures.
samba().DisableSeccompForTesting(false);
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_SMBCLIENT_FAILED);
samba().DisableSeccompForTesting(true);
FetchAndValidateDevicePolicy(ERROR_NONE);
}
#endif // #ifndef BRILLO_ASAN_BUILD
// GPO files are cached and not re-downloaded if the version didn't change.
TEST_F(AuthPolicyTest, UsesCachedGposForSameVersion) {
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kDeviceGuestModeEnabled, kPolicyBool);
writer.WriteToFile(stub_gpo1_path_);
validate_device_policy_ = [](const em::ChromeDeviceSettingsProto& policy) {
EXPECT_EQ(kPolicyBool, policy.guest_mode_enabled().guest_mode_enabled());
};
const GpoVersionCache* cache = samba().GetGpoVersionCacheForTesting();
// Join domain. |kOneGpoKeepVersionMachineName| freezes the GPO version at 1.
EXPECT_EQ(ERROR_NONE, Join(kOneGpoKeepVersionMachineName, kUserPrincipal,
MakePasswordFd()));
MarkDeviceAsLocked();
// First policy fetch should be a cache miss with expected version 1.
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(1, cache->cache_misses_for_testing());
EXPECT_EQ(0, cache->cache_hits_for_testing());
// Next time should be a cache hit since the version is still 1.
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(1, cache->cache_misses_for_testing());
EXPECT_EQ(1, cache->cache_hits_for_testing());
}
// GPO files are re-downloaded if the version changed.
TEST_F(AuthPolicyTest, DoesNotUseCachedGposForDifferentVersion) {
policy::PRegUserDevicePolicyWriter writer;
writer.AppendBoolean(policy::key::kDeviceGuestModeEnabled, kPolicyBool);
writer.WriteToFile(stub_gpo1_path_);
validate_device_policy_ = [](const em::ChromeDeviceSettingsProto& policy) {
EXPECT_EQ(kPolicyBool, policy.guest_mode_enabled().guest_mode_enabled());
};
GpoVersionCache* cache = samba().GetGpoVersionCacheForTesting();
// Join domain.
EXPECT_EQ(ERROR_NONE,
Join(kOneGpoMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
// First policy fetch should be a cache miss with expected version 1.
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(1, cache->cache_misses_for_testing());
EXPECT_EQ(0, cache->cache_hits_for_testing());
// Next time should be a cache miss again since |kOneGpoMachineName| caused
// the version to increase on every FetchAndValidateDevicePolicy() call.
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(2, cache->cache_misses_for_testing());
EXPECT_EQ(0, cache->cache_hits_for_testing());
}
// Caching should not complain about GPOs with missing files.
TEST_F(AuthPolicyTest, CachesGposWithMissingFile) {
const GpoVersionCache* cache = samba().GetGpoVersionCacheForTesting();
// Write file 1, but omit file 2.
policy::PRegUserDevicePolicyWriter writer;
writer.WriteToFile(stub_gpo1_path_);
EXPECT_FALSE(base::PathExists(stub_gpo2_path_));
// First time should be a cache miss for both.
// Note that with each FetchAndValidateDevicePolicy() call, the effect of
// |kTwoGposKeepVersionMachineName| is
// - an increase of the version of the first GPO, so that the download code is
// triggered, but
// - a freeze of the version of the second GPO, so that it's cached.
// First time should be a cache miss.
JoinAndFetchDevicePolicy(kTwoGposKeepVersionMachineName);
EXPECT_EQ(2, cache->cache_misses_for_testing());
EXPECT_EQ(0, cache->cache_hits_for_testing());
// Second time should be a cache miss for the first file (since its version
// increased) and a cache hit for the missing second file.
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(3, cache->cache_misses_for_testing());
EXPECT_EQ(1, cache->cache_hits_for_testing());
}
// The GPO cache is purged occasionally.
TEST_F(AuthPolicyTest, PurgesGpoCacheOccasionally) {
// Set a testing clock to stub out cache TTL.
GpoVersionCache* cache = samba().GetGpoVersionCacheForTesting();
auto clock_ptr = std::make_unique<base::SimpleTestClock>();
base::SimpleTestClock* clock = clock_ptr.get();
cache->SetClockForTesting(std::move(clock_ptr));
// Pick delta so that one Advance() call won't purge the cache, but two will.
const base::TimeDelta delta =
samba().GetGpoVersionCacheTTLForTesting() * 2 / 3;
JoinAndFetchDevicePolicy(kOneGpoKeepVersionMachineName);
EXPECT_EQ(1, cache->cache_misses_for_testing());
EXPECT_EQ(0, cache->cache_hits_for_testing());
// First Advance() should NOT cause a cache purge, so we get a cache hit.
clock->Advance(delta);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(1, cache->cache_misses_for_testing());
EXPECT_EQ(1, cache->cache_hits_for_testing());
// Second Advance() should cause a cache purge, so we get a cache miss.
clock->Advance(delta);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(2, cache->cache_misses_for_testing());
EXPECT_EQ(1, cache->cache_hits_for_testing());
}
// The lifetime of the GPO cache can be changed by the DeviceGpoCacheLifetime
// policy. It can also be turned off by that policy.
TEST_F(AuthPolicyTest, GpoCacheTTLCanBeSetByPolicy) {
// Join domain.
EXPECT_EQ(ERROR_NONE, Join(kOneGpoKeepVersionMachineName, kUserPrincipal,
MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &DontValidateDevicePolicy;
EXPECT_TRUE(samba().GetGpoVersionCacheForTesting()->IsEnabled());
// Setting the policy to 0 should turn the cache off and set the TTL to 0.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendInteger(policy::key::kDeviceGpoCacheLifetime, kZeroHours);
writer.WriteToFile(stub_gpo1_path_);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_FALSE(samba().GetGpoVersionCacheForTesting()->IsEnabled());
EXPECT_EQ(base::TimeDelta::FromHours(kZeroHours),
samba().GetGpoVersionCacheTTLForTesting());
// Setting the policy to > 0 should turn the cache on again and set the TTL.
// Note that this part would fail (policy wouldn't reload) if the cache was
// still on, see below.
policy::PRegUserDevicePolicyWriter writer2;
writer2.AppendInteger(policy::key::kDeviceGpoCacheLifetime, kNonZeroHours);
writer2.WriteToFile(stub_gpo1_path_);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_TRUE(samba().GetGpoVersionCacheForTesting()->IsEnabled());
EXPECT_EQ(base::TimeDelta::FromHours(kNonZeroHours),
samba().GetGpoVersionCacheTTLForTesting());
// Fetch again to fill the cache.
FetchAndValidateDevicePolicy(ERROR_NONE);
// Rewriting the policy should be ignored now since a cached version is used.
// (A rewrite in this test doesn't change the GPO version since we're using
// kOneGpoKeepVersionMachineName).
policy::PRegUserDevicePolicyWriter writer3;
writer3.AppendInteger(policy::key::kDeviceGpoCacheLifetime, kZeroHours);
writer3.WriteToFile(stub_gpo1_path_);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_TRUE(samba().GetGpoVersionCacheForTesting()->IsEnabled());
}
// Authpolicyd writes an auth data cache file on domain join and after auth.
TEST_F(AuthPolicyTest, AuthDataCacheWrittenOnJoinAndAuth) {
base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE));
// Cache file should be written after domain join.
EXPECT_FALSE(base::PathExists(cache_path));
EXPECT_EQ(ERROR_NONE, Join(kMachineName, kUserPrincipal, MakePasswordFd()));
EXPECT_TRUE(base::PathExists(cache_path));
// Cache file should be written by user auth as well.
EXPECT_TRUE(base::DeleteFile(cache_path, false /* recursive */));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
EXPECT_TRUE(base::PathExists(cache_path));
// The file should NOT be written on failed auth (because at the point of
// failure we don't know yet whether the realm is affiliated or not).
EXPECT_TRUE(base::DeleteFile(cache_path, false /* recursive */));
EXPECT_EQ(ERROR_PARSE_UPN_FAILED,
Auth(kInvalidUserPrincipal, "", MakePasswordFd()));
EXPECT_FALSE(base::PathExists(cache_path));
}
// Checks whether the auth data cache contains all the data that we'd expect.
TEST_F(AuthPolicyTest, AuthDataCacheContainsExpectedData) {
// Join domain with machine domain != user domain.
JoinDomainRequest request;
request.set_machine_name(kMachineName);
request.set_machine_domain(kMachineRealm);
request.set_user_principal_name(kUserPrincipal);
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
// Load the cache and check values.
AuthDataCache* cache = samba().GetAuthDataCacheForTesting();
ASSERT_TRUE(cache->GetWorkgroup(kMachineRealm));
ASSERT_TRUE(cache->GetKdcIp(kMachineRealm));
ASSERT_TRUE(cache->GetDcName(kMachineRealm));
ASSERT_TRUE(cache->GetIsAffiliated(kMachineRealm));
EXPECT_EQ(kDefaultWorkgroup, *cache->GetWorkgroup(kMachineRealm));
EXPECT_EQ(kDefaultKdcIp, *cache->GetKdcIp(kMachineRealm));
EXPECT_EQ(kDefaultDcName, *cache->GetDcName(kMachineRealm));
// The machine realm is always affiliated with itself!
EXPECT_TRUE(*cache->GetIsAffiliated(kMachineRealm));
EXPECT_FALSE(cache->GetWorkgroup(kUserRealm));
EXPECT_FALSE(cache->GetKdcIp(kUserRealm));
EXPECT_FALSE(cache->GetDcName(kUserRealm));
EXPECT_FALSE(cache->GetIsAffiliated(kUserRealm));
// Auth should fill the user realm data.
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
ASSERT_TRUE(cache->GetWorkgroup(kUserRealm));
ASSERT_TRUE(cache->GetKdcIp(kUserRealm));
ASSERT_TRUE(cache->GetDcName(kUserRealm));
ASSERT_TRUE(cache->GetIsAffiliated(kUserRealm));
EXPECT_EQ(kDefaultWorkgroup, *cache->GetWorkgroup(kUserRealm));
EXPECT_EQ(kDefaultKdcIp, *cache->GetKdcIp(kUserRealm));
EXPECT_EQ(kDefaultDcName, *cache->GetDcName(kUserRealm));
EXPECT_TRUE(*cache->GetIsAffiliated(kUserRealm));
}
// Authpolicyd loads auth data cache on startup and uses the data for workgroup
// and KDC IP.
TEST_F(AuthPolicyTest, AuthDataCacheLoadsAndWorksForWorkgroupAndKdcIp) {
// Join domain, so that a config file is written. Authpolicyd only loads a
// auth data cache file if a config is expected.
JoinDomainRequest request;
request.set_machine_name(kMachineName);
request.set_machine_domain(kMachineRealm);
request.set_user_principal_name(kUserPrincipal);
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
// Create a cache file with stub workgroup and KDC IP data. Note that machine
// KDC IP is not checked. The cached KDC IP is overwritten during the machine
// password check, which happens immediately on startup. The check needs the
// server time and the KDC IP is fetched along with it, immediately
// overwriting the cached value.
protos::DebugFlags flags;
AuthDataCache cache(&flags);
cache.SetWorkgroup(kUserRealm, kCacheTestUserWorkgroup);
cache.SetKdcIp(kUserRealm, kCacheTestUserKdcIp);
cache.SetWorkgroup(kMachineRealm, kCacheTestMachineWorkgroup);
base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE));
EXPECT_TRUE(cache.Save(cache_path));
// Restart authpolicyd. That should load the cache file.
samba().ResetForTesting();
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
// Check that the device smb.conf contains the workgroup from the cache.
SmbConf device_smb_conf;
ReadSmbConf(paths_->Get(Path::DEVICE_SMB_CONF), &device_smb_conf);
EXPECT_EQ(kCacheTestMachineWorkgroup, device_smb_conf.workgroup);
// Check that the user smb.conf contains the workgroup from the cache.
SmbConf user_smb_conf;
ReadSmbConf(paths_->Get(Path::USER_SMB_CONF), &user_smb_conf);
EXPECT_EQ(kCacheTestUserWorkgroup, user_smb_conf.workgroup);
// Check that the user krb5.conf contains the KDC IP from the cache.
Krb5Conf user_krb5_conf;
ReadKrb5Conf(paths_->Get(Path::USER_KRB5_CONF), &user_krb5_conf);
EXPECT_EQ(EmbraceIp(kCacheTestUserKdcIp), user_krb5_conf.kdc);
}
// Authpolicyd loads auth data cache on startup and uses the data for DC name.
TEST_F(AuthPolicyTest, AuthDataCacheWorksForDcName) {
// Join domain, so that a config file is written. Authpolicyd only loads a
// auth data cache file if a config is expected.
// Note that |kOneGpoMachineName| is used here. This makes sure that
// stub_smbclient is called with the DC name in the URL of the GPO to
// download. Since stub_smbclient doesn't accept the DC name we set here
// (|kCacheTest*DcName|, see |kHostAndShare|), it should respond with a
// failed connection error and we should see ERROR_SMBCLIENT_FAILED as a
// result. There's no other artifact of DC name that could be tested.
JoinDomainRequest request;
request.set_machine_name(kOneGpoMachineName);
request.set_machine_domain(kMachineRealm);
request.set_user_principal_name(kUserPrincipal);
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
// Create a cache file with stub DC name.
protos::DebugFlags flags;
AuthDataCache cache(&flags);
cache.SetDcName(kUserRealm, kCacheTestUserDcName);
cache.SetDcName(kMachineRealm, kCacheTestMachineDcName);
base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE));
EXPECT_TRUE(cache.Save(cache_path));
// Restart authpolicyd, so that the cache file gets loaded.
samba().ResetForTesting();
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
// Since stub_smbclient doesn't accept the DC names from the cache, we should
// get an error here (best we can do to verify cached DC name was used).
FetchAndValidateDevicePolicy(ERROR_SMBCLIENT_FAILED);
FetchAndValidateUserPolicy(DefaultAuth(), ERROR_SMBCLIENT_FAILED);
}
// Authpolicyd loads auth data cache on startup and uses the data for DC name.
TEST_F(AuthPolicyTest, AuthDataCacheDiesForUnaffiliatedUsers) {
JoinAndFetchDevicePolicy(kMachineName);
// Set affiliation flag to false for the user realm. Note that this shouldn't
// be possible, authpolicy shouldn't cache any data for unaffiliated realms.
// See AuthDataCacheTurnedOffForUnaffiliatedUsers.
samba().GetAuthDataCacheForTesting()->SetIsAffiliated(kUserRealm, false);
// SambaInterface::IsUserAffiliated() should bail on Auth() because data for
// an unaffiliated realm is cached.
EXPECT_DEATH((void)Auth(kUserPrincipal, "", MakePasswordFd()),
"Caching for unaffiliated realms not supported");
}
// Authpolicyd loads auth data cache on startup and uses the data for DC name.
TEST_F(AuthPolicyTest, AuthDataCacheTurnedOffForUnaffiliatedUsers) {
// |kUnaffiliatedMachineName| causes the user realm to be unaffiliated.
JoinDomainRequest request;
request.set_machine_name(kUnaffiliatedMachineName);
request.set_machine_domain(kMachineRealm);
request.set_user_principal_name(kUserPrincipal);
EXPECT_EQ(ERROR_NONE, JoinEx(request, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &CheckDevicePolicyEmpty;
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_EQ(ERROR_NONE, Auth(kUserPrincipal, "", MakePasswordFd()));
// The cache should still exist from domain join, but it shouldn't contain the
// user realm.
AuthDataCache* cache = samba().GetAuthDataCacheForTesting();
EXPECT_FALSE(cache->GetWorkgroup(kUserRealm));
EXPECT_FALSE(cache->GetKdcIp(kUserRealm));
EXPECT_FALSE(cache->GetDcName(kUserRealm));
EXPECT_FALSE(cache->GetIsAffiliated(kUserRealm));
}
// The auth data cache cache is purged occasionally.
TEST_F(AuthPolicyTest, PurgesAuthDataCacheOccasionally) {
// Set a testing clock to stub out cache TTL.
auto clock_ptr = std::make_unique<base::SimpleTestClock>();
base::SimpleTestClock* clock = clock_ptr.get();
samba().GetAuthDataCacheForTesting()->SetClockForTesting(
std::move(clock_ptr));
JoinAndFetchDevicePolicy(kMachineName);
// Save a cache file that contains a different workgroup.
protos::DebugFlags flags;
AuthDataCache cache(&flags);
cache.SetClockForTesting(std::make_unique<base::SimpleTestClock>());
cache.SetWorkgroup(kUserRealm, kCacheTestUserWorkgroup);
base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE));
EXPECT_TRUE(cache.Save(cache_path));
// Restart authpolicy to make sure that the cached workgroup is used.
samba().ResetForTesting();
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
DefaultAuth();
SmbConf user_smb_conf;
ReadSmbConf(paths_->Get(Path::USER_SMB_CONF), &user_smb_conf);
EXPECT_EQ(kCacheTestUserWorkgroup, user_smb_conf.workgroup);
// Restart authpolicy again and call auth, but this time advance the clock
// enough to clear the cache, so that the default workgroup is used.
samba().ResetForTesting();
EXPECT_EQ(ERROR_NONE, samba().Initialize(true /* expect_config */));
clock->Advance(samba().GetAuthDataCacheTTLForTesting() * 2);
DefaultAuth();
ReadSmbConf(paths_->Get(Path::USER_SMB_CONF), &user_smb_conf);
EXPECT_EQ(kDefaultWorkgroup, user_smb_conf.workgroup);
}
// The lifetime of the auth data cache can be changed by the
// DeviceAuthDataCacheLifetime policy. It can also be turned off by that policy.
TEST_F(AuthPolicyTest, AuthDataCacheTTLCanBeSetByPolicy) {
base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE));
// Join domain.
EXPECT_EQ(ERROR_NONE,
Join(kOneGpoMachineName, kUserPrincipal, MakePasswordFd()));
MarkDeviceAsLocked();
validate_device_policy_ = &DontValidateDevicePolicy;
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_TRUE(base::PathExists(cache_path));
// Setting the policy to 0 should turn the cache off, clear the file and set
// the TTL to 0.
policy::PRegUserDevicePolicyWriter writer;
writer.AppendInteger(policy::key::kDeviceAuthDataCacheLifetime, kZeroHours);
writer.WriteToFile(stub_gpo1_path_);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_FALSE(samba().GetAuthDataCacheForTesting()->IsEnabled());
EXPECT_EQ(base::TimeDelta::FromHours(kZeroHours),
samba().GetAuthDataCacheTTLForTesting());
EXPECT_FALSE(base::PathExists(cache_path));
// Setting the policy to > 0 should turn the cache on again and set the TTL.
policy::PRegUserDevicePolicyWriter writer2;
writer2.AppendInteger(policy::key::kDeviceAuthDataCacheLifetime,
kNonZeroHours);
writer2.WriteToFile(stub_gpo1_path_);
FetchAndValidateDevicePolicy(ERROR_NONE);
EXPECT_TRUE(samba().GetAuthDataCacheForTesting()->IsEnabled());
EXPECT_EQ(base::TimeDelta::FromHours(kNonZeroHours),
samba().GetAuthDataCacheTTLForTesting());
EXPECT_FALSE(base::PathExists(cache_path));
}
} // namespace authpolicy