blob: eb87ff368fff37a1ff0b8f0a23185640f97eaef0 [file]
// 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 <chrono>
#include <filesystem>
#include <sstream>
#include <thread>
#include "common/config.h"
#include "util/time.h"
namespace doris::kerberos {
KerberosTicketCache::KerberosTicketCache(const KerberosConfig& config, const std::string& root_path,
std::unique_ptr<Krb5Interface> krb5_interface)
: _config(config),
_ccache_root_dir(root_path),
_krb5_interface(std::move(krb5_interface)) {}
KerberosTicketCache::~KerberosTicketCache() {
stop_periodic_refresh();
_cleanup_context();
if (std::filesystem::exists(_ticket_cache_path)) {
std::filesystem::remove(_ticket_cache_path);
}
LOG(INFO) << "destroy kerberos ticket cache " << _ticket_cache_path
<< " with principal: " << _config.get_principal();
}
Status KerberosTicketCache::initialize() {
std::lock_guard<std::mutex> lock(_mutex);
RETURN_IF_ERROR(_init_ticket_cache_path());
Status st = _initialize_context();
LOG(INFO) << "initialized kerberos ticket cache " << _ticket_cache_path
<< " with principal: " << _config.get_principal()
<< " and keytab: " << _config.get_keytab_path() << ": " << st.to_string();
return st;
}
Status KerberosTicketCache::_init_ticket_cache_path() {
std::string cache_file_md5 = "doris_krb_" + _config.get_hash_code();
// The path should be with prefix "_ccache_root_dir"
std::filesystem::path full_path = std::filesystem::path(_ccache_root_dir) / cache_file_md5;
full_path = std::filesystem::weakly_canonical(full_path);
std::filesystem::path parent_path = full_path.parent_path();
LOG(INFO) << "try creating kerberos ticket path: " << full_path.string()
<< " for principal: " << _config.get_principal();
try {
if (!std::filesystem::exists(parent_path)) {
// Create the parent dir if not exists
std::filesystem::create_directories(parent_path);
} else {
// Delete the ticket cache file if exists
if (std::filesystem::exists(full_path)) {
std::filesystem::remove(full_path);
}
}
_ticket_cache_path = full_path.string();
return Status::OK();
} catch (const std::filesystem::filesystem_error& e) {
return Status::InternalError("Error when setting kerberos ticket cache file: {}, {}",
full_path.native(), e.what());
} catch (const std::exception& e) {
return Status::InternalError("Exception when setting kerberos ticket cache file: {}, {}",
full_path.native(), e.what());
} catch (...) {
return Status::InternalError("Unknown error when setting kerberos ticket cache file: {}",
full_path.native());
}
}
Status KerberosTicketCache::login() {
std::lock_guard<std::mutex> lock(_mutex);
krb5_keytab keytab = nullptr;
krb5_ccache temp_ccache = nullptr;
try {
// Open the keytab file
RETURN_IF_ERROR(
_krb5_interface->kt_resolve(_context, _config.get_keytab_path().c_str(), &keytab));
// init ccache
RETURN_IF_ERROR(
_krb5_interface->cc_resolve(_context, _ticket_cache_path.c_str(), &temp_ccache));
// get init creds
krb5_get_init_creds_opt* opts = nullptr;
RETURN_IF_ERROR(_krb5_interface->get_init_creds_opt_alloc(_context, &opts));
krb5_creds creds;
Status status = _krb5_interface->get_init_creds_keytab(_context, &creds, _principal, keytab,
0, // start time
nullptr, // TKT service name
opts);
if (!status.ok()) {
_krb5_interface->get_init_creds_opt_free(_context, opts);
_krb5_interface->kt_close(_context, keytab);
if (temp_ccache) {
_krb5_interface->cc_close(_context, temp_ccache);
}
return status;
}
_ticket_lifetime_sec = static_cast<int64_t>(creds.times.endtime) -
static_cast<int64_t>(creds.times.starttime);
// init ccache file
status = _krb5_interface->cc_initialize(_context, temp_ccache, _principal);
if (!status.ok()) {
_krb5_interface->free_cred_contents(_context, &creds);
_krb5_interface->get_init_creds_opt_free(_context, opts);
_krb5_interface->kt_close(_context, keytab);
_krb5_interface->cc_close(_context, temp_ccache);
return status;
}
// save ccache
status = _krb5_interface->cc_store_cred(_context, temp_ccache, &creds);
if (!status.ok()) {
_krb5_interface->free_cred_contents(_context, &creds);
_krb5_interface->get_init_creds_opt_free(_context, opts);
_krb5_interface->kt_close(_context, keytab);
_krb5_interface->cc_close(_context, temp_ccache);
return status;
}
// clean
_krb5_interface->free_cred_contents(_context, &creds);
_krb5_interface->get_init_creds_opt_free(_context, opts);
_krb5_interface->kt_close(_context, keytab);
// Only set _ccache if everything succeeded
if (_ccache) {
_krb5_interface->cc_close(_context, _ccache);
}
_ccache = temp_ccache;
} catch (...) {
if (keytab) {
_krb5_interface->kt_close(_context, keytab);
}
if (temp_ccache) {
_krb5_interface->cc_close(_context, temp_ccache);
}
return Status::InternalError("Failed to login with kerberos");
}
return Status::OK();
}
Status KerberosTicketCache::login_with_cache() {
std::lock_guard<std::mutex> lock(_mutex);
// Close existing ccache if any
if (_ccache) {
_krb5_interface->cc_close(_context, _ccache);
_ccache = nullptr;
}
return _krb5_interface->cc_resolve(_context, _ticket_cache_path.c_str(), &_ccache);
}
Status KerberosTicketCache::write_ticket_cache() {
std::lock_guard<std::mutex> lock(_mutex);
if (!_ccache) {
return Status::InternalError("No credentials cache available");
}
// MIT Kerberos automatically writes to the cache file
// when using the FILE: cache type
return Status::OK();
}
Status KerberosTicketCache::refresh_tickets() {
try {
return login();
} catch (const std::exception& e) {
std::stringstream ss;
ss << "Failed to refresh tickets: " << e.what();
return Status::InternalError(ss.str());
}
}
void KerberosTicketCache::start_periodic_refresh() {
_should_stop_refresh = false;
_refresh_thread = std::make_unique<std::thread>([this]() {
auto refresh_interval = std::chrono::milliseconds(
static_cast<int>(_config.get_refresh_interval_second() * 1000));
auto sleep_duration = _refresh_thread_sleep_time;
std::chrono::milliseconds accumulated_time(0);
while (!_should_stop_refresh) {
std::this_thread::sleep_for(sleep_duration);
accumulated_time += sleep_duration;
if (accumulated_time >= refresh_interval) {
accumulated_time = std::chrono::milliseconds(0); // Reset accumulated time
Status st = refresh_tickets();
if (!st.ok()) {
// ignore and continue
LOG(WARNING) << st.to_string();
} else {
LOG(INFO) << "refresh kerberos ticket cache: " << _ticket_cache_path
<< ", lifetime sec: " << _ticket_lifetime_sec;
}
}
}
});
}
void KerberosTicketCache::stop_periodic_refresh() {
if (_refresh_thread) {
_should_stop_refresh = true;
_refresh_thread->join();
_refresh_thread.reset();
}
}
Status KerberosTicketCache::_initialize_context() {
if (!_config.get_krb5_conf_path().empty()) {
if (setenv("KRB5_CONFIG", _config.get_krb5_conf_path().c_str(), 1) != 0) {
return Status::InvalidArgument("Failed to set KRB5_CONFIG environment variable");
}
LOG(INFO) << "Using custom krb5.conf: " << _config.get_krb5_conf_path();
}
RETURN_IF_ERROR(_krb5_interface->init_context(&_context));
return _krb5_interface->parse_name(_context, _config.get_principal().c_str(), &_principal);
}
void KerberosTicketCache::_cleanup_context() {
if (_principal) {
_krb5_interface->free_principal(_context, _principal);
}
if (_ccache) {
_krb5_interface->cc_close(_context, _ccache);
}
if (_context) {
_krb5_interface->free_context(_context);
}
}
std::vector<KerberosTicketInfo> KerberosTicketCache::get_ticket_info() {
std::lock_guard<std::mutex> lock(_mutex);
std::vector<KerberosTicketInfo> result;
if (!_ccache || !_context) {
// If no valid cache or context, return empty vector
return result;
}
krb5_cc_cursor cursor;
if (_krb5_interface->cc_start_seq_get(_context, _ccache, &cursor) != Status::OK()) {
return result;
}
// Iterate through all credentials in the cache
krb5_creds creds;
while (_krb5_interface->cc_next_cred(_context, _ccache, &cursor, &creds) == Status::OK()) {
KerberosTicketInfo info;
info.principal = _config.get_principal();
info.keytab_path = _config.get_keytab_path();
info.start_time = static_cast<int64_t>(creds.times.starttime);
info.expiry_time = static_cast<int64_t>(creds.times.endtime);
info.auth_time = static_cast<int64_t>(creds.times.authtime);
// minus 2,
// one is shared_from_this(), the other is ref in _ticket_caches of KerberosTicketMgr
info.use_count = shared_from_this().use_count() - 2;
// Get the service principal name
char* service_name = nullptr;
if (_krb5_interface->unparse_name(_context, creds.server, &service_name) == Status::OK()) {
info.service_principal = service_name;
_krb5_interface->free_unparsed_name(_context, service_name);
} else {
info.service_principal = "Unparse Error";
}
info.cache_path = _ticket_cache_path;
info.refresh_interval_second = _config.get_refresh_interval_second();
info.hash_code = _config.get_hash_code();
result.push_back(std::move(info));
_krb5_interface->free_cred_contents(_context, &creds);
}
_krb5_interface->cc_end_seq_get(_context, _ccache, &cursor);
return result;
}
} // namespace doris::kerberos