Skip to content

Commit

Permalink
feat: support iam conditions (#2416)
Browse files Browse the repository at this point in the history
* support IAM condition

* update Storage JSON definition, rev,. 20190913

* throw InvalidOperationException if version is greater than 1

* nit: doc

* do not cache Iam instance on bucket; make $version arg a key in $options array

* revert version argument from IAM constructor

* docs: document optionsRequestedPolicyVersion

* revert stored $iam instance on bucket

* docs: add example of a policy in PolicyBuilder

* more docs

* fix link

* map requestedPolicyVersion arg to optionsRequestedPolicyVersion in Storage req opts

* fix(docs): optionsRequestedPolicyVersion => requestedPolicyVersion

* fix

* test: validate policy version and conditions

* test: assert requestedPolicyVersion arg is mapped to optionsRequestedPolicyVersion

* merge Storage definition from master

* fix: lint

* lint

* fix

* docs: update inline sample to use prefix condition

* add IAM get/set system tests

* add conditional policy system test

* fix docs

Co-Authored-By: David Supplee <dwsupplee@gmail.com>

* fix @see markdown links

* fix ;

* add @deprecated tag

* use BadMethodCallException

* fix style

* fix style

* add snippet coverage

* update bucket->iam snippet tests

* fix snippet parsing issue

* Update Storage/tests/System/IamTest.php

Co-Authored-By: David Supplee <dwsupplee@gmail.com>

* Update Core/src/Iam/PolicyBuilder.php

Co-Authored-By: David Supplee <dwsupplee@gmail.com>

Co-authored-by: David Supplee <dwsupplee@gmail.com>
  • Loading branch information
jkwlui and dwsupplee committed Feb 5, 2020
1 parent 3170002 commit ddc58ee
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 8 deletions.
Binary file added .rnd
Binary file not shown.
4 changes: 4 additions & 0 deletions Core/src/Iam/Iam.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public function __construct(IamConnectionInterface $connection, $resource, array
* ```
*
* @param array $options Configuration Options
* @param int $options['requestedPolicyVersion'] Specify the policy version to
* request from the server. Please see
* [policy versioning](https://cloud.google.com/iam/docs/policies#versions)
* for more information.
* @return array An array of policy data
*/
public function policy(array $options = [])
Expand Down
88 changes: 83 additions & 5 deletions Core/src/Iam/PolicyBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
namespace Google\Cloud\Core\Iam;

use InvalidArgumentException;
use BadMethodCallException;

/**
* Helper class for creating valid IAM policies
Expand Down Expand Up @@ -51,6 +52,38 @@ class PolicyBuilder
/**
* Create a PolicyBuilder.
*
* To use conditions in the bindings, the version of the policy must be set
* to 3.
*
* @see https://cloud.google.com/iam/docs/policies#versions Policy versioning
* @see https://cloud-dot-devsite.googleplex.com/storage/docs/access-control/using-iam-permissions#conditions-iam
* Using Cloud IAM Conditions on buckets
*
* Example:
* ```
* $policy = [
* 'etag' => 'AgIc==',
* 'version' => 3,
* 'bindings' => [
* [
* 'role' => 'roles/admin',
* 'members' => [
* 'user:admin@domain.com',
* 'user2:admin@domain.com'
* ],
* 'condition' => [
* 'title' => 'match-prefix',
* 'description' => 'Applies to objects matching a prefix',
* 'expression' =>
* 'resource.name.startsWith("projects/_/buckets/bucket-name/objects/prefix-a-")'
* ]
* ]
* ],
* ];
*
* $builder = new PolicyBuilder($policy);
* ```
*
* @param array $policy A policy array
* @throws InvalidArgumentException
*/
Expand Down Expand Up @@ -81,6 +114,10 @@ public function __construct(array $policy = [])
* 'role' => 'roles/admin',
* 'members' => [
* 'user:admin@domain.com'
* ],
* 'condition' => [
* 'expression' =>
* 'request.time < timestamp("2020-07-01T00:00:00.000Z")'
* ]
* ]
* ]);
Expand All @@ -92,17 +129,20 @@ public function __construct(array $policy = [])
*/
public function setBindings(array $bindings = [])
{
$this->bindings = [];
foreach ($bindings as $binding) {
$this->addBinding($binding['role'], $binding['members']);
}

$this->bindings = $bindings;
return $this;
}

/**
* Add a new binding to the policy.
*
* This method will fail with an InvalidOpereationException if it is
* called on a Policy with a version greater than 1 as that indicates
* a more complicated policy than this method is prepared to handle.
* Changes to such policies must be made manually by the setBindings()
* method.
*
*
* Example:
* ```
* $builder->addBinding('roles/admin', [ 'user:admin@domain.com' ]);
Expand All @@ -112,9 +152,13 @@ public function setBindings(array $bindings = [])
* @param array $members An array of members to assign to the binding
* @return PolicyBuilder
* @throws InvalidArgumentException
* @throws BadMethodCallException if the policy's version is greater than 1.
* @deprecated
*/
public function addBinding($role, array $members)
{
$this->validatePolicyVersion();

$this->bindings[] = [
'role' => $role,
'members' => $members
Expand All @@ -126,6 +170,12 @@ public function addBinding($role, array $members)
/**
* Remove a binding from the policy.
*
* This method will fail with a BadMethodCallException if it is
* called on a Policy with a version greater than 1 as that indicates
* a more complicated policy than this method is prepared to handle.
* Changes to such policies must be made manually by the setBindings()
* method.
*
* Example:
* ```
* $builder->setBindings([
Expand All @@ -144,9 +194,13 @@ public function addBinding($role, array $members)
* @param array $members An array of members to remove from the role
* @return PolicyBuilder
* @throws InvalidArgumentException
* @throws BadMethodCallException if the policy's version is greater than 1.
* @deprecated
*/
public function removeBinding($role, array $members)
{
$this->validatePolicyVersion();

$bindings = $this->bindings;
foreach ((array) $bindings as $i => $binding) {
if ($binding['role'] == $role) {
Expand Down Expand Up @@ -226,4 +280,28 @@ public function result()
'version' => $this->version
]);
}

private function validatePolicyVersion()
{
if (isset($this->version) && $this->version > 1) {
throw new BadMethodCallException("Helper methods cannot be " .
"invoked on policies with version {$this->version}.");
}

$this->validateConditions();
}

private function validateConditions()
{
if (!$this->bindings) {
return;
}

foreach ($this->bindings as $binding) {
if (isset($binding['condition'])) {
throw new BadMethodCallException("Helper methods cannot " .
"be invoked on policies containing conditions.");
}
}
}
}
88 changes: 86 additions & 2 deletions Core/tests/Unit/Iam/PolicyBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function testBuilder()

$builder = new PolicyBuilder;
$builder->setEtag($etag);
$builder->setVersion(2);
$builder->setVersion(1);
$builder->addBinding($role, $members);

$result = $builder->result();
Expand All @@ -55,7 +55,7 @@ public function testBuilder()
]
],
'etag' => $etag,
'version' => 2
'version' => 1
];

$this->assertEquals($policy, $result);
Expand Down Expand Up @@ -139,6 +139,43 @@ public function testConstructWithExistingPolicy()
$this->assertEquals($policy, $result);
}

/**
* @expectedException BadMethodCallException
* @expectedExceptionMessage Helper methods cannot be invoked on policies with version 3.
*/
public function testAddBindingVersionThrowsException()
{
$builder = new PolicyBuilder();
$builder->setVersion(3);

$builder->addBinding('test', ['user:test@test.com']);
}

/**
* @expectedException BadMethodCallException
* @expectedExceptionMessage Helper methods cannot be invoked on policies containing conditions.
*/
public function testAddBindingWithConditionsThrowsException()
{
$policy = [
'bindings' => [
[
'role' => 'test',
'members' => [
'user:test@test.com',
],
'condition' => [
'expression' => 'true',
]
],
],
];
$builder = new PolicyBuilder($policy);
$builder->setVersion(1);

$builder->addBinding('test2', ['user:test@test.com']);
}

public function testRemoveBinding()
{
$policy = [
Expand Down Expand Up @@ -225,4 +262,51 @@ public function testRemoveBindingInvalidRoleThrowsException()
$builder = new PolicyBuilder($policy);
$builder->removeBinding('test2', ['user:test@test.com']);
}

/**
* @expectedException BadMethodCallException
* @expectedExceptionMessage Helper methods cannot be invoked on policies with version 3.
*/
public function testRemoveBindingVersionThrowsException()
{
$policy = [
'version' => 3,
'bindings' => [
[
'role' => 'test',
'members' => [
'user:test@test.com',
]
],
]
];

$builder = new PolicyBuilder($policy);
$builder->removeBinding('test', ['user:test@test.com']);
}

/**
* @expectedException BadMethodCallException
* @expectedExceptionMessage Helper methods cannot be invoked on policies containing conditions.
*/
public function testRemoveBindingWithConditionsThrowsException()
{
$policy = [
'bindings' => [
[
'role' => 'test',
'members' => [
'user:test@test.com',
],
'condition' => [
'expression' => 'true',
]
],
],
];

$builder = new PolicyBuilder($policy);
$builder->setVersion(1);
$builder->removeBinding('test', ['user:test@test.com']);
}
}
11 changes: 10 additions & 1 deletion Storage/src/Bucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -1170,18 +1170,27 @@ public function isWritable($file = null)
/**
* Manage the IAM policy for the current Bucket.
*
* Please note that this method may not yet be available in your project.
* To request a policy with conditions, pass an array with
* '[requestedPolicyVersion => 3]' as argument to the policy() and
* reload() methods.
*
* Example:
* ```
* $iam = $bucket->iam();
*
* // Returns the stored policy, or fetches the policy if none exists.
* $policy = $iam->policy(['requestedPolicyVersion' => 3]);
*
* // Fetches a policy from the server.
* $policy = $iam->reload(['requestedPolicyVersion' => 3]);
* ```
*
* @codingStandardsIgnoreStart
* @see https://cloud.google.com/storage/docs/access-control/iam-with-json-and-xml Storage Access Control Documentation
* @see https://cloud.google.com/storage/docs/json_api/v1/buckets/getIamPolicy Get Bucket IAM Policy
* @see https://cloud.google.com/storage/docs/json_api/v1/buckets/setIamPolicy Set Bucket IAM Policy
* @see https://cloud.google.com/storage/docs/json_api/v1/buckets/testIamPermissions Test Bucket Permissions
* @see https://cloud.google.com/iam/docs/policies#versions policy versioning.
* @codingStandardsIgnoreEnd
*
* @return Iam
Expand Down
5 changes: 5 additions & 0 deletions Storage/src/Connection/IamBucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public function __construct(ConnectionInterface $connection)
*/
public function getPolicy(array $args)
{
if (isset($args['requestedPolicyVersion'])) {
$args['optionsRequestedPolicyVersion'] = $args['requestedPolicyVersion'];
unset($args['requestedPolicyVersion']);
}

return $this->connection->getBucketIamPolicy($args);
}

Expand Down
3 changes: 3 additions & 0 deletions Storage/tests/Snippet/BucketTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,10 @@ public function testIam()
{
$snippet = $this->snippetFromMethod(Bucket::class, 'iam');
$snippet->addLocal('bucket', $this->bucket);
$this->connection->getBucketIamPolicy(Argument::withEntry('optionsRequestedPolicyVersion', 3))
->shouldBeCalled();

$this->bucket->___setProperty('connection', $this->connection->reveal());
$res = $snippet->invoke('iam');
$this->assertInstanceOf(Iam::class, $res->returnVal());
}
Expand Down

0 comments on commit ddc58ee

Please sign in to comment.