Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions google/cloud/internal/json_parsing.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ StatusOr<std::int32_t> ValidateIntField(nlohmann::json const& json,
return it->get<std::int32_t>();
}

StatusOr<std::vector<std::string>> ValidateStringArrayField(
nlohmann::json const& json, absl::string_view name,
absl::string_view object_name, internal::ErrorContext const& ec) {
auto it = json.find(std::string{name});
if (it == json.end()) return MissingFieldError(name, object_name, ec);
if (!it->is_array()) return InvalidTypeError(name, object_name, ec);
if (!std::all_of(it->begin(), it->end(),
[](nlohmann::json const& e) { return e.is_string(); })) {
return InvalidTypeError(name, object_name, ec);
}
return it->get<std::vector<std::string>>();
}

Status MissingFieldError(absl::string_view name, absl::string_view object_name,
internal::ErrorContext const& ec) {
return InvalidArgumentError(
Expand Down
6 changes: 6 additions & 0 deletions google/cloud/internal/json_parsing.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ StatusOr<std::int32_t> ValidateIntField(nlohmann::json const& json,
std::int32_t default_value,
internal::ErrorContext const& ec);

/// Returns the string values for `json[name]` (which must exist) or a
/// descriptive error.
StatusOr<std::vector<std::string>> ValidateStringArrayField(
nlohmann::json const& json, absl::string_view name,
absl::string_view object_name, internal::ErrorContext const& ec);

/// Use when a JSON field cannot be found but is required.
Status MissingFieldError(absl::string_view name, absl::string_view object_name,
internal::ErrorContext const& ec);
Expand Down
62 changes: 50 additions & 12 deletions google/cloud/internal/json_parsing_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ namespace {
using ::google::cloud::testing_util::StatusIs;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::Pair;

TEST(ExternalAccountParsing, ValidateStringFieldSuccess) {
TEST(JsonParsingTest, ValidateStringFieldSuccess) {
auto const json = nlohmann::json{{"someField", "value"}};
auto actual = ValidateStringField(
json, "someField", "test-object",
Expand All @@ -37,7 +38,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldSuccess) {
EXPECT_EQ(*actual, "value");
}

TEST(ExternalAccountParsing, ValidateStringFieldMissing) {
TEST(JsonParsingTest, ValidateStringFieldMissing) {
auto const json = nlohmann::json{{"some-field", "value"}};
auto actual = ValidateStringField(
json, "missingField", "test-object",
Expand All @@ -51,7 +52,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldMissing) {
Contains(Pair("origin", "test")));
}

TEST(ExternalAccountParsing, ValidateStringFieldNotString) {
TEST(JsonParsingTest, ValidateStringFieldNotString) {
auto const json =
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
auto actual = ValidateStringField(
Expand All @@ -66,7 +67,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldNotString) {
Contains(Pair("origin", "test")));
}

TEST(ExternalAccountParsing, ValidateStringFieldDefaultSuccess) {
TEST(JsonParsingTest, ValidateStringFieldDefaultSuccess) {
auto const json = nlohmann::json{{"someField", "value"}};
auto actual = ValidateStringField(
json, "someField", "test-object", "default-value",
Expand All @@ -75,7 +76,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldDefaultSuccess) {
EXPECT_EQ(*actual, "value");
}

TEST(ExternalAccountParsing, ValidateStringFieldDefaultMissing) {
TEST(JsonParsingTest, ValidateStringFieldDefaultMissing) {
auto const json = nlohmann::json{{"anotherField", "value"}};
auto actual = ValidateStringField(
json, "someField", "test-object", "default-value",
Expand All @@ -84,7 +85,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldDefaultMissing) {
EXPECT_EQ(*actual, "default-value");
}

TEST(ExternalAccountParsing, ValidateStringFieldDefaultNotInt) {
TEST(JsonParsingTest, ValidateStringFieldDefaultNotInt) {
auto const json =
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
auto actual = ValidateStringField(
Expand All @@ -99,7 +100,7 @@ TEST(ExternalAccountParsing, ValidateStringFieldDefaultNotInt) {
Contains(Pair("origin", "test")));
}

TEST(ExternalAccountParsing, ValidateIntFieldSuccess) {
TEST(JsonParsingTest, ValidateIntFieldSuccess) {
auto const json = nlohmann::json{{"someField", 42}};
auto actual = ValidateIntField(
json, "someField", "test-object",
Expand All @@ -108,7 +109,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldSuccess) {
EXPECT_EQ(*actual, 42);
}

TEST(ExternalAccountParsing, ValidateIntFieldMissing) {
TEST(JsonParsingTest, ValidateIntFieldMissing) {
auto const json = nlohmann::json{{"some-field", 42}};
auto actual = ValidateIntField(
json, "missingField", "test-object",
Expand All @@ -122,7 +123,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldMissing) {
Contains(Pair("origin", "test")));
}

TEST(ExternalAccountParsing, ValidateIntFieldNotString) {
TEST(JsonParsingTest, ValidateIntFieldNotString) {
auto const json =
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
auto actual = ValidateIntField(
Expand All @@ -137,7 +138,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldNotString) {
Contains(Pair("origin", "test")));
}

TEST(ExternalAccountParsing, ValidateIntFieldDefaultSuccess) {
TEST(JsonParsingTest, ValidateIntFieldDefaultSuccess) {
auto const json = nlohmann::json{{"someField", 42}};
auto actual = ValidateIntField(
json, "someField", "test-object", 42,
Expand All @@ -146,7 +147,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldDefaultSuccess) {
EXPECT_EQ(*actual, 42);
}

TEST(ExternalAccountParsing, ValidateIntFieldDefaultMissing) {
TEST(JsonParsingTest, ValidateIntFieldDefaultMissing) {
auto const json = nlohmann::json{{"anotherField", "value"}};
auto actual = ValidateIntField(
json, "someField", "test-object", 42,
Expand All @@ -155,7 +156,7 @@ TEST(ExternalAccountParsing, ValidateIntFieldDefaultMissing) {
EXPECT_EQ(*actual, 42);
}

TEST(ExternalAccountParsing, ValidateIntFieldDefaultNotString) {
TEST(JsonParsingTest, ValidateIntFieldDefaultNotString) {
auto const json =
nlohmann::json{{"some-field", "value"}, {"wrongType", true}};
auto actual = ValidateIntField(
Expand All @@ -170,6 +171,43 @@ TEST(ExternalAccountParsing, ValidateIntFieldDefaultNotString) {
Contains(Pair("origin", "test")));
}

TEST(JsonParsingTest, ValidateStringArrayFieldSuccess) {
auto const json = nlohmann::json{{"someField", {"value1", "value2"}}};
auto actual = ValidateStringArrayField(
json, "someField", "test-object",
internal::ErrorContext({{"origin", "test"}, {"filename", "/dev/null"}}));
ASSERT_STATUS_OK(actual);
EXPECT_THAT(*actual, ElementsAre("value1", "value2"));
}

TEST(JsonParsingTest, ValidateStringArrayFieldMissing) {
auto const json = nlohmann::json{{"some-field", {"value1", "value2"}}};
auto actual = ValidateStringArrayField(
json, "missingField", "test-object",
internal::ErrorContext({{"origin", "test"}, {"filename", "/dev/null"}}));
EXPECT_THAT(actual, StatusIs(StatusCode::kInvalidArgument,
AllOf(HasSubstr("missingField"),
HasSubstr("test-object"))));
EXPECT_THAT(actual.status().error_info().metadata(),
Contains(Pair("filename", "/dev/null")));
EXPECT_THAT(actual.status().error_info().metadata(),
Contains(Pair("origin", "test")));
}

TEST(JsonParsingTest, ValidateStringArrayFieldNotString) {
auto const json = nlohmann::json({"wrongType", {"value1", true}});
auto actual = ValidateStringArrayField(
json, "wrongType", "test-object",
internal::ErrorContext({{"origin", "test"}, {"filename", "/dev/null"}}));
EXPECT_THAT(actual, StatusIs(StatusCode::kInvalidArgument,
AllOf(HasSubstr("wrongType"),
HasSubstr("test-object"))));
EXPECT_THAT(actual.status().error_info().metadata(),
Contains(Pair("filename", "/dev/null")));
EXPECT_THAT(actual.status().error_info().metadata(),
Contains(Pair("origin", "test")));
}

} // namespace
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ class MockMinimalIamCredentialsRest : public MinimalIamCredentialsRest {
public:
MOCK_METHOD(StatusOr<google::cloud::AccessToken>, GenerateAccessToken,
(GenerateAccessTokenRequest const&), (override));
MOCK_METHOD(StatusOr<AllowedLocationsResponse>, AllowedLocations,
(ServiceAccountAllowedLocationsRequest const&), (override));
MOCK_METHOD(StatusOr<AllowedLocationsResponse>, AllowedLocations,
(WorkloadIdentityAllowedLocationsRequest const&), (override));
MOCK_METHOD(StatusOr<AllowedLocationsResponse>, AllowedLocations,
(WorkforceIdentityAllowedLocationsRequest const&), (override));
MOCK_METHOD(StatusOr<std::string>, universe_domain, (Options const& options),
(override, const));
};
Expand Down
131 changes: 129 additions & 2 deletions google/cloud/internal/oauth2_minimal_iam_credentials_rest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ namespace google {
namespace cloud {
namespace oauth2_internal {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace {

std::string IamCredentialsEndpoint(
StatusOr<std::string> const& universe_domain) {
return absl::StrCat("https://iamcredentials.",
universe_domain ? *universe_domain : "googleapis.com");
}

} // namespace

using ::google::cloud::internal::InvalidArgumentError;

Expand Down Expand Up @@ -73,12 +82,60 @@ MinimalIamCredentialsRestStub::GenerateAccessToken(

std::string MinimalIamCredentialsRestStub::MakeRequestPath(
GenerateAccessTokenRequest const& request) const {
auto ud = universe_domain(Options{});
return absl::StrCat("https://iamcredentials.", ud ? *ud : "googleapis.com",
return absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
"/v1/projects/-/serviceAccounts/",
request.service_account, ":generateAccessToken");
}

StatusOr<AllowedLocationsResponse>
MinimalIamCredentialsRestStub::AllowedLocationsHelper(std::string path) {
auto authorization_header =
credentials_->Authorization(std::chrono::system_clock::now());
if (!authorization_header) return std::move(authorization_header).status();
rest_internal::RestRequest rest_request;
rest_request.AddHeader(*std::move(authorization_header));
rest_request.SetPath(std::move(path));

auto client = client_factory_(options_);
rest_internal::RestContext context;
auto response = client->Get(context, rest_request);
if (!response) return std::move(response).status();
return ParseAllowedLocationsResponse(
**response,
internal::ErrorContext(
{{"gcloud-cpp.root.class", "MinimalIamCredentialsRestStub"},
{"gcloud-cpp.root.function", __func__},
{"path", rest_request.path()}}));
}

StatusOr<AllowedLocationsResponse>
MinimalIamCredentialsRestStub::AllowedLocations(
ServiceAccountAllowedLocationsRequest const& request) {
auto path = absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
"/v1/projects/-/serviceAccounts/",
request.service_account_email, "/allowedLocations");
return AllowedLocationsHelper(std::move(path));
}

StatusOr<AllowedLocationsResponse>
MinimalIamCredentialsRestStub::AllowedLocations(
WorkloadIdentityAllowedLocationsRequest const& request) {
auto path = absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
"/v1/projects/", request.project_id,
"/locations/global/workloadIdentityPools/",
request.pool_id, "/allowedLocations");
return AllowedLocationsHelper(std::move(path));
}

StatusOr<AllowedLocationsResponse>
MinimalIamCredentialsRestStub::AllowedLocations(
WorkforceIdentityAllowedLocationsRequest const& request) {
auto path = absl::StrCat(IamCredentialsEndpoint(universe_domain(Options{})),
"/v1/locations/global/workforcePools/",
request.pool_id, "/allowedLocations");
return AllowedLocationsHelper(std::move(path));
}

MinimalIamCredentialsRestLogging::MinimalIamCredentialsRestLogging(
std::shared_ptr<MinimalIamCredentialsRest> child)
: child_(std::move(child)) {}
Expand All @@ -104,6 +161,53 @@ MinimalIamCredentialsRestLogging::GenerateAccessToken(
return response;
}

StatusOr<AllowedLocationsResponse>
MinimalIamCredentialsRestLogging::AllowedLocations(
ServiceAccountAllowedLocationsRequest const& request) {
GCP_LOG(INFO) << __func__ << "() << {service_account_email="
<< request.service_account_email << "}";
auto response = child_->AllowedLocations(request);
if (!response) {
GCP_LOG(INFO) << __func__ << "() >> status={" << response.status() << "}";
return response;
}
GCP_LOG(INFO) << __func__ << "() >> response={locations="
<< absl::StrJoin(response->locations, ",")
<< ", encoded_locations=" << response->encoded_locations << "}";
return response;
}

StatusOr<AllowedLocationsResponse>
MinimalIamCredentialsRestLogging::AllowedLocations(
WorkloadIdentityAllowedLocationsRequest const& request) {
GCP_LOG(INFO) << __func__ << "() << {project_id=" << request.project_id
<< ", pool_id=" << request.pool_id << "}";
auto response = child_->AllowedLocations(request);
if (!response) {
GCP_LOG(INFO) << __func__ << "() >> status={" << response.status() << "}";
return response;
}
GCP_LOG(INFO) << __func__ << "() >> response={locations="
<< absl::StrJoin(response->locations, ",")
<< ", encoded_locations=" << response->encoded_locations << "}";
return response;
}

StatusOr<AllowedLocationsResponse>
MinimalIamCredentialsRestLogging::AllowedLocations(
WorkforceIdentityAllowedLocationsRequest const& request) {
GCP_LOG(INFO) << __func__ << "() << {pool_id=" << request.pool_id << "}";
auto response = child_->AllowedLocations(request);
if (!response) {
GCP_LOG(INFO) << __func__ << "() >> status={" << response.status() << "}";
return response;
}
GCP_LOG(INFO) << __func__ << "() >> response={locations="
<< absl::StrJoin(response->locations, ",")
<< ", encoded_locations=" << response->encoded_locations << "}";
return response;
}

StatusOr<AccessToken> ParseGenerateAccessTokenResponse(
rest_internal::RestResponse& response,
google::cloud::internal::ErrorContext const& ec) {
Expand Down Expand Up @@ -132,6 +236,29 @@ StatusOr<AccessToken> ParseGenerateAccessTokenResponse(
return google::cloud::AccessToken{*std::move(token), *expire_time};
}

StatusOr<AllowedLocationsResponse> ParseAllowedLocationsResponse(
rest_internal::RestResponse& response,
google::cloud::internal::ErrorContext const& ec) {
if (IsHttpError(response)) return AsStatus(std::move(response));
auto response_payload =
rest_internal::ReadAll(std::move(response).ExtractPayload());
if (!response_payload) return std::move(response_payload).status();
auto parsed = nlohmann::json::parse(*response_payload, nullptr, false);
if (!parsed.is_object()) {
return InvalidArgumentError("cannot parse response as a JSON object",
GCP_ERROR_INFO().WithContext(ec));
}
auto locations = ValidateStringArrayField(parsed, "locations",
"AllowedLocations() response", ec);
if (!locations) return std::move(locations).status();

auto encoded_locations = ValidateStringField(
parsed, "encodedLocations", "AllowedLocations() response", ec);
if (!encoded_locations) return std::move(encoded_locations).status();
return AllowedLocationsResponse{*std::move(locations),
*std::move(encoded_locations)};
}

std::shared_ptr<MinimalIamCredentialsRest> MakeMinimalIamCredentialsRestStub(
std::shared_ptr<oauth2_internal::Credentials> credentials, Options options,
HttpClientFactory client_factory) {
Expand Down
Loading
Loading