Repository navigation
[feature](storage) Support native GCP IAM authentication #68540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sollhui
wants to merge
56
commits into
apache:master
Choose a base branch
from
sollhui:feature/gcp-native-iam-master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+11,903
−193
Open
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
513fce5
[feature](storage) Support native GCP IAM authentication
sollhui 6ddc932
[fix](storage) Honor native GCP Hadoop settings and reject token fail…
sollhui 75610a4
[fix](fe) Let resource ALTER override persisted GCS aliases
sollhui dec2d8d
[fix](cloud) Preserve vault credentials when clearing an unused IAM role
sollhui 238355a
[fix](cloud) Clear native credentials when replacing instance access …
sollhui 9545bc5
[chore](fe) Merge master and preserve GCP and UUID regression coverage
sollhui 3344664
[fix](storage) Restrict GCP OAuth endpoints and repair storage updates
sollhui a3ce68c
[fix](build) Fix GCP SDK error construction and FE test mocks
sollhui 7dc131d
[fix](storage) Correct native GCP identity and signed URL handling
sollhui 4c8c096
[fix](fe) Fix native GCP storage routing and Iceberg test key
sollhui f65ef0e
[fix](be) Initialize endpoint in GCS signed URL test
sollhui 888dee9
[fix](cloud) Clean up FilterCopyFilesTest sync callbacks
sollhui 4a5ae68
[fix](auth) Protect native GCP endpoints and vault credentials
sollhui 60254be
[fix](fe) Reject raw Hadoop auth overrides for native GCP
sollhui b9f6a16
[fix](fe) Preserve native GCP paths and vault identities
sollhui cacc8a4
[fix](storage) Complete native GCP scanner and credential paths
sollhui 2845cc6
[fix](fe) Align GCS authentication and Paimon URI handling
sollhui 409f1ea
[fix](build) Resolve master merge and Paimon FileIO compatibility
sollhui 9bf90ec
[fix](test) Use the storage adapter backend properties API
sollhui f479d05
[fix](fe) Allow resource ALTER to clear GCP impersonation
sollhui b60f30a
[fix](build) Scope optional GCS plugin closure dependencies
sollhui d4f4a8d
[fix](storage) Preserve GCP configuration on policy and Paimon REST p…
sollhui 0f59c5d
[fix](connector) Defer native GCP support for Paimon and Hudi
sollhui 8c18a56
[fix](connector) Remove deferred Paimon and Hudi auth guards
sollhui 4ef6ccf
[fix](fe) Preserve S3 selection and recognize GCS endpoint hosts
sollhui 30f208f
[fix](storage) Honor GCP signing CA and preserve Iceberg FileIO selec…
sollhui 1b99a05
[refactor](fe) isolate GCP authentication behind filesystem SPI
sollhui 131e521
[fix](iceberg) Pass storage Hadoop configuration to Glue FileIO
sollhui d8de579
[fix](storage) Preserve legacy GCP credentials and discard inactive a…
sollhui 50e84be
[fix](test) Avoid closing an uninitialized GCS filesystem
sollhui 5895190
[fix](build) Merge master and advance the connector API version
sollhui 5674070
[refactor](fe) Move native credential wire conversion into filesystem…
sollhui 254f601
[fix](build) Merge master and align Iceberg catalog test arguments
sollhui c7d9d04
[refactor](fe) Keep GCS routing and credential rules out of S3 plugins
sollhui 1889e17
[refactor](fe) Separate typed storage identities from wire adapters
sollhui 80d7fae
[fix](build) Merge master and resolve GCP IAM integration conflicts
sollhui 4757dcc
[fix](test) Align CI fixtures with current Variant and URI semantics
sollhui f187fb2
[fix](test) Revert unrelated CI fixture fixes from GCP IAM PR
sollhui 06cd1c2
[fix](storage) Resolve native GCP IAM conflicts with master
sollhui 413eb47
[fix](fe) Validate HDFS vault properties before checking connectivity
sollhui 47872fc
[fix](storage) Scope provider routing changes to GCS and S3 fallbacks
sollhui 4526bcd
[fix](fe) Preserve HDFS vault Kerberos validation errors
sollhui 4f00011
[fix](storage) Exchange service-account ADC for GCS OAuth tokens
sollhui 21766f5
[fix](fe) Map MinIO resource providers for storage policy pushes
sollhui b406815
[fix](fe) Normalize resource aliases for the GCS provider spelling
sollhui cd81402
[fix](storage) Fix GCS resource validation and OAuth test setup
sollhui 67767d4
[fix](fe) Map GCS alias to GCP in cloud object store adapter
sollhui b14c332
[fix](iceberg) Isolate GCS FileIO settings from Glue clients
sollhui 50d22fc
[chore](fe) Merge master and reconcile connector API versions
sollhui 33dc40d
[fix](iceberg) Separate metadata credentials from GCS object authenti…
sollhui 0505a9f
[fix](storage) Preserve legacy GCS static key precedence
sollhui 7d62677
[fix](storage) Preserve resource and vault ALTER patch semantics
sollhui 9f13b43
[refactor](fe) Separate filesystem routing validation and fallback hooks
sollhui c81f510
Merge independent storage ALTER and filesystem routing prerequisites
sollhui 3ba41de
[refactor](storage) Integrate independently reviewable GCP prerequisites
sollhui 27707c6
[refactor](storage) Focus native GCP IAM review on provider support
sollhui File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| // 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 "io/fs/gcs_signed_url_provider.h" | ||
|
|
||
| #include <fmt/format.h> | ||
| #include <rapidjson/document.h> | ||
|
|
||
| #include <exception> | ||
| #include <string_view> | ||
| #include <utility> | ||
|
|
||
| #include "cpp/obj-client/auth/gcp/gcp_token_provider.h" | ||
| #include "cpp/obj-client/auth/gcp/gcs_signed_url.h" | ||
| #include "cpp/sync_point.h" | ||
| #include "service/http/http_client.h" | ||
| #include "util/url_coding.h" | ||
|
|
||
| namespace doris::io { | ||
| namespace { | ||
|
|
||
| constexpr std::string_view IAM_CREDENTIALS_ENDPOINT = | ||
| "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"; | ||
|
|
||
| std::string iam_error_message(const rapidjson::Document& document) { | ||
| if (!document.IsObject() || !document.HasMember("error") || !document["error"].IsObject()) { | ||
| return "unparseable error response"; | ||
| } | ||
| const auto& error = document["error"]; | ||
| std::string message; | ||
| if (error.HasMember("status") && error["status"].IsString()) { | ||
| message = error["status"].GetString(); | ||
| } | ||
| if (error.HasMember("message") && error["message"].IsString()) { | ||
| if (!message.empty()) { | ||
| message.append(": "); | ||
| } | ||
| message.append(error["message"].GetString()); | ||
| } | ||
| if (message.empty()) { | ||
| return "error response did not contain status or message"; | ||
| } | ||
| constexpr size_t MAX_ERROR_MESSAGE_SIZE = 1024; | ||
| if (message.size() > MAX_ERROR_MESSAGE_SIZE) { | ||
| message.resize(MAX_ERROR_MESSAGE_SIZE); | ||
| message.append("..."); | ||
| } | ||
| return message; | ||
| } | ||
|
|
||
| bool is_unreserved(unsigned char c) { | ||
| return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || | ||
| c == '_' || c == '.' || c == '~'; | ||
| } | ||
|
|
||
| std::string percent_encode(std::string_view value) { | ||
| constexpr char HEX[] = "0123456789ABCDEF"; | ||
| std::string encoded; | ||
| encoded.reserve(value.size()); | ||
| for (unsigned char c : value) { | ||
| if (is_unreserved(c)) { | ||
| encoded.push_back(static_cast<char>(c)); | ||
| continue; | ||
| } | ||
| encoded.push_back('%'); | ||
| encoded.push_back(HEX[c >> 4]); | ||
| encoded.push_back(HEX[c & 0x0F]); | ||
| } | ||
| return encoded; | ||
| } | ||
|
|
||
| Status call_iam_sign_blob(std::string_view access_token, std::string_view service_account, | ||
| std::string_view string_to_sign, int64_t request_timeout_ms, | ||
| const std::string& ca_cert_file_path, std::string* signature) { | ||
| std::string encoded_payload; | ||
| base64_encode(std::string(string_to_sign), &encoded_payload); | ||
| std::string request_body = fmt::format(R"({{"payload":"{}"}})", encoded_payload); | ||
| std::string endpoint = | ||
| std::string(IAM_CREDENTIALS_ENDPOINT) + percent_encode(service_account) + ":signBlob"; | ||
|
|
||
| HttpClient client; | ||
| // Keep the response body for non-2xx replies so IAM permission and | ||
| // service-account errors are actionable to operators. | ||
| RETURN_IF_ERROR(client.init(endpoint, false, HttpClient::AuthTokenMode::NONE)); | ||
| if (!ca_cert_file_path.empty()) { | ||
| RETURN_IF_ERROR(client.set_ca_cert_file(ca_cert_file_path)); | ||
| } | ||
| client.set_authorization("Bearer " + std::string(access_token)); | ||
| client.set_content_type("application/json"); | ||
| client.set_timeout_ms(request_timeout_ms > 0 ? request_timeout_ms : 10000); | ||
|
|
||
| TEST_SYNC_POINT_RETURN_WITH_VALUE("GcsV4Signer::sign_blob", Status::OK(), access_token, | ||
| service_account, string_to_sign, signature); | ||
| std::string response; | ||
| RETURN_IF_ERROR(client.execute_post_request(request_body, &response)); | ||
|
|
||
| rapidjson::Document document; | ||
| document.Parse(response.data(), response.size()); | ||
| const auto http_status = client.get_http_status(); | ||
| if (http_status < 200 || http_status >= 300) { | ||
| return Status::HttpError("IAM signBlob failed with HTTP {}: {}", http_status, | ||
| iam_error_message(document)); | ||
| } | ||
| if (document.HasParseError() || !document.IsObject() || !document.HasMember("signedBlob") || | ||
| !document["signedBlob"].IsString()) { | ||
| return Status::InternalError("IAM signBlob returned an invalid response"); | ||
| } | ||
| if (!base64_decode(document["signedBlob"].GetString(), signature)) { | ||
| return Status::InternalError("IAM signBlob returned an invalid base64 signature"); | ||
| } | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| Status resolve_gcs_signer_email(const GcpCredentialConfig& credential, | ||
| const GcpTokenProvider& token_provider, std::string* signer_email) { | ||
| *signer_email = credential.impersonation_service_account.empty() | ||
| ? token_provider.get_service_account_email() | ||
| : credential.impersonation_service_account; | ||
| if (!is_valid_gcp_service_account_email(*signer_email)) { | ||
| return Status::InvalidArgument( | ||
| "GCS V4 signing requires a service account identity; configure " | ||
| "gs.impersonation_service_account for credentials without one"); | ||
| } | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status generate_gcs_v4_signed_url(const GcsV4SignedUrlProviderOptions& options, | ||
| const GcpCredentialConfig& credential, | ||
| const std::shared_ptr<GcpTokenProvider>& token_provider, | ||
| std::string* signed_url) { | ||
| if (signed_url == nullptr) { | ||
| return Status::InvalidArgument("signed_url output must not be null"); | ||
| } | ||
| signed_url->clear(); | ||
|
|
||
| if (token_provider == nullptr) { | ||
| return Status::InternalError("GCS V4 signing token provider is not initialized"); | ||
| } | ||
|
|
||
| try { | ||
| auto token = token_provider->get_token(); | ||
| if (!token.has_value()) { | ||
| return Status::InternalError("failed to obtain OAuth token for IAM signBlob"); | ||
| } | ||
|
|
||
| // Resolve identity from the same credentials that supplied the token. DEFAULT may | ||
| // select a VM, a key file, or another ADC source; never infer it from VM presence. | ||
| std::string signer_email; | ||
| RETURN_IF_ERROR(resolve_gcs_signer_email(credential, *token_provider, &signer_email)); | ||
|
|
||
| auto result = build_gcs_v4_signed_url( | ||
| {.endpoint = options.endpoint, | ||
| .bucket = options.bucket, | ||
| .key = options.key, | ||
| .signer_email = signer_email, | ||
| .expiration_secs = options.expiration_secs}, | ||
| std::chrono::system_clock::now(), [&](std::string_view string_to_sign) { | ||
| std::string signature; | ||
| auto status = call_iam_sign_blob(*token, signer_email, string_to_sign, | ||
| options.request_timeout_ms, | ||
| options.ca_cert_file_path, &signature); | ||
| if (!status.ok()) { | ||
| return GcsSignBlobResult {.error = status.to_string()}; | ||
| } | ||
| return GcsSignBlobResult {.signature = std::move(signature)}; | ||
| }); | ||
| if (!result.ok()) { | ||
| return Status::InternalError("failed to build GCS V4 signed URL: {}", result.error); | ||
| } | ||
| *signed_url = std::move(result.signed_url); | ||
| return Status::OK(); | ||
| } catch (const std::exception& e) { | ||
| return Status::InternalError("failed to initialize GCS URL signer: {}", e.what()); | ||
| } | ||
| } | ||
|
|
||
| } // namespace doris::io | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // 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. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <cstdint> | ||
| #include <memory> | ||
| #include <string> | ||
|
|
||
| #include "common/status.h" | ||
| #include "cpp/obj-client/auth/gcp/gcp_auth.h" | ||
|
|
||
| namespace doris { | ||
| class GcpTokenProvider; | ||
| } | ||
|
|
||
| namespace doris::io { | ||
|
|
||
| struct GcsV4SignedUrlProviderOptions { | ||
| std::string endpoint; | ||
| std::string bucket; | ||
| std::string key; | ||
| int64_t expiration_secs = 0; | ||
| int64_t request_timeout_ms = 10000; | ||
| std::string ca_cert_file_path; | ||
| }; | ||
|
|
||
| // Resolve the identity selected by ADC or explicit impersonation, without falling back | ||
| // to an unrelated VM service account when the selected credentials have no identity. | ||
| Status resolve_gcs_signer_email(const GcpCredentialConfig& credential, | ||
| const GcpTokenProvider& token_provider, std::string* signer_email); | ||
|
|
||
| // Resolves the signing service account, obtains an OAuth token, and invokes | ||
| // IAM Credentials signBlob before delegating URL construction to common auth. | ||
| Status generate_gcs_v4_signed_url(const GcsV4SignedUrlProviderOptions& options, | ||
| const GcpCredentialConfig& credential, | ||
| const std::shared_ptr<GcpTokenProvider>& token_provider, | ||
| std::string* signed_url); | ||
|
|
||
| } // namespace doris::io |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Pass the configured CA to IAM signBlob. When an outbound HTTPS proxy uses a private CA supplied only through ca_cert_file_paths, the GCS object client and OAuth token provider trust it, but this HttpClient uses libcurl default CAs. Uploading a load error log can succeed while signBlob fails TLS verification, so the returned error-log path stays BE-local instead of becoming a usable URL. Use the selected CA for this request too.