Adding sessions module and keystone client upgrade

This change implements the sahara.service.sessions module with the basic
session cache object. It also adds the authentication plugin from the
keystonemiddleware into the context for rest transactions.

It also migrates the keystone client utility functions to use the new
session methodology. The trust module has been accordingly fixed to make
greater use of authentication objects instead of client objects.

* adding auth plugin to context
* adding sessions module
* adding test for sessions
* adding keystonemiddleware base auth plugin object to context on api
  call
* adding keystone session to sessions module
* refactoring keystone client to use sessions
* adding keystone methods to retrieve auth plugins, tokens, and service
  catalog
* changing sahara.service.trusts to use new keystone methods
* fixing trust tests to fit new authentication methodologies

Change-Id: I65ed4b4dcee8752bf4e66ef9e47305ff408d8d5d
Partial-Implements: bp keystone-sessions
This commit is contained in:
Michael McCune 2015-07-07 14:11:37 -04:00
parent 81af38fabe
commit f240d55ac5
9 changed files with 419 additions and 111 deletions

View File

@ -47,6 +47,7 @@ class Context(context.RequestContext):
resource_uuid=None,
current_instance_info=None,
request_id=None,
auth_plugin=None,
overwrite=True,
**kwargs):
if kwargs:
@ -64,6 +65,7 @@ class Context(context.RequestContext):
self.tenant_name = tenant_name
self.remote_semaphore = remote_semaphore or semaphore.Semaphore(
CONF.cluster_remote_threshold)
self.auth_plugin = auth_plugin
self.roles = roles
if overwrite or not hasattr(context._request_store, 'context'):
self.update_store()
@ -87,6 +89,7 @@ class Context(context.RequestContext):
self.resource_uuid,
self.current_instance_info,
self.request_id,
self.auth_plugin,
overwrite=False)
def to_dict(self):

106
sahara/service/sessions.py Normal file
View File

@ -0,0 +1,106 @@
# Copyright (c) 2015 Red Hat, 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.
from keystoneclient import session as keystone
from oslo_config import cfg
from oslo_log import log as logging
from sahara import exceptions as ex
from sahara.i18n import _
from sahara.i18n import _LE
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
_SESSION_CACHE = None
SESSION_TYPE_GENERIC = 'generic'
SESSION_TYPE_KEYSTONE = 'keystone'
def cache():
global _SESSION_CACHE
if not _SESSION_CACHE:
_SESSION_CACHE = SessionCache()
return _SESSION_CACHE
class SessionCache(object):
'''A cache of keystone Session objects
When a requested Session is not currently cached, it will be
acquired from specific information in this module. Sessions should
be referenced by their OpenStack project name and not the service
name, this is to allow for multiple service implementations
while retaining the ability to generate Session objects. In all
cases, the constant values in this module should be used to
communicate the session type.
'''
def __init__(self):
'''create a new SessionCache'''
self._sessions = {}
self._session_funcs = {
SESSION_TYPE_GENERIC: self.get_generic_session,
SESSION_TYPE_KEYSTONE: self.get_keystone_session,
}
def _set_session(self, session_type, session):
'''Set the session for a given type.
:param session_type: the type of session to set.
:param session: the session to associate with the type
'''
self._sessions[session_type] = session
def get_session(self, session_type=SESSION_TYPE_GENERIC):
'''Return a Session for the requested type
:param session_type: the type of Session to get, if None a generic
session will be returned.
:raises SaharaException: if the requested session type is not
found.
'''
session_function = self._session_funcs.get(session_type)
if session_function:
return session_function()
else:
LOG.error(
_LE('Requesting an unknown session type (type: {type})').
format(type=session_type))
raise ex.SaharaException(
_('Session type {type} not recognized').
format(type=session_type))
def get_generic_session(self):
session = self._sessions.get(SESSION_TYPE_GENERIC)
if not session:
session = keystone.Session()
self._set_session(SESSION_TYPE_GENERIC, session)
return session
def get_keystone_session(self):
session = self._sessions.get(SESSION_TYPE_KEYSTONE)
if not session:
if CONF.keystone.ca_file:
session = keystone.Session(cert=CONF.keystone.ca_file,
verify=CONF.keystone.api_insecure)
else:
session = self.get_generic_session()
self._set_session(SESSION_TYPE_KEYSTONE, session)
return session

View File

@ -39,36 +39,42 @@ def _get_expiry():
hours=CONF.cluster_operation_trust_expiration_hours)
def create_trust(trustor,
trustee,
role_names,
impersonation=True,
project_id=None,
expires=True):
def create_trust(trustor, trustee, role_names, impersonation=True,
project_id=None, expires=True):
'''Create a trust and return it's identifier
:param trustor: The Keystone client delegating the trust.
:param trustee: The Keystone client consuming the trust.
:param trustor: The user delegating the trust, this is an auth plugin.
:param trustee: The user consuming the trust, this is an auth plugin.
:param role_names: A list of role names to be assigned.
:param impersonation: Should the trustee impersonate trustor,
default is True.
:param project_id: The project that the trust will be scoped into,
default is the trustor's project id.
:param expires: The trust will expire if this is set to True.
:returns: A valid trust id.
:raises CreationFailed: If the trust cannot be created.
'''
if project_id is None:
project_id = trustor.tenant_id
project_id = keystone.project_id_from_auth(trustor)
try:
expires_at = _get_expiry() if expires else None
trust = trustor.trusts.create(trustor_user=trustor.user_id,
trustee_user=trustee.user_id,
impersonation=impersonation,
role_names=role_names,
project=project_id,
expires_at=expires_at)
trustor_user_id = keystone.user_id_from_auth(trustor)
trustee_user_id = keystone.user_id_from_auth(trustee)
client = keystone.client_from_auth(trustor)
trust = client.trusts.create(trustor_user=trustor_user_id,
trustee_user=trustee_user_id,
impersonation=impersonation,
role_names=role_names,
project=project_id,
expires_at=expires_at)
LOG.debug('Created trust {trust_id}'.format(
trust_id=six.text_type(trust.id)))
return trust.id
@ -87,9 +93,10 @@ def create_trust_for_cluster(cluster, expires=True):
:param expires: The trust will expire if this is set to True.
'''
trustor = keystone.client()
ctx = context.current()
trustee = keystone.client_for_admin()
trustor = keystone.auth()
trustee = keystone.auth_for_admin(
project_name=CONF.keystone_authtoken.admin_tenant_name)
trust_id = create_trust(trustor=trustor,
trustee=trustee,
@ -104,13 +111,16 @@ def create_trust_for_cluster(cluster, expires=True):
def delete_trust(trustee, trust_id):
'''Delete a trust from a trustee
:param trustee: The Keystone client to delete the trust from.
:param trustee: The user to delete the trust from, this is an auth plugin.
:param trust_id: The identifier of the trust to delete.
:raises DeletionFailed: If the trust cannot be deleted.
'''
try:
trustee.trusts.delete(trust_id)
client = keystone.client_from_auth(trustee)
client.trusts.delete(trust_id)
LOG.debug('Deleted trust {trust_id}'.format(
trust_id=six.text_type(trust_id)))
except Exception as e:
@ -130,9 +140,8 @@ def delete_trust_from_cluster(cluster):
'''
if cluster.trust_id:
keystone_client = keystone.client_for_admin_from_trust(
cluster.trust_id)
delete_trust(keystone_client, cluster.trust_id)
keystone_auth = keystone.auth_for_admin(trust_id=cluster.trust_id)
delete_trust(keystone_auth, cluster.trust_id)
ctx = context.current()
conductor.cluster_update(ctx,
cluster,
@ -154,6 +163,8 @@ def use_os_admin_auth_token(cluster):
ctx = context.current()
ctx.username = CONF.keystone_authtoken.admin_user
ctx.tenant_id = cluster.tenant_id
client = keystone.client_for_admin_from_trust(cluster.trust_id)
ctx.auth_token = client.auth_token
ctx.service_catalog = json.dumps(client.service_catalog.get_data())
ctx.auth_plugin = keystone.auth_for_admin(
trust_id=cluster.trust_id)
ctx.auth_token = keystone.token_from_auth(ctx.auth_plugin)
ctx.service_catalog = json.dumps(
keystone.service_catalog_from_auth(ctx.auth_plugin))

View File

@ -0,0 +1,33 @@
# Copyright (c) 2015 Red Hat, 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.
from keystoneclient import session as keystone
from sahara import exceptions as ex
from sahara.service import sessions
from sahara.tests.unit import base
class TestSessionCache(base.SaharaTestCase):
def test_get_session(self):
sc = sessions.cache()
session = sc.get_session()
self.assertTrue(isinstance(session, keystone.Session))
self.assertRaises(ex.SaharaException,
sc.get_session,
session_type='bad service')

View File

@ -27,20 +27,27 @@ class FakeTrust(object):
class TestTrusts(base.SaharaTestCase):
def _trustor(self):
def _client(self):
create = mock.Mock()
create.return_value = FakeTrust("trust_id")
trustor_trusts = mock.Mock(create=create)
trustor = mock.Mock(user_id="trustor_id", tenant_id="tenant_id",
trusts=trustor_trusts)
return trustor
client_trusts = mock.Mock(create=create)
client = mock.Mock(trusts=client_trusts)
return client
def test_create_trust(self):
trustor = self._trustor()
trustee = mock.Mock(user_id="trustee_id")
@mock.patch('sahara.utils.openstack.keystone.client_from_auth')
@mock.patch('sahara.utils.openstack.keystone.project_id_from_auth')
@mock.patch('sahara.utils.openstack.keystone.user_id_from_auth')
def test_create_trust(self, user_id_from_auth, project_id_from_auth,
client_from_auth):
project_id_from_auth.return_value = 'tenant_id'
user_id_from_auth.side_effect = ['trustor_id', 'trustee_id']
trustor = 'trustor_id'
trustee = 'trustee_id'
client = self._client()
client_from_auth.return_value = client
trust_id = trusts.create_trust(trustor, trustee,
"role_names", expires=True)
trustor.trusts.create.assert_called_with(
client.trusts.create.assert_called_with(
trustor_user="trustor_id",
trustee_user="trustee_id",
impersonation=True,
@ -50,29 +57,42 @@ class TestTrusts(base.SaharaTestCase):
)
self.assertEqual("trust_id", trust_id)
@mock.patch('sahara.utils.openstack.keystone.client_for_admin')
@mock.patch('sahara.utils.openstack.keystone.client')
@mock.patch('sahara.conductor.API.cluster_update')
@mock.patch('sahara.context.current')
def test_create_trust_for_cluster(self, m_current, m_cluster_update,
m_client, m_client_for_admin):
ctx = mock.Mock(roles="role_names")
trustee = mock.Mock(user_id="trustee_id")
trustor = self._trustor()
user_id_from_auth.side_effect = ['trustor_id', 'trustee_id']
client = self._client()
client_from_auth.return_value = client
trust_id = trusts.create_trust(trustor, trustee, "role_names",
project_id='injected_project',
expires=False)
client.trusts.create.assert_called_with(trustor_user="trustor_id",
trustee_user="trustee_id",
impersonation=True,
role_names="role_names",
project="injected_project",
expires_at=None)
self.assertEqual("trust_id", trust_id)
m_current.return_value = ctx
m_client_for_admin.return_value = trustee
m_client.return_value = trustor
@mock.patch('sahara.conductor.API.cluster_update')
@mock.patch('sahara.service.trusts.create_trust')
@mock.patch('sahara.utils.openstack.keystone.auth_for_admin')
@mock.patch('sahara.context.current')
def test_create_trust_for_cluster(self, context_current, auth_for_admin,
create_trust, cluster_update):
self.override_config('admin_tenant_name', 'admin_project',
group='keystone_authtoken')
trustor_auth = mock.Mock()
ctx = mock.Mock(roles="role_names", auth_plugin=trustor_auth)
context_current.return_value = ctx
trustee_auth = mock.Mock()
auth_for_admin.return_value = trustee_auth
create_trust.return_value = 'trust_id'
trusts.create_trust_for_cluster("cluster")
trustor.trusts.create.assert_called_with(
trustor_user="trustor_id",
trustee_user="trustee_id",
impersonation=True,
role_names="role_names",
project="tenant_id",
expires_at=mock.ANY
)
m_cluster_update.assert_called_with(ctx, "cluster",
{"trust_id": "trust_id"})
auth_for_admin.assert_called_with(project_name='admin_project')
create_trust.assert_called_with(trustor=trustor_auth,
trustee=trustee_auth,
role_names='role_names',
expires=True)
cluster_update.assert_called_with(ctx, "cluster",
{"trust_id": "trust_id"})

View File

@ -78,6 +78,7 @@ class Rest(flask.Blueprint):
kwargs.pop("tenant_id")
req_id = flask.request.environ.get(oslo_req_id.ENV_REQUEST_ID)
auth_plugin = flask.request.environ.get('keystone.token_auth')
ctx = context.Context(
flask.request.headers['X-User-Id'],
flask.request.headers['X-Tenant-Id'],
@ -86,6 +87,7 @@ class Rest(flask.Blueprint):
flask.request.headers['X-User-Name'],
flask.request.headers['X-Tenant-Name'],
flask.request.headers['X-Roles'].split(','),
auth_plugin=auth_plugin,
request_id=req_id)
context.set_ctx(ctx)
if flask.request.method in ['POST', 'PUT', 'PATCH']:

View File

@ -20,6 +20,7 @@ from keystoneclient.v3 import client as keystone_client_v3
from oslo_config import cfg
from sahara import context
from sahara.service import sessions
from sahara.utils.openstack import base
@ -73,14 +74,142 @@ CONF.register_opts(opts)
CONF.register_opts(ssl_opts, group=keystone_group)
def auth():
'''Return a token auth plugin for the current context.'''
ctx = context.current()
return ctx.auth_plugin or _token_auth(ctx.auth_token, ctx.tenant_id)
def auth_for_admin(project_name=None, trust_id=None):
'''Return an auth plugin for the admin.
:param project_name: a project to scope the auth with (optional).
:param trust_id: a trust to scope the auth with (optional).
:returns: an auth plugin object for the admin.
'''
# TODO(elmiko) revisit the project_domain_name if we start getting
# into federated authentication. it will need to match the domain that
# the project_name exists in.
auth = _password_auth(
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password,
project_name=project_name,
user_domain_name=CONF.admin_user_domain_name,
project_domain_name=CONF.admin_project_domain_name,
trust_id=trust_id)
return auth
def auth_for_proxy(username, password, trust_id=None):
'''Return an auth plugin for the proxy user.
:param username: the name of the proxy user.
:param password: the proxy user's password.
:param trust_id: a trust to scope the auth with (optional).
:returns: an auth plugin object for the proxy user.
'''
auth = _password_auth(
username=username,
password=password,
user_domain_name=CONF.proxy_user_domain_name,
trust_id=trust_id)
return auth
def client():
'''Return the current context client.'''
ctx = context.current()
return _client(username=ctx.username, token=ctx.auth_token,
tenant_id=ctx.tenant_id)
return client_from_auth(auth())
def client_for_admin():
'''Return the Sahara admin user client.'''
auth = auth_for_admin(
project_name=CONF.keystone_authtoken.admin_tenant_name)
return client_from_auth(auth)
def client_from_auth(auth):
'''Return a session based client from the auth plugin provided.
A session is obtained from the global session cache.
:param auth: the auth plugin object to use in client creation.
:returns: a keystone client
'''
session = sessions.cache().get_session(sessions.SESSION_TYPE_KEYSTONE)
if CONF.use_identity_api_v3:
client_class = keystone_client_v3.Client
else:
client_class = keystone_client.Client
return client_class(session=session, auth=auth)
def project_id_from_auth(auth):
'''Return the project id associated with an auth plugin.
:param auth: the auth plugin to inspect.
:returns: the project id associated with the auth plugin.
'''
return auth.get_project_id(
sessions.cache().get_session(sessions.SESSION_TYPE_KEYSTONE))
def service_catalog_from_auth(auth):
'''Return the service catalog associated with an auth plugin.
:param auth: the auth plugin to inspect.
:returns: a list containing the service catalog.
'''
if CONF.use_identity_api_v3:
return auth.get_access(
sessions.cache().get_session()).get('catalog', [])
else:
return auth.get_access(
sessions.cache().get_session()).get('serviceCatalog', [])
# TODO(elmiko) factor this out when redoing the barbicanclient
def session_for_admin():
'''Return a Keystone session for the admin user.'''
auth = _password_auth(
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password,
project_name=CONF.keystone_authtoken.admin_tenant_name,
user_domain_name=CONF.admin_user_domain_name,
project_domain_name=CONF.admin_project_domain_name)
return keystone_session.Session(auth=auth)
def token_from_auth(auth):
'''Return an authentication token from an auth plugin.
:param auth: the auth plugin to acquire a token from.
:returns: an auth token in string format.
'''
return keystone_session.Session(auth=auth).get_token()
def user_id_from_auth(auth):
'''Return a user id associated with an auth plugin.
:param auth: the auth plugin to inspect.
:returns: a token associated with the auth.
'''
return auth.get_user_id(sessions.cache().get_session(
sessions.SESSION_TYPE_KEYSTONE))
# TODO(elmiko) deprecate this when all client have been migrated to sessions
def _client(username, password=None, token=None, tenant_name=None,
tenant_id=None, trust_id=None, domain_name=None):
@ -111,64 +240,68 @@ def _client(username, password=None, token=None, tenant_name=None,
return keystone
def _admin_client(project_name=None, trust_id=None):
username = CONF.keystone_authtoken.admin_user
password = CONF.keystone_authtoken.admin_password
keystone = _client(username=username,
password=password,
tenant_name=project_name,
trust_id=trust_id)
return keystone
def _password_auth(username, password, project_name, user_domain_name=None,
project_domain_name=None, trust_id=None):
'''Return a password auth plugin object.
:param username: the user to authenticate as.
def client_for_admin():
'''Return the Sahara admin user client.'''
return _admin_client(
project_name=CONF.keystone_authtoken.admin_tenant_name)
:param password: the user's password.
:param project_name: the project(ex. tenant) name to scope the auth.
def client_for_admin_from_trust(trust_id):
'''Return the Sahara admin user client scoped to a trust.'''
return _admin_client(trust_id=trust_id)
:param user_domain_name: the domain the user belongs to.
:param project_domain_name: the domain the project belongs to.
def client_for_proxy_user(username, password, trust_id=None):
'''Return a client for the proxy user specified.'''
return _client(username=username,
password=password,
domain_name=CONF.proxy_user_domain_name,
trust_id=trust_id)
:param trust_id: a trust id to scope the auth.
def _session(username, password, project_name, user_domain_name=None,
project_domain_name=None):
:returns: a password auth plugin object.
'''
passwd_kwargs = dict(
auth_url=base.retrieve_auth_url(),
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password
username=username,
password=password
)
if CONF.use_identity_api_v3:
passwd_kwargs.update(dict(
project_name=project_name,
user_domain_name=user_domain_name,
project_domain_name=project_domain_name
project_domain_name=project_domain_name,
trust_id=trust_id
))
auth = keystone_identity.v3.Password(**passwd_kwargs)
else:
passwd_kwargs.update(dict(
tenant_name=project_name
tenant_name=project_name,
trust_id=trust_id
))
auth = keystone_identity.v2.Password(**passwd_kwargs)
return keystone_session.Session(auth=auth)
return auth
def session_for_admin():
'''Return a Keystone session for the admin user.'''
return _session(
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password,
project_name=CONF.keystone_authtoken.admin_tenant_name,
user_domain_name=CONF.admin_user_domain_name,
project_domain_name=CONF.admin_project_domain_name)
def _token_auth(token, project_id, project_domain_name='Default'):
'''Return a token auth plugin object.
:param token: the token to use for authentication.
:param project_id: the project(ex. tenant) id to scope the auth.
:returns: a token auth plugin object.
'''
token_kwargs = dict(
auth_url=base.retrieve_auth_url(),
token=token
)
if CONF.use_identity_api_v3:
token_kwargs.update(dict(
project_id=project_id,
project_domain_name=project_domain_name,
))
auth = keystone_identity.v3.Token(**token_kwargs)
else:
token_kwargs.update(dict(
tenant_id=project_id
))
auth = keystone_identity.v2.Token(**token_kwargs)
return auth

View File

@ -55,8 +55,8 @@ def client(username, password, trust_id=None):
'''
if trust_id:
proxyclient = k.client_for_proxy_user(username, password, trust_id)
return client_from_token(proxyclient.auth_token)
proxyauth = k.auth_for_proxy_user(username, password, trust_id)
return client_from_token(k.token_from_auth(proxyauth))
else:
return swiftclient.Connection(
auth_version='2.0',

View File

@ -61,8 +61,8 @@ def create_proxy_user_for_job_execution(job_execution):
'''
username = 'job_{0}'.format(job_execution.id)
password = proxy_user_create(username)
current_user = k.client()
proxy_user = k.client_for_proxy_user(username, password)
current_user = k.auth()
proxy_user = k.auth_for_proxy(username, password)
trust_id = t.create_trust(trustor=current_user,
trustee=proxy_user,
role_names=CONF.proxy_user_role_names)
@ -87,9 +87,9 @@ def delete_proxy_user_for_job_execution(job_execution):
proxy_username = proxy_configs.get('proxy_username')
proxy_password = proxy_configs.get('proxy_password')
proxy_trust_id = proxy_configs.get('proxy_trust_id')
proxy_user = k.client_for_proxy_user(proxy_username,
proxy_password,
proxy_trust_id)
proxy_user = k.auth_for_proxy(proxy_username,
proxy_password,
proxy_trust_id)
t.delete_trust(proxy_user, proxy_trust_id)
proxy_user_delete(proxy_username)
update = job_execution.job_configs.to_dict()
@ -108,8 +108,8 @@ def create_proxy_user_for_cluster(cluster):
return cluster
username = 'cluster_{0}'.format(cluster.id)
password = proxy_user_create(username)
current_user = k.client()
proxy_user = k.client_for_proxy_user(username, password)
current_user = k.auth()
proxy_user = k.auth_for_proxy(username, password)
trust_id = t.create_trust(trustor=current_user,
trustee=proxy_user,
role_names=CONF.proxy_user_role_names)
@ -133,9 +133,9 @@ def delete_proxy_user_for_cluster(cluster):
proxy_username = proxy_configs.get('proxy_username')
proxy_password = proxy_configs.get('proxy_password')
proxy_trust_id = proxy_configs.get('proxy_trust_id')
proxy_user = k.client_for_proxy_user(proxy_username,
proxy_password,
proxy_trust_id)
proxy_user = k.auth_for_proxy(proxy_username,
proxy_password,
proxy_trust_id)
t.delete_trust(proxy_user, proxy_trust_id)
proxy_user_delete(proxy_username)
update = {'cluster_configs': cluster.cluster_configs.to_dict()}