| /* |
| * 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 "paimon/rest/dlf_auth.h" |
| |
| #include <openssl/evp.h> |
| |
| #include <array> |
| #include <climits> |
| #include <ctime> |
| #include <fstream> |
| #include <iomanip> |
| #include <limits> |
| #include <regex> |
| #include <set> |
| #include <sstream> |
| #include <string_view> |
| #include <thread> |
| #include <utility> |
| |
| #include "fmt/format.h" |
| #include "paimon/catalog_options.h" |
| #include "paimon/common/utils/options_utils.h" |
| #include "paimon/common/utils/string_utils.h" |
| #include "paimon/common/utils/url_utils.h" |
| #include "paimon/common/utils/uuid.h" |
| #include "paimon/rest/rest_http_client.h" |
| #include "rapidjson/document.h" |
| |
| namespace paimon { |
| |
| namespace { |
| |
| constexpr int32_t kEcsMetadataRequestTimeoutMillis = 3 * 60 * 1000; |
| |
| constexpr int64_t kTokenExpirationSafeTimeMillis = 60 * 60 * 1000; |
| constexpr size_t kMaxTokenResponseBytes = 1024 * 1024; |
| constexpr const char kDefaultEcsMetadataUrl[] = |
| "http://100.100.100.200/latest/meta-data/Ram/security-credentials/"; |
| |
| constexpr const char kAuthorizationHeader[] = "Authorization"; |
| constexpr const char kContentMd5Header[] = "Content-MD5"; |
| constexpr const char kContentTypeHeader[] = "Content-Type"; |
| constexpr const char kDlfDateHeader[] = "x-dlf-date"; |
| constexpr const char kDlfSecurityTokenHeader[] = "x-dlf-security-token"; |
| constexpr const char kDlfVersionHeader[] = "x-dlf-version"; |
| constexpr const char kDlfContentSha256Header[] = "x-dlf-content-sha256"; |
| constexpr const char kUnsignedPayload[] = "UNSIGNED-PAYLOAD"; |
| constexpr const char kJsonMediaType[] = "application/json"; |
| |
| constexpr const char kOpenApiDateHeader[] = "Date"; |
| constexpr const char kOpenApiAcceptHeader[] = "Accept"; |
| constexpr const char kOpenApiHostHeader[] = "Host"; |
| constexpr const char kAcsSignatureMethodHeader[] = "x-acs-signature-method"; |
| constexpr const char kAcsSignatureNonceHeader[] = "x-acs-signature-nonce"; |
| constexpr const char kAcsSignatureVersionHeader[] = "x-acs-signature-version"; |
| constexpr const char kAcsVersionHeader[] = "x-acs-version"; |
| constexpr const char kAcsSecurityTokenHeader[] = "x-acs-security-token"; |
| |
| Result<std::string> RequiredNonEmptyOption(const std::map<std::string, std::string>& options, |
| const std::string& key) { |
| Result<std::string> value = OptionsUtils::GetNonEmptyValueFromMap(options, key); |
| if (!value.ok()) { |
| return Status::Invalid(fmt::format("option '{}' must be configured for DLF auth", key)); |
| } |
| return value.value(); |
| } |
| |
| Result<std::string> RequiredJsonString(const rapidjson::Value& object, const char* key) { |
| if (!object.HasMember(key) || !object[key].IsString() || object[key].GetStringLength() == 0) { |
| return Status::Invalid(fmt::format("DLF token field '{}' must be a non-empty string", key)); |
| } |
| return std::string(object[key].GetString(), object[key].GetStringLength()); |
| } |
| |
| Result<std::optional<std::string>> OptionalJsonString(const rapidjson::Value& object, |
| const char* key) { |
| if (!object.HasMember(key) || object[key].IsNull()) { |
| return std::optional<std::string>(); |
| } |
| if (!object[key].IsString()) { |
| return Status::Invalid(fmt::format("DLF token field '{}' must be a string", key)); |
| } |
| return std::optional<std::string>( |
| std::string(object[key].GetString(), object[key].GetStringLength())); |
| } |
| |
| Result<std::tm> ToUtc(std::chrono::system_clock::time_point time) { |
| std::time_t seconds = std::chrono::system_clock::to_time_t(time); |
| std::tm utc{}; |
| if (gmtime_r(&seconds, &utc) == nullptr) { |
| return Status::Invalid("failed to convert DLF signing time to UTC"); |
| } |
| return utc; |
| } |
| |
| Result<std::string> FormatDlfTime(std::chrono::system_clock::time_point time) { |
| PAIMON_ASSIGN_OR_RAISE(std::tm utc, ToUtc(time)); |
| std::array<char, 32> buffer{}; |
| if (std::strftime(buffer.data(), buffer.size(), "%Y%m%dT%H%M%SZ", &utc) == 0) { |
| return Status::Invalid("failed to format DLF signing time"); |
| } |
| return std::string(buffer.data()); |
| } |
| |
| Result<std::string> FormatRfc1123Time(std::chrono::system_clock::time_point time) { |
| PAIMON_ASSIGN_OR_RAISE(std::tm utc, ToUtc(time)); |
| static constexpr std::array<const char*, 7> kWeekdays = {"Sun", "Mon", "Tue", "Wed", |
| "Thu", "Fri", "Sat"}; |
| static constexpr std::array<const char*, 12> kMonths = { |
| "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; |
| if (utc.tm_wday < 0 || utc.tm_wday >= static_cast<int32_t>(kWeekdays.size()) || |
| utc.tm_mon < 0 || utc.tm_mon >= static_cast<int32_t>(kMonths.size())) { |
| return Status::Invalid("failed to format DLF OpenAPI signing time"); |
| } |
| return fmt::format("{}, {:02d} {} {:04d} {:02d}:{:02d}:{:02d} GMT", kWeekdays[utc.tm_wday], |
| utc.tm_mday, kMonths[utc.tm_mon], utc.tm_year + 1900, utc.tm_hour, |
| utc.tm_min, utc.tm_sec); |
| } |
| |
| Result<int64_t> ParseExpiration(const std::string& expiration) { |
| static const std::regex kExpirationPattern( |
| "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"); |
| if (!std::regex_match(expiration, kExpirationPattern)) { |
| return Status::Invalid("invalid DLF token expiration"); |
| } |
| std::tm utc{}; |
| std::istringstream stream(expiration); |
| stream >> std::get_time(&utc, "%Y-%m-%dT%H:%M:%SZ"); |
| if (stream.fail() || stream.peek() != std::char_traits<char>::eof()) { |
| return Status::Invalid("invalid DLF token expiration"); |
| } |
| int32_t year = utc.tm_year; |
| int32_t month = utc.tm_mon; |
| int32_t day = utc.tm_mday; |
| int32_t hour = utc.tm_hour; |
| int32_t minute = utc.tm_min; |
| int32_t second = utc.tm_sec; |
| std::time_t timestamp = timegm(&utc); |
| std::tm verified{}; |
| if (timestamp == static_cast<std::time_t>(-1) || gmtime_r(×tamp, &verified) == nullptr) { |
| return Status::Invalid("invalid DLF token expiration"); |
| } |
| if (verified.tm_year != year || verified.tm_mon != month || verified.tm_mday != day || |
| verified.tm_hour != hour || verified.tm_min != minute || verified.tm_sec != second) { |
| return Status::Invalid("invalid DLF token expiration"); |
| } |
| if (timestamp > std::numeric_limits<int64_t>::max() / 1000) { |
| return Status::Invalid("DLF token expiration is out of range"); |
| } |
| return static_cast<int64_t>(timestamp) * 1000; |
| } |
| |
| using Bytes = std::vector<uint8_t>; |
| using EvpMdContext = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>; |
| using EvpPkey = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>; |
| |
| Result<Bytes> Digest(const EVP_MD* digest, std::string_view data) { |
| EvpMdContext context(EVP_MD_CTX_new(), EVP_MD_CTX_free); |
| if (!context || EVP_DigestInit_ex(context.get(), digest, nullptr) != 1 || |
| EVP_DigestUpdate(context.get(), data.data(), data.size()) != 1) { |
| return Status::IOError("failed to calculate DLF request digest"); |
| } |
| Bytes output(EVP_MAX_MD_SIZE); |
| unsigned int output_size = 0; |
| if (EVP_DigestFinal_ex(context.get(), output.data(), &output_size) != 1) { |
| return Status::IOError("failed to calculate DLF request digest"); |
| } |
| output.resize(output_size); |
| return output; |
| } |
| |
| Result<Bytes> Hmac(const EVP_MD* digest, const Bytes& key, std::string_view data) { |
| if (key.size() > static_cast<size_t>(INT_MAX)) { |
| return Status::Invalid("DLF signing key is too large"); |
| } |
| EvpPkey signing_key( |
| EVP_PKEY_new_mac_key(EVP_PKEY_HMAC, nullptr, key.data(), static_cast<int32_t>(key.size())), |
| EVP_PKEY_free); |
| EvpMdContext context(EVP_MD_CTX_new(), EVP_MD_CTX_free); |
| if (!signing_key || !context || |
| EVP_DigestSignInit(context.get(), nullptr, digest, nullptr, signing_key.get()) != 1 || |
| EVP_DigestSignUpdate(context.get(), data.data(), data.size()) != 1) { |
| return Status::IOError("failed to calculate DLF request signature"); |
| } |
| size_t output_size = 0; |
| if (EVP_DigestSignFinal(context.get(), nullptr, &output_size) != 1) { |
| return Status::IOError("failed to calculate DLF request signature"); |
| } |
| Bytes output(output_size); |
| if (EVP_DigestSignFinal(context.get(), output.data(), &output_size) != 1) { |
| return Status::IOError("failed to calculate DLF request signature"); |
| } |
| output.resize(output_size); |
| return output; |
| } |
| |
| Bytes ToBytes(const std::string& value) { |
| return Bytes(value.begin(), value.end()); |
| } |
| |
| std::string HexEncode(const Bytes& value) { |
| static constexpr char kHex[] = "0123456789abcdef"; |
| std::string encoded; |
| encoded.reserve(value.size() * 2); |
| for (uint8_t byte : value) { |
| encoded.push_back(kHex[byte >> 4]); |
| encoded.push_back(kHex[byte & 0x0f]); |
| } |
| return encoded; |
| } |
| |
| Result<std::string> Base64Encode(const Bytes& value) { |
| if (value.size() > static_cast<size_t>(INT_MAX)) { |
| return Status::Invalid("DLF digest is too large to encode"); |
| } |
| size_t capacity = 4 * ((value.size() + 2) / 3) + 1; |
| std::string encoded(capacity, '\0'); |
| int32_t size = EVP_EncodeBlock(reinterpret_cast<unsigned char*>(encoded.data()), value.data(), |
| static_cast<int32_t>(value.size())); |
| if (size < 0) { |
| return Status::IOError("failed to encode DLF request digest"); |
| } |
| encoded.resize(static_cast<size_t>(size)); |
| return encoded; |
| } |
| |
| Result<std::string> Md5Base64(const std::string& value) { |
| PAIMON_ASSIGN_OR_RAISE(Bytes digest, Digest(EVP_md5(), value)); |
| return Base64Encode(digest); |
| } |
| |
| std::string Trimmed(const std::string& value) { |
| std::string trimmed = value; |
| StringUtils::Trim(&trimmed); |
| return trimmed; |
| } |
| |
| std::string DefaultCanonicalRequest(const RestAuthParameter& parameter, |
| const DlfRequestSigner::Headers& headers) { |
| std::string canonical = parameter.method + "\n" + parameter.resource_path + "\n"; |
| bool first = true; |
| for (const auto& [key, value] : parameter.parameters) { |
| if (!first) { |
| canonical += "&"; |
| } |
| canonical += Trimmed(key); |
| if (!value.empty()) { |
| canonical += "=" + Trimmed(value); |
| } |
| first = false; |
| } |
| |
| static const std::set<std::string> kSignedHeaders = { |
| "content-md5", "content-type", "x-dlf-content-sha256", |
| "x-dlf-date", "x-dlf-version", "x-dlf-security-token"}; |
| std::map<std::string, std::string> sorted_headers; |
| for (const auto& [key, value] : headers) { |
| std::string lower_key = StringUtils::ToLowerCase(key); |
| if (kSignedHeaders.count(lower_key) > 0) { |
| sorted_headers[lower_key] = Trimmed(value); |
| } |
| } |
| for (const auto& [key, value] : sorted_headers) { |
| canonical += "\n" + key + ":" + value; |
| } |
| auto content_iter = headers.find(kDlfContentSha256Header); |
| std::string content_sha = |
| content_iter == headers.end() ? std::string(kUnsignedPayload) : content_iter->second; |
| return canonical + "\n" + content_sha; |
| } |
| |
| Result<std::string> RequiredHeader(const DlfRequestSigner::Headers& headers, |
| const std::string& name) { |
| auto iter = headers.find(name); |
| if (iter == headers.end() || iter->second.empty()) { |
| return Status::Invalid(fmt::format("DLF signing header '{}' is missing", name)); |
| } |
| return iter->second; |
| } |
| |
| std::string OpenApiCanonicalizedHeaders(const DlfRequestSigner::Headers& headers) { |
| std::map<std::string, std::string> sorted; |
| for (const auto& [key, value] : headers) { |
| std::string lower_key = StringUtils::ToLowerCase(key); |
| if (StringUtils::StartsWith(lower_key, "x-acs-")) { |
| sorted[lower_key] = Trimmed(value); |
| } |
| } |
| std::string canonical; |
| for (const auto& [key, value] : sorted) { |
| canonical += key + ":" + value + "\n"; |
| } |
| return canonical; |
| } |
| |
| std::string OpenApiCanonicalizedResource(const RestAuthParameter& parameter) { |
| std::string resource = UrlUtils::DecodeString(parameter.resource_path); |
| if (parameter.parameters.empty()) { |
| return resource; |
| } |
| resource += "?"; |
| bool first = true; |
| for (const auto& [key, value] : parameter.parameters) { |
| if (!first) { |
| resource += "&"; |
| } |
| resource += key; |
| std::string decoded = UrlUtils::DecodeString(value); |
| if (!decoded.empty()) { |
| resource += "=" + decoded; |
| } |
| first = false; |
| } |
| return resource; |
| } |
| |
| Result<std::string> GenerateNonce(std::chrono::system_clock::time_point now) { |
| std::string uuid; |
| if (!UUID::Generate(&uuid)) { |
| return Status::IOError("failed to generate DLF OpenAPI signing nonce"); |
| } |
| int64_t millis = |
| std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count(); |
| std::ostringstream thread_id; |
| thread_id << std::this_thread::get_id(); |
| return fmt::format("{}{}{}", uuid, millis, thread_id.str()); |
| } |
| |
| Result<std::unique_ptr<DlfRequestSigner>> CreateSigner(const std::string& algorithm, |
| const std::string& region) { |
| if (algorithm == DlfDefaultSigner::kIdentifier) { |
| return std::make_unique<DlfDefaultSigner>(region); |
| } |
| if (algorithm == DlfOpenApiSigner::kIdentifier) { |
| return std::make_unique<DlfOpenApiSigner>(); |
| } |
| return Status::Invalid(fmt::format( |
| "unsupported DLF signing algorithm '{}', supported values are 'default' and 'openapi'", |
| algorithm)); |
| } |
| |
| } // namespace |
| |
| DlfToken::DlfToken(const std::string& access_key_id, const std::string& access_key_secret, |
| const std::optional<std::string>& security_token, |
| const std::optional<int64_t>& expiration_at_millis) |
| : access_key_id_(access_key_id), |
| access_key_secret_(access_key_secret), |
| security_token_(security_token), |
| expiration_at_millis_(expiration_at_millis) {} |
| |
| Result<DlfToken> DlfToken::FromJson(const std::string& json) { |
| rapidjson::Document document; |
| document.Parse(json.data(), json.size()); |
| if (document.HasParseError() || !document.IsObject()) { |
| return Status::Invalid("failed to parse DLF token JSON"); |
| } |
| PAIMON_ASSIGN_OR_RAISE(std::string access_key_id, RequiredJsonString(document, "AccessKeyId")); |
| PAIMON_ASSIGN_OR_RAISE(std::string access_key_secret, |
| RequiredJsonString(document, "AccessKeySecret")); |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> security_token, |
| OptionalJsonString(document, "SecurityToken")); |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> expiration, |
| OptionalJsonString(document, "Expiration")); |
| std::optional<int64_t> expiration_at_millis; |
| if (expiration) { |
| PAIMON_ASSIGN_OR_RAISE(int64_t parsed_expiration, ParseExpiration(expiration.value())); |
| expiration_at_millis = parsed_expiration; |
| } |
| return DlfToken(access_key_id, access_key_secret, security_token, expiration_at_millis); |
| } |
| |
| bool DlfToken::ShouldRefresh(std::chrono::system_clock::time_point now) const { |
| if (!expiration_at_millis_) { |
| return false; |
| } |
| int64_t now_millis = |
| std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count(); |
| return expiration_at_millis_.value() - now_millis < kTokenExpirationSafeTimeMillis; |
| } |
| |
| DlfLocalFileTokenLoader::DlfLocalFileTokenLoader(const std::string& token_file_path, |
| int32_t max_attempts, |
| std::chrono::milliseconds retry_delay) |
| : token_file_path_(token_file_path), max_attempts_(max_attempts), retry_delay_(retry_delay) {} |
| |
| Result<DlfToken> DlfLocalFileTokenLoader::LoadToken() { |
| if (token_file_path_.empty()) { |
| return Status::Invalid("DLF token file path is empty"); |
| } |
| if (max_attempts_ <= 0 || retry_delay_.count() < 0) { |
| return Status::Invalid("invalid DLF token file retry configuration"); |
| } |
| Status last_status = Status::Invalid("failed to load DLF token file"); |
| for (int32_t attempt = 1; attempt <= max_attempts_; ++attempt) { |
| std::ifstream file(token_file_path_, std::ios::binary); |
| if (!file.is_open()) { |
| last_status = Status::IOError( |
| fmt::format("failed to read DLF token file '{}'", token_file_path_)); |
| } else { |
| std::string contents(kMaxTokenResponseBytes + 1, '\0'); |
| file.read(contents.data(), static_cast<std::streamsize>(contents.size())); |
| std::streamsize size = file.gcount(); |
| if (file.bad()) { |
| last_status = Status::IOError( |
| fmt::format("failed to read DLF token file '{}'", token_file_path_)); |
| } else if (size > static_cast<std::streamsize>(kMaxTokenResponseBytes)) { |
| last_status = Status::Invalid("DLF token file is too large"); |
| } else { |
| contents.resize(static_cast<size_t>(size)); |
| Result<DlfToken> token = DlfToken::FromJson(contents); |
| if (token.ok()) { |
| return token; |
| } |
| last_status = Status::Invalid("failed to parse DLF token file"); |
| } |
| } |
| if (attempt < max_attempts_) { |
| std::this_thread::sleep_for(retry_delay_ * attempt); |
| } |
| } |
| return last_status; |
| } |
| |
| std::string DlfLocalFileTokenLoader::Description() const { |
| return token_file_path_; |
| } |
| |
| DlfEcsTokenLoader::DlfEcsTokenLoader(const std::string& metadata_url, |
| const std::optional<std::string>& role_name, |
| std::unique_ptr<HttpClient> http_client) |
| : metadata_url_(metadata_url), role_name_(role_name), http_client_(std::move(http_client)) {} |
| |
| std::unique_ptr<DlfEcsTokenLoader> DlfEcsTokenLoader::Create( |
| const std::string& metadata_url, const std::optional<std::string>& role_name) { |
| return std::make_unique<DlfEcsTokenLoader>(metadata_url, role_name, |
| std::make_unique<CurlHttpClient>()); |
| } |
| |
| Result<std::string> DlfEcsTokenLoader::Get(const std::string& url) const { |
| if (!http_client_) { |
| return Status::Invalid("DLF ECS metadata HTTP client is not configured"); |
| } |
| HttpRequest request; |
| request.url = url; |
| request.request_timeout_ms = kEcsMetadataRequestTimeoutMillis; |
| std::string body; |
| Result<HttpResponse> response = |
| http_client_->Execute(request, [&body](const char* data, int64_t size) { |
| if (size < 0 || body.size() + static_cast<size_t>(size) > kMaxTokenResponseBytes) { |
| return Status::Invalid("DLF ECS metadata response is too large"); |
| } |
| body.append(data, static_cast<size_t>(size)); |
| return Status::OK(); |
| }); |
| if (!response.ok()) { |
| return Status::IOError("failed to request DLF credentials from ECS metadata service: ", |
| response.status().message()); |
| } |
| HttpResponse http_response = std::move(response).value(); |
| if (http_response.status_code < 200 || http_response.status_code >= 300) { |
| return Status::IOError(fmt::format("DLF ECS metadata service returned HTTP status {}", |
| http_response.status_code)); |
| } |
| if (StringUtils::IsNullOrWhitespaceOnly(body)) { |
| return Status::Invalid("DLF ECS metadata service returned an empty response"); |
| } |
| return body; |
| } |
| |
| Result<DlfToken> DlfEcsTokenLoader::LoadToken() { |
| if (metadata_url_.empty()) { |
| return Status::Invalid("DLF ECS metadata URL is empty"); |
| } |
| if (!role_name_) { |
| PAIMON_ASSIGN_OR_RAISE(std::string role, Get(metadata_url_)); |
| StringUtils::Trim(&role); |
| if (role.empty()) { |
| return Status::Invalid("DLF ECS metadata service returned an empty role name"); |
| } |
| role_name_ = role; |
| } |
| PAIMON_ASSIGN_OR_RAISE(std::string token_json, Get(metadata_url_ + role_name_.value())); |
| Result<DlfToken> token = DlfToken::FromJson(token_json); |
| if (!token.ok()) { |
| return Status::Invalid("failed to parse DLF ECS token response"); |
| } |
| return token; |
| } |
| |
| std::string DlfEcsTokenLoader::Description() const { |
| return metadata_url_; |
| } |
| |
| DlfDefaultSigner::DlfDefaultSigner(const std::string& region) : region_(region) {} |
| |
| Result<DlfRequestSigner::Headers> DlfDefaultSigner::SignHeaders( |
| const std::string& body, std::chrono::system_clock::time_point now, |
| const std::optional<std::string>& security_token, const std::string& host) const { |
| PAIMON_ASSIGN_OR_RAISE(std::string date_time, FormatDlfTime(now)); |
| Headers headers = {{kDlfDateHeader, date_time}, |
| {kDlfContentSha256Header, kUnsignedPayload}, |
| {kDlfVersionHeader, "v1"}}; |
| if (!body.empty()) { |
| PAIMON_ASSIGN_OR_RAISE(std::string content_md5, Md5Base64(body)); |
| headers[kContentTypeHeader] = kJsonMediaType; |
| headers[kContentMd5Header] = content_md5; |
| } |
| if (security_token) { |
| headers[kDlfSecurityTokenHeader] = security_token.value(); |
| } |
| return headers; |
| } |
| |
| Result<std::string> DlfDefaultSigner::Authorization(const RestAuthParameter& parameter, |
| const DlfToken& token, const std::string& host, |
| const Headers& sign_headers) const { |
| PAIMON_ASSIGN_OR_RAISE(std::string date_time, RequiredHeader(sign_headers, kDlfDateHeader)); |
| if (date_time.size() < 8) { |
| return Status::Invalid("DLF signing date is invalid"); |
| } |
| std::string date = date_time.substr(0, 8); |
| std::string scope = fmt::format("{}/{}/DlfNext/aliyun_v4_request", date, region_); |
| std::string canonical_request = DefaultCanonicalRequest(parameter, sign_headers); |
| PAIMON_ASSIGN_OR_RAISE(Bytes canonical_hash, Digest(EVP_sha256(), canonical_request)); |
| std::string string_to_sign = |
| fmt::format("DLF4-HMAC-SHA256\n{}\n{}\n{}", date_time, scope, HexEncode(canonical_hash)); |
| |
| PAIMON_ASSIGN_OR_RAISE( |
| Bytes date_key, |
| Hmac(EVP_sha256(), ToBytes("aliyun_v4" + token.GetAccessKeySecret()), date)); |
| PAIMON_ASSIGN_OR_RAISE(Bytes region_key, Hmac(EVP_sha256(), date_key, region_)); |
| PAIMON_ASSIGN_OR_RAISE(Bytes service_key, Hmac(EVP_sha256(), region_key, "DlfNext")); |
| PAIMON_ASSIGN_OR_RAISE(Bytes signing_key, Hmac(EVP_sha256(), service_key, "aliyun_v4_request")); |
| PAIMON_ASSIGN_OR_RAISE(Bytes signature, Hmac(EVP_sha256(), signing_key, string_to_sign)); |
| return fmt::format("DLF4-HMAC-SHA256 Credential={}/{},Signature={}", token.GetAccessKeyId(), |
| scope, HexEncode(signature)); |
| } |
| |
| Result<DlfRequestSigner::Headers> DlfOpenApiSigner::SignHeaders( |
| const std::string& body, std::chrono::system_clock::time_point now, |
| const std::optional<std::string>& security_token, const std::string& host) const { |
| if (host.empty()) { |
| return Status::Invalid("DLF OpenAPI signing host is empty"); |
| } |
| PAIMON_ASSIGN_OR_RAISE(std::string date, FormatRfc1123Time(now)); |
| PAIMON_ASSIGN_OR_RAISE(std::string nonce, GenerateNonce(now)); |
| Headers headers = {{kOpenApiDateHeader, date}, {kOpenApiAcceptHeader, kJsonMediaType}, |
| {kOpenApiHostHeader, host}, {kAcsSignatureMethodHeader, "HMAC-SHA1"}, |
| {kAcsSignatureNonceHeader, nonce}, {kAcsSignatureVersionHeader, "1.0"}, |
| {kAcsVersionHeader, "2026-01-18"}}; |
| if (!body.empty()) { |
| PAIMON_ASSIGN_OR_RAISE(std::string content_md5, Md5Base64(body)); |
| headers[kContentMd5Header] = content_md5; |
| headers[kContentTypeHeader] = kJsonMediaType; |
| } |
| if (security_token) { |
| headers[kAcsSecurityTokenHeader] = security_token.value(); |
| } |
| return headers; |
| } |
| |
| Result<std::string> DlfOpenApiSigner::Authorization(const RestAuthParameter& parameter, |
| const DlfToken& token, const std::string& host, |
| const Headers& sign_headers) const { |
| PAIMON_ASSIGN_OR_RAISE(std::string accept, RequiredHeader(sign_headers, kOpenApiAcceptHeader)); |
| PAIMON_ASSIGN_OR_RAISE(std::string date, RequiredHeader(sign_headers, kOpenApiDateHeader)); |
| std::string content_md5; |
| auto md5_iter = sign_headers.find(kContentMd5Header); |
| if (md5_iter != sign_headers.end()) { |
| content_md5 = md5_iter->second; |
| } |
| std::string content_type; |
| auto type_iter = sign_headers.find(kContentTypeHeader); |
| if (type_iter != sign_headers.end()) { |
| content_type = type_iter->second; |
| } |
| std::string string_to_sign = |
| parameter.method + "\n" + accept + "\n" + content_md5 + "\n" + content_type + "\n" + date + |
| "\n" + OpenApiCanonicalizedHeaders(sign_headers) + OpenApiCanonicalizedResource(parameter); |
| PAIMON_ASSIGN_OR_RAISE(Bytes signature, |
| Hmac(EVP_sha1(), ToBytes(token.GetAccessKeySecret()), string_to_sign)); |
| PAIMON_ASSIGN_OR_RAISE(std::string encoded_signature, Base64Encode(signature)); |
| return fmt::format("acs {}:{}", token.GetAccessKeyId(), encoded_signature); |
| } |
| |
| DlfAuthProvider::DlfAuthProvider(std::unique_ptr<DlfTokenLoader> token_loader, |
| const std::optional<DlfToken>& token, const std::string& host, |
| std::unique_ptr<DlfRequestSigner> signer, Clock clock) |
| : token_loader_(std::move(token_loader)), |
| token_(token), |
| host_(host), |
| signer_(std::move(signer)), |
| clock_(std::move(clock)) {} |
| |
| Result<std::unique_ptr<DlfAuthProvider>> DlfAuthProvider::Create( |
| const std::map<std::string, std::string>& options) { |
| PAIMON_ASSIGN_OR_RAISE(std::string uri, RequiredNonEmptyOption(options, CatalogOptions::URI)); |
| std::string region; |
| PAIMON_ASSIGN_OR_RAISE( |
| std::optional<std::string> configured_region, |
| OptionsUtils::GetOptionalValueFromMap<std::string>(options, CatalogOptions::DLF_REGION)); |
| if (configured_region) { |
| if (configured_region->empty()) { |
| return Status::Invalid("option 'dlf.region' must not be empty"); |
| } |
| region = configured_region.value(); |
| } else { |
| PAIMON_ASSIGN_OR_RAISE(region, ParseRegionFromUri(uri)); |
| } |
| |
| std::string algorithm; |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> configured_algorithm, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_SIGNING_ALGORITHM)); |
| if (configured_algorithm) { |
| algorithm = configured_algorithm.value(); |
| } else { |
| algorithm = ParseSigningAlgorithmFromUri(uri); |
| } |
| |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> loader_name, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_TOKEN_LOADER)); |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> token_path, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_TOKEN_PATH)); |
| if (loader_name) { |
| if (loader_name.value() == "ecs") { |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> configured_metadata_url, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_TOKEN_ECS_METADATA_URL)); |
| std::string metadata_url = configured_metadata_url.value_or(kDefaultEcsMetadataUrl); |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> role_name, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_TOKEN_ECS_ROLE_NAME)); |
| return FromTokenLoader(DlfEcsTokenLoader::Create(metadata_url, role_name), uri, region, |
| algorithm, std::chrono::system_clock::now); |
| } |
| if (loader_name.value() == "local_file") { |
| PAIMON_ASSIGN_OR_RAISE(std::string path, |
| RequiredNonEmptyOption(options, CatalogOptions::DLF_TOKEN_PATH)); |
| return FromTokenLoader( |
| std::make_unique<DlfLocalFileTokenLoader>(path, 5, std::chrono::seconds(1)), uri, |
| region, algorithm, std::chrono::system_clock::now); |
| } |
| return Status::NotImplemented( |
| fmt::format("unsupported DLF token loader '{}', supported values are 'ecs' and " |
| "'local_file'", |
| loader_name.value())); |
| } |
| if (token_path) { |
| if (token_path->empty()) { |
| return Status::Invalid("option 'dlf.token-path' must not be empty"); |
| } |
| return FromTokenLoader(std::make_unique<DlfLocalFileTokenLoader>(token_path.value(), 5, |
| std::chrono::seconds(1)), |
| uri, region, algorithm, std::chrono::system_clock::now); |
| } |
| |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> access_key_id, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_ACCESS_KEY_ID)); |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> access_key_secret, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_ACCESS_KEY_SECRET)); |
| if (access_key_id && !access_key_id->empty() && access_key_secret && |
| !access_key_secret->empty()) { |
| PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> security_token, |
| OptionsUtils::GetOptionalValueFromMap<std::string>( |
| options, CatalogOptions::DLF_SECURITY_TOKEN)); |
| DlfToken token(access_key_id.value(), access_key_secret.value(), security_token, |
| std::nullopt); |
| return FromAccessKey(token, uri, region, algorithm, std::chrono::system_clock::now); |
| } |
| return Status::Invalid("DLF token path or access key must be configured for DLF auth"); |
| } |
| |
| Result<std::unique_ptr<DlfAuthProvider>> DlfAuthProvider::FromAccessKey( |
| const DlfToken& token, const std::string& uri, const std::string& region, |
| const std::string& signing_algorithm, Clock clock) { |
| if (token.GetAccessKeyId().empty() || token.GetAccessKeySecret().empty()) { |
| return Status::Invalid("DLF access key id and secret must not be empty"); |
| } |
| PAIMON_ASSIGN_OR_RAISE(std::string host, ExtractHost(uri)); |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DlfRequestSigner> signer, |
| CreateSigner(signing_algorithm, region)); |
| return std::unique_ptr<DlfAuthProvider>( |
| new DlfAuthProvider(nullptr, token, host, std::move(signer), std::move(clock))); |
| } |
| |
| Result<std::unique_ptr<DlfAuthProvider>> DlfAuthProvider::FromTokenLoader( |
| std::unique_ptr<DlfTokenLoader> token_loader, const std::string& uri, const std::string& region, |
| const std::string& signing_algorithm, Clock clock) { |
| if (!token_loader) { |
| return Status::Invalid("DLF token loader must not be null"); |
| } |
| PAIMON_ASSIGN_OR_RAISE(std::string host, ExtractHost(uri)); |
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<DlfRequestSigner> signer, |
| CreateSigner(signing_algorithm, region)); |
| return std::unique_ptr<DlfAuthProvider>(new DlfAuthProvider( |
| std::move(token_loader), std::nullopt, host, std::move(signer), std::move(clock))); |
| } |
| |
| Result<DlfToken> DlfAuthProvider::GetFreshToken(std::chrono::system_clock::time_point now) const { |
| std::scoped_lock lock(token_mutex_); |
| if (token_ && !token_->ShouldRefresh(now)) { |
| return token_.value(); |
| } |
| if (!token_loader_) { |
| return Status::Invalid("DLF credentials expired and no token loader is configured"); |
| } |
| PAIMON_ASSIGN_OR_RAISE(DlfToken loaded, token_loader_->LoadToken()); |
| if (loaded.GetAccessKeyId().empty() || loaded.GetAccessKeySecret().empty()) { |
| return Status::Invalid("DLF token loader returned empty access key credentials"); |
| } |
| token_ = loaded; |
| return loaded; |
| } |
| |
| Result<std::map<std::string, std::string>> DlfAuthProvider::MergeAuthHeader( |
| const std::map<std::string, std::string>& base_header, |
| const RestAuthParameter& parameter) const { |
| PAIMON_ASSIGN_OR_RAISE(DlfToken token, GetFreshToken(clock_())); |
| std::chrono::system_clock::time_point signing_time = clock_(); |
| PAIMON_ASSIGN_OR_RAISE( |
| DlfRequestSigner::Headers sign_headers, |
| signer_->SignHeaders(parameter.data, signing_time, token.GetSecurityToken(), host_)); |
| PAIMON_ASSIGN_OR_RAISE(std::string authorization, |
| signer_->Authorization(parameter, token, host_, sign_headers)); |
| std::map<std::string, std::string> headers = base_header; |
| for (const auto& [key, value] : sign_headers) { |
| headers[key] = value; |
| } |
| headers[kAuthorizationHeader] = authorization; |
| return headers; |
| } |
| |
| Result<std::string> DlfAuthProvider::ParseRegionFromUri(const std::string& uri) { |
| static const std::regex kRegionPattern("(?:pre-)?([a-z]+-[a-z]+(?:-[0-9]+)?)"); |
| std::smatch match; |
| if (std::regex_search(uri, match, kRegionPattern) && match.size() > 1 && |
| !match.str(1).empty()) { |
| return match.str(1); |
| } |
| return Status::Invalid( |
| "could not determine DLF region from option 'dlf.region' or REST catalog URI"); |
| } |
| |
| std::string DlfAuthProvider::ParseSigningAlgorithmFromUri(const std::string& uri) { |
| std::string lower_uri = StringUtils::ToLowerCase(uri); |
| return lower_uri.find("dlfnext") == std::string::npos ? DlfDefaultSigner::kIdentifier |
| : DlfOpenApiSigner::kIdentifier; |
| } |
| |
| Result<std::string> DlfAuthProvider::ExtractHost(const std::string& uri) { |
| std::string host = RestHttpClient::NormalizeUri(uri); |
| std::string lower_uri = StringUtils::ToLowerCase(host); |
| if (StringUtils::StartsWith(lower_uri, "http://")) { |
| host.erase(0, 7); |
| } else if (StringUtils::StartsWith(lower_uri, "https://")) { |
| host.erase(0, 8); |
| } |
| size_t path = host.find('/'); |
| if (path != std::string::npos) { |
| host.resize(path); |
| } |
| if (host.empty() || host.find('?') != std::string::npos || |
| host.find('#') != std::string::npos || host.find('@') != std::string::npos) { |
| return Status::Invalid("could not determine DLF signing host from REST catalog URI"); |
| } |
| return host; |
| } |
| |
| } // namespace paimon |