Skip to content

Commit a41a136

Browse files
authored
impl: parse impersonated ADC json (#14809)
1 parent d58ddd2 commit a41a136

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed

google/cloud/internal/oauth2_impersonate_service_account_credentials.cc

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/internal/oauth2_impersonate_service_account_credentials.h"
16+
#include "google/cloud/internal/make_status.h"
1617
#include "google/cloud/internal/oauth2_credential_constants.h"
1718
#include "google/cloud/internal/unified_rest_credentials.h"
19+
#include "absl/strings/strip.h"
20+
#include <nlohmann/json.hpp>
1821

1922
namespace google {
2023
namespace cloud {
@@ -34,6 +37,103 @@ GenerateAccessTokenRequest MakeRequest(
3437

3538
} // namespace
3639

40+
StatusOr<ImpersonatedServiceAccountCredentialsInfo>
41+
ParseImpersonatedServiceAccountCredentials(std::string const& content,
42+
std::string const& source) {
43+
auto credentials = nlohmann::json::parse(content, nullptr, false);
44+
if (credentials.is_discarded()) {
45+
return internal::InvalidArgumentError(
46+
"Invalid ImpersonateServiceAccountCredentials, parsing failed on data "
47+
"from " +
48+
source,
49+
GCP_ERROR_INFO());
50+
}
51+
52+
ImpersonatedServiceAccountCredentialsInfo info;
53+
54+
auto it = credentials.find("service_account_impersonation_url");
55+
if (it == credentials.end()) {
56+
return internal::InvalidArgumentError(
57+
"Missing `service_account_impersonation_url` field on data from " +
58+
source,
59+
GCP_ERROR_INFO());
60+
}
61+
if (!it->is_string()) {
62+
return internal::InvalidArgumentError(
63+
"Malformed `service_account_impersonation_url` field is not a string "
64+
"on data from " +
65+
source,
66+
GCP_ERROR_INFO());
67+
}
68+
// We strip the service account from the path URL.
69+
auto url = it->get<std::string>();
70+
auto colon = url.rfind(':');
71+
if (colon == std::string::npos) {
72+
return internal::InvalidArgumentError(
73+
"Malformed `service_account_impersonation_url` field contents on data "
74+
"from " +
75+
source,
76+
GCP_ERROR_INFO());
77+
}
78+
if (url.substr(colon) != ":generateAccessToken") {
79+
// While `generateIdToken` is a valid RPC, we do not currently support ID
80+
// token flow. So we might as well error when parsing the credentials.
81+
return internal::InvalidArgumentError(
82+
"Only access token authentication is supported for impersonated "
83+
"service accounts from " +
84+
source,
85+
GCP_ERROR_INFO());
86+
}
87+
auto slash = url.rfind('/', colon);
88+
if (slash == std::string::npos) {
89+
return internal::InvalidArgumentError(
90+
"Malformed `service_account_impersonation_url` field contents on data "
91+
"from " +
92+
source,
93+
GCP_ERROR_INFO());
94+
}
95+
info.service_account = std::string{url.substr(slash + 1, colon - slash - 1)};
96+
97+
it = credentials.find("delegates");
98+
if (it != credentials.end()) {
99+
if (!it->is_array()) {
100+
return internal::InvalidArgumentError(
101+
"Malformed `delegates` field is not an array on data from " + source,
102+
GCP_ERROR_INFO());
103+
}
104+
for (auto const& delegate : it->items()) {
105+
info.delegates.push_back(delegate.value().get<std::string>());
106+
}
107+
}
108+
109+
it = credentials.find("quota_project_id");
110+
if (it != credentials.end()) {
111+
if (!it->is_string()) {
112+
return internal::InvalidArgumentError(
113+
"Malformed `quota_project_id` field is not a string on data from " +
114+
source,
115+
GCP_ERROR_INFO());
116+
}
117+
info.quota_project_id = it->get<std::string>();
118+
}
119+
120+
it = credentials.find("source_credentials");
121+
if (it == credentials.end()) {
122+
return internal::InvalidArgumentError(
123+
"Missing `source_credentials` field on data from " + source,
124+
GCP_ERROR_INFO());
125+
}
126+
if (!it->is_object()) {
127+
return internal::InvalidArgumentError(
128+
"Malformed `source_credentials` field is not an object on data from " +
129+
source,
130+
GCP_ERROR_INFO());
131+
}
132+
info.source_credentials = it->dump();
133+
134+
return info;
135+
}
136+
37137
ImpersonateServiceAccountCredentials::ImpersonateServiceAccountCredentials(
38138
google::cloud::internal::ImpersonateServiceAccountConfig const& config,
39139
HttpClientFactory client_factory)

google/cloud/internal/oauth2_impersonate_service_account_credentials.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ namespace cloud {
2626
namespace oauth2_internal {
2727
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
2828

29+
struct ImpersonatedServiceAccountCredentialsInfo {
30+
std::string service_account;
31+
std::vector<std::string> delegates;
32+
absl::optional<std::string> quota_project_id;
33+
std::string source_credentials;
34+
};
35+
36+
/// Parses the contents of a JSON keyfile into an
37+
/// ImpersonatedServiceAccountCredentialsInfo.
38+
StatusOr<ImpersonatedServiceAccountCredentialsInfo>
39+
ParseImpersonatedServiceAccountCredentials(std::string const& content,
40+
std::string const& source);
41+
2942
/**
3043
* Provides Credentials when impersonating an existing service account.
3144
*/

google/cloud/internal/oauth2_impersonate_service_account_credentials_test.cc

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "google/cloud/internal/oauth2_impersonate_service_account_credentials.h"
1616
#include "google/cloud/testing_util/status_matchers.h"
1717
#include <gmock/gmock.h>
18+
#include <nlohmann/json.hpp>
1819
#include <memory>
1920

2021
namespace google {
@@ -30,8 +31,83 @@ using ::google::cloud::testing_util::IsOkAndHolds;
3031
using ::google::cloud::testing_util::StatusIs;
3132
using ::std::chrono::minutes;
3233
using ::std::chrono::seconds;
34+
using ::testing::AllOf;
35+
using ::testing::ElementsAre;
36+
using ::testing::HasSubstr;
37+
using ::testing::Optional;
3338
using ::testing::Return;
3439

40+
auto constexpr kFullValidConfig = R"""({
41+
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken",
42+
"delegates": [
43+
44+
45+
],
46+
"quota_project_id": "my-project",
47+
"source_credentials": {
48+
"type": "authorized_user"
49+
},
50+
"type": "impersonated_service_account"
51+
})""";
52+
53+
TEST(ParseImpersonatedServiceAccountCredentials, Success) {
54+
auto actual =
55+
ParseImpersonatedServiceAccountCredentials(kFullValidConfig, "test-data");
56+
ASSERT_STATUS_OK(actual);
57+
EXPECT_EQ(actual->service_account, "[email protected]");
58+
EXPECT_THAT(actual->delegates,
59+
ElementsAre("[email protected]",
60+
61+
EXPECT_THAT(actual->quota_project_id, Optional<std::string>("my-project"));
62+
EXPECT_THAT(actual->source_credentials,
63+
AllOf(HasSubstr("type"), HasSubstr("authorized_user")));
64+
}
65+
66+
TEST(ParseImpersonatedServiceAccountCredentials, MissingRequiredFieldsError) {
67+
for (auto const& required_field :
68+
{"service_account_impersonation_url", "source_credentials"}) {
69+
auto json = nlohmann::json::parse(kFullValidConfig);
70+
json.erase(required_field);
71+
auto actual = ParseImpersonatedServiceAccountCredentials(json.dump(), "");
72+
EXPECT_THAT(actual, StatusIs(StatusCode::kInvalidArgument,
73+
AllOf(HasSubstr("Missing"),
74+
HasSubstr(required_field))));
75+
}
76+
}
77+
78+
TEST(ParseImpersonatedServiceAccountCredentials, MissingOptionalFieldsIsOk) {
79+
for (auto const& optional_field : {"delegates", "quota_project_id"}) {
80+
auto json = nlohmann::json::parse(kFullValidConfig);
81+
json.erase(optional_field);
82+
auto actual = ParseImpersonatedServiceAccountCredentials(json.dump(), "");
83+
EXPECT_STATUS_OK(actual);
84+
}
85+
}
86+
87+
TEST(ParseImpersonatedServiceAccountCredentials, MalformedServiceAccountPath) {
88+
auto json = nlohmann::json::parse(kFullValidConfig);
89+
json["service_account_impersonation_url"] = "not-a-valid-url-path";
90+
auto actual = ParseImpersonatedServiceAccountCredentials(json.dump(), "");
91+
EXPECT_THAT(actual,
92+
StatusIs(StatusCode::kInvalidArgument,
93+
AllOf(HasSubstr("Malformed"),
94+
HasSubstr("service_account_impersonation_url"),
95+
HasSubstr("contents"))));
96+
}
97+
98+
TEST(ParseImpersonatedServiceAccountCredentials, MalformedJsonFields) {
99+
for (auto const& non_int_field :
100+
{"service_account_impersonation_url", "delegates", "quota_project_id",
101+
"source_credentials"}) {
102+
auto json = nlohmann::json::parse(kFullValidConfig);
103+
json[non_int_field] = 0; // Provide an unexpected JSON type
104+
auto actual = ParseImpersonatedServiceAccountCredentials(json.dump(), "");
105+
EXPECT_THAT(actual, StatusIs(StatusCode::kInvalidArgument,
106+
AllOf(HasSubstr("Malformed"),
107+
HasSubstr(non_int_field))));
108+
}
109+
}
110+
35111
class MockMinimalIamCredentialsRest : public MinimalIamCredentialsRest {
36112
public:
37113
MOCK_METHOD(StatusOr<google::cloud::AccessToken>, GenerateAccessToken,

0 commit comments

Comments
 (0)