diff --git a/keystone/cmd/cli.py b/keystone/cmd/cli.py index 34c729d107..7025c63e39 100644 --- a/keystone/cmd/cli.py +++ b/keystone/cmd/cli.py @@ -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, diff --git a/keystone/tests/unit/test_cli.py b/keystone/tests/unit/test_cli.py index d967eb53fe..c5c1a15a6d 100644 --- a/keystone/tests/unit/test_cli.py +++ b/keystone/tests/unit/test_cli.py @@ -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): diff --git a/releasenotes/notes/add-bootstrap-cli-192500228cc6e574.yaml b/releasenotes/notes/add-bootstrap-cli-192500228cc6e574.yaml new file mode 100644 index 0000000000..7469243b64 --- /dev/null +++ b/releasenotes/notes/add-bootstrap-cli-192500228cc6e574.yaml @@ -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.