2025-12-25 upload
This commit is contained in:
50
venv/Lib/site-packages/ldap3/abstract/__init__.py
Normal file
50
venv/Lib/site-packages/ldap3/abstract/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2016.08.31
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 Giovanni Cannata
|
||||
#
|
||||
# This file is part of ldap3.
|
||||
#
|
||||
# ldap3 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# ldap3 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with ldap3 in the COPYING and COPYING.LESSER files.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
STATUS_INIT = 'Initialized' # The entry object is initialized
|
||||
STATUS_VIRTUAL = 'Virtual' # The entry is a new writable entry, still empty
|
||||
STATUS_MANDATORY_MISSING = 'Missing mandatory attributes' # The entry has some mandatory attributes missing
|
||||
STATUS_READ = 'Read' # The entry has been read
|
||||
STATUS_WRITABLE = 'Writable' # The entry has been made writable, still no changes
|
||||
STATUS_PENDING_CHANGES = 'Pending changes' # The entry has some changes to commit, mandatory attributes are present
|
||||
STATUS_COMMITTED = 'Committed' # The entry changes has been committed
|
||||
STATUS_READY_FOR_DELETION = 'Ready for deletion' # The entry is set to be deleted
|
||||
STATUS_READY_FOR_MOVING = 'Ready for moving' # The entry is set to be moved in the DIT
|
||||
STATUS_READY_FOR_RENAMING = 'Ready for renaming' # The entry is set to be renamed
|
||||
STATUS_DELETED = 'Deleted' # The entry has been deleted
|
||||
|
||||
STATUSES = [STATUS_INIT,
|
||||
STATUS_VIRTUAL,
|
||||
STATUS_MANDATORY_MISSING,
|
||||
STATUS_READ,
|
||||
STATUS_WRITABLE,
|
||||
STATUS_PENDING_CHANGES,
|
||||
STATUS_COMMITTED,
|
||||
STATUS_READY_FOR_DELETION,
|
||||
STATUS_READY_FOR_MOVING,
|
||||
STATUS_READY_FOR_RENAMING,
|
||||
STATUS_DELETED]
|
||||
|
||||
INITIAL_STATUSES = [STATUS_READ, STATUS_WRITABLE, STATUS_VIRTUAL]
|
||||
121
venv/Lib/site-packages/ldap3/abstract/attrDef.py
Normal file
121
venv/Lib/site-packages/ldap3/abstract/attrDef.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2014.01.11
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 Giovanni Cannata
|
||||
|
||||
#
|
||||
# This file is part of ldap3.
|
||||
#
|
||||
# ldap3 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# ldap3 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with ldap3 in the COPYING and COPYING.LESSER files.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from os import linesep
|
||||
|
||||
from .. import SEQUENCE_TYPES
|
||||
from ..core.exceptions import LDAPKeyError
|
||||
from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED
|
||||
|
||||
|
||||
class AttrDef(object):
|
||||
"""Hold the definition of an attribute
|
||||
|
||||
:param name: the real attribute name
|
||||
:type name: string
|
||||
:param key: the friendly name to use in queries and when accessing the attribute, default to the real attribute name
|
||||
:type key: string
|
||||
:param validate: called to check if the value in the query is valid, the callable is called with the value parameter
|
||||
:type validate: callable
|
||||
:param pre_query: called to transform values returned by search
|
||||
:type pre_query: callable
|
||||
:param post_query: called to transform values returned by search
|
||||
:type post_query: callable
|
||||
:param default: value returned when the attribute is absent (defaults to NotImplemented to allow use of None as default)
|
||||
:type default: string, integer
|
||||
:param dereference_dn: reference to an ObjectDef instance. When the attribute value contains a dn it will be searched and substituted in the entry
|
||||
:type dereference_dn: ObjectDef
|
||||
:param description: custom attribute description
|
||||
:type description: string
|
||||
:param mandatory: specify if attribute is defined as mandatory in LDAP schema
|
||||
:type mandatory: boolean
|
||||
"""
|
||||
|
||||
def __init__(self, name, key=None, validate=None, pre_query=None, post_query=None, default=NotImplemented, dereference_dn=None, description=None, mandatory=False, single_value=None, alias=None):
|
||||
self.name = name
|
||||
self.key = ''.join(key.split()) if key else name # key set to name if not present
|
||||
self.validate = validate
|
||||
self.pre_query = pre_query
|
||||
self.post_query = post_query
|
||||
self.default = default
|
||||
self.dereference_dn = dereference_dn
|
||||
self.description = description
|
||||
self.mandatory = mandatory
|
||||
self.single_value = single_value
|
||||
self.oid_info = None
|
||||
if not alias:
|
||||
self.other_names = None
|
||||
elif isinstance(alias, SEQUENCE_TYPES): # multiple aliases
|
||||
self.\
|
||||
other_names = set(alias)
|
||||
else: # single alias
|
||||
self.other_names = set([alias]) # python 2 compatibility
|
||||
|
||||
if log_enabled(BASIC):
|
||||
log(BASIC, 'instantiated AttrDef: <%r>', self)
|
||||
|
||||
def __repr__(self):
|
||||
r = 'ATTR: ' + ', '.join([self.key] + list(self.other_names)) if self.other_names else self.key
|
||||
r += '' if self.name == self.key else ' [' + self.name + ']'
|
||||
r += '' if self.default is NotImplemented else ' - default: ' + str(self.default)
|
||||
r += '' if self.mandatory is None else ' - mandatory: ' + str(self.mandatory)
|
||||
r += '' if self.single_value is None else ' - single_value: ' + str(self.single_value)
|
||||
r += '' if not self.dereference_dn else ' - dereference_dn: ' + str(self.dereference_dn)
|
||||
r += '' if not self.description else ' - description: ' + str(self.description)
|
||||
if self.oid_info:
|
||||
for line in str(self.oid_info).split(linesep):
|
||||
r += linesep + ' ' + line
|
||||
return r
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, AttrDef):
|
||||
return self.key == other.key
|
||||
|
||||
return False
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, AttrDef):
|
||||
return self.key < other.key
|
||||
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
if self.key:
|
||||
return hash(self.key)
|
||||
else:
|
||||
return id(self) # unique for each instance
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if hasattr(self, 'key') and key == 'key': # key cannot be changed because is being used for __hash__
|
||||
error_message = 'key \'%s\' already set' % key
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPKeyError(error_message)
|
||||
else:
|
||||
object.__setattr__(self, key, value)
|
||||
290
venv/Lib/site-packages/ldap3/abstract/attribute.py
Normal file
290
venv/Lib/site-packages/ldap3/abstract/attribute.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2014.01.06
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 Giovanni Cannata
|
||||
#
|
||||
# This file is part of ldap3.
|
||||
#
|
||||
# ldap3 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# ldap3 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with ldap3 in the COPYING and COPYING.LESSER files.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from os import linesep
|
||||
|
||||
from .. import MODIFY_ADD, MODIFY_REPLACE, MODIFY_DELETE, SEQUENCE_TYPES
|
||||
from ..core.exceptions import LDAPCursorError
|
||||
from ..utils.repr import to_stdout_encoding
|
||||
from . import STATUS_PENDING_CHANGES, STATUS_VIRTUAL, STATUS_READY_FOR_DELETION, STATUS_READY_FOR_MOVING, STATUS_READY_FOR_RENAMING
|
||||
from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
class Attribute(object):
|
||||
"""Attribute/values object, it includes the search result (after post_query transformation) of each attribute in an entry
|
||||
|
||||
Attribute object is read only
|
||||
|
||||
- values: contain the processed attribute values
|
||||
- raw_values': contain the unprocessed attribute values
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, attr_def, entry, cursor):
|
||||
self.key = attr_def.key
|
||||
self.definition = attr_def
|
||||
self.values = []
|
||||
self.raw_values = []
|
||||
self.response = None
|
||||
self.entry = entry
|
||||
self.cursor = cursor
|
||||
other_names = [name for name in attr_def.oid_info.name if self.key.lower() != name.lower()] if attr_def.oid_info else None
|
||||
self.other_names = set(other_names) if other_names else None # self.other_names is None if there are no short names, else is a set of secondary names
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.values) == 1:
|
||||
r = to_stdout_encoding(self.key) + ': ' + to_stdout_encoding(self.values[0])
|
||||
elif len(self.values) > 1:
|
||||
r = to_stdout_encoding(self.key) + ': ' + to_stdout_encoding(self.values[0])
|
||||
filler = ' ' * (len(self.key) + 6)
|
||||
for value in self.values[1:]:
|
||||
r += linesep + filler + to_stdout_encoding(value)
|
||||
else:
|
||||
r = to_stdout_encoding(self.key) + ': ' + to_stdout_encoding('<no value>')
|
||||
|
||||
return r
|
||||
|
||||
def __str__(self):
|
||||
if len(self.values) == 1:
|
||||
return to_stdout_encoding(self.values[0])
|
||||
else:
|
||||
return to_stdout_encoding(self.values)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.values)
|
||||
|
||||
def __iter__(self):
|
||||
return self.values.__iter__()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.values[item]
|
||||
|
||||
def __getstate__(self):
|
||||
cpy = dict(self.__dict__)
|
||||
cpy['cursor'] = None
|
||||
return cpy
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.value == other:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
:return: The single value or a list of values of the attribute.
|
||||
"""
|
||||
if not self.values:
|
||||
return None
|
||||
|
||||
return self.values[0] if len(self.values) == 1 else self.values
|
||||
|
||||
|
||||
class OperationalAttribute(Attribute):
|
||||
"""Operational attribute/values object. Include the search result of an
|
||||
operational attribute in an entry
|
||||
|
||||
OperationalAttribute object is read only
|
||||
|
||||
- values: contains the processed attribute values
|
||||
- raw_values: contains the unprocessed attribute values
|
||||
|
||||
It may not have an AttrDef
|
||||
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.values) == 1:
|
||||
r = to_stdout_encoding(self.key) + ' [OPERATIONAL]: ' + to_stdout_encoding(self.values[0])
|
||||
elif len(self.values) > 1:
|
||||
r = to_stdout_encoding(self.key) + ' [OPERATIONAL]: ' + to_stdout_encoding(self.values[0])
|
||||
filler = ' ' * (len(self.key) + 6)
|
||||
for value in sorted(self.values[1:]):
|
||||
r += linesep + filler + to_stdout_encoding(value)
|
||||
else:
|
||||
r = ''
|
||||
|
||||
return r
|
||||
|
||||
|
||||
class WritableAttribute(Attribute):
|
||||
def __repr__(self):
|
||||
filler = ' ' * (len(self.key) + 6)
|
||||
if len(self.values) == 1:
|
||||
r = to_stdout_encoding(self.key) + ': ' + to_stdout_encoding(self.values[0])
|
||||
elif len(self.values) > 1:
|
||||
r = to_stdout_encoding(self.key) + ': ' + to_stdout_encoding(self.values[0])
|
||||
for value in self.values[1:]:
|
||||
r += linesep + filler + to_stdout_encoding(value)
|
||||
else:
|
||||
r = to_stdout_encoding(self.key) + to_stdout_encoding(': <Virtual>')
|
||||
if self.definition.name in self.entry._changes:
|
||||
r += linesep + filler + 'CHANGES: ' + str(self.entry._changes[self.definition.name])
|
||||
return r
|
||||
|
||||
def __iadd__(self, other):
|
||||
self.add(other)
|
||||
return Ellipsis # hack to avoid calling set() in entry __setattr__
|
||||
|
||||
def __isub__(self, other):
|
||||
self.delete(other)
|
||||
return Ellipsis # hack to avoid calling set_value in entry __setattr__
|
||||
|
||||
def _update_changes(self, changes, remove_old=False):
|
||||
# checks for friendly key in AttrDef and uses the real attribute name
|
||||
if self.definition and self.definition.name:
|
||||
key = self.definition.name
|
||||
else:
|
||||
key = self.key
|
||||
|
||||
if key not in self.entry._changes or remove_old: # remove old changes (for removing attribute)
|
||||
self.entry._changes[key] = []
|
||||
|
||||
self.entry._changes[key].append(changes)
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'updated changes <%r> for <%s> attribute in <%s> entry', changes, self.key, self.entry.entry_dn)
|
||||
self.entry._state.set_status(STATUS_PENDING_CHANGES)
|
||||
|
||||
def add(self, values):
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'adding %r to <%s> attribute in <%s> entry', values, self.key, self.entry.entry_dn)
|
||||
# new value for attribute to commit with a MODIFY_ADD
|
||||
if self.entry._state._initial_status == STATUS_VIRTUAL:
|
||||
error_message = 'cannot perform a modify operation in a new entry'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if self.entry.entry_status in [STATUS_READY_FOR_DELETION, STATUS_READY_FOR_MOVING, STATUS_READY_FOR_RENAMING]:
|
||||
error_message = self.entry.entry_status + ' - cannot add attributes'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if values is None:
|
||||
error_message = 'value to add cannot be None'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if values is not None:
|
||||
validated = self.definition.validate(values) # returns True, False or a value to substitute to the actual values
|
||||
if validated is False:
|
||||
error_message = 'value \'%s\' non valid for attribute \'%s\'' % (values, self.key)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
elif validated is not True: # a valid LDAP value equivalent to the actual values
|
||||
values = validated
|
||||
self._update_changes((MODIFY_ADD, values if isinstance(values, SEQUENCE_TYPES) else [values]))
|
||||
|
||||
def set(self, values):
|
||||
# new value for attribute to commit with a MODIFY_REPLACE, old values are deleted
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'setting %r to <%s> attribute in <%s> entry', values, self.key, self.entry.entry_dn)
|
||||
if self.entry.entry_status in [STATUS_READY_FOR_DELETION, STATUS_READY_FOR_MOVING, STATUS_READY_FOR_RENAMING]:
|
||||
error_message = self.entry.entry_status + ' - cannot set attributes'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if values is None:
|
||||
error_message = 'new value cannot be None'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
validated = self.definition.validate(values) # returns True, False or a value to substitute to the actual values
|
||||
if validated is False:
|
||||
error_message = 'value \'%s\' non valid for attribute \'%s\'' % (values, self.key)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
elif validated is not True: # a valid LDAP value equivalent to the actual values
|
||||
values = validated
|
||||
self._update_changes((MODIFY_REPLACE, values if isinstance(values, SEQUENCE_TYPES) else [values]), remove_old=True)
|
||||
|
||||
def delete(self, values):
|
||||
# value for attribute to delete in commit with a MODIFY_DELETE
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'deleting %r from <%s> attribute in <%s> entry', values, self.key, self.entry.entry_dn)
|
||||
if self.entry._state._initial_status == STATUS_VIRTUAL:
|
||||
error_message = 'cannot delete an attribute value in a new entry'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if self.entry.entry_status in [STATUS_READY_FOR_DELETION, STATUS_READY_FOR_MOVING, STATUS_READY_FOR_RENAMING]:
|
||||
error_message = self.entry.entry_status + ' - cannot delete attributes'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if values is None:
|
||||
error_message = 'value to delete cannot be None'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if not isinstance(values, SEQUENCE_TYPES):
|
||||
values = [values]
|
||||
for single_value in values:
|
||||
if single_value not in self.values:
|
||||
error_message = 'value \'%s\' not present in \'%s\'' % (single_value, ', '.join(self.values))
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
self._update_changes((MODIFY_DELETE, values))
|
||||
|
||||
def remove(self):
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'removing <%s> attribute in <%s> entry', self.key, self.entry.entry_dn)
|
||||
if self.entry._state._initial_status == STATUS_VIRTUAL:
|
||||
error_message = 'cannot remove an attribute in a new entry'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if self.entry.entry_status in [STATUS_READY_FOR_DELETION, STATUS_READY_FOR_MOVING, STATUS_READY_FOR_RENAMING]:
|
||||
error_message = self.entry.entry_status + ' - cannot remove attributes'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
self._update_changes((MODIFY_REPLACE, []), True)
|
||||
|
||||
def discard(self):
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'discarding <%s> attribute in <%s> entry', self.key, self.entry.entry_dn)
|
||||
del self.entry._changes[self.key]
|
||||
if not self.entry._changes:
|
||||
self.entry._state.set_status(self.entry._state._initial_status)
|
||||
|
||||
@property
|
||||
def virtual(self):
|
||||
return False if len(self.values) else True
|
||||
|
||||
@property
|
||||
def changes(self):
|
||||
if self.key in self.entry._changes:
|
||||
return self.entry._changes[self.key]
|
||||
return None
|
||||
912
venv/Lib/site-packages/ldap3/abstract/cursor.py
Normal file
912
venv/Lib/site-packages/ldap3/abstract/cursor.py
Normal file
@@ -0,0 +1,912 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2014.01.06
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 Giovanni Cannata
|
||||
#
|
||||
# This file is part of ldap3.
|
||||
#
|
||||
# ldap3 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# ldap3 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with ldap3 in the COPYING and COPYING.LESSER files.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from os import linesep
|
||||
from time import sleep
|
||||
|
||||
from . import STATUS_VIRTUAL, STATUS_READ, STATUS_WRITABLE
|
||||
from .. import SUBTREE, LEVEL, DEREF_ALWAYS, DEREF_NEVER, BASE, SEQUENCE_TYPES, STRING_TYPES, get_config_parameter
|
||||
from ..abstract import STATUS_PENDING_CHANGES
|
||||
from .attribute import Attribute, OperationalAttribute, WritableAttribute
|
||||
from .attrDef import AttrDef
|
||||
from .objectDef import ObjectDef
|
||||
from .entry import Entry, WritableEntry
|
||||
from ..core.exceptions import LDAPCursorError, LDAPObjectDereferenceError
|
||||
from ..core.results import RESULT_SUCCESS
|
||||
from ..utils.ciDict import CaseInsensitiveWithAliasDict
|
||||
from ..utils.dn import safe_dn, safe_rdn
|
||||
from ..utils.conv import to_raw
|
||||
from ..utils.config import get_config_parameter
|
||||
from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED
|
||||
from ..protocol.oid import ATTRIBUTE_DIRECTORY_OPERATION, ATTRIBUTE_DISTRIBUTED_OPERATION, ATTRIBUTE_DSA_OPERATION, CLASS_AUXILIARY
|
||||
|
||||
Operation = namedtuple('Operation', ('request', 'result', 'response'))
|
||||
|
||||
|
||||
def _ret_search_value(value):
|
||||
return value[0] + '=' + value[1:] if value[0] in '<>~' and value[1] != '=' else value
|
||||
|
||||
|
||||
def _create_query_dict(query_text):
|
||||
"""
|
||||
Create a dictionary with query key:value definitions
|
||||
query_text is a comma delimited key:value sequence
|
||||
"""
|
||||
query_dict = dict()
|
||||
if query_text:
|
||||
for arg_value_str in query_text.split(','):
|
||||
if ':' in arg_value_str:
|
||||
arg_value_list = arg_value_str.split(':')
|
||||
query_dict[arg_value_list[0].strip()] = arg_value_list[1].strip()
|
||||
|
||||
return query_dict
|
||||
|
||||
|
||||
class Cursor(object):
|
||||
# entry_class and attribute_class define the type of entry and attribute used by the cursor
|
||||
# entry_initial_status defines the initial status of a entry
|
||||
# entry_class = Entry, must be defined in subclasses
|
||||
# attribute_class = Attribute, must be defined in subclasses
|
||||
# entry_initial_status = STATUS, must be defined in subclasses
|
||||
|
||||
def __init__(self, connection, object_def, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):
|
||||
conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]
|
||||
self.connection = connection
|
||||
self.get_operational_attributes = get_operational_attributes
|
||||
if connection._deferred_bind or connection._deferred_open: # probably a lazy connection, tries to bind
|
||||
connection._fire_deferred()
|
||||
|
||||
if isinstance(object_def, (STRING_TYPES, SEQUENCE_TYPES)):
|
||||
if connection.closed: # try to open connection if closed to read schema
|
||||
connection.bind()
|
||||
object_def = ObjectDef(object_def, connection.server.schema, auxiliary_class=auxiliary_class)
|
||||
self.definition = object_def
|
||||
if attributes: # checks if requested attributes are defined in ObjectDef
|
||||
not_defined_attributes = []
|
||||
if isinstance(attributes, STRING_TYPES):
|
||||
attributes = [attributes]
|
||||
|
||||
for attribute in attributes:
|
||||
if attribute not in self.definition._attributes and attribute.lower() not in conf_attributes_excluded_from_object_def:
|
||||
not_defined_attributes.append(attribute)
|
||||
|
||||
if not_defined_attributes:
|
||||
error_message = 'Attributes \'%s\' non in definition' % ', '.join(not_defined_attributes)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
|
||||
self.attributes = set(attributes) if attributes else set([attr.name for attr in self.definition])
|
||||
self.controls = controls
|
||||
self.execution_time = None
|
||||
self.entries = []
|
||||
self.schema = self.connection.server.schema
|
||||
self._do_not_reset = False # used for refreshing entry in entry_refresh() without removing all entries from the Cursor
|
||||
self._operation_history = list() # a list storing all the requests, results and responses for the last cursor operation
|
||||
|
||||
def __repr__(self):
|
||||
r = 'CURSOR : ' + self.__class__.__name__ + linesep
|
||||
r += 'CONN : ' + str(self.connection) + linesep
|
||||
r += 'DEFS : ' + ', '.join(self.definition._object_class)
|
||||
if self.definition._auxiliary_class:
|
||||
r += ' [AUX: ' + ', '.join(self.definition._auxiliary_class) + ']'
|
||||
r += linesep
|
||||
# for attr_def in sorted(self.definition):
|
||||
# r += (attr_def.key if attr_def.key == attr_def.name else (attr_def.key + ' <' + attr_def.name + '>')) + ', '
|
||||
# if r[-2] == ',':
|
||||
# r = r[:-2]
|
||||
# r += ']' + linesep
|
||||
if hasattr(self, 'attributes'):
|
||||
r += 'ATTRS : ' + repr(sorted(self.attributes)) + (' [OPERATIONAL]' if self.get_operational_attributes else '') + linesep
|
||||
if isinstance(self, Reader):
|
||||
if hasattr(self, 'base'):
|
||||
r += 'BASE : ' + repr(self.base) + (' [SUB]' if self.sub_tree else ' [LEVEL]') + linesep
|
||||
if hasattr(self, '_query') and self._query:
|
||||
r += 'QUERY : ' + repr(self._query) + ('' if '(' in self._query else (' [AND]' if self.components_in_and else ' [OR]')) + linesep
|
||||
if hasattr(self, 'validated_query') and self.validated_query:
|
||||
r += 'PARSED : ' + repr(self.validated_query) + ('' if '(' in self._query else (' [AND]' if self.components_in_and else ' [OR]')) + linesep
|
||||
if hasattr(self, 'query_filter') and self.query_filter:
|
||||
r += 'FILTER : ' + repr(self.query_filter) + linesep
|
||||
|
||||
if hasattr(self, 'execution_time') and self.execution_time:
|
||||
r += 'ENTRIES: ' + str(len(self.entries))
|
||||
r += ' [executed at: ' + str(self.execution_time.isoformat()) + ']' + linesep
|
||||
|
||||
if self.failed:
|
||||
r += 'LAST OPERATION FAILED [' + str(len(self.errors)) + ' failure' + ('s' if len(self.errors) > 1 else '') + ' at operation' + ('s ' if len(self.errors) > 1 else ' ') + ', '.join([str(i) for i, error in enumerate(self.operations) if error.result['result'] != RESULT_SUCCESS]) + ']'
|
||||
|
||||
return r
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def __iter__(self):
|
||||
return self.entries.__iter__()
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Return indexed item, if index is not found then try to sequentially search in DN of entries.
|
||||
If only one entry is found return it else raise a KeyError exception. The exception message
|
||||
includes the number of entries that matches, if less than 10 entries match then show the DNs
|
||||
in the exception message.
|
||||
"""
|
||||
try:
|
||||
return self.entries[item]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if isinstance(item, STRING_TYPES):
|
||||
found = self.match_dn(item)
|
||||
|
||||
if len(found) == 1:
|
||||
return found[0]
|
||||
elif len(found) > 1:
|
||||
error_message = 'Multiple entries found: %d entries match the text in dn' % len(found) + ('' if len(found) > 10 else (' [' + '; '.join([e.entry_dn for e in found]) + ']'))
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise KeyError(error_message)
|
||||
|
||||
error_message = 'no entry found'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise KeyError(error_message)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.entries)
|
||||
|
||||
if str is not bytes: # Python 3
|
||||
def __bool__(self): # needed to make the cursor appears as existing in "if cursor:" even if there are no entries
|
||||
return True
|
||||
else: # Python 2
|
||||
def __nonzero__(self):
|
||||
return True
|
||||
|
||||
def _get_attributes(self, response, attr_defs, entry):
|
||||
"""Assign the result of the LDAP query to the Entry object dictionary.
|
||||
|
||||
If the optional 'post_query' callable is present in the AttrDef it is called with each value of the attribute and the callable result is stored in the attribute.
|
||||
|
||||
Returns the default value for missing attributes.
|
||||
If the 'dereference_dn' in AttrDef is a ObjectDef then the attribute values are treated as distinguished name and the relevant entry is retrieved and stored in the attribute value.
|
||||
|
||||
"""
|
||||
conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')
|
||||
conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]
|
||||
attributes = CaseInsensitiveWithAliasDict()
|
||||
used_attribute_names = set()
|
||||
for attr in attr_defs:
|
||||
attr_def = attr_defs[attr]
|
||||
attribute_name = None
|
||||
for attr_name in response['attributes']:
|
||||
if attr_def.name.lower() == attr_name.lower():
|
||||
attribute_name = attr_name
|
||||
break
|
||||
|
||||
if attribute_name or attr_def.default is not NotImplemented: # attribute value found in result or default value present - NotImplemented allows use of None as default
|
||||
attribute = self.attribute_class(attr_def, entry, self)
|
||||
attribute.response = response
|
||||
attribute.raw_values = response['raw_attributes'][attribute_name] if attribute_name else None
|
||||
if attr_def.post_query and attr_def.name in response['attributes'] and response['raw_attributes'] != list():
|
||||
attribute.values = attr_def.post_query(attr_def.key, response['attributes'][attribute_name])
|
||||
else:
|
||||
if attr_def.default is NotImplemented or (attribute_name and response['raw_attributes'][attribute_name] != list()):
|
||||
attribute.values = response['attributes'][attribute_name]
|
||||
else:
|
||||
attribute.values = attr_def.default if isinstance(attr_def.default, SEQUENCE_TYPES) else [attr_def.default]
|
||||
if not isinstance(attribute.values, list): # force attribute values to list (if attribute is single-valued)
|
||||
attribute.values = [attribute.values]
|
||||
if attr_def.dereference_dn: # try to get object referenced in value
|
||||
if attribute.values:
|
||||
temp_reader = Reader(self.connection, attr_def.dereference_dn, base='', get_operational_attributes=self.get_operational_attributes, controls=self.controls)
|
||||
temp_values = []
|
||||
for element in attribute.values:
|
||||
if entry.entry_dn != element:
|
||||
temp_values.append(temp_reader.search_object(element))
|
||||
else:
|
||||
error_message = 'object %s is referencing itself in the \'%s\' attribute' % (entry.entry_dn, attribute.definition.name)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPObjectDereferenceError(error_message)
|
||||
del temp_reader # remove the temporary Reader
|
||||
attribute.values = temp_values
|
||||
attributes[attribute.key] = attribute
|
||||
if attribute.other_names:
|
||||
attributes.set_alias(attribute.key, attribute.other_names)
|
||||
if attr_def.other_names:
|
||||
attributes.set_alias(attribute.key, attr_def.other_names)
|
||||
used_attribute_names.add(attribute_name)
|
||||
|
||||
if self.attributes:
|
||||
used_attribute_names.update(self.attributes)
|
||||
|
||||
for attribute_name in response['attributes']:
|
||||
if attribute_name not in used_attribute_names:
|
||||
operational_attribute = False
|
||||
# check if the type is an operational attribute
|
||||
if attribute_name in self.schema.attribute_types:
|
||||
if self.schema.attribute_types[attribute_name].no_user_modification or self.schema.attribute_types[attribute_name].usage in [ATTRIBUTE_DIRECTORY_OPERATION, ATTRIBUTE_DISTRIBUTED_OPERATION, ATTRIBUTE_DSA_OPERATION]:
|
||||
operational_attribute = True
|
||||
else:
|
||||
operational_attribute = True
|
||||
if not operational_attribute and attribute_name not in attr_defs and attribute_name.lower() not in conf_attributes_excluded_from_object_def:
|
||||
error_message = 'attribute \'%s\' not in object class \'%s\' for entry %s' % (attribute_name, ', '.join(entry.entry_definition._object_class), entry.entry_dn)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
attribute = OperationalAttribute(AttrDef(conf_operational_attribute_prefix + attribute_name), entry, self)
|
||||
attribute.raw_values = response['raw_attributes'][attribute_name]
|
||||
attribute.values = response['attributes'][attribute_name] if isinstance(response['attributes'][attribute_name], SEQUENCE_TYPES) else [response['attributes'][attribute_name]]
|
||||
if (conf_operational_attribute_prefix + attribute_name) not in attributes:
|
||||
attributes[conf_operational_attribute_prefix + attribute_name] = attribute
|
||||
|
||||
return attributes
|
||||
|
||||
def match_dn(self, dn):
|
||||
"""Return entries with text in DN"""
|
||||
matched = []
|
||||
for entry in self.entries:
|
||||
if dn.lower() in entry.entry_dn.lower():
|
||||
matched.append(entry)
|
||||
return matched
|
||||
|
||||
def match(self, attributes, value):
|
||||
"""Return entries with text in one of the specified attributes"""
|
||||
matched = []
|
||||
if not isinstance(attributes, SEQUENCE_TYPES):
|
||||
attributes = [attributes]
|
||||
|
||||
for entry in self.entries:
|
||||
found = False
|
||||
for attribute in attributes:
|
||||
if attribute in entry:
|
||||
for attr_value in entry[attribute].values:
|
||||
if hasattr(attr_value, 'lower') and hasattr(value, 'lower') and value.lower() in attr_value.lower():
|
||||
found = True
|
||||
elif value == attr_value:
|
||||
found = True
|
||||
if found:
|
||||
matched.append(entry)
|
||||
break
|
||||
if found:
|
||||
break
|
||||
# checks raw values, tries to convert value to byte
|
||||
raw_value = to_raw(value)
|
||||
if isinstance(raw_value, (bytes, bytearray)):
|
||||
for attr_value in entry[attribute].raw_values:
|
||||
if hasattr(attr_value, 'lower') and hasattr(raw_value, 'lower') and raw_value.lower() in attr_value.lower():
|
||||
found = True
|
||||
elif raw_value == attr_value:
|
||||
found = True
|
||||
if found:
|
||||
matched.append(entry)
|
||||
break
|
||||
if found:
|
||||
break
|
||||
return matched
|
||||
|
||||
def _create_entry(self, response):
|
||||
if not response['type'] == 'searchResEntry':
|
||||
return None
|
||||
|
||||
entry = self.entry_class(response['dn'], self) # define an Entry (writable or readonly), as specified in the cursor definition
|
||||
entry._state.attributes = self._get_attributes(response, self.definition._attributes, entry)
|
||||
entry._state.raw_attributes = deepcopy(response['raw_attributes'])
|
||||
|
||||
entry._state.response = response
|
||||
entry._state.read_time = datetime.now()
|
||||
entry._state.set_status(self.entry_initial_status)
|
||||
for attr in entry: # returns the whole attribute object
|
||||
entry.__dict__[attr.key] = attr
|
||||
|
||||
return entry
|
||||
|
||||
def _execute_query(self, query_scope, attributes):
|
||||
if not self.connection:
|
||||
error_message = 'no connection established'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
old_query_filter = None
|
||||
if query_scope == BASE: # requesting a single object so an always-valid filter is set
|
||||
if hasattr(self, 'query_filter'): # only Reader has a query filter
|
||||
old_query_filter = self.query_filter
|
||||
self.query_filter = '(objectclass=*)'
|
||||
else:
|
||||
self._create_query_filter()
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'executing query - base: %s - filter: %s - scope: %s for <%s>', self.base, self.query_filter, query_scope, self)
|
||||
with self.connection:
|
||||
result = self.connection.search(search_base=self.base,
|
||||
search_filter=self.query_filter,
|
||||
search_scope=query_scope,
|
||||
dereference_aliases=self.dereference_aliases,
|
||||
attributes=attributes if attributes else list(self.attributes),
|
||||
get_operational_attributes=self.get_operational_attributes,
|
||||
controls=self.controls)
|
||||
if not self.connection.strategy.sync:
|
||||
response, result, request = self.connection.get_response(result, get_request=True)
|
||||
else:
|
||||
if self.connection.strategy.thread_safe:
|
||||
_, result, response, _ = result
|
||||
else:
|
||||
response = self.connection.response
|
||||
result = self.connection.result
|
||||
request = self.connection.request
|
||||
|
||||
self._store_operation_in_history(request, result, response)
|
||||
|
||||
if self._do_not_reset: # trick to not remove entries when using _refresh()
|
||||
return self._create_entry(response[0])
|
||||
|
||||
self.entries = []
|
||||
for r in response:
|
||||
entry = self._create_entry(r)
|
||||
if entry is not None:
|
||||
self.entries.append(entry)
|
||||
if 'objectClass' in entry:
|
||||
for object_class in entry.objectClass:
|
||||
if self.schema and self.schema.object_classes[object_class].kind == CLASS_AUXILIARY and object_class not in self.definition._auxiliary_class:
|
||||
# add auxiliary class to object definition
|
||||
self.definition._auxiliary_class.append(object_class)
|
||||
self.definition._populate_attr_defs(object_class)
|
||||
self.execution_time = datetime.now()
|
||||
|
||||
if old_query_filter: # requesting a single object so an always-valid filter is set
|
||||
self.query_filter = old_query_filter
|
||||
|
||||
def remove(self, entry):
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'removing entry <%s> in <%s>', entry, self)
|
||||
self.entries.remove(entry)
|
||||
|
||||
def _reset_history(self):
|
||||
self._operation_history = list()
|
||||
|
||||
def _store_operation_in_history(self, request, result, response):
|
||||
self._operation_history.append(Operation(request, result, response))
|
||||
|
||||
@property
|
||||
def operations(self):
|
||||
return self._operation_history
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
return [error for error in self._operation_history if error.result['result'] != RESULT_SUCCESS]
|
||||
|
||||
@property
|
||||
def failed(self):
|
||||
if hasattr(self, '_operation_history'):
|
||||
return any([error.result['result'] != RESULT_SUCCESS for error in self._operation_history])
|
||||
|
||||
|
||||
class Reader(Cursor):
|
||||
"""Reader object to perform searches:
|
||||
|
||||
:param connection: the LDAP connection object to use
|
||||
:type connection: LDAPConnection
|
||||
:param object_def: the ObjectDef of the LDAP object returned
|
||||
:type object_def: ObjectDef
|
||||
:param query: the simplified query (will be transformed in an LDAP filter)
|
||||
:type query: str
|
||||
:param base: starting base of the search
|
||||
:type base: str
|
||||
:param components_in_and: specify if assertions in the query must all be satisfied or not (AND/OR)
|
||||
:type components_in_and: bool
|
||||
:param sub_tree: specify if the search must be performed ad Single Level (False) or Whole SubTree (True)
|
||||
:type sub_tree: bool
|
||||
:param get_operational_attributes: specify if operational attributes are returned or not
|
||||
:type get_operational_attributes: bool
|
||||
:param controls: controls to be used in search
|
||||
:type controls: tuple
|
||||
|
||||
"""
|
||||
entry_class = Entry # entries are read_only
|
||||
attribute_class = Attribute # attributes are read_only
|
||||
entry_initial_status = STATUS_READ
|
||||
|
||||
def __init__(self, connection, object_def, base, query='', components_in_and=True, sub_tree=True, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):
|
||||
Cursor.__init__(self, connection, object_def, get_operational_attributes, attributes, controls, auxiliary_class)
|
||||
self._components_in_and = components_in_and
|
||||
self.sub_tree = sub_tree
|
||||
self._query = query
|
||||
self.base = base
|
||||
self.dereference_aliases = DEREF_ALWAYS
|
||||
self.validated_query = None
|
||||
self._query_dict = dict()
|
||||
self._validated_query_dict = dict()
|
||||
self.query_filter = None
|
||||
self.reset()
|
||||
|
||||
if log_enabled(BASIC):
|
||||
log(BASIC, 'instantiated Reader Cursor: <%r>', self)
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self._query
|
||||
|
||||
@query.setter
|
||||
def query(self, value):
|
||||
self._query = value
|
||||
self.reset()
|
||||
|
||||
@property
|
||||
def components_in_and(self):
|
||||
return self._components_in_and
|
||||
|
||||
@components_in_and.setter
|
||||
def components_in_and(self, value):
|
||||
self._components_in_and = value
|
||||
self.reset()
|
||||
|
||||
def clear(self):
|
||||
"""Clear the Reader search parameters
|
||||
|
||||
"""
|
||||
self.dereference_aliases = DEREF_ALWAYS
|
||||
self._reset_history()
|
||||
|
||||
def reset(self):
|
||||
"""Clear all the Reader parameters
|
||||
|
||||
"""
|
||||
self.clear()
|
||||
self.validated_query = None
|
||||
self._query_dict = dict()
|
||||
self._validated_query_dict = dict()
|
||||
self.execution_time = None
|
||||
self.query_filter = None
|
||||
self.entries = []
|
||||
self._create_query_filter()
|
||||
|
||||
def _validate_query(self):
|
||||
"""Processes the text query and verifies that the requested friendly names are in the Reader dictionary
|
||||
If the AttrDef has a 'validate' property the callable is executed and if it returns False an Exception is raised
|
||||
|
||||
"""
|
||||
if not self._query_dict:
|
||||
self._query_dict = _create_query_dict(self._query)
|
||||
|
||||
query = ''
|
||||
for d in sorted(self._query_dict):
|
||||
attr = d[1:] if d[0] in '&|' else d
|
||||
for attr_def in self.definition:
|
||||
if ''.join(attr.split()).lower() == attr_def.key.lower():
|
||||
attr = attr_def.key
|
||||
break
|
||||
if attr in self.definition:
|
||||
vals = sorted(self._query_dict[d].split(';'))
|
||||
|
||||
query += (d[0] + attr if d[0] in '&|' else attr) + ': '
|
||||
for val in vals:
|
||||
val = val.strip()
|
||||
val_not = True if val[0] == '!' else False
|
||||
val_search_operator = '=' # default
|
||||
if val_not:
|
||||
if val[1:].lstrip()[0] not in '=<>~':
|
||||
value = val[1:].lstrip()
|
||||
else:
|
||||
val_search_operator = val[1:].lstrip()[0]
|
||||
value = val[1:].lstrip()[1:]
|
||||
else:
|
||||
if val[0] not in '=<>~':
|
||||
value = val.lstrip()
|
||||
else:
|
||||
val_search_operator = val[0]
|
||||
value = val[1:].lstrip()
|
||||
|
||||
if self.definition[attr].validate:
|
||||
validated = self.definition[attr].validate(value) # returns True, False or a value to substitute to the actual values
|
||||
if validated is False:
|
||||
error_message = 'validation failed for attribute %s and value %s' % (d, val)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
elif validated is not True: # a valid LDAP value equivalent to the actual values
|
||||
value = validated
|
||||
if val_not:
|
||||
query += '!' + val_search_operator + str(value)
|
||||
else:
|
||||
query += val_search_operator + str(value)
|
||||
|
||||
query += ';'
|
||||
query = query[:-1] + ', '
|
||||
else:
|
||||
error_message = 'attribute \'%s\' not in definition' % attr
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
self.validated_query = query[:-2]
|
||||
self._validated_query_dict = _create_query_dict(self.validated_query)
|
||||
|
||||
def _create_query_filter(self):
|
||||
"""Converts the query dictionary to the filter text"""
|
||||
self.query_filter = ''
|
||||
|
||||
if self.definition._object_class:
|
||||
self.query_filter += '(&'
|
||||
if isinstance(self.definition._object_class, SEQUENCE_TYPES) and len(self.definition._object_class) == 1:
|
||||
self.query_filter += '(objectClass=' + self.definition._object_class[0] + ')'
|
||||
elif isinstance(self.definition._object_class, SEQUENCE_TYPES):
|
||||
self.query_filter += '(&'
|
||||
for object_class in self.definition._object_class:
|
||||
self.query_filter += '(objectClass=' + object_class + ')'
|
||||
self.query_filter += ')'
|
||||
else:
|
||||
error_message = 'object class must be a string or a list'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
|
||||
if self._query and self._query.startswith('(') and self._query.endswith(')'): # query is already an LDAP filter
|
||||
if 'objectclass' not in self._query.lower():
|
||||
self.query_filter += self._query + ')' # if objectclass not in filter adds from definition
|
||||
else:
|
||||
self.query_filter = self._query
|
||||
return
|
||||
elif self._query: # if a simplified filter is present
|
||||
if not self.components_in_and:
|
||||
self.query_filter += '(|'
|
||||
elif not self.definition._object_class:
|
||||
self.query_filter += '(&'
|
||||
|
||||
self._validate_query()
|
||||
|
||||
attr_counter = 0
|
||||
for attr in sorted(self._validated_query_dict):
|
||||
attr_counter += 1
|
||||
multi = True if ';' in self._validated_query_dict[attr] else False
|
||||
vals = sorted(self._validated_query_dict[attr].split(';'))
|
||||
attr_def = self.definition[attr[1:]] if attr[0] in '&|' else self.definition[attr]
|
||||
if attr_def.pre_query:
|
||||
modvals = []
|
||||
for val in vals:
|
||||
modvals.append(val[0] + attr_def.pre_query(attr_def.key, val[1:]))
|
||||
vals = modvals
|
||||
if multi:
|
||||
if attr[0] in '&|':
|
||||
self.query_filter += '(' + attr[0]
|
||||
else:
|
||||
self.query_filter += '(|'
|
||||
|
||||
for val in vals:
|
||||
if val[0] == '!':
|
||||
self.query_filter += '(!(' + attr_def.name + _ret_search_value(val[1:]) + '))'
|
||||
else:
|
||||
self.query_filter += '(' + attr_def.name + _ret_search_value(val) + ')'
|
||||
if multi:
|
||||
self.query_filter += ')'
|
||||
|
||||
if not self.components_in_and:
|
||||
self.query_filter += '))'
|
||||
else:
|
||||
self.query_filter += ')'
|
||||
|
||||
if not self.definition._object_class and attr_counter == 1: # removes unneeded starting filter
|
||||
self.query_filter = self.query_filter[2: -1]
|
||||
|
||||
if self.query_filter == '(|)' or self.query_filter == '(&)': # removes empty filter
|
||||
self.query_filter = ''
|
||||
else: # no query, remove unneeded leading (&
|
||||
self.query_filter = self.query_filter[2:]
|
||||
|
||||
def search(self, attributes=None):
|
||||
"""Perform the LDAP search
|
||||
|
||||
:return: Entries found in search
|
||||
|
||||
"""
|
||||
self.clear()
|
||||
query_scope = SUBTREE if self.sub_tree else LEVEL
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'performing search in <%s>', self)
|
||||
self._execute_query(query_scope, attributes)
|
||||
|
||||
return self.entries
|
||||
|
||||
def search_object(self, entry_dn=None, attributes=None): # base must be a single dn
|
||||
"""Perform the LDAP search operation SINGLE_OBJECT scope
|
||||
|
||||
:return: Entry found in search
|
||||
|
||||
"""
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'performing object search in <%s>', self)
|
||||
self.clear()
|
||||
if entry_dn:
|
||||
old_base = self.base
|
||||
self.base = entry_dn
|
||||
self._execute_query(BASE, attributes)
|
||||
self.base = old_base
|
||||
else:
|
||||
self._execute_query(BASE, attributes)
|
||||
|
||||
return self.entries[0] if len(self.entries) > 0 else None
|
||||
|
||||
def search_level(self, attributes=None):
|
||||
"""Perform the LDAP search operation with SINGLE_LEVEL scope
|
||||
|
||||
:return: Entries found in search
|
||||
|
||||
"""
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'performing single level search in <%s>', self)
|
||||
self.clear()
|
||||
self._execute_query(LEVEL, attributes)
|
||||
|
||||
return self.entries
|
||||
|
||||
def search_subtree(self, attributes=None):
|
||||
"""Perform the LDAP search operation WHOLE_SUBTREE scope
|
||||
|
||||
:return: Entries found in search
|
||||
|
||||
"""
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'performing whole subtree search in <%s>', self)
|
||||
self.clear()
|
||||
self._execute_query(SUBTREE, attributes)
|
||||
|
||||
return self.entries
|
||||
|
||||
def _entries_generator(self, responses):
|
||||
for response in responses:
|
||||
yield self._create_entry(response)
|
||||
|
||||
def search_paged(self, paged_size, paged_criticality=True, generator=True, attributes=None):
|
||||
"""Perform a paged search, can be called as an Iterator
|
||||
|
||||
:param attributes: optional attributes to search
|
||||
:param paged_size: number of entries returned in each search
|
||||
:type paged_size: int
|
||||
:param paged_criticality: specify if server must not execute the search if it is not capable of paging searches
|
||||
:type paged_criticality: bool
|
||||
:param generator: if True the paged searches are executed while generating the entries,
|
||||
if False all the paged searches are execute before returning the generator
|
||||
:type generator: bool
|
||||
:return: Entries found in search
|
||||
|
||||
"""
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'performing paged search in <%s> with paged size %s', self, str(paged_size))
|
||||
if not self.connection:
|
||||
error_message = 'no connection established'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
|
||||
self.clear()
|
||||
self._create_query_filter()
|
||||
self.entries = []
|
||||
self.execution_time = datetime.now()
|
||||
response = self.connection.extend.standard.paged_search(search_base=self.base,
|
||||
search_filter=self.query_filter,
|
||||
search_scope=SUBTREE if self.sub_tree else LEVEL,
|
||||
dereference_aliases=self.dereference_aliases,
|
||||
attributes=attributes if attributes else self.attributes,
|
||||
get_operational_attributes=self.get_operational_attributes,
|
||||
controls=self.controls,
|
||||
paged_size=paged_size,
|
||||
paged_criticality=paged_criticality,
|
||||
generator=generator)
|
||||
if generator:
|
||||
return self._entries_generator(response)
|
||||
else:
|
||||
return list(self._entries_generator(response))
|
||||
|
||||
|
||||
class Writer(Cursor):
|
||||
entry_class = WritableEntry
|
||||
attribute_class = WritableAttribute
|
||||
entry_initial_status = STATUS_WRITABLE
|
||||
|
||||
@staticmethod
|
||||
def from_cursor(cursor, connection=None, object_def=None, custom_validator=None):
|
||||
if connection is None:
|
||||
connection = cursor.connection
|
||||
if object_def is None:
|
||||
object_def = cursor.definition
|
||||
writer = Writer(connection, object_def, attributes=cursor.attributes)
|
||||
for entry in cursor.entries:
|
||||
if isinstance(cursor, Reader):
|
||||
entry.entry_writable(object_def, writer, custom_validator=custom_validator)
|
||||
elif isinstance(cursor, Writer):
|
||||
pass
|
||||
else:
|
||||
error_message = 'unknown cursor type %s' % str(type(cursor))
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s', error_message)
|
||||
raise LDAPCursorError(error_message)
|
||||
writer.execution_time = cursor.execution_time
|
||||
if log_enabled(BASIC):
|
||||
log(BASIC, 'instantiated Writer Cursor <%r> from cursor <%r>', writer, cursor)
|
||||
return writer
|
||||
|
||||
@staticmethod
|
||||
def from_response(connection, object_def, response=None):
|
||||
if response is None:
|
||||
if not connection.strategy.sync:
|
||||
error_message = 'with asynchronous strategies response must be specified'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s', error_message)
|
||||
raise LDAPCursorError(error_message)
|
||||
elif connection.response:
|
||||
response = connection.response
|
||||
else:
|
||||
error_message = 'response not present'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s', error_message)
|
||||
raise LDAPCursorError(error_message)
|
||||
writer = Writer(connection, object_def)
|
||||
|
||||
for resp in response:
|
||||
if resp['type'] == 'searchResEntry':
|
||||
entry = writer._create_entry(resp)
|
||||
writer.entries.append(entry)
|
||||
if log_enabled(BASIC):
|
||||
log(BASIC, 'instantiated Writer Cursor <%r> from response', writer)
|
||||
return writer
|
||||
|
||||
def __init__(self, connection, object_def, get_operational_attributes=False, attributes=None, controls=None, auxiliary_class=None):
|
||||
Cursor.__init__(self, connection, object_def, get_operational_attributes, attributes, controls, auxiliary_class)
|
||||
self.dereference_aliases = DEREF_NEVER
|
||||
|
||||
if log_enabled(BASIC):
|
||||
log(BASIC, 'instantiated Writer Cursor: <%r>', self)
|
||||
|
||||
def commit(self, refresh=True):
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'committed changes for <%s>', self)
|
||||
self._reset_history()
|
||||
successful = True
|
||||
for entry in self.entries:
|
||||
if not entry.entry_commit_changes(refresh=refresh, controls=self.controls, clear_history=False):
|
||||
successful = False
|
||||
|
||||
self.execution_time = datetime.now()
|
||||
|
||||
return successful
|
||||
|
||||
def discard(self):
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'discarded changes for <%s>', self)
|
||||
for entry in self.entries:
|
||||
entry.entry_discard_changes()
|
||||
|
||||
def _refresh_object(self, entry_dn, attributes=None, tries=4, seconds=2, controls=None): # base must be a single dn
|
||||
"""Performs the LDAP search operation SINGLE_OBJECT scope
|
||||
|
||||
:return: Entry found in search
|
||||
|
||||
"""
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'refreshing object <%s> for <%s>', entry_dn, self)
|
||||
if not self.connection:
|
||||
error_message = 'no connection established'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
|
||||
response = []
|
||||
with self.connection:
|
||||
counter = 0
|
||||
while counter < tries:
|
||||
result = self.connection.search(search_base=entry_dn,
|
||||
search_filter='(objectclass=*)',
|
||||
search_scope=BASE,
|
||||
dereference_aliases=DEREF_NEVER,
|
||||
attributes=attributes if attributes else self.attributes,
|
||||
get_operational_attributes=self.get_operational_attributes,
|
||||
controls=controls)
|
||||
if not self.connection.strategy.sync:
|
||||
response, result, request = self.connection.get_response(result, get_request=True)
|
||||
else:
|
||||
if self.connection.strategy.thread_safe:
|
||||
_, result, response, request = result
|
||||
else:
|
||||
response = self.connection.response
|
||||
result = self.connection.result
|
||||
request = self.connection.request
|
||||
|
||||
if result['result'] in [RESULT_SUCCESS]:
|
||||
break
|
||||
sleep(seconds)
|
||||
counter += 1
|
||||
self._store_operation_in_history(request, result, response)
|
||||
|
||||
if len(response) == 1:
|
||||
return self._create_entry(response[0])
|
||||
elif len(response) == 0:
|
||||
return None
|
||||
|
||||
error_message = 'more than 1 entry returned for a single object search'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
|
||||
def new(self, dn):
|
||||
if log_enabled(BASIC):
|
||||
log(BASIC, 'creating new entry <%s> for <%s>', dn, self)
|
||||
dn = safe_dn(dn)
|
||||
for entry in self.entries: # checks if dn is already used in an cursor entry
|
||||
if entry.entry_dn == dn:
|
||||
error_message = 'dn already present in cursor'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
rdns = safe_rdn(dn, decompose=True)
|
||||
entry = self.entry_class(dn, self) # defines a new empty Entry
|
||||
for attr in entry.entry_mandatory_attributes: # defines all mandatory attributes as virtual
|
||||
entry._state.attributes[attr] = self.attribute_class(entry._state.definition[attr], entry, self)
|
||||
entry.__dict__[attr] = entry._state.attributes[attr]
|
||||
entry.objectclass.set(self.definition._object_class)
|
||||
for rdn in rdns: # adds virtual attributes from rdns in entry name (should be more than one with + syntax)
|
||||
if rdn[0] in entry._state.definition._attributes:
|
||||
rdn_name = entry._state.definition._attributes[rdn[0]].name # normalize case folding
|
||||
if rdn_name not in entry._state.attributes:
|
||||
entry._state.attributes[rdn_name] = self.attribute_class(entry._state.definition[rdn_name], entry, self)
|
||||
entry.__dict__[rdn_name] = entry._state.attributes[rdn_name]
|
||||
entry.__dict__[rdn_name].set(rdn[1])
|
||||
else:
|
||||
error_message = 'rdn type \'%s\' not in object class definition' % rdn[0]
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
entry._state.set_status(STATUS_VIRTUAL) # set intial status
|
||||
entry._state.set_status(STATUS_PENDING_CHANGES) # tries to change status to PENDING_CHANGES. If mandatory attributes are missing status is reverted to MANDATORY_MISSING
|
||||
self.entries.append(entry)
|
||||
return entry
|
||||
|
||||
def refresh_entry(self, entry, tries=4, seconds=2):
|
||||
conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')
|
||||
|
||||
self._do_not_reset = True
|
||||
attr_list = []
|
||||
if log_enabled(PROTOCOL):
|
||||
log(PROTOCOL, 'refreshing entry <%s> for <%s>', entry, self)
|
||||
for attr in entry._state.attributes: # check friendly attribute name in AttrDef, do not check operational attributes
|
||||
if attr.lower().startswith(conf_operational_attribute_prefix.lower()):
|
||||
continue
|
||||
if entry._state.definition[attr].name:
|
||||
attr_list.append(entry._state.definition[attr].name)
|
||||
else:
|
||||
attr_list.append(entry._state.definition[attr].key)
|
||||
|
||||
temp_entry = self._refresh_object(entry.entry_dn, attr_list, tries, seconds=seconds) # if any attributes is added adds only to the entry not to the definition
|
||||
self._do_not_reset = False
|
||||
if temp_entry:
|
||||
temp_entry._state.origin = entry._state.origin
|
||||
entry.__dict__.clear()
|
||||
entry.__dict__['_state'] = temp_entry._state
|
||||
for attr in entry._state.attributes: # returns the attribute key
|
||||
entry.__dict__[attr] = entry._state.attributes[attr]
|
||||
|
||||
for attr in entry.entry_attributes: # if any attribute of the class was deleted makes it virtual
|
||||
if attr not in entry._state.attributes and attr in entry.entry_definition._attributes:
|
||||
entry._state.attributes[attr] = WritableAttribute(entry.entry_definition[attr], entry, self)
|
||||
entry.__dict__[attr] = entry._state.attributes[attr]
|
||||
entry._state.set_status(entry._state._initial_status)
|
||||
return True
|
||||
return False
|
||||
712
venv/Lib/site-packages/ldap3/abstract/entry.py
Normal file
712
venv/Lib/site-packages/ldap3/abstract/entry.py
Normal file
@@ -0,0 +1,712 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2016.08.19
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2016 - 2020 Giovanni Cannata
|
||||
#
|
||||
# This file is part of ldap3.
|
||||
#
|
||||
# ldap3 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# ldap3 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with ldap3 in the COPYING and COPYING.LESSER files.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import json
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ..utils.ordDict import OrderedDict # for Python 2.6
|
||||
|
||||
from os import linesep
|
||||
from copy import deepcopy
|
||||
|
||||
from .. import STRING_TYPES, SEQUENCE_TYPES, MODIFY_ADD, MODIFY_REPLACE
|
||||
from .attribute import WritableAttribute
|
||||
from .objectDef import ObjectDef
|
||||
from .attrDef import AttrDef
|
||||
from ..core.exceptions import LDAPKeyError, LDAPCursorError, LDAPCursorAttributeError
|
||||
from ..utils.conv import check_json_dict, format_json, prepare_for_stream
|
||||
from ..protocol.rfc2849 import operation_to_ldif, add_ldif_header
|
||||
from ..utils.dn import safe_dn, safe_rdn, to_dn
|
||||
from ..utils.repr import to_stdout_encoding
|
||||
from ..utils.ciDict import CaseInsensitiveWithAliasDict
|
||||
from ..utils.config import get_config_parameter
|
||||
from . import STATUS_VIRTUAL, STATUS_WRITABLE, STATUS_PENDING_CHANGES, STATUS_COMMITTED, STATUS_DELETED,\
|
||||
STATUS_INIT, STATUS_READY_FOR_DELETION, STATUS_READY_FOR_MOVING, STATUS_READY_FOR_RENAMING, STATUS_MANDATORY_MISSING, STATUSES, INITIAL_STATUSES
|
||||
from ..core.results import RESULT_SUCCESS
|
||||
from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED
|
||||
|
||||
|
||||
class EntryState(object):
|
||||
"""Contains data on the status of the entry. Does not pollute the Entry __dict__.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, dn, cursor):
|
||||
self.dn = dn
|
||||
self._initial_status = None
|
||||
self._to = None # used for move and rename
|
||||
self.status = STATUS_INIT
|
||||
self.attributes = CaseInsensitiveWithAliasDict()
|
||||
self.raw_attributes = CaseInsensitiveWithAliasDict()
|
||||
self.response = None
|
||||
self.cursor = cursor
|
||||
self.origin = None # reference to the original read-only entry (set when made writable). Needed to update attributes in read-only when modified (only if both refer the same server)
|
||||
self.read_time = None
|
||||
self.changes = OrderedDict() # includes changes to commit in a writable entry
|
||||
if cursor.definition:
|
||||
self.definition = cursor.definition
|
||||
else:
|
||||
self.definition = None
|
||||
|
||||
def __repr__(self):
|
||||
if self.__dict__ and self.dn is not None:
|
||||
r = 'DN: ' + to_stdout_encoding(self.dn) + ' - STATUS: ' + ((self._initial_status + ', ') if self._initial_status != self.status else '') + self.status + ' - READ TIME: ' + (self.read_time.isoformat() if self.read_time else '<never>') + linesep
|
||||
r += 'attributes: ' + ', '.join(sorted(self.attributes.keys())) + linesep
|
||||
r += 'object def: ' + (', '.join(sorted(self.definition._object_class)) if self.definition._object_class else '<None>') + linesep
|
||||
r += 'attr defs: ' + ', '.join(sorted(self.definition._attributes.keys())) + linesep
|
||||
r += 'response: ' + ('present' if self.response else '<None>') + linesep
|
||||
r += 'cursor: ' + (self.cursor.__class__.__name__ if self.cursor else '<None>') + linesep
|
||||
return r
|
||||
else:
|
||||
return object.__repr__(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def __getstate__(self):
|
||||
cpy = dict(self.__dict__)
|
||||
cpy['cursor'] = None
|
||||
return cpy
|
||||
|
||||
def set_status(self, status):
|
||||
conf_ignored_mandatory_attributes_in_object_def = [v.lower() for v in get_config_parameter('IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF')]
|
||||
if status not in STATUSES:
|
||||
error_message = 'invalid entry status ' + str(status)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
if status in INITIAL_STATUSES:
|
||||
self._initial_status = status
|
||||
self.status = status
|
||||
if status == STATUS_DELETED:
|
||||
self._initial_status = STATUS_VIRTUAL
|
||||
if status == STATUS_COMMITTED:
|
||||
self._initial_status = STATUS_WRITABLE
|
||||
if self.status == STATUS_VIRTUAL or (self.status == STATUS_PENDING_CHANGES and self._initial_status == STATUS_VIRTUAL): # checks if all mandatory attributes are present in new entries
|
||||
for attr in self.definition._attributes:
|
||||
if self.definition._attributes[attr].mandatory and attr.lower() not in conf_ignored_mandatory_attributes_in_object_def:
|
||||
if (attr not in self.attributes or self.attributes[attr].virtual) and attr not in self.changes:
|
||||
self.status = STATUS_MANDATORY_MISSING
|
||||
break
|
||||
|
||||
@property
|
||||
def entry_raw_attributes(self):
|
||||
return self.raw_attributes
|
||||
|
||||
|
||||
class EntryBase(object):
|
||||
"""The Entry object contains a single LDAP entry.
|
||||
Attributes can be accessed either by sequence, by assignment
|
||||
or as dictionary keys. Keys are not case sensitive.
|
||||
|
||||
The Entry object is read only
|
||||
|
||||
- The DN is retrieved by entry_dn
|
||||
- The cursor reference is in _cursor
|
||||
- Raw attributes values are retrieved with _raw_attributes and the _raw_attribute() methods
|
||||
"""
|
||||
|
||||
def __init__(self, dn, cursor):
|
||||
self._state = EntryState(dn, cursor)
|
||||
|
||||
def __repr__(self):
|
||||
if self.__dict__ and self.entry_dn is not None:
|
||||
r = 'DN: ' + to_stdout_encoding(self.entry_dn) + ' - STATUS: ' + ((self._state._initial_status + ', ') if self._state._initial_status != self.entry_status else '') + self.entry_status + ' - READ TIME: ' + (self.entry_read_time.isoformat() if self.entry_read_time else '<never>') + linesep
|
||||
if self._state.attributes:
|
||||
for attr in sorted(self._state.attributes):
|
||||
if self._state.attributes[attr] or (hasattr(self._state.attributes[attr], 'changes') and self._state.attributes[attr].changes):
|
||||
r += ' ' + repr(self._state.attributes[attr]) + linesep
|
||||
return r
|
||||
else:
|
||||
return object.__repr__(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def __iter__(self):
|
||||
for attribute in self._state.attributes:
|
||||
yield self._state.attributes[attribute]
|
||||
# raise StopIteration # deprecated in PEP 479
|
||||
return
|
||||
|
||||
def __contains__(self, item):
|
||||
try:
|
||||
self.__getitem__(item)
|
||||
return True
|
||||
except LDAPKeyError:
|
||||
return False
|
||||
|
||||
def __getattr__(self, item):
|
||||
if isinstance(item, STRING_TYPES):
|
||||
if item == '_state':
|
||||
return object.__getattr__(self, item)
|
||||
item = ''.join(item.split()).lower()
|
||||
attr_found = None
|
||||
for attr in self._state.attributes.keys():
|
||||
if item == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.aliases():
|
||||
if item == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.keys():
|
||||
if item + ';binary' == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.aliases():
|
||||
if item + ';binary' == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.keys():
|
||||
if item + ';range' in attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.aliases():
|
||||
if item + ';range' in attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
error_message = 'attribute \'%s\' not found' % item
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorAttributeError(error_message)
|
||||
return self._state.attributes[attr]
|
||||
error_message = 'attribute name must be a string'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorAttributeError(error_message)
|
||||
|
||||
def __setattr__(self, item, value):
|
||||
if item == '_state':
|
||||
object.__setattr__(self, item, value)
|
||||
elif item in self._state.attributes:
|
||||
error_message = 'attribute \'%s\' is read only' % item
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorAttributeError(error_message)
|
||||
else:
|
||||
error_message = 'entry is read only, cannot add \'%s\'' % item
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorAttributeError(error_message)
|
||||
|
||||
def __getitem__(self, item):
|
||||
if isinstance(item, STRING_TYPES):
|
||||
item = ''.join(item.split()).lower()
|
||||
attr_found = None
|
||||
for attr in self._state.attributes.keys():
|
||||
if item == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.aliases():
|
||||
if item == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.keys():
|
||||
if item + ';binary' == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
for attr in self._state.attributes.aliases():
|
||||
if item + ';binary' == attr.lower():
|
||||
attr_found = attr
|
||||
break
|
||||
if not attr_found:
|
||||
error_message = 'key \'%s\' not found' % item
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPKeyError(error_message)
|
||||
return self._state.attributes[attr]
|
||||
|
||||
error_message = 'key must be a string'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPKeyError(error_message)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, EntryBase):
|
||||
return self.entry_dn == other.entry_dn
|
||||
|
||||
return False
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, EntryBase):
|
||||
return self.entry_dn <= other.entry_dn
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def entry_dn(self):
|
||||
return self._state.dn
|
||||
|
||||
@property
|
||||
def entry_cursor(self):
|
||||
return self._state.cursor
|
||||
|
||||
@property
|
||||
def entry_status(self):
|
||||
return self._state.status
|
||||
|
||||
@property
|
||||
def entry_definition(self):
|
||||
return self._state.definition
|
||||
|
||||
@property
|
||||
def entry_raw_attributes(self):
|
||||
return self._state.raw_attributes
|
||||
|
||||
def entry_raw_attribute(self, name):
|
||||
"""
|
||||
|
||||
:param name: name of the attribute
|
||||
:return: raw (unencoded) value of the attribute, None if attribute is not found
|
||||
"""
|
||||
return self._state.raw_attributes[name] if name in self._state.raw_attributes else None
|
||||
|
||||
@property
|
||||
def entry_mandatory_attributes(self):
|
||||
return [attribute for attribute in self.entry_definition._attributes if self.entry_definition._attributes[attribute].mandatory]
|
||||
|
||||
@property
|
||||
def entry_attributes(self):
|
||||
return list(self._state.attributes.keys())
|
||||
|
||||
@property
|
||||
def entry_attributes_as_dict(self):
|
||||
return dict((attribute_key, deepcopy(attribute_value.values)) for (attribute_key, attribute_value) in self._state.attributes.items())
|
||||
|
||||
@property
|
||||
def entry_read_time(self):
|
||||
return self._state.read_time
|
||||
|
||||
@property
|
||||
def _changes(self):
|
||||
return self._state.changes
|
||||
|
||||
def entry_to_json(self, raw=False, indent=4, sort=True, stream=None, checked_attributes=True, include_empty=True):
|
||||
json_entry = dict()
|
||||
json_entry['dn'] = self.entry_dn
|
||||
if checked_attributes:
|
||||
if not include_empty:
|
||||
# needed for python 2.6 compatibility
|
||||
json_entry['attributes'] = dict((key, self.entry_attributes_as_dict[key]) for key in self.entry_attributes_as_dict if self.entry_attributes_as_dict[key])
|
||||
else:
|
||||
json_entry['attributes'] = self.entry_attributes_as_dict
|
||||
if raw:
|
||||
if not include_empty:
|
||||
# needed for python 2.6 compatibility
|
||||
json_entry['raw'] = dict((key, self.entry_raw_attributes[key]) for key in self.entry_raw_attributes if self.entry_raw_attributes[key])
|
||||
else:
|
||||
json_entry['raw'] = dict(self.entry_raw_attributes)
|
||||
|
||||
if str is bytes: # Python 2
|
||||
check_json_dict(json_entry)
|
||||
|
||||
json_output = json.dumps(json_entry,
|
||||
ensure_ascii=True,
|
||||
sort_keys=sort,
|
||||
indent=indent,
|
||||
check_circular=True,
|
||||
default=format_json,
|
||||
separators=(',', ': '))
|
||||
|
||||
if stream:
|
||||
stream.write(json_output)
|
||||
|
||||
return json_output
|
||||
|
||||
def entry_to_ldif(self, all_base64=False, line_separator=None, sort_order=None, stream=None):
|
||||
ldif_lines = operation_to_ldif('searchResponse', [self._state.response], all_base64, sort_order=sort_order)
|
||||
ldif_lines = add_ldif_header(ldif_lines)
|
||||
line_separator = line_separator or linesep
|
||||
ldif_output = line_separator.join(ldif_lines)
|
||||
if stream:
|
||||
if stream.tell() == 0:
|
||||
header = add_ldif_header(['-'])[0]
|
||||
stream.write(prepare_for_stream(header + line_separator + line_separator))
|
||||
stream.write(prepare_for_stream(ldif_output + line_separator + line_separator))
|
||||
return ldif_output
|
||||
|
||||
|
||||
class Entry(EntryBase):
|
||||
"""The Entry object contains a single LDAP entry.
|
||||
Attributes can be accessed either by sequence, by assignment
|
||||
or as dictionary keys. Keys are not case sensitive.
|
||||
|
||||
The Entry object is read only
|
||||
|
||||
- The DN is retrieved by entry_dn
|
||||
- The Reader reference is in _cursor()
|
||||
- Raw attributes values are retrieved by the _ra_attributes and
|
||||
_raw_attribute() methods
|
||||
|
||||
"""
|
||||
def entry_writable(self, object_def=None, writer_cursor=None, attributes=None, custom_validator=None, auxiliary_class=None):
|
||||
conf_operational_attribute_prefix = get_config_parameter('ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX')
|
||||
if not self.entry_cursor.schema:
|
||||
error_message = 'schema must be available to make an entry writable'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
# returns a new WritableEntry and its Writer cursor
|
||||
if object_def is None:
|
||||
if self.entry_cursor.definition._object_class:
|
||||
object_def = self.entry_definition._object_class
|
||||
auxiliary_class = self.entry_definition._auxiliary_class + (auxiliary_class if isinstance(auxiliary_class, SEQUENCE_TYPES) else [])
|
||||
elif 'objectclass' in self:
|
||||
object_def = self.objectclass.values
|
||||
|
||||
if not object_def:
|
||||
error_message = 'object class must be specified to make an entry writable'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
|
||||
if not isinstance(object_def, ObjectDef):
|
||||
object_def = ObjectDef(object_def, self.entry_cursor.schema, custom_validator, auxiliary_class)
|
||||
|
||||
if attributes:
|
||||
if isinstance(attributes, STRING_TYPES):
|
||||
attributes = [attributes]
|
||||
|
||||
if isinstance(attributes, SEQUENCE_TYPES):
|
||||
for attribute in attributes:
|
||||
if attribute not in object_def._attributes:
|
||||
error_message = 'attribute \'%s\' not in schema for \'%s\'' % (attribute, object_def)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
else:
|
||||
attributes = []
|
||||
|
||||
if not writer_cursor:
|
||||
from .cursor import Writer # local import to avoid circular reference in import at startup
|
||||
writable_cursor = Writer(self.entry_cursor.connection, object_def)
|
||||
else:
|
||||
writable_cursor = writer_cursor
|
||||
|
||||
if attributes: # force reading of attributes
|
||||
writable_entry = writable_cursor._refresh_object(self.entry_dn, list(attributes) + self.entry_attributes)
|
||||
else:
|
||||
writable_entry = writable_cursor._create_entry(self._state.response)
|
||||
writable_cursor.entries.append(writable_entry)
|
||||
writable_entry._state.read_time = self.entry_read_time
|
||||
writable_entry._state.origin = self # reference to the original read-only entry
|
||||
# checks original entry for custom definitions in AttrDefs
|
||||
attr_to_add = []
|
||||
attr_to_remove = []
|
||||
object_def_to_add = []
|
||||
object_def_to_remove = []
|
||||
for attr in writable_entry._state.origin.entry_definition._attributes:
|
||||
original_attr = writable_entry._state.origin.entry_definition._attributes[attr]
|
||||
if attr != original_attr.name and (attr not in writable_entry._state.attributes or conf_operational_attribute_prefix + original_attr.name not in writable_entry._state.attributes):
|
||||
old_attr_def = writable_entry.entry_definition._attributes[original_attr.name]
|
||||
new_attr_def = AttrDef(original_attr.name,
|
||||
key=attr,
|
||||
validate=original_attr.validate,
|
||||
pre_query=original_attr.pre_query,
|
||||
post_query=original_attr.post_query,
|
||||
default=original_attr.default,
|
||||
dereference_dn=original_attr.dereference_dn,
|
||||
description=original_attr.description,
|
||||
mandatory=old_attr_def.mandatory, # keeps value read from schema
|
||||
single_value=old_attr_def.single_value, # keeps value read from schema
|
||||
alias=original_attr.other_names)
|
||||
od = writable_entry.entry_definition
|
||||
object_def_to_remove.append(old_attr_def)
|
||||
object_def_to_add.append(new_attr_def)
|
||||
# updates attribute name in entry attributes
|
||||
new_attr = WritableAttribute(new_attr_def, writable_entry, writable_cursor)
|
||||
if original_attr.name in writable_entry._state.attributes:
|
||||
new_attr.other_names = writable_entry._state.attributes[original_attr.name].other_names
|
||||
new_attr.raw_values = writable_entry._state.attributes[original_attr.name].raw_values
|
||||
new_attr.values = writable_entry._state.attributes[original_attr.name].values
|
||||
new_attr.response = writable_entry._state.attributes[original_attr.name].response
|
||||
attr_to_add.append((attr, new_attr))
|
||||
attr_to_remove.append(original_attr.name)
|
||||
# writable_entry._state.attributes[attr] = new_attr
|
||||
## writable_entry._state.attributes.set_alias(attr, new_attr.other_names)
|
||||
# del writable_entry._state.attributes[original_attr.name]
|
||||
for attr, new_attr in attr_to_add:
|
||||
writable_entry._state.attributes[attr] = new_attr
|
||||
for attr in attr_to_remove:
|
||||
del writable_entry._state.attributes[attr]
|
||||
for object_def in object_def_to_remove:
|
||||
o = writable_entry.entry_definition
|
||||
o -= object_def
|
||||
for object_def in object_def_to_add:
|
||||
o = writable_entry.entry_definition
|
||||
o += object_def
|
||||
|
||||
writable_entry._state.set_status(STATUS_WRITABLE)
|
||||
return writable_entry
|
||||
|
||||
|
||||
class WritableEntry(EntryBase):
|
||||
def __setitem__(self, key, value):
|
||||
if value is not Ellipsis: # hack for using implicit operators in writable attributes
|
||||
self.__setattr__(key, value)
|
||||
|
||||
def __setattr__(self, item, value):
|
||||
conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]
|
||||
if item == '_state' and isinstance(value, EntryState):
|
||||
self.__dict__['_state'] = value
|
||||
return
|
||||
|
||||
if value is not Ellipsis: # hack for using implicit operators in writable attributes
|
||||
# checks if using an alias
|
||||
if item in self.entry_cursor.definition._attributes or item.lower() in conf_attributes_excluded_from_object_def:
|
||||
if item not in self._state.attributes: # setting value to an attribute still without values
|
||||
new_attribute = WritableAttribute(self.entry_cursor.definition._attributes[item], self, cursor=self.entry_cursor)
|
||||
self._state.attributes[str(item)] = new_attribute # force item to a string for key in attributes dict
|
||||
self._state.attributes[item].set(value) # try to add to new_values
|
||||
else:
|
||||
error_message = 'attribute \'%s\' not defined' % item
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorAttributeError(error_message)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if isinstance(item, STRING_TYPES):
|
||||
if item == '_state':
|
||||
return self.__dict__['_state']
|
||||
item = ''.join(item.split()).lower()
|
||||
for attr in self._state.attributes.keys():
|
||||
if item == attr.lower():
|
||||
return self._state.attributes[attr]
|
||||
for attr in self._state.attributes.aliases():
|
||||
if item == attr.lower():
|
||||
return self._state.attributes[attr]
|
||||
if item in self.entry_definition._attributes: # item is a new attribute to commit, creates the AttrDef and add to the attributes to retrive
|
||||
self._state.attributes[item] = WritableAttribute(self.entry_definition._attributes[item], self, self.entry_cursor)
|
||||
self.entry_cursor.attributes.add(item)
|
||||
return self._state.attributes[item]
|
||||
error_message = 'attribute \'%s\' not defined' % item
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorAttributeError(error_message)
|
||||
else:
|
||||
error_message = 'attribute name must be a string'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorAttributeError(error_message)
|
||||
|
||||
@property
|
||||
def entry_virtual_attributes(self):
|
||||
return [attr for attr in self.entry_attributes if self[attr].virtual]
|
||||
|
||||
def entry_commit_changes(self, refresh=True, controls=None, clear_history=True):
|
||||
if clear_history:
|
||||
self.entry_cursor._reset_history()
|
||||
|
||||
if self.entry_status == STATUS_READY_FOR_DELETION:
|
||||
result = self.entry_cursor.connection.delete(self.entry_dn, controls)
|
||||
if not self.entry_cursor.connection.strategy.sync:
|
||||
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
|
||||
else:
|
||||
if self.entry_cursor.connection.strategy.thread_safe:
|
||||
_, result, response, request = result
|
||||
else:
|
||||
response = self.entry_cursor.connection.response
|
||||
result = self.entry_cursor.connection.result
|
||||
request = self.entry_cursor.connection.request
|
||||
self.entry_cursor._store_operation_in_history(request, result, response)
|
||||
if result['result'] == RESULT_SUCCESS:
|
||||
dn = self.entry_dn
|
||||
if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # deletes original read-only Entry
|
||||
cursor = self._state.origin.entry_cursor
|
||||
self._state.origin.__dict__.clear()
|
||||
self._state.origin.__dict__['_state'] = EntryState(dn, cursor)
|
||||
self._state.origin._state.set_status(STATUS_DELETED)
|
||||
cursor = self.entry_cursor
|
||||
self.__dict__.clear()
|
||||
self._state = EntryState(dn, cursor)
|
||||
self._state.set_status(STATUS_DELETED)
|
||||
return True
|
||||
return False
|
||||
elif self.entry_status == STATUS_READY_FOR_MOVING:
|
||||
result = self.entry_cursor.connection.modify_dn(self.entry_dn, '+'.join(safe_rdn(self.entry_dn)), new_superior=self._state._to)
|
||||
if not self.entry_cursor.connection.strategy.sync:
|
||||
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
|
||||
else:
|
||||
if self.entry_cursor.connection.strategy.thread_safe:
|
||||
_, result, response, request = result
|
||||
else:
|
||||
response = self.entry_cursor.connection.response
|
||||
result = self.entry_cursor.connection.result
|
||||
request = self.entry_cursor.connection.request
|
||||
self.entry_cursor._store_operation_in_history(request, result, response)
|
||||
if result['result'] == RESULT_SUCCESS:
|
||||
self._state.dn = safe_dn('+'.join(safe_rdn(self.entry_dn)) + ',' + self._state._to)
|
||||
if refresh:
|
||||
if self.entry_refresh():
|
||||
if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # refresh dn of origin
|
||||
self._state.origin._state.dn = self.entry_dn
|
||||
self._state.set_status(STATUS_COMMITTED)
|
||||
self._state._to = None
|
||||
return True
|
||||
return False
|
||||
elif self.entry_status == STATUS_READY_FOR_RENAMING:
|
||||
rdn = '+'.join(safe_rdn(self._state._to))
|
||||
result = self.entry_cursor.connection.modify_dn(self.entry_dn, rdn)
|
||||
if not self.entry_cursor.connection.strategy.sync:
|
||||
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
|
||||
else:
|
||||
if self.entry_cursor.connection.strategy.thread_safe:
|
||||
_, result, response, request = result
|
||||
else:
|
||||
response = self.entry_cursor.connection.response
|
||||
result = self.entry_cursor.connection.result
|
||||
request = self.entry_cursor.connection.request
|
||||
self.entry_cursor._store_operation_in_history(request, result, response)
|
||||
if result['result'] == RESULT_SUCCESS:
|
||||
self._state.dn = rdn + ',' + ','.join(to_dn(self.entry_dn)[1:])
|
||||
if refresh:
|
||||
if self.entry_refresh():
|
||||
if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # refresh dn of origin
|
||||
self._state.origin._state.dn = self.entry_dn
|
||||
self._state.set_status(STATUS_COMMITTED)
|
||||
self._state._to = None
|
||||
return True
|
||||
return False
|
||||
elif self.entry_status in [STATUS_VIRTUAL, STATUS_MANDATORY_MISSING]:
|
||||
missing_attributes = []
|
||||
for attr in self.entry_mandatory_attributes:
|
||||
if (attr not in self._state.attributes or self._state.attributes[attr].virtual) and attr not in self._changes:
|
||||
missing_attributes.append('\'' + attr + '\'')
|
||||
error_message = 'mandatory attributes %s missing in entry %s' % (', '.join(missing_attributes), self.entry_dn)
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
elif self.entry_status == STATUS_PENDING_CHANGES:
|
||||
if self._changes:
|
||||
if self.entry_definition._auxiliary_class: # checks if an attribute is from an auxiliary class and adds it to the objectClass attribute if not present
|
||||
for attr in self._changes:
|
||||
# checks schema to see if attribute is defined in one of the already present object classes
|
||||
attr_classes = self.entry_cursor.schema.attribute_types[attr].mandatory_in + self.entry_cursor.schema.attribute_types[attr].optional_in
|
||||
for object_class in self.objectclass:
|
||||
if object_class in attr_classes:
|
||||
break
|
||||
else: # executed only if the attribute class is not present in the objectClass attribute
|
||||
# checks if attribute is defined in one of the possible auxiliary classes
|
||||
for aux_class in self.entry_definition._auxiliary_class:
|
||||
if aux_class in attr_classes:
|
||||
if self._state._initial_status == STATUS_VIRTUAL: # entry is new, there must be a pending objectClass MODIFY_REPLACE
|
||||
self._changes['objectClass'][0][1].append(aux_class)
|
||||
else:
|
||||
self.objectclass += aux_class
|
||||
if self._state._initial_status == STATUS_VIRTUAL:
|
||||
new_attributes = dict()
|
||||
for attr in self._changes:
|
||||
new_attributes[attr] = self._changes[attr][0][1]
|
||||
result = self.entry_cursor.connection.add(self.entry_dn, None, new_attributes, controls)
|
||||
else:
|
||||
result = self.entry_cursor.connection.modify(self.entry_dn, self._changes, controls)
|
||||
|
||||
if not self.entry_cursor.connection.strategy.sync: # asynchronous request
|
||||
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
|
||||
else:
|
||||
if self.entry_cursor.connection.strategy.thread_safe:
|
||||
_, result, response, request = result
|
||||
else:
|
||||
response = self.entry_cursor.connection.response
|
||||
result = self.entry_cursor.connection.result
|
||||
request = self.entry_cursor.connection.request
|
||||
self.entry_cursor._store_operation_in_history(request, result, response)
|
||||
|
||||
if result['result'] == RESULT_SUCCESS:
|
||||
if refresh:
|
||||
if self.entry_refresh():
|
||||
if self._state.origin and self.entry_cursor.connection.server == self._state.origin.entry_cursor.connection.server: # updates original read-only entry if present
|
||||
for attr in self: # adds AttrDefs from writable entry to origin entry definition if some is missing
|
||||
if attr.key in self.entry_definition._attributes and attr.key not in self._state.origin.entry_definition._attributes:
|
||||
self._state.origin.entry_cursor.definition.add_attribute(self.entry_cursor.definition._attributes[attr.key]) # adds AttrDef from writable entry to original entry if missing
|
||||
temp_entry = self._state.origin.entry_cursor._create_entry(self._state.response)
|
||||
self._state.origin.__dict__.clear()
|
||||
self._state.origin.__dict__['_state'] = temp_entry._state
|
||||
for attr in self: # returns the whole attribute object
|
||||
if not hasattr(attr,'virtual'):
|
||||
self._state.origin.__dict__[attr.key] = self._state.origin._state.attributes[attr.key]
|
||||
self._state.origin._state.read_time = self.entry_read_time
|
||||
else:
|
||||
self.entry_discard_changes() # if not refreshed remove committed changes
|
||||
self._state.set_status(STATUS_COMMITTED)
|
||||
return True
|
||||
return False
|
||||
|
||||
def entry_discard_changes(self):
|
||||
self._changes.clear()
|
||||
self._state.set_status(self._state._initial_status)
|
||||
|
||||
def entry_delete(self):
|
||||
if self.entry_status not in [STATUS_WRITABLE, STATUS_COMMITTED, STATUS_READY_FOR_DELETION]:
|
||||
error_message = 'cannot delete entry, invalid status: ' + self.entry_status
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
self._state.set_status(STATUS_READY_FOR_DELETION)
|
||||
|
||||
def entry_refresh(self, tries=4, seconds=2):
|
||||
"""
|
||||
|
||||
Refreshes the entry from the LDAP Server
|
||||
"""
|
||||
if self.entry_cursor.connection:
|
||||
if self.entry_cursor.refresh_entry(self, tries, seconds):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def entry_move(self, destination_dn):
|
||||
if self.entry_status not in [STATUS_WRITABLE, STATUS_COMMITTED, STATUS_READY_FOR_MOVING]:
|
||||
error_message = 'cannot move entry, invalid status: ' + self.entry_status
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
self._state._to = safe_dn(destination_dn)
|
||||
self._state.set_status(STATUS_READY_FOR_MOVING)
|
||||
|
||||
def entry_rename(self, new_name):
|
||||
if self.entry_status not in [STATUS_WRITABLE, STATUS_COMMITTED, STATUS_READY_FOR_RENAMING]:
|
||||
error_message = 'cannot rename entry, invalid status: ' + self.entry_status
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPCursorError(error_message)
|
||||
self._state._to = new_name
|
||||
self._state.set_status(STATUS_READY_FOR_RENAMING)
|
||||
|
||||
@property
|
||||
def entry_changes(self):
|
||||
return self._changes
|
||||
270
venv/Lib/site-packages/ldap3/abstract/objectDef.py
Normal file
270
venv/Lib/site-packages/ldap3/abstract/objectDef.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
"""
|
||||
|
||||
# Created on 2014.02.02
|
||||
#
|
||||
# Author: Giovanni Cannata
|
||||
#
|
||||
# Copyright 2014 - 2020 Giovanni Cannata
|
||||
#
|
||||
# This file is part of ldap3.
|
||||
#
|
||||
# ldap3 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# ldap3 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with ldap3 in the COPYING and COPYING.LESSER files.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from os import linesep
|
||||
|
||||
from .attrDef import AttrDef
|
||||
from ..core.exceptions import LDAPKeyError, LDAPObjectError, LDAPAttributeError, LDAPSchemaError
|
||||
from .. import STRING_TYPES, SEQUENCE_TYPES, Server, Connection
|
||||
from ..protocol.rfc4512 import SchemaInfo, constant_to_class_kind
|
||||
from ..protocol.formatters.standard import find_attribute_validator
|
||||
from ..utils.ciDict import CaseInsensitiveWithAliasDict
|
||||
from ..utils.config import get_config_parameter
|
||||
from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED
|
||||
|
||||
|
||||
class ObjectDef(object):
|
||||
"""Represent an object in the LDAP server. AttrDefs are stored in a dictionary; the key is the friendly name defined in AttrDef.
|
||||
|
||||
AttrDefs can be added and removed using the += and -= operators
|
||||
|
||||
ObjectDef can be accessed either as a sequence and a dictionary. When accessed the whole AttrDef instance is returned
|
||||
|
||||
"""
|
||||
def __init__(self, object_class=None, schema=None, custom_validator=None, auxiliary_class=None):
|
||||
if object_class is None:
|
||||
object_class = []
|
||||
|
||||
if not isinstance(object_class, SEQUENCE_TYPES):
|
||||
object_class = [object_class]
|
||||
|
||||
if auxiliary_class is None:
|
||||
auxiliary_class = []
|
||||
|
||||
if not isinstance(auxiliary_class, SEQUENCE_TYPES):
|
||||
auxiliary_class = [auxiliary_class]
|
||||
|
||||
self.__dict__['_attributes'] = CaseInsensitiveWithAliasDict()
|
||||
self.__dict__['_custom_validator'] = custom_validator
|
||||
self.__dict__['_oid_info'] = []
|
||||
|
||||
if isinstance(schema, Connection) and (schema._deferred_bind or schema._deferred_open): # probably a lazy connection, tries to bind
|
||||
schema._fire_deferred()
|
||||
|
||||
if schema is not None:
|
||||
if isinstance(schema, Server):
|
||||
schema = schema.schema
|
||||
elif isinstance(schema, Connection):
|
||||
schema = schema.server.schema
|
||||
elif isinstance(schema, SchemaInfo):
|
||||
pass
|
||||
elif schema:
|
||||
error_message = 'unable to read schema'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPSchemaError(error_message)
|
||||
if schema is None:
|
||||
error_message = 'schema not present'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPSchemaError(error_message)
|
||||
self.__dict__['_schema'] = schema
|
||||
|
||||
if self._schema:
|
||||
object_class = [schema.object_classes[name].name[0] for name in object_class] # uses object class names capitalized as in schema
|
||||
auxiliary_class = [schema.object_classes[name].name[0] for name in auxiliary_class]
|
||||
for object_name in object_class:
|
||||
if object_name:
|
||||
self._populate_attr_defs(object_name)
|
||||
|
||||
for object_name in auxiliary_class:
|
||||
if object_name:
|
||||
self._populate_attr_defs(object_name)
|
||||
|
||||
self.__dict__['_object_class'] = object_class
|
||||
self.__dict__['_auxiliary_class'] = auxiliary_class
|
||||
|
||||
if log_enabled(BASIC):
|
||||
log(BASIC, 'instantiated ObjectDef: <%r>', self)
|
||||
|
||||
def _populate_attr_defs(self, object_name):
|
||||
if object_name in self._schema.object_classes:
|
||||
object_schema = self._schema.object_classes[object_name]
|
||||
self.__dict__['_oid_info'].append(object_name + " (" + constant_to_class_kind(object_schema.kind) + ") " + str(object_schema.oid))
|
||||
|
||||
if object_schema.superior:
|
||||
for sup in object_schema.superior:
|
||||
self._populate_attr_defs(sup)
|
||||
for attribute_name in object_schema.must_contain:
|
||||
self.add_from_schema(attribute_name, True)
|
||||
for attribute_name in object_schema.may_contain:
|
||||
if attribute_name not in self._attributes: # the attribute could already be defined as "mandatory" in a superclass
|
||||
self.add_from_schema(attribute_name, False)
|
||||
else:
|
||||
error_message = 'object class \'%s\' not defined in schema' % object_name
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPObjectError(error_message)
|
||||
|
||||
def __repr__(self):
|
||||
if self._object_class:
|
||||
r = 'OBJ : ' + ', '.join(self._object_class) + linesep
|
||||
else:
|
||||
r = 'OBJ : <None>' + linesep
|
||||
if self._auxiliary_class:
|
||||
r += 'AUX : ' + ', '.join(self._auxiliary_class) + linesep
|
||||
else:
|
||||
r += 'AUX : <None>' + linesep
|
||||
r += 'OID: ' + ', '.join([oid for oid in self._oid_info]) + linesep
|
||||
r += 'MUST: ' + ', '.join(sorted([attr for attr in self._attributes if self._attributes[attr].mandatory])) + linesep
|
||||
r += 'MAY : ' + ', '.join(sorted([attr for attr in self._attributes if not self._attributes[attr].mandatory])) + linesep
|
||||
|
||||
return r
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.__getattr__(item)
|
||||
|
||||
def __getattr__(self, item):
|
||||
item = ''.join(item.split()).lower()
|
||||
if '_attributes' in self.__dict__:
|
||||
try:
|
||||
return self._attributes[item]
|
||||
except KeyError:
|
||||
error_message = 'key \'%s\' not present' % item
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPKeyError(error_message)
|
||||
else:
|
||||
error_message = 'internal _attributes property not defined'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPKeyError(error_message)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
error_message = 'object \'%s\' is read only' % key
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPObjectError(error_message)
|
||||
|
||||
def __iadd__(self, other):
|
||||
self.add_attribute(other)
|
||||
return self
|
||||
|
||||
def __isub__(self, other):
|
||||
if isinstance(other, AttrDef):
|
||||
self.remove_attribute(other.key)
|
||||
elif isinstance(other, STRING_TYPES):
|
||||
self.remove_attribute(other)
|
||||
|
||||
return self
|
||||
|
||||
def __iter__(self):
|
||||
for attribute in self._attributes:
|
||||
yield self._attributes[attribute]
|
||||
|
||||
def __len__(self):
|
||||
return len(self._attributes)
|
||||
|
||||
if str is not bytes: # Python 3
|
||||
def __bool__(self): # needed to make the objectDef appears as existing in "if cursor:" even if there are no entries
|
||||
return True
|
||||
else: # Python 2
|
||||
def __nonzero__(self):
|
||||
return True
|
||||
|
||||
def __contains__(self, item):
|
||||
try:
|
||||
self.__getitem__(item)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def add_from_schema(self, attribute_name, mandatory=False):
|
||||
attr_def = AttrDef(attribute_name)
|
||||
attr_def.validate = find_attribute_validator(self._schema, attribute_name, self._custom_validator)
|
||||
attr_def.mandatory = mandatory # in schema mandatory is specified in the object class, not in the attribute class
|
||||
if self._schema and self._schema.attribute_types and attribute_name in self._schema.attribute_types:
|
||||
attr_def.single_value = self._schema.attribute_types[attribute_name].single_value
|
||||
attr_def.oid_info = self._schema.attribute_types[attribute_name]
|
||||
self.add_attribute(attr_def)
|
||||
|
||||
def add_attribute(self, definition=None):
|
||||
"""Add an AttrDef to the ObjectDef. Can be called with the += operator.
|
||||
:param definition: the AttrDef object to add, can also be a string containing the name of attribute to add. Can be a list of both
|
||||
|
||||
"""
|
||||
conf_attributes_excluded_from_object_def = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF')]
|
||||
if isinstance(definition, STRING_TYPES):
|
||||
self.add_from_schema(definition)
|
||||
elif isinstance(definition, AttrDef):
|
||||
if definition.key.lower() not in conf_attributes_excluded_from_object_def:
|
||||
if definition.key not in self._attributes:
|
||||
self._attributes[definition.key] = definition
|
||||
if definition.name and definition.name != definition.key:
|
||||
self._attributes.set_alias(definition.key, definition.name)
|
||||
other_names = [name for name in definition.oid_info.name if definition.key.lower() != name.lower()] if definition.oid_info else None
|
||||
if other_names:
|
||||
self._attributes.set_alias(definition.key, other_names)
|
||||
|
||||
if not definition.validate:
|
||||
validator = find_attribute_validator(self._schema, definition.key, self._custom_validator)
|
||||
self._attributes[definition.key].validate = validator
|
||||
elif isinstance(definition, SEQUENCE_TYPES):
|
||||
for element in definition:
|
||||
self.add_attribute(element)
|
||||
else:
|
||||
error_message = 'unable to add element to object definition'
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPObjectError(error_message)
|
||||
|
||||
def remove_attribute(self, item):
|
||||
"""Remove an AttrDef from the ObjectDef. Can be called with the -= operator.
|
||||
:param item: the AttrDef to remove, can also be a string containing the name of attribute to remove
|
||||
|
||||
"""
|
||||
key = None
|
||||
if isinstance(item, STRING_TYPES):
|
||||
key = ''.join(item.split()).lower()
|
||||
elif isinstance(item, AttrDef):
|
||||
key = item.key.lower()
|
||||
|
||||
if key:
|
||||
for attr in self._attributes:
|
||||
if key == attr.lower():
|
||||
del self._attributes[attr]
|
||||
break
|
||||
else:
|
||||
error_message = 'key \'%s\' not present' % key
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPKeyError(error_message)
|
||||
else:
|
||||
error_message = 'key type must be str or AttrDef not ' + str(type(item))
|
||||
if log_enabled(ERROR):
|
||||
log(ERROR, '%s for <%s>', error_message, self)
|
||||
raise LDAPAttributeError(error_message)
|
||||
|
||||
def clear_attributes(self):
|
||||
"""Empty the ObjectDef attribute list
|
||||
|
||||
"""
|
||||
self.__dict__['object_class'] = None
|
||||
self.__dict__['auxiliary_class'] = None
|
||||
self.__dict__['_attributes'] = dict()
|
||||
Reference in New Issue
Block a user