Add ability to create users and projects from keystone-manage

This adds the ability to create users and projects directly from
keystone-manage.  We also add the ability to specify specific UUIDs
for both users and projects via the creation functions.

Change-Id: Icd193eff25556d21ec26bb29908b8ad6548fdc91
This commit is contained in:
Dave Wilde 2024-03-07 14:27:21 -06:00
parent 2ac039b717
commit a8366c4827
2 changed files with 222 additions and 1 deletions

View File

@ -26,6 +26,7 @@ import pbr.version
from keystone.cmd import bootstrap
from keystone.cmd import doctor
from keystone.cmd import idutils
from keystone.common import driver_hints
from keystone.common import fernet_utils
from keystone.common import jwt_utils
@ -201,6 +202,73 @@ class BootStrap(BaseApp):
klass.do_bootstrap()
class ProjectSetup(BaseApp):
"""Create project with specified UUID."""
name = 'project_setup'
def __init__(self):
self.identity = idutils.Identity()
@classmethod
def add_argument_parser(cls, subparsers):
parser = super(ProjectSetup, cls).add_argument_parser(subparsers)
parser.add_argument('--project-name', default=None, required=True,
help='The name of the keystone project being'
' created.')
parser.add_argument('--project-id', default=None,
help='The UUID of the keystone project being'
' created.')
return parser
def do_project_setup(self):
"""Create project with specified UUID."""
self.identity.project_name = CONF.command.project_name
self.identity.project_id = CONF.command.project_id
self.identity.project_setup()
@classmethod
def main(cls):
klass = cls()
klass.do_project_setup()
class UserSetup(BaseApp):
"""Create user with specified UUID."""
name = 'user_setup'
def __init__(self):
self.identity = idutils.Identity()
@classmethod
def add_argument_parser(cls, subparsers):
parser = super(UserSetup, cls).add_argument_parser(subparsers)
parser.add_argument('--username', default=None, required=True,
help='The username of the keystone user that'
' is being created.')
parser.add_argument('--user-password-plain', default=None,
required=True,
help='The plaintext password for the keystone'
' user that is being created.')
parser.add_argument('--user-id', default=None,
help='The UUID of the keystone user being '
'created.')
return parser
def do_user_setup(self):
"""Create user with specified UUID."""
self.identity.user_name = CONF.command.username
self.identity.user_password = CONF.command.user_password_plain
self.identity.user_id = CONF.command.user_id
self.identity.user_setup()
@classmethod
def main(cls):
klass = cls()
klass.do_user_setup()
class Doctor(BaseApp):
"""Diagnose common problems with keystone deployments."""
@ -1330,12 +1398,14 @@ CMDS = [
MappingPopulate,
MappingPurge,
MappingEngineTester,
ProjectSetup,
ReceiptRotate,
ReceiptSetup,
SamlIdentityProviderMetadata,
TokenRotate,
TokenSetup,
TrustFlush
TrustFlush,
UserSetup
]

151
keystone/cmd/idutils.py Normal file
View File

@ -0,0 +1,151 @@
# 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 uuid
from oslo_log import log
from keystone.common import provider_api
from keystone.common.validation import validators
import keystone.conf
from keystone import exception
from keystone.identity.mapping_backends import mapping
from keystone import notifications
from keystone.server import backends
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class Identity(object):
def __init__(self):
backends.load_backends()
self.user_id = None
self.user_name = None
self.user_password = None
self.project_id = None
self.project_name = None
self.default_domain_id = CONF.identity.default_domain_id
def project_setup(self):
try:
project_id = self.project_id
if project_id is None:
project_id = uuid.uuid4().hex
project = {
'enabled': True,
'id': project_id,
'domain_id': self.default_domain_id,
'description': 'Bootstrap project for initializing the cloud.',
'name': self.project_name
}
PROVIDERS.resource_api.create_project(project_id, project)
LOG.info('Created project %s', self.project_name)
except exception.Conflict:
LOG.info('Project %s already exists, skipping creation.',
self.project_name)
project = PROVIDERS.resource_api.get_project_by_name(
self.project_name, self.default_domain_id
)
self.project_id = project['id']
def _create_user(self, user_ref, initiator=None):
_self = PROVIDERS.identity_api.create_user.__self__
user = user_ref.copy()
if 'password' in user:
validators.validate_password(user['password'])
user['name'] = user['name'].strip()
user.setdefault('enabled', True)
domain_id = user['domain_id']
PROVIDERS.resource_api.get_domain(domain_id)
_self._assert_default_project_id_is_not_domain(
user_ref.get('default_project_id'))
# For creating a user, the domain is in the object itself
domain_id = user_ref['domain_id']
driver = _self._select_identity_driver(domain_id)
user = _self._clear_domain_id_if_domain_unaware(driver, user)
# Generate a local ID - in the future this might become a function of
# the underlying driver so that it could conform to rules set down by
# that particular driver type.
user['id'] = self.user_id
ref = _self._create_user_with_federated_objects(user, driver)
notifications.Audit.created(_self._USER, user['id'], initiator)
return _self._set_domain_id_and_mapping(
ref, domain_id, driver, mapping.EntityType.USER)
def user_setup(self):
# NOTE(morganfainberg): Do not create the user if it already exists.
try:
user = PROVIDERS.identity_api.get_user_by_name(
self.user_name, self.default_domain_id
)
LOG.info('User %s already exists, skipping creation.',
self.user_name)
if self.user_id is not None and user['id'] != self.user_id:
msg = (f'user `{self.user_name}` already exists '
f'with `{self.user_id}`')
raise exception.Conflict(type='user_id', details=msg)
# If the user is not enabled, re-enable them. This also helps
# provide some useful logging output later.
update = {}
enabled = user['enabled']
if not enabled:
update['enabled'] = True
try:
PROVIDERS.identity_api.driver.authenticate(
user['id'], self.user_password
)
except AssertionError:
# This means that authentication failed and that we need to
# update the user's password. This is going to persist a
# revocation event that will make all previous tokens for the
# user invalid, which is OK because it falls within the scope
# of revocation. If a password changes, we shouldn't be able to
# use tokens obtained with an old password.
update['password'] = self.user_password
# Only make a call to update the user if the password has changed
# or the user was previously disabled. This allows bootstrap to act
# as a recovery tool, without having to create a new user.
if update:
user = PROVIDERS.identity_api.update_user(
user['id'], update
)
LOG.info('Reset password for user %s.', self.user_name)
if not enabled and user['enabled']:
# Although we always try to enable the user, this log
# message only makes sense if we know that the user was
# previously disabled.
LOG.info('Enabled user %s.', self.user_name)
except exception.UserNotFound:
user = self._create_user(
user_ref={
'name': self.user_name,
'enabled': True,
'domain_id': self.default_domain_id,
'password': self.user_password
}
)
LOG.info('Created user %s', self.user_name)
self.user_id = user['id']