summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml13
-rw-r--r--doc/source/admin/caching-layer.rst8
-rw-r--r--keystone/credential/backends/sql.py3
-rw-r--r--keystone/identity/backends/ldap/common.py34
-rw-r--r--keystone/identity/backends/ldap/core.py7
-rw-r--r--keystone/tests/unit/test_backend_ldap.py55
-rw-r--r--keystone/tests/unit/test_v3_credential.py34
-rw-r--r--releasenotes/notes/bug-1782922-db822fda486ac773.yaml10
-rw-r--r--releasenotes/notes/bug-1840291-35af1ac7ba06e166.yaml6
9 files changed, 155 insertions, 15 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index 547d423f1..fa38264ea 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -68,6 +68,17 @@
tox_env: upgrade
osa_test_repo: openstack/openstack-ansible-os_keystone
+- job:
+ name: keystone-dsvm-ldap-domain-specific-driver
+ parent: devstack-tempest
+ vars:
+ devstack_localrc:
+ KEYSTONE_CLEAR_LDAP: 'yes'
+ LDAP_PASSWORD: 'nomoresecret'
+ USE_PYTHON3: True
+ devstack_services:
+ ldap: true
+
# Experimental
- job:
name: keystone-tox-patch_cover
@@ -180,7 +191,7 @@
- keystoneclient-devstack-functional:
voting: false
irrelevant-files: *irrelevant-files
- - legacy-tempest-dsvm-ldap-domain-specific-driver:
+ - keystone-dsvm-ldap-domain-specific-driver:
voting: false
irrelevant-files: &tempest-irrelevant-files
- ^(test-|)requirements.txt$
diff --git a/doc/source/admin/caching-layer.rst b/doc/source/admin/caching-layer.rst
index e54ebceee..419791073 100644
--- a/doc/source/admin/caching-layer.rst
+++ b/doc/source/admin/caching-layer.rst
@@ -147,18 +147,18 @@ will perform proper invalidations of the cached methods listed above.
For more information about the different back ends (and configuration
options), see:
-- `dogpile.cache.memory <https://dogpilecache.readthedocs.io/en/latest/api.html#memory-backends>`__
+- `dogpile.cache.memory <https://dogpilecache.sqlalchemy.org/en/latest/api.html#memory-backends>`__
-- `dogpile.cache.memcached <https://dogpilecache.readthedocs.io/en/latest/api.html#memcached-backends>`__
+- `dogpile.cache.memcached <https://dogpilecache.sqlalchemy.org/en/latest/api.html#memcached-backends>`__
.. note::
The memory back end is not suitable for use in a production
environment.
-- `dogpile.cache.redis <https://dogpilecache.readthedocs.io/en/latest/api.html#redis-backends>`__
+- `dogpile.cache.redis <https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends>`__
-- `dogpile.cache.dbm <https://dogpilecache.readthedocs.io/en/latest/api.html#file-backends>`__
+- `dogpile.cache.dbm <https://dogpilecache.sqlalchemy.org/en/latest/api.html#file-backends>`__
Cache invalidation
------------------
diff --git a/keystone/credential/backends/sql.py b/keystone/credential/backends/sql.py
index 144307161..e499e2a86 100644
--- a/keystone/credential/backends/sql.py
+++ b/keystone/credential/backends/sql.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from oslo_db import api as oslo_db_api
+
from keystone.common import driver_hints
from keystone.common import sql
from keystone.credential.backends import base
@@ -96,6 +98,7 @@ class Credential(base.CredentialDriverBase):
query = query.filter_by(project_id=project_id)
query.delete()
+ @oslo_db_api.wrap_db_retry(retry_on_deadlock=True)
def delete_credentials_for_user(self, user_id):
with sql.session_for_write() as session:
query = session.query(CredentialModel)
diff --git a/keystone/identity/backends/ldap/common.py b/keystone/identity/backends/ldap/common.py
index e2f85a7ee..b9becea74 100644
--- a/keystone/identity/backends/ldap/common.py
+++ b/keystone/identity/backends/ldap/common.py
@@ -1293,9 +1293,37 @@ class BaseLdap(object):
else:
return self._id_to_dn_string(object_id)
- @staticmethod
- def _dn_to_id(dn):
- return ldap.dn.str2dn(dn)[0][0][1]
+ def _dn_to_id(self, dn):
+ # Check if the naming attribute in the DN is the same as keystone's
+ # configured 'id' attribute'. If so, extract the ID value from the DN
+ if self.id_attr == ldap.dn.str2dn(dn)[0][0][0].lower():
+ return ldap.dn.str2dn(dn)[0][0][1]
+ else:
+ # The 'ID' attribute is NOT in the DN, so we need to perform an
+ # LDAP search to look it up from the user entry itself.
+ with self.get_connection() as conn:
+ search_result = conn.search_s(dn, ldap.SCOPE_BASE)
+
+ if search_result:
+ try:
+ id_list = search_result[0][1][self.id_attr]
+ except KeyError:
+ message = ('ID attribute %(id_attr)s not found in LDAP '
+ 'object %(dn)s.') % ({'id_attr': self.id_attr,
+ 'dn': search_result})
+ LOG.warning(message)
+ raise exception.NotFound(message=message)
+ if len(id_list) > 1:
+ message = ('In order to keep backward compatibility, in '
+ 'the case of multivalued ids, we are '
+ 'returning the first id %(id_attr) in the '
+ 'DN.') % ({'id_attr': id_list[0]})
+ LOG.warning(message)
+ return id_list[0]
+ else:
+ message = _('DN attribute %(dn)s not found in LDAP') % (
+ {'dn': dn})
+ raise exception.NotFound(message=message)
def _ldap_res_to_model(self, res):
# LDAP attribute names may be returned in a different case than
diff --git a/keystone/identity/backends/ldap/core.py b/keystone/identity/backends/ldap/core.py
index b6b87185f..2afa730e9 100644
--- a/keystone/identity/backends/ldap/core.py
+++ b/keystone/identity/backends/ldap/core.py
@@ -311,8 +311,11 @@ class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
return obj
def get_filtered(self, user_id):
- user = self.get(user_id)
- return self.filter_attributes(user)
+ try:
+ user = self.get(user_id)
+ return self.filter_attributes(user)
+ except ldap.NO_SUCH_OBJECT:
+ raise self.NotFound(user_id=user_id)
def get_all(self, ldap_filter=None, hints=None):
objs = super(UserApi, self).get_all(ldap_filter=ldap_filter,
diff --git a/keystone/tests/unit/test_backend_ldap.py b/keystone/tests/unit/test_backend_ldap.py
index a13c718bc..aa7a50747 100644
--- a/keystone/tests/unit/test_backend_ldap.py
+++ b/keystone/tests/unit/test_backend_ldap.py
@@ -1819,7 +1819,33 @@ class LDAPIdentity(BaseLDAPIdentity):
self.assertEqual(self.user_foo['email'], user_ref['id'])
@mock.patch.object(common_ldap.BaseLdap, '_ldap_get')
- def test_get_id_from_dn_for_multivalued_attribute_id(self, mock_ldap_get):
+ def test_get_multivalued_attribute_id_from_dn(self,
+ mock_ldap_get):
+ driver = PROVIDERS.identity_api._select_identity_driver(
+ CONF.identity.default_domain_id)
+ driver.user.id_attr = 'mail'
+
+ # make 'email' multivalued so we can test the error condition
+ email1 = uuid.uuid4().hex
+ email2 = uuid.uuid4().hex
+ # Mock the ldap search results to return user entries with
+ # user_name_attribute('sn') value has emptyspaces, emptystring
+ # and attibute itself is not set.
+ mock_ldap_get.return_value = (
+ 'cn=users,dc=example,dc=com',
+ {
+ 'mail': [email1, email2],
+ }
+ )
+
+ # This is not a valid scenario, since we do not support multiple value
+ # attribute id on DN.
+ self.assertRaises(exception.NotFound,
+ PROVIDERS.identity_api.get_user, email1)
+
+ @mock.patch.object(common_ldap.BaseLdap, '_ldap_get')
+ def test_raise_not_found_dn_for_multivalued_attribute_id(self,
+ mock_ldap_get):
driver = PROVIDERS.identity_api._select_identity_driver(
CONF.identity.default_domain_id)
driver.user.id_attr = 'mail'
@@ -1836,10 +1862,29 @@ class LDAPIdentity(BaseLDAPIdentity):
}
)
- user_ref = PROVIDERS.identity_api.get_user(email1)
- # make sure we get the ID from DN (old behavior) if the ID attribute
- # has multiple values
- self.assertEqual('nobodycares', user_ref['id'])
+ # This is not a valid scenario, since we do not support multiple value
+ # attribute id on DN.
+ self.assertRaises(exception.NotFound,
+ PROVIDERS.identity_api.get_user, email1)
+
+ @mock.patch.object(common_ldap.BaseLdap, '_ldap_get')
+ def test_get_id_not_in_dn(self,
+ mock_ldap_get):
+ driver = PROVIDERS.identity_api._select_identity_driver(
+ CONF.identity.default_domain_id)
+ driver.user.id_attr = 'sAMAccountName'
+
+ user_id = uuid.uuid4().hex
+ mock_ldap_get.return_value = (
+ 'cn=someuser,dc=example,dc=com',
+ {
+ 'cn': 'someuser',
+ 'sn': [uuid.uuid4().hex],
+ 'sAMAccountName': [user_id],
+ }
+ )
+ user_ref = PROVIDERS.identity_api.get_user(user_id)
+ self.assertEqual(user_id, user_ref['id'])
@mock.patch.object(common_ldap.BaseLdap, '_ldap_get')
def test_id_attribute_not_found(self, mock_ldap_get):
diff --git a/keystone/tests/unit/test_v3_credential.py b/keystone/tests/unit/test_v3_credential.py
index b5e08249a..2538c3a10 100644
--- a/keystone/tests/unit/test_v3_credential.py
+++ b/keystone/tests/unit/test_v3_credential.py
@@ -17,6 +17,8 @@ import json
import uuid
from keystoneclient.contrib.ec2 import utils as ec2_utils
+import mock
+from oslo_db import exception as oslo_db_exception
from six.moves import http_client
from testtools import matchers
@@ -262,6 +264,38 @@ class CredentialTestCase(CredentialBaseTestCase):
'/credentials/%(credential_id)s' % {
'credential_id': self.credential['id']})
+ def test_delete_credential_retries_on_deadlock(self):
+ patcher = mock.patch('sqlalchemy.orm.query.Query.delete',
+ autospec=True)
+
+ class FakeDeadlock(object):
+ def __init__(self, mock_patcher):
+ self.deadlock_count = 2
+ self.mock_patcher = mock_patcher
+ self.patched = True
+
+ def __call__(self, *args, **kwargs):
+ if self.deadlock_count > 1:
+ self.deadlock_count -= 1
+ else:
+ self.mock_patcher.stop()
+ self.patched = False
+ raise oslo_db_exception.DBDeadlock
+
+ sql_delete_mock = patcher.start()
+ side_effect = FakeDeadlock(patcher)
+ sql_delete_mock.side_effect = side_effect
+
+ try:
+ PROVIDERS.credential_api.delete_credentials_for_user(
+ user_id=self.user['id'])
+ finally:
+ if side_effect.patched:
+ patcher.stop()
+
+ # initial attempt + 1 retry
+ self.assertEqual(sql_delete_mock.call_count, 2)
+
def test_create_ec2_credential(self):
"""Call ``POST /credentials`` for creating ec2 credential."""
blob, ref = unit.new_ec2_credential(user_id=self.user['id'],
diff --git a/releasenotes/notes/bug-1782922-db822fda486ac773.yaml b/releasenotes/notes/bug-1782922-db822fda486ac773.yaml
new file mode 100644
index 000000000..d8f5e26d7
--- /dev/null
+++ b/releasenotes/notes/bug-1782922-db822fda486ac773.yaml
@@ -0,0 +1,10 @@
+---
+fixes:
+ - |
+ [`bug 1782922 <https://bugs.launchpad.net/keystone/+bug/1782922>`_]
+ Fixed the problem where Keystone indiscriminately return the first RDN
+ as the user ID, regardless whether it matches the configured
+ 'user_id_attribute' or not. This will break deployments where
+ 'group_members_are_ids' are set to False and 'user_id_attribute' is not
+ in the DN. This patch will perform a lookup by DN if the first RND does
+ not match the configured 'user_id_attribute'.
diff --git a/releasenotes/notes/bug-1840291-35af1ac7ba06e166.yaml b/releasenotes/notes/bug-1840291-35af1ac7ba06e166.yaml
new file mode 100644
index 000000000..142f8e0b5
--- /dev/null
+++ b/releasenotes/notes/bug-1840291-35af1ac7ba06e166.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ [`bug 1840291 <https://bugs.launchpad.net/keystone/+bug/1840291>`_]
+ Adds retries for ``delete_credential_for_user`` method to avoid
+ DBDeadlocks when deleting large number of credentials concurrently.