Introducing barbican-manage utility command

A new 'barbican-manage' utility command is introduced as Barbican
admin tool. This command interacts with Barbican service for
management operations which usually cannot be accomplished with
REST APIs. This can improve usability and extensibility in the
future.

The related blueprint is https://review.openstack.org/#/c/253719/

This CR includes
1) implementation of barbican_manage.py
2) unit test code
3) document of barbican-manage command

Co-Authored-By: Michael Perng <mperng@us.ibm.com>
Change-Id: I784b46df86742d00d1737e3f8964280514a7fa1b
This commit is contained in:
Jeff Feng 2016-02-18 14:54:24 -06:00
parent 75f570097a
commit 77a164b062
5 changed files with 506 additions and 0 deletions

300
barbican/cmd/barbican_manage.py Executable file
View File

@ -0,0 +1,300 @@
#!/usr/bin/env python
# Copyright 2010-2015 OpenStack LLC.
# 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.
"""
CLI interface for barbican management
"""
from __future__ import print_function
import argparse
import six
import sys
from oslo_config import cfg
from oslo_log import log as logging
from barbican.cmd import pkcs11_kek_rewrap as pkcs11_rewrap
from barbican.common import config
from barbican.model.migration import commands
from barbican.plugin.crypto import pkcs11
import barbican.version
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
# Decorators for actions
def args(*args, **kwargs):
def _decorator(func):
func.__dict__.setdefault('args', []).insert(0, (args, kwargs))
return func
return _decorator
class DbCommands(object):
"""Class for managing barbican database"""
description = "Subcommands for managing barbican database"
cleanup_description = "Cleanup soft-deleted secrets in database"
@args('--db-url', '-d', metavar='<db-url>', dest='dburl',
help='barbican database URL')
def cleanup(self, dburl=None):
raise NotImplementedError
revision_description = "Create a new database version file"
@args('--db-url', '-d', metavar='<db-url>', dest='dburl',
help='barbican database URL')
@args('--message', '-m', metavar='<message>', default='DB change',
help='the message for the DB change')
@args('--autogenerate', action="store_true", dest='autogen',
default=False, help='autogenerate from models')
def revision(self, dburl=None, message=None, autogen=None):
"""Process the 'revision' Alembic command."""
if dburl is None:
commands.generate(autogenerate=autogen, message=str(message),
sql_url=CONF.sql_connection)
else:
commands.generate(autogenerate=autogen, message=str(message),
sql_url=str(dburl))
upgrade_description = "Upgrade to a future database version"
@args('--db-url', '-d', metavar='<db-url>', dest='dburl',
help='barbican database URL')
@args('--version', '-v', metavar='<version>', default='head',
help='the version to upgrade to, or else '
'the latest/head if not specified.')
def upgrade(self, dburl=None, version=None):
"""Process the 'upgrade' Alembic command."""
if dburl is None:
commands.upgrade(to_version=str(version),
sql_url=CONF.sql_connection)
else:
commands.upgrade(to_version=str(version), sql_url=str(dburl))
history_description = "Show database changset history"
@args('--db-url', '-d', metavar='<db-url>', dest='dburl',
help='barbican database URL')
@args('--verbose', '-V', action='store_true', dest='verbose',
default=False, help='Show full information about the revisions.')
def history(self, dburl=None, verbose=None):
if dburl is None:
commands.history(verbose, sql_url=CONF.sql_connection)
else:
commands.history(verbose, sql_url=str(dburl))
current_description = "Show current revision of database"
@args('--db-url', '-d', metavar='<db-url>', dest='dburl',
help='barbican database URL')
@args('--verbose', '-V', action='store_true', dest='verbose',
default=False, help='Show full information about the revisions.')
def current(self, dburl=None, verbose=None):
if dburl is None:
commands.current(verbose, sql_url=CONF.sql_connection)
else:
commands.current(verbose, sql_url=str(dburl))
class HSMCommands(object):
"""Class for managing HSM/pkcs11 plugin"""
description = "Subcommands for managing HSM/PKCS11"
gen_mkek_description = "Generates a new MKEK"
@args('--library-path', metavar='<library-path>', dest='libpath',
default='/usr/lib/libCryptoki2_64.so',
help='Path to vendor PKCS11 library')
@args('--slot-id', metavar='<slot-id>', dest='slotid', default=1,
help='HSM Slot id (Should correspond to a configured PKCS11 slot, \
default is 1)')
@args('--passphrase', metavar='<passphrase>', default=None, required=True,
help='Password to login to PKCS11 session')
@args('--label', '-L', metavar='<label>', default='primarymkek',
help='The label of the Master Key Encrypt Key')
@args('--length', '-l', metavar='<length>', default=32,
help='The length of the Master Key Encrypt Key (default is 32)')
def gen_mkek(self, passphrase, libpath=None, slotid=None, label=None,
length=None):
self._create_pkcs11_session(str(passphrase), str(libpath), int(slotid))
self._verify_label_does_not_exist(str(label), self.session)
self.pkcs11.generate_key(int(length), self.session, str(label),
encrypt=True, wrap=True, master_key=True)
self.pkcs11.return_session(self.session)
print ("MKEK successfully generated!")
gen_hmac_description = "Generates a new HMAC key"
@args('--library-path', metavar='<library-path>', dest='libpath',
default='/usr/lib/libCryptoki2_64.so',
help='Path to vendor PKCS11 library')
@args('--slot-id', metavar='<slot-id>', dest='slotid', default=1,
help='HSM Slot id (Should correspond to a configured PKCS11 slot, \
default is 1)')
@args('--passphrase', metavar='<passphrase>', default=None, required=True,
help='Password to login to PKCS11 session')
@args('--label', '-L', metavar='<label>', default='primarymkek',
help='The label of the Matser HMAC Key')
@args('--length', '-l', metavar='<length>', default=32,
help='The length of the Master HMAC Key (default is 32)')
def gen_hmac(self, passphrase, libpath=None, slotid=None, label=None,
length=None):
self._create_pkcs11_session(str(passphrase), str(libpath), int(slotid))
self._verify_label_does_not_exist(str(label), self.session)
self.pkcs11.generate_key(int(length), self.session, str(label),
sign=True, master_key=True)
self.pkcs11.return_session(self.session)
print ("HMAC successfully generated!")
rewrap_pkek_description = "Re-wrap project MKEKs"
@args('--dry-run', action="store_true", dest='dryrun', default=False,
help='Displays changes that will be made (Non-destructive)')
def rewrap_pkek(self, dryrun=None):
rewrapper = pkcs11_rewrap.KekRewrap(pkcs11_rewrap.CONF)
rewrapper.execute(dryrun)
rewrapper.pkcs11.return_session(rewrapper.hsm_session)
def _create_pkcs11_session(self, passphrase, libpath, slotid):
self.pkcs11 = pkcs11.PKCS11(
library_path=libpath, login_passphrase=passphrase,
rw_session=True, slot_id=slotid
)
self.session = self.pkcs11.get_session()
def _verify_label_does_not_exist(self, label, session):
key_handle = self.pkcs11.get_key_handle(label, session)
if key_handle:
print (
"The label {label} already exists! "
"Please try again.".format(label=label)
)
sys.exit(1)
CATEGORIES = {
'db': DbCommands,
'hsm': HSMCommands,
}
# Modifying similiar code from nova/cmd/manage.py
def methods_of(obj):
"""Get all callable methods of an object that don't start with underscore
returns a list of tuples of the form (method_name, method)
"""
result = []
for fn in dir(obj):
if callable(getattr(obj, fn)) and not fn.startswith('_'):
result.append((fn, getattr(obj, fn),
getattr(obj, fn+'_description', None)))
return result
# Shamelessly taking same code from nova/cmd/manage.py
def add_command_parsers(subparsers):
"""Add subcommand parser to oslo_config object"""
for category in CATEGORIES:
command_object = CATEGORIES[category]()
desc = getattr(command_object, 'description', None)
parser = subparsers.add_parser(category, description=desc)
parser.set_defaults(command_object=command_object)
category_subparsers = parser.add_subparsers(dest='action')
for (action, action_fn, action_desc) in methods_of(command_object):
parser = category_subparsers.add_parser(action,
description=action_desc)
action_kwargs = []
for args, kwargs in getattr(action_fn, 'args', []):
# Assuming dest is the arg name without the leading
# hyphens if no dest is supplied
kwargs.setdefault('dest', args[0][2:])
if kwargs['dest'].startswith('action_kwarg_'):
action_kwargs.append(
kwargs['dest'][len('action_kwarg_'):])
else:
action_kwargs.append(kwargs['dest'])
kwargs['dest'] = 'action_kwarg_' + kwargs['dest']
parser.add_argument(*args, **kwargs)
parser.set_defaults(action_fn=action_fn)
parser.set_defaults(action_kwargs=action_kwargs)
parser.add_argument('action_args', nargs='*',
help=argparse.SUPPRESS)
# Define subcommand category
category_opt = cfg.SubCommandOpt('category',
title='Command categories',
help='Available categories',
handler=add_command_parsers)
def main():
"""Parse options and call the appropriate class/method."""
CONF = config.new_config()
CONF.register_cli_opt(category_opt)
try:
logging.register_options(CONF)
logging.setup(CONF, "barbican-manage")
cfg_files = cfg.find_config_files(project='barbican')
CONF(args=sys.argv[1:],
project='barbican',
prog='barbican-manage',
version=barbican.version.__version__,
default_config_files=cfg_files)
except RuntimeError as e:
sys.exit("ERROR: %s" % e)
# find sub-command and its arguments
fn = CONF.category.action_fn
fn_args = [arg.decode('utf-8') for arg in CONF.category.action_args]
fn_kwargs = {}
for k in CONF.category.action_kwargs:
v = getattr(CONF.category, 'action_kwarg_' + k)
if v is None:
continue
if isinstance(v, six.string_types):
v = v.decode('utf-8')
fn_kwargs[k] = v
# call the action with the remaining arguments
try:
ret = fn(*fn_args, **fn_kwargs)
return(ret)
except Exception as e:
sys.exit("ERROR: %s" % e)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,125 @@
# Copyright (c) 2015 Rackspace, Inc.
#
# 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.
import fixtures
import mock
from barbican.cmd import barbican_manage as manager
from barbican.tests import utils
class TestBarbicanManageBase(utils.BaseTestCase):
def setUp(self):
super(TestBarbicanManageBase, self).setUp()
def clear_conf():
manager.CONF.reset()
manager.CONF.unregister_opt(manager.category_opt)
clear_conf()
self.addCleanup(clear_conf)
self.useFixture(fixtures.MonkeyPatch(
'oslo_log.log.setup', lambda barbican_test, version='test': None))
manager.CONF.set_override('sql_connection', 'mockdburl')
def _main_test_helper(self, argv, func_name=None, *exp_args, **exp_kwargs):
self.useFixture(fixtures.MonkeyPatch('sys.argv', argv))
manager.main()
func_name.assert_called_once_with(*exp_args, **exp_kwargs)
class TestBarbicanManage(TestBarbicanManageBase):
"""Test barbican-manage functionality."""
@mock.patch('barbican.model.migration.commands.generate')
def test_db_revision(self, mock_generate):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'revision', '--db-url',
'mockdb', '--message', 'mockmsg'], mock_generate,
autogenerate=False, message='mockmsg', sql_url='mockdb')
@mock.patch('barbican.model.migration.commands.generate')
def test_db_revision_autogenerate(self, mock_generate):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'revision', '--db-url',
'mockdb', '--message', 'mockmsg', '--autogenerate'],
mock_generate, autogenerate=True, message='mockmsg',
sql_url='mockdb')
@mock.patch('barbican.model.migration.commands.generate')
def test_db_revision_no_dburl(self, mock_generate):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'revision', '--message',
'mockmsg'], mock_generate, autogenerate=False, message='mockmsg',
sql_url='mockdburl')
@mock.patch('barbican.model.migration.commands.upgrade')
def test_db_upgrade(self, mock_upgrade):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'upgrade', '--db-url',
'mockdb'], mock_upgrade, to_version='head', sql_url='mockdb')
@mock.patch('barbican.model.migration.commands.upgrade')
def test_db_upgrade_no_dburl(self, mock_upgrade):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'upgrade'], mock_upgrade,
to_version='head', sql_url='mockdburl')
@mock.patch('barbican.model.migration.commands.history')
def test_db_history(self, mock_history):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'history', '--db-url',
'mockdb'], mock_history, False, sql_url='mockdb')
@mock.patch('barbican.model.migration.commands.history')
def test_db_history_no_dburl(self, mock_history):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'history'], mock_history,
False, sql_url='mockdburl')
@mock.patch('barbican.model.migration.commands.current')
def test_db_current(self, mock_current):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'current', '--db-url',
'mockdb'], mock_current, False, sql_url='mockdb')
@mock.patch('barbican.model.migration.commands.current')
def test_db_current_no_dburl(self, mock_current):
self._main_test_helper(
['barbican.cmd.barbican_manage', 'db', 'current'], mock_current,
False, sql_url='mockdburl')
@mock.patch('barbican.plugin.crypto.pkcs11.PKCS11')
def test_hsm_gen_mkek(self, mock_pkcs11):
mock_pkcs11.return_value.get_session.return_value = long(1)
mock_pkcs11.return_value.get_key_handle.return_value = None
mock_pkcs11.return_value.generate_key.return_value = long(0)
mock_genkey = mock_pkcs11.return_value.generate_key
self._main_test_helper(
['barbican.cmd.barbican_manage', 'hsm', 'gen_mkek',
'--library-path', 'mocklib', '--passphrase', 'mockpassewd',
'--label', 'mocklabel'], mock_genkey,
32, 1, 'mocklabel', encrypt=True, wrap=True, master_key=True)
@mock.patch('barbican.plugin.crypto.pkcs11.PKCS11')
def test_hsm_gen_hmac(self, mock_pkcs11):
mock_pkcs11.return_value.get_session.return_value = long(1)
mock_pkcs11.return_value.get_key_handle.return_value = None
mock_pkcs11.return_value.generate_key.return_value = long(0)
mock_genkey = mock_pkcs11.return_value.generate_key
self._main_test_helper(
['barbican.cmd.barbican_manage', 'hsm', 'gen_hmac',
'--library-path', 'mocklib', '--passphrase', 'mockpassewd',
'--label', 'mocklabel'], mock_genkey,
32, 1, 'mocklabel', sign=True, master_key=True)

View File

@ -0,0 +1,78 @@
===================================
Barbican Service Management Utility
===================================
Description
===========
``barbican-manage`` is a utility that is used to control the barbican key
manager service database and Hardware Secure Module (HSM) plugin device. Use
cases include migrating the secret database or generating a Master Key
Encryption Key (MKEK) in the HSM. This command set should only be executed by
a user with admin privileges.
Options
=======
The standard pattern for executing a barbican-manage command is:
``barbican-manage <category> <command> [<args>]``
Running ``barbican-manage`` without arguments shows a list of available command
categories. Currently, there are 2 supported categories: *db* and *hsm*.
Running with a category argument shows a list of commands in that category:
* ``barbican-manage db --help``
* ``barbican-manage hsm --help``
* ``barbican-manage --version`` shows the version number of barbican service.
The following sections describe the available categories and arguments for
barbican-manage.
Barbican Database
~~~~~~~~~~~~~~~~~
.. Warning::
Before executing **barbican-manage db** commands, make sure you are
familiar with `Database Migration`_ first.
``barbican-manage db revision [--db-url] [--message] [--autogenerate]``
Create a new database version file.
``barbican-manage db upgrade [--db-url] [--version]``
Upgrade to a future version database.
``barbican-manage db history [--db-url] [--verbose]``
Show database changeset history.
``barbican-manage db current [--db-url] [--verbose]``
Show current revision of database.
Barbican PKCS11/HSM
~~~~~~~~~~~~~~~~~~~
``barbican-manage hsm gen_mkek [--library-path] [--passphrase] [--slot-id] [--label] [--length]``
Create a new Master key encryption key in HSM.
This MKEK will be used to encrypt all project key encryption keys.
Its label must be unique.
``barbican-manage hsm gen_hmac [--library-path] [--passphrase] [--slot-id] [--label] [--length]``
Create a new Master HMAC key in HSM.
This HMAC key will be used to generate an authentication tag of encrypted
project key encryption keys. Its label must be unique.
``barbican-manage hsm rewrap_pkek [--dry-run]``
Rewrap project key encryption keys after rotating to new MKEK and/or HMAC
key(s) in HSM. The new MKEK and HMAC key should have already been generated
using the above commands. The user will have to configure new MKEK and HMAC
key labels in /etc/barbican.conf and restart barbican server before
executing this command.
.. _Database Migration: http://docs.openstack.org/developer/barbican/contribute/database_migrations.html

View File

@ -10,3 +10,5 @@ management of secrets.
:maxdepth: 1
access_control.rst
barbican_manage.rst

View File

@ -22,6 +22,7 @@ packages =
[entry_points]
console_scripts =
barbican-manage = barbican.cmd.barbican_manage:main
barbican-db-manage = barbican.cmd.db_manage:main
barbican-keystone-listener = barbican.cmd.keystone_listener:main
barbican-worker = barbican.cmd.worker:main