Skip to content

Commit 72d3831

Browse files
authored
feat(rest): add initial oauth2 support to rest catalog (#577)
Add OAuth2 authentication support for the REST catalog, including: - OAuth2Manager with static token and client_credentials grant flows TODO: - RefreshToken and ExchangeToken will be supported later
1 parent 31aa68a commit 72d3831

19 files changed

+663
-35
lines changed

src/iceberg/catalog/rest/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ add_subdirectory(auth)
2020
set(ICEBERG_REST_SOURCES
2121
auth/auth_manager.cc
2222
auth/auth_managers.cc
23+
auth/auth_properties.cc
2324
auth/auth_session.cc
25+
auth/oauth2_util.cc
2426
catalog_properties.cc
2527
endpoint.cc
2628
error_handlers.cc

src/iceberg/catalog/rest/auth/auth_manager.cc

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919

2020
#include "iceberg/catalog/rest/auth/auth_manager.h"
2121

22+
#include <optional>
23+
2224
#include "iceberg/catalog/rest/auth/auth_manager_internal.h"
2325
#include "iceberg/catalog/rest/auth/auth_properties.h"
2426
#include "iceberg/catalog/rest/auth/auth_session.h"
27+
#include "iceberg/catalog/rest/auth/oauth2_util.h"
2528
#include "iceberg/util/macros.h"
2629
#include "iceberg/util/transform_util.h"
2730

@@ -80,7 +83,8 @@ class BasicAuthManager : public AuthManager {
8083
"Missing required property '{}'", AuthProperties::kBasicPassword);
8184
std::string credential = username_it->second + ":" + password_it->second;
8285
return AuthSession::MakeDefault(
83-
{{"Authorization", "Basic " + TransformUtil::Base64Encode(credential)}});
86+
{{std::string(kAuthorizationHeader),
87+
"Basic " + TransformUtil::Base64Encode(credential)}});
8488
}
8589
};
8690

@@ -90,4 +94,76 @@ Result<std::unique_ptr<AuthManager>> MakeBasicAuthManager(
9094
return std::make_unique<BasicAuthManager>();
9195
}
9296

97+
/// \brief OAuth2 authentication manager.
98+
class OAuth2Manager : public AuthManager {
99+
public:
100+
Result<std::shared_ptr<AuthSession>> InitSession(
101+
HttpClient& init_client,
102+
const std::unordered_map<std::string, std::string>& properties) override {
103+
ICEBERG_ASSIGN_OR_RAISE(auto config, AuthProperties::FromProperties(properties));
104+
// No token refresh during init (short-lived session).
105+
config.Set(AuthProperties::kKeepRefreshed, false);
106+
107+
// Credential takes priority: fetch a fresh token for the config request.
108+
if (!config.credential().empty()) {
109+
auto init_session = AuthSession::MakeDefault(AuthHeaders(config.token()));
110+
ICEBERG_ASSIGN_OR_RAISE(init_token_response_,
111+
FetchToken(init_client, *init_session, config));
112+
return AuthSession::MakeDefault(AuthHeaders(init_token_response_->access_token));
113+
}
114+
115+
if (!config.token().empty()) {
116+
return AuthSession::MakeDefault(AuthHeaders(config.token()));
117+
}
118+
119+
return AuthSession::MakeDefault({});
120+
}
121+
122+
Result<std::shared_ptr<AuthSession>> CatalogSession(
123+
HttpClient& client,
124+
const std::unordered_map<std::string, std::string>& properties) override {
125+
ICEBERG_ASSIGN_OR_RAISE(auto config, AuthProperties::FromProperties(properties));
126+
127+
// Reuse token from init phase.
128+
if (init_token_response_.has_value()) {
129+
auto token_response = std::move(*init_token_response_);
130+
init_token_response_.reset();
131+
return AuthSession::MakeOAuth2(token_response, config.oauth2_server_uri(),
132+
config.client_id(), config.client_secret(),
133+
config.scope(), client);
134+
}
135+
136+
// If token is provided, use it directly.
137+
if (!config.token().empty()) {
138+
return AuthSession::MakeDefault(AuthHeaders(config.token()));
139+
}
140+
141+
// Fetch a new token using client_credentials grant.
142+
if (!config.credential().empty()) {
143+
auto base_session = AuthSession::MakeDefault(AuthHeaders(config.token()));
144+
OAuthTokenResponse token_response;
145+
ICEBERG_ASSIGN_OR_RAISE(token_response, FetchToken(client, *base_session, config));
146+
// TODO(lishuxu): should we directly pass config to the MakeOAuth2 call?
147+
return AuthSession::MakeOAuth2(token_response, config.oauth2_server_uri(),
148+
config.client_id(), config.client_secret(),
149+
config.scope(), client);
150+
}
151+
152+
return AuthSession::MakeDefault({});
153+
}
154+
155+
// TODO(lishuxu): Override TableSession() for token exchange (RFC 8693).
156+
// TODO(lishuxu): Override ContextualSession() for per-context exchange.
157+
158+
private:
159+
/// Cached token from InitSession
160+
std::optional<OAuthTokenResponse> init_token_response_;
161+
};
162+
163+
Result<std::unique_ptr<AuthManager>> MakeOAuth2Manager(
164+
[[maybe_unused]] std::string_view name,
165+
[[maybe_unused]] const std::unordered_map<std::string, std::string>& properties) {
166+
return std::make_unique<OAuth2Manager>();
167+
}
168+
93169
} // namespace iceberg::rest::auth

src/iceberg/catalog/rest/auth/auth_manager_internal.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ Result<std::unique_ptr<AuthManager>> MakeBasicAuthManager(
4242
std::string_view name,
4343
const std::unordered_map<std::string, std::string>& properties);
4444

45+
/// \brief Create an OAuth2 authentication manager.
46+
Result<std::unique_ptr<AuthManager>> MakeOAuth2Manager(
47+
std::string_view name,
48+
const std::unordered_map<std::string, std::string>& properties);
49+
4550
} // namespace iceberg::rest::auth

src/iceberg/catalog/rest/auth/auth_managers.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ std::string InferAuthType(
5252
}
5353

5454
// Infer from OAuth2 properties (credential or token)
55-
bool has_credential = properties.contains(AuthProperties::kOAuth2Credential);
56-
bool has_token = properties.contains(AuthProperties::kOAuth2Token);
55+
bool has_credential = properties.contains(AuthProperties::kCredential.key());
56+
bool has_token = properties.contains(AuthProperties::kToken.key());
5757
if (has_credential || has_token) {
5858
return AuthProperties::kAuthTypeOAuth2;
5959
}
@@ -65,6 +65,7 @@ AuthManagerRegistry CreateDefaultRegistry() {
6565
return {
6666
{AuthProperties::kAuthTypeNone, MakeNoopAuthManager},
6767
{AuthProperties::kAuthTypeBasic, MakeBasicAuthManager},
68+
{AuthProperties::kAuthTypeOAuth2, MakeOAuth2Manager},
6869
};
6970
}
7071

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/catalog/rest/auth/auth_properties.h"
21+
22+
#include <utility>
23+
24+
#include "iceberg/catalog/rest/catalog_properties.h"
25+
26+
namespace iceberg::rest::auth {
27+
28+
namespace {
29+
30+
std::pair<std::string, std::string> ParseCredential(const std::string& credential) {
31+
auto colon_pos = credential.find(':');
32+
if (colon_pos == std::string::npos) {
33+
return {"", credential};
34+
}
35+
return {credential.substr(0, colon_pos), credential.substr(colon_pos + 1)};
36+
}
37+
38+
} // namespace
39+
40+
std::unordered_map<std::string, std::string> AuthProperties::optional_oauth_params()
41+
const {
42+
std::unordered_map<std::string, std::string> params;
43+
if (auto audience = Get(kAudience); !audience.empty()) {
44+
params.emplace(kAudience.key(), std::move(audience));
45+
}
46+
if (auto resource = Get(kResource); !resource.empty()) {
47+
params.emplace(kResource.key(), std::move(resource));
48+
}
49+
return params;
50+
}
51+
52+
Result<AuthProperties> AuthProperties::FromProperties(
53+
const std::unordered_map<std::string, std::string>& properties) {
54+
AuthProperties config;
55+
config.configs_ = properties;
56+
57+
// Parse client_id/client_secret from credential
58+
if (auto cred = config.credential(); !cred.empty()) {
59+
auto [id, secret] = ParseCredential(cred);
60+
config.client_id_ = std::move(id);
61+
config.client_secret_ = std::move(secret);
62+
}
63+
64+
// Resolve token endpoint: if not explicitly set, derive from catalog URI
65+
if (properties.find(kOAuth2ServerUri.key()) == properties.end() ||
66+
properties.at(kOAuth2ServerUri.key()).empty()) {
67+
auto uri_it = properties.find(RestCatalogProperties::kUri.key());
68+
if (uri_it != properties.end() && !uri_it->second.empty()) {
69+
std::string_view base = uri_it->second;
70+
while (!base.empty() && base.back() == '/') {
71+
base.remove_suffix(1);
72+
}
73+
config.Set(kOAuth2ServerUri,
74+
std::string(base) + "/" + std::string(kOAuth2ServerUri.value()));
75+
}
76+
}
77+
78+
// TODO(lishuxu): Parse JWT exp claim from token to set expires_at_millis_.
79+
80+
return config;
81+
}
82+
83+
} // namespace iceberg::rest::auth

src/iceberg/catalog/rest/auth/auth_properties.h

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,55 +19,88 @@
1919

2020
#pragma once
2121

22+
#include <cstdint>
23+
#include <optional>
2224
#include <string>
23-
#include <string_view>
25+
#include <unordered_map>
26+
27+
#include "iceberg/catalog/rest/iceberg_rest_export.h"
28+
#include "iceberg/result.h"
29+
#include "iceberg/util/config.h"
2430

2531
/// \file iceberg/catalog/rest/auth/auth_properties.h
26-
/// \brief Property keys and constants for REST catalog authentication.
32+
/// \brief Property keys and configuration for REST catalog authentication.
2733

2834
namespace iceberg::rest::auth {
2935

30-
/// \brief Property keys and constants for authentication configuration.
31-
///
32-
/// This struct defines all the property keys used to configure authentication
33-
/// for the REST catalog. It follows the same naming conventions as Java Iceberg.
34-
struct AuthProperties {
35-
/// \brief Property key for specifying the authentication type.
36+
/// \brief Authentication properties
37+
class ICEBERG_REST_EXPORT AuthProperties : public ConfigBase<AuthProperties> {
38+
public:
39+
template <typename T>
40+
using Entry = const ConfigBase<AuthProperties>::Entry<T>;
41+
42+
// ---- Authentication type constants (not Entry-based) ----
43+
3644
inline static const std::string kAuthType = "rest.auth.type";
37-
/// \brief Authentication type: no authentication.
3845
inline static const std::string kAuthTypeNone = "none";
39-
/// \brief Authentication type: HTTP Basic authentication.
4046
inline static const std::string kAuthTypeBasic = "basic";
41-
/// \brief Authentication type: OAuth2 authentication.
4247
inline static const std::string kAuthTypeOAuth2 = "oauth2";
43-
/// \brief Authentication type: AWS SigV4 authentication.
4448
inline static const std::string kAuthTypeSigV4 = "sigv4";
4549

46-
/// \brief Property key for Basic auth username.
50+
// ---- Basic auth entries ----
51+
4752
inline static const std::string kBasicUsername = "rest.auth.basic.username";
48-
/// \brief Property key for Basic auth password.
4953
inline static const std::string kBasicPassword = "rest.auth.basic.password";
5054

51-
/// \brief Property key for OAuth2 token (bearer token).
52-
inline static const std::string kOAuth2Token = "token";
53-
/// \brief Property key for OAuth2 credential (client_id:client_secret).
54-
inline static const std::string kOAuth2Credential = "credential";
55-
/// \brief Property key for OAuth2 scope.
56-
inline static const std::string kOAuth2Scope = "scope";
57-
/// \brief Property key for OAuth2 server URI.
58-
inline static const std::string kOAuth2ServerUri = "oauth2-server-uri";
59-
/// \brief Property key for enabling token refresh.
60-
inline static const std::string kOAuth2TokenRefreshEnabled = "token-refresh-enabled";
61-
/// \brief Default OAuth2 scope for catalog operations.
62-
inline static const std::string kOAuth2DefaultScope = "catalog";
63-
64-
/// \brief Property key for SigV4 region.
55+
// ---- SigV4 entries ----
56+
6557
inline static const std::string kSigV4Region = "rest.auth.sigv4.region";
66-
/// \brief Property key for SigV4 service name.
6758
inline static const std::string kSigV4Service = "rest.auth.sigv4.service";
68-
/// \brief Property key for SigV4 delegate auth type.
6959
inline static const std::string kSigV4DelegateAuthType =
7060
"rest.auth.sigv4.delegate-auth-type";
61+
62+
// ---- OAuth2 entries ----
63+
64+
inline static Entry<std::string> kToken{"token", ""};
65+
inline static Entry<std::string> kCredential{"credential", ""};
66+
inline static Entry<std::string> kScope{"scope", "catalog"};
67+
inline static Entry<std::string> kOAuth2ServerUri{"oauth2-server-uri",
68+
"v1/oauth/tokens"};
69+
inline static Entry<bool> kKeepRefreshed{"token-refresh-enabled", true};
70+
inline static Entry<bool> kExchangeEnabled{"token-exchange-enabled", true};
71+
inline static Entry<std::string> kAudience{"audience", ""};
72+
inline static Entry<std::string> kResource{"resource", ""};
73+
74+
/// \brief Build an AuthProperties from a properties map.
75+
static Result<AuthProperties> FromProperties(
76+
const std::unordered_map<std::string, std::string>& properties);
77+
78+
/// \brief Get the bearer token.
79+
std::string token() const { return Get(kToken); }
80+
/// \brief Get the raw credential string.
81+
std::string credential() const { return Get(kCredential); }
82+
/// \brief Get the OAuth2 scope.
83+
std::string scope() const { return Get(kScope); }
84+
/// \brief Get the token endpoint URI.
85+
std::string oauth2_server_uri() const { return Get(kOAuth2ServerUri); }
86+
/// \brief Whether token refresh is enabled.
87+
bool keep_refreshed() const { return Get(kKeepRefreshed); }
88+
/// \brief Whether token exchange is enabled.
89+
bool exchange_enabled() const { return Get(kExchangeEnabled); }
90+
91+
/// \brief Parsed client_id from credential (empty if no colon).
92+
const std::string& client_id() const { return client_id_; }
93+
/// \brief Parsed client_secret from credential.
94+
const std::string& client_secret() const { return client_secret_; }
95+
96+
/// \brief Build optional OAuth params (audience, resource) from config.
97+
std::unordered_map<std::string, std::string> optional_oauth_params() const;
98+
99+
private:
100+
std::string client_id_;
101+
std::string client_secret_;
102+
std::string token_type_;
103+
std::optional<int64_t> expires_at_millis_;
71104
};
72105

73106
} // namespace iceberg::rest::auth

src/iceberg/catalog/rest/auth/auth_session.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
#include <utility>
2323

24+
#include "iceberg/catalog/rest/auth/oauth2_util.h"
25+
2426
namespace iceberg::rest::auth {
2527

2628
namespace {
@@ -49,4 +51,13 @@ std::shared_ptr<AuthSession> AuthSession::MakeDefault(
4951
return std::make_shared<DefaultAuthSession>(std::move(headers));
5052
}
5153

54+
std::shared_ptr<AuthSession> AuthSession::MakeOAuth2(
55+
const OAuthTokenResponse& initial_token, const std::string& /*token_endpoint*/,
56+
const std::string& /*client_id*/, const std::string& /*client_secret*/,
57+
const std::string& /*scope*/, HttpClient& /*client*/) {
58+
// TODO(lishuxu): Create OAuth2AuthSession with auto-refresh support.
59+
return MakeDefault({{std::string(kAuthorizationHeader),
60+
std::string(kBearerPrefix) + initial_token.access_token}});
61+
}
62+
5263
} // namespace iceberg::rest::auth

0 commit comments

Comments
 (0)