Coverage Report

Created: 2026-03-15 17:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/common/kerberos/kerberos_ticket_cache.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "common/kerberos/kerberos_ticket_cache.h"
19
20
#include <chrono>
21
#include <filesystem>
22
#include <sstream>
23
#include <thread>
24
25
#include "common/config.h"
26
#include "util/time.h"
27
28
namespace doris::kerberos {
29
30
KerberosTicketCache::KerberosTicketCache(const KerberosConfig& config, const std::string& root_path,
31
                                         std::unique_ptr<Krb5Interface> krb5_interface)
32
6
        : _config(config),
33
6
          _ccache_root_dir(root_path),
34
6
          _krb5_interface(std::move(krb5_interface)) {}
35
36
6
KerberosTicketCache::~KerberosTicketCache() {
37
6
    stop_periodic_refresh();
38
6
    _cleanup_context();
39
6
    if (std::filesystem::exists(_ticket_cache_path)) {
40
3
        std::filesystem::remove(_ticket_cache_path);
41
3
    }
42
6
    LOG(INFO) << "destroy kerberos ticket cache " << _ticket_cache_path
43
6
              << " with principal: " << _config.get_principal();
44
6
}
45
46
4
Status KerberosTicketCache::initialize() {
47
4
    std::lock_guard<std::mutex> lock(_mutex);
48
4
    RETURN_IF_ERROR(_init_ticket_cache_path());
49
4
    Status st = _initialize_context();
50
4
    LOG(INFO) << "initialized kerberos ticket cache " << _ticket_cache_path
51
4
              << " with principal: " << _config.get_principal()
52
4
              << " and keytab: " << _config.get_keytab_path() << ": " << st.to_string();
53
4
    return st;
54
4
}
55
56
4
Status KerberosTicketCache::_init_ticket_cache_path() {
57
4
    std::string cache_file_md5 = "doris_krb_" + _config.get_hash_code();
58
59
    // The path should be with prefix "_ccache_root_dir"
60
4
    std::filesystem::path full_path = std::filesystem::path(_ccache_root_dir) / cache_file_md5;
61
4
    full_path = std::filesystem::weakly_canonical(full_path);
62
4
    std::filesystem::path parent_path = full_path.parent_path();
63
64
4
    LOG(INFO) << "try creating kerberos ticket path: " << full_path.string()
65
4
              << " for principal: " << _config.get_principal();
66
4
    try {
67
4
        if (!std::filesystem::exists(parent_path)) {
68
            // Create the parent dir if not exists
69
0
            std::filesystem::create_directories(parent_path);
70
4
        } else {
71
            // Delete the ticket cache file if exists
72
4
            if (std::filesystem::exists(full_path)) {
73
0
                std::filesystem::remove(full_path);
74
0
            }
75
4
        }
76
77
4
        _ticket_cache_path = full_path.string();
78
4
        return Status::OK();
79
4
    } catch (const std::filesystem::filesystem_error& e) {
80
0
        return Status::InternalError("Error when setting kerberos ticket cache file: {}, {}",
81
0
                                     full_path.native(), e.what());
82
0
    } catch (const std::exception& e) {
83
0
        return Status::InternalError("Exception when setting kerberos ticket cache file: {}, {}",
84
0
                                     full_path.native(), e.what());
85
0
    } catch (...) {
86
0
        return Status::InternalError("Unknown error when setting kerberos ticket cache file: {}",
87
0
                                     full_path.native());
88
0
    }
89
4
}
90
91
3
Status KerberosTicketCache::login() {
92
3
    std::lock_guard<std::mutex> lock(_mutex);
93
94
3
    krb5_keytab keytab = nullptr;
95
3
    krb5_ccache temp_ccache = nullptr;
96
3
    try {
97
        // Open the keytab file
98
3
        RETURN_IF_ERROR(
99
3
                _krb5_interface->kt_resolve(_context, _config.get_keytab_path().c_str(), &keytab));
100
101
        // init ccache
102
3
        RETURN_IF_ERROR(
103
3
                _krb5_interface->cc_resolve(_context, _ticket_cache_path.c_str(), &temp_ccache));
104
105
        // get init creds
106
3
        krb5_get_init_creds_opt* opts = nullptr;
107
3
        RETURN_IF_ERROR(_krb5_interface->get_init_creds_opt_alloc(_context, &opts));
108
109
3
        krb5_creds creds;
110
3
        Status status = _krb5_interface->get_init_creds_keytab(_context, &creds, _principal, keytab,
111
3
                                                               0,       // start time
112
3
                                                               nullptr, // TKT service name
113
3
                                                               opts);
114
115
3
        if (!status.ok()) {
116
0
            _krb5_interface->get_init_creds_opt_free(_context, opts);
117
0
            _krb5_interface->kt_close(_context, keytab);
118
0
            if (temp_ccache) {
119
0
                _krb5_interface->cc_close(_context, temp_ccache);
120
0
            }
121
0
            return status;
122
0
        }
123
3
        _ticket_lifetime_sec = static_cast<int64_t>(creds.times.endtime) -
124
3
                               static_cast<int64_t>(creds.times.starttime);
125
126
        // init ccache file
127
3
        status = _krb5_interface->cc_initialize(_context, temp_ccache, _principal);
128
3
        if (!status.ok()) {
129
0
            _krb5_interface->free_cred_contents(_context, &creds);
130
0
            _krb5_interface->get_init_creds_opt_free(_context, opts);
131
0
            _krb5_interface->kt_close(_context, keytab);
132
0
            _krb5_interface->cc_close(_context, temp_ccache);
133
0
            return status;
134
0
        }
135
136
        // save ccache
137
3
        status = _krb5_interface->cc_store_cred(_context, temp_ccache, &creds);
138
3
        if (!status.ok()) {
139
0
            _krb5_interface->free_cred_contents(_context, &creds);
140
0
            _krb5_interface->get_init_creds_opt_free(_context, opts);
141
0
            _krb5_interface->kt_close(_context, keytab);
142
0
            _krb5_interface->cc_close(_context, temp_ccache);
143
0
            return status;
144
0
        }
145
146
        // clean
147
3
        _krb5_interface->free_cred_contents(_context, &creds);
148
3
        _krb5_interface->get_init_creds_opt_free(_context, opts);
149
3
        _krb5_interface->kt_close(_context, keytab);
150
151
        // Only set _ccache if everything succeeded
152
3
        if (_ccache) {
153
0
            _krb5_interface->cc_close(_context, _ccache);
154
0
        }
155
3
        _ccache = temp_ccache;
156
157
3
    } catch (...) {
158
0
        if (keytab) {
159
0
            _krb5_interface->kt_close(_context, keytab);
160
0
        }
161
0
        if (temp_ccache) {
162
0
            _krb5_interface->cc_close(_context, temp_ccache);
163
0
        }
164
0
        return Status::InternalError("Failed to login with kerberos");
165
0
    }
166
3
    return Status::OK();
167
3
}
168
169
0
Status KerberosTicketCache::login_with_cache() {
170
0
    std::lock_guard<std::mutex> lock(_mutex);
171
172
    // Close existing ccache if any
173
0
    if (_ccache) {
174
0
        _krb5_interface->cc_close(_context, _ccache);
175
0
        _ccache = nullptr;
176
0
    }
177
178
0
    return _krb5_interface->cc_resolve(_context, _ticket_cache_path.c_str(), &_ccache);
179
0
}
180
181
0
Status KerberosTicketCache::write_ticket_cache() {
182
0
    std::lock_guard<std::mutex> lock(_mutex);
183
184
0
    if (!_ccache) {
185
0
        return Status::InternalError("No credentials cache available");
186
0
    }
187
188
    // MIT Kerberos automatically writes to the cache file
189
    // when using the FILE: cache type
190
0
    return Status::OK();
191
0
}
192
193
2
Status KerberosTicketCache::refresh_tickets() {
194
2
    try {
195
2
        return login();
196
2
    } catch (const std::exception& e) {
197
0
        std::stringstream ss;
198
0
        ss << "Failed to refresh tickets: " << e.what();
199
0
        return Status::InternalError(ss.str());
200
0
    }
201
2
}
202
203
1
void KerberosTicketCache::start_periodic_refresh() {
204
1
    _should_stop_refresh = false;
205
1
    _refresh_thread = std::make_unique<std::thread>([this]() {
206
1
        auto refresh_interval = std::chrono::milliseconds(
207
1
                static_cast<int>(_config.get_refresh_interval_second() * 1000));
208
1
        auto sleep_duration = _refresh_thread_sleep_time;
209
1
        std::chrono::milliseconds accumulated_time(0);
210
3.77k
        while (!_should_stop_refresh) {
211
3.77k
            std::this_thread::sleep_for(sleep_duration);
212
3.77k
            accumulated_time += sleep_duration;
213
3.77k
            if (accumulated_time >= refresh_interval) {
214
1
                accumulated_time = std::chrono::milliseconds(0); // Reset accumulated time
215
1
                Status st = refresh_tickets();
216
1
                if (!st.ok()) {
217
                    // ignore and continue
218
0
                    LOG(WARNING) << st.to_string();
219
1
                } else {
220
1
                    LOG(INFO) << "refresh kerberos ticket cache: " << _ticket_cache_path
221
1
                              << ", lifetime sec: " << _ticket_lifetime_sec;
222
1
                }
223
1
            }
224
3.77k
        }
225
1
    });
226
1
}
227
228
7
void KerberosTicketCache::stop_periodic_refresh() {
229
7
    if (_refresh_thread) {
230
1
        _should_stop_refresh = true;
231
1
        _refresh_thread->join();
232
1
        _refresh_thread.reset();
233
1
    }
234
7
}
235
236
4
Status KerberosTicketCache::_initialize_context() {
237
4
    if (!_config.get_krb5_conf_path().empty()) {
238
4
        if (setenv("KRB5_CONFIG", _config.get_krb5_conf_path().c_str(), 1) != 0) {
239
0
            return Status::InvalidArgument("Failed to set KRB5_CONFIG environment variable");
240
0
        }
241
4
        LOG(INFO) << "Using custom krb5.conf: " << _config.get_krb5_conf_path();
242
4
    }
243
244
4
    RETURN_IF_ERROR(_krb5_interface->init_context(&_context));
245
246
4
    return _krb5_interface->parse_name(_context, _config.get_principal().c_str(), &_principal);
247
4
}
248
249
6
void KerberosTicketCache::_cleanup_context() {
250
6
    if (_principal) {
251
0
        _krb5_interface->free_principal(_context, _principal);
252
0
    }
253
6
    if (_ccache) {
254
0
        _krb5_interface->cc_close(_context, _ccache);
255
0
    }
256
6
    if (_context) {
257
0
        _krb5_interface->free_context(_context);
258
0
    }
259
6
}
260
261
0
std::vector<KerberosTicketInfo> KerberosTicketCache::get_ticket_info() {
262
0
    std::lock_guard<std::mutex> lock(_mutex);
263
0
    std::vector<KerberosTicketInfo> result;
264
265
0
    if (!_ccache || !_context) {
266
        // If no valid cache or context, return empty vector
267
0
        return result;
268
0
    }
269
270
0
    krb5_cc_cursor cursor;
271
0
    if (_krb5_interface->cc_start_seq_get(_context, _ccache, &cursor) != Status::OK()) {
272
0
        return result;
273
0
    }
274
275
    // Iterate through all credentials in the cache
276
0
    krb5_creds creds;
277
0
    while (_krb5_interface->cc_next_cred(_context, _ccache, &cursor, &creds) == Status::OK()) {
278
0
        KerberosTicketInfo info;
279
0
        info.principal = _config.get_principal();
280
0
        info.keytab_path = _config.get_keytab_path();
281
0
        info.start_time = static_cast<int64_t>(creds.times.starttime);
282
0
        info.expiry_time = static_cast<int64_t>(creds.times.endtime);
283
0
        info.auth_time = static_cast<int64_t>(creds.times.authtime);
284
        // minus 2,
285
        // one is shared_from_this(), the other is ref in _ticket_caches of KerberosTicketMgr
286
0
        info.use_count = shared_from_this().use_count() - 2;
287
288
        // Get the service principal name
289
0
        char* service_name = nullptr;
290
0
        if (_krb5_interface->unparse_name(_context, creds.server, &service_name) == Status::OK()) {
291
0
            info.service_principal = service_name;
292
0
            _krb5_interface->free_unparsed_name(_context, service_name);
293
0
        } else {
294
0
            info.service_principal = "Unparse Error";
295
0
        }
296
0
        info.cache_path = _ticket_cache_path;
297
0
        info.refresh_interval_second = _config.get_refresh_interval_second();
298
0
        info.hash_code = _config.get_hash_code();
299
300
0
        result.push_back(std::move(info));
301
0
        _krb5_interface->free_cred_contents(_context, &creds);
302
0
    }
303
304
0
    _krb5_interface->cc_end_seq_get(_context, _ccache, &cursor);
305
0
    return result;
306
0
}
307
308
} // namespace doris::kerberos