Refactor request methods onto request object

Take advantage of webob Request objects to move methods that deal with
manipulating the request onto the request.

This removes a debug statement that prints a static list of headers that
will be removed because we no longer keep the static list. This list was
not really useful to debugging as the list doesn't change.

Change-Id: I8601cd7ff9cea778911cbfc306dc03089530bc9d
This commit is contained in:
Jamie Lennox 2015-05-06 13:31:45 +10:00
parent 6f9b7a4528
commit 9fd3986089
4 changed files with 404 additions and 217 deletions

View File

@ -219,6 +219,7 @@ from keystonemiddleware.auth_token import _base
from keystonemiddleware.auth_token import _cache from keystonemiddleware.auth_token import _cache
from keystonemiddleware.auth_token import _exceptions as exc from keystonemiddleware.auth_token import _exceptions as exc
from keystonemiddleware.auth_token import _identity from keystonemiddleware.auth_token import _identity
from keystonemiddleware.auth_token import _request
from keystonemiddleware.auth_token import _revocations from keystonemiddleware.auth_token import _revocations
from keystonemiddleware.auth_token import _signing_dir from keystonemiddleware.auth_token import _signing_dir
from keystonemiddleware.auth_token import _user_plugin from keystonemiddleware.auth_token import _user_plugin
@ -363,26 +364,6 @@ CONF.register_opts(_OPTS, group=_base.AUTHTOKEN_GROUP)
_LOG = logging.getLogger(__name__) _LOG = logging.getLogger(__name__)
_HEADER_TEMPLATE = {
'X%s-Domain-Id': 'domain_id',
'X%s-Domain-Name': 'domain_name',
'X%s-Project-Id': 'project_id',
'X%s-Project-Name': 'project_name',
'X%s-Project-Domain-Id': 'project_domain_id',
'X%s-Project-Domain-Name': 'project_domain_name',
'X%s-User-Id': 'user_id',
'X%s-User-Name': 'username',
'X%s-User-Domain-Id': 'user_domain_id',
'X%s-User-Domain-Name': 'user_domain_name',
}
_DEPRECATED_HEADER_TEMPLATE = {
'X-User': 'username',
'X-Tenant-Id': 'project_id',
'X-Tenant-Name': 'project_name',
'X-Tenant': 'project_name',
}
class _BIND_MODE(object): class _BIND_MODE(object):
DISABLED = 'disabled' DISABLED = 'disabled'
@ -400,42 +381,6 @@ def _token_is_v3(token_info):
return ('token' in token_info) return ('token' in token_info)
def _v3_to_v2_catalog(catalog):
"""Convert a catalog to v2 format.
X_SERVICE_CATALOG must be specified in v2 format. If you get a token
that is in v3 convert it.
"""
v2_services = []
for v3_service in catalog:
# first copy over the entries we allow for the service
v2_service = {'type': v3_service['type']}
try:
v2_service['name'] = v3_service['name']
except KeyError:
pass
# now convert the endpoints. Because in v3 we specify region per
# URL not per group we have to collect all the entries of the same
# region together before adding it to the new service.
regions = {}
for v3_endpoint in v3_service.get('endpoints', []):
region_name = v3_endpoint.get('region')
try:
region = regions[region_name]
except KeyError:
region = {'region': region_name} if region_name else {}
regions[region_name] = region
interface_name = v3_endpoint['interface'].lower() + 'URL'
region[interface_name] = v3_endpoint['url']
v2_service['endpoints'] = list(regions.values())
v2_services.append(v2_service)
return v2_services
def _conf_values_type_convert(conf): def _conf_values_type_convert(conf):
"""Convert conf values into correct type.""" """Convert conf values into correct type."""
if not conf: if not conf:
@ -518,7 +463,6 @@ class AuthProtocol(object):
self._check_revocations_for_cached = self._conf_get( self._check_revocations_for_cached = self._conf_get(
'check_revocations_for_cached') 'check_revocations_for_cached')
self._init_auth_headers()
def _conf_get(self, name, group=_base.AUTHTOKEN_GROUP): def _conf_get(self, name, group=_base.AUTHTOKEN_GROUP):
# try config from paste-deploy first # try config from paste-deploy first
@ -527,7 +471,7 @@ class AuthProtocol(object):
else: else:
return CONF[group][name] return CONF[group][name]
@webob.dec.wsgify @webob.dec.wsgify(RequestClass=_request._AuthTokenRequest)
def __call__(self, request): def __call__(self, request):
"""Handle incoming request. """Handle incoming request.
@ -536,7 +480,7 @@ class AuthProtocol(object):
""" """
self._token_cache.initialize(request.environ) self._token_cache.initialize(request.environ)
self._remove_auth_headers(request) request.remove_auth_headers()
try: try:
user_auth_ref = None user_auth_ref = None
@ -548,8 +492,8 @@ class AuthProtocol(object):
user_auth_ref, user_token_info = self._validate_token( user_auth_ref, user_token_info = self._validate_token(
user_token_info, request.environ) user_token_info, request.environ)
request.environ['keystone.token_info'] = user_token_info request.environ['keystone.token_info'] = user_token_info
user_headers = self._build_user_headers(user_auth_ref) request.set_user_headers(user_auth_ref,
request.headers.update(user_headers) self._include_service_catalog)
except exc.InvalidToken: except exc.InvalidToken:
if self._delay_auth_decision: if self._delay_auth_decision:
self._LOG.info( self._LOG.info(
@ -567,8 +511,7 @@ class AuthProtocol(object):
if serv_token is not None: if serv_token is not None:
serv_auth_ref, serv_token_info = self._validate_token( serv_auth_ref, serv_token_info = self._validate_token(
serv_token, request.environ) serv_token, request.environ)
serv_headers = self._build_service_headers(serv_auth_ref) request.set_service_headers(serv_auth_ref)
request.headers.update(serv_headers)
except exc.InvalidToken: except exc.InvalidToken:
if self._delay_auth_decision: if self._delay_auth_decision:
self._LOG.info( self._LOG.info(
@ -597,41 +540,6 @@ class AuthProtocol(object):
return response return response
def _init_auth_headers(self):
"""Initialize auth header list.
Both user and service token headers are generated.
"""
auth_headers = ['X-Service-Catalog',
'X-Identity-Status',
'X-Service-Identity-Status',
'X-Roles',
'X-Service-Roles']
for key in six.iterkeys(_HEADER_TEMPLATE):
auth_headers.append(key % '')
# Service headers
auth_headers.append(key % '-Service')
# Deprecated headers
auth_headers.append('X-Role')
for key in six.iterkeys(_DEPRECATED_HEADER_TEMPLATE):
auth_headers.append(key)
self._auth_headers = auth_headers
def _remove_auth_headers(self, request):
"""Remove headers so a user can't fake authentication.
Both user and service token headers are removed.
:param env: wsgi request environment
"""
self._LOG.debug('Removing headers from request environment: %s',
','.join(self._auth_headers))
for k in self._auth_headers:
request.headers.pop(k, None)
def _get_user_token_from_request(self, request): def _get_user_token_from_request(self, request):
"""Get token id from request. """Get token id from request.
@ -774,61 +682,6 @@ class AuthProtocol(object):
self._LOG.warn(_LW('Authorization failed for token')) self._LOG.warn(_LW('Authorization failed for token'))
raise exc.InvalidToken(_('Token authorization failed')) raise exc.InvalidToken(_('Token authorization failed'))
def _build_user_headers(self, auth_ref):
"""Convert token object into headers.
Build headers that represent authenticated user - see main
doc info at start of file for details of headers to be defined.
:param token_info: token object returned by identity
server on authentication
:raises exc.InvalidToken: when unable to parse token object
"""
roles = ','.join(auth_ref.role_names)
rval = {
'X-Identity-Status': 'Confirmed',
'X-Roles': roles,
}
for header_tmplt, attr in six.iteritems(_HEADER_TEMPLATE):
rval[header_tmplt % ''] = getattr(auth_ref, attr)
# Deprecated headers
rval['X-Role'] = roles
for header_tmplt, attr in six.iteritems(_DEPRECATED_HEADER_TEMPLATE):
rval[header_tmplt] = getattr(auth_ref, attr)
if self._include_service_catalog and auth_ref.has_service_catalog():
catalog = auth_ref.service_catalog.get_data()
if auth_ref.version == 'v3':
catalog = _v3_to_v2_catalog(catalog)
rval['X-Service-Catalog'] = jsonutils.dumps(catalog)
return rval
def _build_service_headers(self, auth_ref):
"""Convert token object into service headers.
Build headers that represent authenticated user - see main
doc info at start of file for details of headers to be defined.
:param auth_ref: authentication information
"""
roles = ','.join(auth_ref.role_names)
rval = {
'X-Service-Identity-Status': 'Confirmed',
'X-Service-Roles': roles,
}
header_type = '-Service'
for header_tmplt, attr in six.iteritems(_HEADER_TEMPLATE):
rval[header_tmplt % header_type] = getattr(auth_ref, attr)
return rval
def _invalid_user_token(self, msg=False): def _invalid_user_token(self, msg=False):
# NOTE(jamielennox): use False as the default so that None is valid # NOTE(jamielennox): use False as the default so that None is valid
if msg is False: if msg is False:

View File

@ -0,0 +1,180 @@
# 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 itertools
from oslo_serialization import jsonutils
import six
import webob
def _v3_to_v2_catalog(catalog):
"""Convert a catalog to v2 format.
X_SERVICE_CATALOG must be specified in v2 format. If you get a token
that is in v3 convert it.
"""
v2_services = []
for v3_service in catalog:
# first copy over the entries we allow for the service
v2_service = {'type': v3_service['type']}
try:
v2_service['name'] = v3_service['name']
except KeyError:
pass
# now convert the endpoints. Because in v3 we specify region per
# URL not per group we have to collect all the entries of the same
# region together before adding it to the new service.
regions = {}
for v3_endpoint in v3_service.get('endpoints', []):
region_name = v3_endpoint.get('region')
try:
region = regions[region_name]
except KeyError:
region = {'region': region_name} if region_name else {}
regions[region_name] = region
interface_name = v3_endpoint['interface'].lower() + 'URL'
region[interface_name] = v3_endpoint['url']
v2_service['endpoints'] = list(regions.values())
v2_services.append(v2_service)
return v2_services
class _AuthTokenRequest(webob.Request):
_HEADER_TEMPLATE = {
'X%s-Domain-Id': 'domain_id',
'X%s-Domain-Name': 'domain_name',
'X%s-Project-Id': 'project_id',
'X%s-Project-Name': 'project_name',
'X%s-Project-Domain-Id': 'project_domain_id',
'X%s-Project-Domain-Name': 'project_domain_name',
'X%s-User-Id': 'user_id',
'X%s-User-Name': 'username',
'X%s-User-Domain-Id': 'user_domain_id',
'X%s-User-Domain-Name': 'user_domain_name',
}
_ROLES_TEMPLATE = 'X%s-Roles'
_USER_HEADER_PREFIX = ''
_SERVICE_HEADER_PREFIX = '-Service'
_USER_STATUS_HEADER = 'X-Identity-Status'
_SERVICE_STATUS_HEADER = 'X-Service-Identity-Status'
_SERVICE_CATALOG_HEADER = 'X-Service-Catalog'
_CONFIRMED = 'Confirmed'
_INVALID = 'Invalid'
# header names that have been deprecated in favour of something else.
_DEPRECATED_HEADER_MAP = {
'X-Role': 'X-Roles',
'X-User': 'X-User-Name',
'X-Tenant-Id': 'X-Project-Id',
'X-Tenant-Name': 'X-Project-Name',
'X-Tenant': 'X-Project-Name',
}
def _confirmed(cls, value):
return cls._CONFIRMED if value else cls._INVALID
@property
def user_token_valid(self):
"""User token is marked as valid.
:returns: True if the X-Identity-Status header is set to Confirmed.
:rtype: bool
"""
return self.headers[self._USER_STATUS_HEADER] == self._CONFIRMED
@user_token_valid.setter
def user_token_valid(self, value):
self.headers[self._USER_STATUS_HEADER] = self._confirmed(value)
@property
def service_token_valid(self):
"""Service token is marked as valid.
:returns: True if the X-Service-Identity-Status header
is set to Confirmed.
:rtype: bool
"""
return self.headers[self._SERVICE_STATUS_HEADER] == self._CONFIRMED
@service_token_valid.setter
def service_token_valid(self, value):
self.headers[self._SERVICE_STATUS_HEADER] = self._confirmed(value)
def _set_auth_headers(self, auth_ref, prefix):
names = ','.join(auth_ref.role_names)
self.headers[self._ROLES_TEMPLATE % prefix] = names
for header_tmplt, attr in six.iteritems(self._HEADER_TEMPLATE):
self.headers[header_tmplt % prefix] = getattr(auth_ref, attr)
def set_user_headers(self, auth_ref, include_service_catalog):
"""Convert token object into headers.
Build headers that represent authenticated user - see main
doc info at start of __init__ file for details of headers to be defined
"""
self._set_auth_headers(auth_ref, self._USER_HEADER_PREFIX)
for k, v in six.iteritems(self._DEPRECATED_HEADER_MAP):
self.headers[k] = self.headers[v]
if include_service_catalog and auth_ref.has_service_catalog():
catalog = auth_ref.service_catalog.get_data()
if auth_ref.version == 'v3':
catalog = _v3_to_v2_catalog(catalog)
c = jsonutils.dumps(catalog)
self.headers[self._SERVICE_CATALOG_HEADER] = c
self.user_token_valid = True
def set_service_headers(self, auth_ref):
"""Convert token object into service headers.
Build headers that represent authenticated user - see main
doc info at start of __init__ file for details of headers to be defined
"""
self._set_auth_headers(auth_ref, self._SERVICE_HEADER_PREFIX)
self.service_token_valid = True
def _all_auth_headers(self):
"""All the authentication headers that can be set on the request"""
yield self._SERVICE_CATALOG_HEADER
yield self._USER_STATUS_HEADER
yield self._SERVICE_STATUS_HEADER
for header in self._DEPRECATED_HEADER_MAP:
yield header
prefixes = (self._USER_HEADER_PREFIX, self._SERVICE_HEADER_PREFIX)
for tmpl, prefix in itertools.product(self._HEADER_TEMPLATE, prefixes):
yield tmpl % prefix
for prefix in prefixes:
yield self._ROLES_TEMPLATE % prefix
def remove_auth_headers(self):
"""Remove headers so a user can't fake authentication."""
for header in self._all_auth_headers():
self.headers.pop(header, None)

View File

@ -23,7 +23,6 @@ import time
import uuid import uuid
import fixtures import fixtures
from keystoneclient import access
from keystoneclient import auth from keystoneclient import auth
from keystoneclient.common import cms from keystoneclient.common import cms
from keystoneclient import exceptions from keystoneclient import exceptions
@ -1839,69 +1838,6 @@ class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
self.assertEqual(self.middleware._token_cache.get(token), data) self.assertEqual(self.middleware._token_cache.get(token), data)
class CatalogConversionTests(BaseAuthTokenMiddlewareTest):
PUBLIC_URL = 'http://server:5000/v2.0'
ADMIN_URL = 'http://admin:35357/v2.0'
INTERNAL_URL = 'http://internal:5000/v2.0'
REGION_ONE = 'RegionOne'
REGION_TWO = 'RegionTwo'
REGION_THREE = 'RegionThree'
def test_basic_convert(self):
token = fixture.V3Token()
s = token.add_service(type='identity')
s.add_standard_endpoints(public=self.PUBLIC_URL,
admin=self.ADMIN_URL,
internal=self.INTERNAL_URL,
region=self.REGION_ONE)
auth_ref = access.AccessInfo.factory(body=token)
catalog_data = auth_ref.service_catalog.get_data()
catalog = auth_token._v3_to_v2_catalog(catalog_data)
self.assertEqual(1, len(catalog))
service = catalog[0]
self.assertEqual(1, len(service['endpoints']))
endpoints = service['endpoints'][0]
self.assertEqual('identity', service['type'])
self.assertEqual(4, len(endpoints))
self.assertEqual(self.PUBLIC_URL, endpoints['publicURL'])
self.assertEqual(self.ADMIN_URL, endpoints['adminURL'])
self.assertEqual(self.INTERNAL_URL, endpoints['internalURL'])
self.assertEqual(self.REGION_ONE, endpoints['region'])
def test_multi_region(self):
token = fixture.V3Token()
s = token.add_service(type='identity')
s.add_endpoint('internal', self.INTERNAL_URL, region=self.REGION_ONE)
s.add_endpoint('public', self.PUBLIC_URL, region=self.REGION_TWO)
s.add_endpoint('admin', self.ADMIN_URL, region=self.REGION_THREE)
auth_ref = access.AccessInfo.factory(body=token)
catalog_data = auth_ref.service_catalog.get_data()
catalog = auth_token._v3_to_v2_catalog(catalog_data)
self.assertEqual(1, len(catalog))
service = catalog[0]
# the 3 regions will come through as 3 separate endpoints
expected = [{'internalURL': self.INTERNAL_URL,
'region': self.REGION_ONE},
{'publicURL': self.PUBLIC_URL,
'region': self.REGION_TWO},
{'adminURL': self.ADMIN_URL,
'region': self.REGION_THREE}]
self.assertEqual('identity', service['type'])
self.assertEqual(3, len(service['endpoints']))
for e in expected:
self.assertIn(e, expected)
class DelayedAuthTests(BaseAuthTokenMiddlewareTest): class DelayedAuthTests(BaseAuthTokenMiddlewareTest):
def test_header_in_401(self): def test_header_in_401(self):

View File

@ -0,0 +1,218 @@
# 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 itertools
import uuid
from keystoneclient import access
from keystoneclient import fixture
from keystonemiddleware.auth_token import _request
from keystonemiddleware.tests.unit import utils
class RequestObjectTests(utils.TestCase):
def setUp(self):
super(RequestObjectTests, self).setUp()
self.request = _request._AuthTokenRequest.blank('/')
def test_setting_user_token_valid(self):
self.assertNotIn('X-Identity-Status', self.request.headers)
self.request.user_token_valid = True
self.assertEqual('Confirmed',
self.request.headers['X-Identity-Status'])
self.assertTrue(self.request.user_token_valid)
self.request.user_token_valid = False
self.assertEqual('Invalid',
self.request.headers['X-Identity-Status'])
self.assertFalse(self.request.user_token_valid)
def test_setting_service_token_valid(self):
self.assertNotIn('X-Service-Identity-Status', self.request.headers)
self.request.service_token_valid = True
self.assertEqual('Confirmed',
self.request.headers['X-Service-Identity-Status'])
self.assertTrue(self.request.service_token_valid)
self.request.service_token_valid = False
self.assertEqual('Invalid',
self.request.headers['X-Service-Identity-Status'])
self.assertFalse(self.request.service_token_valid)
def test_removing_headers(self):
GOOD = ('X-Auth-Token',
'unknownstring',
uuid.uuid4().hex)
BAD = ('X-Domain-Id',
'X-Domain-Name',
'X-Project-Id',
'X-Project-Name',
'X-Project-Domain-Id',
'X-Project-Domain-Name',
'X-User-Id',
'X-User-Name',
'X-User-Domain-Id',
'X-User-Domain-Name',
'X-Roles',
'X-Identity-Status',
'X-Service-Domain-Id',
'X-Service-Domain-Name',
'X-Service-Project-Id',
'X-Service-Project-Name',
'X-Service-Project-Domain-Id',
'X-Service-Project-Domain-Name',
'X-Service-User-Id',
'X-Service-User-Name',
'X-Service-User-Domain-Id',
'X-Service-User-Domain-Name',
'X-Service-Roles',
'X-Service-Identity-Status',
'X-Service-Catalog',
'X-Role',
'X-User',
'X-Tenant-Id',
'X-Tenant-Name',
'X-Tenant',
)
header_vals = {}
for header in itertools.chain(GOOD, BAD):
v = uuid.uuid4().hex
header_vals[header] = v
self.request.headers[header] = v
self.request.remove_auth_headers()
for header in BAD:
self.assertNotIn(header, self.request.headers)
for header in GOOD:
self.assertEqual(header_vals[header], self.request.headers[header])
def _test_v3_headers(self, token, prefix):
self.assertEqual(token.domain_id,
self.request.headers['X%s-Domain-Id' % prefix])
self.assertEqual(token.domain_name,
self.request.headers['X%s-Domain-Name' % prefix])
self.assertEqual(token.project_id,
self.request.headers['X%s-Project-Id' % prefix])
self.assertEqual(token.project_name,
self.request.headers['X%s-Project-Name' % prefix])
self.assertEqual(
token.project_domain_id,
self.request.headers['X%s-Project-Domain-Id' % prefix])
self.assertEqual(
token.project_domain_name,
self.request.headers['X%s-Project-Domain-Name' % prefix])
self.assertEqual(token.user_id,
self.request.headers['X%s-User-Id' % prefix])
self.assertEqual(token.user_name,
self.request.headers['X%s-User-Name' % prefix])
self.assertEqual(
token.user_domain_id,
self.request.headers['X%s-User-Domain-Id' % prefix])
self.assertEqual(
token.user_domain_name,
self.request.headers['X%s-User-Domain-Name' % prefix])
def test_project_scoped_user_headers(self):
token = fixture.V3Token()
token.set_project_scope()
token_id = uuid.uuid4().hex
auth_ref = access.AccessInfo.factory(token_id=token_id, body=token)
self.request.set_user_headers(auth_ref, include_service_catalog=True)
self._test_v3_headers(token, '')
def test_project_scoped_service_headers(self):
token = fixture.V3Token()
token.set_project_scope()
token_id = uuid.uuid4().hex
auth_ref = access.AccessInfo.factory(token_id=token_id, body=token)
self.request.set_service_headers(auth_ref)
self._test_v3_headers(token, '-Service')
class CatalogConversionTests(utils.TestCase):
PUBLIC_URL = 'http://server:5000/v2.0'
ADMIN_URL = 'http://admin:35357/v2.0'
INTERNAL_URL = 'http://internal:5000/v2.0'
REGION_ONE = 'RegionOne'
REGION_TWO = 'RegionTwo'
REGION_THREE = 'RegionThree'
def test_basic_convert(self):
token = fixture.V3Token()
s = token.add_service(type='identity')
s.add_standard_endpoints(public=self.PUBLIC_URL,
admin=self.ADMIN_URL,
internal=self.INTERNAL_URL,
region=self.REGION_ONE)
auth_ref = access.AccessInfo.factory(body=token)
catalog_data = auth_ref.service_catalog.get_data()
catalog = _request._v3_to_v2_catalog(catalog_data)
self.assertEqual(1, len(catalog))
service = catalog[0]
self.assertEqual(1, len(service['endpoints']))
endpoints = service['endpoints'][0]
self.assertEqual('identity', service['type'])
self.assertEqual(4, len(endpoints))
self.assertEqual(self.PUBLIC_URL, endpoints['publicURL'])
self.assertEqual(self.ADMIN_URL, endpoints['adminURL'])
self.assertEqual(self.INTERNAL_URL, endpoints['internalURL'])
self.assertEqual(self.REGION_ONE, endpoints['region'])
def test_multi_region(self):
token = fixture.V3Token()
s = token.add_service(type='identity')
s.add_endpoint('internal', self.INTERNAL_URL, region=self.REGION_ONE)
s.add_endpoint('public', self.PUBLIC_URL, region=self.REGION_TWO)
s.add_endpoint('admin', self.ADMIN_URL, region=self.REGION_THREE)
auth_ref = access.AccessInfo.factory(body=token)
catalog_data = auth_ref.service_catalog.get_data()
catalog = _request._v3_to_v2_catalog(catalog_data)
self.assertEqual(1, len(catalog))
service = catalog[0]
# the 3 regions will come through as 3 separate endpoints
expected = [{'internalURL': self.INTERNAL_URL,
'region': self.REGION_ONE},
{'publicURL': self.PUBLIC_URL,
'region': self.REGION_TWO},
{'adminURL': self.ADMIN_URL,
'region': self.REGION_THREE}]
self.assertEqual('identity', service['type'])
self.assertEqual(3, len(service['endpoints']))
for e in expected:
self.assertIn(e, expected)