| // 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 "kinit_context.h" |
| |
| #include <boost/asio/deadline_timer.hpp> |
| #include <krb5/krb5.h> |
| #include <stdint.h> |
| #include <memory> |
| #include <mutex> |
| #include <new> |
| |
| #include "boost/asio/basic_deadline_timer.hpp" |
| #include "boost/asio/detail/impl/epoll_reactor.hpp" |
| #include "boost/asio/detail/impl/timer_queue_ptime.ipp" |
| #include "boost/date_time/posix_time/posix_time_duration.hpp" |
| #include "boost/system/error_code.hpp" |
| #include "fmt/core.h" |
| #include "utils/defer.h" |
| #include "utils/error_code.h" |
| #include "utils/filesystem.h" |
| #include "utils/flags.h" |
| #include "utils/fmt_logging.h" |
| #include "utils/rand.h" |
| #include "utils/shared_io_service.h" |
| #include "utils/singleton.h" |
| #include "utils/strings.h" |
| #include "utils/time_utils.h" |
| |
| namespace dsn { |
| namespace security { |
| class kinit_context; |
| |
| DSN_DECLARE_bool(enable_auth); |
| DSN_DECLARE_bool(enable_zookeeper_kerberos); |
| |
| #define KRB5_RETURN_NOT_OK(err, msg) \ |
| do { \ |
| krb5_error_code __err_code__ = (err); \ |
| if (__err_code__ != 0) { \ |
| return krb5_call_to_errors(__err_code__, (msg)); \ |
| } \ |
| } while (0); |
| |
| DSN_DEFINE_string(security, krb5_keytab, "", "absolute path of keytab file"); |
| DSN_DEFINE_string(security, krb5_config, "", "absolute path of krb5_config file"); |
| DSN_DEFINE_string(security, krb5_principal, "", "kerberos principal"); |
| DSN_DEFINE_string(security, service_fqdn, "", "the fully qualified domain name of the server"); |
| DSN_DEFINE_string(security, service_name, "", "service name"); |
| |
| // Attention: we can't do these check work by `DSN_DEFINE_validator`, because somebody may don't |
| // want to use security, so these configuration may not setted. In this situation, these checks |
| // will not pass. |
| error_s check_configuration() |
| { |
| CHECK(FLAGS_enable_auth || FLAGS_enable_zookeeper_kerberos, |
| "There is no need to check configuration if FLAGS_enable_auth" |
| " and FLAGS_enable_zookeeper_kerberos both are not true"); |
| |
| if (utils::is_empty(FLAGS_krb5_keytab) || !utils::filesystem::file_exists(FLAGS_krb5_keytab)) { |
| return error_s::make(ERR_INVALID_PARAMETERS, |
| fmt::format("invalid keytab file \"{}\"", FLAGS_krb5_keytab)); |
| } |
| |
| if (utils::is_empty(FLAGS_krb5_config) || !utils::filesystem::file_exists(FLAGS_krb5_config)) { |
| return error_s::make(ERR_INVALID_PARAMETERS, |
| fmt::format("invalid krb5 config file \"{}\"", FLAGS_krb5_config)); |
| } |
| |
| if (utils::is_empty(FLAGS_krb5_principal)) { |
| return error_s::make(ERR_INVALID_PARAMETERS, "empty principal"); |
| } |
| |
| return error_s::ok(); |
| } |
| |
| class kinit_context : public utils::singleton<kinit_context> |
| { |
| public: |
| // implementation of 'kinit -k -t <keytab_file> <principal>' |
| error_s kinit(); |
| // If kinit has been executed outside the program, then directly obtain the principal |
| // information of the unix account for permission verification. |
| error_s get_principal_without_kinit(); |
| const std::string &username() const { return _user_name; } |
| |
| private: |
| kinit_context() = default; |
| ~kinit_context(); |
| |
| // init kerberos context |
| void init_krb5_ctx(); |
| |
| // get _user_name from _principal |
| error_s parse_username_from_principal(); |
| |
| // get or renew credentials from KDC and store it to _ccache |
| error_s get_credentials(); |
| void schedule_renew_credentials(); |
| int32_t get_next_renew_interval(); |
| |
| error_s wrap_krb5_err(krb5_error_code krb5_err, const std::string &msg); |
| error_s krb5_call_to_errors(krb5_error_code krb5_code, const std::string &prefix_msg); |
| |
| private: |
| krb5_context _krb5_context; |
| // krb5 principal |
| krb5_principal _principal; |
| krb5_keytab _keytab; |
| // credential cache |
| // TODO(zlw): reuse ticket from ccache |
| krb5_ccache _ccache; |
| krb5_get_init_creds_opt *_opt = nullptr; |
| |
| // principal and username that logged in as, this determines "who I am" |
| std::string _user_name; |
| |
| uint64_t _cred_expire_timestamp; |
| std::shared_ptr<boost::asio::deadline_timer> _timer; |
| |
| friend class utils::singleton<kinit_context>; |
| }; |
| |
| kinit_context::~kinit_context() { krb5_get_init_creds_opt_free(_krb5_context, _opt); } |
| |
| error_s kinit_context::kinit() |
| { |
| error_s err = check_configuration(); |
| if (!err.is_ok()) { |
| return err; |
| } |
| |
| // create a krb5 library context. |
| init_krb5_ctx(); |
| |
| // convert a string principal name to a krb5_principal structure. |
| KRB5_RETURN_NOT_OK(krb5_parse_name(_krb5_context, FLAGS_krb5_principal, &_principal), |
| "couldn't parse principal"); |
| |
| // get _user_name from _principal |
| RETURN_NOT_OK(parse_username_from_principal()); |
| |
| // get a handle for a key table. |
| KRB5_RETURN_NOT_OK(krb5_kt_resolve(_krb5_context, FLAGS_krb5_keytab, &_keytab), |
| "couldn't resolve keytab file"); |
| |
| // acquire credential cache handle |
| KRB5_RETURN_NOT_OK(krb5_cc_default(_krb5_context, &_ccache), |
| "couldn't acquire credential cache handle"); |
| |
| // initialize credential cache |
| KRB5_RETURN_NOT_OK(krb5_cc_initialize(_krb5_context, _ccache, _principal), |
| "initialize credential cache failed"); |
| |
| // allocate a new initial credential options structure |
| KRB5_RETURN_NOT_OK(krb5_get_init_creds_opt_alloc(_krb5_context, &_opt), |
| "alloc get_init_creds_opt structure failed"); |
| |
| // get and schedule to renew credentials from KDC and store it into _ccache |
| RETURN_NOT_OK(get_credentials()); |
| schedule_renew_credentials(); |
| |
| return error_s::ok(); |
| } |
| |
| // obtain _principal info under the current unix account for permission verification. |
| error_s kinit_context::get_principal_without_kinit() |
| { |
| // get krb5_ctx |
| init_krb5_ctx(); |
| |
| // acquire credential cache handle |
| KRB5_RETURN_NOT_OK(krb5_cc_default(_krb5_context, &_ccache), |
| "couldn't acquire credential cache handle"); |
| |
| // get '_principal' from '_ccache' |
| KRB5_RETURN_NOT_OK(krb5_cc_get_principal(_krb5_context, _ccache, &_principal), |
| "get principal from cache failed"); |
| |
| // get '_user_name' from '_principal' |
| RETURN_NOT_OK(parse_username_from_principal()); |
| |
| return error_s::ok(); |
| } |
| |
| void kinit_context::init_krb5_ctx() |
| { |
| static std::once_flag once; |
| std::call_once(once, [&]() { |
| int64_t err = krb5_init_context(&_krb5_context); |
| CHECK_EQ(err, 0); |
| }); |
| } |
| |
| error_s kinit_context::parse_username_from_principal() |
| { |
| // Attention: here we just assume the length of username must be little than 1024 |
| const uint16_t BUF_LEN = 1024; |
| char buf[BUF_LEN]; |
| krb5_error_code err = krb5_aname_to_localname(_krb5_context, _principal, sizeof(buf), buf); |
| |
| // KRB5_LNAME_NOTRANS means no translation available for requested principal |
| if (err == KRB5_LNAME_NOTRANS) { |
| if (_principal->length > 0) { |
| int cnt = 0; |
| while (cnt < _principal->length) { |
| std::string tname; |
| tname.assign((const char *)_principal->data[cnt].data, |
| _principal->data[cnt].length); |
| if (!_user_name.empty()) { |
| _user_name += '/'; |
| } |
| _user_name += tname; |
| cnt++; |
| } |
| return error_s::ok(); |
| } |
| return error_s::make(ERR_KRB5_INTERNAL, "parse username from principal failed"); |
| } |
| |
| // KRB5_CONFIG_NOTENUFSPACE means BUF_LEN is not enough |
| if (err == KRB5_CONFIG_NOTENUFSPACE) { |
| return error_s::make(ERR_KRB5_INTERNAL, fmt::format("username is larger than {}", BUF_LEN)); |
| } |
| KRB5_RETURN_NOT_OK(err, "krb5 parse aname to localname failed"); |
| |
| if (utils::is_empty(buf)) { |
| return error_s::make(ERR_KRB5_INTERNAL, "empty username"); |
| } |
| |
| _user_name.assign((const char *)buf); |
| return error_s::ok(); |
| } |
| |
| error_s kinit_context::get_credentials() |
| { |
| krb5_creds creds; |
| error_s err = error_s::ok(); |
| |
| // get initial credentials using a key table |
| // Notice: the contents of a krb5_creds structure need to be freed by ourselves |
| err = wrap_krb5_err(krb5_get_init_creds_keytab(_krb5_context, |
| &creds, |
| _principal, |
| _keytab, |
| 0 /*valid from now*/, |
| nullptr /*empty TKT service name*/, |
| _opt), |
| "get_init_cred"); |
| if (!err.is_ok()) { |
| LOG_WARNING("get credentials of {} from KDC failed, reason({})", |
| FLAGS_krb5_principal, |
| err.description()); |
| return err; |
| } |
| auto cleanup = dsn::defer([&]() { krb5_free_cred_contents(_krb5_context, &creds); }); |
| |
| // store credentials into _ccache. |
| err = wrap_krb5_err(krb5_cc_store_cred(_krb5_context, _ccache, &creds), "store_cred"); |
| if (!err.is_ok()) { |
| LOG_WARNING("store credentials of {} to cache failed, err({})", |
| FLAGS_krb5_principal, |
| err.description()); |
| return err; |
| } |
| |
| _cred_expire_timestamp = creds.times.endtime; |
| LOG_INFO("get credentials of {} from KDC ok, expires at {}", |
| FLAGS_krb5_principal, |
| utils::time_s_to_date_time(_cred_expire_timestamp)); |
| return err; |
| } |
| |
| void kinit_context::schedule_renew_credentials() |
| { |
| int64_t renew_gap = get_next_renew_interval(); |
| LOG_INFO("schedule to renew credentials in {} seconds later", renew_gap); |
| |
| // why don't we use timers in rDSN framework? |
| // 1. currently the rdsn framework may not started yet. |
| // 2. the rdsn framework is used for codes of a service_app, |
| // not for codes under service_app |
| if (nullptr == _timer) { |
| _timer.reset(new boost::asio::deadline_timer(tools::shared_io_service::instance().ios)); |
| } |
| _timer->expires_from_now(boost::posix_time::seconds(renew_gap)); |
| _timer->async_wait([this](const boost::system::error_code &err) { |
| if (!err.failed()) { |
| get_credentials(); |
| schedule_renew_credentials(); |
| } else if (err == boost::system::errc::operation_canceled) { |
| LOG_WARNING("the renew credentials timer is cancelled"); |
| } else { |
| CHECK(false, "unhandled error({})", err.message()); |
| } |
| }); |
| } |
| |
| int32_t kinit_context::get_next_renew_interval() |
| { |
| int32_t time_remaining = _cred_expire_timestamp - utils::get_current_physical_time_s(); |
| |
| // If the time remaining between now and ticket expiry is: |
| // * > 10 minutes: We attempt to reacquire the ticket between 5 seconds and 5 minutes before |
| // the |
| // ticket expires. |
| // * 5 - 10 minutes: We attempt to reacquire the ticket betwen 5 seconds and 1 minute before the |
| // ticket expires. |
| // * < 5 minutes: Attempt to reacquire the ticket every 'time_remaining'. |
| // The jitter is added to make sure that every server doesn't flood the KDC at the same time. |
| if (time_remaining > 600) { |
| return time_remaining - rand::next_u32(5, 300); |
| } else if (time_remaining > 300) { |
| return time_remaining - rand::next_u32(5, 60); |
| } |
| return time_remaining; |
| } |
| |
| // switch krb5_error_code to error_s |
| error_s kinit_context::krb5_call_to_errors(krb5_error_code krb5_code, const std::string &prefix_msg) |
| { |
| std::string msg = prefix_msg; |
| |
| const char *error_msg = krb5_get_error_message(_krb5_context, krb5_code); |
| msg += error_msg; |
| krb5_free_error_message(_krb5_context, error_msg); |
| |
| return error_s::make(ERR_KRB5_INTERNAL, msg); |
| } |
| |
| error_s kinit_context::wrap_krb5_err(krb5_error_code krb5_err, const std::string &msg) |
| { |
| error_s result_err; |
| if (krb5_err != 0) { |
| result_err = krb5_call_to_errors(krb5_err, msg); |
| } else { |
| result_err = error_s::ok(); |
| } |
| |
| return result_err; |
| } |
| |
| error_s run_kinit() { return kinit_context::instance().kinit(); } |
| |
| error_s run_get_principal_without_kinit() |
| { |
| return kinit_context::instance().get_principal_without_kinit(); |
| } |
| |
| const std::string &get_username() { return kinit_context::instance().username(); } |
| } // namespace security |
| } // namespace dsn |