Hash credentials on user, project/tenant and pwd

Preprovision credential provider hashes credentials based on all
fields specified in the YAML. The same configured credentials can
be used to build both v2 and v3 credential objects, so we need to
hash on the fields that are common between v2 and v3 only.

Because v2 only understand tenants (and not project) the
intersection would be only user and password. Because of that, and
because we want to promote project against tenant, accept
project in v2 credentials as well, by translating it to tenant at
__init__ time.

Change-Id: Ib62c26cdffc2db6f6352d9889c689db3ff09aa5d
This commit is contained in:
Andrea Frittoli (andreaf) 2016-05-18 19:14:22 +01:00
parent 2855a18e75
commit 52deb8b155
5 changed files with 172 additions and 84 deletions

View File

@ -186,11 +186,6 @@ def get_configured_credentials(credential_type, fill_in=True,
params[attr] = getattr(_section, attr) params[attr] = getattr(_section, attr)
else: else:
params[attr] = getattr(_section, prefix + "_" + attr) params[attr] = getattr(_section, prefix + "_" + attr)
# NOTE(andreaf) v2 API still uses tenants, so we must translate project
# to tenant before building the Credentials object
if identity_version == 'v2':
params['tenant_name'] = params.get('project_name')
params.pop('project_name', None)
# Build and validate credentials. We are reading configured credentials, # Build and validate credentials. We are reading configured credentials,
# so validate them even if fill_in is False # so validate them even if fill_in is False
credentials = get_credentials(fill_in=fill_in, credentials = get_credentials(fill_in=fill_in,

View File

@ -43,6 +43,11 @@ def read_accounts_yaml(path):
class PreProvisionedCredentialProvider(cred_provider.CredentialProvider): class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
# Exclude from the hash fields specific to v2 or v3 identity API
# i.e. only include user*, project*, tenant* and password
HASH_CRED_FIELDS = (set(auth.KeystoneV2Credentials.ATTRIBUTES) &
set(auth.KeystoneV3Credentials.ATTRIBUTES))
def __init__(self, identity_version, test_accounts_file, def __init__(self, identity_version, test_accounts_file,
accounts_lock_dir, name=None, credentials_domain=None, accounts_lock_dir, name=None, credentials_domain=None,
admin_role=None, object_storage_operator_role=None, admin_role=None, object_storage_operator_role=None,
@ -104,6 +109,7 @@ class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
object_storage_operator_role=None, object_storage_operator_role=None,
object_storage_reseller_admin_role=None): object_storage_reseller_admin_role=None):
hash_dict = {'roles': {}, 'creds': {}, 'networks': {}} hash_dict = {'roles': {}, 'creds': {}, 'networks': {}}
# Loop over the accounts read from the yaml file # Loop over the accounts read from the yaml file
for account in accounts: for account in accounts:
roles = [] roles = []
@ -116,7 +122,9 @@ class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
if 'resources' in account: if 'resources' in account:
resources = account.pop('resources') resources = account.pop('resources')
temp_hash = hashlib.md5() temp_hash = hashlib.md5()
temp_hash.update(six.text_type(account).encode('utf-8')) account_for_hash = dict((k, v) for (k, v) in six.iteritems(account)
if k in cls.HASH_CRED_FIELDS)
temp_hash.update(six.text_type(account_for_hash).encode('utf-8'))
temp_hash_key = temp_hash.hexdigest() temp_hash_key = temp_hash.hexdigest()
hash_dict['creds'][temp_hash_key] = account hash_dict['creds'][temp_hash_key] = account
for role in roles: for role in roles:
@ -262,13 +270,13 @@ class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
for _hash in self.hash_dict['creds']: for _hash in self.hash_dict['creds']:
# Comparing on the attributes that are expected in the YAML # Comparing on the attributes that are expected in the YAML
init_attributes = creds.get_init_attributes() init_attributes = creds.get_init_attributes()
# Only use the attributes initially used to calculate the hash
init_attributes = [x for x in init_attributes if
x in self.HASH_CRED_FIELDS]
hash_attributes = self.hash_dict['creds'][_hash].copy() hash_attributes = self.hash_dict['creds'][_hash].copy()
if ('user_domain_name' in init_attributes and 'user_domain_name' # NOTE(andreaf) Not all fields may be available on all credentials
not in hash_attributes): # so defaulting to None for that case.
# Allow for the case of domain_name populated from config if all([getattr(creds, k, None) == hash_attributes.get(k, None) for
domain_name = self.credentials_domain
hash_attributes['user_domain_name'] = domain_name
if all([getattr(creds, k) == hash_attributes[k] for
k in init_attributes]): k in init_attributes]):
return _hash return _hash
raise AttributeError('Invalid credentials %s' % creds) raise AttributeError('Invalid credentials %s' % creds)
@ -351,23 +359,20 @@ class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
return net_creds return net_creds
def _extend_credentials(self, creds_dict): def _extend_credentials(self, creds_dict):
# In case of v3, adds a user_domain_name field to the creds # Add or remove credential domain fields to fit the identity version
# dict if not defined domain_fields = set(x for x in auth.KeystoneV3Credentials.ATTRIBUTES
if 'domain' in x)
msg = 'Assuming they are valid in the default domain.'
if self.identity_version == 'v3': if self.identity_version == 'v3':
user_domain_fields = set(['user_domain_name', 'user_domain_id']) if not domain_fields.intersection(set(creds_dict.keys())):
if not user_domain_fields.intersection(set(creds_dict.keys())): msg = 'Using credentials %s for v3 API calls. ' + msg
creds_dict['user_domain_name'] = self.credentials_domain LOG.warning(msg, self._sanitize_creds(creds_dict))
# NOTE(andreaf) In case of v2, replace project with tenant if project creds_dict['domain_name'] = self.credentials_domain
# is provided and tenant is not
if self.identity_version == 'v2': if self.identity_version == 'v2':
if ('project_name' in creds_dict and if domain_fields.intersection(set(creds_dict.keys())):
'tenant_name' in creds_dict and msg = 'Using credentials %s for v2 API calls. ' + msg
creds_dict['project_name'] != creds_dict['tenant_name']): LOG.warning(msg, self._sanitize_creds(creds_dict))
clean_creds = self._sanitize_creds(creds_dict) # Remove all valid domain attributes
msg = 'Cannot specify project and tenant at the same time %s' for attr in domain_fields.intersection(set(creds_dict.keys())):
raise exceptions.InvalidCredentials(msg % clean_creds) creds_dict.pop(attr)
if ('project_name' in creds_dict and
'tenant_name' not in creds_dict):
creds_dict['tenant_name'] = creds_dict['project_name']
creds_dict.pop('project_name')
return creds_dict return creds_dict

View File

@ -532,6 +532,7 @@ class Credentials(object):
""" """
ATTRIBUTES = [] ATTRIBUTES = []
COLLISIONS = []
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Enforce the available attributes at init time (only). """Enforce the available attributes at init time (only).
@ -543,6 +544,13 @@ class Credentials(object):
self._apply_credentials(kwargs) self._apply_credentials(kwargs)
def _apply_credentials(self, attr): def _apply_credentials(self, attr):
for (key1, key2) in self.COLLISIONS:
val1 = attr.get(key1)
val2 = attr.get(key2)
if val1 and val2 and val1 != val2:
msg = ('Cannot have conflicting values for %s and %s' %
(key1, key2))
raise exceptions.InvalidCredentials(msg)
for key in attr.keys(): for key in attr.keys():
if key in self.ATTRIBUTES: if key in self.ATTRIBUTES:
setattr(self, key, attr[key]) setattr(self, key, attr[key])
@ -600,7 +608,33 @@ class Credentials(object):
class KeystoneV2Credentials(Credentials): class KeystoneV2Credentials(Credentials):
ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id', ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id',
'tenant_id'] 'tenant_id', 'project_id', 'project_name']
COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
def __str__(self):
"""Represent only attributes included in self.ATTRIBUTES"""
attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
_repr = dict((k, getattr(self, k)) for k in attrs)
return str(_repr)
def __setattr__(self, key, value):
# NOTE(andreaf) In order to ease the migration towards 'project' we
# support v2 credentials configured with 'project' and translate it
# to tenant on the fly. The original kwargs are stored for clients
# that may rely on them. We also set project when tenant is defined
# so clients can rely on project being part of credentials.
parent = super(KeystoneV2Credentials, self)
# for project_* set tenant only
if key == 'project_id':
parent.__setattr__('tenant_id', value)
elif key == 'project_name':
parent.__setattr__('tenant_name', value)
if key == 'tenant_id':
parent.__setattr__('project_id', value)
elif key == 'tenant_name':
parent.__setattr__('project_name', value)
# trigger default behaviour for all attributes
parent.__setattr__(key, value)
def is_valid(self): def is_valid(self):
"""Check of credentials (no API call) """Check of credentials (no API call)
@ -611,9 +645,6 @@ class KeystoneV2Credentials(Credentials):
return None not in (self.username, self.password) return None not in (self.username, self.password)
COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
class KeystoneV3Credentials(Credentials): class KeystoneV3Credentials(Credentials):
"""Credentials suitable for the Keystone Identity V3 API""" """Credentials suitable for the Keystone Identity V3 API"""
@ -621,16 +652,7 @@ class KeystoneV3Credentials(Credentials):
'project_domain_id', 'project_domain_name', 'project_id', 'project_domain_id', 'project_domain_name', 'project_id',
'project_name', 'tenant_id', 'tenant_name', 'user_domain_id', 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
'user_domain_name', 'user_id'] 'user_domain_name', 'user_id']
COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
def _apply_credentials(self, attr):
for (key1, key2) in COLLISIONS:
val1 = attr.get(key1)
val2 = attr.get(key2)
if val1 and val2 and val1 != val2:
msg = ('Cannot have conflicting values for %s and %s' %
(key1, key2))
raise exceptions.InvalidCredentials(msg)
super(KeystoneV3Credentials, self)._apply_credentials(attr)
def __setattr__(self, key, value): def __setattr__(self, key, value):
parent = super(KeystoneV3Credentials, self) parent = super(KeystoneV3Credentials, self)

View File

@ -27,7 +27,6 @@ from tempest.common import preprov_creds
from tempest import config from tempest import config
from tempest.lib import auth from tempest.lib import auth
from tempest.lib import exceptions as lib_exc from tempest.lib import exceptions as lib_exc
from tempest.lib.services.identity.v2 import token_client
from tempest.tests import base from tempest.tests import base
from tempest.tests import fake_config from tempest.tests import fake_config
from tempest.tests.lib import fake_identity from tempest.tests.lib import fake_identity
@ -43,40 +42,46 @@ class TestPreProvisionedCredentials(base.TestCase):
'object_storage_operator_role': 'operator', 'object_storage_operator_role': 'operator',
'object_storage_reseller_admin_role': 'reseller'} 'object_storage_reseller_admin_role': 'reseller'}
identity_response = fake_identity._fake_v2_response
token_client = ('tempest.lib.services.identity.v2.token_client'
'.TokenClient.raw_request')
@classmethod
def _fake_accounts(cls, admin_role):
return [
{'username': 'test_user1', 'tenant_name': 'test_tenant1',
'password': 'p'},
{'username': 'test_user2', 'project_name': 'test_tenant2',
'password': 'p'},
{'username': 'test_user3', 'tenant_name': 'test_tenant3',
'password': 'p'},
{'username': 'test_user4', 'project_name': 'test_tenant4',
'password': 'p'},
{'username': 'test_user5', 'tenant_name': 'test_tenant5',
'password': 'p'},
{'username': 'test_user6', 'project_name': 'test_tenant6',
'password': 'p', 'roles': ['role1', 'role2']},
{'username': 'test_user7', 'tenant_name': 'test_tenant7',
'password': 'p', 'roles': ['role2', 'role3']},
{'username': 'test_user8', 'project_name': 'test_tenant8',
'password': 'p', 'roles': ['role4', 'role1']},
{'username': 'test_user9', 'tenant_name': 'test_tenant9',
'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
{'username': 'test_user10', 'project_name': 'test_tenant10',
'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
{'username': 'test_user11', 'tenant_name': 'test_tenant11',
'password': 'p', 'roles': [admin_role]},
{'username': 'test_user12', 'project_name': 'test_tenant12',
'password': 'p', 'roles': [admin_role]}]
def setUp(self): def setUp(self):
super(TestPreProvisionedCredentials, self).setUp() super(TestPreProvisionedCredentials, self).setUp()
self.useFixture(fake_config.ConfigFixture()) self.useFixture(fake_config.ConfigFixture())
self.patchobject(config, 'TempestConfigPrivate', self.patchobject(config, 'TempestConfigPrivate',
fake_config.FakePrivate) fake_config.FakePrivate)
self.patchobject(token_client.TokenClient, 'raw_request', self.patch(self.token_client, side_effect=self.identity_response)
fake_identity._fake_v2_response)
self.useFixture(lockutils_fixtures.ExternalLockFixture()) self.useFixture(lockutils_fixtures.ExternalLockFixture())
self.test_accounts = [ self.test_accounts = self._fake_accounts(cfg.CONF.identity.admin_role)
{'username': 'test_user1', 'tenant_name': 'test_tenant1',
'password': 'p'},
{'username': 'test_user2', 'tenant_name': 'test_tenant2',
'password': 'p'},
{'username': 'test_user3', 'tenant_name': 'test_tenant3',
'password': 'p'},
{'username': 'test_user4', 'tenant_name': 'test_tenant4',
'password': 'p'},
{'username': 'test_user5', 'tenant_name': 'test_tenant5',
'password': 'p'},
{'username': 'test_user6', 'tenant_name': 'test_tenant6',
'password': 'p', 'roles': ['role1', 'role2']},
{'username': 'test_user7', 'tenant_name': 'test_tenant7',
'password': 'p', 'roles': ['role2', 'role3']},
{'username': 'test_user8', 'tenant_name': 'test_tenant8',
'password': 'p', 'roles': ['role4', 'role1']},
{'username': 'test_user9', 'tenant_name': 'test_tenant9',
'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
{'username': 'test_user10', 'tenant_name': 'test_tenant10',
'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
{'username': 'test_user11', 'tenant_name': 'test_tenant11',
'password': 'p', 'roles': [cfg.CONF.identity.admin_role]},
{'username': 'test_user12', 'tenant_name': 'test_tenant12',
'password': 'p', 'roles': [cfg.CONF.identity.admin_role]},
]
self.accounts_mock = self.useFixture(mockpatch.Patch( self.accounts_mock = self.useFixture(mockpatch.Patch(
'tempest.common.preprov_creds.read_accounts_yaml', 'tempest.common.preprov_creds.read_accounts_yaml',
return_value=self.test_accounts)) return_value=self.test_accounts))
@ -89,24 +94,33 @@ class TestPreProvisionedCredentials(base.TestCase):
def _get_hash_list(self, accounts_list): def _get_hash_list(self, accounts_list):
hash_list = [] hash_list = []
hash_fields = (
preprov_creds.PreProvisionedCredentialProvider.HASH_CRED_FIELDS)
for account in accounts_list: for account in accounts_list:
hash = hashlib.md5() hash = hashlib.md5()
hash.update(six.text_type(account).encode('utf-8')) account_for_hash = dict((k, v) for (k, v) in six.iteritems(account)
if k in hash_fields)
hash.update(six.text_type(account_for_hash).encode('utf-8'))
temp_hash = hash.hexdigest() temp_hash = hash.hexdigest()
hash_list.append(temp_hash) hash_list.append(temp_hash)
return hash_list return hash_list
def test_get_hash(self): def test_get_hash(self):
self.patchobject(token_client.TokenClient, 'raw_request', # Test with all accounts to make sure we try all combinations
fake_identity._fake_v2_response) # and hide no race conditions
test_account_class = preprov_creds.PreProvisionedCredentialProvider( hash_index = 0
**self.fixed_params) for test_cred_dict in self.test_accounts:
hash_list = self._get_hash_list(self.test_accounts) test_account_class = (
test_cred_dict = self.test_accounts[3] preprov_creds.PreProvisionedCredentialProvider(
test_creds = auth.get_credentials(fake_identity.FAKE_AUTH_URL, **self.fixed_params))
**test_cred_dict) hash_list = self._get_hash_list(self.test_accounts)
results = test_account_class.get_hash(test_creds) test_creds = auth.get_credentials(
self.assertEqual(hash_list[3], results) fake_identity.FAKE_AUTH_URL,
identity_version=self.fixed_params['identity_version'],
**test_cred_dict)
results = test_account_class.get_hash(test_creds)
self.assertEqual(hash_list[hash_index], results)
hash_index += 1
def test_get_hash_dict(self): def test_get_hash_dict(self):
test_account_class = preprov_creds.PreProvisionedCredentialProvider( test_account_class = preprov_creds.PreProvisionedCredentialProvider(
@ -331,3 +345,53 @@ class TestPreProvisionedCredentials(base.TestCase):
self.assertIn('id', network) self.assertIn('id', network)
self.assertEqual('fake-id', network['id']) self.assertEqual('fake-id', network['id'])
self.assertEqual('network-2', network['name']) self.assertEqual('network-2', network['name'])
class TestPreProvisionedCredentialsV3(TestPreProvisionedCredentials):
fixed_params = {'name': 'test class',
'identity_version': 'v3',
'test_accounts_file': 'fake_accounts_file',
'accounts_lock_dir': 'fake_locks_dir',
'admin_role': 'admin',
'object_storage_operator_role': 'operator',
'object_storage_reseller_admin_role': 'reseller'}
identity_response = fake_identity._fake_v3_response
token_client = ('tempest.lib.services.identity.v3.token_client'
'.V3TokenClient.raw_request')
@classmethod
def _fake_accounts(cls, admin_role):
return [
{'username': 'test_user1', 'project_name': 'test_project1',
'domain_name': 'domain', 'password': 'p'},
{'username': 'test_user2', 'project_name': 'test_project2',
'domain_name': 'domain', 'password': 'p'},
{'username': 'test_user3', 'project_name': 'test_project3',
'domain_name': 'domain', 'password': 'p'},
{'username': 'test_user4', 'project_name': 'test_project4',
'domain_name': 'domain', 'password': 'p'},
{'username': 'test_user5', 'project_name': 'test_project5',
'domain_name': 'domain', 'password': 'p'},
{'username': 'test_user6', 'project_name': 'test_project6',
'domain_name': 'domain', 'password': 'p',
'roles': ['role1', 'role2']},
{'username': 'test_user7', 'project_name': 'test_project7',
'domain_name': 'domain', 'password': 'p',
'roles': ['role2', 'role3']},
{'username': 'test_user8', 'project_name': 'test_project8',
'domain_name': 'domain', 'password': 'p',
'roles': ['role4', 'role1']},
{'username': 'test_user9', 'project_name': 'test_project9',
'domain_name': 'domain', 'password': 'p',
'roles': ['role1', 'role2', 'role3', 'role4']},
{'username': 'test_user10', 'project_name': 'test_project10',
'domain_name': 'domain', 'password': 'p',
'roles': ['role1', 'role2', 'role3', 'role4']},
{'username': 'test_user11', 'project_name': 'test_project11',
'domain_name': 'domain', 'password': 'p',
'roles': [admin_role]},
{'username': 'test_user12', 'project_name': 'test_project12',
'domain_name': 'domain', 'password': 'p',
'roles': [admin_role]}]

View File

@ -36,8 +36,10 @@ class CredentialsTests(base.TestCase):
# Check the right version of credentials has been returned # Check the right version of credentials has been returned
self.assertIsInstance(credentials, credentials_class) self.assertIsInstance(credentials, credentials_class)
# Check the id attributes are filled in # Check the id attributes are filled in
# NOTE(andreaf) project_* attributes are accepted as input but
# never set on the credentials object
attributes = [x for x in credentials.ATTRIBUTES if ( attributes = [x for x in credentials.ATTRIBUTES if (
'_id' in x and x != 'domain_id')] '_id' in x and x != 'domain_id' and x != 'project_id')]
for attr in attributes: for attr in attributes:
if filled: if filled:
self.assertIsNotNone(getattr(credentials, attr)) self.assertIsNotNone(getattr(credentials, attr))