Skip to content

Commit 69ea716

Browse files
authored
fix(storage): cache all credential types (#11447)
Applications may call `AuthorizedHeader()` and that can generate calls to the GCE metadata service or Google's Security Token Service.
1 parent 971a4e4 commit 69ea716

File tree

6 files changed

+220
-11
lines changed

6 files changed

+220
-11
lines changed

google/cloud/storage/oauth2/authorized_user_credentials.cc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,23 @@ AuthorizedUserCredentials<storage::internal::CurlRequestBuilder,
7878
AuthorizedUserCredentials(
7979
google::cloud::oauth2_internal::AuthorizedUserCredentialsInfo info,
8080
ChannelOptions const& channel_options)
81-
: impl_(
81+
: AuthorizedUserCredentials(
8282
std::move(info),
8383
Options{}.set<CARootsFilePathOption>(channel_options.ssl_root_path()),
8484
[](Options const& o) {
8585
return rest_internal::MakeDefaultRestClient(std::string{}, o);
8686
}) {}
8787

88+
AuthorizedUserCredentials<storage::internal::CurlRequestBuilder,
89+
std::chrono::system_clock>::
90+
AuthorizedUserCredentials(
91+
google::cloud::oauth2_internal::AuthorizedUserCredentialsInfo info,
92+
Options options, oauth2_internal::HttpClientFactory client_factory)
93+
: impl_(std::make_shared<oauth2_internal::CachedCredentials>(
94+
std::make_shared<oauth2_internal::AuthorizedUserCredentials>(
95+
std::move(info), std::move(options),
96+
std::move(client_factory)))) {}
97+
8898
} // namespace oauth2
8999
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
90100
} // namespace storage

google/cloud/storage/oauth2/authorized_user_credentials.h

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "google/cloud/storage/oauth2/refreshing_credentials_wrapper.h"
2424
#include "google/cloud/storage/version.h"
2525
#include "google/cloud/internal/oauth2_authorized_user_credentials.h"
26+
#include "google/cloud/internal/oauth2_cached_credentials.h"
2627
#include "google/cloud/internal/oauth2_credential_constants.h"
2728
#include "google/cloud/status.h"
2829
#include <chrono>
@@ -114,11 +115,21 @@ class AuthorizedUserCredentials<storage::internal::CurlRequestBuilder,
114115
ChannelOptions const& channel_options = {});
115116

116117
StatusOr<std::string> AuthorizationHeader() override {
117-
return oauth2_internal::AuthorizationHeaderJoined(impl_);
118+
return oauth2_internal::AuthorizationHeaderJoined(*impl_);
118119
}
119120

120121
private:
121-
google::cloud::oauth2_internal::AuthorizedUserCredentials impl_;
122+
friend struct AuthorizedUserCredentialsTester;
123+
AuthorizedUserCredentials(
124+
google::cloud::oauth2_internal::AuthorizedUserCredentialsInfo,
125+
Options options, oauth2_internal::HttpClientFactory client_factory);
126+
127+
StatusOr<std::string> AuthorizationHeaderForTesting(
128+
std::chrono::system_clock::time_point tp) {
129+
return oauth2_internal::AuthorizationHeaderJoined(*impl_, tp);
130+
}
131+
132+
std::shared_ptr<google::cloud::oauth2_internal::Credentials> impl_;
122133
};
123134

124135
/// @copydoc AuthorizedUserCredentials

google/cloud/storage/oauth2/authorized_user_credentials_test.cc

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
#include "google/cloud/storage/oauth2/credential_constants.h"
1717
#include "google/cloud/storage/testing/mock_http_request.h"
1818
#include "google/cloud/testing_util/mock_fake_clock.h"
19+
#include "google/cloud/testing_util/mock_http_payload.h"
20+
#include "google/cloud/testing_util/mock_rest_client.h"
21+
#include "google/cloud/testing_util/mock_rest_response.h"
1922
#include "google/cloud/testing_util/status_matchers.h"
2023
#include <gmock/gmock.h>
2124
#include <nlohmann/json.hpp>
@@ -26,18 +29,40 @@ namespace cloud {
2629
namespace storage {
2730
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
2831
namespace oauth2 {
32+
33+
// Define a helper to test the specialization.
34+
struct AuthorizedUserCredentialsTester {
35+
static StatusOr<std::string> Header(
36+
AuthorizedUserCredentials<>& tested,
37+
std::chrono::system_clock::time_point tp) {
38+
return tested.AuthorizationHeaderForTesting(tp);
39+
}
40+
41+
static AuthorizedUserCredentials<> MakeAuthorizedUserCredentials(
42+
google::cloud::oauth2_internal::AuthorizedUserCredentialsInfo info,
43+
oauth2_internal::HttpClientFactory factory) {
44+
return AuthorizedUserCredentials<>(std::move(info), Options{},
45+
std::move(factory));
46+
}
47+
};
48+
2949
namespace {
3050

3151
using ::google::cloud::storage::internal::HttpResponse;
3252
using ::google::cloud::storage::testing::MockHttpRequest;
3353
using ::google::cloud::storage::testing::MockHttpRequestBuilder;
3454
using ::google::cloud::testing_util::FakeClock;
3555
using ::google::cloud::testing_util::IsOk;
56+
using ::google::cloud::testing_util::IsOkAndHolds;
57+
using ::google::cloud::testing_util::MakeMockHttpPayloadSuccess;
58+
using ::google::cloud::testing_util::MockRestClient;
59+
using ::google::cloud::testing_util::MockRestResponse;
3660
using ::google::cloud::testing_util::StatusIs;
3761
using ::testing::_;
3862
using ::testing::AllOf;
3963
using ::testing::An;
4064
using ::testing::AtLeast;
65+
using ::testing::ByMove;
4166
using ::testing::HasSubstr;
4267
using ::testing::Not;
4368
using ::testing::Return;
@@ -413,6 +438,60 @@ TEST_F(AuthorizedUserCredentialsTest, ParseAuthorizedUserRefreshResponse) {
413438
EXPECT_EQ(token.token, "Authorization: Type access-token-r1");
414439
}
415440

441+
TEST_F(AuthorizedUserCredentialsTest, Caching) {
442+
// We need to mock the Security Token Service or this would be an
443+
// integration test that requires a valid user account.
444+
auto make_mock_client = [](std::string const& payload) {
445+
auto response = std::make_unique<MockRestResponse>();
446+
EXPECT_CALL(*response, StatusCode)
447+
.WillRepeatedly(
448+
Return(google::cloud::rest_internal::HttpStatusCode::kOk));
449+
EXPECT_CALL(std::move(*response), ExtractPayload)
450+
.WillOnce(Return(ByMove(MakeMockHttpPayloadSuccess(payload))));
451+
auto mock = std::make_unique<MockRestClient>();
452+
using PostPayloadType = std::vector<std::pair<std::string, std::string>>;
453+
EXPECT_CALL(*mock, Post(_, _, An<PostPayloadType const&>()))
454+
.WillOnce(Return(ByMove(std::unique_ptr<rest_internal::RestResponse>(
455+
std::move(response)))));
456+
return std::unique_ptr<rest_internal::RestClient>(std::move(mock));
457+
};
458+
459+
auto constexpr kPayload1 = R"js({
460+
"access_token": "access-token-1", "id_token": "id-token-1",
461+
"token_type": "Bearer", "expires_in": 3600})js";
462+
auto constexpr kPayload2 = R"js({
463+
"access_token": "access-token-2", "id_token": "id-token-2",
464+
"token_type": "Bearer", "expires_in": 3600})js";
465+
466+
using MockHttpClientFactory =
467+
::testing::MockFunction<std::unique_ptr<rest_internal::RestClient>(
468+
Options const&)>;
469+
MockHttpClientFactory mock_factory;
470+
EXPECT_CALL(mock_factory, Call)
471+
.WillOnce(Return(ByMove(make_mock_client(kPayload1))))
472+
.WillOnce(Return(ByMove(make_mock_client(kPayload2))));
473+
474+
auto tested = AuthorizedUserCredentialsTester::MakeAuthorizedUserCredentials(
475+
oauth2_internal::AuthorizedUserCredentialsInfo{},
476+
mock_factory.AsStdFunction());
477+
auto const tp = std::chrono::system_clock::now();
478+
auto initial = AuthorizedUserCredentialsTester::Header(tested, tp);
479+
ASSERT_STATUS_OK(initial);
480+
481+
auto cached = AuthorizedUserCredentialsTester::Header(
482+
tested, tp + std::chrono::seconds(30));
483+
EXPECT_THAT(cached, IsOkAndHolds(*initial));
484+
485+
cached = AuthorizedUserCredentialsTester::Header(
486+
tested, tp + std::chrono::seconds(300));
487+
EXPECT_THAT(cached, IsOkAndHolds(*initial));
488+
489+
auto uncached = AuthorizedUserCredentialsTester::Header(
490+
tested, tp + std::chrono::hours(2));
491+
ASSERT_STATUS_OK(uncached);
492+
EXPECT_NE(*initial, *uncached);
493+
}
494+
416495
} // namespace
417496
} // namespace oauth2
418497
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END

google/cloud/storage/oauth2/compute_engine_credentials.cc

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/storage/oauth2/compute_engine_credentials.h"
16+
#include "google/cloud/internal/oauth2_cached_credentials.h"
1617
#include "google/cloud/internal/oauth2_compute_engine_credentials.h"
1718
#include <nlohmann/json.hpp>
1819

@@ -61,9 +62,19 @@ ParseComputeEngineRefreshResponse(
6162
ComputeEngineCredentials<storage::internal::CurlRequestBuilder,
6263
std::chrono::system_clock>::
6364
ComputeEngineCredentials(std::string service_account_email)
64-
: impl_(std::move(service_account_email), Options{}, [](Options const& o) {
65-
return rest_internal::MakeDefaultRestClient(std::string{}, o);
66-
}) {}
65+
: ComputeEngineCredentials(
66+
std::move(service_account_email), [](Options const& o) {
67+
return rest_internal::MakeDefaultRestClient(std::string{}, o);
68+
}) {}
69+
70+
ComputeEngineCredentials<storage::internal::CurlRequestBuilder,
71+
std::chrono::system_clock>::
72+
ComputeEngineCredentials(std::string service_account_email,
73+
oauth2_internal::HttpClientFactory client_factory)
74+
: impl_(std::make_shared<oauth2_internal::ComputeEngineCredentials>(
75+
std::move(service_account_email), Options{},
76+
std::move(client_factory))),
77+
cached_(std::make_shared<oauth2_internal::CachedCredentials>(impl_)) {}
6778

6879
} // namespace oauth2
6980
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END

google/cloud/storage/oauth2/compute_engine_credentials.h

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "google/cloud/storage/oauth2/refreshing_credentials_wrapper.h"
2424
#include "google/cloud/storage/version.h"
2525
#include "google/cloud/internal/getenv.h"
26+
#include "google/cloud/internal/oauth2_cached_credentials.h"
2627
#include "google/cloud/internal/oauth2_compute_engine_credentials.h"
2728
#include "google/cloud/status.h"
2829
#include <chrono>
@@ -106,10 +107,10 @@ class ComputeEngineCredentials<storage::internal::CurlRequestBuilder,
106107
explicit ComputeEngineCredentials(std::string service_account_email);
107108

108109
StatusOr<std::string> AuthorizationHeader() override {
109-
return oauth2_internal::AuthorizationHeaderJoined(impl_);
110+
return oauth2_internal::AuthorizationHeaderJoined(*cached_);
110111
}
111112

112-
std::string AccountEmail() const override { return impl_.AccountEmail(); }
113+
std::string AccountEmail() const override { return impl_->AccountEmail(); }
113114

114115
/**
115116
* Returns the email or alias of this credential's service account.
@@ -120,7 +121,7 @@ class ComputeEngineCredentials<storage::internal::CurlRequestBuilder,
120121
* initializing this credential, that alias is returned as this credential's
121122
* email address if the credential has not been refreshed yet.
122123
*/
123-
std::string service_account_email() { return impl_.service_account_email(); }
124+
std::string service_account_email() { return impl_->service_account_email(); }
124125

125126
/**
126127
* Returns the set of scopes granted to this credential's service account.
@@ -129,10 +130,20 @@ class ComputeEngineCredentials<storage::internal::CurlRequestBuilder,
129130
* server to fetch service account metadata, this method will return an empty
130131
* set if the credential has not been refreshed yet.
131132
*/
132-
std::set<std::string> scopes() const { return impl_.scopes(); }
133+
std::set<std::string> scopes() const { return impl_->scopes(); }
133134

134135
private:
135-
google::cloud::oauth2_internal::ComputeEngineCredentials impl_;
136+
friend struct ComputeEngineCredentialsTester;
137+
ComputeEngineCredentials(std::string service_account_email,
138+
oauth2_internal::HttpClientFactory client_factory);
139+
140+
StatusOr<std::string> AuthorizationHeaderForTesting(
141+
std::chrono::system_clock::time_point tp) {
142+
return oauth2_internal::AuthorizationHeaderJoined(*cached_, tp);
143+
}
144+
145+
std::shared_ptr<oauth2_internal::ComputeEngineCredentials> impl_;
146+
std::shared_ptr<oauth2_internal::CachedCredentials> cached_;
136147
};
137148

138149
/// @copydoc ComputeEngineCredentials

google/cloud/storage/oauth2/compute_engine_credentials_test.cc

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
#include "google/cloud/storage/oauth2/credential_constants.h"
1818
#include "google/cloud/storage/testing/mock_http_request.h"
1919
#include "google/cloud/testing_util/mock_fake_clock.h"
20+
#include "google/cloud/testing_util/mock_http_payload.h"
21+
#include "google/cloud/testing_util/mock_rest_client.h"
22+
#include "google/cloud/testing_util/mock_rest_response.h"
2023
#include "google/cloud/testing_util/status_matchers.h"
2124
#include <gmock/gmock.h>
2225
#include <nlohmann/json.hpp>
@@ -28,6 +31,23 @@ namespace cloud {
2831
namespace storage {
2932
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
3033
namespace oauth2 {
34+
35+
// Define a helper to test the specialization.
36+
struct ComputeEngineCredentialsTester {
37+
static StatusOr<std::string> Header(
38+
ComputeEngineCredentials<>& tested,
39+
std::chrono::system_clock::time_point tp) {
40+
return tested.AuthorizationHeaderForTesting(tp);
41+
}
42+
43+
static ComputeEngineCredentials<> MakeComputeEngineCredentials(
44+
std::string service_account_email,
45+
oauth2_internal::HttpClientFactory factory) {
46+
return ComputeEngineCredentials<>(std::move(service_account_email),
47+
std::move(factory));
48+
}
49+
};
50+
3151
namespace {
3252

3353
using ::google::cloud::storage::internal::GceMetadataHostname;
@@ -36,8 +56,13 @@ using ::google::cloud::storage::testing::MockHttpRequest;
3656
using ::google::cloud::storage::testing::MockHttpRequestBuilder;
3757
using ::google::cloud::testing_util::FakeClock;
3858
using ::google::cloud::testing_util::IsOk;
59+
using ::google::cloud::testing_util::IsOkAndHolds;
60+
using ::google::cloud::testing_util::MakeMockHttpPayloadSuccess;
61+
using ::google::cloud::testing_util::MockRestClient;
62+
using ::google::cloud::testing_util::MockRestResponse;
3963
using ::google::cloud::testing_util::StatusIs;
4064
using ::testing::_;
65+
using ::testing::ByMove;
4166
using ::testing::HasSubstr;
4267
using ::testing::IsEmpty;
4368
using ::testing::Not;
@@ -408,6 +433,68 @@ TEST_F(ComputeEngineCredentialsTest, AccountEmail) {
408433
EXPECT_EQ(credentials.service_account_email(), refreshed_email);
409434
}
410435

436+
TEST_F(ComputeEngineCredentialsTest, Caching) {
437+
// We need to mock the Compute Engine Metadata Service or this would be an
438+
// integration test that only runs on GCE.
439+
auto make_mock_client = [](std::string const& payload) {
440+
auto response = std::make_unique<MockRestResponse>();
441+
EXPECT_CALL(*response, StatusCode)
442+
.WillRepeatedly(
443+
Return(google::cloud::rest_internal::HttpStatusCode::kOk));
444+
EXPECT_CALL(std::move(*response), ExtractPayload)
445+
.WillOnce(Return(ByMove(MakeMockHttpPayloadSuccess(payload))));
446+
auto mock = std::make_unique<MockRestClient>();
447+
EXPECT_CALL(*mock, Get)
448+
.WillOnce(Return(ByMove(std::unique_ptr<rest_internal::RestResponse>(
449+
std::move(response)))));
450+
return std::unique_ptr<rest_internal::RestClient>(std::move(mock));
451+
};
452+
453+
// We expect a call to the metadata service to get the service account
454+
// metadata. Then one call to get the token (which is cached), and finally a
455+
// call the refresh the token.
456+
auto constexpr kPayload0 =
457+
R"""({"email": "test-only-email", "scopes": ["s1", "s2]})""";
458+
auto constexpr kPayload1 = R"""({
459+
"access_token": "test-token-1",
460+
"expires_in": 3600,
461+
"token_type": "tokentype"
462+
})""";
463+
auto constexpr kPayload2 = R"""({
464+
"access_token": "test-token-2",
465+
"expires_in": 3600,
466+
"token_type": "tokentype"
467+
})""";
468+
469+
using MockHttpClientFactory =
470+
::testing::MockFunction<std::unique_ptr<rest_internal::RestClient>(
471+
Options const&)>;
472+
MockHttpClientFactory mock_factory;
473+
EXPECT_CALL(mock_factory, Call)
474+
.WillOnce(Return(ByMove(make_mock_client(kPayload0))))
475+
.WillOnce(Return(ByMove(make_mock_client(kPayload1))))
476+
.WillOnce(Return(ByMove(make_mock_client(kPayload2))));
477+
478+
auto tested = ComputeEngineCredentialsTester::MakeComputeEngineCredentials(
479+
"test-only", mock_factory.AsStdFunction());
480+
auto const tp = std::chrono::system_clock::now();
481+
auto initial = ComputeEngineCredentialsTester::Header(tested, tp);
482+
ASSERT_STATUS_OK(initial);
483+
484+
auto cached = ComputeEngineCredentialsTester::Header(
485+
tested, tp + std::chrono::seconds(30));
486+
EXPECT_THAT(cached, IsOkAndHolds(*initial));
487+
488+
cached = ComputeEngineCredentialsTester::Header(
489+
tested, tp + std::chrono::seconds(300));
490+
EXPECT_THAT(cached, IsOkAndHolds(*initial));
491+
492+
auto uncached = ComputeEngineCredentialsTester::Header(
493+
tested, tp + std::chrono::hours(2));
494+
ASSERT_STATUS_OK(uncached);
495+
EXPECT_NE(*initial, *uncached);
496+
}
497+
411498
} // namespace
412499
} // namespace oauth2
413500
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END

0 commit comments

Comments
 (0)