gslib/commands/hmac.py (289 lines of code) (raw):
# -*- coding: utf-8 -*-
# Copyright 2019 Google Inc. All Rights Reserved.
#
# 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.
"""
Implementation of HMAC key management command for GCS.
NOTE: Any modification to this file or corresponding HMAC logic
should be submitted in its own PR and release to avoid
concurrency issues in testing.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from gslib.command import Command
from gslib.command_argument import CommandArgument
from gslib.cs_api_map import ApiSelector
from gslib.exception import CommandException
from gslib.help_provider import CreateHelpText
from gslib.metrics import LogCommandParams
from gslib.project_id import PopulateProjectId
from gslib.utils.cloud_api_helper import GetCloudApiInstance
from gslib.utils.shim_util import GcloudStorageFlag
from gslib.utils.shim_util import GcloudStorageMap
from gslib.utils.text_util import InsistAscii
from gslib.utils import shim_util
_CREATE_SYNOPSIS = """
gsutil hmac create [-p <project>] <service_account_email>
"""
_DELETE_SYNOPSIS = """
gsutil hmac delete [-p <project>] <access_id>
"""
_GET_SYNOPSIS = """
gsutil hmac get [-p <project>] <access_id>
"""
_LIST_SYNOPSIS = """
gsutil hmac list [-a] [-l] [-p <project>] [-u <service_account_email>]
"""
_UPDATE_SYNOPSIS = """
gsutil hmac update -s (ACTIVE|INACTIVE) [-e <etag>] [-p <project>] <access_id>
"""
_CREATE_DESCRIPTION = """
<B>CREATE</B>
The ``hmac create`` command creates an HMAC key for the specified service
account:
gsutil hmac create test.service.account@test_project.iam.gserviceaccount.com
The secret key material is only available upon creation, so be sure to store
the returned secret along with the access_id.
<B>CREATE OPTIONS</B>
The ``create`` sub-command has the following option
-p <project> Specify the ID or number of the project in which
to create a key.
"""
_DELETE_DESCRIPTION = """
<B>DELETE</B>
The ``hmac delete`` command permanently deletes the specified HMAC key:
gsutil hmac delete GOOG56JBMFZX6PMPTQ62VD2
Note that keys must be updated to be in the ``INACTIVE`` state before they can be
deleted.
<B>DELETE OPTIONS</B>
The ``delete`` sub-command has the following option
-p <project> Specify the ID or number of the project from which to
delete a key.
"""
_GET_DESCRIPTION = """
<B>GET</B>
The ``hmac get`` command retrieves the specified HMAC key's metadata:
gsutil hmac get GOOG56JBMFZX6PMPTQ62VD2
Note that there is no option to retrieve a key's secret material after it has
been created.
<B>GET OPTIONS</B>
The ``get`` sub-command has the following option
-p <project> Specify the ID or number of the project from which to
get a key.
"""
_LIST_DESCRIPTION = """
<B>LIST</B>
The ``hmac list`` command lists the HMAC key metadata for keys in the
specified project. If no project is specified in the command, the default
project is used.
<B>LIST OPTIONS</B>
The ``list`` sub-command has the following options
-a Show all keys, including recently deleted
keys.
-l Use long listing format. Shows each key's full
metadata excluding the secret.
-p <project> Specify the ID or number of the project from
which to list keys.
-u <service_account_email> Filter keys for a single service account.
"""
_UPDATE_DESCRIPTION = """
<B>UPDATE</B>
The ``hmac update`` command sets the state of the specified key:
gsutil hmac update -s INACTIVE -e M42da= GOOG56JBMFZX6PMPTQ62VD2
Valid state arguments are ``ACTIVE`` and ``INACTIVE``. To set a key to state
``DELETED``, use the ``hmac delete`` command on an ``INACTIVE`` key. If an etag
is set in the command, it will only succeed if the provided etag matches the etag
of the stored key.
<B>UPDATE OPTIONS</B>
The ``update`` sub-command has the following options
-s <ACTIVE|INACTIVE> Sets the state of the specified key to either
``ACTIVE`` or ``INACTIVE``.
-e <etag> If provided, the update will only be performed
if the specified etag matches the etag of the
stored key.
-p <project> Specify the ID or number of the project in
which to update a key.
"""
_SYNOPSIS = (_CREATE_SYNOPSIS + _DELETE_SYNOPSIS.lstrip('\n') +
_GET_SYNOPSIS.lstrip('\n') + _LIST_SYNOPSIS.lstrip('\n') +
_UPDATE_SYNOPSIS.lstrip('\n') + '\n\n')
_DESCRIPTION = """
You can use the ``hmac`` command to interact with service account `HMAC keys
<https://cloud.google.com/storage/docs/authentication/hmackeys>`_.
The ``hmac`` command has five sub-commands:
""" + '\n'.join([
_CREATE_DESCRIPTION,
_DELETE_DESCRIPTION,
_GET_DESCRIPTION,
_LIST_DESCRIPTION,
_UPDATE_DESCRIPTION,
])
_DETAILED_HELP_TEXT = CreateHelpText(_SYNOPSIS, _DESCRIPTION)
_VALID_UPDATE_STATES = ['INACTIVE', 'ACTIVE']
_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
_create_help_text = CreateHelpText(_CREATE_SYNOPSIS, _CREATE_DESCRIPTION)
_delete_help_text = CreateHelpText(_DELETE_SYNOPSIS, _DELETE_DESCRIPTION)
_get_help_text = CreateHelpText(_GET_SYNOPSIS, _GET_DESCRIPTION)
_list_help_text = CreateHelpText(_LIST_SYNOPSIS, _LIST_DESCRIPTION)
_update_help_text = CreateHelpText(_UPDATE_SYNOPSIS, _UPDATE_DESCRIPTION)
def _AccessIdException(command_name, subcommand, synopsis):
return CommandException(
'%s %s requires an Access ID to be specified as the last argument.\n%s' %
(command_name, subcommand, synopsis))
def _KeyMetadataOutput(metadata):
"""Format the key metadata for printing to the console."""
def FormatInfo(name, value, new_line=True):
"""Format the metadata name-value pair into two aligned columns."""
width = 22
info_str = '\t%-*s %s' % (width, name + ':', value)
if new_line:
info_str += '\n'
return info_str
message = 'Access ID %s:\n' % metadata.accessId
message += FormatInfo('State', metadata.state)
message += FormatInfo('Service Account', metadata.serviceAccountEmail)
message += FormatInfo('Project', metadata.projectId)
message += FormatInfo('Time Created',
metadata.timeCreated.strftime(_TIME_FORMAT))
message += FormatInfo('Time Last Updated',
metadata.updated.strftime(_TIME_FORMAT))
message += FormatInfo('Etag', metadata.etag, new_line=False)
return message
_CREATE_COMMAND_FORMAT = ('--format=value[separator="' +
shim_util.get_format_flag_newline() + '"](' +
'format("Access ID: {}", metadata.accessId),' +
'format("Secret: {}", secret))')
_DESCRIBE_COMMAND_FORMAT = (
'--format=value[separator="' + shim_util.get_format_flag_newline() +
'"](format("Access ID {}:", accessId),' +
'format("\tState: {}", state),' +
'format("\tService Account: {}", serviceAccountEmail),' +
'format("\tProject: {}", projectId),' +
'format("\tTime Created: {}",' +
' timeCreated.date(format="%a\',\' %d %b %Y %H:%M:%S GMT")),' +
'format("\tTime Last Updated: {}",' +
' updated.date(format="%a\',\' %d %b %Y %H:%M:%S GMT")),' +
'format("\tEtag: {}", etag))')
_LIST_COMMAND_SHORT_FORMAT = (
'--format=table[no-heading](format("{}\t{:<12} {}",'
'accessId, state, serviceAccountEmail))')
_PROJECT_FLAG = GcloudStorageFlag('--project')
CREATE_COMMAND = GcloudStorageMap(
gcloud_command=['storage', 'hmac', 'create', _CREATE_COMMAND_FORMAT],
flag_map={
'-p': _PROJECT_FLAG,
})
DELETE_COMMAND = GcloudStorageMap(gcloud_command=['storage', 'hmac', 'delete'],
flag_map={
'-p': _PROJECT_FLAG,
})
GET_COMMAND = GcloudStorageMap(
gcloud_command=['storage', 'hmac', 'describe', _DESCRIBE_COMMAND_FORMAT],
flag_map={'-p': _PROJECT_FLAG})
LIST_COMMAND = GcloudStorageMap(
gcloud_command=['storage', 'hmac', 'list', _LIST_COMMAND_SHORT_FORMAT],
flag_map={
'-a': GcloudStorageFlag('--all'),
'-u': GcloudStorageFlag('--service-account'),
'-p': _PROJECT_FLAG
})
LIST_COMMAND_LONG_FORMAT = GcloudStorageMap(
gcloud_command=['storage', 'hmac', 'list', _DESCRIBE_COMMAND_FORMAT],
flag_map={
'-a': GcloudStorageFlag('--all'),
'-l': GcloudStorageFlag('--long'),
'-u': GcloudStorageFlag('--service-account'),
'-p': _PROJECT_FLAG
})
UPDATE_COMMAND = GcloudStorageMap(
gcloud_command=['storage', 'hmac', 'update', _DESCRIBE_COMMAND_FORMAT],
flag_map={
'-s':
GcloudStorageFlag({
'ACTIVE': '--activate',
'INACTIVE': '--deactivate',
}),
'-e':
GcloudStorageFlag('--etag'),
'-p':
_PROJECT_FLAG
})
class HmacCommand(Command):
"""Implementation of gsutil hmac command."""
command_spec = Command.CreateCommandSpec(
'hmac',
min_args=1,
max_args=8,
supported_sub_args='ae:lp:s:u:',
file_url_ok=True,
urls_start_arg=1,
gs_api_support=[ApiSelector.JSON],
gs_default_api=ApiSelector.JSON,
usage_synopsis=_SYNOPSIS,
argparse_arguments={
'create': [CommandArgument.MakeZeroOrMoreCloudOrFileURLsArgument()],
'delete': [CommandArgument.MakeZeroOrMoreCloudOrFileURLsArgument()],
'get': [CommandArgument.MakeZeroOrMoreCloudOrFileURLsArgument()],
'list': [CommandArgument.MakeZeroOrMoreCloudOrFileURLsArgument()],
'update': [CommandArgument.MakeZeroOrMoreCloudOrFileURLsArgument()],
},
)
help_spec = Command.HelpSpec(
help_name='hmac',
help_name_aliases=[],
help_type='command_help',
help_one_line_summary=('CRUD operations on service account HMAC keys.'),
help_text=_DETAILED_HELP_TEXT,
subcommand_help_text={
'create': _create_help_text,
'delete': _delete_help_text,
'get': _get_help_text,
'list': _list_help_text,
'update': _update_help_text,
})
def get_gcloud_storage_args(self):
if self.args[0] == 'list' and '-l' in self.args:
gcloud_storage_map = GcloudStorageMap(
gcloud_command={'list': LIST_COMMAND_LONG_FORMAT},
flag_map={},
)
else:
gcloud_storage_map = GcloudStorageMap(
gcloud_command={
'create': CREATE_COMMAND,
'delete': DELETE_COMMAND,
'update': UPDATE_COMMAND,
'get': GET_COMMAND,
'list': LIST_COMMAND
},
flag_map={},
)
return super().get_gcloud_storage_args(gcloud_storage_map)
def _CreateHmacKey(self, thread_state=None):
"""Creates HMAC key for a service account."""
if self.args:
self.service_account_email = self.args[0]
else:
err_msg = ('%s %s requires a service account to be specified as the '
'last argument.\n%s')
raise CommandException(
err_msg %
(self.command_name, self.action_subcommand, _CREATE_SYNOPSIS))
gsutil_api = GetCloudApiInstance(self, thread_state=thread_state)
response = gsutil_api.CreateHmacKey(self.project_id,
self.service_account_email,
provider='gs')
print('%-12s %s' % ('Access ID:', response.metadata.accessId))
print('%-12s %s' % ('Secret:', response.secret))
def _DeleteHmacKey(self, thread_state=None):
"""Deletes an HMAC key."""
if self.args:
access_id = self.args[0]
else:
raise _AccessIdException(self.command_name, self.action_subcommand,
_DELETE_SYNOPSIS)
gsutil_api = GetCloudApiInstance(self, thread_state=thread_state)
gsutil_api.DeleteHmacKey(self.project_id, access_id, provider='gs')
def _GetHmacKey(self, thread_state=None):
"""Gets HMAC key from its Access Id."""
if self.args:
access_id = self.args[0]
else:
raise _AccessIdException(self.command_name, self.action_subcommand,
_GET_SYNOPSIS)
gsutil_api = GetCloudApiInstance(self, thread_state=thread_state)
response = gsutil_api.GetHmacKey(self.project_id, access_id, provider='gs')
print(_KeyMetadataOutput(response))
def _ListHmacKeys(self, thread_state=None):
"""Lists HMAC keys for a project or service account."""
if self.args:
raise CommandException(
'%s %s received unexpected arguments.\n%s' %
(self.command_name, self.action_subcommand, _LIST_SYNOPSIS))
gsutil_api = GetCloudApiInstance(self, thread_state=thread_state)
response = gsutil_api.ListHmacKeys(self.project_id,
self.service_account_email,
self.show_all,
provider='gs')
short_list_format = '%s\t%-12s %s'
if self.long_list:
for item in response:
print(_KeyMetadataOutput(item))
print()
else:
for item in response:
print(short_list_format %
(item.accessId, item.state, item.serviceAccountEmail))
def _UpdateHmacKey(self, thread_state=None):
"""Update an HMAC key's state."""
if not self.state:
raise CommandException(
'A state flag must be supplied for %s %s\n%s' %
(self.command_name, self.action_subcommand, _UPDATE_SYNOPSIS))
elif self.state not in _VALID_UPDATE_STATES:
raise CommandException('The state flag value must be one of %s' %
', '.join(_VALID_UPDATE_STATES))
if self.args:
access_id = self.args[0]
else:
raise _AccessIdException(self.command_name, self.action_subcommand,
_UPDATE_SYNOPSIS)
gsutil_api = GetCloudApiInstance(self, thread_state=thread_state)
response = gsutil_api.UpdateHmacKey(self.project_id,
access_id,
self.state,
self.etag,
provider='gs')
print(_KeyMetadataOutput(response))
def RunCommand(self):
"""Command entry point for the hmac command."""
if self.gsutil_api.GetApiSelector(provider='gs') != ApiSelector.JSON:
raise CommandException(
'The "hmac" command can only be used with the GCS JSON API')
self.action_subcommand = self.args.pop(0)
self.ParseSubOpts(check_args=True)
# Commands with both suboptions and subcommands need to reparse for
# suboptions, so we log again.
LogCommandParams(sub_opts=self.sub_opts)
self.service_account_email = None
self.state = None
self.show_all = False
self.long_list = False
self.etag = None
if self.sub_opts:
for o, a in self.sub_opts:
if o == '-u':
self.service_account_email = a
elif o == '-p':
# Project IDs are sent as header values when using gs and s3 XML APIs.
InsistAscii(a, 'Invalid non-ASCII character found in project ID')
self.project_id = a
elif o == '-s':
self.state = a
elif o == '-a':
self.show_all = True
elif o == '-l':
self.long_list = True
elif o == '-e':
self.etag = a
if not self.project_id:
self.project_id = PopulateProjectId(None)
method_for_arg = {
'create': self._CreateHmacKey,
'delete': self._DeleteHmacKey,
'get': self._GetHmacKey,
'list': self._ListHmacKeys,
'update': self._UpdateHmacKey,
}
if self.action_subcommand not in method_for_arg:
raise CommandException('Invalid subcommand "%s" for the %s command.\n'
'See "gsutil help hmac".' %
(self.action_subcommand, self.command_name))
LogCommandParams(subcommands=[self.action_subcommand])
method_for_arg[self.action_subcommand]()
return 0