Skip to content

Commit 3078eef

Browse files
committed
impl(oauth2): add AllowedLocations methods to minimal iam rest stub
1 parent fc9f8f0 commit 3078eef

7 files changed

+467
-14
lines changed

google/cloud/internal/json_parsing.cc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ StatusOr<std::int32_t> ValidateIntField(nlohmann::json const& json,
6363
return it->get<std::int32_t>();
6464
}
6565

66+
StatusOr<std::vector<std::string>> ValidateStringArrayField(
67+
nlohmann::json const& json, absl::string_view name,
68+
absl::string_view object_name, internal::ErrorContext const& ec) {
69+
auto it = json.find(std::string{name});
70+
if (it == json.end()) return MissingFieldError(name, object_name, ec);
71+
if (!it->is_array()) return InvalidTypeError(name, object_name, ec);
72+
if (!std::all_of(it->begin(), it->end(),
73+
[](nlohmann::json const& e) { return e.is_string(); })) {
74+
return InvalidTypeError(name, object_name, ec);
75+
}
76+
return it->get<std::vector<std::string>>();
77+
}
78+
6679
Status MissingFieldError(absl::string_view name, absl::string_view object_name,
6780
internal::ErrorContext const& ec) {
6881
return InvalidArgumentError(

google/cloud/internal/json_parsing.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ StatusOr<std::int32_t> ValidateIntField(nlohmann::json const& json,
5757
std::int32_t default_value,
5858
internal::ErrorContext const& ec);
5959

60+
/// Returns the string values for `json[name]` (which must exist) or a
61+
/// descriptive error.
62+
StatusOr<std::vector<std::string>> ValidateStringArrayField(
63+
nlohmann::json const& json, absl::string_view name,
64+
absl::string_view object_name, internal::ErrorContext const& ec);
65+
6066
/// Use when a JSON field cannot be found but is required.
6167
Status MissingFieldError(absl::string_view name, absl::string_view object_name,
6268
internal::ErrorContext const& ec);

google/cloud/internal/json_parsing_test.cc

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ namespace {
2525
using ::google::cloud::testing_util::StatusIs;
2626
using ::testing::AllOf;
2727
using ::testing::Contains;
28+
using ::testing::ElementsAre;
2829
using ::testing::HasSubstr;
2930
using ::testing::Pair;
3031

31-
TEST(ExternalAccountParsing, ValidateStringFieldSuccess) {
32+
TEST(JsonParsingTest, ValidateStringFieldSuccess) {
3233
auto const json = nlohmann::json{{"someField", "value"}};
3334
auto actual = ValidateStringField(
3435
json, "someField", "test-object",
@@ -37,7 +38,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldSuccess) {
3738
EXPECT_EQ(*actual, "value");
3839
}
3940

40-
TEST(ExternalAccountParsing, ValidateStringFieldMissing) {
41+
TEST(JsonParsingTest, ValidateStringFieldMissing) {
4142
auto const json = nlohmann::json{{"some-field", "value"}};
4243
auto actual = ValidateStringField(
4344
json, "missingField", "test-object",
@@ -51,7 +52,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldMissing) {
5152
Contains(Pair("origin", "test")));
5253
}
5354

54-
TEST(ExternalAccountParsing, ValidateStringFieldNotString) {
55+
TEST(JsonParsingTest, ValidateStringFieldNotString) {
5556
auto const json =
5657
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
5758
auto actual = ValidateStringField(
@@ -66,7 +67,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldNotString) {
6667
Contains(Pair("origin", "test")));
6768
}
6869

69-
TEST(ExternalAccountParsing, ValidateStringFieldDefaultSuccess) {
70+
TEST(JsonParsingTest, ValidateStringFieldDefaultSuccess) {
7071
auto const json = nlohmann::json{{"someField", "value"}};
7172
auto actual = ValidateStringField(
7273
json, "someField", "test-object", "default-value",
@@ -75,7 +76,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldDefaultSuccess) {
7576
EXPECT_EQ(*actual, "value");
7677
}
7778

78-
TEST(ExternalAccountParsing, ValidateStringFieldDefaultMissing) {
79+
TEST(JsonParsingTest, ValidateStringFieldDefaultMissing) {
7980
auto const json = nlohmann::json{{"anotherField", "value"}};
8081
auto actual = ValidateStringField(
8182
json, "someField", "test-object", "default-value",
@@ -84,7 +85,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldDefaultMissing) {
8485
EXPECT_EQ(*actual, "default-value");
8586
}
8687

87-
TEST(ExternalAccountParsing, ValidateStringFieldDefaultNotInt) {
88+
TEST(JsonParsingTest, ValidateStringFieldDefaultNotInt) {
8889
auto const json =
8990
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
9091
auto actual = ValidateStringField(
@@ -99,7 +100,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldDefaultNotInt) {
99100
Contains(Pair("origin", "test")));
100101
}
101102

102-
TEST(ExternalAccountParsing, ValidateIntFieldSuccess) {
103+
TEST(JsonParsingTest, ValidateIntFieldSuccess) {
103104
auto const json = nlohmann::json{{"someField", 42}};
104105
auto actual = ValidateIntField(
105106
json, "someField", "test-object",
@@ -108,7 +109,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldSuccess) {
108109
EXPECT_EQ(*actual, 42);
109110
}
110111

111-
TEST(ExternalAccountParsing, ValidateIntFieldMissing) {
112+
TEST(JsonParsingTest, ValidateIntFieldMissing) {
112113
auto const json = nlohmann::json{{"some-field", 42}};
113114
auto actual = ValidateIntField(
114115
json, "missingField", "test-object",
@@ -122,7 +123,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldMissing) {
122123
Contains(Pair("origin", "test")));
123124
}
124125

125-
TEST(ExternalAccountParsing, ValidateIntFieldNotString) {
126+
TEST(JsonParsingTest, ValidateIntFieldNotString) {
126127
auto const json =
127128
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
128129
auto actual = ValidateIntField(
@@ -137,7 +138,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldNotString) {
137138
Contains(Pair("origin", "test")));
138139
}
139140

140-
TEST(ExternalAccountParsing, ValidateIntFieldDefaultSuccess) {
141+
TEST(JsonParsingTest, ValidateIntFieldDefaultSuccess) {
141142
auto const json = nlohmann::json{{"someField", 42}};
142143
auto actual = ValidateIntField(
143144
json, "someField", "test-object", 42,
@@ -146,7 +147,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldDefaultSuccess) {
146147
EXPECT_EQ(*actual, 42);
147148
}
148149

149-
TEST(ExternalAccountParsing, ValidateIntFieldDefaultMissing) {
150+
TEST(JsonParsingTest, ValidateIntFieldDefaultMissing) {
150151
auto const json = nlohmann::json{{"anotherField", "value"}};
151152
auto actual = ValidateIntField(
152153
json, "someField", "test-object", 42,
@@ -155,7 +156,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldDefaultMissing) {
155156
EXPECT_EQ(*actual, 42);
156157
}
157158

158-
TEST(ExternalAccountParsing, ValidateIntFieldDefaultNotString) {
159+
TEST(JsonParsingTest, ValidateIntFieldDefaultNotString) {
159160
auto const json =
160161
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
161162
auto actual = ValidateIntField(
@@ -170,6 +171,43 @@ TEST(ExternalAccountParsing, ValidateIntFieldDefaultNotString) {
170171
Contains(Pair("origin", "test")));
171172
}
172173

174+
TEST(JsonParsingTest, ValidateStringArrayFieldSuccess) {
175+
auto const json = nlohmann::json{{"someField", {"value1", "value2"}}};
176+
auto actual = ValidateStringArrayField(
177+
json, "someField", "test-object",
178+
internal::ErrorContext({{"origin", "test"}, {"filename", "/dev/null"}}));
179+
ASSERT_STATUS_OK(actual);
180+
EXPECT_THAT(*actual, ElementsAre("value1", "value2"));
181+
}
182+
183+
TEST(JsonParsingTest, ValidateStringArrayFieldMissing) {
184+
auto const json = nlohmann::json{{"some-field", {"value1", "value2"}}};
185+
auto actual = ValidateStringArrayField(
186+
json, "missingField", "test-object",
187+
internal::ErrorContext({{"origin", "test"}, {"filename", "/dev/null"}}));
188+
EXPECT_THAT(actual, StatusIs(StatusCode::kInvalidArgument,
189+
AllOf(HasSubstr("missingField"),
190+
HasSubstr("test-object"))));
191+
EXPECT_THAT(actual.status().error_info().metadata(),
192+
Contains(Pair("filename", "/dev/null")));
193+
EXPECT_THAT(actual.status().error_info().metadata(),
194+
Contains(Pair("origin", "test")));
195+
}
196+
197+
TEST(JsonParsingTest, ValidateStringArrayFieldNotString) {
198+
auto const json = nlohmann::json({"wrongType", {"value1", true}});
199+
auto actual = ValidateStringArrayField(
200+
json, "wrongType", "test-object",
201+
internal::ErrorContext({{"origin", "test"}, {"filename", "/dev/null"}}));
202+
EXPECT_THAT(actual, StatusIs(StatusCode::kInvalidArgument,
203+
AllOf(HasSubstr("wrongType"),
204+
HasSubstr("test-object"))));
205+
EXPECT_THAT(actual.status().error_info().metadata(),
206+
Contains(Pair("filename", "/dev/null")));
207+
EXPECT_THAT(actual.status().error_info().metadata(),
208+
Contains(Pair("origin", "test")));
209+
}
210+
173211
} // namespace
174212
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
175213
} // namespace oauth2_internal

google/cloud/internal/oauth2_impersonate_service_account_credentials_test.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ class MockMinimalIamCredentialsRest : public MinimalIamCredentialsRest {
155155
public:
156156
MOCK_METHOD(StatusOr<google::cloud::AccessToken>, GenerateAccessToken,
157157
(GenerateAccessTokenRequest const&), (override));
158+
MOCK_METHOD(StatusOr<AllowedLocationsResponse>, AllowedLocations,
159+
(ServiceAccountAllowedLocationsRequest const&), (override));
160+
MOCK_METHOD(StatusOr<AllowedLocationsResponse>, AllowedLocations,
161+
(WorkloadIdentityAllowedLocationsRequest const&), (override));
162+
MOCK_METHOD(StatusOr<AllowedLocationsResponse>, AllowedLocations,
163+
(WorkforceIdentityAllowedLocationsRequest const&), (override));
158164
MOCK_METHOD(StatusOr<std::string>, universe_domain, (Options const& options),
159165
(override, const));
160166
};

google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ namespace google {
3333
namespace cloud {
3434
namespace oauth2_internal {
3535
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
36+
namespace {
37+
38+
std::string IamCredentialsEndpoint(
39+
StatusOr<std::string> const& universe_domain) {
40+
return absl::StrCat("https://iamcredentials.",
41+
universe_domain ? *universe_domain : "googleapis.com");
42+
}
43+
44+
} // namespace
3645

3746
using ::google::cloud::internal::InvalidArgumentError;
3847

@@ -73,12 +82,60 @@ MinimalIamCredentialsRestStub::GenerateAccessToken(
7382

7483
std::string MinimalIamCredentialsRestStub::MakeRequestPath(
7584
GenerateAccessTokenRequest const& request) const {
76-
auto ud = universe_domain(Options{});
77-
return absl::StrCat("https://iamcredentials.", ud ? *ud : "googleapis.com",
85+
return absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
7886
"/v1/projects/-/serviceAccounts/",
7987
request.service_account, ":generateAccessToken");
8088
}
8189

90+
StatusOr<AllowedLocationsResponse>
91+
MinimalIamCredentialsRestStub::AllowedLocationsHelper(std::string path) {
92+
auto authorization_header =
93+
credentials_->Authorization(std::chrono::system_clock::now());
94+
if (!authorization_header) return std::move(authorization_header).status();
95+
rest_internal::RestRequest rest_request;
96+
rest_request.AddHeader(*std::move(authorization_header));
97+
rest_request.SetPath(std::move(path));
98+
99+
auto client = client_factory_(options_);
100+
rest_internal::RestContext context;
101+
auto response = client->Get(context, rest_request);
102+
if (!response) return std::move(response).status();
103+
return ParseAllowedLocationsResponse(
104+
**response,
105+
internal::ErrorContext(
106+
{{"gcloud-cpp.root.class", "MinimalIamCredentialsRestStub"},
107+
{"gcloud-cpp.root.function", __func__},
108+
{"path", rest_request.path()}}));
109+
}
110+
111+
StatusOr<AllowedLocationsResponse>
112+
MinimalIamCredentialsRestStub::AllowedLocations(
113+
ServiceAccountAllowedLocationsRequest const& request) {
114+
auto path = absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
115+
"/v1/projects/-/serviceAccounts/",
116+
request.service_account_email, "/allowedLocations");
117+
return AllowedLocationsHelper(std::move(path));
118+
}
119+
120+
StatusOr<AllowedLocationsResponse>
121+
MinimalIamCredentialsRestStub::AllowedLocations(
122+
WorkloadIdentityAllowedLocationsRequest const& request) {
123+
auto path = absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
124+
"/v1/projects/", request.project_id,
125+
"/locations/global/workloadIdentityPools/",
126+
request.pool_id, "/allowedLocations");
127+
return AllowedLocationsHelper(std::move(path));
128+
}
129+
130+
StatusOr<AllowedLocationsResponse>
131+
MinimalIamCredentialsRestStub::AllowedLocations(
132+
WorkforceIdentityAllowedLocationsRequest const& request) {
133+
auto path = absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
134+
"/v1/locations/global/workforcePools/",
135+
request.pool_id, "/allowedLocations");
136+
return AllowedLocationsHelper(std::move(path));
137+
}
138+
82139
MinimalIamCredentialsRestLogging::MinimalIamCredentialsRestLogging(
83140
std::shared_ptr<MinimalIamCredentialsRest> child)
84141
: child_(std::move(child)) {}
@@ -104,6 +161,53 @@ MinimalIamCredentialsRestLogging::GenerateAccessToken(
104161
return response;
105162
}
106163

164+
StatusOr<AllowedLocationsResponse>
165+
MinimalIamCredentialsRestLogging::AllowedLocations(
166+
ServiceAccountAllowedLocationsRequest const& request) {
167+
GCP_LOG(INFO) << __func__ << "() << {service_account_email="
168+
<< request.service_account_email << "}";
169+
auto response = child_->AllowedLocations(request);
170+
if (!response) {
171+
GCP_LOG(INFO) << __func__ << "() >> status={" << response.status() << "}";
172+
return response;
173+
}
174+
GCP_LOG(INFO) << __func__ << "() >> response={locations="
175+
<< absl::StrJoin(response->locations, ",")
176+
<< ", encoded_locations=" << response->encoded_locations << "}";
177+
return response;
178+
}
179+
180+
StatusOr<AllowedLocationsResponse>
181+
MinimalIamCredentialsRestLogging::AllowedLocations(
182+
WorkloadIdentityAllowedLocationsRequest const& request) {
183+
GCP_LOG(INFO) << __func__ << "() << {project_id=" << request.project_id
184+
<< ", pool_id=" << request.pool_id << "}";
185+
auto response = child_->AllowedLocations(request);
186+
if (!response) {
187+
GCP_LOG(INFO) << __func__ << "() >> status={" << response.status() << "}";
188+
return response;
189+
}
190+
GCP_LOG(INFO) << __func__ << "() >> response={locations="
191+
<< absl::StrJoin(response->locations, ",")
192+
<< ", encoded_locations=" << response->encoded_locations << "}";
193+
return response;
194+
}
195+
196+
StatusOr<AllowedLocationsResponse>
197+
MinimalIamCredentialsRestLogging::AllowedLocations(
198+
WorkforceIdentityAllowedLocationsRequest const& request) {
199+
GCP_LOG(INFO) << __func__ << "() << {pool_id=" << request.pool_id << "}";
200+
auto response = child_->AllowedLocations(request);
201+
if (!response) {
202+
GCP_LOG(INFO) << __func__ << "() >> status={" << response.status() << "}";
203+
return response;
204+
}
205+
GCP_LOG(INFO) << __func__ << "() >> response={locations="
206+
<< absl::StrJoin(response->locations, ",")
207+
<< ", encoded_locations=" << response->encoded_locations << "}";
208+
return response;
209+
}
210+
107211
StatusOr<AccessToken> ParseGenerateAccessTokenResponse(
108212
rest_internal::RestResponse& response,
109213
google::cloud::internal::ErrorContext const& ec) {
@@ -132,6 +236,29 @@ StatusOr<AccessToken> ParseGenerateAccessTokenResponse(
132236
return google::cloud::AccessToken{*std::move(token), *expire_time};
133237
}
134238

239+
StatusOr<AllowedLocationsResponse> ParseAllowedLocationsResponse(
240+
rest_internal::RestResponse& response,
241+
google::cloud::internal::ErrorContext const& ec) {
242+
if (IsHttpError(response)) return AsStatus(std::move(response));
243+
auto response_payload =
244+
rest_internal::ReadAll(std::move(response).ExtractPayload());
245+
if (!response_payload) return std::move(response_payload).status();
246+
auto parsed = nlohmann::json::parse(*response_payload, nullptr, false);
247+
if (!parsed.is_object()) {
248+
return InvalidArgumentError("cannot parse response as a JSON object",
249+
GCP_ERROR_INFO().WithContext(ec));
250+
}
251+
auto locations = ValidateStringArrayField(parsed, "locations",
252+
"AllowedLocations() response", ec);
253+
if (!locations) return std::move(locations).status();
254+
255+
auto encoded_locations = ValidateStringField(
256+
parsed, "encodedLocations", "AllowedLocations() response", ec);
257+
if (!encoded_locations) return std::move(encoded_locations).status();
258+
return AllowedLocationsResponse{*std::move(locations),
259+
*std::move(encoded_locations)};
260+
}
261+
135262
std::shared_ptr<MinimalIamCredentialsRest> MakeMinimalIamCredentialsRestStub(
136263
std::shared_ptr<oauth2_internal::Credentials> credentials, Options options,
137264
HttpClientFactory client_factory) {

0 commit comments

Comments
 (0)