""" """ # Created on 2016.08.19 # # Author: Giovanni Cannata # # Copyright 2016 - 2018 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 . import json try: from collections import OrderedDict except ImportError: from ..utils.ordDict import OrderedDict # for Python 2.6 from os import linesep 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 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 '') + linesep r += 'attributes: ' + ', '.join(sorted(self.attributes.keys())) + linesep r += 'object def: ' + (', '.join(sorted(self.definition._object_class)) if self.definition._object_class else '') + linesep r += 'attr defs: ' + ', '.join(sorted(self.definition._attributes.keys())) + linesep r += 'response: ' + ('present' if self.response else '') + linesep r += 'cursor: ' + (self.cursor.__class__.__name__ if self.cursor else '') + linesep return r else: return object.__repr__(self) def __str__(self): return self.__repr__() 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 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 _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.__dict__['_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 '') + 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 self.__dict__['_state'] 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 LDAPCursorError(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 LDAPCursorError(error_message) def __setattr__(self, item, value): if 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 LDAPCursorError(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 LDAPCursorError(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.entry_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.entry_raw_attributes[name] if name in self._state.entry_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, 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 _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): 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 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: 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) object_def = writable_entry.entry_definition object_def -= old_attr_def object_def += 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 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] 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 LDAPCursorError(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 LDAPCursorError(error_message) else: error_message = 'attribute name must be a string' if log_enabled(ERROR): log(ERROR, '%s for <%s>', error_message, self) raise LDAPCursorError(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: 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: 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: 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: 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 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