diff --git a/ci/cloudbuild/builds/lib/integration.sh b/ci/cloudbuild/builds/lib/integration.sh index bd27a9403c82f..bcd7683741921 100644 --- a/ci/cloudbuild/builds/lib/integration.sh +++ b/ci/cloudbuild/builds/lib/integration.sh @@ -31,7 +31,7 @@ source module ci/lib/io.sh export PATH="${HOME}/.local/bin:${PATH}" python3 -m pip uninstall -y --quiet googleapis-storage-testbench python3 -m pip install --upgrade --user --quiet --disable-pip-version-check \ - "git+https://github.com/googleapis/storage-testbench@v0.52.0" + "git+https://github.com/googleapis/storage-testbench@v0.55.0" # Some of the tests will need a valid roots.pem file. rm -f /dev/shm/roots.pem diff --git a/google/cloud/storage/bucket_ip_filter.cc b/google/cloud/storage/bucket_ip_filter.cc new file mode 100644 index 0000000000000..ce54f0ade0681 --- /dev/null +++ b/google/cloud/storage/bucket_ip_filter.cc @@ -0,0 +1,96 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/storage/bucket_ip_filter.h" +#include "google/cloud/internal/absl_str_join_quiet.h" +#include "google/cloud/internal/ios_flags_saver.h" +#include + +namespace google { +namespace cloud { +namespace storage { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +bool operator==(BucketIpFilterPublicNetworkSource const& lhs, + BucketIpFilterPublicNetworkSource const& rhs) { + return lhs.allowed_ip_cidr_ranges == rhs.allowed_ip_cidr_ranges; +} + +bool operator!=(BucketIpFilterPublicNetworkSource const& lhs, + BucketIpFilterPublicNetworkSource const& rhs) { + return !(lhs == rhs); +} + +std::ostream& operator<<(std::ostream& os, + BucketIpFilterPublicNetworkSource const& rhs) { + return os << "BucketIpFilterPublicNetworkSource={allowed_ip_cidr_ranges=[" + << absl::StrJoin(rhs.allowed_ip_cidr_ranges, ", ") << "]}"; +} + +bool operator==(BucketIpFilterVpcNetworkSource const& lhs, + BucketIpFilterVpcNetworkSource const& rhs) { + return std::tie(lhs.network, lhs.allowed_ip_cidr_ranges) == + std::tie(rhs.network, rhs.allowed_ip_cidr_ranges); +} + +bool operator!=(BucketIpFilterVpcNetworkSource const& lhs, + BucketIpFilterVpcNetworkSource const& rhs) { + return !(lhs == rhs); +} + +std::ostream& operator<<(std::ostream& os, + BucketIpFilterVpcNetworkSource const& rhs) { + return os << "BucketIpFilterVpcNetworkSource={network=" << rhs.network + << ", allowed_ip_cidr_ranges=[" + << absl::StrJoin(rhs.allowed_ip_cidr_ranges, ", ") << "]}"; +} + +bool operator==(BucketIpFilter const& lhs, BucketIpFilter const& rhs) { + return std::tie(lhs.allow_all_service_agent_access, lhs.allow_cross_org_vpcs, + lhs.mode, lhs.public_network_source, + lhs.vpc_network_sources) == + std::tie(rhs.allow_all_service_agent_access, rhs.allow_cross_org_vpcs, + rhs.mode, rhs.public_network_source, rhs.vpc_network_sources); +} + +bool operator!=(BucketIpFilter const& lhs, BucketIpFilter const& rhs) { + return !(lhs == rhs); +} + +std::ostream& operator<<(std::ostream& os, BucketIpFilter const& rhs) { + google::cloud::internal::IosFlagsSaver save_format(os); + os << "BucketIpFilter={mode=" << rhs.mode.value_or(""); + if (rhs.allow_all_service_agent_access) { + os << ", allow_all_service_agent_access=" << std::boolalpha + << *rhs.allow_all_service_agent_access; + } + if (rhs.allow_cross_org_vpcs) { + os << ", allow_cross_org_vpcs=" << std::boolalpha + << *rhs.allow_cross_org_vpcs; + } + if (rhs.public_network_source) { + os << ", public_network_source=" << *rhs.public_network_source; + } + if (rhs.vpc_network_sources) { + os << ", vpc_network_sources=[" + << absl::StrJoin(*rhs.vpc_network_sources, ", ", absl::StreamFormatter()) + << "]"; + } + return os << "}"; +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/bucket_ip_filter.h b/google/cloud/storage/bucket_ip_filter.h new file mode 100644 index 0000000000000..fe2b39ac86b57 --- /dev/null +++ b/google/cloud/storage/bucket_ip_filter.h @@ -0,0 +1,82 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_IP_FILTER_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_IP_FILTER_H + +#include "google/cloud/storage/version.h" +#include "absl/types/optional.h" +#include +#include +#include + +namespace google { +namespace cloud { +namespace storage { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * Defines a public network source for the bucket IP filter. + */ +struct BucketIpFilterPublicNetworkSource { + std::vector allowed_ip_cidr_ranges; +}; + +bool operator==(BucketIpFilterPublicNetworkSource const& lhs, + BucketIpFilterPublicNetworkSource const& rhs); +bool operator!=(BucketIpFilterPublicNetworkSource const& lhs, + BucketIpFilterPublicNetworkSource const& rhs); + +std::ostream& operator<<(std::ostream& os, + BucketIpFilterPublicNetworkSource const& rhs); + +/** + * Defines a VPC network source for the bucket IP filter. + */ +struct BucketIpFilterVpcNetworkSource { + std::string network; + std::vector allowed_ip_cidr_ranges; +}; + +bool operator==(BucketIpFilterVpcNetworkSource const& lhs, + BucketIpFilterVpcNetworkSource const& rhs); +bool operator!=(BucketIpFilterVpcNetworkSource const& lhs, + BucketIpFilterVpcNetworkSource const& rhs); + +std::ostream& operator<<(std::ostream& os, + BucketIpFilterVpcNetworkSource const& rhs); + +/** + * The IP filtering configuration for a Bucket. + */ +struct BucketIpFilter { + absl::optional allow_all_service_agent_access; + absl::optional allow_cross_org_vpcs; + absl::optional mode; + absl::optional public_network_source; + absl::optional> + vpc_network_sources; +}; + +bool operator==(BucketIpFilter const& lhs, BucketIpFilter const& rhs); +bool operator!=(BucketIpFilter const& lhs, BucketIpFilter const& rhs); + +std::ostream& operator<<(std::ostream& os, BucketIpFilter const& rhs); + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_IP_FILTER_H diff --git a/google/cloud/storage/bucket_ip_filter_test.cc b/google/cloud/storage/bucket_ip_filter_test.cc new file mode 100644 index 0000000000000..d8a21e8a8c569 --- /dev/null +++ b/google/cloud/storage/bucket_ip_filter_test.cc @@ -0,0 +1,116 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/storage/bucket_ip_filter.h" +#include + +namespace google { +namespace cloud { +namespace storage { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +namespace { + +TEST(BucketIpFilterTest, PublicNetworkSource) { + BucketIpFilterPublicNetworkSource source; + source.allowed_ip_cidr_ranges.emplace_back("1.2.3.4/32"); + source.allowed_ip_cidr_ranges.emplace_back("5.6.7.8/32"); + + BucketIpFilterPublicNetworkSource copy = source; + EXPECT_EQ(source, copy); + + copy.allowed_ip_cidr_ranges.pop_back(); + EXPECT_NE(source, copy); +} + +TEST(BucketIpFilterTest, PublicNetworkSourceOrderMatters) { + BucketIpFilterPublicNetworkSource const source1{{"1.2.3.4/32", "5.6.7.8/32"}}; + BucketIpFilterPublicNetworkSource const source2{{"5.6.7.8/32", "1.2.3.4/32"}}; + + // The two sources have the same elements but in a different order. + // They should NOT be equal. + EXPECT_NE(source1, source2); +} + +TEST(BucketIpFilterTest, VpcNetworkSource) { + BucketIpFilterVpcNetworkSource source; + source.network = "projects/p/global/networks/n"; + source.allowed_ip_cidr_ranges.emplace_back("1.2.3.4/32"); + source.allowed_ip_cidr_ranges.emplace_back("5.6.7.8/32"); + + BucketIpFilterVpcNetworkSource copy = source; + EXPECT_EQ(source, copy); + + copy.network = "changed"; + EXPECT_NE(source, copy); +} + +TEST(BucketIpFilterTest, VpcNetworkSourceOrderMatters) { + BucketIpFilterVpcNetworkSource const source1{"projects/p/global/networks/n", + {"1.2.3.4/32", "5.6.7.8/32"}}; + BucketIpFilterVpcNetworkSource const source2{"projects/p/global/networks/n", + {"5.6.7.8/32", "1.2.3.4/32"}}; + + // The two sources have the same elements but in a different order. + // They should NOT be equal. + EXPECT_NE(source1, source2); +} + +TEST(BucketIpFilterTest, IpFilter) { + BucketIpFilter filter; + filter.mode = "Enabled"; + filter.allow_all_service_agent_access = true; + filter.allow_cross_org_vpcs = true; + filter.public_network_source = + BucketIpFilterPublicNetworkSource{{"1.2.3.4/32"}}; + filter.vpc_network_sources = + absl::make_optional>( + {BucketIpFilterVpcNetworkSource{"projects/p/global/networks/n", + {"5.6.7.8/32"}}, + BucketIpFilterVpcNetworkSource{"projects/p/global/networks/m", + {"9.0.1.2/32"}}}); + + BucketIpFilter copy = filter; + EXPECT_EQ(filter, copy); + + copy.mode = "Disabled"; + EXPECT_NE(filter, copy); +} + +TEST(BucketIpFilterTest, IpFilterOrderMatters) { + BucketIpFilter filter1; + filter1.vpc_network_sources = + absl::make_optional>( + {BucketIpFilterVpcNetworkSource{"projects/p/global/networks/n", + {"1.2.3.4/32"}}, + BucketIpFilterVpcNetworkSource{"projects/p/global/networks/m", + {"5.6.7.8/32"}}}); + + BucketIpFilter filter2; + filter2.vpc_network_sources = + absl::make_optional>( + {BucketIpFilterVpcNetworkSource{"projects/p/global/networks/m", + {"5.6.7.8/32"}}, + BucketIpFilterVpcNetworkSource{"projects/p/global/networks/n", + {"1.2.3.4/32"}}}); + + // The two filters have the same elements but in a different order. + // They should NOT be equal. + EXPECT_NE(filter1, filter2); +} + +} // namespace +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace storage +} // namespace cloud +} // namespace google diff --git a/google/cloud/storage/bucket_metadata.cc b/google/cloud/storage/bucket_metadata.cc index f10c02faa01f2..60019e4eafcdf 100644 --- a/google/cloud/storage/bucket_metadata.cc +++ b/google/cloud/storage/bucket_metadata.cc @@ -96,6 +96,7 @@ bool operator==(BucketMetadata const& lhs, BucketMetadata const& rhs) { && lhs.etag_ == rhs.etag_ // && lhs.hierarchical_namespace_ == rhs.hierarchical_namespace_ // && lhs.iam_configuration_ == rhs.iam_configuration_ // + && lhs.ip_filter_ == rhs.ip_filter_ // && lhs.id_ == rhs.id_ // && lhs.kind_ == rhs.kind_ // && lhs.labels_ == rhs.labels_ // @@ -164,6 +165,10 @@ std::ostream& operator<<(std::ostream& os, BucketMetadata const& rhs) { os << ", iam_configuration=" << rhs.iam_configuration(); } + if (rhs.has_ip_filter()) { + os << ", ip_filter=" << rhs.ip_filter(); + } + os << ", id=" << rhs.id() << ", kind=" << rhs.kind(); for (auto const& kv : rhs.labels_) { @@ -402,6 +407,51 @@ BucketMetadataPatchBuilder::ResetIamConfiguration() { return *this; } +BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::SetIpFilter( + BucketIpFilter const& v) { + internal::PatchBuilder ip_filter; + if (v.mode.has_value()) { + ip_filter.SetStringField("mode", *v.mode); + } + if (v.allow_all_service_agent_access.has_value()) { + ip_filter.SetBoolField("allowAllServiceAgentAccess", + *v.allow_all_service_agent_access); + } + if (v.allow_cross_org_vpcs.has_value()) { + ip_filter.SetBoolField("allowCrossOrgVpcs", *v.allow_cross_org_vpcs); + } + if (v.public_network_source.has_value()) { + internal::PatchBuilder public_network_source; + auto array = nlohmann::json::array(); + for (auto const& r : v.public_network_source->allowed_ip_cidr_ranges) { + array.emplace_back(r); + } + public_network_source.SetArrayField("allowedIpCidrRanges", array.dump()); + ip_filter.AddSubPatch("publicNetworkSource", public_network_source); + } + if (v.vpc_network_sources.has_value()) { + auto array = nlohmann::json::array(); + for (auto const& r : *v.vpc_network_sources) { + nlohmann::json vpc_network_source; + vpc_network_source["network"] = r.network; + auto allowed_ips = nlohmann::json::array(); + for (auto const& ip : r.allowed_ip_cidr_ranges) { + allowed_ips.emplace_back(ip); + } + vpc_network_source["allowedIpCidrRanges"] = allowed_ips; + array.emplace_back(vpc_network_source); + } + ip_filter.SetArrayField("vpcNetworkSources", array.dump()); + } + impl_.AddSubPatch("ipFilter", ip_filter); + return *this; +} + +BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::ResetIpFilter() { + impl_.RemoveField("ipFilter"); + return *this; +} + BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::SetHierarchicalNamespace( BucketHierarchicalNamespace const& v) { diff --git a/google/cloud/storage/bucket_metadata.h b/google/cloud/storage/bucket_metadata.h index 44e004c25d532..09d22b19b3f8d 100644 --- a/google/cloud/storage/bucket_metadata.h +++ b/google/cloud/storage/bucket_metadata.h @@ -23,6 +23,7 @@ #include "google/cloud/storage/bucket_encryption.h" #include "google/cloud/storage/bucket_hierarchical_namespace.h" #include "google/cloud/storage/bucket_iam_configuration.h" +#include "google/cloud/storage/bucket_ip_filter.h" #include "google/cloud/storage/bucket_lifecycle.h" #include "google/cloud/storage/bucket_logging.h" #include "google/cloud/storage/bucket_object_retention.h" @@ -269,6 +270,23 @@ class BucketMetadata { } ///@} + /// @name Accessors and modifiers for the IP filter configuration. + ///@{ + bool has_ip_filter() const { return ip_filter_.has_value(); } + BucketIpFilter const& ip_filter() const { return *ip_filter_; } + absl::optional const& ip_filter_as_optional() const { + return ip_filter_; + } + BucketMetadata& set_ip_filter(BucketIpFilter v) { + ip_filter_ = std::move(v); + return *this; + } + BucketMetadata& reset_ip_filter() { + ip_filter_.reset(); + return *this; + } + ///@} + /// Return the bucket id. std::string const& id() const { return id_; } @@ -664,6 +682,7 @@ class BucketMetadata { std::string etag_; absl::optional hierarchical_namespace_; absl::optional iam_configuration_; + absl::optional ip_filter_; std::string id_; std::string kind_; std::map labels_; @@ -742,6 +761,9 @@ class BucketMetadataPatchBuilder { BucketIamConfiguration const& v); BucketMetadataPatchBuilder& ResetIamConfiguration(); + BucketMetadataPatchBuilder& SetIpFilter(BucketIpFilter const& v); + BucketMetadataPatchBuilder& ResetIpFilter(); + /// Sets a new hierarchical namespace configuration. BucketMetadataPatchBuilder& SetHierarchicalNamespace( BucketHierarchicalNamespace const& v); diff --git a/google/cloud/storage/bucket_metadata_test.cc b/google/cloud/storage/bucket_metadata_test.cc index 23f307f3a988c..da580280cd388 100644 --- a/google/cloud/storage/bucket_metadata_test.cc +++ b/google/cloud/storage/bucket_metadata_test.cc @@ -120,6 +120,18 @@ BucketMetadata CreateBucketMetadataForTest() { }, "publicAccessPrevention": "inherited" }, + "ipFilter": { + "mode": "Enabled", + "allowAllServiceAgentAccess": true, + "allowCrossOrgVpcs": true, + "publicNetworkSource": { + "allowedIpCidrRanges": ["1.2.3.4/32"] + }, + "vpcNetworkSources": [{ + "network": "projects/p/global/networks/n", + "allowedIpCidrRanges": ["5.6.7.8/32", "5.6.7.9/32"] + }] + }, "id": "test-bucket", "kind": "storage#bucket", "labels": { @@ -239,6 +251,21 @@ TEST(BucketMetadataTest, Parse) { actual.iam_configuration().uniform_bucket_level_access->locked_time)); EXPECT_EQ(actual.iam_configuration().public_access_prevention.value_or(""), "inherited"); + + // ip_filter + ASSERT_TRUE(actual.has_ip_filter()); + EXPECT_EQ(actual.ip_filter().mode, "Enabled"); + EXPECT_TRUE(actual.ip_filter().allow_all_service_agent_access); + EXPECT_TRUE(actual.ip_filter().allow_cross_org_vpcs); + ASSERT_TRUE(actual.ip_filter().public_network_source.has_value()); + EXPECT_THAT(actual.ip_filter().public_network_source->allowed_ip_cidr_ranges, + ElementsAre("1.2.3.4/32")); + ASSERT_TRUE(actual.ip_filter().vpc_network_sources.has_value()); + EXPECT_THAT( + *actual.ip_filter().vpc_network_sources, + ElementsAre(BucketIpFilterVpcNetworkSource{ + "projects/p/global/networks/n", {"5.6.7.8/32", "5.6.7.9/32"}})); + EXPECT_EQ("test-bucket", actual.id()); EXPECT_EQ("storage#bucket", actual.kind()); EXPECT_EQ(2, actual.labels().size()); @@ -389,6 +416,9 @@ TEST(BucketMetadataTest, IOStream) { EXPECT_THAT(actual, HasSubstr("locked_time=2020-01-02T03:04:05Z")); EXPECT_THAT(actual, HasSubstr("public_access_prevention=inherited")); + // ip_filter() + EXPECT_THAT(actual, HasSubstr("ip_filter=BucketIpFilter")); + // lifecycle() EXPECT_THAT(actual, HasSubstr("age=30")); @@ -1296,6 +1326,54 @@ TEST(BucketMetadataPatchBuilder, ResetIamConfiguration) { ASSERT_TRUE(json["iamConfiguration"].is_null()) << json; } +TEST(BucketMetadataPatchBuilder, SetIpFilter) { + BucketMetadataPatchBuilder builder; + BucketIpFilter ip_filter; + ip_filter.mode = "Enabled"; + ip_filter.allow_all_service_agent_access = true; + ip_filter.allow_cross_org_vpcs = true; + ip_filter.public_network_source = + BucketIpFilterPublicNetworkSource{{"1.2.3.4/32"}}; + ip_filter.vpc_network_sources = + absl::make_optional>( + {BucketIpFilterVpcNetworkSource{"projects/p/global/networks/n", + {"5.6.7.8/32", "5.6.7.9/32"}}, + BucketIpFilterVpcNetworkSource{"projects/p/global/networks/m", + {"9.0.1.2/32"}}}); + builder.SetIpFilter(ip_filter); + + auto actual = builder.BuildPatch(); + auto json = nlohmann::json::parse(actual); + ASSERT_EQ(1U, json.count("ipFilter")) << json; + ASSERT_TRUE(json["ipFilter"].is_object()) << json; + auto const expected = nlohmann::json{ + {"mode", "Enabled"}, + {"allowAllServiceAgentAccess", true}, + {"allowCrossOrgVpcs", true}, + {"publicNetworkSource", + {{"allowedIpCidrRanges", nlohmann::json::array({"1.2.3.4/32"})}}}, + {"vpcNetworkSources", + nlohmann::json::array({ + {{"network", "projects/p/global/networks/n"}, + {"allowedIpCidrRanges", + nlohmann::json::array({"5.6.7.8/32", "5.6.7.9/32"})}}, + {{"network", "projects/p/global/networks/m"}, + {"allowedIpCidrRanges", nlohmann::json::array({"9.0.1.2/32"})}}, + })}, + }; + EXPECT_EQ(json["ipFilter"], expected); +} + +TEST(BucketMetadataPatchBuilder, ResetIpFilter) { + BucketMetadataPatchBuilder builder; + builder.ResetIpFilter(); + + auto actual = builder.BuildPatch(); + auto json = nlohmann::json::parse(actual); + ASSERT_EQ(1U, json.count("ipFilter")) << json; + ASSERT_TRUE(json["ipFilter"].is_null()) << json; +} + TEST(BucketMetadataPatchBuilder, SetHierarchicalNamespace) { BucketMetadataPatchBuilder builder; builder.SetHierarchicalNamespace(BucketHierarchicalNamespace{true}); diff --git a/google/cloud/storage/google_cloud_cpp_storage.bzl b/google/cloud/storage/google_cloud_cpp_storage.bzl index 2c71c5bd327a7..4d0eee214d9c4 100644 --- a/google/cloud/storage/google_cloud_cpp_storage.bzl +++ b/google/cloud/storage/google_cloud_cpp_storage.bzl @@ -26,6 +26,7 @@ google_cloud_cpp_storage_hdrs = [ "bucket_encryption.h", "bucket_hierarchical_namespace.h", "bucket_iam_configuration.h", + "bucket_ip_filter.h", "bucket_lifecycle.h", "bucket_logging.h", "bucket_metadata.h", @@ -167,6 +168,7 @@ google_cloud_cpp_storage_srcs = [ "bucket_custom_placement_config.cc", "bucket_hierarchical_namespace.cc", "bucket_iam_configuration.cc", + "bucket_ip_filter.cc", "bucket_logging.cc", "bucket_metadata.cc", "bucket_object_retention.cc", diff --git a/google/cloud/storage/google_cloud_cpp_storage.cmake b/google/cloud/storage/google_cloud_cpp_storage.cmake index d1fe20b4fa382..952393a976145 100644 --- a/google/cloud/storage/google_cloud_cpp_storage.cmake +++ b/google/cloud/storage/google_cloud_cpp_storage.cmake @@ -38,6 +38,8 @@ add_library( bucket_hierarchical_namespace.h bucket_iam_configuration.cc bucket_iam_configuration.h + bucket_ip_filter.cc + bucket_ip_filter.h bucket_lifecycle.h bucket_logging.cc bucket_logging.h @@ -430,6 +432,7 @@ if (BUILD_TESTING) bucket_access_control_test.cc bucket_cors_entry_test.cc bucket_iam_configuration_test.cc + bucket_ip_filter_test.cc bucket_metadata_test.cc bucket_object_retention_test.cc bucket_soft_delete_policy_test.cc diff --git a/google/cloud/storage/internal/bucket_metadata_parser.cc b/google/cloud/storage/internal/bucket_metadata_parser.cc index 18924bd133a27..cadfbbe3cea71 100644 --- a/google/cloud/storage/internal/bucket_metadata_parser.cc +++ b/google/cloud/storage/internal/bucket_metadata_parser.cc @@ -192,6 +192,49 @@ Status ParseIamConfiguration(BucketMetadata& meta, nlohmann::json const& json) { return Status{}; } +Status ParseIpFilter(BucketMetadata& meta, nlohmann::json const& json) { + if (!json.contains("ipFilter")) return Status{}; + BucketIpFilter value; + auto f = json["ipFilter"]; + if (f.contains("mode")) { + value.mode = f.value("mode", ""); + } + if (f.contains("allowAllServiceAgentAccess")) { + value.allow_all_service_agent_access = + f.value("allowAllServiceAgentAccess", false); + } + if (f.contains("allowCrossOrgVpcs")) { + value.allow_cross_org_vpcs = f.value("allowCrossOrgVpcs", false); + } + if (f.contains("publicNetworkSource")) { + BucketIpFilterPublicNetworkSource pns; + auto p = f["publicNetworkSource"]; + if (p.contains("allowedIpCidrRanges")) { + for (auto const& kv : p["allowedIpCidrRanges"].items()) { + pns.allowed_ip_cidr_ranges.emplace_back(kv.value().get()); + } + } + value.public_network_source = std::move(pns); + } + if (f.contains("vpcNetworkSources")) { + std::vector vns; + for (auto const& kv : f["vpcNetworkSources"].items()) { + BucketIpFilterVpcNetworkSource entry; + entry.network = kv.value().value("network", ""); + if (kv.value().contains("allowedIpCidrRanges")) { + for (auto const& ip : kv.value()["allowedIpCidrRanges"].items()) { + entry.allowed_ip_cidr_ranges.emplace_back( + ip.value().get()); + } + } + vns.push_back(std::move(entry)); + } + value.vpc_network_sources = std::move(vns); + } + meta.set_ip_filter(std::move(value)); + return Status{}; +} + Status ParseLifecycle(BucketMetadata& meta, nlohmann::json const& json) { if (!json.contains("lifecycle")) return Status{}; auto const& l = json["lifecycle"]; @@ -402,6 +445,38 @@ void ToJsonIamConfiguration(nlohmann::json& json, BucketMetadata const& meta) { json["iamConfiguration"] = std::move(value); } +void ToJsonIpFilter(nlohmann::json& json, BucketMetadata const& meta) { + if (!meta.has_ip_filter()) return; + nlohmann::json ip_filter; + auto const& f = meta.ip_filter(); + if (f.mode.has_value()) { + ip_filter["mode"] = *f.mode; + } + if (f.allow_all_service_agent_access.has_value()) { + ip_filter["allowAllServiceAgentAccess"] = *f.allow_all_service_agent_access; + } + if (f.allow_cross_org_vpcs.has_value()) { + ip_filter["allowCrossOrgVpcs"] = *f.allow_cross_org_vpcs; + } + if (f.public_network_source.has_value()) { + nlohmann::json pns; + pns["allowedIpCidrRanges"] = + f.public_network_source->allowed_ip_cidr_ranges; + ip_filter["publicNetworkSource"] = std::move(pns); + } + if (f.vpc_network_sources.has_value()) { + nlohmann::json vns; + for (auto const& v : *f.vpc_network_sources) { + nlohmann::json entry; + entry["network"] = v.network; + entry["allowedIpCidrRanges"] = v.allowed_ip_cidr_ranges; + vns.push_back(std::move(entry)); + } + ip_filter["vpcNetworkSources"] = std::move(vns); + } + json["ipFilter"] = std::move(ip_filter); +} + void ToJsonLabels(nlohmann::json& json, BucketMetadata const& meta) { if (meta.labels().empty()) return; nlohmann::json value; @@ -544,6 +619,7 @@ StatusOr BucketMetadataParser::FromJson( }, ParseHierarchicalNamespace, ParseIamConfiguration, + ParseIpFilter, [](BucketMetadata& meta, nlohmann::json const& json) { meta.set_id(json.value("id", "")); return Status{}; @@ -634,6 +710,7 @@ std::string BucketMetadataToJsonString(BucketMetadata const& meta) { ToJsonEncryption(json, meta); ToJsonHierarchicalNamespace(json, meta); ToJsonIamConfiguration(json, meta); + ToJsonIpFilter(json, meta); ToJsonLabels(json, meta); ToJsonLifecycle(json, meta); ToJsonLocation(json, meta); diff --git a/google/cloud/storage/internal/bucket_requests.cc b/google/cloud/storage/internal/bucket_requests.cc index 8cc0834631a98..8178e4090576c 100644 --- a/google/cloud/storage/internal/bucket_requests.cc +++ b/google/cloud/storage/internal/bucket_requests.cc @@ -89,6 +89,16 @@ void DiffIamConfiguration(BucketMetadataPatchBuilder& builder, } } +void DiffIpFilter(BucketMetadataPatchBuilder& builder, BucketMetadata const& o, + BucketMetadata const& u) { + if (o.ip_filter_as_optional() == u.ip_filter_as_optional()) return; + if (u.has_ip_filter()) { + builder.SetIpFilter(u.ip_filter()); + } else { + builder.ResetIpFilter(); + } +} + void DiffLabels(BucketMetadataPatchBuilder& builder, BucketMetadata const& o, BucketMetadata const& u) { if (o.labels() == u.labels()) return; @@ -199,6 +209,7 @@ BucketMetadataPatchBuilder DiffBucketMetadata(BucketMetadata const& original, DiffDefaultObjectAcl(builder, original, updated); DiffEncryption(builder, original, updated); DiffIamConfiguration(builder, original, updated); + DiffIpFilter(builder, original, updated); DiffLabels(builder, original, updated); DiffLifecycle(builder, original, updated); DiffLogging(builder, original, updated); diff --git a/google/cloud/storage/internal/bucket_requests_test.cc b/google/cloud/storage/internal/bucket_requests_test.cc index f66a5de0b02c7..75633095c6817 100644 --- a/google/cloud/storage/internal/bucket_requests_test.cc +++ b/google/cloud/storage/internal/bucket_requests_test.cc @@ -452,6 +452,125 @@ TEST(PatchBucketRequestTest, DiffResetIamConfigurationUBLA) { EXPECT_EQ(expected, patch); } +TEST(PatchBucketRequestTest, DiffSetIpFilter) { + BucketMetadata original = CreateBucketMetadataForTest(); + original.reset_ip_filter(); + BucketMetadata updated = original; + BucketIpFilter ip_filter; + ip_filter.mode = "Enabled"; + ip_filter.allow_all_service_agent_access = true; + ip_filter.allow_cross_org_vpcs = true; + ip_filter.public_network_source = + BucketIpFilterPublicNetworkSource{{"1.2.3.4/32"}}; + ip_filter.vpc_network_sources = + absl::make_optional>( + {BucketIpFilterVpcNetworkSource{"projects/p/global/networks/n", + {"5.6.7.8/32"}}, + BucketIpFilterVpcNetworkSource{"projects/p/global/networks/m", + {"9.0.1.2/32"}}}); + updated.set_ip_filter(std::move(ip_filter)); + PatchBucketRequest request("test-bucket", original, updated); + + auto patch = nlohmann::json::parse(request.payload()); + auto expected = nlohmann::json::parse(R"""({ + "ipFilter": { + "mode": "Enabled", + "allowAllServiceAgentAccess": true, + "allowCrossOrgVpcs": true, + "publicNetworkSource": { + "allowedIpCidrRanges": ["1.2.3.4/32"] + }, + "vpcNetworkSources": [{ + "network": "projects/p/global/networks/n", + "allowedIpCidrRanges": ["5.6.7.8/32"] + }, { + "network": "projects/p/global/networks/m", + "allowedIpCidrRanges": ["9.0.1.2/32"] + }] + } + })"""); + EXPECT_EQ(expected, patch); +} + +TEST(PatchBucketRequestTest, DiffSetIpFilterMultipleCidrRanges) { + BucketMetadata original = CreateBucketMetadataForTest(); + original.reset_ip_filter(); + BucketMetadata updated = original; + BucketIpFilter ip_filter; + ip_filter.mode = "Enabled"; + ip_filter.vpc_network_sources = + absl::make_optional>({ + BucketIpFilterVpcNetworkSource{"projects/p/global/networks/n", + {"1.2.3.4/32", "5.6.7.8/32"}}, + }); + updated.set_ip_filter(std::move(ip_filter)); + PatchBucketRequest request("test-bucket", original, updated); + + auto patch = nlohmann::json::parse(request.payload()); + auto expected = nlohmann::json::parse(R"""({ + "ipFilter": { + "mode": "Enabled", + "vpcNetworkSources": [{ + "network": "projects/p/global/networks/n", + "allowedIpCidrRanges": ["1.2.3.4/32", "5.6.7.8/32"] + }] + } + })"""); + EXPECT_EQ(expected, patch); +} + +TEST(PatchBucketRequestTest, DiffSetIpFilterMultipleCidrRangesAndNetworks) { + BucketMetadata original = CreateBucketMetadataForTest(); + original.reset_ip_filter(); + BucketMetadata updated = original; + BucketIpFilter ip_filter; + ip_filter.mode = "Enabled"; + ip_filter.allow_all_service_agent_access = true; + ip_filter.allow_cross_org_vpcs = true; + ip_filter.public_network_source = + BucketIpFilterPublicNetworkSource{{"1.2.3.4/32"}}; + ip_filter.vpc_network_sources = + absl::make_optional>( + {BucketIpFilterVpcNetworkSource{"projects/p/global/networks/n", + {"5.6.7.8/32", "8.7.6.5/32"}}, + BucketIpFilterVpcNetworkSource{"projects/p/global/networks/m", + {"9.0.1.2/32"}}}); + updated.set_ip_filter(std::move(ip_filter)); + PatchBucketRequest request("test-bucket", original, updated); + + auto patch = nlohmann::json::parse(request.payload()); + auto expected = nlohmann::json::parse(R"""({ + "ipFilter": { + "mode": "Enabled", + "allowAllServiceAgentAccess": true, + "allowCrossOrgVpcs": true, + "publicNetworkSource": { + "allowedIpCidrRanges": ["1.2.3.4/32"] + }, + "vpcNetworkSources": [{ + "network": "projects/p/global/networks/n", + "allowedIpCidrRanges": ["5.6.7.8/32", "8.7.6.5/32"] + }, { + "network": "projects/p/global/networks/m", + "allowedIpCidrRanges": ["9.0.1.2/32"] + }] + } + })"""); + EXPECT_EQ(expected, patch); +} + +TEST(PatchBucketRequestTest, DiffResetIpFilter) { + BucketMetadata original = CreateBucketMetadataForTest(); + original.set_ip_filter(BucketIpFilter{}); + BucketMetadata updated = original; + updated.reset_ip_filter(); + PatchBucketRequest request("test-bucket", original, updated); + + auto patch = nlohmann::json::parse(request.payload()); + auto expected = nlohmann::json::parse(R"""({"ipFilter": null})"""); + EXPECT_EQ(expected, patch); +} + TEST(PatchBucketRequestTest, DiffSetLabels) { BucketMetadata original = CreateBucketMetadataForTest(); original.mutable_labels() = { diff --git a/google/cloud/storage/internal/grpc/bucket_metadata_parser.cc b/google/cloud/storage/internal/grpc/bucket_metadata_parser.cc index 36f159ace5da2..921a10bd066f1 100644 --- a/google/cloud/storage/internal/grpc/bucket_metadata_parser.cc +++ b/google/cloud/storage/internal/grpc/bucket_metadata_parser.cc @@ -113,6 +113,9 @@ google::storage::v2::Bucket ToProto(storage::BucketMetadata const& rhs) { if (rhs.has_soft_delete_policy()) { *result.mutable_soft_delete_policy() = ToProto(rhs.soft_delete_policy()); } + if (rhs.has_ip_filter()) { + *result.mutable_ip_filter() = ToProto(rhs.ip_filter()); + } return result; } @@ -203,6 +206,9 @@ storage::BucketMetadata FromProto(google::storage::v2::Bucket const& rhs, if (rhs.has_autoclass()) { metadata.set_autoclass(FromProto(rhs.autoclass())); } + if (rhs.has_ip_filter()) { + metadata.set_ip_filter(FromProto(rhs.ip_filter())); + } return metadata; } @@ -322,6 +328,70 @@ storage::BucketIamConfiguration FromProto( return result; } +google::storage::v2::Bucket::IpFilter ToProto( + storage::BucketIpFilter const& rhs) { + google::storage::v2::Bucket::IpFilter result; + if (rhs.mode.has_value()) { + result.set_mode(*rhs.mode); + } + if (rhs.allow_all_service_agent_access.has_value()) { + result.set_allow_all_service_agent_access( + *rhs.allow_all_service_agent_access); + } + if (rhs.allow_cross_org_vpcs.has_value()) { + result.set_allow_cross_org_vpcs(*rhs.allow_cross_org_vpcs); + } + if (rhs.public_network_source.has_value()) { + auto& pns = *result.mutable_public_network_source(); + for (auto const& v : rhs.public_network_source->allowed_ip_cidr_ranges) { + pns.add_allowed_ip_cidr_ranges(v); + } + } + if (rhs.vpc_network_sources.has_value()) { + for (auto const& v : *rhs.vpc_network_sources) { + auto& entry = *result.add_vpc_network_sources(); + entry.set_network(v.network); + for (auto const& ip : v.allowed_ip_cidr_ranges) { + entry.add_allowed_ip_cidr_ranges(ip); + } + } + } + return result; +} + +storage::BucketIpFilter FromProto( + google::storage::v2::Bucket::IpFilter const& rhs) { + storage::BucketIpFilter result; + if (!rhs.mode().empty()) { + result.mode = rhs.mode(); + } + if (rhs.has_allow_all_service_agent_access()) { + result.allow_all_service_agent_access = + rhs.allow_all_service_agent_access(); + } + result.allow_cross_org_vpcs = rhs.allow_cross_org_vpcs(); + if (rhs.has_public_network_source()) { + storage::BucketIpFilterPublicNetworkSource pns; + for (auto const& v : rhs.public_network_source().allowed_ip_cidr_ranges()) { + pns.allowed_ip_cidr_ranges.push_back(v); + } + result.public_network_source = std::move(pns); + } + if (!rhs.vpc_network_sources().empty()) { + std::vector vns; + for (auto const& v : rhs.vpc_network_sources()) { + storage::BucketIpFilterVpcNetworkSource entry; + entry.network = v.network(); + for (auto const& ip : v.allowed_ip_cidr_ranges()) { + entry.allowed_ip_cidr_ranges.push_back(ip); + } + vns.push_back(std::move(entry)); + } + result.vpc_network_sources = std::move(vns); + } + return result; +} + google::storage::v2::Bucket::Lifecycle::Rule::Action ToProto( storage::LifecycleRuleAction rhs) { google::storage::v2::Bucket::Lifecycle::Rule::Action result; diff --git a/google/cloud/storage/internal/grpc/bucket_metadata_parser.h b/google/cloud/storage/internal/grpc/bucket_metadata_parser.h index 4efc7631ffabe..00a41b6ee2a3e 100644 --- a/google/cloud/storage/internal/grpc/bucket_metadata_parser.h +++ b/google/cloud/storage/internal/grpc/bucket_metadata_parser.h @@ -50,6 +50,11 @@ google::storage::v2::Bucket::IamConfig ToProto( storage::BucketIamConfiguration FromProto( google::storage::v2::Bucket::IamConfig const& rhs); +google::storage::v2::Bucket::IpFilter ToProto( + storage::BucketIpFilter const& rhs); +storage::BucketIpFilter FromProto( + google::storage::v2::Bucket::IpFilter const& rhs); + google::storage::v2::Bucket::Lifecycle::Rule::Action ToProto( storage::LifecycleRuleAction rhs); storage::LifecycleRuleAction FromProto( diff --git a/google/cloud/storage/internal/grpc/bucket_metadata_parser_test.cc b/google/cloud/storage/internal/grpc/bucket_metadata_parser_test.cc index b2c234ce90e15..58e5cf49aba3d 100644 --- a/google/cloud/storage/internal/grpc/bucket_metadata_parser_test.cc +++ b/google/cloud/storage/internal/grpc/bucket_metadata_parser_test.cc @@ -574,6 +574,35 @@ TEST(GrpcBucketMetadataParser, BucketCustomPlacementConfigRoundtrip) { EXPECT_THAT(end, IsProtoEqual(start)); } +TEST(GrpcBucketMetadataParser, BucketIpFilterRoundtrip) { + auto constexpr kText = R"pb( + mode: "Enabled" + allow_all_service_agent_access: true + allow_cross_org_vpcs: true + public_network_source { allowed_ip_cidr_ranges: "1.2.3.4/32" } + vpc_network_sources { + network: "projects/p/global/networks/n" + allowed_ip_cidr_ranges: "5.6.7.8/32" + } + )pb"; + google::storage::v2::Bucket::IpFilter start; + EXPECT_TRUE(google::protobuf::TextFormat::ParseFromString(kText, &start)); + storage::BucketIpFilter expected; + expected.mode = "Enabled"; + expected.allow_all_service_agent_access = true; + expected.allow_cross_org_vpcs = true; + expected.public_network_source = + storage::BucketIpFilterPublicNetworkSource{{"1.2.3.4/32"}}; + expected.vpc_network_sources = + absl::make_optional>( + {storage::BucketIpFilterVpcNetworkSource{ + "projects/p/global/networks/n", {"5.6.7.8/32"}}}); + auto const middle = FromProto(start); + EXPECT_EQ(middle, expected); + auto const end = ToProto(middle); + EXPECT_THAT(end, IsProtoEqual(start)); +} + } // namespace GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace storage_internal diff --git a/google/cloud/storage/internal/grpc/bucket_request_parser.cc b/google/cloud/storage/internal/grpc/bucket_request_parser.cc index 5ced4c9933d83..e55098356ed27 100644 --- a/google/cloud/storage/internal/grpc/bucket_request_parser.cc +++ b/google/cloud/storage/internal/grpc/bucket_request_parser.cc @@ -209,6 +209,45 @@ Status PatchIamConfig(Bucket& b, nlohmann::json const& i) { return Status{}; } +Status PatchIpFilter(Bucket& b, nlohmann::json const& p) { + if (p.is_null()) { + b.clear_ip_filter(); + return Status{}; + } + auto& ip_filter = *b.mutable_ip_filter(); + if (p.contains("mode")) { + ip_filter.set_mode(p.value("mode", "")); + } + if (p.contains("allowAllServiceAgentAccess")) { + ip_filter.set_allow_all_service_agent_access( + p.value("allowAllServiceAgentAccess", false)); + } + if (p.contains("allowCrossOrgVpcs")) { + ip_filter.set_allow_cross_org_vpcs(p.value("allowCrossOrgVpcs", false)); + } + if (p.contains("publicNetworkSource")) { + auto& pns = *ip_filter.mutable_public_network_source(); + auto const& public_network_source = p["publicNetworkSource"]; + if (public_network_source.contains("allowedIpCidrRanges")) { + for (auto const& v : public_network_source["allowedIpCidrRanges"]) { + pns.add_allowed_ip_cidr_ranges(v.get()); + } + } + } + if (p.contains("vpcNetworkSources")) { + for (auto const& v : p["vpcNetworkSources"]) { + auto& entry = *ip_filter.add_vpc_network_sources(); + entry.set_network(v.value("network", "")); + if (v.contains("allowedIpCidrRanges")) { + for (auto const& ip : v["allowedIpCidrRanges"]) { + entry.add_allowed_ip_cidr_ranges(ip.get()); + } + } + } + } + return Status{}; +} + Status PatchAutoclass(Bucket& bucket, nlohmann::json const& p) { if (p.is_null()) { bucket.clear_autoclass(); @@ -558,6 +597,7 @@ StatusOr ToProto( {"billing", "", PatchBilling}, {"retentionPolicy", "retention_policy", PatchRetentionPolicy}, {"iamConfiguration", "iam_config", PatchIamConfig}, + {"ipFilter", "ip_filter", PatchIpFilter}, {"autoclass", "", PatchAutoclass}, }; diff --git a/google/cloud/storage/internal/grpc/bucket_request_parser_test.cc b/google/cloud/storage/internal/grpc/bucket_request_parser_test.cc index a952af3970ff7..2deee727d8437 100644 --- a/google/cloud/storage/internal/grpc/bucket_request_parser_test.cc +++ b/google/cloud/storage/internal/grpc/bucket_request_parser_test.cc @@ -640,6 +640,61 @@ TEST(GrpcBucketRequestParser, PatchBucketRequestResetLabels) { EXPECT_THAT(*actual, IsProtoEqual(expected)); } +TEST(GrpcBucketRequestParser, PatchBucketRequestResetIpFilter) { + auto constexpr kTextProto = R"pb( + bucket { name: "projects/_/buckets/bucket-name" } + update_mask { paths: "ip_filter" } + )pb"; + google::storage::v2::UpdateBucketRequest expected; + ASSERT_TRUE(TextFormat::ParseFromString(kTextProto, &expected)); + + storage::internal::PatchBucketRequest req( + "bucket-name", storage::BucketMetadataPatchBuilder{}.ResetIpFilter()); + + auto actual = ToProto(req); + ASSERT_STATUS_OK(actual); + EXPECT_THAT(*actual, IsProtoEqual(expected)); +} + +TEST(GrpcBucketRequestParser, PatchBucketRequestIpFilter) { + auto constexpr kTextProto = R"pb( + bucket { + name: "projects/_/buckets/bucket-name" + ip_filter { + mode: "Enabled" + allow_all_service_agent_access: true + allow_cross_org_vpcs: true + public_network_source { allowed_ip_cidr_ranges: "1.2.3.4/32" } + vpc_network_sources { + network: "projects/p/global/networks/n" + allowed_ip_cidr_ranges: "5.6.7.8/32" + } + } + } + update_mask { paths: "ip_filter" } + )pb"; + google::storage::v2::UpdateBucketRequest expected; + ASSERT_TRUE(TextFormat::ParseFromString(kTextProto, &expected)); + + storage::BucketIpFilter ip_filter; + ip_filter.mode = "Enabled"; + ip_filter.allow_all_service_agent_access = true; + ip_filter.allow_cross_org_vpcs = true; + ip_filter.public_network_source = + storage::BucketIpFilterPublicNetworkSource{{"1.2.3.4/32"}}; + ip_filter.vpc_network_sources = + absl::make_optional>( + {storage::BucketIpFilterVpcNetworkSource{ + "projects/p/global/networks/n", {"5.6.7.8/32"}}}); + storage::internal::PatchBucketRequest req( + "bucket-name", + storage::BucketMetadataPatchBuilder().SetIpFilter(std::move(ip_filter))); + + auto actual = ToProto(req); + ASSERT_STATUS_OK(actual); + EXPECT_THAT(*actual, IsProtoEqual(expected)); +} + TEST(GrpcBucketRequestParser, UpdateBucketRequestAllOptions) { google::storage::v2::UpdateBucketRequest expected; ASSERT_TRUE(TextFormat::ParseFromString( diff --git a/google/cloud/storage/storage_client_unit_tests.bzl b/google/cloud/storage/storage_client_unit_tests.bzl index a9b757d1058fa..d7a6aaefb1ed1 100644 --- a/google/cloud/storage/storage_client_unit_tests.bzl +++ b/google/cloud/storage/storage_client_unit_tests.bzl @@ -21,6 +21,7 @@ storage_client_unit_tests = [ "bucket_access_control_test.cc", "bucket_cors_entry_test.cc", "bucket_iam_configuration_test.cc", + "bucket_ip_filter_test.cc", "bucket_metadata_test.cc", "bucket_object_retention_test.cc", "bucket_soft_delete_policy_test.cc", diff --git a/google/cloud/storage/tests/bucket_integration_test.cc b/google/cloud/storage/tests/bucket_integration_test.cc index fb0d6a8cfd094..fc7832650489f 100644 --- a/google/cloud/storage/tests/bucket_integration_test.cc +++ b/google/cloud/storage/tests/bucket_integration_test.cc @@ -559,6 +559,37 @@ TEST_F(BucketIntegrationTest, GetMetadataIfMetagenerationNotMatchFailure) { ASSERT_STATUS_OK(status); } +TEST_F(BucketIntegrationTest, PatchIpFilter) { + auto client = MakeIntegrationTestClient(); + auto bucket_name = MakeRandomBucketName(); + + auto insert_meta = client.CreateBucketForProject( + bucket_name, project_id_, BucketMetadata(), Projection("full")); + ASSERT_STATUS_OK(insert_meta); + ScheduleForDelete(*insert_meta); + + BucketIpFilter ip_filter; + ip_filter.mode = "Enabled"; + ip_filter.allow_all_service_agent_access = true; + ip_filter.allow_cross_org_vpcs = true; + ip_filter.public_network_source = + BucketIpFilterPublicNetworkSource{{"1.2.3.4/32"}}; + auto network_name = "projects/" + project_id_ + "/global/networks/default"; + ip_filter.vpc_network_sources = + absl::make_optional>( + {BucketIpFilterVpcNetworkSource{network_name, {"5.6.7.8/32"}}}); + + auto patched = client.PatchBucket( + bucket_name, BucketMetadataPatchBuilder().SetIpFilter(ip_filter)); + ASSERT_STATUS_OK(patched); + + ASSERT_TRUE(patched->has_ip_filter()); + EXPECT_EQ(patched->ip_filter(), ip_filter); + + auto status = client.DeleteBucket(bucket_name); + ASSERT_STATUS_OK(status); +} + TEST_F(BucketIntegrationTest, NativeIamCRUD) { std::string bucket_name = MakeRandomBucketName(); auto client = MakeBucketIntegrationTestClient();