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
113 changes: 113 additions & 0 deletions google-cloud-storage/acceptance/storage/bucket_ip_filter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# 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.

require_relative "../storage_helper"
describe Google::Cloud::Storage::Bucket, :storage do
let(:ip_filter_disabled) do
{
mode: "Disabled",
public_network_source: {
allowed_ip_cidr_ranges: ["0.0.0.0/0", "::/0"]
}
}
end

let(:ip_filter_disabled_update) do
{
mode: "Disabled",
public_network_source: {
allowed_ip_cidr_ranges: ["8.8.8.8/32"]
},
vpc_network_sources: [
{
network: "projects/#{storage.project}/global/networks/default",
allowed_ip_cidr_ranges: ["0.0.0.0/0"]
}
]
}
end
let(:bucket_name) { "#{$bucket_names.first}-ip-filter" }
let :bucket do
storage.bucket(bucket_name, projection: "full") ||
storage.create_bucket(bucket_name, ip_filter: ip_filter_disabled)
end

after(:all) do
safe_gcs_execute { bucket.delete }
end

it "returns a 400 error when provided an invalid IP CIDR range" do
invalid_ip_filter = {
mode: "Enabled",
public_network_source: {
allowed_ip_cidr_ranges: ["invalid-ip-range"]
}
}

# Verify that creating a bucket with an invalid CIDR raises an error
err = expect {
storage.create_bucket "#{bucket_name}-invalid", ip_filter: invalid_ip_filter
}.must_raise Google::Cloud::InvalidArgumentError

_(err.message).must_match /invalid/i
end

it "creates, gets, updates, and deletes a bucket with ip_filter" do


_(bucket.ip_filter).wont_be_nil
_(bucket.ip_filter.mode).must_equal "Disabled"
_(bucket.ip_filter.public_network_source.allowed_ip_cidr_ranges).must_equal ["0.0.0.0/0", "::/0"]

# Get the bucket and verify ip_filter
bucket = storage.bucket bucket_name, projection: "full"
_(bucket.ip_filter).wont_be_nil
_(bucket.ip_filter.mode).must_equal "Disabled"
_(bucket.ip_filter.public_network_source.allowed_ip_cidr_ranges).must_equal ["0.0.0.0/0", "::/0"]

# list_bucket_ip_filters
found = false
storage.buckets(projection: "full").all do |b|
found = true if b.name == bucket_name
end
_(found).must_equal true

# Update the ip_filter
safe_gcs_execute do
bucket.update do |b|
b.ip_filter = ip_filter_disabled_update
end
end

_(bucket.ip_filter.mode).must_equal "Disabled"
_(bucket.ip_filter.public_network_source.allowed_ip_cidr_ranges).must_equal ["8.8.8.8/32"]
_(bucket.ip_filter.vpc_network_sources.first.network).must_equal "projects/#{storage.project}/global/networks/default"
_(bucket.ip_filter.vpc_network_sources.first.allowed_ip_cidr_ranges).must_equal ["0.0.0.0/0"]

# Disable ip_filter and clear network sources
safe_gcs_execute do
bucket.ip_filter = {
mode: "Disabled",
public_network_source: {
allowed_ip_cidr_ranges: []
},
vpc_network_sources: []
}
end

_(bucket.ip_filter.public_network_source.allowed_ip_cidr_ranges).must_be_nil
_(bucket.ip_filter.vpc_network_sources).must_be_nil

end
end
83 changes: 81 additions & 2 deletions google-cloud-storage/lib/google/cloud/storage/bucket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,80 @@ def delete if_metageneration_match: nil, if_metageneration_not_match: nil
user_project: user_project
end

##
# The bucket's IP filter configuration.
# This value can be modified by calling {#ip_filter=}.
#
# @note The IP filter metadata is not returned by the API by default.
# You must retrieve the bucket with the `projection: :full` (or `"full"`)
# option to access this configuration. See {Project#bucket}.
#
# @return [Google::Apis::StorageV1::Bucket::IpFilter, nil] The bucket's IP filter configuration,
# or `nil` if not configured.
#
# @example
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
#
# bucket = storage.bucket "my-bucket", projection: "full"
# ip_filter = bucket.ip_filter
# if ip_filter
# puts "Mode: #{ip_filter.mode}"
# puts "Public CIDR: #{ip_filter.public_network_source&.allowed_ip_cidr_ranges}"
# end
#
def ip_filter
@gapi.ip_filter
end

##
# Sets the value for IP filter in the bucket. This value can
# be queried by calling {#ip_filter}.
#
# @param [Google::Apis::StorageV1::Bucket::IpFilter, Hash] new_ip_filter The bucket's new IP filter.
# Acceptable Hash structure:
# * `:mode` (String) - The mode of the IP filter. Acceptable values are: "Disabled", "Enabled"
# * `:public_network_source` (Hash) - The public network source configuration:
# * `:allowed_ip_cidr_ranges` (Array<String>) - Array of IP CIDR ranges allowed for public access.
# * `:vpc_network_sources` (Array<Hash>) - The VPC network sources configuration:
# * `:network` (String) - The VPC network resource path, e.g. "projects/PROJECT_ID/global/networks/NETWORK_NAME".
# * `:allowed_ip_cidr_ranges` (Array<String>) - Array of IP CIDR ranges allowed for VPC access.
# * `:allow_cross_org_vpcs` (Boolean) - Whether to allow cross-org VPC access.
# * `:allow_all_service_agent_access` (Boolean) - Whether to allow all service agent access.
#
# @example Enable IP filter with Hash:
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
#
# bucket = storage.bucket "my-bucket", projection: "full"
# bucket.ip_filter = {
# mode: "Enabled",
# allow_all_service_agent_access: true,
# public_network_source: {
# allowed_ip_cidr_ranges: ["0.0.0.0/0", "::/0"]
# }
# }
#
# @example Clear/delete IP filter:
# require "google/cloud/storage"
#
# storage = Google::Cloud::Storage.new
#
# bucket = storage.bucket "my-bucket", projection: "full"
# bucket.ip_filter = {
# mode: "Disabled",
# public_network_source: {
# allowed_ip_cidr_ranges: []
# }
# }
#
def ip_filter= new_ip_filter
@gapi.ip_filter = new_ip_filter || {}
patch_gapi! :ip_filter
end

##
# Retrieves a list of files matching the criteria.
#
Expand Down Expand Up @@ -3337,9 +3411,14 @@ def create_notification topic, custom_attrs: nil, event_types: nil,
##
# Reloads the bucket with current data from the Storage service.
#
def reload!
# @param [String] projection Set of properties to return. Accepted values
# are `noAcl` and `full`. The default value is `noAcl`. If set to `full`,
# the bucket will include additional metadata, such as ACL policies and
# IP filter settings.
#
def reload! projection: nil
ensure_service!
@gapi = service.get_bucket name, user_project: user_project
@gapi = service.get_bucket name, user_project: user_project, projection: projection
# If NotFound then lazy will never be unset
@lazy = nil
self
Expand Down
11 changes: 8 additions & 3 deletions google-cloud-storage/lib/google/cloud/storage/bucket/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ def next
ensure_service!
gapi = @service.list_buckets prefix: @prefix, token: @token,
max: @max, user_project: @user_project,
soft_deleted: @soft_deleted
soft_deleted: @soft_deleted,
return_partial_success: !@unreachable.nil?,
projection: @projection
Bucket::List.from_gapi gapi, @service, @prefix, @max,
user_project: @user_project
user_project: @user_project, soft_deleted: @soft_deleted,
return_partial_success: !@unreachable.nil?,
projection: @projection
end

##
Expand Down Expand Up @@ -155,7 +159,7 @@ def all request_limit: nil, &block
# @private New Bucket::List from a Google API Client
# Google::Apis::StorageV1::Buckets object.
def self.from_gapi gapi_list, service, prefix = nil, max = nil,
user_project: nil, soft_deleted: nil, return_partial_success: nil
user_project: nil, soft_deleted: nil, return_partial_success: nil, projection: nil
buckets = new(Array(gapi_list.items).map do |gapi_object|
Bucket.from_gapi gapi_object, service, user_project: user_project
end)
Expand All @@ -165,6 +169,7 @@ def self.from_gapi gapi_list, service, prefix = nil, max = nil,
buckets.instance_variable_set :@max, max
buckets.instance_variable_set :@user_project, user_project
buckets.instance_variable_set :@soft_deleted, soft_deleted
buckets.instance_variable_set :@projection, projection
buckets.instance_variable_set :@unreachable, Array(gapi_list.unreachable) if return_partial_success
buckets
end
Expand Down
52 changes: 42 additions & 10 deletions google-cloud-storage/lib/google/cloud/storage/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,12 @@ def add_custom_header header_name, header_value
#
# See also {Bucket#requester_pays=} and {Bucket#requester_pays}.
# @param [Boolean] return_partial_success
# If true, the response will contain a list of unreachable buckets.
# If false, ListBuckets will throw an error if there are any unreachable buckets.
# If true, the response will contain a list of unreachable buckets.
# If false, ListBuckets will throw an error if there are any unreachable buckets.
# @param [String] projection Set of properties to return. Accepted values
# are `noAcl` and `full`. The default value is `noAcl`. If set to `full`,
# the bucket will include additional metadata, such as ACL policies and
# IP filter settings.
#
# @return [Array<Google::Cloud::Storage::Bucket>] (See
# {Google::Cloud::Storage::Bucket::List})
Expand Down Expand Up @@ -214,11 +218,11 @@ def add_custom_header header_name, header_value
# puts unreachable_bucket_name
# end
#
def buckets prefix: nil, token: nil, max: nil, user_project: nil, soft_deleted: nil, return_partial_success: nil
def buckets prefix: nil, token: nil, max: nil, user_project: nil, soft_deleted: nil, return_partial_success: nil, projection: nil
gapi = service.list_buckets \
prefix: prefix, token: token, max: max, user_project: user_project, soft_deleted: soft_deleted, return_partial_success: return_partial_success
prefix: prefix, token: token, max: max, user_project: user_project, soft_deleted: soft_deleted, return_partial_success: return_partial_success, projection: projection
Bucket::List.from_gapi \
gapi, service, prefix, max, user_project: user_project, soft_deleted: soft_deleted, return_partial_success: return_partial_success
gapi, service, prefix, max, user_project: user_project, soft_deleted: soft_deleted, return_partial_success: return_partial_success, projection: projection

end
alias find_buckets buckets
Expand Down Expand Up @@ -249,6 +253,10 @@ def buckets prefix: nil, token: nil, max: nil, user_project: nil, soft_deleted:
# on whether the bucket's current metageneration matches the given value.
# @param [Boolean] soft_deleted If true, returns the soft-deleted bucket.
# This parameter is required if generation is specified.
# @param [String] projection Set of properties to return. Accepted values
# are `noAcl` and `full`. The default value is `noAcl`. If set to `full`,
# the bucket will include additional metadata, such as ACL policies and
# IP filter settings.
#
# The value provided will be applied to all operations on the returned
# bucket instance and its files.
Expand Down Expand Up @@ -298,7 +306,8 @@ def bucket bucket_name,
soft_deleted: nil,
if_metageneration_match: nil,
if_metageneration_not_match: nil,
user_project: nil
user_project: nil,
projection: nil
if skip_lookup
return Bucket.new_lazy bucket_name, service,
user_project: user_project
Expand All @@ -308,7 +317,8 @@ def bucket bucket_name,
if_metageneration_not_match: if_metageneration_not_match,
user_project: user_project,
soft_deleted: soft_deleted,
generation: generation
generation: generation,
projection: projection

Bucket.from_gapi gapi, service, user_project: user_project
rescue Google::Cloud::NotFoundError
Expand Down Expand Up @@ -423,7 +433,23 @@ def bucket bucket_name,
# See also {Bucket#requester_pays=} and {Bucket#requester_pays}.
# @param [Boolean] enable_object_retention
# When set to true, object retention is enabled for this bucket.
# @param [String] projection Set of properties to return. Accepted values
# are `noAcl` and `full`. The default value is `noAcl`. If set to `full`,
# the bucket will include additional metadata, such as ACL policies and
# IP filter settings.
#
# @param [Hash] ip_filter The bucket's IP filter configuration.
# Acceptable values are:
# * A {Google::Apis::StorageV1::Bucket::IpFilter} object.
# * A Hash that can be converted to a {Google::Apis::StorageV1::Bucket::IpFilter} object:
# * `:mode` (String) - The mode of the IP filter. Acceptable values are: "Disabled", "Enabled"
# * `:public_network_source` (Hash) - The public network source configuration:
# * `:allowed_ip_cidr_ranges` (Array<String>) - Array of IP CIDR ranges allowed for public access.
# * `:vpc_network_sources` (Array<Hash>) - The VPC network sources configuration:
# * `:network` (String) - The VPC network resource path, e.g. "projects/PROJECT_ID/global/networks/NETWORK_NAME".
# * `:allowed_ip_cidr_ranges` (Array<String>) - Array of IP CIDR ranges allowed for VPC access.
# * `:allow_cross_org_vpcs` (Boolean) - Whether to allow cross-org VPC access.
# * `:allow_all_service_agent_access` (Boolean) - Whether to allow all service agent access.
# @yield [bucket] a block for configuring the bucket before it is
# created
# @yieldparam [Bucket] bucket the bucket object to be configured
Expand Down Expand Up @@ -477,12 +503,16 @@ def create_bucket bucket_name,
user_project: nil,
autoclass_enabled: false,
enable_object_retention: nil,
hierarchical_namespace: nil
hierarchical_namespace: nil,
ip_filter: nil,
projection: nil

params = {
name: bucket_name,
location: location,
custom_placement_config: custom_placement_config,
hierarchical_namespace: hierarchical_namespace
hierarchical_namespace: hierarchical_namespace,
ip_filter: ip_filter
}.delete_if { |_, v| v.nil? }
new_bucket = Google::Apis::StorageV1::Bucket.new(**params)
storage_class = storage_class_for storage_class
Expand All @@ -496,6 +526,7 @@ def create_bucket bucket_name,
b.versioning = versioning unless versioning.nil?
b.requester_pays = requester_pays unless requester_pays.nil?
b.hierarchical_namespace = hierarchical_namespace unless hierarchical_namespace.nil?
b.ip_filter = ip_filter unless ip_filter.nil?
end
yield updater if block_given?
updater.check_for_changed_labels!
Expand All @@ -504,7 +535,8 @@ def create_bucket bucket_name,
gapi = service.insert_bucket \
new_bucket, acl: acl_rule(acl), default_acl: acl_rule(default_acl),
user_project: user_project,
enable_object_retention: enable_object_retention
enable_object_retention: enable_object_retention,
projection: projection
Bucket.from_gapi gapi, service, user_project: user_project
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
Expand Down
Loading
Loading