576 lines
24 KiB
C++
576 lines
24 KiB
C++
/*
|
|
* Copyright (C) 2025 ScyllaDB
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
*/
|
|
|
|
#include <stdexcept>
|
|
|
|
#include <boost/regex.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
#include "db/config.hh"
|
|
#include "utils/log.hh"
|
|
#include "utils/hash.hh"
|
|
#include "utils/http.hh"
|
|
#include "utils/rjson.hh"
|
|
#include "utils/base64.hh"
|
|
#include "utils/loading_cache.hh"
|
|
#include "utils/rest/client.hh"
|
|
#include "utils/azure/identity/exceptions.hh"
|
|
#include "utils/azure/identity/default_credentials.hh"
|
|
#include "utils/azure/identity/service_principal_credentials.hh"
|
|
#include "azure_host.hh"
|
|
#include "encryption.hh"
|
|
#include "encryption_exceptions.hh"
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
static logging::logger azlog("azure_vault");
|
|
|
|
namespace encryption {
|
|
|
|
vault_error vault_error::make_error(const http::reply::status_type& status, std::string_view result) {
|
|
const auto& jres = rjson::parse(result);
|
|
const auto& error_details = rjson::get(jres, ERROR_KEY);
|
|
return vault_error(
|
|
status,
|
|
rjson::get<std::string>(error_details, ERROR_CODE_KEY),
|
|
rjson::get<std::string>(error_details, ERROR_MESSAGE_KEY)
|
|
);
|
|
}
|
|
|
|
class vault_log_filter : public rest::http_log_filter {
|
|
public:
|
|
enum class op_type {
|
|
wrapkey,
|
|
unwrapkey,
|
|
};
|
|
|
|
explicit vault_log_filter(op_type op) : _op(op) {}
|
|
|
|
string_opt filter_header(std::string_view name, std::string_view value) const override {
|
|
if (boost::iequals(name, "Authorization") && value.starts_with("Bearer")) {
|
|
return REDACTED_VALUE;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
string_opt filter_body(body_type type, std::string_view body) const override {
|
|
if ((_op == op_type::wrapkey && type == body_type::request)
|
|
|| (_op == op_type::unwrapkey && type == body_type::response)) {
|
|
auto j = rjson::parse(body);
|
|
auto val = rjson::find(j, "value");
|
|
if (val) {
|
|
val->SetString(REDACTED_VALUE);
|
|
return rjson::print(j);
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
private:
|
|
op_type _op;
|
|
};
|
|
|
|
class azure_host::impl {
|
|
public:
|
|
static inline constexpr std::chrono::milliseconds default_expiry = 600s;
|
|
static inline constexpr std::chrono::milliseconds default_refresh = 1200s;
|
|
|
|
impl(encryption_context*, const std::string& name, const host_options& options);
|
|
future<> init();
|
|
const host_options& options() const;
|
|
future<key_and_id_type> get_or_create_key(const key_info&, const option_override* = nullptr);
|
|
future<key_ptr> get_key_by_id(const id_type&, const key_info&);
|
|
private:
|
|
encryption_context* _ctxt;
|
|
const std::string _name;
|
|
const std::string _log_prefix;
|
|
const host_options _options;
|
|
std::unique_ptr<azure::credentials> _credentials;
|
|
bool _initialized;
|
|
|
|
struct attr_cache_key {
|
|
seastar::sstring master_key;
|
|
key_info info;
|
|
bool operator==(const attr_cache_key& v) const = default;
|
|
};
|
|
|
|
struct attr_cache_key_hash {
|
|
size_t operator()(const attr_cache_key& k) const {
|
|
return utils::tuple_hash()(std::tie(k.master_key, k.info.len));
|
|
}
|
|
};
|
|
|
|
friend struct fmt::formatter<attr_cache_key>;
|
|
|
|
struct id_cache_key {
|
|
azure_host::id_type id;
|
|
bool operator==(const id_cache_key& v) const = default;
|
|
};
|
|
|
|
struct id_cache_key_hash {
|
|
size_t operator()(const id_cache_key& k) const {
|
|
return std::hash<azure_host::id_type>()(k.id);
|
|
}
|
|
};
|
|
|
|
friend struct fmt::formatter<id_cache_key>;
|
|
|
|
template<typename Key, typename Value, typename Hash>
|
|
using cache_type = utils::loading_cache<
|
|
Key,
|
|
Value,
|
|
2,
|
|
utils::loading_cache_reload_enabled::yes,
|
|
utils::simple_entry_size<Value>,
|
|
Hash
|
|
>;
|
|
cache_type<attr_cache_key, key_and_id_type, attr_cache_key_hash> _attr_cache;
|
|
cache_type<id_cache_key, bytes, id_cache_key_hash> _id_cache;
|
|
|
|
static constexpr char AKV_HOST_TEMPLATE[] = "{}.vault.azure.net";
|
|
static constexpr char AKV_PATH_TEMPLATE[] = "/keys/{}/{}/{}?api-version=7.4";
|
|
static constexpr char AKV_LATEST_VERSION[] = ""; // an empty version denotes the latest
|
|
static constexpr char AKV_WRAPKEY_OP[] = "wrapkey";
|
|
static constexpr char AKV_UNWRAPKEY_OP[] = "unwrapkey";
|
|
static constexpr char AKV_ENCRYPTION_ALG[] = "RSA-OAEP-256";
|
|
static constexpr char AKV_TOKEN_RESOURCE_URI[] = "https://vault.azure.net"; // no trailing slash
|
|
|
|
static std::tuple<std::string, std::string> parse_key(std::string_view);
|
|
static std::tuple<std::string, std::string, unsigned> parse_vault(std::string_view);
|
|
future<shared_ptr<tls::certificate_credentials>> make_creds();
|
|
future<rjson::value> send_request(const sstring& host, unsigned port, bool use_https, const sstring& path, const rjson::value& body, const rest::http_log_filter& filter);
|
|
future<rjson::value> send_request_with_retry(const sstring& host, unsigned port, bool use_https, const sstring& path, const rjson::value& body, const rest::http_log_filter& filter);
|
|
future<key_and_id_type> create_key(const attr_cache_key&);
|
|
future<bytes> find_key(const id_cache_key&);
|
|
};
|
|
|
|
azure_host::impl::impl(encryption_context* ctxt, const std::string& name, const azure_host::host_options& options)
|
|
: _ctxt(ctxt)
|
|
, _name(name)
|
|
, _log_prefix(fmt::format("AzureVault:{}", name))
|
|
, _options(options)
|
|
, _credentials()
|
|
, _initialized(false)
|
|
, _attr_cache(utils::loading_cache_config{
|
|
.max_size = std::numeric_limits<size_t>::max(),
|
|
.expiry = options.key_cache_expiry.value_or(default_expiry),
|
|
.refresh = options.key_cache_refresh.value_or(default_refresh)}, azlog, std::bind_front(&impl::create_key, this))
|
|
, _id_cache(utils::loading_cache_config{
|
|
.max_size = std::numeric_limits<size_t>::max(),
|
|
.expiry = options.key_cache_expiry.value_or(default_expiry),
|
|
.refresh = options.key_cache_refresh.value_or(default_refresh)}, azlog, std::bind_front(&impl::find_key, this))
|
|
{
|
|
if (!_options.tenant_id.empty() && !_options.client_id.empty() && (!_options.client_secret.empty() || !_options.client_cert.empty())) {
|
|
_credentials = std::make_unique<azure::service_principal_credentials>(
|
|
options.tenant_id, options.client_id, options.client_secret, options.client_cert,
|
|
options.authority, options.truststore, options.priority_string, _log_prefix);
|
|
return;
|
|
}
|
|
azlog.info("[{}] No credentials configured. Falling back to default credentials.", _log_prefix);
|
|
_credentials = std::make_unique<azure::default_credentials>(
|
|
azure::default_credentials::all_sources, _options.imds_endpoint,
|
|
_options.truststore, _options.priority_string, _log_prefix);
|
|
}
|
|
|
|
/**
|
|
* Wraps exceptions to encryption::base_error exceptions.
|
|
* Should be used in all public methods.
|
|
*/
|
|
template <typename T, typename Callable>
|
|
static future<T> wrap_exceptions(const std::string& context, Callable&& func) {
|
|
try {
|
|
co_return co_await func();
|
|
} catch (base_error&) {
|
|
throw;
|
|
} catch (const std::invalid_argument& e) {
|
|
std::throw_with_nested(configuration_error(fmt::format("{}: {}", context, e.what())));
|
|
} catch (const rjson::malformed_value& e) {
|
|
std::throw_with_nested(malformed_response_error(fmt::format("{}: {}", context, e.what())));
|
|
} catch (...) {
|
|
std::throw_with_nested(service_error(fmt::format("{}: {}", context, std::current_exception())));
|
|
}
|
|
}
|
|
|
|
future<> azure_host::impl::init() {
|
|
if (_initialized) {
|
|
co_return;
|
|
}
|
|
if (_options.master_key.empty()) {
|
|
azlog.info("[{}] No master key configured. Not verifying.", _log_prefix);
|
|
co_return;
|
|
}
|
|
azlog.info("[{}] Verifying access to master key {}", _log_prefix, _options.master_key);
|
|
co_await wrap_exceptions<void>("init", [this] -> future<> {
|
|
azlog.debug("[{}] Wrapping a dummy key", _log_prefix);
|
|
attr_cache_key k{
|
|
.master_key = _options.master_key,
|
|
.info = key_info{ .alg = "AES", .len = 128 },
|
|
};
|
|
auto [key, id] = co_await create_key(k);
|
|
azlog.debug("[{}] Unwrapping the dummy key", _log_prefix);
|
|
auto data = co_await find_key({ .id = id });
|
|
if (key->key() != data) {
|
|
throw service_error(fmt::format("[{}] Key verification failed", _log_prefix));
|
|
}
|
|
_initialized = true;
|
|
});
|
|
}
|
|
|
|
const azure_host::host_options& azure_host::impl::options() const {
|
|
return _options;
|
|
}
|
|
|
|
template<typename T, typename C>
|
|
static T get_option(const encryption::azure_host::option_override* oov, std::optional<T> C::* f, const T& def) {
|
|
if (oov) {
|
|
return (oov->*f).value_or(def);
|
|
}
|
|
return def;
|
|
};
|
|
|
|
future<azure_host::key_and_id_type> azure_host::impl::get_or_create_key(const key_info& info, const option_override* oov) {
|
|
attr_cache_key key {
|
|
.master_key = get_option(oov, &option_override::master_key, _options.master_key),
|
|
.info = info,
|
|
};
|
|
|
|
if (key.master_key.empty()) {
|
|
throw configuration_error(fmt::format("[{}] No master key set in azure host config or encryption attributes", _log_prefix));
|
|
}
|
|
co_return co_await wrap_exceptions<key_and_id_type>("get_or_create_key", [this, &key] -> future<key_and_id_type> {
|
|
co_return co_await _attr_cache.get(key);
|
|
});
|
|
}
|
|
|
|
future<azure_host::key_ptr> azure_host::impl::get_key_by_id(const azure_host::id_type& id, const key_info& info) {
|
|
id_cache_key key { .id = id };
|
|
co_return co_await wrap_exceptions<key_ptr>("get_key_by_id", [this, &key, &info] -> future<key_ptr> {
|
|
auto data = co_await _id_cache.get(key);
|
|
co_return make_shared<symmetric_key>(info, data);
|
|
});
|
|
}
|
|
|
|
std::tuple<std::string, std::string> azure_host::impl::parse_key(std::string_view spec) {
|
|
auto i = spec.find_last_of('/');
|
|
if (i == std::string_view::npos) {
|
|
throw std::invalid_argument(fmt::format("Invalid master key spec '{}'. Must be in format <vaultname>/<keyname>", spec));
|
|
}
|
|
if (i >= spec.size() - 1) {
|
|
throw std::invalid_argument(fmt::format("Invalid master key spec '{}'. Key name is missing. Expected format: <vaultname>/<keyname>", spec));
|
|
}
|
|
return std::make_tuple(std::string(spec.substr(0, i)), std::string(spec.substr(i + 1)));
|
|
}
|
|
|
|
std::tuple<std::string, std::string, unsigned> azure_host::impl::parse_vault(std::string_view vault) {
|
|
static const boost::regex vault_name_re(R"([a-zA-Z0-9-]+)");
|
|
|
|
boost::smatch match;
|
|
std::string tmp{vault};
|
|
|
|
if (boost::regex_match(tmp, match, vault_name_re)) {
|
|
// If the vault is just a name, use the default Azure Key Vault endpoint.
|
|
return {"https", fmt::format(AKV_HOST_TEMPLATE, vault), 443};
|
|
}
|
|
|
|
try {
|
|
auto info = utils::http::parse_simple_url(tmp);
|
|
return {info.scheme, info.host, info.port};
|
|
} catch (...) {
|
|
std::throw_with_nested(std::invalid_argument(fmt::format("Invalid vault '{}'. Must be either a name or an endpoint in format: http(s)://<host>[:port]", vault)));
|
|
}
|
|
}
|
|
|
|
future<shared_ptr<tls::certificate_credentials>> azure_host::impl::make_creds() {
|
|
auto creds = ::make_shared<tls::certificate_credentials>();
|
|
if (!_options.priority_string.empty()) {
|
|
creds->set_priority_string(_options.priority_string);
|
|
} else {
|
|
creds->set_priority_string(db::config::default_tls_priority);
|
|
}
|
|
if (!_options.truststore.empty()) {
|
|
co_await creds->set_x509_trust_file(_options.truststore, seastar::tls::x509_crt_format::PEM);
|
|
} else {
|
|
co_await creds->set_system_trust();
|
|
}
|
|
co_return creds;
|
|
}
|
|
|
|
/**
|
|
* @brief Retries for transient errors.
|
|
*
|
|
* Retries are performed for 401, 408, 429, 500, 502, 503, and 504 errors.
|
|
* 401 _may_ indicate an edge case where the cached token expired during the request. As such, it is retried immediately.
|
|
* The rest of the error codes are taken from the generic retry policy of the Azure C++ SDK [1] and they follow an exponential backoff strategy.
|
|
* Three retries are attempted in total.
|
|
* The latencies between retries are: 100, 200 and 400 milliseconds.
|
|
*
|
|
* This retry policy is destined only for short-lived transient errors.
|
|
* Transient errors that require higher delays should be handled by the upper layers.
|
|
* Persistent throttling (429) errors are not expected, and they likely indicate a misconfiguration.
|
|
*
|
|
* [1] https://github.com/Azure/azure-sdk-for-cpp/blob/126452efd30860263398a152f11f337007f529f4/sdk/core/azure-core/inc/azure/core/http/policies/policy.hpp#L133
|
|
*/
|
|
future<rjson::value> azure_host::impl::send_request_with_retry(const sstring& host, unsigned port, bool use_https, const sstring& path, const rjson::value& body, const rest::http_log_filter& filter) {
|
|
constexpr int MAX_RETRIES = 3;
|
|
constexpr std::chrono::milliseconds DELTA_BACKOFF {100};
|
|
std::chrono::milliseconds backoff;
|
|
|
|
int retries = 0;
|
|
while (true) {
|
|
try {
|
|
co_return co_await send_request(host, port, use_https, path, body, filter);
|
|
} catch (azure::auth_error& e) {
|
|
std::throw_with_nested(permission_error(fmt::format("{}/{}", host, path)));
|
|
} catch (vault_error& e) {
|
|
auto status = e.status();
|
|
|
|
if (retries >= MAX_RETRIES) {
|
|
if (status == http::reply::status_type::unauthorized) {
|
|
std::throw_with_nested(permission_error(fmt::format("{}/{}", host, path)));
|
|
} else {
|
|
std::throw_with_nested(service_error(fmt::format("{}/{}", host, path)));
|
|
}
|
|
}
|
|
|
|
// Always retry if the request is unauthorized, to catch races where
|
|
// the token expired while making the request. This is not optimal,
|
|
// as an unauthorized response may have a non-retryable cause, but
|
|
// there is no way to tell from the error code, and the error message
|
|
// is not meant for decision making.
|
|
if (status == http::reply::status_type::unauthorized) {
|
|
azlog.debug("[{}] {}/{}: Request failed with status {}. Reason: {}. Retrying...",
|
|
_log_prefix, host, path, static_cast<int>(status), e.what());
|
|
retries++;
|
|
continue;
|
|
}
|
|
|
|
bool should_retry =
|
|
status == http::reply::status_type::request_timeout ||
|
|
status == http::reply::status_type::too_many_requests ||
|
|
status == http::reply::status_type::internal_server_error ||
|
|
status == http::reply::status_type::bad_gateway ||
|
|
status == http::reply::status_type::service_unavailable ||
|
|
status == http::reply::status_type::gateway_timeout;
|
|
|
|
if (!should_retry) {
|
|
std::throw_with_nested(service_error(fmt::format("{}/{}", host, path)));
|
|
}
|
|
|
|
backoff = DELTA_BACKOFF * (1 << retries);
|
|
azlog.debug("[{}] {}/{}: Request failed with status {}. Reason: {}. Retrying in {} ms...",
|
|
_log_prefix, host, path, static_cast<int>(status), e.what(), backoff.count());
|
|
retries++;
|
|
} catch (...) {
|
|
std::throw_with_nested(network_error(fmt::format("{}/{}", host, path)));
|
|
}
|
|
co_await seastar::sleep(backoff);
|
|
}
|
|
}
|
|
|
|
future<rjson::value> azure_host::impl::send_request(const sstring& host, unsigned port, bool use_https, const sstring& path, const rjson::value& body, const rest::http_log_filter& filter) {
|
|
auto token = co_await _credentials->get_access_token(AKV_TOKEN_RESOURCE_URI);
|
|
|
|
shared_ptr<tls::certificate_credentials> creds;
|
|
std::optional<seastar::tls::tls_options> options;
|
|
|
|
if (use_https) {
|
|
creds = co_await make_creds();
|
|
// Do not wait when terminating the TLS connection. TLS close_notify
|
|
// alerts are ignored by the Key Vault service.
|
|
//
|
|
// Also, do not use numeric hosts as SNI hostnames. SNI works only with DNS hostnames.
|
|
// This by extension also disables hostname validation, but that's fine; numeric hosts
|
|
// may appear only in testing.
|
|
bool is_numeric_host = seastar::net::inet_address::parse_numerical(host).has_value();
|
|
sstring server_name = is_numeric_host ? sstring{} : host;
|
|
options = { .server_name = server_name, .bye_timeout = std::chrono::seconds(0) };
|
|
}
|
|
|
|
rest::httpclient client(host, port, std::move(creds), options);
|
|
client.target(path);
|
|
client.method(httpd::operation_type::POST);
|
|
client.add_header("Authorization", fmt::format("Bearer {}", token.token));
|
|
client.content("application/json", std::move(rjson::print(body)));
|
|
|
|
azlog.trace("Sending request: {}", rest::redacted_request_type{ client.request(), filter });
|
|
|
|
auto res = co_await client.send();
|
|
if (res.result() == http::reply::status_type::ok) {
|
|
azlog.trace("Got response: {}", rest::redacted_result_type{ res, filter });
|
|
co_return rjson::parse(res.body());
|
|
} else {
|
|
azlog.trace("Got unexpected response: {}", rest::redacted_result_type{ res, filter });
|
|
throw vault_error::make_error(res.result(), res.body());
|
|
}
|
|
}
|
|
|
|
future<azure_host::key_and_id_type> azure_host::impl::create_key(const attr_cache_key& k) {
|
|
static const vault_log_filter filter{vault_log_filter::op_type::wrapkey};
|
|
auto& info = k.info;
|
|
if (_ctxt && this_shard_id() != 0) {
|
|
auto [data, id] = co_await smp::submit_to(0, [this, k]() -> future<std::tuple<bytes, id_type>> {
|
|
auto host = _ctxt->get_azure_host(_name);
|
|
auto [key, id] = co_await host->_impl->_attr_cache.get(k);
|
|
co_return std::make_tuple(key != nullptr ? key->key() : bytes{}, id);
|
|
});
|
|
co_return key_and_id_type{
|
|
data.empty() ? nullptr : make_shared<symmetric_key>(info, data),
|
|
id
|
|
};
|
|
}
|
|
azlog.debug("[{}] Creating new key: {}", _log_prefix, info);
|
|
auto [vault, keyname] = parse_key(k.master_key);
|
|
auto [scheme, host, port] = parse_vault(vault);
|
|
auto key = make_shared<symmetric_key>(info);
|
|
auto path = fmt::format(AKV_PATH_TEMPLATE, keyname, AKV_LATEST_VERSION, AKV_WRAPKEY_OP);
|
|
auto body = [&key] {
|
|
auto b = rjson::empty_object();
|
|
rjson::add(b, "alg", AKV_ENCRYPTION_ALG);
|
|
rjson::add(b, "value", base64url_encode(key->key()));
|
|
return b;
|
|
}();
|
|
rjson::value resp;
|
|
try {
|
|
resp = co_await send_request_with_retry(host, port, scheme == "https", path, body, filter);
|
|
} catch (...) {
|
|
azlog.error("[{}] Failed to wrap key {} with master_key={}: {}", _log_prefix, info, k.master_key, std::current_exception());
|
|
throw;
|
|
}
|
|
auto key_id = rjson::get<std::string>(resp, "kid");
|
|
auto cipher = rjson::get<std::string>(resp, "value");
|
|
boost::regex version_regex(R"foo(.*/([^/]+)$)foo");
|
|
boost::smatch match;
|
|
if (!boost::regex_search(key_id, match, version_regex)) {
|
|
throw std::runtime_error(fmt::format("Failed to parse key version from key id {}", key_id));
|
|
}
|
|
auto key_version = match[1].str();
|
|
|
|
auto sid = fmt::format("{}/{}/{}:{}", vault, keyname, key_version, cipher);
|
|
bytes id(sid.begin(), sid.end());
|
|
|
|
azlog.trace("[{}] Created key id {}", _log_prefix, sid);
|
|
co_return key_and_id_type{ key, id };
|
|
}
|
|
|
|
future<bytes> azure_host::impl::find_key(const id_cache_key& k) {
|
|
static const vault_log_filter filter{vault_log_filter::op_type::unwrapkey};
|
|
if (_ctxt && this_shard_id() != 0) {
|
|
co_return co_await smp::submit_to(0, [this, k]() -> future<bytes> {
|
|
auto host = _ctxt->get_azure_host(_name);
|
|
auto bytes = co_await host->_impl->_id_cache.get(k);
|
|
co_return bytes;
|
|
});
|
|
}
|
|
const auto id = to_string_view(k.id);
|
|
azlog.debug("[{}] Finding key: {}", _log_prefix, id);
|
|
|
|
auto [vault, keyname, version, cipher] = [&id] {
|
|
// Regex for key ID in format:
|
|
// "<vault>/<keyname>/<version>:<cipher>" or "http(s)://<host>:<port>/<keyname>/<version>:<cipher>"
|
|
// Captures:
|
|
// 1. Vault (either a name or an endpoint)
|
|
// 2. Key name
|
|
// 3. Key version
|
|
// 4. Cipher text for data encryption key
|
|
static const boost::regex id_re(R"foo(((?:https?://[^/]+)|[^/]+)/([^/]+)/([^:]+):(.+))foo");
|
|
boost::match_results<std::string_view::const_iterator> match;
|
|
if (!boost::regex_match(id.begin(), id.end(), match, id_re)) {
|
|
throw std::invalid_argument(fmt::format("Not a valid key id: {}", id));
|
|
}
|
|
return std::make_tuple(match[1].str(), match[2].str(), match[3].str(), match[4].str());
|
|
}();
|
|
|
|
auto [scheme, host, port] = parse_vault(vault);
|
|
auto path = seastar::format(AKV_PATH_TEMPLATE, keyname, version, AKV_UNWRAPKEY_OP);
|
|
auto body = [&cipher] {
|
|
auto b = rjson::empty_object();
|
|
rjson::add(b, "alg", AKV_ENCRYPTION_ALG);
|
|
rjson::add(b, "value", cipher);
|
|
return b;
|
|
}();
|
|
rjson::value resp;
|
|
try {
|
|
resp = co_await send_request_with_retry(host, port, scheme == "https", path, body, filter);
|
|
} catch (...) {
|
|
azlog.error("[{}] Failed to unwrap key {}: {}", _log_prefix, k.id, std::current_exception());
|
|
throw;
|
|
}
|
|
auto data = base64url_decode(rjson::get<std::string>(resp, "value"));
|
|
co_return data;
|
|
}
|
|
|
|
// ==================== azure_host class implementation ====================
|
|
|
|
azure_host::azure_host(const std::string& name, const host_options& options)
|
|
: _impl(std::make_unique<impl>(nullptr, name, options))
|
|
{}
|
|
|
|
azure_host::azure_host(encryption_context& ctxt, const std::string& name, const host_options& options)
|
|
: _impl(std::make_unique<impl>(&ctxt, name, options))
|
|
{}
|
|
|
|
azure_host::azure_host(encryption_context& ctxt, const std::string& name, const std::unordered_map<sstring, sstring>& map)
|
|
: azure_host(ctxt, name, [&map] {
|
|
host_options opts;
|
|
map_wrapper<std::unordered_map<sstring, sstring>> m(map);
|
|
|
|
opts.tenant_id = m("azure_tenant_id").value_or("");
|
|
opts.client_id = m("azure_client_id").value_or("");
|
|
opts.client_secret = m("azure_client_secret").value_or("");
|
|
opts.client_cert = m("azure_client_certificate_path").value_or("");
|
|
opts.authority = m("azure_authority_host").value_or("");
|
|
opts.imds_endpoint = m("imds_endpoint").value_or("");
|
|
|
|
opts.master_key = m("master_key").value_or("");
|
|
|
|
opts.truststore = m("truststore").value_or("");
|
|
opts.priority_string = m("priority_string").value_or("");
|
|
|
|
opts.key_cache_expiry = parse_expiry(m("key_cache_expiry"));
|
|
opts.key_cache_refresh = parse_expiry(m("key_cache_refresh"));
|
|
|
|
return opts;
|
|
}())
|
|
{}
|
|
|
|
azure_host::~azure_host() = default;
|
|
|
|
future<> azure_host::init() {
|
|
return _impl->init();
|
|
}
|
|
|
|
const azure_host::host_options& azure_host::options() const {
|
|
return _impl->options();
|
|
}
|
|
|
|
future<azure_host::key_and_id_type> azure_host::get_or_create_key(const key_info& info, const option_override* oov) {
|
|
return _impl->get_or_create_key(info, oov);
|
|
}
|
|
|
|
future<azure_host::key_ptr> azure_host::get_key_by_id(const azure_host::id_type& id, const key_info& info) {
|
|
return _impl->get_key_by_id(id, info);
|
|
}
|
|
|
|
} // namespace encryption
|
|
|
|
template<>
|
|
struct fmt::formatter<encryption::azure_host::impl::attr_cache_key> {
|
|
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
|
|
auto format(const encryption::azure_host::impl::attr_cache_key& d, fmt::format_context& ctxt) const {
|
|
return fmt::format_to(ctxt.out(), "{},{},{}", d.master_key, d.info.alg, d.info.len);
|
|
}
|
|
};
|
|
|
|
template<>
|
|
struct fmt::formatter<encryption::azure_host::impl::id_cache_key> {
|
|
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
|
|
auto format(const encryption::azure_host::impl::id_cache_key& d, fmt::format_context& ctxt) const {
|
|
return fmt::format_to(ctxt.out(), "{}", d.id);
|
|
}
|
|
};
|