| // Copyright 2019 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "kerberos/kerberos_adaptor.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include <base/files/scoped_temp_dir.h> |
| #include <base/memory/ref_counted.h> |
| #include <base/memory/scoped_refptr.h> |
| #include <base/message_loop/message_loop.h> |
| #include <base/run_loop.h> |
| #include <brillo/asan.h> |
| #include <dbus/login_manager/dbus-constants.h> |
| #include <dbus/mock_bus.h> |
| #include <dbus/mock_exported_object.h> |
| #include <dbus/mock_object_proxy.h> |
| #include <dbus/object_path.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "kerberos/account_manager.h" |
| #include "kerberos/fake_krb5_interface.h" |
| #include "kerberos/kerberos_metrics.h" |
| #include "kerberos/krb5_jail_wrapper.h" |
| #include "kerberos/platform_helper.h" |
| #include "kerberos/proto_bindings/kerberos_service.pb.h" |
| |
| using brillo::dbus_utils::DBusObject; |
| using dbus::MockBus; |
| using dbus::MockExportedObject; |
| using dbus::MockObjectProxy; |
| using dbus::ObjectPath; |
| using testing::_; |
| using testing::AnyNumber; |
| using testing::Invoke; |
| using testing::Mock; |
| using testing::NiceMock; |
| using testing::Return; |
| using ByteArray = kerberos::KerberosAdaptor::ByteArray; |
| |
| namespace kerberos { |
| namespace { |
| |
| // Some arbitrary D-Bus message serial number. Required for mocking D-Bus calls. |
| const int kDBusSerial = 123; |
| |
| // Stub user data. |
| constexpr char kUser[] = "user"; |
| constexpr char kUserHash[] = "user-hash"; |
| constexpr char kPrincipalName[] = "user@REALM.COM"; |
| constexpr char kOtherPrincipalName[] = "other_user@REALM.COM"; |
| constexpr bool kManaged = true; |
| constexpr bool kUnmanaged = false; |
| constexpr char kPassword[] = "hello123"; |
| |
| // Stub D-Bus object path for the mock daemon. |
| constexpr char kObjectPath[] = "/object/path"; |
| |
| // Real storage base dir. |
| constexpr char KDaemonStore[] = "/run/daemon-store/kerberosd"; |
| |
| // Empty Kerberos configuration. |
| constexpr char kEmptyConfig[] = ""; |
| |
| class MockMetrics : public KerberosMetrics { |
| public: |
| explicit MockMetrics(const base::FilePath& storage_dir) |
| : KerberosMetrics(storage_dir) {} |
| ~MockMetrics() override = default; |
| |
| MOCK_METHOD(void, StartAcquireTgtTimer, (), (override)); |
| MOCK_METHOD(void, StopAcquireTgtTimerAndReport, (), (override)); |
| MOCK_METHOD(void, |
| ReportValidateConfigErrorCode, |
| (ConfigErrorCode), |
| (override)); |
| MOCK_METHOD(void, |
| ReportDBusCallResult, |
| (const std::string&, ErrorType), |
| (override)); |
| MOCK_METHOD(bool, ShouldReportDailyUsageStats, (), (override)); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockMetrics); |
| }; |
| |
| // Stub completion callback for RegisterAsync(). |
| void DoNothing(bool /* unused */) {} |
| |
| // Serializes |message| as byte array. |
| ByteArray SerializeAsBlob(const google::protobuf::MessageLite& message) { |
| ByteArray result; |
| result.resize(message.ByteSize()); |
| CHECK(message.SerializeToArray(result.data(), result.size())); |
| return result; |
| } |
| |
| // Parses a response message from a byte array. |
| template <typename TResponse> |
| TResponse ParseResponse(const ByteArray& response_blob) { |
| TResponse response; |
| EXPECT_TRUE( |
| response.ParseFromArray(response_blob.data(), response_blob.size())); |
| return response; |
| } |
| |
| // Stub RetrievePrimarySession Session Manager method. |
| std::unique_ptr<dbus::Response> StubRetrievePrimarySession( |
| dbus::MethodCall* method_call, |
| int /* timeout_ms */, |
| dbus::ScopedDBusError* /* error */) { |
| // Respond with username = kUser and sanitized_username = kUserHash. |
| method_call->SetSerial(kDBusSerial); |
| auto response = dbus::Response::FromMethodCall(method_call); |
| dbus::MessageWriter writer(response.get()); |
| writer.AppendString(kUser); |
| writer.AppendString(kUserHash); |
| |
| // Note: The mock wraps this back into a std::unique_ptr. |
| return response; |
| } |
| |
| } // namespace |
| |
| class KerberosAdaptorTest : public ::testing::Test { |
| public: |
| KerberosAdaptorTest() = default; |
| ~KerberosAdaptorTest() override = default; |
| |
| void SetUp() override { |
| ::testing::Test::SetUp(); |
| |
| mock_bus_ = base::MakeRefCounted<MockBus>(dbus::Bus::Options()); |
| |
| // Mock out D-Bus initialization. |
| const ObjectPath object_path(kObjectPath); |
| mock_exported_object_ = |
| base::MakeRefCounted<MockExportedObject>(mock_bus_.get(), object_path); |
| EXPECT_CALL(*mock_bus_, GetExportedObject(object_path)) |
| .WillRepeatedly(Return(mock_exported_object_.get())); |
| EXPECT_CALL(*mock_exported_object_, Unregister()).Times(AnyNumber()); |
| EXPECT_CALL(*mock_exported_object_, ExportMethod(_, _, _, _)) |
| .Times(AnyNumber()); |
| EXPECT_CALL(*mock_exported_object_, SendSignal(_)) |
| .WillRepeatedly( |
| Invoke(this, &KerberosAdaptorTest::OnKerberosFilesChanged)); |
| |
| // Create temp directory for files written during tests. |
| CHECK(storage_dir_.CreateUniqueTempDir()); |
| |
| // Create mock metrics. |
| auto metrics = |
| std::make_unique<NiceMock<MockMetrics>>(storage_dir_.GetPath()); |
| metrics_ = metrics.get(); |
| ON_CALL(*metrics_, ShouldReportDailyUsageStats) |
| .WillByDefault(Return(false)); |
| |
| // Create KerberosAdaptor instance. Do this AFTER creating the proxy mocks |
| // since they might be accessed during initialization. |
| auto dbus_object = |
| std::make_unique<DBusObject>(nullptr, mock_bus_, object_path); |
| adaptor_ = std::make_unique<KerberosAdaptor>(std::move(dbus_object)); |
| adaptor_->set_storage_dir_for_testing(storage_dir_.GetPath()); |
| adaptor_->set_metrics_for_testing(std::move(metrics)); |
| adaptor_->set_krb5_for_testing(std::make_unique<FakeKrb5Interface>()); |
| adaptor_->RegisterAsync(base::BindRepeating(&DoNothing)); |
| } |
| |
| void TearDown() override { adaptor_.reset(); } |
| |
| protected: |
| void OnKerberosFilesChanged(dbus::Signal* signal) { |
| EXPECT_EQ(signal->GetInterface(), "org.chromium.Kerberos"); |
| EXPECT_EQ(signal->GetMember(), "KerberosFilesChanged"); |
| dbus::MessageReader reader(signal); |
| std::string principal_name; |
| EXPECT_TRUE(reader.PopString(&principal_name)); |
| EXPECT_EQ(kPrincipalName, principal_name); |
| } |
| |
| // Adds an account with given |principal_name| and |is_managed| parameters. |
| ErrorType AddAccount(const std::string& principal_name, bool is_managed) { |
| AddAccountRequest request; |
| request.set_principal_name(principal_name); |
| request.set_is_managed(is_managed); |
| ByteArray response_blob = adaptor_->AddAccount(SerializeAsBlob(request)); |
| return ParseResponse<AddAccountResponse>(response_blob).error(); |
| } |
| |
| // Removes the account with |principal_name|. |
| RemoveAccountResponse RemoveAccount(const std::string& principal_name) { |
| RemoveAccountRequest request; |
| request.set_principal_name(principal_name); |
| ByteArray response_blob = adaptor_->RemoveAccount(SerializeAsBlob(request)); |
| return ParseResponse<RemoveAccountResponse>(response_blob); |
| } |
| |
| // Removes all accounts. |
| ClearAccountsResponse ClearAccounts(ClearMode mode) { |
| ClearAccountsRequest request; |
| request.set_mode(mode); |
| ByteArray response_blob = adaptor_->ClearAccounts(SerializeAsBlob(request)); |
| return ParseResponse<ClearAccountsResponse>(response_blob); |
| } |
| |
| // Lists accounts. |
| ErrorType ListAccounts() { |
| ListAccountsRequest request; |
| ByteArray response_blob = adaptor_->ListAccounts(SerializeAsBlob(request)); |
| return ParseResponse<ListAccountsResponse>(response_blob).error(); |
| } |
| |
| // Sets a default config for |principal_name|. |
| ErrorType SetConfig(const std::string& principal_name) { |
| SetConfigRequest request; |
| request.set_principal_name(principal_name); |
| request.set_krb5conf(kEmptyConfig); |
| ByteArray response_blob = adaptor_->SetConfig(SerializeAsBlob(request)); |
| return ParseResponse<SetConfigResponse>(response_blob).error(); |
| } |
| |
| // Validates a default config. |
| ErrorType ValidateConfig() { |
| ValidateConfigRequest request; |
| request.set_krb5conf(kEmptyConfig); |
| ByteArray response_blob = |
| adaptor_->ValidateConfig(SerializeAsBlob(request)); |
| return ParseResponse<ValidateConfigResponse>(response_blob).error(); |
| } |
| |
| // Acquires a default Kerberos ticket for |principal_name| with default |
| // password. |
| ErrorType AcquireKerberosTgt(const std::string& principal_name) { |
| AcquireKerberosTgtRequest request; |
| request.set_principal_name(principal_name); |
| ByteArray response_blob = adaptor_->AcquireKerberosTgt( |
| SerializeAsBlob(request), WriteStringToPipe(kPassword)); |
| return ParseResponse<AcquireKerberosTgtResponse>(response_blob).error(); |
| } |
| |
| // Acquires a default Kerberos ticket for |principal_name|. |
| ErrorType GetKerberosFiles(const std::string& principal_name) { |
| GetKerberosFilesRequest request; |
| request.set_principal_name(principal_name); |
| ByteArray response_blob = |
| adaptor_->GetKerberosFiles(SerializeAsBlob(request)); |
| return ParseResponse<GetKerberosFilesResponse>(response_blob).error(); |
| } |
| |
| // KEEP ORDER between these. It's important for destruction. |
| scoped_refptr<MockBus> mock_bus_; |
| scoped_refptr<MockExportedObject> mock_exported_object_; |
| std::unique_ptr<KerberosAdaptor> adaptor_; |
| base::MessageLoop loop_; |
| |
| base::ScopedTempDir storage_dir_; |
| |
| NiceMock<MockMetrics>* metrics_ = nullptr; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(KerberosAdaptorTest); |
| }; |
| |
| // RetrievePrimarySession is called to figure out the proper storage dir if the |
| // dir is NOT overwritten by KerberosAdaptor::set_storage_dir_for_testing(). |
| TEST_F(KerberosAdaptorTest, RetrievesPrimarySession) { |
| // Stub out Session Manager's RetrievePrimarySession D-Bus method. |
| auto mock_session_manager_proxy = base::MakeRefCounted<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, |
| CallMethodAndBlockWithErrorDetails(_, _, _)) |
| .WillOnce(Invoke(&StubRetrievePrimarySession)); |
| |
| // Recreate an adaptor, but don't call set_storage_dir_for_testing(). |
| auto dbus_object = |
| std::make_unique<DBusObject>(nullptr, mock_bus_, ObjectPath(kObjectPath)); |
| auto adaptor = std::make_unique<KerberosAdaptor>(std::move(dbus_object)); |
| adaptor->RegisterAsync(base::BindRepeating(&DoNothing)); |
| |
| // Check if the right storage dir is set. |
| EXPECT_EQ(base::FilePath(KDaemonStore).Append(kUserHash), |
| adaptor->GetAccountManagerForTesting()->GetStorageDirForTesting()); |
| } |
| |
| // AddAccount and RemoveAccount succeed when a new account is added and removed. |
| TEST_F(KerberosAdaptorTest, AddRemoveAccountSuccess) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, RemoveAccount(kPrincipalName).error()); |
| } |
| |
| // RemoveAccount succeeds and returns the list of remaining accounts. |
| TEST_F(KerberosAdaptorTest, RemoveAccountSuccess) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, AddAccount(kOtherPrincipalName, kUnmanaged)); |
| |
| RemoveAccountResponse response = RemoveAccount(kPrincipalName); |
| EXPECT_EQ(ERROR_NONE, response.error()); |
| ASSERT_EQ(1, response.accounts_size()); |
| EXPECT_EQ(kOtherPrincipalName, response.accounts(0).principal_name()); |
| response = RemoveAccount(kOtherPrincipalName); |
| EXPECT_EQ(ERROR_NONE, response.error()); |
| EXPECT_EQ(0, response.accounts_size()); |
| } |
| |
| // RemoveAccount fails if the account doesn't exist, and returns the list of |
| // remaining accounts. |
| TEST_F(KerberosAdaptorTest, RemoveAccountFails) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| |
| RemoveAccountResponse response = RemoveAccount(kOtherPrincipalName); |
| EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME, response.error()); |
| ASSERT_EQ(1, response.accounts_size()); |
| EXPECT_EQ(kPrincipalName, response.accounts(0).principal_name()); |
| } |
| |
| // AddAccount and ClearAccounts succeed when a new account is added and cleared. |
| TEST_F(KerberosAdaptorTest, AddClearAccountsSuccess) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, ClearAccounts(CLEAR_ALL).error()); |
| } |
| |
| // ClearAccounts succeeds to clear all accounts and returns the list of |
| // remaining accounts. |
| TEST_F(KerberosAdaptorTest, ClearAccountsSuccessClearAll) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, AddAccount(kOtherPrincipalName, kUnmanaged)); |
| |
| ClearAccountsResponse response = ClearAccounts(CLEAR_ALL); |
| EXPECT_EQ(ERROR_NONE, response.error()); |
| EXPECT_EQ(0, response.accounts_size()); |
| } |
| |
| // ClearAccounts succeeds to clear managed accounts and returns the list of |
| // remaining accounts. |
| TEST_F(KerberosAdaptorTest, ClearAccountsSuccessClearManaged) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, AddAccount(kOtherPrincipalName, kManaged)); |
| |
| ClearAccountsResponse response = ClearAccounts(CLEAR_ONLY_MANAGED_ACCOUNTS); |
| EXPECT_EQ(ERROR_NONE, response.error()); |
| ASSERT_EQ(1, response.accounts_size()); |
| EXPECT_EQ(kPrincipalName, response.accounts(0).principal_name()); |
| } |
| |
| // ClearAccounts succeeds to clear unmanaged accounts and returns the list of |
| // remaining accounts. |
| TEST_F(KerberosAdaptorTest, ClearAccountsSuccessClearUnmanaged) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, AddAccount(kOtherPrincipalName, kManaged)); |
| |
| ClearAccountsResponse response = ClearAccounts(CLEAR_ONLY_UNMANAGED_ACCOUNTS); |
| EXPECT_EQ(ERROR_NONE, response.error()); |
| ASSERT_EQ(1, response.accounts_size()); |
| EXPECT_EQ(kOtherPrincipalName, response.accounts(0).principal_name()); |
| } |
| |
| // ClearAccounts succeeds to clear unmanaged remembered passwords and returns |
| // the list of remaining accounts. |
| TEST_F(KerberosAdaptorTest, ClearAccountsSuccessClearUnmanagedPasswords) { |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, AddAccount(kOtherPrincipalName, kManaged)); |
| |
| ClearAccountsResponse response = |
| ClearAccounts(CLEAR_ONLY_UNMANAGED_REMEMBERED_PASSWORDS); |
| EXPECT_EQ(ERROR_NONE, response.error()); |
| ASSERT_EQ(2, response.accounts_size()); |
| EXPECT_EQ(kPrincipalName, response.accounts(0).principal_name()); |
| EXPECT_EQ(kOtherPrincipalName, response.accounts(1).principal_name()); |
| } |
| |
| // Checks that metrics are reported for all D-Bus calls. |
| TEST_F(KerberosAdaptorTest, Metrics_ReportDBusCallResult) { |
| EXPECT_CALL(*metrics_, ReportDBusCallResult("AddAccount", ERROR_NONE)); |
| EXPECT_CALL(*metrics_, ReportDBusCallResult("ListAccounts", ERROR_NONE)); |
| EXPECT_CALL(*metrics_, ReportDBusCallResult("SetConfig", ERROR_NONE)); |
| EXPECT_CALL(*metrics_, ReportDBusCallResult("ValidateConfig", ERROR_NONE)); |
| EXPECT_CALL(*metrics_, |
| ReportDBusCallResult("AcquireKerberosTgt", ERROR_NONE)); |
| EXPECT_CALL(*metrics_, ReportDBusCallResult("GetKerberosFiles", ERROR_NONE)); |
| EXPECT_CALL(*metrics_, ReportDBusCallResult("RemoveAccount", ERROR_NONE)); |
| EXPECT_CALL(*metrics_, ReportDBusCallResult("ClearAccounts", ERROR_NONE)); |
| |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, ListAccounts()); |
| EXPECT_EQ(ERROR_NONE, SetConfig(kPrincipalName)); |
| EXPECT_EQ(ERROR_NONE, ValidateConfig()); |
| EXPECT_EQ(ERROR_NONE, AcquireKerberosTgt(kPrincipalName)); |
| EXPECT_EQ(ERROR_NONE, GetKerberosFiles(kPrincipalName)); |
| EXPECT_EQ(ERROR_NONE, RemoveAccount(kPrincipalName).error()); |
| EXPECT_EQ(ERROR_NONE, ClearAccounts(CLEAR_ALL).error()); |
| } |
| |
| // AcquireKerberosTgt should trigger timing events. |
| TEST_F(KerberosAdaptorTest, Metrics_AcquireTgtTimer) { |
| EXPECT_CALL(*metrics_, StartAcquireTgtTimer()); |
| EXPECT_CALL(*metrics_, StopAcquireTgtTimerAndReport()); |
| EXPECT_EQ(ERROR_UNKNOWN_PRINCIPAL_NAME, AcquireKerberosTgt(kPrincipalName)); |
| } |
| |
| // ValidateConfig should trigger timing events. |
| TEST_F(KerberosAdaptorTest, Metrics_ValidateConfigErrorCode) { |
| EXPECT_CALL(*metrics_, ReportValidateConfigErrorCode(CONFIG_ERROR_NONE)); |
| EXPECT_EQ(ERROR_NONE, AddAccount(kPrincipalName, kUnmanaged)); |
| EXPECT_EQ(ERROR_NONE, ValidateConfig()); |
| } |
| |
| // TODO(https://crbug.com/952247): Add more tests. |
| |
| } // namespace kerberos |