Skip to content

Commit bf0f26e

Browse files
committed
redesign api
1 parent 8432389 commit bf0f26e

16 files changed

Lines changed: 372 additions & 247 deletions

src/iceberg/catalog/rest/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ 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
2425
auth/oauth2_util.cc
2526
catalog_properties.cc

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

Lines changed: 32 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
#include "iceberg/catalog/rest/auth/auth_properties.h"
2626
#include "iceberg/catalog/rest/auth/auth_session.h"
2727
#include "iceberg/catalog/rest/auth/oauth2_util.h"
28-
#include "iceberg/catalog/rest/catalog_properties.h"
2928
#include "iceberg/util/macros.h"
3029
#include "iceberg/util/transform_util.h"
3130

@@ -84,7 +83,8 @@ class BasicAuthManager : public AuthManager {
8483
"Missing required property '{}'", AuthProperties::kBasicPassword);
8584
std::string credential = username_it->second + ":" + password_it->second;
8685
return AuthSession::MakeDefault(
87-
{{"Authorization", "Basic " + TransformUtil::Base64Encode(credential)}});
86+
{{std::string(kAuthorizationHeader),
87+
"Basic " + TransformUtil::Base64Encode(credential)}});
8888
}
8989
};
9090

@@ -95,29 +95,25 @@ Result<std::unique_ptr<AuthManager>> MakeBasicAuthManager(
9595
}
9696

9797
/// \brief OAuth2 authentication manager.
98-
///
99-
/// Two-phase init: InitSession fetches and caches a token for the config request;
100-
/// CatalogSession reuses the cached token and enables refresh.
101-
class OAuth2AuthManager : public AuthManager {
98+
class OAuth2Manager : public AuthManager {
10299
public:
103100
Result<std::shared_ptr<AuthSession>> InitSession(
104101
HttpClient& init_client,
105102
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+
106107
// Credential takes priority: fetch a fresh token for the config request.
107-
auto credential_it = properties.find(AuthProperties::kOAuth2Credential);
108-
if (credential_it != properties.end() && !credential_it->second.empty()) {
109-
ICEBERG_ASSIGN_OR_RAISE(auto ctx, ParseOAuth2Context(properties));
110-
auto noop_session = AuthSession::MakeDefault({});
108+
if (!config.credential().empty()) {
109+
auto init_session = AuthSession::MakeDefault(AuthHeaders(config.token()));
111110
ICEBERG_ASSIGN_OR_RAISE(init_token_response_,
112-
FetchToken(init_client, ctx.token_endpoint, ctx.client_id,
113-
ctx.client_secret, ctx.scope, *noop_session));
114-
return AuthSession::MakeDefault(
115-
{{"Authorization", "Bearer " + init_token_response_->access_token}});
111+
FetchToken(init_client, *init_session, config));
112+
return AuthSession::MakeDefault(AuthHeaders(init_token_response_->access_token));
116113
}
117114

118-
auto token_it = properties.find(AuthProperties::kOAuth2Token);
119-
if (token_it != properties.end() && !token_it->second.empty()) {
120-
return AuthSession::MakeDefault({{"Authorization", "Bearer " + token_it->second}});
115+
if (!config.token().empty()) {
116+
return AuthSession::MakeDefault(AuthHeaders(config.token()));
121117
}
122118

123119
return AuthSession::MakeDefault({});
@@ -126,104 +122,48 @@ class OAuth2AuthManager : public AuthManager {
126122
Result<std::shared_ptr<AuthSession>> CatalogSession(
127123
HttpClient& client,
128124
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.
129128
if (init_token_response_.has_value()) {
130129
auto token_response = std::move(*init_token_response_);
131130
init_token_response_.reset();
132-
ICEBERG_ASSIGN_OR_RAISE(auto ctx, ParseOAuth2Context(properties));
133-
return AuthSession::MakeOAuth2(token_response, ctx.token_endpoint, ctx.client_id,
134-
ctx.client_secret, ctx.scope, client);
131+
return AuthSession::MakeOAuth2(token_response, config.oauth2_server_uri(),
132+
config.client_id(), config.client_secret(),
133+
config.scope(), client);
135134
}
136135

137136
// If token is provided, use it directly.
138-
auto token_it = properties.find(AuthProperties::kOAuth2Token);
139-
if (token_it != properties.end() && !token_it->second.empty()) {
140-
return AuthSession::MakeDefault({{"Authorization", "Bearer " + token_it->second}});
137+
if (!config.token().empty()) {
138+
return AuthSession::MakeDefault(AuthHeaders(config.token()));
141139
}
142140

143141
// Fetch a new token using client_credentials grant.
144-
auto credential_it = properties.find(AuthProperties::kOAuth2Credential);
145-
if (credential_it != properties.end() && !credential_it->second.empty()) {
146-
ICEBERG_ASSIGN_OR_RAISE(auto ctx, ParseOAuth2Context(properties));
147-
auto noop_session = AuthSession::MakeDefault({});
142+
if (!config.credential().empty()) {
143+
auto base_session = AuthSession::MakeDefault(AuthHeaders(config.token()));
148144
OAuthTokenResponse token_response;
149-
ICEBERG_ASSIGN_OR_RAISE(token_response,
150-
FetchToken(client, ctx.token_endpoint, ctx.client_id,
151-
ctx.client_secret, ctx.scope, *noop_session));
152-
return AuthSession::MakeOAuth2(token_response, ctx.token_endpoint, ctx.client_id,
153-
ctx.client_secret, ctx.scope, client);
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);
154150
}
155151

156152
return AuthSession::MakeDefault({});
157153
}
158154

159-
// TODO(lishuxu): Override TableSession() to support token exchange (RFC 8693).
160-
// TODO(lishuxu): Override ContextualSession() to support per-context token exchange.
155+
// TODO(lishuxu): Override TableSession() for token exchange (RFC 8693).
156+
// TODO(lishuxu): Override ContextualSession() for per-context exchange.
161157

162158
private:
163-
struct OAuth2Context {
164-
std::string client_id;
165-
std::string client_secret;
166-
std::string token_endpoint;
167-
std::string scope;
168-
};
169-
170-
/// \brief Parse credential, token endpoint, and scope from properties.
171-
static Result<OAuth2Context> ParseOAuth2Context(
172-
const std::unordered_map<std::string, std::string>& properties) {
173-
OAuth2Context ctx;
174-
175-
auto credential_it = properties.find(AuthProperties::kOAuth2Credential);
176-
if (credential_it == properties.end() || credential_it->second.empty()) {
177-
return InvalidArgument("OAuth2 authentication requires '{}' property",
178-
AuthProperties::kOAuth2Credential);
179-
}
180-
const auto& credential = credential_it->second;
181-
auto colon_pos = credential.find(':');
182-
if (colon_pos == std::string::npos) {
183-
// No colon: treat entire string as client_secret with empty client_id.
184-
ctx.client_secret = credential;
185-
} else {
186-
ctx.client_id = credential.substr(0, colon_pos);
187-
ctx.client_secret = credential.substr(colon_pos + 1);
188-
}
189-
190-
auto uri_it = properties.find(AuthProperties::kOAuth2ServerUri);
191-
if (uri_it != properties.end() && !uri_it->second.empty()) {
192-
ctx.token_endpoint = uri_it->second;
193-
} else {
194-
// {uri}/v1/oauth/tokens.
195-
auto catalog_uri_it = properties.find(RestCatalogProperties::kUri.key());
196-
if (catalog_uri_it == properties.end() || catalog_uri_it->second.empty()) {
197-
return InvalidArgument(
198-
"OAuth2 authentication requires '{}' or '{}' property to determine "
199-
"token endpoint",
200-
AuthProperties::kOAuth2ServerUri, RestCatalogProperties::kUri.key());
201-
}
202-
std::string_view base = catalog_uri_it->second;
203-
while (!base.empty() && base.back() == '/') {
204-
base.remove_suffix(1);
205-
}
206-
ctx.token_endpoint =
207-
std::string(base) + "/" + std::string(AuthProperties::kOAuth2DefaultTokenPath);
208-
}
209-
210-
ctx.scope = AuthProperties::kOAuth2DefaultScope;
211-
auto scope_it = properties.find(AuthProperties::kOAuth2Scope);
212-
if (scope_it != properties.end() && !scope_it->second.empty()) {
213-
ctx.scope = scope_it->second;
214-
}
215-
216-
return ctx;
217-
}
218-
219159
/// Cached token from InitSession
220160
std::optional<OAuthTokenResponse> init_token_response_;
221161
};
222162

223-
Result<std::unique_ptr<AuthManager>> MakeOAuth2AuthManager(
163+
Result<std::unique_ptr<AuthManager>> MakeOAuth2Manager(
224164
[[maybe_unused]] std::string_view name,
225165
[[maybe_unused]] const std::unordered_map<std::string, std::string>& properties) {
226-
return std::make_unique<OAuth2AuthManager>();
166+
return std::make_unique<OAuth2Manager>();
227167
}
228168

229169
} // namespace iceberg::rest::auth

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Result<std::unique_ptr<AuthManager>> MakeBasicAuthManager(
4343
const std::unordered_map<std::string, std::string>& properties);
4444

4545
/// \brief Create an OAuth2 authentication manager.
46-
Result<std::unique_ptr<AuthManager>> MakeOAuth2AuthManager(
46+
Result<std::unique_ptr<AuthManager>> MakeOAuth2Manager(
4747
std::string_view name,
4848
const std::unordered_map<std::string, std::string>& properties);
4949

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

Lines changed: 3 additions & 3 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,7 +65,7 @@ AuthManagerRegistry CreateDefaultRegistry() {
6565
return {
6666
{AuthProperties::kAuthTypeNone, MakeNoopAuthManager},
6767
{AuthProperties::kAuthTypeBasic, MakeBasicAuthManager},
68-
{AuthProperties::kAuthTypeOAuth2, MakeOAuth2AuthManager},
68+
{AuthProperties::kAuthTypeOAuth2, MakeOAuth2Manager},
6969
};
7070
}
7171

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

0 commit comments

Comments
 (0)