Create a trustee user for each bay

Docker registry, k8s load balancer and volume driver have a similar
need to use trust, so we need to create a trustee for each bay.

Change-Id: If034e74ce2ea80a7faa886d4edf789e576c30eb5
Partially-Implements: blueprint create-trustee-user-for-each-bay
This commit is contained in:
Hua Wang 2015-12-24 19:40:50 +08:00
parent 18a6cf6f97
commit 725bd5c99d
16 changed files with 264 additions and 48 deletions

View File

@ -55,6 +55,8 @@ MAGNUM_SERVICE_PORT=${MAGNUM_SERVICE_PORT:-9511}
MAGNUM_SERVICE_PORT_INT=${MAGNUM_SERVICE_PORT_INT:-19511}
MAGNUM_SERVICE_PROTOCOL=${MAGNUM_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL}
MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD=${MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD:-secret}
# Support entry points installation of console scripts
if [[ -d $MAGNUM_DIR/bin ]]; then
MAGNUM_BIN_DIR=$MAGNUM_DIR/bin
@ -191,6 +193,16 @@ function create_magnum_conf {
iniset $MAGNUM_CONF certificates storage_path "$MAGNUM_LOCAL_CERT_DIR"
iniset $MAGNUM_CONF certificates cert_manager_type "local"
fi
trustee_domain_id=$(get_or_create_domain magnum 'Owns users and projects created by magnum')
trustee_domain_admin_id=$(get_or_create_user trustee_domain_admin $MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD $trustee_domain_id)
openstack --os-auth-url $KEYSTONE_SERVICE_URI_V3 \
--os-identity-api-version 3 role add \
--user $trustee_domain_admin_id --domain $trustee_domain_id \
admin
iniset $MAGNUM_CONF trust trustee_domain_id $trustee_domain_id
iniset $MAGNUM_CONF trust trustee_domain_admin_id $trustee_domain_admin_id
iniset $MAGNUM_CONF trust trustee_domain_admin_password $MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD
}
function update_heat_policy {

View File

@ -44,7 +44,8 @@ class BayPatchType(types.JsonPatchType):
internal_attrs = ['/api_address', '/node_addresses',
'/master_addresses', '/stack_id',
'/ca_cert_ref', '/magnum_cert_ref',
'/registry_trust_id']
'/trust_id', '/trustee_user_name',
'/trustee_password', '/trustee_user_id']
return types.JsonPatchType.internal_attrs() + internal_attrs

View File

@ -547,3 +547,11 @@ class TrustCreateFailed(MagnumException):
class TrustDeleteFailed(MagnumException):
message = _("Failed to delete trust %(trust_id)s.")
class TrusteeCreateFailed(MagnumException):
message = _("Failed to create trustee %(username) in domain $(domain_id)")
class TrusteeDeleteFailed(MagnumException):
message = _("Failed to delete trustee %(trustee_id)")

View File

@ -10,17 +10,37 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from keystoneclient.auth.identity import v3
import keystoneclient.exceptions as kc_exception
from keystoneclient import session
from keystoneclient.v3 import client as kc_v3
from oslo_config import cfg
from oslo_log import log as logging
from magnum.common import exception
from magnum.common import utils
from magnum.i18n import _
from magnum.i18n import _LE
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
trust_opts = [
cfg.StrOpt('trustee_domain_id',
help=_('Id of the domain to create trustee for bays')),
cfg.StrOpt('trustee_domain_admin_id',
help=_('Id of the admin with roles sufficient to manage users'
' in the trustee_domain')),
cfg.StrOpt('trustee_domain_admin_password',
help=_('Password of trustee_domain_admin')),
cfg.ListOpt('roles',
default=[],
help=_('The roles which are delegated to the trustee '
'by the trustor'))
]
CONF.register_opts(trust_opts, group='trust')
CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
@ -31,6 +51,7 @@ class KeystoneClientV3(object):
self.context = context
self._client = None
self._admin_client = None
self._domain_admin_client = None
@property
def auth_url(self):
@ -74,6 +95,18 @@ class KeystoneClientV3(object):
**admin_credentials)
return self._admin_client
@property
def domain_admin_client(self):
if not self._domain_admin_client:
auth = v3.Password(
auth_url=self.auth_url,
user_id=CONF.trust.trustee_domain_admin_id,
domain_id=CONF.trust.trustee_domain_id,
password=CONF.trust.trustee_domain_admin_password)
sess = session.Session(auth=auth)
self._domain_admin_client = kc_v3.Client(session=sess)
return self._domain_admin_client
@staticmethod
def _is_v2_valid(auth_token_info):
return 'access' in auth_token_info
@ -110,26 +143,29 @@ class KeystoneClientV3(object):
return kc_v3.Client(**kwargs)
def create_trust(self, trustee_user, role_names, impersonation=True):
def create_trust(self, trustee_user):
trustor_user_id = self.client.auth_ref.user_id
trustor_project_id = self.client.auth_ref.project_id
# inherit the role of the trustor, unless set CONF.trust.roles
if CONF.trust.roles:
roles = CONF.trust.roles
else:
roles = self.context.roles
try:
trust = self.client.trusts.create(
trustor_user=trustor_user_id,
project=trustor_project_id,
trustee_user=trustee_user,
impersonation=impersonation,
role_names=role_names)
impersonation=True,
role_names=roles)
except Exception:
LOG.exception(_LE('Failed to create trust'))
raise exception.TrustCreateFailed(
trustee_user_id=trustee_user)
return trust
def create_trust_to_admin(self, role_names, impersonation=True):
trustee_user = self.admin_client.auth_ref.user_id
return self.create_trust(trustee_user, role_names, impersonation)
def delete_trust(self, trust_id):
if trust_id is None:
return
@ -140,3 +176,25 @@ class KeystoneClientV3(object):
except Exception:
LOG.exception(_LE('Failed to delete trust'))
raise exception.TrustDeleteFailed(trust_id=trust_id)
def create_trustee(self, username, password, domain_id):
password = utils.generate_password(length=18)
try:
user = self.domain_admin_client.users.create(
name=username,
password=password,
domain=domain_id)
except Exception:
LOG.exception(_LE('Failed to create trustee'))
raise exception.TrusteeCreateFailed(username=username,
domain_id=domain_id)
return user
def delete_trustee(self, trustee_id):
try:
self.domain_admin_client.users.delete(trustee_id)
except kc_exception.NotFound:
pass
except Exception:
LOG.exception(_LE('Failed to delete trustee'))
raise exception.TrusteeDeleteFailed(trustee_id=trustee_id)

View File

@ -41,6 +41,13 @@ from magnum.i18n import _
from magnum.i18n import _LE
from magnum.i18n import _LW
# Default symbols to use for passwords. Avoids visually confusing characters.
# ~6 bits per symbol
DEFAULT_PASSWORD_SYMBOLS = ['23456789', # Removed: 0,1
'ABCDEFGHJKLMNPQRSTUVWXYZ', # Removed: I, O
'abcdefghijkmnopqrstuvwxyz'] # Removed: l
UTILS_OPTS = [
cfg.StrOpt('rootwrap_config',
default="/etc/magnum/rootwrap.conf",
@ -48,6 +55,9 @@ UTILS_OPTS = [
'running commands as root.'),
cfg.StrOpt('tempdir',
help='Explicitly specify the temporary working directory.'),
cfg.ListOpt('password_symbols',
default=DEFAULT_PASSWORD_SYMBOLS,
help='Symbols to use for passwords')
]
CONF = cfg.CONF
@ -565,3 +575,40 @@ def get_memory_bytes(memory):
return float(signed_number) * (10 ** float(suffix[1:]))
else:
raise exception.UnsupportedK8sMemoryFormat()
def generate_password(length, symbolgroups=None):
"""Generate a random password from the supplied symbol groups.
At least one symbol from each group will be included. Unpredictable
results if length is less than the number of symbol groups.
Believed to be reasonably secure (with a reasonable password length!)
"""
if symbolgroups is None:
symbolgroups = CONF.password_symbols
r = random.SystemRandom()
# NOTE(jerdfelt): Some password policies require at least one character
# from each group of symbols, so start off with one random character
# from each symbol group
password = [r.choice(s) for s in symbolgroups]
# If length < len(symbolgroups), the leading characters will only
# be from the first length groups. Try our best to not be predictable
# by shuffling and then truncating.
r.shuffle(password)
password = password[:length]
length -= len(password)
# then fill with random characters from all symbol groups
symbols = ''.join(symbolgroups)
password.extend([r.choice(symbols) for _i in range(length)])
# finally shuffle to ensure first x characters aren't from a
# predictable group
r.shuffle(password)
return ''.join(password)

View File

@ -24,6 +24,7 @@ import six
from magnum.common import clients
from magnum.common import exception
from magnum.common import short_id
from magnum.common import utils
from magnum.conductor.handlers.common import cert_manager
from magnum.conductor import scale_manager
from magnum.conductor.template_definition import TemplateDefinition as TDef
@ -53,19 +54,10 @@ bay_heat_opts = [
'interval is in minutes. The default is no timeout.'))
]
docker_registry_opts = [
cfg.StrOpt('trustee_user_id',
default=None,
help='User id of the trustee'),
cfg.ListOpt('trust_roles',
default=['registry_user'],
help='The roles which are delegated to the trustee '
'by the trustor.')
]
CONF = cfg.CONF
CONF.register_opts(bay_heat_opts, group='bay_heat')
CONF.register_opts(docker_registry_opts, 'docker_registry')
CONF.import_opt('trustee_domain_id', 'magnum.common.keystone',
group='trust')
LOG = logging.getLogger(__name__)
@ -127,6 +119,19 @@ class Handler(object):
def __init__(self):
super(Handler, self).__init__()
@staticmethod
def _create_trustee_and_trust(osc, bay):
password = utils.generate_password(length=18)
trustee = osc.keystone().create_trustee(
bay.uuid,
password,
CONF.trust.trustee_domain_id)
bay.trustee_username = trustee.name
bay.trustee_user_id = trustee.id
bay.trustee_password = password
trust = osc.keystone().create_trust(trustee.id)
bay.trust_id = trust.id
# Bay Operations
def bay_create(self, context, bay, bay_create_timeout):
@ -134,17 +139,10 @@ class Handler(object):
osc = clients.OpenStackClients(context)
baymodel = objects.BayModel.get_by_uuid(context,
bay.baymodel_id)
if baymodel.registry_enabled:
trust = osc.keystone().create_trust(
CONF.docker_registry.trustee_user_id,
CONF.docker_registry.trust_roles)
bay.registry_trust_id = trust.id
bay.uuid = uuid.uuid4()
self._create_trustee_and_trust(osc, bay)
try:
# Generate certificate and set the cert reference to bay
bay.uuid = uuid.uuid4()
cert_manager.generate_certificates_to_bay(bay)
created_stack = _create_stack(context, osc, bay,
bay_create_timeout)
@ -192,10 +190,18 @@ class Handler(object):
return bay
@staticmethod
def _delete_trustee_and_trust(osc, bay):
osc.keystone().delete_trust(bay.trust_id)
osc.keystone().delete_trustee(bay.trustee_user_id)
def bay_delete(self, context, uuid):
LOG.debug('bay_heat bay_delete')
osc = clients.OpenStackClients(context)
bay = objects.Bay.get_by_uuid(context, uuid)
self._delete_trustee_and_trust(osc, bay)
stack_id = bay.stack_id
# NOTE(sdake): This will execute a stack_delete operation. This will
# Ignore HTTPNotFound exceptions (stack wasn't present). In the case
@ -218,7 +224,6 @@ class Handler(object):
except Exception:
raise
osc.keystone().delete_trust(bay.registry_trust_id)
self._poll_and_check(osc, bay)
return None

View File

@ -0,0 +1,41 @@
# Copyright 2016 OpenStack Foundation
# 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.
"""create trustee for each bay
Revision ID: 5d4caa6e0a42
Revises: bb42b7cad130
Create Date: 2016-02-17 14:16:12.927874
"""
# revision identifiers, used by Alembic.
revision = '5d4caa6e0a42'
down_revision = 'bb42b7cad130'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.alter_column('bay', 'registry_trust_id',
new_column_name='trust_id',
existing_type=sa.String(255))
op.add_column('bay', sa.Column('trustee_username',
sa.String(length=255), nullable=True))
op.add_column('bay', sa.Column('trustee_user_id',
sa.String(length=255), nullable=True))
op.add_column('bay', sa.Column('trustee_password',
sa.String(length=255), nullable=True))

View File

@ -120,8 +120,12 @@ class Bay(Base):
bay_create_timeout = Column(Integer())
discovery_url = Column(String(255))
master_addresses = Column(JSONEncodedList)
# TODO(wanghua): encrypt registry_trust_id in db
registry_trust_id = Column(String(255))
# TODO(wanghua): encrypt trust_id in db
trust_id = Column(String(255))
trustee_username = Column(String(255))
trustee_user_id = Column(String(255))
# TODO(wanghua): encrypt trustee_password in db
trustee_password = Column(String(255))
# (yuanying) if we use barbican,
# cert_ref size is determined by below format
# * http(s)://${DOMAIN_NAME}/v1/containers/${UUID}

View File

@ -31,7 +31,10 @@ class Bay(base.MagnumPersistentObject, base.MagnumObject,
# Version 1.2: Add 'registry_trust_id' field
# Version 1.3: Added 'baymodel' field
# Version 1.4: Added more types of status to bay's status field
VERSION = '1.4'
# Version 1.5: Reanme 'registry_trust_id' to 'trust_id'
# Add 'trustee_user_name', 'trustee_password',
# 'trustee_user_id' field
VERSION = '1.5'
dbapi = dbapi.get_instance()
@ -54,8 +57,11 @@ class Bay(base.MagnumPersistentObject, base.MagnumObject,
'master_addresses': fields.ListOfStringsField(nullable=True),
'ca_cert_ref': fields.StringField(nullable=True),
'magnum_cert_ref': fields.StringField(nullable=True),
'registry_trust_id': fields.StringField(nullable=True),
'baymodel': fields.ObjectField('BayModel'),
'trust_id': fields.StringField(nullable=True),
'trustee_username': fields.StringField(nullable=True),
'trustee_password': fields.StringField(nullable=True),
'trustee_user_id': fields.StringField(nullable=True)
}
@staticmethod

View File

@ -46,8 +46,7 @@ def list_opts():
('conductor', magnum.conductor.config.SERVICE_OPTS),
('database', magnum.db.sql_opts),
('docker', magnum.common.docker_utils.docker_opts),
('docker_registry',
magnum.conductor.handlers.bay_conductor.docker_registry_opts),
('trust', magnum.common.keystone.trust_opts),
('magnum_client', magnum.common.clients.magnum_client_opts),
('heat_client', magnum.common.clients.heat_client_opts),
('glance_client', magnum.common.clients.glance_client_opts),

View File

@ -111,25 +111,31 @@ class KeystoneClientTest(base.BaseTestCase):
ks_client = keystone.KeystoneClientV3(self.ctx)
self.assertIsNone(ks_client.delete_trust(trust_id='atrust123'))
def test_create_trust(self, mock_ks):
def test_create_trust_with_all_roles(self, mock_ks):
mock_ks.return_value.auth_ref.user_id = '123456'
mock_ks.return_value.auth_ref.project_id = '654321'
self.ctx.roles = ['role1', 'role2']
ks_client = keystone.KeystoneClientV3(self.ctx)
ks_client.create_trust(trustee_user='888888',
role_names='xxxx')
ks_client.create_trust(trustee_user='888888')
mock_ks.return_value.trusts.create.assert_called_once_with(
trustor_user='123456', project='654321',
trustee_user='888888', role_names='xxxx',
trustee_user='888888', role_names=['role1', 'role2'],
impersonation=True)
@mock.patch.object(keystone.KeystoneClientV3,
'create_trust')
def test_create_trust_to_admin(self, mock_create_trust, mock_ks):
mock_ks.return_value.auth_ref.user_id = '777777'
def test_create_trust_with_limit_roles(self, mock_ks):
mock_ks.return_value.auth_ref.user_id = '123456'
mock_ks.return_value.auth_ref.project_id = '654321'
self.ctx.roles = ['role1', 'role2']
ks_client = keystone.KeystoneClientV3(self.ctx)
ks_client.create_trust_to_admin(role_names='xxxx')
mock_create_trust.assert_called_once_with('777777', 'xxxx', True)
cfg.CONF.set_override('roles', ['role3'], group='trust')
ks_client.create_trust(trustee_user='888888')
mock_ks.return_value.trusts.create.assert_called_once_with(
trustor_user='123456', project='654321',
trustee_user='888888', role_names=['role3'],
impersonation=True)

View File

@ -598,3 +598,13 @@ class Urllib2_invalid_scheme(base.TestCase):
def test_raise_exception_invalid_scheme_https(self):
utils.raise_exception_invalid_scheme(url='https://www.openstack.org')
class GeneratePasswordTestCase(base.TestCase):
def test_generate_password(self):
password = utils.generate_password(length=12)
self.assertTrue([c for c in password if c in '0123456789'])
self.assertTrue([c for c in password
if c in 'abcdefghijklmnopqrstuvwxyz'])
self.assertTrue([c for c in password
if c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'])

View File

@ -44,6 +44,23 @@ class TestHandler(db_base.DbTestCase):
self.bay = objects.Bay(self.context, **bay_dict)
self.bay.create()
self.p = patch(
'magnum.conductor.handlers.bay_conductor.Handler.'
'_create_trustee_and_trust')
def create_trustee_and_trust(osc, bay):
bay.trust_id = 'trust_id'
bay.trustee_username = 'user_name'
bay.trustee_user_id = 'user_id'
bay.trustee_password = 'password'
self.p.side_effect = create_trustee_and_trust
self.p.start()
def tearDown(self):
self.p.stop()
super(TestHandler, self).tearDown()
@patch('magnum.conductor.scale_manager.ScaleManager')
@patch('magnum.conductor.handlers.bay_conductor.Handler._poll_and_check')
@patch('magnum.conductor.handlers.bay_conductor._update_stack')

View File

@ -96,8 +96,6 @@ def get_test_bay(**kw):
'node_count': kw.get('node_count', 3),
'master_count': kw.get('master_count', 3),
'master_addresses': kw.get('master_addresses', ['172.17.2.18']),
'registry_trust_id': kw.get('registry_trust_id',
'1f2281ac-e532-4e53-bbe6-3c9be24b0504'),
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at'),
}

View File

@ -28,6 +28,10 @@ class TestBayObject(base.DbTestCase):
def setUp(self):
super(TestBayObject, self).setUp()
self.fake_bay = utils.get_test_bay()
self.fake_bay['trust_id'] = 'trust_id'
self.fake_bay['trustee_username'] = 'trustee_user'
self.fake_bay['trustee_user_id'] = 'trustee_user_id'
self.fake_bay['trustee_password'] = 'password'
baymodel_id = self.fake_bay['baymodel_id']
self.fake_baymodel = objects.BayModel(uuid=baymodel_id)

View File

@ -423,7 +423,7 @@ class _TestObject(object):
# For more information on object version testing, read
# http://docs.openstack.org/developer/magnum/objects.html
object_data = {
'Bay': '1.4-ca3c05dc1b27b2e50c082bb2a1a64151',
'Bay': '1.5-a3b9292ef5d35175b93ca46ba3baec2d',
'BayModel': '1.9-d5d32553721d0cadfcc45ddc316d9c1a',
'Certificate': '1.0-2aff667971b85c1edf8d15684fd7d5e2',
'Container': '1.3-e2d9d2e8a8844d421148cd9fde6c6bd6',