Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(storage): support optionsRequestedPolicyVersion #9989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 13, 2020
Merged
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
8 changes: 4 additions & 4 deletions api_core/google/api_core/iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ def bindings(self):
policy.version = 3

policy.bindings = [
{
"role": "roles/viewer",
"members": {USER, ADMIN_GROUP, SERVICE_ACCOUNT},
"condition": CONDITION
{
"role": "roles/viewer",
"members": {USER, ADMIN_GROUP, SERVICE_ACCOUNT},
"condition": CONDITION
},
...
]
Expand Down
17 changes: 16 additions & 1 deletion storage/google/cloud/storage/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -1458,7 +1458,7 @@ def create_resumable_upload_session(
except resumable_media.InvalidResponse as exc:
_raise_from_invalid_response(exc)

def get_iam_policy(self, client=None):
def get_iam_policy(self, client=None, requested_policy_version=None):
"""Retrieve the IAM policy for the object.

.. note:
Expand All @@ -1477,6 +1477,18 @@ def get_iam_policy(self, client=None):
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the current object's bucket.

:type requested_policy_version: int or ``NoneType``
:param requested_policy_version: Optional. The version of IAM policies to request.
If a policy with a condition is requested without
setting this, the server will return an error.
This must be set to a value of 3 to retrieve IAM
policies containing conditions. This is to prevent
client code that isn't aware of IAM conditions from
interpreting and modifying policies incorrectly.
The service might return a policy with version lower
than the one that was requested, based on the
feature syntax in the policy fetched.

:rtype: :class:`google.api_core.iam.Policy`
:returns: the policy instance, based on the resource returned from
the ``getIamPolicy`` API request.
Expand All @@ -1488,6 +1500,9 @@ def get_iam_policy(self, client=None):
if self.user_project is not None:
query_params["userProject"] = self.user_project

if requested_policy_version is not None:
query_params["optionsRequestedPolicyVersion"] = requested_policy_version

info = client._connection.api_request(
method="GET",
path="%s/iam" % (self.path,),
Expand Down
41 changes: 40 additions & 1 deletion storage/google/cloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1865,7 +1865,7 @@ def disable_website(self):
"""
return self.configure_website(None, None)

def get_iam_policy(self, client=None):
def get_iam_policy(self, client=None, requested_policy_version=None):
"""Retrieve the IAM policy for the bucket.

See
Expand All @@ -1878,16 +1878,55 @@ def get_iam_policy(self, client=None):
:param client: Optional. The client to use. If not passed, falls back
to the ``client`` stored on the current bucket.

:type requested_policy_version: int or ``NoneType``
:param requested_policy_version: Optional. The version of IAM policies to request.
If a policy with a condition is requested without
setting this, the server will return an error.
This must be set to a value of 3 to retrieve IAM
policies containing conditions. This is to prevent
client code that isn't aware of IAM conditions from
interpreting and modifying policies incorrectly.
The service might return a policy with version lower
than the one that was requested, based on the
feature syntax in the policy fetched.

:rtype: :class:`google.api_core.iam.Policy`
:returns: the policy instance, based on the resource returned from
the ``getIamPolicy`` API request.

Example:

.. code-block:: python

from google.cloud.storage.iam import STORAGE_OBJECT_VIEWER_ROLE

policy = bucket.get_iam_policy(requested_policy_version=3)

policy.version = 3

# Add a binding to the policy via it's bindings property
policy.bindings.append({
"role": STORAGE_OBJECT_VIEWER_ROLE,
"members": {"serviceAccount:[email protected]", ...},
# Optional:
"condition": {
"title": "prefix"
"description": "Objects matching prefix"
"expression": "resource.name.startsWith(\"projects/project-name/buckets/bucket-name/objects/prefix\")"
}
})

bucket.set_iam_policy(policy)
"""
client = self._require_client(client)
query_params = {}

if self.user_project is not None:
query_params["userProject"] = self.user_project

if requested_policy_version is not None:
query_params["optionsRequestedPolicyVersion"] = requested_policy_version

info = client._connection.api_request(
method="GET",
path="%s/iam" % (self.path,),
Expand Down
60 changes: 60 additions & 0 deletions storage/tests/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,66 @@ def test_bucket_update_labels(self):
bucket.update()
self.assertEqual(bucket.labels, {})

def test_get_set_iam_policy(self):
import pytest
from google.cloud.storage.iam import STORAGE_OBJECT_VIEWER_ROLE
from google.api_core.exceptions import BadRequest, PreconditionFailed

bucket_name = "iam-policy" + unique_resource_id("-")
bucket = retry_429_503(Config.CLIENT.create_bucket)(bucket_name)
self.case_buckets_to_delete.append(bucket_name)
self.assertTrue(bucket.exists())

policy_no_version = bucket.get_iam_policy()
self.assertEqual(policy_no_version.version, 1)

policy = bucket.get_iam_policy(requested_policy_version=3)
self.assertEqual(policy, policy_no_version)

member = "serviceAccount:{}".format(Config.CLIENT.get_service_account_email())

BINDING_W_CONDITION = {
"role": STORAGE_OBJECT_VIEWER_ROLE,
"members": {member},
"condition": {
"title": "always-true",
"description": "test condition always-true",
"expression": "true",
},
}
policy.bindings.append(BINDING_W_CONDITION)

with pytest.raises(
PreconditionFailed, match="enable uniform bucket-level access"
):
bucket.set_iam_policy(policy)

bucket.iam_configuration.uniform_bucket_level_access_enabled = True
bucket.patch()

policy = bucket.get_iam_policy(requested_policy_version=3)
policy.bindings.append(BINDING_W_CONDITION)

with pytest.raises(BadRequest, match="at least 3"):
bucket.set_iam_policy(policy)

policy.version = 3
returned_policy = bucket.set_iam_policy(policy)
self.assertEqual(returned_policy.version, 3)
self.assertEqual(returned_policy.bindings, policy.bindings)

with pytest.raises(
BadRequest, match="cannot be less than the existing policy version"
):
bucket.get_iam_policy()
with pytest.raises(
BadRequest, match="cannot be less than the existing policy version"
):
bucket.get_iam_policy(requested_policy_version=2)

fetched_policy = bucket.get_iam_policy(requested_policy_version=3)
self.assertEqual(fetched_policy.bindings, returned_policy.bindings)

@unittest.skipUnless(USER_PROJECT, "USER_PROJECT not set in environment.")
def test_crud_bucket_with_requester_pays(self):
new_bucket_name = "w-requester-pays" + unique_resource_id("-")
Expand Down
43 changes: 39 additions & 4 deletions storage/tests/unit/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -1928,7 +1928,7 @@ def test_get_iam_policy(self):
BLOB_NAME = "blob-name"
PATH = "/b/name/o/%s" % (BLOB_NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
OWNER1 = "user:[email protected]"
OWNER2 = "group:[email protected]"
EDITOR1 = "domain:google.com"
Expand Down Expand Up @@ -1973,14 +1973,49 @@ def test_get_iam_policy(self):
},
)

def test_get_iam_policy_w_requested_policy_version(self):
from google.cloud.storage.iam import STORAGE_OWNER_ROLE

BLOB_NAME = "blob-name"
PATH = "/b/name/o/%s" % (BLOB_NAME,)
ETAG = "DEADBEEF"
VERSION = 1
OWNER1 = "user:[email protected]"
OWNER2 = "group:[email protected]"
RETURNED = {
"resourceId": PATH,
"etag": ETAG,
"version": VERSION,
"bindings": [{"role": STORAGE_OWNER_ROLE, "members": [OWNER1, OWNER2]}],
}
after = ({"status": http_client.OK}, RETURNED)
connection = _Connection(after)
client = _Client(connection)
bucket = _Bucket(client=client)
blob = self._make_one(BLOB_NAME, bucket=bucket)

blob.get_iam_policy(requested_policy_version=3)

kw = connection._requested
self.assertEqual(len(kw), 1)
self.assertEqual(
kw[0],
{
"method": "GET",
"path": "%s/iam" % (PATH,),
"query_params": {"optionsRequestedPolicyVersion": 3},
"_target_object": None,
},
)

def test_get_iam_policy_w_user_project(self):
from google.api_core.iam import Policy

BLOB_NAME = "blob-name"
USER_PROJECT = "user-project-123"
PATH = "/b/name/o/%s" % (BLOB_NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
RETURNED = {
"resourceId": PATH,
"etag": ETAG,
Expand Down Expand Up @@ -2023,7 +2058,7 @@ def test_set_iam_policy(self):
BLOB_NAME = "blob-name"
PATH = "/b/name/o/%s" % (BLOB_NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
OWNER1 = "user:[email protected]"
OWNER2 = "group:[email protected]"
EDITOR1 = "domain:google.com"
Expand Down Expand Up @@ -2074,7 +2109,7 @@ def test_set_iam_policy_w_user_project(self):
USER_PROJECT = "user-project-123"
PATH = "/b/name/o/%s" % (BLOB_NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
BINDINGS = []
RETURNED = {"etag": ETAG, "version": VERSION, "bindings": BINDINGS}
after = ({"status": http_client.OK}, RETURNED)
Expand Down
37 changes: 33 additions & 4 deletions storage/tests/unit/test_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -2023,7 +2023,7 @@ def test_get_iam_policy(self):
NAME = "name"
PATH = "/b/%s" % (NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
OWNER1 = "user:[email protected]"
OWNER2 = "group:[email protected]"
EDITOR1 = "domain:google.com"
Expand Down Expand Up @@ -2067,7 +2067,7 @@ def test_get_iam_policy_w_user_project(self):
USER_PROJECT = "user-project-123"
PATH = "/b/%s" % (NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
RETURNED = {
"resourceId": PATH,
"etag": ETAG,
Expand All @@ -2092,6 +2092,35 @@ def test_get_iam_policy_w_user_project(self):
self.assertEqual(kw[0]["path"], "%s/iam" % (PATH,))
self.assertEqual(kw[0]["query_params"], {"userProject": USER_PROJECT})

def test_get_iam_policy_w_requested_policy_version(self):
from google.cloud.storage.iam import STORAGE_OWNER_ROLE

NAME = "name"
PATH = "/b/%s" % (NAME,)
ETAG = "DEADBEEF"
VERSION = 1
OWNER1 = "user:[email protected]"
OWNER2 = "group:[email protected]"
RETURNED = {
"resourceId": PATH,
"etag": ETAG,
"version": VERSION,
"bindings": [{"role": STORAGE_OWNER_ROLE, "members": [OWNER1, OWNER2]}],
}
connection = _Connection(RETURNED)
client = _Client(connection, None)
bucket = self._make_one(client=client, name=NAME)

policy = bucket.get_iam_policy(requested_policy_version=3)

self.assertEqual(policy.version, VERSION)

kw = connection._requested
self.assertEqual(len(kw), 1)
self.assertEqual(kw[0]["method"], "GET")
self.assertEqual(kw[0]["path"], "%s/iam" % (PATH,))
self.assertEqual(kw[0]["query_params"], {"optionsRequestedPolicyVersion": 3})

def test_set_iam_policy(self):
import operator
from google.cloud.storage.iam import STORAGE_OWNER_ROLE
Expand All @@ -2102,7 +2131,7 @@ def test_set_iam_policy(self):
NAME = "name"
PATH = "/b/%s" % (NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
OWNER1 = "user:[email protected]"
OWNER2 = "group:[email protected]"
EDITOR1 = "domain:google.com"
Expand Down Expand Up @@ -2155,7 +2184,7 @@ def test_set_iam_policy_w_user_project(self):
USER_PROJECT = "user-project-123"
PATH = "/b/%s" % (NAME,)
ETAG = "DEADBEEF"
VERSION = 17
VERSION = 1
OWNER1 = "user:[email protected]"
OWNER2 = "group:[email protected]"
EDITOR1 = "domain:google.com"
Expand Down