Add keystone-manage bootstrap command

Add the keystone-manage bootstrap command so that admin_token can
be deprecated/removed in the future. This will allow for bootstrapping
an initial user into the cloud instead of needing a global-admin
token to perform initial actions.

Change-Id: I113c6934b6b83ceff23a94101967a6df1126873f
bp: bootstrap
This commit is contained in:
Morgan Fainberg 2015-12-09 16:09:21 -08:00
parent 44d0c2f5a5
commit d446e15285
3 changed files with 192 additions and 1 deletions

View File

@ -16,6 +16,7 @@ from __future__ import absolute_import
from __future__ import print_function
import os
import uuid
from oslo_config import cfg
from oslo_log import log
@ -31,7 +32,7 @@ from keystone.common import utils
from keystone import exception
from keystone.federation import idp
from keystone.federation import utils as mapping_engine
from keystone.i18n import _, _LW
from keystone.i18n import _, _LW, _LI
from keystone.server import backends
from keystone import token
@ -51,6 +52,122 @@ class BaseApp(object):
return parser
class BootStrap(BaseApp):
"""Perform the basic bootstrap process"""
name = "bootstrap"
def __init__(self):
self.load_backends()
self.tenant_id = uuid.uuid4().hex
self.role_id = uuid.uuid4().hex
self.username = None
self.project_name = None
self.role_name = None
self.password = None
@classmethod
def add_argument_parser(cls, subparsers):
parser = super(BootStrap, cls).add_argument_parser(subparsers)
parser.add_argument('--bootstrap-username', default='admin',
help=('The username of the initial keystone '
'user during bootstrap process.'))
# NOTE(morganfainberg): See below for ENV Variable that can be used
# in lieu of the command-line arguments.
parser.add_argument('--bootstrap-password', default=None,
help='The bootstrap user password')
parser.add_argument('--bootstrap-project-name', default='admin',
help=('The initial project created during the '
'keystone bootstrap process.'))
parser.add_argument('--bootstrap-role-name', default='admin',
help=('The initial role-name created during the '
'keystone bootstrap process.'))
return parser
def load_backends(self):
drivers = backends.load_backends()
self.resource_manager = drivers['resource_api']
self.identity_manager = drivers['identity_api']
self.assignment_manager = drivers['assignment_api']
self.role_manager = drivers['role_api']
def _get_config(self):
self.username = (
os.environ.get('OS_BOOTSTRAP_USERNAME') or
CONF.command.bootstrap_username)
self.project_name = (
os.environ.get('OS_BOOTSTRAP_PROJECT_NAME') or
CONF.command.bootstrap_project_name)
self.role_name = (
os.environ.get('OS_BOOTSTRAP_ROLE_NAME') or
CONF.command.bootstrap_role_name)
self.password = (
os.environ.get('OS_BOOTSTRAP_PASSWORD') or
CONF.command.bootstrap_password)
def do_bootstrap(self):
"""Perform the bootstrap actions.
Create bootstrap user, project, and role so that CMS, humans, or
scripts can continue to perform initial setup (domains, projects,
services, endpoints, etc) of Keystone when standing up a new
deployment.
"""
self._get_config()
if self.password is None:
print(_('Either --bootstrap-password argument or '
'OS_BOOTSTRAP_PASSWORD must be set.'))
raise ValueError
# NOTE(morganfainberg): Ensure the default domain is in-fact created
default_domain = migration_helpers.get_default_domain()
try:
self.resource_manager.create_domain(
domain_id=default_domain['id'],
domain=default_domain)
LOG.info(_LI('Created domain %s'), default_domain['id'])
except exception.Conflict:
# NOTE(morganfainberg): Domain already exists, continue on.
LOG.info(_LI('Domain %s already exists, skipping creation.'),
default_domain['id'])
LOG.info(_LI('Creating project %s'), self.project_name)
self.resource_manager.create_project(
tenant_id=self.tenant_id,
tenant={'enabled': True,
'id': self.tenant_id,
'domain_id': default_domain['id'],
'description': 'Bootstrap project for initializing the '
'cloud.',
'name': self.project_name},
)
LOG.info(_LI('Creating user %s'), self.username)
user = self.identity_manager.create_user(
user_ref={'name': self.username,
'enabled': True,
'domain_id': default_domain['id'],
'password': self.password
}
)
LOG.info(_LI('Creating Role %s'), self.role_name)
self.role_manager.create_role(
role_id=self.role_id,
role={'name': self.role_name,
'id': self.role_id},
)
self.assignment_manager.add_role_to_user_and_project(
user_id=user['id'],
tenant_id=self.tenant_id,
role_id=self.role_id
)
@classmethod
def main(cls):
klass = cls()
klass.do_bootstrap()
class DbSync(BaseApp):
"""Sync the database."""
@ -641,6 +758,7 @@ class MappingEngineTester(BaseApp):
CMDS = [
BootStrap,
DbSync,
DbVersion,
DomainConfigUpload,

View File

@ -15,6 +15,7 @@
import os
import uuid
import fixtures
import mock
from oslo_config import cfg
from six.moves import range
@ -42,6 +43,63 @@ class CliTestCase(unit.SQLDriverOverrides, unit.TestCase):
cli.TokenFlush.main()
class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
def setUp(self):
self.useFixture(database.Database())
super(CliBootStrapTestCase, self).setUp()
def config_files(self):
self.config_fixture.register_cli_opt(cli.command_opt)
config_files = super(CliBootStrapTestCase, self).config_files()
config_files.append(unit.dirs.tests_conf('backend_sql.conf'))
return config_files
def config(self, config_files):
CONF(args=['bootstrap', '--bootstrap-password', uuid.uuid4().hex],
project='keystone',
default_config_files=config_files)
def test_bootstrap(self):
bootstrap = cli.BootStrap()
bootstrap.do_bootstrap()
project = bootstrap.resource_manager.get_project_by_name(
bootstrap.project_name,
'default')
user = bootstrap.identity_manager.get_user_by_name(
bootstrap.username,
'default')
role = bootstrap.role_manager.get_role(bootstrap.role_id)
role_list = (
bootstrap.assignment_manager.get_roles_for_user_and_project(
user['id'],
project['id']))
self.assertIs(len(role_list), 1)
self.assertEqual(role_list[0], role['id'])
# NOTE(morganfainberg): Pass an empty context, it isn't used by
# `authenticate` method.
bootstrap.identity_manager.authenticate(
{},
user['id'],
bootstrap.password)
class CliBootStrapTestCaseWithEnvironment(CliBootStrapTestCase):
def config(self, config_files):
CONF(args=['bootstrap'], project='keystone',
default_config_files=config_files)
def setUp(self):
super(CliBootStrapTestCaseWithEnvironment, self).setUp()
self.useFixture(
fixtures.EnvironmentVariable('OS_BOOTSTRAP_PASSWORD',
newvalue=uuid.uuid4().hex))
self.useFixture(
fixtures.EnvironmentVariable('OS_BOOTSTRAP_USERNAME',
newvalue=uuid.uuid4().hex))
class CliDomainConfigAllTestCase(unit.SQLDriverOverrides, unit.TestCase):
def setUp(self):

View File

@ -0,0 +1,15 @@
---
features:
- keystone-manage now supports the bootstrap command
on the CLI so that a keystone install can be
initialized without the need of the admin_token
filter in the paste-ini.
security:
- The use of admin_token filter is insecure compared
to the use of a proper username/password. Historically
the admin_token filter has been left enabled in
Keystone after initialization due to the way CMS
systems work. Moving to an out-of-band initialization
will eliminate the security concerns around a static
shared string that conveys admin access to Keystone
and therefore to the entire installation.