wgtmac commented on code in PR #577: URL: https://github.com/apache/iceberg-cpp/pull/577#discussion_r2925584625
########## src/iceberg/catalog/rest/auth/oauth2_util.h: ########## @@ -0,0 +1,82 @@ +/* + * 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 <string> + +#include "iceberg/catalog/rest/iceberg_rest_export.h" +#include "iceberg/catalog/rest/type_fwd.h" +#include "iceberg/result.h" + +/// \file iceberg/catalog/rest/auth/oauth2_util.h +/// \brief OAuth2 token utilities for REST catalog authentication. + +namespace iceberg::rest::auth { + +/// \brief Response from an OAuth2 token endpoint. +struct ICEBERG_REST_EXPORT OAuthTokenResponse { + std::string access_token; // required + std::string token_type; // required, typically "bearer" + int64_t expires_in = 0; // optional, seconds until expiration Review Comment: ```suggestion std::optional<int64_t> expires_in; // optional, seconds until expiration ``` Let's use optional because the default `0` will cause trouble. ########## src/iceberg/catalog/rest/auth/oauth2_util.h: ########## @@ -0,0 +1,82 @@ +/* + * 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 <string> + +#include "iceberg/catalog/rest/iceberg_rest_export.h" +#include "iceberg/catalog/rest/type_fwd.h" +#include "iceberg/result.h" + +/// \file iceberg/catalog/rest/auth/oauth2_util.h +/// \brief OAuth2 token utilities for REST catalog authentication. + +namespace iceberg::rest::auth { + +/// \brief Response from an OAuth2 token endpoint. +struct ICEBERG_REST_EXPORT OAuthTokenResponse { Review Comment: Why this is defined here but not in the `src/iceberg/catalog/rest/types.h`? ########## src/iceberg/catalog/rest/auth/oauth2_util.h: ########## @@ -0,0 +1,82 @@ +/* + * 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 <string> + +#include "iceberg/catalog/rest/iceberg_rest_export.h" +#include "iceberg/catalog/rest/type_fwd.h" +#include "iceberg/result.h" + +/// \file iceberg/catalog/rest/auth/oauth2_util.h +/// \brief OAuth2 token utilities for REST catalog authentication. + +namespace iceberg::rest::auth { + +/// \brief Response from an OAuth2 token endpoint. +struct ICEBERG_REST_EXPORT OAuthTokenResponse { + std::string access_token; // required + std::string token_type; // required, typically "bearer" + int64_t expires_in = 0; // optional, seconds until expiration + std::string refresh_token; // optional + std::string scope; // optional + + /// \brief Validates the token response. + Status Validate() const; + + bool operator==(const OAuthTokenResponse&) const = default; +}; + +/// \brief Parse an OAuthTokenResponse from a JSON string. +/// +/// \param json_str The JSON string to parse. +/// \return The parsed token response or an error. +ICEBERG_REST_EXPORT Result<OAuthTokenResponse> OAuthTokenResponseFromJsonString( + const std::string& json_str); + +/// \brief Fetch an OAuth2 token using the client_credentials grant type. +/// +/// Sends a POST request with form-encoded body to the token endpoint: +/// grant_type=client_credentials&client_id=...&client_secret=...&scope=... +/// +/// \param client HTTP client to use for the request. +/// \param token_endpoint Full URL of the OAuth2 token endpoint. +/// \param client_id OAuth2 client ID. +/// \param client_secret OAuth2 client secret. +/// \param scope OAuth2 scope to request. +/// \param session Auth session for the request (typically a no-op session). +/// \return The token response or an error. +ICEBERG_REST_EXPORT Result<OAuthTokenResponse> FetchToken( Review Comment: It looks strange that `AuthSession& session` is required here so we have to create a AuthSession anyway before calling this. Should we use the same input parameter list as the Java api? ########## src/iceberg/catalog/rest/auth/auth_session.cc: ########## @@ -49,4 +51,12 @@ std::shared_ptr<AuthSession> AuthSession::MakeDefault( return std::make_shared<DefaultAuthSession>(std::move(headers)); } +std::shared_ptr<AuthSession> AuthSession::MakeOAuth2( + const OAuthTokenResponse& initial_token, const std::string& /*token_endpoint*/, + const std::string& /*client_id*/, const std::string& /*client_secret*/, + const std::string& /*scope*/, HttpClient& /*client*/) { + // TODO(lishuxu): Create OAuth2AuthSession with auto-refresh support. + return MakeDefault({{"Authorization", "Bearer " + initial_token.access_token}}); Review Comment: Can we avoid magic string like `"Authorization"` and `"Bearer "` by defining them as constants in `src/iceberg/catalog/rest/oauth2_util.h`? ########## src/iceberg/catalog/rest/auth/auth_manager.cc: ########## @@ -90,4 +94,136 @@ Result<std::unique_ptr<AuthManager>> MakeBasicAuthManager( return std::make_unique<BasicAuthManager>(); } +/// \brief OAuth2 authentication manager. +/// +/// Two-phase init: InitSession fetches and caches a token for the config request; +/// CatalogSession reuses the cached token and enables refresh. +class OAuth2AuthManager : public AuthManager { + public: + Result<std::shared_ptr<AuthSession>> InitSession( + HttpClient& init_client, + const std::unordered_map<std::string, std::string>& properties) override { + // Credential takes priority: fetch a fresh token for the config request. + auto credential_it = properties.find(AuthProperties::kOAuth2Credential); + if (credential_it != properties.end() && !credential_it->second.empty()) { + ICEBERG_ASSIGN_OR_RAISE(auto ctx, ParseOAuth2Context(properties)); + auto noop_session = AuthSession::MakeDefault({}); + ICEBERG_ASSIGN_OR_RAISE(init_token_response_, + FetchToken(init_client, ctx.token_endpoint, ctx.client_id, + ctx.client_secret, ctx.scope, *noop_session)); + return AuthSession::MakeDefault( + {{"Authorization", "Bearer " + init_token_response_->access_token}}); Review Comment: ditto, let's avoid magic strings scattered around. ########## src/iceberg/catalog/rest/auth/auth_manager.cc: ########## @@ -90,4 +94,136 @@ Result<std::unique_ptr<AuthManager>> MakeBasicAuthManager( return std::make_unique<BasicAuthManager>(); } +/// \brief OAuth2 authentication manager. +/// +/// Two-phase init: InitSession fetches and caches a token for the config request; +/// CatalogSession reuses the cached token and enables refresh. +class OAuth2AuthManager : public AuthManager { + public: + Result<std::shared_ptr<AuthSession>> InitSession( + HttpClient& init_client, + const std::unordered_map<std::string, std::string>& properties) override { + // Credential takes priority: fetch a fresh token for the config request. + auto credential_it = properties.find(AuthProperties::kOAuth2Credential); + if (credential_it != properties.end() && !credential_it->second.empty()) { + ICEBERG_ASSIGN_OR_RAISE(auto ctx, ParseOAuth2Context(properties)); + auto noop_session = AuthSession::MakeDefault({}); + ICEBERG_ASSIGN_OR_RAISE(init_token_response_, + FetchToken(init_client, ctx.token_endpoint, ctx.client_id, + ctx.client_secret, ctx.scope, *noop_session)); + return AuthSession::MakeDefault( + {{"Authorization", "Bearer " + init_token_response_->access_token}}); + } + + auto token_it = properties.find(AuthProperties::kOAuth2Token); + if (token_it != properties.end() && !token_it->second.empty()) { + return AuthSession::MakeDefault({{"Authorization", "Bearer " + token_it->second}}); + } + + return AuthSession::MakeDefault({}); + } + + Result<std::shared_ptr<AuthSession>> CatalogSession( + HttpClient& client, + const std::unordered_map<std::string, std::string>& properties) override { + if (init_token_response_.has_value()) { + auto token_response = std::move(*init_token_response_); + init_token_response_.reset(); + ICEBERG_ASSIGN_OR_RAISE(auto ctx, ParseOAuth2Context(properties)); + return AuthSession::MakeOAuth2(token_response, ctx.token_endpoint, ctx.client_id, + ctx.client_secret, ctx.scope, client); + } + + // If token is provided, use it directly. + auto token_it = properties.find(AuthProperties::kOAuth2Token); + if (token_it != properties.end() && !token_it->second.empty()) { + return AuthSession::MakeDefault({{"Authorization", "Bearer " + token_it->second}}); + } + + // Fetch a new token using client_credentials grant. + auto credential_it = properties.find(AuthProperties::kOAuth2Credential); + if (credential_it != properties.end() && !credential_it->second.empty()) { + ICEBERG_ASSIGN_OR_RAISE(auto ctx, ParseOAuth2Context(properties)); + auto noop_session = AuthSession::MakeDefault({}); + OAuthTokenResponse token_response; + ICEBERG_ASSIGN_OR_RAISE(token_response, + FetchToken(client, ctx.token_endpoint, ctx.client_id, + ctx.client_secret, ctx.scope, *noop_session)); + return AuthSession::MakeOAuth2(token_response, ctx.token_endpoint, ctx.client_id, + ctx.client_secret, ctx.scope, client); + } + + return AuthSession::MakeDefault({}); + } + + // TODO(lishuxu): Override TableSession() to support token exchange (RFC 8693). + // TODO(lishuxu): Override ContextualSession() to support per-context token exchange. + + private: + struct OAuth2Context { Review Comment: Is this trying to mirror the `AuthConfig` in Java? Perhaps we should extend it from `ConfigBase` to accept a map for better extensibility. ########## src/iceberg/catalog/rest/auth/oauth2_util.h: ########## @@ -0,0 +1,82 @@ +/* + * 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 <string> + +#include "iceberg/catalog/rest/iceberg_rest_export.h" +#include "iceberg/catalog/rest/type_fwd.h" +#include "iceberg/result.h" + +/// \file iceberg/catalog/rest/auth/oauth2_util.h +/// \brief OAuth2 token utilities for REST catalog authentication. + +namespace iceberg::rest::auth { + +/// \brief Response from an OAuth2 token endpoint. +struct ICEBERG_REST_EXPORT OAuthTokenResponse { + std::string access_token; // required + std::string token_type; // required, typically "bearer" + int64_t expires_in = 0; // optional, seconds until expiration + std::string refresh_token; // optional + std::string scope; // optional + + /// \brief Validates the token response. + Status Validate() const; + + bool operator==(const OAuthTokenResponse&) const = default; +}; + +/// \brief Parse an OAuthTokenResponse from a JSON string. +/// +/// \param json_str The JSON string to parse. +/// \return The parsed token response or an error. +ICEBERG_REST_EXPORT Result<OAuthTokenResponse> OAuthTokenResponseFromJsonString( Review Comment: ditto, should this be moved to `src/iceberg/catalog/rest/json_serde_internal.h` and also add its `ToJson`? -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
