"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "keystone/identity/backends/ldap/common.py" between
keystone-16.0.1.tar.gz and keystone-16.0.2.tar.gz

About: OpenStack Keystone (Core Service: Identity) provides an authentication and authorization service for other OpenStack services. Provides a catalog of endpoints for all OpenStack services.
The "Train" series (maintained release).

common.py  (keystone-16.0.1):common.py  (keystone-16.0.2)
skipping to change at line 17 skipping to change at line 17
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import abc import abc
import codecs import codecs
import functools
import os.path import os.path
import re import re
import sys import sys
import uuid
import weakref import weakref
import ldap.controls import ldap.controls
import ldap.filter import ldap.filter
import ldappool import ldappool
from oslo_log import log from oslo_log import log
from oslo_utils import reflection from oslo_utils import reflection
import six import six
from six.moves import zip from six.moves import zip
from six import PY2 from six import PY2
skipping to change at line 94 skipping to change at line 94
If the value is a binary string assume it's UTF-8 encoded and decode If the value is a binary string assume it's UTF-8 encoded and decode
it into a unicode string. Otherwise convert the value from its it into a unicode string. Otherwise convert the value from its
type into a unicode string. type into a unicode string.
:param value: value to be returned as unicode :param value: value to be returned as unicode
:returns: value as unicode :returns: value as unicode
:raises UnicodeDecodeError: for invalid UTF-8 encoding :raises UnicodeDecodeError: for invalid UTF-8 encoding
""" """
if isinstance(value, six.binary_type): if isinstance(value, six.binary_type):
return _utf8_decoder(value)[0] try:
return _utf8_decoder(value)[0]
except UnicodeDecodeError:
# NOTE(lbragstad): We could be dealing with a UUID in byte form,
# which some LDAP implementations use.
uuid_byte_string_length = 16
if len(value) == uuid_byte_string_length:
return six.text_type(uuid.UUID(bytes_le=value))
else:
raise
return six.text_type(value) return six.text_type(value)
def py2ldap(val): def py2ldap(val):
"""Type convert a Python value to a type accepted by LDAP (unicode). """Type convert a Python value to a type accepted by LDAP (unicode).
The LDAP API only accepts strings for values therefore convert The LDAP API only accepts strings for values therefore convert
the value's type to a unicode string. A subsequent type conversion the value's type to a unicode string. A subsequent type conversion
will encode the unicode as UTF-8 as required by the python-ldap API, will encode the unicode as UTF-8 as required by the python-ldap API,
but for now we just want a string representation of the value. but for now we just want a string representation of the value.
skipping to change at line 617 skipping to change at line 626
raise IOError(_("tls_cacertdir %s not found " raise IOError(_("tls_cacertdir %s not found "
"or is not a directory") % "or is not a directory") %
tls_cacertdir) tls_cacertdir)
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir) ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir)
if tls_req_cert in list(LDAP_TLS_CERTS.values()): if tls_req_cert in list(LDAP_TLS_CERTS.values()):
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert) ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert)
else: else:
LOG.debug('LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s', LOG.debug('LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s',
tls_req_cert) tls_req_cert)
class MsgId(list): class AsynchronousMessage(object):
"""Wrapper class to hold connection and msgid.""" """A container for handling asynchronous LDAP responses.
Some LDAP APIs, like `search_ext`, are asynchronous and return a message ID
when the server successfully initiates the operation. Clients can use this
message ID and the original connection to make the request to fetch the
results using `result3`.
This object holds the message ID, the original connection, and a callable
weak reference Finalizer that cleans up context managers specific to the
connection associated to the message ID.
:param message_id: The message identifier (str).
:param connection: The connection associated with the message identifier
(ldappool.StateConnector).
The `clean` attribute is a callable that cleans up the context manager used
to create or return the connection object (weakref.finalize).
"""
def __init__(self, message_id, connection, context_manager):
self.id = message_id
self.connection = connection
self.clean = weakref.finalize(
self, self._cleanup_connection_context_manager, context_manager
)
pass def _cleanup_connection_context_manager(self, context_manager):
context_manager.__exit__(None, None, None)
def use_conn_pool(func): def use_conn_pool(func):
"""Use this only for connection pool specific ldap API. """Use this only for connection pool specific ldap API.
This adds connection object to decorated API as next argument after self. This adds connection object to decorated API as next argument after self.
""" """
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
# assert isinstance(self, PooledLDAPHandler) # assert isinstance(self, PooledLDAPHandler)
with self._get_pool_connection() as conn: with self._get_pool_connection() as conn:
skipping to change at line 772 skipping to change at line 807
@use_conn_pool @use_conn_pool
def search_s(self, conn, base, scope, def search_s(self, conn, base, scope,
filterstr='(objectClass=*)', attrlist=None, attrsonly=0): filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
return conn.search_s(base, scope, filterstr, attrlist, return conn.search_s(base, scope, filterstr, attrlist,
attrsonly) attrsonly)
def search_ext(self, base, scope, def search_ext(self, base, scope,
filterstr='(objectClass=*)', attrlist=None, attrsonly=0, filterstr='(objectClass=*)', attrlist=None, attrsonly=0,
serverctrls=None, clientctrls=None, serverctrls=None, clientctrls=None,
timeout=-1, sizelimit=0): timeout=-1, sizelimit=0):
"""Return a ``MsgId`` instance, it asynchronous API. """Return an AsynchronousMessage instance, it asynchronous API.
The ``MsgId`` instance can be safely used in a call to ``result3()``. The AsynchronousMessage instance can be safely used in a call to
`result3()`.
To work with ``result3()`` API in predictable manner, the same LDAP To work with `result3()` API in predictable manner, the same LDAP
connection is needed which originally provided the ``msgid``. So, this connection is needed which originally provided the `msgid`. So, this
method wraps the existing connection and ``msgid`` in a new ``MsgId`` method wraps the existing connection and `msgid` in a new
instance. The connection associated with ``search_ext`` is released `AsynchronousMessage` instance. The connection associated with
once last hard reference to the ``MsgId`` instance is freed. `search_ext()` is released after `result3()` fetches the data
associated with `msgid`.
""" """
conn_ctxt = self._get_pool_connection() conn_ctxt = self._get_pool_connection()
conn = conn_ctxt.__enter__() conn = conn_ctxt.__enter__()
try: try:
msgid = conn.search_ext(base, scope, msgid = conn.search_ext(base, scope,
filterstr, attrlist, attrsonly, filterstr, attrlist, attrsonly,
serverctrls, clientctrls, serverctrls, clientctrls,
timeout, sizelimit) timeout, sizelimit)
except Exception: except Exception:
conn_ctxt.__exit__(*sys.exc_info()) conn_ctxt.__exit__(*sys.exc_info())
raise raise
res = MsgId((conn, msgid)) return AsynchronousMessage(msgid, conn, conn_ctxt)
weakref.ref(res, functools.partial(conn_ctxt.__exit__,
None, None, None))
return res
def result3(self, msgid, all=1, timeout=None, def result3(self, message, all=1, timeout=None,
resp_ctrl_classes=None): resp_ctrl_classes=None):
"""Wait for and return the result. """Wait for and return the result to an asynchronous message.
This method returns the result of an operation previously initiated by This method returns the result of an operation previously initiated by
one of the LDAP asynchronous operation routines (eg search_ext()). It one of the LDAP asynchronous operation routines (e.g., `search_ext()`).
returned an invocation identifier (a message id) upon successful The `search_ext()` method in python-ldap returns an invocation
initiation of their operation. identifier, or a message ID, upon successful initiation of the
operation by the LDAP server.
Input msgid is expected to be instance of class MsgId which has LDAP
session/connection used to execute search_ext and message idenfier. The `message` is expected to be instance of class
`AsynchronousMessage`, which contains the message ID and the connection
The connection associated with search_ext is released once last hard used to make the original request.
reference to MsgId object is freed. This will happen when function
which requested msgId and used it in result3 exits. The connection and context manager associated with `search_ext()` are
cleaned up when message.clean() is called.
""" """
conn, msg_id = msgid results = message.connection.result3(message.id, all, timeout)
return conn.result3(msg_id, all, timeout)
# Now that we have the results from the LDAP server for the message, we
# don't need the the context manager used to create the connection.
message.clean()
return results
@use_conn_pool @use_conn_pool
def modify_s(self, conn, dn, modlist): def modify_s(self, conn, dn, modlist):
return conn.modify_s(dn, modlist) return conn.modify_s(dn, modlist)
class KeystoneLDAPHandler(LDAPHandler): class KeystoneLDAPHandler(LDAPHandler):
"""Convert data types and perform logging. """Convert data types and perform logging.
This LDAP interface wraps the python-ldap based interfaces. The This LDAP interface wraps the python-ldap based interfaces. The
python-ldap interfaces require string values encoded in UTF-8 with python-ldap interfaces require string values encoded in UTF-8 with
skipping to change at line 976 skipping to change at line 1016
criticality=True, criticality=True,
controlValue=(self.page_size, '')) controlValue=(self.page_size, ''))
page_ctrl_oid = ldap.LDAP_CONTROL_PAGE_OID page_ctrl_oid = ldap.LDAP_CONTROL_PAGE_OID
else: else:
lc = ldap.controls.libldap.SimplePagedResultsControl( lc = ldap.controls.libldap.SimplePagedResultsControl(
criticality=True, criticality=True,
size=self.page_size, size=self.page_size,
cookie='') cookie='')
page_ctrl_oid = ldap.controls.SimplePagedResultsControl.controlType page_ctrl_oid = ldap.controls.SimplePagedResultsControl.controlType
msgid = self.conn.search_ext(base, message = self.conn.search_ext(base,
scope, scope,
filterstr, filterstr,
attrlist, attrlist,
serverctrls=[lc]) serverctrls=[lc])
# Endless loop request pages on ldap server until it has no data # Endless loop request pages on ldap server until it has no data
while True: while True:
# Request to the ldap server a page with 'page_size' entries # Request to the ldap server a page with 'page_size' entries
rtype, rdata, rmsgid, serverctrls = self.conn.result3(msgid) rtype, rdata, rmsgid, serverctrls = self.conn.result3(message)
# Receive the data # Receive the data
res.extend(rdata) res.extend(rdata)
pctrls = [c for c in serverctrls pctrls = [c for c in serverctrls
if c.controlType == page_ctrl_oid] if c.controlType == page_ctrl_oid]
if pctrls: if pctrls:
# LDAP server supports pagination # LDAP server supports pagination
if use_old_paging_api: if use_old_paging_api:
est, cookie = pctrls[0].controlValue est, cookie = pctrls[0].controlValue
lc.controlValue = (self.page_size, cookie) lc.controlValue = (self.page_size, cookie)
else: else:
cookie = lc.cookie = pctrls[0].cookie cookie = lc.cookie = pctrls[0].cookie
if cookie: if cookie:
# There is more data still on the server # There is more data still on the server
# so we request another page # so we request another page
msgid = self.conn.search_ext(base, message = self.conn.search_ext(base,
scope, scope,
filterstr, filterstr,
attrlist, attrlist,
serverctrls=[lc]) serverctrls=[lc])
else: else:
# Exit condition no more data on server # Exit condition no more data on server
break break
else: else:
LOG.warning('LDAP Server does not support paging. ' LOG.warning('LDAP Server does not support paging. '
'Disable paging in keystone.conf to ' 'Disable paging in keystone.conf to '
'avoid this message.') 'avoid this message.')
self._disable_paging() self._disable_paging()
break break
return res return res
skipping to change at line 1791 skipping to change at line 1831
self.enabled_emulation_dn = '%s=%s,%s' % sub_vals self.enabled_emulation_dn = '%s=%s,%s' % sub_vals
naming_attr = (naming_attr_name, [naming_attr_value]) naming_attr = (naming_attr_name, [naming_attr_value])
else: else:
# Extract the attribute name and value from the configured DN. # Extract the attribute name and value from the configured DN.
naming_dn = ldap.dn.str2dn(self.enabled_emulation_dn) naming_dn = ldap.dn.str2dn(self.enabled_emulation_dn)
naming_rdn = naming_dn[0][0] naming_rdn = naming_dn[0][0]
naming_attr = (naming_rdn[0], naming_attr = (naming_rdn[0],
naming_rdn[1]) naming_rdn[1])
self.enabled_emulation_naming_attr = naming_attr self.enabled_emulation_naming_attr = naming_attr
# TODO(yoctozepto): methods below use _id_to_dn which requests another def _id_to_member_attribute_value(self, object_id):
# LDAP connection - optimize it """Convert id to value expected by member_attribute."""
def _get_enabled(self, object_id, conn):
if self.group_members_are_ids: if self.group_members_are_ids:
dn = object_id return object_id
else: return self._id_to_dn(object_id)
dn = self._id_to_dn(object_id)
def _is_id_enabled(self, object_id, conn):
member_attr_val = self._id_to_member_attribute_value(object_id)
return self._is_member_enabled(member_attr_val, conn)
def _is_member_enabled(self, member_attr_val, conn):
query = '(%s=%s)' % (self.member_attribute, query = '(%s=%s)' % (self.member_attribute,
ldap.filter.escape_filter_chars(dn)) ldap.filter.escape_filter_chars(member_attr_val))
try: try:
enabled_value = conn.search_s(self.enabled_emulation_dn, enabled_value = conn.search_s(self.enabled_emulation_dn,
ldap.SCOPE_BASE, ldap.SCOPE_BASE,
query, attrlist=DN_ONLY) query, attrlist=DN_ONLY)
except ldap.NO_SUCH_OBJECT: except ldap.NO_SUCH_OBJECT:
return False return False
else: else:
return bool(enabled_value) return bool(enabled_value)
def _add_enabled(self, object_id): def _add_enabled(self, object_id):
if self.group_members_are_ids: member_attr_val = self._id_to_member_attribute_value(object_id)
dn = object_id
else:
dn = self._id_to_dn(object_id)
with self.get_connection() as conn: with self.get_connection() as conn:
# TODO(yoctozepto): _get_enabled potentially calls if not self._is_member_enabled(member_attr_val, conn):
# _id_to_dn 2nd time - optimize it
if not self._get_enabled(object_id, conn):
modlist = [(ldap.MOD_ADD, modlist = [(ldap.MOD_ADD,
self.member_attribute, self.member_attribute,
[dn])] [member_attr_val])]
try: try:
conn.modify_s(self.enabled_emulation_dn, modlist) conn.modify_s(self.enabled_emulation_dn, modlist)
except ldap.NO_SUCH_OBJECT: except ldap.NO_SUCH_OBJECT:
attr_list = [('objectClass', [self.group_objectclass]), attr_list = [('objectClass', [self.group_objectclass]),
(self.member_attribute, (self.member_attribute,
[dn]), [member_attr_val]),
self.enabled_emulation_naming_attr] self.enabled_emulation_naming_attr]
conn.add_s(self.enabled_emulation_dn, attr_list) conn.add_s(self.enabled_emulation_dn, attr_list)
def _remove_enabled(self, object_id): def _remove_enabled(self, object_id):
if self.group_members_are_ids: member_attr_val = self._id_to_member_attribute_value(object_id)
dn = object_id
else:
dn = self._id_to_dn(object_id)
modlist = [(ldap.MOD_DELETE, modlist = [(ldap.MOD_DELETE,
self.member_attribute, self.member_attribute,
[dn])] [member_attr_val])]
with self.get_connection() as conn: with self.get_connection() as conn:
try: try:
conn.modify_s(self.enabled_emulation_dn, modlist) conn.modify_s(self.enabled_emulation_dn, modlist)
except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE): # nosec except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE): # nosec
# It's already gone, good. # It's already gone, good.
pass pass
def create(self, values): def create(self, values):
if self.enabled_emulation: if self.enabled_emulation:
enabled_value = values.pop('enabled', True) enabled_value = values.pop('enabled', True)
skipping to change at line 1863 skipping to change at line 1898
ref['enabled'] = enabled_value ref['enabled'] = enabled_value
return ref return ref
else: else:
return super(EnabledEmuMixIn, self).create(values) return super(EnabledEmuMixIn, self).create(values)
def get(self, object_id, ldap_filter=None): def get(self, object_id, ldap_filter=None):
with self.get_connection() as conn: with self.get_connection() as conn:
ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter) ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter)
if ('enabled' not in self.attribute_ignore and if ('enabled' not in self.attribute_ignore and
self.enabled_emulation): self.enabled_emulation):
ref['enabled'] = self._get_enabled(object_id, conn) ref['enabled'] = self._is_id_enabled(object_id, conn)
return ref return ref
def get_all(self, ldap_filter=None, hints=None): def get_all(self, ldap_filter=None, hints=None):
hints = hints or driver_hints.Hints() hints = hints or driver_hints.Hints()
if 'enabled' not in self.attribute_ignore and self.enabled_emulation: if 'enabled' not in self.attribute_ignore and self.enabled_emulation:
# had to copy BaseLdap.get_all here to ldap_filter by DN # had to copy BaseLdap.get_all here to ldap_filter by DN
obj_list = [self._ldap_res_to_model(x) obj_list = [self._ldap_res_to_model(x)
for x in self._ldap_get_all(hints, ldap_filter) for x in self._ldap_get_all(hints, ldap_filter)
if x[0] != self.enabled_emulation_dn] if x[0] != self.enabled_emulation_dn]
with self.get_connection() as conn: with self.get_connection() as conn:
for obj_ref in obj_list: for obj_ref in obj_list:
obj_ref['enabled'] = self._get_enabled( obj_ref['enabled'] = self._is_id_enabled(
obj_ref['id'], conn) obj_ref['id'], conn)
return obj_list return obj_list
else: else:
return super(EnabledEmuMixIn, self).get_all(ldap_filter, hints) return super(EnabledEmuMixIn, self).get_all(ldap_filter, hints)
def update(self, object_id, values, old_obj=None): def update(self, object_id, values, old_obj=None):
if 'enabled' not in self.attribute_ignore and self.enabled_emulation: if 'enabled' not in self.attribute_ignore and self.enabled_emulation:
data = values.copy() data = values.copy()
enabled_value = data.pop('enabled', None) enabled_value = data.pop('enabled', None)
ref = super(EnabledEmuMixIn, self).update(object_id, data, old_obj) ref = super(EnabledEmuMixIn, self).update(object_id, data, old_obj)
 End of changes. 27 change blocks. 
65 lines changed or deleted 100 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)