aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/clickhouse/src/Access/KerberosInit.cpp
blob: 772938ad9b295d987ad3e1e8e4f0afe59db558a6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#include <Access/KerberosInit.h>
#include <Common/Exception.h>
#include <Common/logger_useful.h>
#include <Poco/Logger.h>
#include <Loggers/Loggers.h>
#include <filesystem>
#include <boost/core/noncopyable.hpp>
#include <fmt/format.h>
#if USE_KRB5
#include <krb5.h>
#include <mutex>

using namespace DB;

namespace DB
{
namespace ErrorCodes
{
    extern const int KERBEROS_ERROR;
}
}

namespace
{
struct K5Data
{
    krb5_context ctx;
    krb5_ccache out_cc;
    krb5_principal me;
    char * name;
    krb5_boolean switch_to_cache;
};

/**
 * This class implements programmatic implementation of kinit.
 */
class KerberosInit : boost::noncopyable
{
public:
    void init(const String & keytab_file, const String & principal, const String & cache_name = "");
    ~KerberosInit();
private:
    struct K5Data k5 {};
    krb5_ccache defcache = nullptr;
    krb5_get_init_creds_opt * options = nullptr;
    // Credentials structure including ticket, session key, and lifetime info.
    krb5_creds my_creds {};
    krb5_keytab keytab = nullptr;
    krb5_principal defcache_princ = nullptr;
    String fmtError(krb5_error_code code) const;
};
}


String KerberosInit::fmtError(krb5_error_code code) const
{
    const char *msg;
    msg = krb5_get_error_message(k5.ctx, code);
    String fmt_error = fmt::format(" ({}, {})", code, msg);
    krb5_free_error_message(k5.ctx, msg);
    return fmt_error;
}

void KerberosInit::init(const String & keytab_file, const String & principal, const String & cache_name)
{
    auto * log = &Poco::Logger::get("KerberosInit");
    LOG_TRACE(log,"Trying to authenticate with Kerberos v5");

    krb5_error_code ret;

    const char *deftype = nullptr;

    if (!std::filesystem::exists(keytab_file))
        throw Exception(ErrorCodes::KERBEROS_ERROR, "Keytab file does not exist");

    ret = krb5_init_context(&k5.ctx);
    if (ret)
        throw Exception(ErrorCodes::KERBEROS_ERROR, "Error while initializing Kerberos 5 library ({})", ret);

    if (!cache_name.empty())
    {
        ret = krb5_cc_resolve(k5.ctx, cache_name.c_str(), &k5.out_cc);
        if (ret)
            throw Exception(ErrorCodes::KERBEROS_ERROR, "Error in resolving cache: {}", fmtError(ret));
        LOG_TRACE(log,"Resolved cache");
    }
    else
    {
        // Resolve the default cache and get its type and default principal (if it is initialized).
        ret = krb5_cc_default(k5.ctx, &defcache);
        if (ret)
            throw Exception(ErrorCodes::KERBEROS_ERROR, "Error while getting default cache: {}", fmtError(ret));
        LOG_TRACE(log,"Resolved default cache");
        deftype = krb5_cc_get_type(k5.ctx, defcache);
        if (krb5_cc_get_principal(k5.ctx, defcache, &defcache_princ) != 0)
            defcache_princ = nullptr;
    }

    // Use the specified principal name.
    ret = krb5_parse_name_flags(k5.ctx, principal.c_str(), 0, &k5.me);
    if (ret)
        throw Exception(ErrorCodes::KERBEROS_ERROR, "Error when parsing principal name ({}): {}", principal, fmtError(ret));

    // Cache related commands
    if (k5.out_cc == nullptr && krb5_cc_support_switch(k5.ctx, deftype))
    {
        // Use an existing cache for the client principal if we can.
        ret = krb5_cc_cache_match(k5.ctx, k5.me, &k5.out_cc);
        if (ret && ret != KRB5_CC_NOTFOUND)
            throw Exception(ErrorCodes::KERBEROS_ERROR, "Error while searching for cache for ({}): {}", principal, fmtError(ret));
        if (0 == ret)
        {
            LOG_TRACE(log,"Using default cache: {}", krb5_cc_get_name(k5.ctx, k5.out_cc));
            k5.switch_to_cache = 1;
        }
        else if (defcache_princ != nullptr)
        {
            // Create a new cache to avoid overwriting the initialized default cache.
            ret = krb5_cc_new_unique(k5.ctx, deftype, nullptr, &k5.out_cc);
            if (ret)
                throw Exception(ErrorCodes::KERBEROS_ERROR, "Error while generating new cache: {}", fmtError(ret));
            LOG_TRACE(log,"Using default cache: {}", krb5_cc_get_name(k5.ctx, k5.out_cc));
            k5.switch_to_cache = 1;
        }
    }

    // Use the default cache if we haven't picked one yet.
    if (k5.out_cc == nullptr)
    {
        k5.out_cc = defcache;
        defcache = nullptr;
        LOG_TRACE(log,"Using default cache: {}", krb5_cc_get_name(k5.ctx, k5.out_cc));
    }

    ret = krb5_unparse_name(k5.ctx, k5.me, &k5.name);
    if (ret)
        throw Exception(ErrorCodes::KERBEROS_ERROR, "Error when unparsing name: {}", fmtError(ret));
    LOG_TRACE(log,"Using principal: {}", k5.name);

    // Allocate a new initial credential options structure.
    ret = krb5_get_init_creds_opt_alloc(k5.ctx, &options);
    if (ret)
        throw Exception(ErrorCodes::KERBEROS_ERROR, "Error in options allocation: {}", fmtError(ret));

    // Resolve keytab
    ret = krb5_kt_resolve(k5.ctx, keytab_file.c_str(), &keytab);
    if (ret)
        throw Exception(ErrorCodes::KERBEROS_ERROR, "Error in resolving keytab ({}): {}", keytab_file, fmtError(ret));
    LOG_TRACE(log,"Using keytab: {}", keytab_file);

    // Set an output credential cache in initial credential options.
    ret = krb5_get_init_creds_opt_set_out_ccache(k5.ctx, options, k5.out_cc);
    if (ret)
        throw Exception(ErrorCodes::KERBEROS_ERROR, "Error in setting output credential cache: {}", fmtError(ret));

    // Action: init or renew
    LOG_TRACE(log,"Trying to renew credentials");
    memset(&my_creds, 0, sizeof(my_creds));
    // Get renewed credential from KDC using an existing credential from output cache.
    ret = krb5_get_renewed_creds(k5.ctx, &my_creds, k5.me, k5.out_cc, nullptr);
    if (ret)
    {
        LOG_TRACE(log,"Renew failed {}", fmtError(ret));
        LOG_TRACE(log,"Trying to get initial credentials");
        // Request KDC for an initial credentials using keytab.
        ret = krb5_get_init_creds_keytab(k5.ctx, &my_creds, k5.me, keytab, 0, nullptr, options);
        if (ret)
            throw Exception(ErrorCodes::KERBEROS_ERROR, "Error in getting initial credentials: {}", fmtError(ret));
        else
            LOG_TRACE(log,"Got initial credentials");
    }
    else
    {
        LOG_TRACE(log,"Successful renewal");
        // Initialize a credential cache. Destroy any existing contents of cache and initialize it for the default principal.
        ret = krb5_cc_initialize(k5.ctx, k5.out_cc, k5.me);
        if (ret)
            throw Exception(ErrorCodes::KERBEROS_ERROR, "Error when initializing cache: {}", fmtError(ret));
        LOG_TRACE(log,"Initialized cache");
        // Store credentials in a credential cache.
        ret = krb5_cc_store_cred(k5.ctx, k5.out_cc, &my_creds);
        if (ret)
            LOG_TRACE(log,"Error while storing credentials");
        LOG_TRACE(log,"Stored credentials");
    }

    if (k5.switch_to_cache)
    {
        // Make a credential cache the primary cache for its collection.
        ret = krb5_cc_switch(k5.ctx, k5.out_cc);
        if (ret)
            throw Exception(ErrorCodes::KERBEROS_ERROR, "Error while switching to new cache: {}", fmtError(ret));
    }

    LOG_TRACE(log,"Authenticated to Kerberos v5");
}

KerberosInit::~KerberosInit()
{
    if (k5.ctx)
    {
        if (defcache)
            krb5_cc_close(k5.ctx, defcache);
        krb5_free_principal(k5.ctx, defcache_princ);

        if (options)
            krb5_get_init_creds_opt_free(k5.ctx, options);
        if (my_creds.client == k5.me)
            my_creds.client = nullptr;
        krb5_free_cred_contents(k5.ctx, &my_creds);
        if (keytab)
            krb5_kt_close(k5.ctx, keytab);

        krb5_free_unparsed_name(k5.ctx, k5.name);
        krb5_free_principal(k5.ctx, k5.me);
        if (k5.out_cc != nullptr)
            krb5_cc_close(k5.ctx, k5.out_cc);
        krb5_free_context(k5.ctx);
    }
}

void kerberosInit(const String & keytab_file, const String & principal, const String & cache_name)
{
    // Using mutex to prevent cache file corruptions
    static std::mutex kinit_mtx;
    std::lock_guard lck(kinit_mtx);
    KerberosInit k_init;
    k_init.init(keytab_file, principal, cache_name);
}
#endif // USE_KRB5