Skip to content

Commit 66d12d7

Browse files
committed
feat(Storage): add support for Bucket IP filter configuration
1 parent f1b4cf9 commit 66d12d7

6 files changed

Lines changed: 521 additions & 0 deletions

File tree

Storage/src/Bucket.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,16 @@ public function delete(array $options = [])
10921092
* 'inherited' and 'enforced' are supported. **defaults to**
10931093
* `inherited`. For more details, see
10941094
* [Public Access Prevention](https://cloud.google.com/storage/docs/public-access-prevention).
1095+
* @type array $ipFilter The bucket's IP filter configuration.
1096+
* @type string $ipFilter.mode The IP filter mode. Accepted values are 'Enabled' and 'Disabled'.
1097+
* @type array $ipFilter.publicNetworkSource Public network configuration.
1098+
* @type array $ipFilter.publicNetworkSource.allowedIpCidrRanges Allowed IP/CIDR ranges.
1099+
* @type array $ipFilter.vpcNetworkSources List of private VPC networks configurations.
1100+
* Each element contains:
1101+
* @type string $ipFilter.vpcNetworkSources[].network Fully qualified VPC network URL/name.
1102+
* @type array $ipFilter.vpcNetworkSources[].allowedIpCidrRanges Allowed IP/CIDR ranges.
1103+
* @type bool $ipFilter.allowCrossOrgVpcs Set true to allow VPCs outside the org.
1104+
* @type bool $ipFilter.allowAllServiceAgentAccess Set true to allow service-to-service agent interactions.
10951105
* }
10961106
* @codingStandardsIgnoreEnd
10971107
* @return array

Storage/src/Connection/ServiceDefinition/storage-v1.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,61 @@
324324
}
325325
}
326326
},
327+
"ipFilter": {
328+
"type": "object",
329+
"description": "The bucket's IP filter configuration.",
330+
"properties": {
331+
"mode": {
332+
"type": "string",
333+
"description": "The bucket's IP filter mode. E.g., 'Enabled', 'Disabled'.",
334+
"enum": [
335+
"Enabled",
336+
"Disabled"
337+
]
338+
},
339+
"publicNetworkSource": {
340+
"type": "object",
341+
"description": "The public network source configuration for IP filtering.",
342+
"properties": {
343+
"allowedIpCidrRanges": {
344+
"type": "array",
345+
"description": "Allowed public IP CIDR ranges.",
346+
"items": {
347+
"type": "string"
348+
}
349+
}
350+
}
351+
},
352+
"vpcNetworkSources": {
353+
"type": "array",
354+
"description": "Private VPC network sources for IP filtering.",
355+
"items": {
356+
"type": "object",
357+
"properties": {
358+
"network": {
359+
"type": "string",
360+
"description": "The fully qualified VPC network name."
361+
},
362+
"allowedIpCidrRanges": {
363+
"type": "array",
364+
"description": "Allowed IP CIDR ranges.",
365+
"items": {
366+
"type": "string"
367+
}
368+
}
369+
}
370+
}
371+
},
372+
"allowCrossOrgVpcs": {
373+
"type": "boolean",
374+
"description": "Whether to allow cross-organization VPCs."
375+
},
376+
"allowAllServiceAgentAccess": {
377+
"type": "boolean",
378+
"description": "Whether to allow service agent access."
379+
}
380+
}
381+
},
327382
"id": {
328383
"type": "string",
329384
"description": "The ID of the bucket. For buckets, the id and name properties are the same."

Storage/src/StorageClient.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,16 @@ public function restore(string $name, string $generation, array $options = [])
496496
* [feature documentation](https://cloud.google.com/storage/docs/uniform-bucket-level-access),
497497
* as well as
498498
* [Should You Use uniform bucket-level access](https://cloud.google.com/storage/docs/uniform-bucket-level-access#should-you-use)
499+
* @type array $ipFilter The bucket's IP filter configuration.
500+
* @type string $ipFilter.mode The IP filter mode. Accepted values are 'Enabled' and 'Disabled'.
501+
* @type array $ipFilter.publicNetworkSource Public network configuration.
502+
* @type array $ipFilter.publicNetworkSource.allowedIpCidrRanges Allowed IP/CIDR ranges.
503+
* @type array $ipFilter.vpcNetworkSources List of private VPC networks configurations.
504+
* Each element contains:
505+
* @type string $ipFilter.vpcNetworkSources[].network Fully qualified VPC network URL/name.
506+
* @type array $ipFilter.vpcNetworkSources[].allowedIpCidrRanges Allowed IP/CIDR ranges.
507+
* @type bool $ipFilter.allowCrossOrgVpcs Set true to allow VPCs outside the org.
508+
* @type bool $ipFilter.allowAllServiceAgentAccess Set true to allow service-to-service agent interactions.
499509
* @type string $rpo Specifies the Turbo Replication setting for a dual-region bucket.
500510
* The possible values are DEFAULT and ASYNC_TURBO. Trying to set the rpo for a non dual-region
501511
* bucket will throw an exception. Non existence of this parameter is equivalent to it being DEFAULT.

Storage/tests/System/ManageBucketsTest.php

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
namespace Google\Cloud\Storage\Tests\System;
1919

2020
use Google\Cloud\Core\Exception\BadRequestException;
21+
use Google\Cloud\Core\Exception\ServiceException;
2122
use Google\Cloud\Storage\Bucket;
2223

2324
/**
@@ -577,4 +578,221 @@ public function encryptionEnforcementConfigs()
577578
]
578579
];
579580
}
581+
582+
/**
583+
* @group storage-ipfilter
584+
*/
585+
public function testCreateBucketWithIpFilterDisabled()
586+
{
587+
$projectId = self::getProjectId(getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH'));
588+
589+
$ipFilterConfig = [
590+
'mode' => 'Disabled',
591+
'publicNetworkSource' => [
592+
'allowedIpCidrRanges' => ['1.2.3.0/24']
593+
],
594+
'vpcNetworkSources' => [
595+
[
596+
'network' => 'projects/' . $projectId . '/global/networks/default',
597+
'allowedIpCidrRanges' => ['10.0.0.0/24']
598+
]
599+
],
600+
'allowCrossOrgVpcs' => true,
601+
'allowAllServiceAgentAccess' => true
602+
];
603+
604+
$bucketName = uniqid(self::TESTING_PREFIX);
605+
$bucket = self::createBucket(
606+
self::$client,
607+
$bucketName,
608+
['ipFilter' => $ipFilterConfig]
609+
);
610+
611+
$info = $bucket->info();
612+
$this->assertArrayHasKey('ipFilter', $info);
613+
$this->assertEquals('Disabled', $info['ipFilter']['mode']);
614+
$this->assertEquals(['1.2.3.0/24'], $info['ipFilter']['publicNetworkSource']['allowedIpCidrRanges']);
615+
$this->assertEquals(
616+
'projects/' . $projectId . '/global/networks/default',
617+
$info['ipFilter']['vpcNetworkSources'][0]['network']
618+
);
619+
$this->assertEquals(['10.0.0.0/24'], $info['ipFilter']['vpcNetworkSources'][0]['allowedIpCidrRanges']);
620+
$this->assertTrue($info['ipFilter']['allowAllServiceAgentAccess']);
621+
$this->assertTrue($info['ipFilter']['allowCrossOrgVpcs']);
622+
623+
return $bucket;
624+
}
625+
626+
/**
627+
* @depends testCreateBucketWithIpFilterDisabled
628+
* @group storage-ipfilter
629+
*/
630+
public function testUpdateIpFilterDisabled(Bucket $bucket)
631+
{
632+
$ipFilterConfig = [
633+
'mode' => 'Disabled',
634+
'publicNetworkSource' => [
635+
'allowedIpCidrRanges' => ['1.2.3.0/24', '5.6.7.0/24']
636+
],
637+
'allowAllServiceAgentAccess' => false
638+
];
639+
640+
$bucket->update(['ipFilter' => $ipFilterConfig]);
641+
$info = $bucket->reload();
642+
643+
$this->assertArrayHasKey('ipFilter', $info);
644+
$this->assertEquals('Disabled', $info['ipFilter']['mode']);
645+
$this->assertEquals(
646+
['1.2.3.0/24', '5.6.7.0/24'],
647+
$info['ipFilter']['publicNetworkSource']['allowedIpCidrRanges']
648+
);
649+
$this->assertFalse($info['ipFilter']['allowAllServiceAgentAccess']);
650+
}
651+
652+
/**
653+
* @group storage-ipfilter
654+
*/
655+
public function testCreateBucketWithInvalidIpFilterFails()
656+
{
657+
$this->expectException(BadRequestException::class);
658+
659+
$ipFilterConfig = [
660+
'mode' => 'Disabled',
661+
'publicNetworkSource' => [
662+
// Host bits must be zero for /24 range, so 1.2.3.4/24 is invalid
663+
'allowedIpCidrRanges' => ['1.2.3.4/24']
664+
],
665+
'allowAllServiceAgentAccess' => true
666+
];
667+
668+
self::createBucket(
669+
self::$client,
670+
uniqid(self::TESTING_PREFIX),
671+
[
672+
'ipFilter' => $ipFilterConfig,
673+
'retries' => 0,
674+
'sysTestRetries' => 0
675+
]
676+
);
677+
}
678+
679+
/**
680+
* @group storage-ipfilter
681+
*/
682+
public function testUpdateBucketWithInvalidIpFilterFails()
683+
{
684+
$bucket = self::createBucket(self::$client, uniqid(self::TESTING_PREFIX));
685+
686+
$this->expectException(BadRequestException::class);
687+
688+
$ipFilterConfig = [
689+
'mode' => 'Disabled',
690+
'publicNetworkSource' => [
691+
'allowedIpCidrRanges' => ['1.2.3.4/24']
692+
],
693+
'allowAllServiceAgentAccess' => true
694+
];
695+
696+
$bucket->update(['ipFilter' => $ipFilterConfig]);
697+
}
698+
699+
/**
700+
* @group storage-ipfilter
701+
*/
702+
public function testGetBucketWithIpFilter()
703+
{
704+
$bucketName = uniqid(self::TESTING_PREFIX);
705+
$ipFilterConfig = [
706+
'mode' => 'Disabled',
707+
'publicNetworkSource' => [
708+
'allowedIpCidrRanges' => ['1.2.3.0/24']
709+
],
710+
'allowAllServiceAgentAccess' => true
711+
];
712+
713+
self::createBucket(
714+
self::$client,
715+
$bucketName,
716+
['ipFilter' => $ipFilterConfig]
717+
);
718+
719+
$bucket = self::$client->bucket($bucketName);
720+
$info = $bucket->reload();
721+
722+
$this->assertArrayHasKey('ipFilter', $info);
723+
$this->assertEquals('Disabled', $info['ipFilter']['mode']);
724+
$this->assertEquals(
725+
['1.2.3.0/24'],
726+
$info['ipFilter']['publicNetworkSource']['allowedIpCidrRanges']
727+
);
728+
}
729+
730+
/**
731+
* @group storage-ipfilter
732+
* @depends testCreateBucketWithIpFilterDisabled
733+
*/
734+
public function testDeleteBucketIpFilter(Bucket $bucket)
735+
{
736+
$ipFilterConfig = [
737+
'mode' => 'Disabled',
738+
'publicNetworkSource' => [
739+
'allowedIpCidrRanges' => []
740+
],
741+
'allowAllServiceAgentAccess' => true
742+
];
743+
744+
$bucket->update(['ipFilter' => $ipFilterConfig]);
745+
$info = $bucket->reload();
746+
747+
$this->assertArrayHasKey('ipFilter', $info);
748+
$this->assertEquals('Disabled', $info['ipFilter']['mode']);
749+
$this->assertArrayNotHasKey('allowedIpCidrRanges', $info['ipFilter']['publicNetworkSource']);
750+
}
751+
752+
/**
753+
* @group storage-ipfilter
754+
*/
755+
public function testBucketAccessWhenEnforced()
756+
{
757+
$iam = self::$bucket->iam();
758+
$isExempt = !empty($iam->testPermissions(['storage.buckets.exemptFromIpFilter']));
759+
760+
if (!$isExempt) {
761+
$this->markTestSkipped(
762+
'Runner is not exempt from IP filter. Skipping lockout tests to prevent orphaned resources.'
763+
);
764+
}
765+
766+
$bucketName = uniqid(self::TESTING_PREFIX);
767+
$ipFilterConfig = [
768+
'mode' => 'Enabled',
769+
'publicNetworkSource' => [
770+
'allowedIpCidrRanges' => ['1.1.1.1/32'] // Blocked IP for runner
771+
],
772+
'allowAllServiceAgentAccess' => true
773+
];
774+
775+
$bucket = self::createBucket(
776+
self::$client,
777+
$bucketName,
778+
['ipFilter' => $ipFilterConfig]
779+
);
780+
781+
$objectName = 'test_ip_filter.txt';
782+
$objectContent = 'hello world';
783+
784+
$object = $bucket->upload($objectContent, ['name' => $objectName]);
785+
$this->assertEquals($objectContent, $object->downloadAsString());
786+
787+
$unauthBucket = self::$unauthenticatedClient->bucket($bucketName);
788+
$unauthObject = $unauthBucket->object($objectName);
789+
790+
try {
791+
$unauthObject->downloadAsString();
792+
$this->fail('Expected unauthenticated download to fail due to IP filter.');
793+
} catch (ServiceException $e) {
794+
$this->assertEquals(403, $e->getCode());
795+
$this->assertStringContainsString('IP filtering', $e->getMessage());
796+
}
797+
}
580798
}

0 commit comments

Comments
 (0)