@@ -117,7 +117,7 @@ class RestCredentialProviderTest : public ::testing::Test {
117117 }
118118 std::shared_ptr<RestApi> shared_api (std::move (api).value ());
119119 return std::make_shared<RestCredentialProvider>(
120- shared_api, catalog_options_, Identifier (" db1" , " t1" ), [this ] {
120+ shared_api, Identifier (" db1" , " t1" ), [this ] {
121121 return std::chrono::system_clock::time_point (
122122 std::chrono::milliseconds (now_millis_.load ()));
123123 });
@@ -194,7 +194,7 @@ TEST_F(RestCredentialProviderTest, ExpiredTokenReloadsOnEveryCall) {
194194 ASSERT_EQ (2 , state_->request_count .load ());
195195}
196196
197- TEST_F (RestCredentialProviderTest, DlfEndpointOverridesTheServerEndpoint ) {
197+ TEST_F (RestCredentialProviderTest, DlfEndpointOverridesTheServerEndpointOnMerge ) {
198198 catalog_options_[CatalogOptions::DLF_OSS_ENDPOINT ] = " dlf-endpoint" ;
199199 {
200200 std::lock_guard<std::mutex> lock (state_->mutex );
@@ -203,14 +203,20 @@ TEST_F(RestCredentialProviderTest, DlfEndpointOverridesTheServerEndpoint) {
203203 std::shared_ptr<RestCredentialProvider> provider = CreateProvider ();
204204 ASSERT_NE (nullptr , provider);
205205
206+ // the token is the cache key and stays exactly as issued, carrying no catalog secrets and
207+ // not the endpoint correction, which is a merge concern
206208 ASSERT_OK_AND_ASSIGN (RestToken token, provider->ValidToken ());
207209 ASSERT_EQ (" ak-1" , token.token .at (" fs.oss.accessKeyId" ));
208- // the endpoint the credentials were issued for wins over the one the server reported
209- ASSERT_EQ (" dlf-endpoint" , token.token .at (kOssEndpointOption ));
210+ ASSERT_EQ (" server-endpoint" , token.token .at (kOssEndpointOption ));
210211 ASSERT_EQ (kExpiresAtMillis , token.expires_at_millis );
211- // the catalog options are not part of the token, so its secrets stay private
212212 ASSERT_EQ (0u , token.token .count (CatalogOptions::TOKEN ));
213213 ASSERT_EQ (2u , token.token .size ());
214+
215+ // merging shapes the credentials into the file system options: the endpoint the credentials
216+ // were issued for wins over the one the server reported
217+ Credentials merged = provider->MergeOptionsWithCredentials (catalog_options_, token.token );
218+ ASSERT_EQ (" ak-1" , merged.at (" fs.oss.accessKeyId" ));
219+ ASSERT_EQ (" dlf-endpoint" , merged.at (kOssEndpointOption ));
214220}
215221
216222TEST_F (RestCredentialProviderTest, EmptyDlfOssEndpointIsNotApplied) {
@@ -222,10 +228,92 @@ TEST_F(RestCredentialProviderTest, EmptyDlfOssEndpointIsNotApplied) {
222228 std::shared_ptr<RestCredentialProvider> provider = CreateProvider ();
223229 ASSERT_NE (nullptr , provider);
224230
225- // an unset dlf endpoint leaves the endpoint the server reported alone
231+ // an unset dlf endpoint leaves the endpoint the credentials carry alone through the merge
226232 ASSERT_OK_AND_ASSIGN (RestToken token, provider->ValidToken ());
227- ASSERT_EQ (" server-endpoint" , token.token .at (kOssEndpointOption ));
228- ASSERT_EQ (1u , token.token .size ());
233+ Credentials merged = provider->MergeOptionsWithCredentials (catalog_options_, token.token );
234+ ASSERT_EQ (" server-endpoint" , merged.at (kOssEndpointOption ));
235+ }
236+
237+ TEST_F (RestCredentialProviderTest, IssuedCredentialsClearStaleBucketScopedCatalogVariants) {
238+ // The OSS backend resolves a bucket-scoped option ahead of the flat one, so a stale
239+ // bucket-scoped catalog value would shadow the flat credential the token just refreshed.
240+ std::shared_ptr<RestCredentialProvider> provider = CreateProvider ();
241+ ASSERT_NE (nullptr , provider);
242+
243+ Credentials base = {
244+ {" fs.oss.accessKeyId" , " catalog-ak" },
245+ {" fs.oss.bucket.b.accessKeyId" , " catalog-bucket-ak" },
246+ {" unrelated" , " kept" },
247+ };
248+ Credentials credentials = {{" fs.oss.accessKeyId" , " token-ak" }};
249+
250+ Credentials merged = provider->MergeOptionsWithCredentials (base, credentials);
251+ ASSERT_EQ (" token-ak" , merged.at (" fs.oss.accessKeyId" ));
252+ ASSERT_EQ (0u , merged.count (" fs.oss.bucket.b.accessKeyId" ));
253+ ASSERT_EQ (" kept" , merged.at (" unrelated" ));
254+ }
255+
256+ TEST_F (RestCredentialProviderTest, SessionTokenCredentialClearsStaleSecurityTokenAliases) {
257+ // A token carrying a fresh session token must clear a stale catalog security token -- the
258+ // OSS backend reads securityToken first, so leaving it in place would pair the refreshed
259+ // key pair with the old STS token and fail authentication. Both the flat and the
260+ // bucket-scoped stale securityToken have to go.
261+ std::shared_ptr<RestCredentialProvider> provider = CreateProvider ();
262+ ASSERT_NE (nullptr , provider);
263+
264+ Credentials base = {
265+ {" fs.oss.securityToken" , " stale-sts" },
266+ {" fs.oss.bucket.b.securityToken" , " stale-bucket-sts" },
267+ };
268+ Credentials credentials = {{" fs.oss.accessKeyId" , " token-ak" },
269+ {" fs.oss.accessKeySecret" , " token-sk" },
270+ {" fs.oss.sessionToken" , " fresh-sts" }};
271+
272+ Credentials merged = provider->MergeOptionsWithCredentials (base, credentials);
273+ ASSERT_EQ (" token-ak" , merged.at (" fs.oss.accessKeyId" ));
274+ ASSERT_EQ (" token-sk" , merged.at (" fs.oss.accessKeySecret" ));
275+ ASSERT_EQ (" fresh-sts" , merged.at (" fs.oss.sessionToken" ));
276+ ASSERT_EQ (0u , merged.count (" fs.oss.securityToken" ));
277+ ASSERT_EQ (0u , merged.count (" fs.oss.bucket.b.securityToken" ));
278+ }
279+
280+ TEST_F (RestCredentialProviderTest, SecurityTokenCredentialClearsStaleSessionTokenAlias) {
281+ // The alias goes the other way too: a fresh securityToken clears a stale sessionToken so the
282+ // backend cannot fall back to it.
283+ std::shared_ptr<RestCredentialProvider> provider = CreateProvider ();
284+ ASSERT_NE (nullptr , provider);
285+
286+ Credentials base = {{" fs.oss.sessionToken" , " stale-sts" }};
287+ Credentials credentials = {{" fs.oss.securityToken" , " fresh-sts" }};
288+
289+ Credentials merged = provider->MergeOptionsWithCredentials (base, credentials);
290+ ASSERT_EQ (" fresh-sts" , merged.at (" fs.oss.securityToken" ));
291+ ASSERT_EQ (0u , merged.count (" fs.oss.sessionToken" ));
292+ }
293+
294+ TEST_F (RestCredentialProviderTest, TokenSuppliedBucketScopedCredentialsArePreserved) {
295+ // When the token itself carries a complete bucket-scoped credential set, those values are
296+ // authoritative: the cleanup that a global credential of the same suffix would otherwise
297+ // trigger must not erase the token's own bucket-scoped keys.
298+ std::shared_ptr<RestCredentialProvider> provider = CreateProvider ();
299+ ASSERT_NE (nullptr , provider);
300+
301+ Credentials base = {};
302+ Credentials credentials = {
303+ {" fs.oss.accessKeyId" , " token-ak" },
304+ {" fs.oss.accessKeySecret" , " token-sk" },
305+ {" fs.oss.securityToken" , " token-sts" },
306+ {" fs.oss.bucket.b.accessKeyId" , " token-bucket-ak" },
307+ {" fs.oss.bucket.b.accessKeySecret" , " token-bucket-sk" },
308+ {" fs.oss.bucket.b.securityToken" , " token-bucket-sts" },
309+ };
310+
311+ Credentials merged = provider->MergeOptionsWithCredentials (base, credentials);
312+ ASSERT_EQ (" token-ak" , merged.at (" fs.oss.accessKeyId" ));
313+ ASSERT_EQ (" token-sts" , merged.at (" fs.oss.securityToken" ));
314+ ASSERT_EQ (" token-bucket-ak" , merged.at (" fs.oss.bucket.b.accessKeyId" ));
315+ ASSERT_EQ (" token-bucket-sk" , merged.at (" fs.oss.bucket.b.accessKeySecret" ));
316+ ASSERT_EQ (" token-bucket-sts" , merged.at (" fs.oss.bucket.b.securityToken" ));
229317}
230318
231319TEST_F (RestCredentialProviderTest, ForbiddenIsReportedToTheCaller) {
0 commit comments