Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,23 @@
ENV["GCLOUD_TEST_STORAGE_KMS_KEY_2"] ||
"projects/#{storage.project_id}/locations/#{bucket_location}/keyRings/ruby-test/cryptoKeys/ruby-test-key-2"
}

let(:customer_managed_config) do
{ restriction_mode: "NotRestricted" }
end

let(:customer_supplied_config) do
{ restriction_mode: "FullyRestricted" }
end

let(:google_managed_config) do
{ restriction_mode: "FullyRestricted" }
end

let :bucket do
b = safe_gcs_execute { storage.create_bucket(bucket_name, location: bucket_location) }
b = safe_gcs_execute { storage.bucket(bucket_name) || storage.create_bucket(bucket_name, location: bucket_location) }
b.default_kms_key = kms_key
b.customer_managed_encryption_enforcement_config = customer_managed_config
b
end

Expand Down Expand Up @@ -71,4 +85,53 @@
_(bucket.default_kms_key).must_be :nil?
end
end

describe "Encryption Enforcement Config" do
let(:google_managed_config_complete) do
{google_managed_encryption_enforcement_config: { restriction_mode: "FullyRestricted" } }
end

it "knows its encryption enforcement config" do
_(bucket.customer_managed_encryption_enforcement_config).wont_be :nil?
_(bucket.customer_managed_encryption_enforcement_config.restriction_mode).must_equal "NotRestricted"
bucket.reload!
_(bucket.customer_managed_encryption_enforcement_config).wont_be :nil?
_(bucket.customer_managed_encryption_enforcement_config.restriction_mode).must_equal "NotRestricted"
end

it "updates encryption enforcement configs" do
_(bucket.customer_supplied_encryption_enforcement_config).must_be :nil?

bucket.customer_supplied_encryption_enforcement_config = customer_supplied_config
_(bucket.customer_supplied_encryption_enforcement_config.restriction_mode).must_equal "FullyRestricted"
bucket.update_bucket_encryption_enforcement_config google_managed_config_complete
_(bucket.google_managed_encryption_enforcement_config.restriction_mode).must_equal "FullyRestricted"

bucket.reload!
_(bucket.customer_supplied_encryption_enforcement_config.restriction_mode).must_equal "FullyRestricted"
_(bucket.google_managed_encryption_enforcement_config.restriction_mode).must_equal "FullyRestricted"
end

it "deletes all encryption enforcement configs" do
# For the update, need to specify all three configs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this statement is correct

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got reference from Java pr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nidhiii-27 Is this correct statement?

bucket.update do |b|
b.customer_supplied_encryption_enforcement_config = customer_supplied_config
b.google_managed_encryption_enforcement_config = google_managed_config
end
_(bucket.customer_managed_encryption_enforcement_config).wont_be :nil?
_(bucket.customer_supplied_encryption_enforcement_config).wont_be :nil?
_(bucket.google_managed_encryption_enforcement_config).wont_be :nil?

bucket.update do |b|
b.customer_managed_encryption_enforcement_config = nil
b.customer_supplied_encryption_enforcement_config = nil
b.google_managed_encryption_enforcement_config = nil
end
# Removed all encryption enforcement configs without removing default_kms_key
_(bucket.customer_managed_encryption_enforcement_config).must_be :nil?
_(bucket.customer_supplied_encryption_enforcement_config).must_be :nil?
_(bucket.google_managed_encryption_enforcement_config).must_be :nil?
_(bucket.default_kms_key).must_equal kms_key
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
let(:bucket_location) { "us-central1" }

let :bucket do
safe_gcs_execute {storage.create_bucket bucket_name, location: bucket_location }
safe_gcs_execute { storage.bucket(bucket_name) || storage.create_bucket(bucket_name, location: bucket_location) }
end

let(:file_path) { "acceptance/data/abc.txt" }
Expand Down
231 changes: 229 additions & 2 deletions google-cloud-storage/lib/google/cloud/storage/bucket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,228 @@ def default_kms_key= new_default_kms_key
default_kms_key_name: new_default_kms_key
patch_gapi! :encryption
end
##
# The bucket's encryption configuration for customer-managed encryption keys.
# This configuration defines the
# default encryption behavior for the bucket and its files, and it can be used to enforce encryption requirements for the bucket.
# For more information, see [Bucket encryption](https://docs.cloud.google.com/storage/docs/encryption/).
# @return [Google::Apis::StorageV1::Bucket::Encryption::CustomerManagedEncryptionEnforcementConfig, nil] The bucket's encryption configuration, or `nil` if no encryption configuration has been set.
# @example
# require "google/cloud/storage"
# #
# storage = Google::Cloud::Storage.new
# bucket = storage.bucket "my-bucket"
# bucket.customer_managed_encryption_enforcement_config
# ==> #<Google::Apis::StorageV1::Bucket::Encryption::CustomerManagedEncryptionEnforcementConfig:0x00007f3b1c102e90 @restriction_mode="NotRestricted">
# The value for `restriction_mode` can be either "NotRestricted" or "FullyRestricted"

def customer_managed_encryption_enforcement_config
@gapi.encryption&.customer_managed_encryption_enforcement_config
end
##
# Sets the customer-managed encryption enforcement configuration for the bucket.
#
# @param new_customer_managed_encryption_enforcement_config [Hash, nil]
# The configuration hash for encryption enforcement.
# * `:restriction_mode` (String) - Can be "NotRestricted" or "FullyRestricted".
# Pass `nil` to clear the current configuration.
#
# @example Enforcing Customer-Managed Encryption
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
# bucket = storage.bucket "my-bucket"
#
# # Set restriction mode to FullyRestricted
# new_config = { restriction_mode: "FullyRestricted" }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating this. We should also add example of how we can set it using the request object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

# bucket.customer_managed_encryption_enforcement_config = new_config
#
# @example Setting via Request Object (Google API Client)
# require "google/apis/storage_v1"
#
# enforcement_config = { restriction_mode: "FullyRestricted" }
#
# request_obj = Google::Apis::StorageV1::Bucket::Encryption.new(
# customer_managed_encryption_enforcement_config: enforcement_config
# )
# bucket.customer_managed_encryption_enforcement_config = request_obj
#
# @return [Hash, Google::Apis::StorageV1::Bucket::Encryption] The updated configuration.
# @raise [Google::Cloud::Error] If the update fails due to permissions or invalid arguments.
def customer_managed_encryption_enforcement_config= new_customer_managed_encryption_enforcement_config
@gapi.encryption ||= API::Bucket::Encryption.new
@gapi.encryption.customer_managed_encryption_enforcement_config =
new_customer_managed_encryption_enforcement_config || {}
patch_gapi! :encryption
end

# Updates the bucket's encryption enforcement configuration.
#
# This method applies a patch to the bucket's encryption settings using the
# provided configuration.
#
# @param incoming_config [Hash, Google::Apis::StorageV1::Bucket::Encryption]
# The encryption configuration to apply. If a Hash is provided, it should
# contain keys corresponding to the encryption enforcement types.
#
# @example Updating to Google-Managed Encryption
# storage = Google::Cloud::Storage.new
# bucket = storage.bucket "my-bucket"
#
# new_config = {
# google_managed_encryption_enforcement_config: { restriction_mode: "NotRestricted" }
# }
#
# bucket.update_bucket_encryption_enforcement_config new_config
#
# @example Passing a Request Object
# require "google/apis/storage_v1"
# config_obj = Google::Apis::StorageV1::Bucket::Encryption.new(
# google_managed_encryption_enforcement_config: { restriction_mode: "NotRestricted" }
# )
# bucket.update_bucket_encryption_enforcement_config config_obj
# @raise [ArgumentError] If the config is empty, contains invalid keys, or is the wrong type.
def update_bucket_encryption_enforcement_config incoming_config
allowed_keys = [
:google_managed_encryption_enforcement_config,
:customer_managed_encryption_enforcement_config,
:customer_supplied_encryption_enforcement_config
]

if incoming_config.is_a? Hash
input_keys = incoming_config.keys
raise ArgumentError, "Config cannot be empty" if input_keys.empty?

extra_keys = input_keys - allowed_keys
unless extra_keys.empty?
raise ArgumentError, "Invalid config detected: #{extra_keys.join(', ')}. " \
"Only #{allowed_keys.join(', ')} are allowed."
end

elsif incoming_config.is_a? Google::Apis::StorageV1::Bucket::Encryption
# For objects, ensure at least one of the allowed enforcement configs is present
has_any_config = allowed_keys.any? { |key| !incoming_config.send(key).nil? }
binding.pry
raise ArgumentError, "Encryption request object must have at least one enforcement config set" unless has_any_config

else
raise ArgumentError, "incoming_config must be a Hash or Google::Apis::StorageV1::Bucket::Encryption"
end

patch_gapi! :encryption, bucket_encryption_config: incoming_config
end

##
# The bucket's encryption configuration for customer-supplied encryption keys.
# For more information, see [Bucket encryption](https://docs.cloud.google.com/storage/docs/encryption/).
# @return [Google::Apis::StorageV1::Bucket::Encryption::CustomerSuppliedEncryptionEnforcementConfig, nil]
# The bucket's encryption configuration, or `nil` if no encryption configuration has been set.
# @example
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
# bucket = storage.bucket "my-bucket"
#
# bucket.customer_supplied_encryption_enforcement_config
# ==> #<Google::Apis::StorageV1::Bucket::Encryption::CustomerSuppliedEncryptionEnforcementConfig:0x00007f3b1c102e90 @restriction_mode="NotRestricted">
# The value for `restriction_mode` can be either "NotRestricted" or "FullyRestricted".

def customer_supplied_encryption_enforcement_config
@gapi.encryption&.customer_supplied_encryption_enforcement_config
end

##
# Sets the bucket's encryption configuration for customer-supplied encryption that will be used to protect files.
# @param new_customer_supplied_encryption_enforcement_config [Hash, nil]
# The configuration hash for encryption enforcement.
# * `:restriction_mode` (String) - Can be "NotRestricted" or "FullyRestricted".
# Pass `nil` to clear the current configuration.
# @example
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
# bucket = storage.bucket "my-bucket"
# new_config = { restriction_mode: "FullyRestricted" }
# bucket.customer_supplied_encryption_enforcement_config = new_config
#
# @example Setting via Request Object (Google API Client)
# require "google/apis/storage_v1"
#
# enforcement_config = { restriction_mode: "FullyRestricted" }
#
# request_obj = Google::Apis::StorageV1::Bucket::Encryption.new(
# customer_supplied_encryption_enforcement_config: enforcement_config
# )
# bucket.customer_supplied_encryption_enforcement_config = request_obj
#
# @return [Hash, Google::Apis::StorageV1::Bucket::Encryption] The updated configuration.
# @raise [Google::Cloud::Error] If the update fails due to permissions or invalid arguments.

def customer_supplied_encryption_enforcement_config= new_customer_supplied_encryption_enforcement_config
@gapi.encryption ||= API::Bucket::Encryption.new
@gapi.encryption.customer_supplied_encryption_enforcement_config =
new_customer_supplied_encryption_enforcement_config || {}
patch_gapi! :encryption
end

##
# The bucket's encryption configuration for google-managed encryption keys.
# This configuration defines the
# default encryption behavior for the bucket and its files, and it can be used to enforce encryption
# requirements for the bucket.
# For more information, see [Bucket encryption](https://docs.cloud.google.com/storage/docs/encryption/).
# @return [Google::Apis::StorageV1::Bucket::Encryption::GoogleManagedEncryptionEnforcementConfig, nil]
# The bucket's encryption configuration, or `nil` if no encryption configuration has been set.
# @example
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
# bucket = storage.bucket "my-bucket"
# bucket.google_managed_encryption_enforcement_config
# ==> #<Google::Apis::StorageV1::Bucket::Encryption::GoogleManagedEncryptionEnforcementConfig:0x00007f3b1c102e90 @restriction_mode="NotRestricted">
# The value for `restriction_mode` can be either "NotRestricted" or "FullyRestricted".

def google_managed_encryption_enforcement_config
@gapi.encryption&.google_managed_encryption_enforcement_config
end

##
# Sets the google-managed encryption enforcement configuration for the bucket.
#
# @param new_google_managed_encryption_enforcement_config [Hash, nil]
# The configuration hash for encryption enforcement.
# * `:restriction_mode` (String) - Can be "NotRestricted" or "FullyRestricted".
# Pass `nil` to clear the current configuration.
#
# @example Enforcing Customer-Managed Encryption
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
# bucket = storage.bucket "my-bucket"
#
# # Set restriction mode to FullyRestricted
# new_config = { restriction_mode: "FullyRestricted" }
# bucket.new_google_managed_encryption_enforcement_config = new_config
#
# @example Setting via Request Object (Google API Client)
# require "google/apis/storage_v1"
#
# enforcement_config = { restriction_mode: "FullyRestricted" }
#
# request_obj = Google::Apis::StorageV1::Bucket::Encryption.new(
# google_managed_encryption_enforcement_config: enforcement_config
# )
# bucket.google_managed_encryption_enforcement_config = request_obj
#
# @return [Hash, Google::Apis::StorageV1::Bucket::Encryption] The updated configuration.
# @raise [Google::Cloud::Error] If the update fails due to permissions or invalid arguments.

def google_managed_encryption_enforcement_config= new_google_managed_encryption_enforcement_config
@gapi.encryption ||= API::Bucket::Encryption.new
@gapi.encryption.google_managed_encryption_enforcement_config =
new_google_managed_encryption_enforcement_config || {}
patch_gapi! :encryption
end

##
# The period of time (in seconds) that files in the bucket must be
Expand Down Expand Up @@ -3252,13 +3474,18 @@ def ensure_gapi!

def patch_gapi! attributes,
if_metageneration_match: nil,
if_metageneration_not_match: nil
if_metageneration_not_match: nil,
bucket_encryption_config: nil
Copy link
Copy Markdown
Contributor

@bajajneha27 bajajneha27 Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we introducing new parameter here? Why can't we combine this in existing parameter attributes ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using the standard bucket.update or @gapi.send(:encryption) patterns, the Ruby SDK was capturing the entire existing encryption state. This included server-generated fields like effectiveTime. When this "polluted" object was sent back via the PATCH API, Google rejected it with an InvalidArgumentError because those fields were not in the expected format.

The Fix

We implemented the update_bucket_encryption_enforcement_configmethod to send only the changed data to the patch request.

How it Works

  • Isolation: Instead of modifying the existing bucket object, the method initializes a fresh, empty Google::Apis::StorageV1::Bucket::Encryption instance.
  • Specific Assignment: It uses dynamic dispatch (public_send) to set only the specific enforcement type requested (e.g., customer_supplied).

update_bucket_encryption_enforcement_config sends the correct patch json in bucket_encryption_config


Comparison of JSON Payloads

Wrong Request (Polluted)

{
  "encryption": {
    "customerManagedEncryptionEnforcementConfig": {
      "restrictionMode": "NotRestricted"
    },
    "customerSuppliedEncryptionEnforcementConfig": {
      "restrictionMode": "NotRestricted"
    },
    "googleManagedEncryptionEnforcementConfig": {
      "effectiveTime": "2026-03-25T11:40:17.182+00:00",
      "restrictionMode": "FullyRestricted"
    }
  }
}

Correct request

{
  "encryption": {
    "customerManagedEncryptionEnforcementConfig": {
      "restrictionMode": "NotRestricted"
    }
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajajneha27 can you please provide your views on this approach

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User can still pass effectiveTime if they want to, in the new attribute.
I think we should not add a separate parameter just for this. We should make use of existing attribute

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides, if user wants to update other attributes AND bucket_encryption_configs, that won't work
https://github.com/googleapis/google-cloud-ruby/pull/33452/changes#diff-1c445f12b16180bcbdde4cab77c9b5ca55acc3fff5a45c1dd7d2bb4abb64bf76R3410-R3414

Copy link
Copy Markdown
Contributor Author

@shubhangi-google shubhangi-google Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @bajajneha27 the existing patch method is sending all the encryption attribute (the existing one and the updated patch together)
as shown in this example #33452 (comment)
here the time format which we are receiving is UTC format from server and API is expecting RFC 3339 format as per encryption.googleManagedEncryptionEnforcementConfig.effectiveTime hence we are getting invalid argument error in response
as a solution I am creating a new method to update the encryption enforcement config which will send only the updated patch
this method is only for handling encryption_config so concern for other attributes will not be valid in this case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow. Why would you get Invalid Argument error if you're not passing effective_time at all ?
What happens if I want to pass bucket_encryption_config and other attributes as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example we want to update customerManagedEncryptionEnforcementConfig restriction mode to "FullyRestricted"
When using the standard bucket.update the @gapi.send(:encryption) patterns, the Ruby SDK is capturing the entire existing encryption state and updating the customerManagedEncryptionEnforcementConfig restriction mode in it and send the updated encryption hash . which includes effective time we are receiving from API which is in UTC format
example:

{
  "encryption": {
    "customerManagedEncryptionEnforcementConfig": {
      "restrictionMode": "NotRestricted"
    },
    "customerSuppliedEncryptionEnforcementConfig": {
      "restrictionMode": "NotRestricted"
    },
    "googleManagedEncryptionEnforcementConfig": {
      "effectiveTime": "2026-03-25T11:40:17.182+00:00",
      "restrictionMode": "FullyRestricted"
    }
  }
}

when this hash is submitted to server its returning error
Google::Cloud::InvalidArgumentError: invalid: Invalid argument. because server is expecting the effectiveTime in 1985-04-12T23:20:50.52Z format and not in UTC format

attributes = Array(attributes)
attributes.flatten!
return if attributes.empty?
ensure_service!
patch_args = attributes.to_h do |attr|
[attr, @gapi.send(attr)]
if bucket_encryption_config
[attr, bucket_encryption_config]
else
[attr, @gapi.send(attr)]
end
end
patch_gapi = API::Bucket.new(**patch_args)
@gapi = service.patch_bucket name,
Expand Down
4 changes: 4 additions & 0 deletions google-cloud-storage/samples/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ group :test do
gem "minitest-hooks", "~> 1.5"
gem "rake"
end
# The following gems have been removed from ruby core and are required for testing.
gem "ostruct"
gem "cgi"
gem "irb"
Loading
Loading