diff --git a/google/cloud/storage/bucket_metadata.cc b/google/cloud/storage/bucket_metadata.cc index eb3fd410b9d65..8b55962a9b520 100644 --- a/google/cloud/storage/bucket_metadata.cc +++ b/google/cloud/storage/bucket_metadata.cc @@ -362,16 +362,16 @@ BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::ResetDefaultAcl() { BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::SetEncryption( BucketEncryption const& v) { internal::PatchBuilder builder; - builder.SetStringField("defaultKmsKeyName", v.default_kms_key_name); + if (v.default_kms_key_name.empty()) { + builder.RemoveField("defaultKmsKeyName"); + } else { + builder.SetStringField("defaultKmsKeyName", v.default_kms_key_name); + } auto add_config_patch = [&](char const* name, auto const& config) { if (config.restriction_mode.empty()) return; - builder.AddSubPatch( - name, internal::PatchBuilder() - .SetStringField("restrictionMode", config.restriction_mode) - .SetStringField("effectiveTime", - google::cloud::internal::FormatRfc3339( - config.effective_time))); + builder.AddSubPatch(name, internal::PatchBuilder().SetStringField( + "restrictionMode", config.restriction_mode)); }; add_config_patch("googleManagedEncryptionEnforcementConfig", v.google_managed_encryption_enforcement_config); diff --git a/google/cloud/storage/bucket_metadata_test.cc b/google/cloud/storage/bucket_metadata_test.cc index 3e8dcdc72a464..ca41bf256f1ce 100644 --- a/google/cloud/storage/bucket_metadata_test.cc +++ b/google/cloud/storage/bucket_metadata_test.cc @@ -526,23 +526,14 @@ TEST(BucketMetadataTest, ToJsonString) { EXPECT_EQ("FullyRestricted", encryption["googleManagedEncryptionEnforcementConfig"].value( "restrictionMode", "")); - EXPECT_EQ("2025-12-18T18:13:15Z", - encryption["googleManagedEncryptionEnforcementConfig"].value( - "effectiveTime", "")); EXPECT_EQ("NotRestricted", encryption["customerManagedEncryptionEnforcementConfig"].value( "restrictionMode", "")); - EXPECT_EQ("2025-12-18T18:13:15Z", - encryption["customerManagedEncryptionEnforcementConfig"].value( - "effectiveTime", "")); EXPECT_EQ("NotRestricted", encryption["customerSuppliedEncryptionEnforcementConfig"].value( "restrictionMode", "")); - EXPECT_EQ("2025-12-18T18:13:15Z", - encryption["customerSuppliedEncryptionEnforcementConfig"].value( - "effectiveTime", "")); // hierarchical_namespace() ASSERT_EQ(1, actual.count("hierarchicalNamespace")); diff --git a/google/cloud/storage/examples/CMakeLists.txt b/google/cloud/storage/examples/CMakeLists.txt index 38c6caf72c74e..3b69c216cd294 100644 --- a/google/cloud/storage/examples/CMakeLists.txt +++ b/google/cloud/storage/examples/CMakeLists.txt @@ -36,6 +36,7 @@ set(storage_examples storage_bucket_autoclass_samples.cc storage_bucket_cors_samples.cc storage_bucket_default_kms_key_samples.cc + storage_bucket_encryption_enforcement_samples.cc storage_bucket_iam_samples.cc storage_bucket_object_retention_samples.cc storage_bucket_requester_pays_samples.cc diff --git a/google/cloud/storage/examples/storage_bucket_encryption_enforcement_samples.cc b/google/cloud/storage/examples/storage_bucket_encryption_enforcement_samples.cc new file mode 100644 index 0000000000000..e98feb19269ab --- /dev/null +++ b/google/cloud/storage/examples/storage_bucket_encryption_enforcement_samples.cc @@ -0,0 +1,255 @@ +// Copyright 2026 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/client.h" +#include "google/cloud/storage/examples/storage_examples_common.h" +#include "google/cloud/internal/format_time_point.h" +#include "google/cloud/internal/getenv.h" +#include +#include +#include + +namespace { + +void GetBucketEncryptionEnforcementConfig( + google::cloud::storage::Client client, + std::vector const& argv) { + //! [get bucket encryption enforcement config] [START + //! storage_get_bucket_encryption_enforcement_config] + namespace gcs = ::google::cloud::storage; + using ::google::cloud::StatusOr; + [](gcs::Client client, std::string const& bucket_name) { + StatusOr metadata = + client.GetBucketMetadata(bucket_name); + if (!metadata) throw std::move(metadata).status(); + + std::cout << "Bucket Name: " << metadata->name() << "\n"; + + if (!metadata->has_encryption()) { + std::cout << " GMEK Enforcement: NOT SET (Default)\n" + << " CMEK Enforcement: NOT SET (Default)\n" + << " CSEK Enforcement: NOT SET (Default)\n"; + return; + } + + auto const& encryption = metadata->encryption(); + + auto format_config = [](auto const& config) { + if (config.restriction_mode.empty()) + return std::string("NOT SET (Default)"); + return "Mode: " + config.restriction_mode + ", Effective Time: " + + google::cloud::internal::FormatRfc3339(config.effective_time); + }; + + std::cout << " GMEK Enforcement: " + << format_config( + encryption.google_managed_encryption_enforcement_config) + << "\n" + << " CMEK Enforcement: " + << format_config( + encryption.customer_managed_encryption_enforcement_config) + << "\n" + << " CSEK Enforcement: " + << format_config( + encryption.customer_supplied_encryption_enforcement_config) + << "\n"; + } + //! [get bucket encryption enforcement config] [END + //! storage_get_bucket_encryption_enforcement_config] + (std::move(client), argv.at(0)); +} + +void SetBucketEncryptionEnforcementConfig( + google::cloud::storage::Client client, + std::vector const& argv) { + //! [set bucket encryption enforcement config] [START + //! storage_set_bucket_encryption_enforcement_config] + namespace gcs = ::google::cloud::storage; + using ::google::cloud::StatusOr; + [](gcs::Client client, std::string const& project_id, + std::string const& bucket_name) { + auto create_bucket = [&](std::string const& name, + gcs::BucketEncryption encryption) { + StatusOr bucket = client.CreateBucketForProject( + name, project_id, gcs::BucketMetadata().set_encryption(encryption)); + if (!bucket) throw std::move(bucket).status(); + return bucket; + }; + + // Example 1: Enforce GMEK Only + gcs::BucketEncryption gmek_encryption; + gmek_encryption.google_managed_encryption_enforcement_config + .restriction_mode = "NotRestricted"; + gmek_encryption.customer_managed_encryption_enforcement_config + .restriction_mode = "FullyRestricted"; + gmek_encryption.customer_supplied_encryption_enforcement_config + .restriction_mode = "FullyRestricted"; + std::cout << "Bucket " + << create_bucket("g-" + bucket_name, gmek_encryption)->name() + << " created with GMEK-only enforcement policy.\n"; + + // In GCS, a single project cannot create or delete buckets more often than + // once every two seconds. We pause to avoid rate limiting. + std::this_thread::sleep_for(std::chrono::seconds(2)); + + // Example 2: Enforce CMEK Only + gcs::BucketEncryption cmek_encryption; + cmek_encryption.google_managed_encryption_enforcement_config + .restriction_mode = "FullyRestricted"; + cmek_encryption.customer_managed_encryption_enforcement_config + .restriction_mode = "NotRestricted"; + cmek_encryption.customer_supplied_encryption_enforcement_config + .restriction_mode = "FullyRestricted"; + std::cout << "Bucket " + << create_bucket("c-" + bucket_name, cmek_encryption)->name() + << " created with CMEK-only enforcement policy.\n"; + + // In GCS, a single project cannot create or delete buckets more often than + // once every two seconds. We pause to avoid rate limiting. + std::this_thread::sleep_for(std::chrono::seconds(2)); + + // Example 3: Restrict CSEK (Ransomware Protection) + gcs::BucketEncryption csek_encryption; + csek_encryption.customer_supplied_encryption_enforcement_config + .restriction_mode = "FullyRestricted"; + std::cout << "Bucket " + << create_bucket("rc-" + bucket_name, csek_encryption)->name() + << " created with a policy to restrict CSEK.\n"; + } + //! [set bucket encryption enforcement config] [END + //! storage_set_bucket_encryption_enforcement_config] + (std::move(client), argv.at(0), argv.at(1)); +} + +void UpdateBucketEncryptionEnforcementConfig( + google::cloud::storage::Client client, + std::vector const& argv) { + //! [update bucket encryption enforcement config] [START + //! storage_update_bucket_encryption_enforcement_config] + namespace gcs = ::google::cloud::storage; + using ::google::cloud::StatusOr; + [](gcs::Client client, std::string const& bucket_name) { + StatusOr original = + client.GetBucketMetadata(bucket_name); + + gcs::BucketMetadata updated_metadata = *original; + gcs::BucketEncryption encryption; + if (original->has_encryption()) { + encryption = original->encryption(); + } + + // 1. Update a specific type (e.g., change GMEK to FullyRestricted) + encryption.google_managed_encryption_enforcement_config.restriction_mode = + "FullyRestricted"; + // 2. Remove a specific type (e.g., remove CMEK enforcement) + encryption.customer_managed_encryption_enforcement_config.restriction_mode = + "NotRestricted"; + // For the update, need to specify all three configs, so keeping this same + // as before + encryption.customer_supplied_encryption_enforcement_config + .restriction_mode = "FullyRestricted"; + + updated_metadata.set_encryption(encryption); + + StatusOr updated = + client.PatchBucket(bucket_name, *original, updated_metadata); + if (!updated) throw std::move(updated).status(); + + std::cout << "Encryption enforcement policy updated for bucket " + << updated->name() << "\n" + << "GMEK is now fully restricted, and CMEK enforcement has been " + "removed.\n"; + } + //! [update bucket encryption enforcement config] [END + //! storage_update_bucket_encryption_enforcement_config] + (std::move(client), argv.at(0)); +} + +void RunAll(std::vector const& argv) { + namespace examples = ::google::cloud::storage::examples; + namespace gcs = ::google::cloud::storage; + + if (!argv.empty()) throw examples::Usage{"auto"}; + examples::CheckEnvironmentVariablesAreSet({"GOOGLE_CLOUD_PROJECT"}); + auto const project_id = + google::cloud::internal::GetEnv("GOOGLE_CLOUD_PROJECT").value(); + auto generator = google::cloud::internal::DefaultPRNG(std::random_device{}()); + // Shorten bucket name to allow for prefixes without exceeding max length + auto const bucket_name = + examples::MakeRandomBucketName(generator).substr(0, 50); + auto client = gcs::Client(examples::CreateBucketOptions()); + + auto constexpr kBucketPeriod = std::chrono::seconds(2); + + // Clean up any potentially leaked buckets from a previous run before creating + // them. + (void)examples::RemoveBucketAndContents(client, "g-" + bucket_name); + if (!examples::UsingEmulator()) std::this_thread::sleep_for(kBucketPeriod); + (void)examples::RemoveBucketAndContents(client, "c-" + bucket_name); + if (!examples::UsingEmulator()) std::this_thread::sleep_for(kBucketPeriod); + (void)examples::RemoveBucketAndContents(client, "rc-" + bucket_name); + if (!examples::UsingEmulator()) std::this_thread::sleep_for(kBucketPeriod); + + std::cout << "\nRunning the SetBucketEncryptionEnforcementConfig() example" + << std::endl; + SetBucketEncryptionEnforcementConfig(client, {project_id, bucket_name}); + + std::cout + << "\nRunning the GetBucketEncryptionEnforcementConfig() example [1]" + << std::endl; + GetBucketEncryptionEnforcementConfig(client, {"c-" + bucket_name}); + + std::cout << "\nRunning the UpdateBucketEncryptionEnforcementConfig() example" + << std::endl; + UpdateBucketEncryptionEnforcementConfig(client, {"c-" + bucket_name}); + + std::cout + << "\nRunning the GetBucketEncryptionEnforcementConfig() example [2]" + << std::endl; + GetBucketEncryptionEnforcementConfig(client, {"c-" + bucket_name}); + + // In GCS a single project cannot create or delete buckets more often than + // once every two seconds. We will pause until that time before deleting the + // buckets. + auto pause = std::chrono::steady_clock::now() + kBucketPeriod; + if (!examples::UsingEmulator()) std::this_thread::sleep_until(pause); + + (void)examples::RemoveBucketAndContents(client, "g-" + bucket_name); + if (!examples::UsingEmulator()) std::this_thread::sleep_for(kBucketPeriod); + (void)examples::RemoveBucketAndContents(client, "c-" + bucket_name); + if (!examples::UsingEmulator()) std::this_thread::sleep_for(kBucketPeriod); + (void)examples::RemoveBucketAndContents(client, "rc-" + bucket_name); +} + +} // namespace + +int main(int argc, char* argv[]) { + namespace examples = ::google::cloud::storage::examples; + auto make_entry = [](std::string const& name, + std::vector arg_names, + examples::ClientCommand const& cmd) { + return examples::CreateCommandEntry(name, std::move(arg_names), cmd); + }; + examples::Example example({ + make_entry("get-bucket-encryption-enforcement-config", {""}, + GetBucketEncryptionEnforcementConfig), + make_entry("set-bucket-encryption-enforcement-config", + {"", ""}, + SetBucketEncryptionEnforcementConfig), + make_entry("update-bucket-encryption-enforcement-config", + {""}, UpdateBucketEncryptionEnforcementConfig), + {"auto", RunAll}, + }); + return example.Run(argc, argv); +} diff --git a/google/cloud/storage/examples/storage_examples.bzl b/google/cloud/storage/examples/storage_examples.bzl index 5bdd0f414b534..a65855c565fdb 100644 --- a/google/cloud/storage/examples/storage_examples.bzl +++ b/google/cloud/storage/examples/storage_examples.bzl @@ -21,6 +21,7 @@ storage_examples = [ "storage_bucket_autoclass_samples.cc", "storage_bucket_cors_samples.cc", "storage_bucket_default_kms_key_samples.cc", + "storage_bucket_encryption_enforcement_samples.cc", "storage_bucket_iam_samples.cc", "storage_bucket_object_retention_samples.cc", "storage_bucket_requester_pays_samples.cc", diff --git a/google/cloud/storage/internal/bucket_metadata_parser.cc b/google/cloud/storage/internal/bucket_metadata_parser.cc index 1c138bb0c3d45..806561c5b7097 100644 --- a/google/cloud/storage/internal/bucket_metadata_parser.cc +++ b/google/cloud/storage/internal/bucket_metadata_parser.cc @@ -412,8 +412,6 @@ void ToJsonEncryption(nlohmann::json& json, BucketMetadata const& meta) { if (config_source.restriction_mode.empty()) return; nlohmann::json config; config["restrictionMode"] = config_source.restriction_mode; - config["effectiveTime"] = - google::cloud::internal::FormatRfc3339(config_source.effective_time); e[name] = std::move(config); }; to_json_config( diff --git a/google/cloud/storage/internal/grpc/bucket_request_parser.cc b/google/cloud/storage/internal/grpc/bucket_request_parser.cc index 703125edd9c02..67e334e49c0ea 100644 --- a/google/cloud/storage/internal/grpc/bucket_request_parser.cc +++ b/google/cloud/storage/internal/grpc/bucket_request_parser.cc @@ -163,18 +163,6 @@ Status PatchLogging(Bucket& b, nlohmann::json const& l) { return Status{}; } -google::protobuf::Timestamp ToProtoTimestamp( - std::chrono::system_clock::time_point tp) { - auto duration = tp.time_since_epoch(); - auto seconds = std::chrono::duration_cast(duration); - auto nanos = - std::chrono::duration_cast(duration - seconds); - google::protobuf::Timestamp ts; - ts.set_seconds(seconds.count()); - ts.set_nanos(static_cast(nanos.count())); - return ts; -} - Status PatchEncryption(Bucket& b, nlohmann::json const& e) { if (e.is_null()) { b.clear_encryption(); @@ -190,13 +178,6 @@ Status PatchEncryption(Bucket& b, nlohmann::json const& e) { if (c.contains("restrictionMode")) { mutable_config->set_restriction_mode(c.value("restrictionMode", "")); } - if (c.contains("effectiveTime")) { - auto ts = - google::cloud::internal::ParseRfc3339(c.value("effectiveTime", "")); - if (ts) { - *mutable_config->mutable_effective_time() = ToProtoTimestamp(*ts); - } - } } }; @@ -336,7 +317,6 @@ void UpdateEncryption(Bucket& bucket, storage::BucketMetadata const& metadata) { auto update_config = [&](auto const& source, auto* dest) { if (source.restriction_mode.empty()) return; dest->set_restriction_mode(source.restriction_mode); - *dest->mutable_effective_time() = ToProtoTimestamp(source.effective_time); }; update_config( 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 68054410cfc1f..329f6c3f47358 100644 --- a/google/cloud/storage/internal/grpc/bucket_request_parser_test.cc +++ b/google/cloud/storage/internal/grpc/bucket_request_parser_test.cc @@ -461,15 +461,12 @@ TEST(GrpcBucketRequestParser, PatchBucketRequestAllOptions) { default_kms_key: "test-only-kms-key" google_managed_encryption_enforcement_config { restriction_mode: "FullyRestricted" - effective_time { seconds: 1766175572 } } customer_managed_encryption_enforcement_config { restriction_mode: "NotRestricted" - effective_time { seconds: 1766175695 } } customer_supplied_encryption_enforcement_config { restriction_mode: "FullyRestricted" - effective_time { seconds: 1766175739 } } } autoclass { enabled: true } @@ -745,15 +742,12 @@ TEST(GrpcBucketRequestParser, UpdateBucketRequestAllOptions) { default_kms_key: "test-only-kms-key" google_managed_encryption_enforcement_config { restriction_mode: "FullyRestricted" - effective_time { seconds: 1766176065 } } customer_managed_encryption_enforcement_config { restriction_mode: "NotRestricted" - effective_time { seconds: 1766176105 } } customer_supplied_encryption_enforcement_config { restriction_mode: "FullyRestricted" - effective_time { seconds: 1766176151 } } } autoclass { enabled: true }