blob: 5e93b84d48c6c13f8bfe8a286027fbbd1c44f0ee [file]
/*
* 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/rest_credential_provider.h"
#include <algorithm>
#include <chrono>
#include <functional>
#include <mutex>
#include <set>
#include <utility>
#include "paimon/catalog_options.h"
#include "paimon/common/utils/string_utils.h"
namespace paimon {
namespace {
/// Declared here rather than taken from the OSS file system, which is an optional build
/// component this module must not depend on.
constexpr const char kOssEndpointOption[] = "fs.oss.endpoint";
constexpr const char kOssOptionPrefix[] = "fs.oss.";
constexpr const char kOssBucketPrefix[] = "fs.oss.bucket.";
// The two OSS names for the STS token: the backend reads securityToken first and only
// consults sessionToken when it is empty, so a token under either name has to clear the
// catalog value under both.
constexpr const char kOssSecurityTokenSuffix[] = "securityToken";
constexpr const char kOssSessionTokenSuffix[] = "sessionToken";
/// The suffix of a flat OSS option, i.e. what follows "fs.oss." for a key like
/// "fs.oss.accessKeyId"; empty for a bucket-scoped ("fs.oss.bucket.<bucket>.<suffix>") or a
/// non-OSS key.
std::string FlatOssOptionSuffix(const std::string& key) {
if (!StringUtils::StartsWith(key, kOssOptionPrefix) ||
StringUtils::StartsWith(key, kOssBucketPrefix)) {
return "";
}
return key.substr(std::string(kOssOptionPrefix).size());
}
} // namespace
size_t RestToken::Hash::operator()(const RestToken& rest_token) const {
size_t result = std::hash<int64_t>()(rest_token.expires_at_millis);
for (const auto& [key, value] : rest_token.token) {
result = result * 31 + std::hash<std::string>()(key);
result = result * 31 + std::hash<std::string>()(value);
}
return result;
}
RestCredentialProvider::RestCredentialProvider(const std::shared_ptr<RestApi>& api,
const Identifier& identifier, Clock clock)
: RestCredentialProvider([api, identifier]() { return api->LoadTableToken(identifier); },
identifier, std::move(clock)) {}
RestCredentialProvider::RestCredentialProvider(TokenLoader token_loader,
const Identifier& identifier, Clock clock)
: token_loader_(std::move(token_loader)),
identifier_(identifier),
clock_(std::move(clock)),
logger_(Logger::GetLogger("RestCredentialProvider")) {}
bool RestCredentialProvider::ShouldRefresh() const {
if (!token_) {
return true;
}
int64_t now_millis =
std::chrono::duration_cast<std::chrono::milliseconds>(clock_().time_since_epoch()).count();
return token_->expires_at_millis - now_millis < RestApi::kTokenExpirationSafeTimeMillis;
}
std::map<std::string, std::string> RestCredentialProvider::MergeOptionsWithCredentials(
const std::map<std::string, std::string>& base_options,
const std::map<std::string, std::string>& credentials) const {
std::map<std::string, std::string> merged = base_options;
for (const auto& [key, value] : credentials) {
merged[key] = value;
}
// The OSS backend resolves a bucket-scoped option ("fs.oss.bucket.<b>.<suffix>") ahead of the
// flat one and reads "fs.oss.securityToken" ahead of its "fs.oss.sessionToken" alias, so a
// stale catalog value can shadow a credential the token just refreshed. Drop the stale
// variants of every credential the token supplies -- including both security-token aliases
// when it supplies either -- so the issued credential is the one that wins.
std::set<std::string> refreshed_suffixes;
bool refreshed_security_token = false;
for (const auto& [key, value] : credentials) {
std::string suffix = FlatOssOptionSuffix(key);
if (suffix.empty()) {
continue;
}
refreshed_suffixes.insert(suffix);
refreshed_security_token = refreshed_security_token || suffix == kOssSecurityTokenSuffix ||
suffix == kOssSessionTokenSuffix;
}
if (refreshed_security_token) {
refreshed_suffixes.insert(kOssSecurityTokenSuffix);
refreshed_suffixes.insert(kOssSessionTokenSuffix);
}
for (auto it = merged.begin(); it != merged.end();) {
const std::string& key = it->first;
// A credential the token itself supplies is authoritative and is never dropped, even when
// it is bucket-scoped or the alias of another key the token carries.
bool token_supplied = credentials.find(key) != credentials.end();
bool stale_bucket_scoped = !token_supplied &&
StringUtils::StartsWith(key, kOssBucketPrefix) &&
std::any_of(refreshed_suffixes.begin(), refreshed_suffixes.end(),
[&](const std::string& suffix) {
return StringUtils::EndsWith(key, "." + suffix);
});
bool stale_security_token_alias =
!token_supplied && refreshed_security_token &&
(key == std::string(kOssOptionPrefix) + kOssSecurityTokenSuffix ||
key == std::string(kOssOptionPrefix) + kOssSessionTokenSuffix);
if (stale_bucket_scoped || stale_security_token_alias) {
it = merged.erase(it);
} else {
++it;
}
}
// The DLF OSS endpoint overrides the standard one, since the credentials are issued
// for the DLF endpoint rather than for the endpoint the catalog was configured with.
auto dlf_oss_endpoint = base_options.find(CatalogOptions::DLF_OSS_ENDPOINT);
if (dlf_oss_endpoint != base_options.end() && !dlf_oss_endpoint->second.empty()) {
merged[kOssEndpointOption] = dlf_oss_endpoint->second;
}
return merged;
}
Status RestCredentialProvider::RefreshToken() const {
PAIMON_LOG_INFO(logger_, "begin refresh data token for identifier [%s]",
identifier_.ToString().c_str());
PAIMON_ASSIGN_OR_RAISE(GetTableTokenResponse response, token_loader_());
PAIMON_LOG_INFO(logger_, "end refresh data token for identifier [%s] expiresAtMillis [%ld]",
identifier_.ToString().c_str(),
static_cast<int64_t>(response.GetExpiresAtMillis()));
token_ = RestToken{response.GetToken(), response.GetExpiresAtMillis()};
return Status::OK();
}
Result<RestToken> RestCredentialProvider::ValidToken() const {
{
std::shared_lock<std::shared_mutex> read_lock(mutex_);
if (!ShouldRefresh()) {
return token_.value();
}
}
std::unique_lock<std::shared_mutex> write_lock(mutex_);
// Double-check, another thread may have refreshed while this one waited for the lock.
if (!ShouldRefresh()) {
return token_.value();
}
PAIMON_RETURN_NOT_OK(RefreshToken());
return token_.value();
}
Result<std::map<std::string, std::string>> RestCredentialProvider::GetCredentials() const {
PAIMON_ASSIGN_OR_RAISE(RestToken token, ValidToken());
return token.token;
}
} // namespace paimon