Allow federated users to auth with domain scope

When a federated user logs in, openstack_auth receives an unscoped
token and no user_domain_name parameter. Currently, if the federated
user has a role in one or more domains, but no roles in any projects,
openstack_auth prevents authorization and denies the user's login with
the error "You are not authorized for any projects or domains." This is
a problem because first, it's inaccurate, as the user is authorized for
at least one domain, and second, a keystone administrator may want to
give federated users access to a domain without any projects in it, for
example so delegate the creation of projects to the federated users
themselves. This patch allows federated users without project roles to
log in by looking up domains as well as projects when attempting to
scope the token. This lookup is skipped if the domain was passed as
part of the request.

This patch also slightly restructures the OpenStackAuthTestsWebSSO
and OpenStackAuthTestsV3 tests because mox needs to simulate only one instance
of the plugin but two instances of the client objects for every call to
authenticate().

Closes-bug: #1649101

Change-Id: I151218ff28c0728898ed5315d63dd8122ce3b166
This commit is contained in:
Colleen Murphy 2016-10-20 21:59:55 +02:00
parent f3c21575d2
commit ca3166707b
2 changed files with 83 additions and 18 deletions

View File

@ -98,6 +98,18 @@ class BasePlugin(object):
msg = _('Unable to retrieve authorized projects.')
raise exceptions.KeystoneAuthException(msg)
def list_domains(self, session, auth_plugin, auth_ref=None):
try:
if self.keystone_version >= 3:
client = v3_client.Client(session=session, auth=auth_plugin)
return client.auth.domains()
else:
return []
except (keystone_exceptions.ClientException,
keystone_exceptions.AuthorizationFailure):
msg = _('Unable to retrieve authorized domains.')
raise exceptions.KeystoneAuthException(msg)
def get_access_info(self, keystone_auth):
"""Get the access info from an unscoped auth
@ -190,22 +202,36 @@ class BasePlugin(object):
session = utils.get_session()
auth_url = unscoped_auth.auth_url
if not domain_name or utils.get_keystone_version() < 3:
if utils.get_keystone_version() < 3:
return None, None
if domain_name:
domains = [domain_name]
else:
domains = self.list_domains(session,
unscoped_auth,
unscoped_auth_ref)
domains = [domain.name for domain in domains if domain.enabled]
# domain support can require domain scoped tokens to perform
# identity operations depending on the policy files being used
# for keystone.
domain_auth = None
domain_auth_ref = None
try:
for domain_name in domains:
token = unscoped_auth_ref.auth_token
domain_auth = utils.get_token_auth_plugin(
auth_url,
token,
domain_name=domain_name)
domain_auth_ref = domain_auth.get_access(session)
except (keystone_exceptions.ClientException,
keystone_exceptions.AuthorizationFailure):
LOG.debug('Error getting domain scoped token.', exc_info=True)
try:
domain_auth_ref = domain_auth.get_access(session)
except (keystone_exceptions.ClientException,
keystone_exceptions.AuthorizationFailure):
pass
else:
if len(domains) > 1:
LOG.info("More than one valid domain found for user %s,"
" scoping to %s" %
(unscoped_auth_ref.user_id, domain_name))
break
return domain_auth, domain_auth_ref

View File

@ -108,8 +108,27 @@ class OpenStackAuthFederatedTestsMixin(object):
client.federation.projects = self.mox.CreateMockAnything()
client.federation.projects.list().AndReturn(projects)
def _mock_unscoped_list_domains(self, client, domains):
client.auth = self.mox.CreateMockAnything()
client.auth.domains().AndReturn(domains)
def _mock_unscoped_token_client(self, unscoped, auth_url=None,
client=True):
client=True, plugin=None):
if not auth_url:
auth_url = settings.OPENSTACK_KEYSTONE_URL
if unscoped and not plugin:
plugin = self._create_token_auth(
None,
token=unscoped.auth_token,
url=auth_url)
plugin.get_access(mox.IsA(session.Session)).AndReturn(unscoped)
plugin.auth_url = auth_url
if client:
return self.ks_client_module.Client(
session=mox.IsA(session.Session),
auth=plugin)
def _mock_plugin(self, unscoped, auth_url=None):
if not auth_url:
auth_url = settings.OPENSTACK_KEYSTONE_URL
plugin = self._create_token_auth(
@ -117,16 +136,17 @@ class OpenStackAuthFederatedTestsMixin(object):
token=unscoped.auth_token,
url=auth_url)
plugin.get_access(mox.IsA(session.Session)).AndReturn(unscoped)
plugin.auth_url = auth_url
if client:
return self.ks_client_module.Client(
session=mox.IsA(session.Session),
auth=plugin)
plugin.auth_url = settings.OPENSTACK_KEYSTONE_URL
return plugin
def _mock_federated_client_list_projects(self, unscoped, projects):
client = self._mock_unscoped_token_client(unscoped)
def _mock_federated_client_list_projects(self, unscoped_auth, projects):
client = self._mock_unscoped_token_client(None, plugin=unscoped_auth)
self._mock_unscoped_federated_list_projects(client, projects)
def _mock_federated_client_list_domains(self, unscoped_auth, domains):
client = self._mock_unscoped_token_client(None, plugin=unscoped_auth)
self._mock_unscoped_list_domains(client, domains)
class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
@ -885,6 +905,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin,
self.data = data_v3.generate_test_data(service_providers=True)
self.sp_data = data_v3.generate_test_data(endpoint='http://sp2')
projects = [self.data.project_one, self.data.project_two]
domains = []
user = self.data.user
unscoped = self.data.unscoped_access_info
form_data = self.get_form_data(user)
@ -925,7 +946,13 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin,
# mock authenticate for service provider
sp_projects = [self.sp_data.project_one, self.sp_data.project_two]
sp_unscoped = self.sp_data.federated_unscoped_access_info
client = self._mock_unscoped_token_client(sp_unscoped, plugin.auth_url)
sp_unscoped_auth = self._mock_plugin(sp_unscoped,
auth_url=plugin.auth_url)
client = self._mock_unscoped_token_client(None, plugin.auth_url,
plugin=sp_unscoped_auth)
self._mock_unscoped_list_domains(client, domains)
client = self._mock_unscoped_token_client(None, plugin.auth_url,
plugin=sp_unscoped_auth)
self._mock_unscoped_federated_list_projects(client, sp_projects)
self._mock_scoped_client_for_tenant(sp_unscoped,
self.sp_data.project_one.id,
@ -961,6 +988,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin,
self.data = data_v3.generate_test_data(service_providers=True)
keystone_provider = 'localkeystone'
projects = [self.data.project_one, self.data.project_two]
domains = []
user = self.data.user
unscoped = self.data.unscoped_access_info
form_data = self.get_form_data(user)
@ -971,7 +999,12 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin,
self._mock_unscoped_token_client(unscoped,
auth_url=auth_url,
client=False)
client = self._mock_unscoped_token_client(unscoped, auth_url)
unscoped_auth = self._mock_plugin(unscoped)
client = self._mock_unscoped_token_client(None, auth_url=auth_url,
plugin=unscoped_auth)
self._mock_unscoped_list_domains(client, domains)
client = self._mock_unscoped_token_client(None, auth_url=auth_url,
plugin=unscoped_auth)
self._mock_unscoped_list_projects(client, user, projects)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
@ -1154,11 +1187,14 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin,
def test_websso_login(self):
projects = [self.data.project_one, self.data.project_two]
domains = []
unscoped = self.data.federated_unscoped_access_info
token = unscoped.auth_token
unscoped_auth = self._mock_plugin(unscoped)
form_data = {'token': token}
self._mock_federated_client_list_projects(unscoped, projects)
self._mock_federated_client_list_domains(unscoped_auth, domains)
self._mock_federated_client_list_projects(unscoped_auth, projects)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
self.mox.ReplayAll()
@ -1173,11 +1209,14 @@ class OpenStackAuthTestsWebSSO(OpenStackAuthTestsMixin,
settings.OPENSTACK_KEYSTONE_URL = 'http://auth.openstack.org:5000/v3'
projects = [self.data.project_one, self.data.project_two]
domains = []
unscoped = self.data.federated_unscoped_access_info
token = unscoped.auth_token
unscoped_auth = self._mock_plugin(unscoped)
form_data = {'token': token}
self._mock_federated_client_list_projects(unscoped, projects)
self._mock_federated_client_list_domains(unscoped_auth, domains)
self._mock_federated_client_list_projects(unscoped_auth, projects)
self._mock_scoped_client_for_tenant(unscoped, self.data.project_one.id)
self.mox.ReplayAll()