Skip to content
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

Storage: add HMAC key support #8430

Merged
merged 15 commits into from
Aug 7, 2019
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
112 changes: 111 additions & 1 deletion storage/google/cloud/storage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from google.cloud.storage.batch import Batch
from google.cloud.storage.bucket import Bucket
from google.cloud.storage.blob import Blob
from google.cloud.storage.hmac_key import HMACKeyMetadata


_marker = object()
Expand Down Expand Up @@ -475,7 +476,7 @@ def list_blobs(
versions=versions,
projection=projection,
fields=fields,
client=self
client=self,
)

def list_buckets(
Expand Down Expand Up @@ -561,6 +562,98 @@ def list_buckets(
extra_params=extra_params,
)

def create_hmac_key(self, service_account_email, project_id=None):
"""Create an HMAC key for a service account.

:type service_account_email: str
:param service_account_email: e-mail address of the service account

:type project_id: str
:param project_id: (Optional) explicit project ID for the key.
Defaults to the client's project.

: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.
"""
if project_id is None:
project_id = self.project

path = "/projects/{}/hmacKeys".format(project_id)
qs_params = {"serviceAccountEmail": service_account_email}
api_response = self._connection.api_request(
method="POST", path=path, query_params=qs_params
)
metadata = HMACKeyMetadata(self)
metadata._properties = api_response["metadata"]
secret = api_response["secret"]
return metadata, secret

def list_hmac_keys(
self,
max_results=None,
service_account_email=None,
show_deleted_keys=None,
project_id=None,
):
"""List HMAC keys for a project.

:type max_results: int
:param max_results:
(Optional) max number of keys to return in a given page.

:type service_account_email: str
:param service_account_email:
(Optional) limit keys to those created by the given service account.

:type show_deleted_keys: bool
:param show_deleted_keys:
(Optional) included deleted keys in the list. Default is to
exclude them.

:type project_id: str
:param project_id: (Optional) explicit project ID for the key.
Defaults to the client's project.

: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.
"""
if project_id is None:
project_id = self.project

path = "/projects/{}/hmacKeys".format(project_id)
extra_params = {}

if service_account_email is not None:
extra_params["serviceAccountEmail"] = service_account_email

if show_deleted_keys is not None:
extra_params["showDeletedKeys"] = show_deleted_keys

return page_iterator.HTTPIterator(
client=self,
api_request=self._connection.api_request,
path=path,
item_to_value=_item_to_hmac_key_metadata,
max_results=max_results,
extra_params=extra_params,
)

def get_hmac_key_metadata(self, access_id, project_id=None):
"""Return a metadata instance for the given HMAC key.

:type access_id: str
:param access_id: Unique ID of an existing key.

:type project_id: str
:param project_id: (Optional) project ID of an existing key.
Defaults to client's project.
"""
metadata = HMACKeyMetadata(self, access_id, project_id)
metadata.reload() # raises NotFound for missing key
return metadata


def _item_to_bucket(iterator, item):
"""Convert a JSON bucket to the native object.
Expand All @@ -578,3 +671,20 @@ def _item_to_bucket(iterator, item):
bucket = Bucket(iterator.client, name)
bucket._set_properties(item)
return bucket


def _item_to_hmac_key_metadata(iterator, item):
"""Convert a JSON key metadata resource to the native object.

:type iterator: :class:`~google.api_core.page_iterator.Iterator`
:param iterator: The iterator that has retrieved the item.

:type item: dict
:param item: An item to be converted to a key metadata instance.

:rtype: :class:`~google.cloud.storage.hmac_key.HMACKeyMetadata`
:returns: The next key metadata instance in the page.
"""
metadata = HMACKeyMetadata(iterator.client)
metadata._properties = item
return metadata
207 changes: 207 additions & 0 deletions storage/google/cloud/storage/hmac_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# Copyright 2019 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
#
# http://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.

from google.cloud.exceptions import NotFound
from google.cloud._helpers import _rfc3339_to_datetime


class HMACKeyMetadata(object):
"""Metadata about an HMAC service account key withn Cloud Storage.

:type client: :class:`~google.cloud.stoage.client.Client`
:param client: client associated with the key metadata.

:type access_id: str
:param access_id: (Optional) unique ID of an existing key.

:type project_id: str
:param project_id: (Optional) project ID of an existing key.
Defaults to client's project.
"""

ACTIVE_STATE = "ACTIVE"
"""Key is active, and may be used to sign requests."""
INACTIVE_STATE = "INACTIVE"
"""Key is inactive, and may not be used to sign requests.

It can be re-activated via :meth:`update`.
"""
DELETED_STATE = "DELETED"
"""Key is deleted. It cannot be re-activated."""

_SETTABLE_STATES = (ACTIVE_STATE, INACTIVE_STATE)

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

if access_id is not None:
self._properties["accessId"] = access_id

if project_id is not None:
self._properties["projectId"] = project_id

def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented

return self._client == other._client and self.access_id == other.access_id

def __hash__(self):
return hash(self._client) + hash(self.access_id)

@property
def access_id(self):
"""ID of the key.

:rtype: str or None
:returns: unique identifier of the key within a project.
"""
return self._properties.get("accessId")

@property
def etag(self):
"""ETag identifying the version of the key metadata.

:rtype: str or None
:returns: ETag for the version of the key's metadata.
"""
return self._properties.get("etag")

@property
def project(self):
"""Project ID associated with the key.

:rtype: str or None
:returns: project identfier for the key.
"""
return self._properties.get("projectId")

@property
def service_account_email(self):
"""Service account e-mail address associated with the key.

:rtype: str or None
:returns: e-mail address for the service account which created the key.
"""
return self._properties.get("serviceAccountEmail")

@property
def state(self):
"""Get / set key's state.

One of:
- ``ACTIVE``
- ``INACTIVE``
- ``DELETED``

:rtype: str or None
:returns: key's current state.
"""
return self._properties.get("state")

@state.setter
def state(self, value):
if value not in self._SETTABLE_STATES:
raise ValueError(
"State may only be set to one of: {}".format(
", ".join(self._SETTABLE_STATES)
)
)

self._properties["state"] = value

@property
def time_created(self):
"""Retrieve the timestamp at which the HMAC key was created.

:rtype: :class:`datetime.datetime` or ``NoneType``
:returns: Datetime object parsed from RFC3339 valid timestamp, or
``None`` if the bucket's resource has not been loaded
from the server.
"""
value = self._properties.get("timeCreated")
if value is not None:
return _rfc3339_to_datetime(value)

@property
def updated(self):
"""Retrieve the timestamp at which the HMAC key was created.

:rtype: :class:`datetime.datetime` or ``NoneType``
:returns: Datetime object parsed from RFC3339 valid timestamp, or
``None`` if the bucket's resource has not been loaded
from the server.
"""
value = self._properties.get("updated")
if value is not None:
return _rfc3339_to_datetime(value)

@property
def path(self):
"""Resource path for the metadata's key."""

if self.access_id is None:
raise ValueError("No 'access_id' set.")

project = self.project
if project is None:
project = self._client.project

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

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)
except NotFound:
return False
else:
return True

def reload(self):
"""Reload properties from Cloud Storage.

:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
self._properties = self._client._connection.api_request(
method="GET", path=self.path
)

def update(self):
"""Save writable properties to Cloud Storage.

:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
payload = {"state": self.state}
self._properties = self._client._connection.api_request(
method="PUT", path=self.path, data=payload
)

def delete(self):
"""Delete the key from Cloud Storage.

:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
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)
Loading