Skip to content

Commit

Permalink
Storage: HMAC add user_project param (#9237)
Browse files Browse the repository at this point in the history
* test(unit): tests user_project is passed through to request

* feat: implement user_project option for HMAC operations

* fix style
  • Loading branch information
jkwlui authored and tseaver committed Sep 25, 2019
1 parent 8762283 commit d8ce06e
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 24 deletions.
25 changes: 22 additions & 3 deletions storage/google/cloud/storage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,9 @@ def list_buckets(
extra_params=extra_params,
)

def create_hmac_key(self, service_account_email, project_id=None):
def create_hmac_key(
self, service_account_email, project_id=None, user_project=None
):
"""Create an HMAC key for a service account.
:type service_account_email: str
Expand All @@ -592,6 +594,9 @@ def create_hmac_key(self, service_account_email, project_id=None):
:param project_id: (Optional) explicit project ID for the key.
Defaults to the client's project.
:type user_project: str
:param user_project: (Optional) This parameter is currently ignored.
:rtype:
Tuple[:class:`~google.cloud.storage.hmac_key.HMACKeyMetadata`, str]
:returns: metadata for the created key, plus the bytes of the key's secret, which is an 40-character base64-encoded string.
Expand All @@ -601,6 +606,10 @@ def create_hmac_key(self, service_account_email, project_id=None):

path = "/projects/{}/hmacKeys".format(project_id)
qs_params = {"serviceAccountEmail": service_account_email}

if user_project is not None:
qs_params["userProject"] = user_project

api_response = self._connection.api_request(
method="POST", path=path, query_params=qs_params
)
Expand All @@ -615,6 +624,7 @@ def list_hmac_keys(
service_account_email=None,
show_deleted_keys=None,
project_id=None,
user_project=None,
):
"""List HMAC keys for a project.
Expand All @@ -635,6 +645,9 @@ def list_hmac_keys(
:param project_id: (Optional) explicit project ID for the key.
Defaults to the client's project.
:type user_project: str
:param user_project: (Optional) This parameter is currently ignored.
:rtype:
Tuple[:class:`~google.cloud.storage.hmac_key.HMACKeyMetadata`, str]
:returns: metadata for the created key, plus the bytes of the key's secret, which is an 40-character base64-encoded string.
Expand All @@ -651,6 +664,9 @@ def list_hmac_keys(
if show_deleted_keys is not None:
extra_params["showDeletedKeys"] = show_deleted_keys

if user_project is not None:
extra_params["userProject"] = user_project

return page_iterator.HTTPIterator(
client=self,
api_request=self._connection.api_request,
Expand All @@ -660,7 +676,7 @@ def list_hmac_keys(
extra_params=extra_params,
)

def get_hmac_key_metadata(self, access_id, project_id=None):
def get_hmac_key_metadata(self, access_id, project_id=None, user_project=None):
"""Return a metadata instance for the given HMAC key.
:type access_id: str
Expand All @@ -669,8 +685,11 @@ def get_hmac_key_metadata(self, access_id, project_id=None):
:type project_id: str
:param project_id: (Optional) project ID of an existing key.
Defaults to client's project.
:type user_project: str
:param user_project: (Optional) This parameter is currently ignored.
"""
metadata = HMACKeyMetadata(self, access_id, project_id)
metadata = HMACKeyMetadata(self, access_id, project_id, user_project)
metadata.reload() # raises NotFound for missing key
return metadata

Expand Down
47 changes: 42 additions & 5 deletions storage/google/cloud/storage/hmac_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class HMACKeyMetadata(object):
:type project_id: str
:param project_id: (Optional) project ID of an existing key.
Defaults to client's project.
:type user_project: str
:param user_project: (Optional) This parameter is currently ignored.
"""

ACTIVE_STATE = "ACTIVE"
Expand All @@ -42,7 +45,7 @@ class HMACKeyMetadata(object):

_SETTABLE_STATES = (ACTIVE_STATE, INACTIVE_STATE)

def __init__(self, client, access_id=None, project_id=None):
def __init__(self, client, access_id=None, project_id=None, user_project=None):
self._client = client
self._properties = {}

Expand All @@ -52,6 +55,8 @@ def __init__(self, client, access_id=None, project_id=None):
if project_id is not None:
self._properties["projectId"] = project_id

self._user_project = user_project

def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
Expand Down Expand Up @@ -170,14 +175,31 @@ def path(self):

return "/projects/{}/hmacKeys/{}".format(project, self.access_id)

@property
def user_project(self):
"""Project ID to be billed for API requests made via this bucket.
This property is currently ignored by the server.
:rtype: str
"""
return self._user_project

def exists(self):
"""Determine whether or not the key for this metadata exists.
:rtype: bool
:returns: True if the key exists in Cloud Storage.
"""
try:
self._client._connection.api_request(method="GET", path=self.path)
qs_params = {}

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

self._client._connection.api_request(
method="GET", path=self.path, query_params=qs_params
)
except NotFound:
return False
else:
Expand All @@ -189,8 +211,13 @@ def reload(self):
:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
qs_params = {}

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

self._properties = self._client._connection.api_request(
method="GET", path=self.path
method="GET", path=self.path, query_params=qs_params
)

def update(self):
Expand All @@ -199,9 +226,13 @@ def update(self):
:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
qs_params = {}
if self.user_project is not None:
qs_params["userProject"] = self.user_project

payload = {"state": self.state}
self._properties = self._client._connection.api_request(
method="PUT", path=self.path, data=payload
method="PUT", path=self.path, data=payload, query_params=qs_params
)

def delete(self):
Expand All @@ -213,4 +244,10 @@ def delete(self):
if self.state != self.INACTIVE_STATE:
raise ValueError("Cannot delete key if not in 'INACTIVE' state.")

self._client._connection.api_request(method="DELETE", path=self.path)
qs_params = {}
if self.user_project is not None:
qs_params["userProject"] = self.user_project

self._client._connection.api_request(
method="DELETE", path=self.path, query_params=qs_params
)
31 changes: 26 additions & 5 deletions storage/tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,7 @@ def dummy_response():
self.assertIsInstance(bucket, Bucket)
self.assertEqual(bucket.name, blob_name)

def _create_hmac_key_helper(self, explicit_project=None):
def _create_hmac_key_helper(self, explicit_project=None, user_project=None):
import datetime
from pytz import UTC
from six.moves.urllib.parse import urlencode
Expand Down Expand Up @@ -996,6 +996,9 @@ def _create_hmac_key_helper(self, explicit_project=None):
if explicit_project is not None:
kwargs["project_id"] = explicit_project

if user_project is not None:
kwargs["user_project"] = user_project

metadata, secret = client.create_hmac_key(service_account_email=EMAIL, **kwargs)

self.assertIsInstance(metadata, HMACKeyMetadata)
Expand All @@ -1013,8 +1016,12 @@ def _create_hmac_key_helper(self, explicit_project=None):
"hmacKeys",
]
)
QS_PARAMS = {"serviceAccountEmail": EMAIL}
FULL_URI = "{}?{}".format(URI, urlencode(QS_PARAMS))
qs_params = {"serviceAccountEmail": EMAIL}

if user_project is not None:
qs_params["userProject"] = user_project

FULL_URI = "{}?{}".format(URI, urlencode(qs_params))
http.request.assert_called_once_with(
method="POST", url=FULL_URI, data=None, headers=mock.ANY
)
Expand All @@ -1025,6 +1032,9 @@ def test_create_hmac_key_defaults(self):
def test_create_hmac_key_explicit_project(self):
self._create_hmac_key_helper(explicit_project="other-project-456")

def test_create_hmac_key_user_project(self):
self._create_hmac_key_helper(user_project="billed-project")

def test_list_hmac_keys_defaults_empty(self):
PROJECT = "PROJECT"
CREDENTIALS = _make_credentials()
Expand Down Expand Up @@ -1060,6 +1070,7 @@ def test_list_hmac_keys_explicit_non_empty(self):
MAX_RESULTS = 3
EMAIL = "[email protected]"
ACCESS_ID = "ACCESS-ID"
USER_PROJECT = "billed-project"
CREDENTIALS = _make_credentials()
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)

Expand All @@ -1083,6 +1094,7 @@ def test_list_hmac_keys_explicit_non_empty(self):
service_account_email=EMAIL,
show_deleted_keys=True,
project_id=OTHER_PROJECT,
user_project=USER_PROJECT,
)
)

Expand All @@ -1107,6 +1119,7 @@ def test_list_hmac_keys_explicit_non_empty(self):
"maxResults": str(MAX_RESULTS),
"serviceAccountEmail": EMAIL,
"showDeletedKeys": "True",
"userProject": USER_PROJECT,
}
http.request.assert_called_once_with(
method="GET", url=mock.ANY, data=None, headers=mock.ANY
Expand Down Expand Up @@ -1160,12 +1173,14 @@ def test_get_hmac_key_metadata_wo_project(self):
)

def test_get_hmac_key_metadata_w_project(self):
from six.moves.urllib.parse import urlencode
from google.cloud.storage.hmac_key import HMACKeyMetadata

PROJECT = "PROJECT"
OTHER_PROJECT = "other-project-456"
EMAIL = "[email protected]"
ACCESS_ID = "ACCESS-ID"
USER_PROJECT = "billed-project"
CREDENTIALS = _make_credentials()
client = self._make_one(project=PROJECT, credentials=CREDENTIALS)

Expand All @@ -1179,7 +1194,9 @@ def test_get_hmac_key_metadata_w_project(self):
http = _make_requests_session([_make_json_response(resource)])
client._http_internal = http

metadata = client.get_hmac_key_metadata(ACCESS_ID, project_id=OTHER_PROJECT)
metadata = client.get_hmac_key_metadata(
ACCESS_ID, project_id=OTHER_PROJECT, user_project=USER_PROJECT
)

self.assertIsInstance(metadata, HMACKeyMetadata)
self.assertIs(metadata._client, client)
Expand All @@ -1197,6 +1214,10 @@ def test_get_hmac_key_metadata_w_project(self):
ACCESS_ID,
]
)

qs_params = {"userProject": USER_PROJECT}
FULL_URI = "{}?{}".format(URI, urlencode(qs_params))

http.request.assert_called_once_with(
method="GET", url=URI, data=None, headers=mock.ANY
method="GET", url=FULL_URI, data=None, headers=mock.ANY
)
Loading

0 comments on commit d8ce06e

Please sign in to comment.