blob: cbd6294fde4e527c3a9701c00ef1e141100507b4 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "common/kerberos/kerberos_ticket_cache.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <memory>
#include <thread>
namespace doris::kerberos {
using testing::_;
// Mock class
class MockKrb5Interface : public Krb5Interface {
public:
MOCK_METHOD(Status, init_context, (krb5_context*), (override));
MOCK_METHOD(Status, parse_name, (krb5_context, const char*, krb5_principal*), (override));
MOCK_METHOD(Status, kt_resolve, (krb5_context, const char*, krb5_keytab*), (override));
MOCK_METHOD(Status, cc_resolve, (krb5_context, const char*, krb5_ccache*), (override));
MOCK_METHOD(Status, get_init_creds_opt_alloc, (krb5_context, krb5_get_init_creds_opt**),
(override));
MOCK_METHOD(Status, get_init_creds_keytab,
(krb5_context, krb5_creds*, krb5_principal, krb5_keytab, krb5_deltat, const char*,
krb5_get_init_creds_opt*),
(override));
MOCK_METHOD(Status, cc_initialize, (krb5_context, krb5_ccache, krb5_principal), (override));
MOCK_METHOD(Status, cc_store_cred, (krb5_context, krb5_ccache, krb5_creds*), (override));
MOCK_METHOD(Status, timeofday, (krb5_context, krb5_timestamp*), (override));
MOCK_METHOD(Status, cc_start_seq_get, (krb5_context, krb5_ccache, krb5_cc_cursor*), (override));
MOCK_METHOD(Status, cc_next_cred, (krb5_context, krb5_ccache, krb5_cc_cursor*, krb5_creds*),
(override));
MOCK_METHOD(void, cc_end_seq_get, (krb5_context, krb5_ccache, krb5_cc_cursor*), (override));
MOCK_METHOD(void, free_principal, (krb5_context, krb5_principal), (override));
MOCK_METHOD(void, free_cred_contents, (krb5_context, krb5_creds*), (override));
MOCK_METHOD(void, get_init_creds_opt_free, (krb5_context, krb5_get_init_creds_opt*),
(override));
MOCK_METHOD(void, kt_close, (krb5_context, krb5_keytab), (override));
MOCK_METHOD(void, cc_close, (krb5_context, krb5_ccache), (override));
MOCK_METHOD(void, free_context, (krb5_context), (override));
MOCK_METHOD(const char*, get_error_message, (krb5_context, krb5_error_code), (override));
MOCK_METHOD(void, free_error_message, (krb5_context, const char*), (override));
MOCK_METHOD(Status, unparse_name, (krb5_context, krb5_principal, char**), (override));
MOCK_METHOD(void, free_unparsed_name, (krb5_context, char*), (override));
};
class KerberosTicketCacheTest : public testing::Test {
protected:
void SetUp() override {
_mock_krb5 = std::make_unique<testing::StrictMock<MockKrb5Interface>>();
_mock_krb5_ptr = _mock_krb5.get();
// Create a temporary directory for testing
_test_dir = std::filesystem::temp_directory_path() / "kerberos_test";
std::filesystem::create_directories(_test_dir);
_config.set_principal_and_keytab("test_principal", "/path/to/keytab");
_config.set_krb5_conf_path("/etc/krb5.conf");
_config.set_refresh_interval(2);
_config.set_min_time_before_refresh(600);
_cache = std::make_unique<KerberosTicketCache>(_config, _test_dir.string(),
std::move(_mock_krb5));
_cache->set_refresh_thread_sleep_time(std::chrono::milliseconds(1));
}
void TearDown() override {
_cache.reset();
if (std::filesystem::exists(_test_dir)) {
std::filesystem::remove_all(_test_dir);
}
}
// Helper function to set up basic expectations for initialization
void SetupBasicInitExpectations() {
EXPECT_CALL(*_mock_krb5_ptr, init_context(_)).WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, parse_name(_, _, _)).WillOnce(testing::Return(Status::OK()));
}
// Helper function to simulate ticket cache file creation/update
void SimulateTicketCacheFileUpdate(const std::string& cache_path) {
std::ofstream cache_file(cache_path);
cache_file << "mock ticket cache content";
cache_file.close();
// Sleep a bit to ensure file timestamps are different
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
void ExpectForLogin(const std::string& cache_path) {
// Setup expectations for login
EXPECT_CALL(*_mock_krb5_ptr, kt_resolve(_, _, _)).WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, cc_resolve(_, _, _)).WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, get_init_creds_opt_alloc(_, _))
.WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, get_init_creds_keytab(_, _, _, _, _, _, _))
.WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, cc_initialize(_, _, _))
.WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, cc_store_cred(_, _, _))
.WillOnce(testing::DoAll(
testing::Invoke([this, cache_path](krb5_context, krb5_ccache, krb5_creds*) {
SimulateTicketCacheFileUpdate(cache_path);
return Status::OK();
})));
// Cleanup calls
EXPECT_CALL(*_mock_krb5_ptr, free_cred_contents(_, _)).Times(1);
EXPECT_CALL(*_mock_krb5_ptr, get_init_creds_opt_free(_, _)).Times(1);
EXPECT_CALL(*_mock_krb5_ptr, kt_close(_, _)).Times(1);
}
// Helper function to get file last write time
std::filesystem::file_time_type GetFileLastWriteTime(const std::string& path) {
return std::filesystem::last_write_time(path);
}
void printFileTime(const std::filesystem::file_time_type& ftime) {
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ftime - std::filesystem::file_time_type::clock::now() +
std::chrono::system_clock::now());
std::time_t cftime = std::chrono::system_clock::to_time_t(sctp);
std::cout << std::put_time(std::localtime(&cftime), "%Y-%m-%d %H:%M:%S") << '\n';
}
protected:
KerberosConfig _config;
std::unique_ptr<KerberosTicketCache> _cache;
MockKrb5Interface* _mock_krb5_ptr;
std::unique_ptr<MockKrb5Interface> _mock_krb5;
std::filesystem::path _test_dir;
};
TEST_F(KerberosTicketCacheTest, Initialize) {
SetupBasicInitExpectations();
ASSERT_TRUE(_cache->initialize().ok());
// Verify that the cache file is created in the test directory
std::string cache_path = _cache->get_ticket_cache_path();
ASSERT_TRUE(cache_path.find(_test_dir.string()) == 0);
}
TEST_F(KerberosTicketCacheTest, LoginSuccess) {
SetupBasicInitExpectations();
ASSERT_TRUE(_cache->initialize().ok());
std::string cache_path = _cache->get_ticket_cache_path();
ExpectForLogin(cache_path);
ASSERT_TRUE(_cache->login().ok());
ASSERT_TRUE(std::filesystem::exists(cache_path));
}
TEST_F(KerberosTicketCacheTest, RefreshTickets) {
SetupBasicInitExpectations();
ASSERT_TRUE(_cache->initialize().ok());
std::string cache_path = _cache->get_ticket_cache_path();
// Create initial ticket cache file
SimulateTicketCacheFileUpdate(cache_path);
auto initial_write_time = GetFileLastWriteTime(cache_path);
// Setup expectations for refresh (login)
EXPECT_CALL(*_mock_krb5_ptr, kt_resolve(_, _, _)).WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, cc_resolve(_, _, _)).WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, get_init_creds_opt_alloc(_, _))
.WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, get_init_creds_keytab(_, _, _, _, _, _, _))
.WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, cc_initialize(_, _, _)).WillOnce(testing::Return(Status::OK()));
EXPECT_CALL(*_mock_krb5_ptr, cc_store_cred(_, _, _))
.WillOnce(testing::DoAll(
testing::Invoke([this, cache_path](krb5_context, krb5_ccache, krb5_creds*) {
SimulateTicketCacheFileUpdate(cache_path);
return Status::OK();
})));
// Cleanup calls
// Because we forcbly refresh the ticket, the _need_refresh() is not called,
// so it only call free_cred_contents once.
EXPECT_CALL(*_mock_krb5_ptr, free_cred_contents(_, _)).Times(1);
EXPECT_CALL(*_mock_krb5_ptr, get_init_creds_opt_free(_, _)).Times(1);
EXPECT_CALL(*_mock_krb5_ptr, kt_close(_, _)).Times(1);
ASSERT_TRUE(_cache->refresh_tickets().ok());
// Verify that the cache file has been updated
auto new_write_time = GetFileLastWriteTime(cache_path);
ASSERT_GT(new_write_time, initial_write_time);
}
TEST_F(KerberosTicketCacheTest, PeriodicRefresh) {
SetupBasicInitExpectations();
ASSERT_TRUE(_cache->initialize().ok());
std::string cache_path = _cache->get_ticket_cache_path();
SimulateTicketCacheFileUpdate(cache_path);
auto initial_write_time = GetFileLastWriteTime(cache_path);
printFileTime(initial_write_time);
ExpectForLogin(cache_path);
// Start periodic refresh
_cache->start_periodic_refresh();
// Wait for a short time to allow some refresh attempts
// Because the refresh interval is 2s, need larger than 2s
std::this_thread::sleep_for(std::chrono::milliseconds(4000));
// Stop periodic refresh
_cache->stop_periodic_refresh();
// Verify that the test directory still exists
ASSERT_TRUE(std::filesystem::exists(_test_dir));
// Verify that the cache file still exists and has been updated
ASSERT_TRUE(std::filesystem::exists(cache_path));
auto final_write_time = GetFileLastWriteTime(cache_path);
printFileTime(final_write_time);
ASSERT_GT(final_write_time, initial_write_time);
}
} // namespace doris::kerberos