Skip to content
Snippets Groups Projects
Commit e982a855 authored by schulmax's avatar schulmax
Browse files

[TASK] Introduces container mapping for the ldap loader

You should take a look at the documentation, because it is
too much for the commit message...
parent f0384baf
No related branches found
No related tags found
No related merge requests found
...@@ -316,20 +316,56 @@ class SqlAlchemyLoader(AbstractLoader): ...@@ -316,20 +316,56 @@ class SqlAlchemyLoader(AbstractLoader):
class LdapLoader(AbstractLoader): class LdapLoader(AbstractLoader):
'''A loader for LDAP data sources using the ldap and ldap.modlist modules. '''A loader for LDAP data sources using the ldap and ldap.modlist modules.
.. function:: __init__(rdn, base, objectClass[, **kwargs]) The LDAP loader supports container mapping in the following form:
- condition_handling is a Flag which can either be LOG or DEFAULT
- on DEFAULT there will be a default container mapping created from the rdn and base give in init
- on LOG there will be NO default container mapping created which can result in records that could be loaded, because no container mapping could be applied(!), but you will get warnings in the log
- move is a list of actions that should move rather than do their actual task
- if "update" is in the list, the update action will move an entry AFTER the actual update of the entry
- if "delete" is in the list, the delete action will move an entry INSTED OF deleting it
- You can define as many container mapping as you want
- container mappings consist of operation, condition, target and an optional rdn
- the container mapping "operation" is a list of operations for which this containerm mapping should be applied
- the container mapping "condition" is python code, that will be thrown into eval (you can ONLY use UNMAPPED property names in the condition!)
- the container mapping "target" is the base, where the record with the matching condition should be written or moved to
- the container mapping "rdn" is the rdn of the record, that will be the first part of the dn
- examples:
- condition:
- data['fooUnmapped'] == 'bar'
- True
- 'bar' in data['fooUnmapped']
- target + rdn:
- target: ou=foo,dc=de rdn: sn dn: sn=valueOfsnInRecord,ou=foo,dc=de
.. function:: __init__(rdn, objectClass[, base = None[, container_mapping = [][, condition_handling = 'LOG'[, move = [][, **kwargs]]]]])
Constructor Constructor
:param rdn: The attribute used to create a DN for new records. :param rdn: The attribute used to create a DN for new records.
:param base: The container in which new records will be created created.
:param objectClass: The objectClass for new records - use this instead of providing it inside your entity. :param objectClass: The objectClass for new records - use this instead of providing it inside your entity.
:param base: The container in which new records will be created created.
:param container_mapping: The container mapping to be applied (priorised!!!)
:param condition_handling: Flag to determine, whether there should be a default mapping or not.
:param move: Determines, whether a record should be moved on update or delete, if a container mapping matches and WOULD move the record
:param **kwargs: Accepts parameters from :class:`.AbstractExtractor`. :param **kwargs: Accepts parameters from :class:`.AbstractExtractor`.
:type rdn: string :type rdn: string
:type base: string
:type objectClass: string :type objectClass: string
:type base: string
:type container_mapping: list
:type condition_handling: string
:type move: list
:type connector: :class:`hshetl.connectors.LdapConnector` :type connector: :class:`hshetl.connectors.LdapConnector`
YAML definition sample: YAML definition samples:
Deprecated:
.. code-block:: yaml .. code-block:: yaml
...@@ -339,12 +375,35 @@ class LdapLoader(AbstractLoader): ...@@ -339,12 +375,35 @@ class LdapLoader(AbstractLoader):
base: ou=rz,o=fh-hannover base: ou=rz,o=fh-hannover
objectClass: person objectClass: person
New:
.. code-block:: yaml
!ldaploader
connector: edirectory
rdn: cn
base: ou=rz,o=fh-hannover (optional if you have a condition mapping with the condition default)
objectClass: person
condition_handling: LOG (LOG or DEFAULT to create a default container mapping from the base)
move: ['update', 'delete']
container_mapping:
-
operation: ['insert', 'update', 'delete']
condition: data['l'] == 'Foo'
target: ou=foo,o=fh-hannover
rdn: sn (optional)
-
operation: ['insert']
condition: True
target: ou=new,o=fh-hannover
''' '''
yaml_tag = u'!ldaploader' yaml_tag = u'!ldaploader'
'''Use this tag inside your YAML configuration, to define this loader.''' '''Use this tag inside your YAML configuration, to define this loader.'''
def __init__(self, rdn, base, objectClass, **kwargs): #TODO: remove base once conversion to transformers is complete
def __init__(self, rdn, objectClass, base = None, container_mapping = [], condition_handling = 'LOG', move = [], **kwargs):
super(LdapLoader, self).__init__(**kwargs) super(LdapLoader, self).__init__(**kwargs)
self.rdn = rdn self.rdn = rdn
'''The attribute that is used to create new DNs.''' '''The attribute that is used to create new DNs.'''
...@@ -352,6 +411,14 @@ class LdapLoader(AbstractLoader): ...@@ -352,6 +411,14 @@ class LdapLoader(AbstractLoader):
'''The container new records will be inserted in.''' '''The container new records will be inserted in.'''
self.objectClass = objectClass self.objectClass = objectClass
'''The objectClass of new inserted records.''' '''The objectClass of new inserted records.'''
self.container_mapping = container_mapping
if None in (self.rdn, self.objectClass) or (self.base == None and self.container_mapping[0]['target'] == None):
raise ConfigurationException('base, rdn or objectClass not found - needs to be configured in the connector or loader!')
if condition_handling == 'DEFAULT' and {'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': self.rdn, 'target': self.base} not in container_mapping:
self.container_mapping.append({'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': self.rdn, 'target': self.base})
'''The container mappings for this loader'''
self.move = move
'''Move records or not'''
def can_execute(self, connector): def can_execute(self, connector):
'''Defines which connector can be handled by this extractor.''' '''Defines which connector can be handled by this extractor.'''
...@@ -360,8 +427,6 @@ class LdapLoader(AbstractLoader): ...@@ -360,8 +427,6 @@ class LdapLoader(AbstractLoader):
def _execute(self, result): def _execute(self, result):
'''Executes the loading of data. Distinguishes between update, insert and delete.''' '''Executes the loading of data. Distinguishes between update, insert and delete.'''
logging.debug('Loads data: ' + self.__class__.__name__) logging.debug('Loads data: ' + self.__class__.__name__)
if None in (self.rdn, self.base, self.objectClass):
raise ConfigurationException('base, rdn or objectClass not found - needs to be configured in the connector or loader!')
with self.connector as connection: with self.connector as connection:
for action in self.operations: for action in self.operations:
getattr(self, '_' + action)(connection, getattr(result, action)) getattr(self, '_' + action)(connection, getattr(result, action))
...@@ -378,7 +443,9 @@ class LdapLoader(AbstractLoader): ...@@ -378,7 +443,9 @@ class LdapLoader(AbstractLoader):
old_entity_dict = self._prepare_data(record.to_dict(container = record.last_container.target_container)) old_entity_dict = self._prepare_data(record.to_dict(container = record.last_container.target_container))
dict_data = record.to_dict() dict_data = record.to_dict()
entity_dict = self._prepare_data(dict_data) entity_dict = self._prepare_data(dict_data)
dn = self._get_dn(record.get_container_identifier(), dict_data) dn = self._get_dn(dict_data, 'update')
if dn == None: continue
old_dn = record.get_container_identifier()['dn'] if record.get_container_identifier() is not None else self.rdn + '=' + str(data[self.rdn]) + ',' + self.base
attributes = modlist.modifyModlist(old_entity_dict, entity_dict) attributes = modlist.modifyModlist(old_entity_dict, entity_dict)
try: try:
if self.dry: if self.dry:
...@@ -386,6 +453,12 @@ class LdapLoader(AbstractLoader): ...@@ -386,6 +453,12 @@ class LdapLoader(AbstractLoader):
else: else:
connection.modify_s(dn, attributes) connection.modify_s(dn, attributes)
logging.info('Modified dn {} with {} '.format(dn, self._generate_modify_logging(attributes, old_entity_dict, entity_dict))) logging.info('Modified dn {} with {} '.format(dn, self._generate_modify_logging(attributes, old_entity_dict, entity_dict)))
if dn != old_dn:
if 'update' in self.move:
logging.info('Container %s will be moved to %s after update' % (old_dn, dn))
self._move(connection, old_dn, dn)
else:
logging.info('Move after update prohibited by config (use move: [\'update\'])')
except Exception, e: except Exception, e:
logging.warn('Update failed for dn \'' + dn + '\': ' + str(e)) logging.warn('Update failed for dn \'' + dn + '\': ' + str(e))
...@@ -393,7 +466,8 @@ class LdapLoader(AbstractLoader): ...@@ -393,7 +466,8 @@ class LdapLoader(AbstractLoader):
'''Inserts every dataset one after another.''' '''Inserts every dataset one after another.'''
for record in data: for record in data:
entity_dict = self._prepare_data(record.to_dict()) entity_dict = self._prepare_data(record.to_dict())
dn = self._get_dn(record.get_container_identifier(), record.to_dict()) dn = self._get_dn(record.to_dict(), 'insert')
if dn == None: continue
attributes = modlist.addModlist(entity_dict) attributes = modlist.addModlist(entity_dict)
try: try:
if self.dry: if self.dry:
...@@ -407,7 +481,15 @@ class LdapLoader(AbstractLoader): ...@@ -407,7 +481,15 @@ class LdapLoader(AbstractLoader):
def _delete(self, connection, data): def _delete(self, connection, data):
'''Deletes every entry one after another.''' '''Deletes every entry one after another.'''
for record in data: for record in data:
dn = self._get_dn(record.get_container_identifier(), record.to_dict()) dn = self._get_dn(record.to_dict(record.last_container.target_container), 'delete')
if dn == None: continue
old_dn = record.get_container_identifier()['dn'] if record.get_container_identifier() is not None else self.rdn + '=' + str(data[self.rdn]) + ',' + self.base
if dn != old_dn:
if 'delete' in self.move:
logging.info('Container %s will be moved to %s instead of being deleted' % (old_dn, dn))
self._move(connection, old_dn, dn)
else:
logging.info('Move after delete prohibited by config (use move: [\'delete\'])')
try: try:
if self.dry: if self.dry:
logging.info('Would delete dn {}'.format(dn)) logging.info('Would delete dn {}'.format(dn))
...@@ -417,13 +499,38 @@ class LdapLoader(AbstractLoader): ...@@ -417,13 +499,38 @@ class LdapLoader(AbstractLoader):
except Exception, e: except Exception, e:
logging.warn('Delete failed for dn \'' + dn + '\': ' + str(e)) logging.warn('Delete failed for dn \'' + dn + '\': ' + str(e))
def _get_dn(self, identifier, data): def _move(self, connection, old_dn, new_dn):
if isinstance(identifier, dict) and 'dn' in identifier and len(identifier) == 1: '''Moves a single record'''
return identifier['dn'] try:
elif identifier == None: if self.dry:
return self.rdn + '=' + str(data[self.rdn]) + ',' + self.base logging.info('Would move dn %s to %s' % (old_dn, new_dn))
else:
rdn = new_dn.split(',')[0]
newsuperior = new_dn[len(rdn)+1:]
connection.rename_s(old_dn, rdn, newsuperior)
logging.info('Moved dn %s to %s' % (old_dn, new_dn))
except Exception, e:
logging.warn('Move failed for old dn %s to new dn %s' % (old_dn, new_dn))
def _get_dn(self, data, operation):
'''Resolves the dn for the given record'''
possible_dns = []
for cmapping in self.container_mapping:
if not cmapping.has_key('rdn'):
cmapping['rdn'] = self.rdn
try:
if operation in cmapping['operation']:
if eval(str(cmapping['condition'])):
possible_dns.append(cmapping['rdn'] + '=' + str(data[cmapping['rdn']]) + ',' + cmapping['target'])
except KeyError, e:
import pdb; pdb.set_trace()
raise LoaderException('The key given in condition does not exist in record.(%s)' % str(e))
if len(possible_dns) == 0:
logging.warn('No container mapping condition matched for record %s' % str(data))
return None
else: else:
raise LoaderException('The identifiers for LDAP must include the dn or nothing but not anything else.') if len(possible_dns) >= 2: logging.warn('Found multiple matching container mapping conditions. Using the first one that matched')
return possible_dns[0]
def _prepare_data(self, data): def _prepare_data(self, data):
'''Prepares dn and data-dictionary for _update, _insert and _delete''' '''Prepares dn and data-dictionary for _update, _insert and _delete'''
......
...@@ -34,4 +34,8 @@ fixtures = { ...@@ -34,4 +34,8 @@ fixtures = {
[{'sn' : 'ipsum', 'cn' : 'erat', 'mail' : 'Suspendisse.aliquet.sem@aliquetmolestie.com', 'telephoneNumber' : '2456'}, [{'sn' : 'ipsum', 'cn' : 'erat', 'mail' : 'Suspendisse.aliquet.sem@aliquetmolestie.com', 'telephoneNumber' : '2456'},
{'sn' : 'velit', 'cn' : 'sagittis', 'mail' : 'Vivamus.rhoncus.Donec@molestiedapibus.co.uk', 'telephoneNumber' : '1234'}, {'sn' : 'velit', 'cn' : 'sagittis', 'mail' : 'Vivamus.rhoncus.Donec@molestiedapibus.co.uk', 'telephoneNumber' : '1234'},
{'sn' : 'velitt', 'cn' : 'foo', 'mail' : 'varius.et.euismod@ipsum.ca', 'telephoneNumber' : '5678'}], {'sn' : 'velitt', 'cn' : 'foo', 'mail' : 'varius.et.euismod@ipsum.ca', 'telephoneNumber' : '5678'}],
'ldap_loader_container_mapping':
[{'operation': ['update'], 'condition': "'123' in data['telephoneNumber']", 'rdn': 'cn', 'target': 'foo=existent'},
{'operation': ['delete'], 'condition': "data['telephoneNumber'] == '5678'", 'rdn': 'cn', 'target': 'foo=trash'},
{'operation': ['insert'], 'condition': "True", 'rdn': 'sn', 'target': 'foo=new'}],
} }
...@@ -212,7 +212,7 @@ class TestLdapLoader(unittest.TestCase): ...@@ -212,7 +212,7 @@ class TestLdapLoader(unittest.TestCase):
def setUp(self): def setUp(self):
self.ldap_connector = Mock(spec = connectors.LdapConnector) self.ldap_connector = Mock(spec = connectors.LdapConnector)
self.ldap_connector.name = 'FooConnector' self.ldap_connector.name = 'FooConnector'
self.ldap_loader = loaders.LdapLoader(rdn = 'cn', base = 'foo=bar', objectClass = 'l', connector = self.ldap_connector) self.ldap_loader = loaders.LdapLoader(rdn = 'cn', base = 'foo=bar', objectClass = 'l', connector = self.ldap_connector, condition_handling = 'DEFAULT')
self.ldap_connection = Mock(spec = ldap.ldapobject.SimpleLDAPObject) self.ldap_connection = Mock(spec = ldap.ldapobject.SimpleLDAPObject)
self.ldap_connector.connection = self.ldap_connection self.ldap_connector.connection = self.ldap_connection
self.ldap_connector.__enter__ = Mock(return_value = self.ldap_connector.connection) self.ldap_connector.__enter__ = Mock(return_value = self.ldap_connector.connection)
...@@ -234,6 +234,7 @@ class TestLdapLoader(unittest.TestCase): ...@@ -234,6 +234,7 @@ class TestLdapLoader(unittest.TestCase):
def test_update(self): def test_update(self):
c = 0 c = 0
self.ldap_loader.container_mapping = [{'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': 'sn', 'target': 'foo=bar'}]
for rec in self.result.update: for rec in self.result.update:
rec.get_container_identifier = Mock(return_value = {'dn': 'sn=' + test.fixtures['ldap_syntax_loader_test_loading_old_data'][c]['sn'] + ',foo=bar'}) rec.get_container_identifier = Mock(return_value = {'dn': 'sn=' + test.fixtures['ldap_syntax_loader_test_loading_old_data'][c]['sn'] + ',foo=bar'})
rec.to_dict = Mock(side_effect = [test.fixtures['ldap_syntax_loader_test_loading_old_data'][c], rec.to_dict = Mock(side_effect = [test.fixtures['ldap_syntax_loader_test_loading_old_data'][c],
...@@ -250,6 +251,7 @@ class TestLdapLoader(unittest.TestCase): ...@@ -250,6 +251,7 @@ class TestLdapLoader(unittest.TestCase):
def test_insert(self): def test_insert(self):
c = 0 c = 0
self.ldap_loader.container_mapping = [{'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': 'cn', 'target': 'foo=bar'}]
for rec in self.result.insert: for rec in self.result.insert:
rec.get_container_identifier = Mock(return_value = None) rec.get_container_identifier = Mock(return_value = None)
rec.to_dict = Mock(return_value = test.fixtures['ldap_syntax_loader_test_loading'][c]) rec.to_dict = Mock(return_value = test.fixtures['ldap_syntax_loader_test_loading'][c])
...@@ -265,6 +267,7 @@ class TestLdapLoader(unittest.TestCase): ...@@ -265,6 +267,7 @@ class TestLdapLoader(unittest.TestCase):
def test_delete(self): def test_delete(self):
c = 0 c = 0
self.ldap_loader.container_mapping = [{'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': 'sn', 'target': 'foo=bar'}]
for rec in self.result.delete: for rec in self.result.delete:
rec.get_container_identifier = Mock(return_value = {'dn': 'sn=' + test.fixtures['ldap_syntax_loader_test_loading_old_data'][c]['sn'] + ',foo=bar'}) rec.get_container_identifier = Mock(return_value = {'dn': 'sn=' + test.fixtures['ldap_syntax_loader_test_loading_old_data'][c]['sn'] + ',foo=bar'})
rec.to_dict = Mock(return_value = test.fixtures['ldap_syntax_loader_test_loading'][c]) rec.to_dict = Mock(return_value = test.fixtures['ldap_syntax_loader_test_loading'][c])
...@@ -279,18 +282,31 @@ class TestLdapLoader(unittest.TestCase): ...@@ -279,18 +282,31 @@ class TestLdapLoader(unittest.TestCase):
self.ldap_connection.delete_s.assert_has_calls(ldap_calls) self.ldap_connection.delete_s.assert_has_calls(ldap_calls)
def test_prepare_data_converts_everything_in_dataset_to_unicode(self): def test_prepare_data_converts_everything_in_dataset_to_unicode(self):
self.ldap_loader.rdn = 'foo' self.ldap_loader.container_mapping = [{'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': 'foo', 'target': 'foo=bar,poo=par'}]
self.ldap_loader.base = 'foo=bar,poo=par'
self.ldap_loader.objectClass = 'virtualPersonStuffThing' self.ldap_loader.objectClass = 'virtualPersonStuffThing'
entity_dict = self.ldap_loader._prepare_data({'key' : 'value', 'foo' : 1337, 'bar' : True})
dn = self.ldap_loader._get_dn({'dn': 'someDN'}, {'key' : 'value', 'foo' : 1337, 'bar' : True})
self.assertEqual(dn, 'someDN')
self.assertEqual(entity_dict, {'objectClass' : 'virtualPersonStuffThing', 'key' : 'value', 'foo' : '1337', 'bar' : 'True'})
entity_dict = self.ldap_loader._prepare_data({'key' : '!value', 'foo' : 42, 'bar' : False}) entity_dict = self.ldap_loader._prepare_data({'key' : '!value', 'foo' : 42, 'bar' : False})
dn = self.ldap_loader._get_dn({'dn': 'foo=42,foo=bar,poo=par'}, {'key' : '!value', 'foo' : 42, 'bar' : False}) dn = self.ldap_loader._get_dn({'key' : '!value', 'foo' : 42, 'bar' : False}, 'insert')
self.assertEqual(dn, 'foo=42,foo=bar,poo=par') self.assertEqual(dn, 'foo=42,foo=bar,poo=par')
self.assertEqual(entity_dict, {'objectClass' : 'virtualPersonStuffThing', 'key' : '!value', 'foo' : '42', 'bar' : 'False'}) self.assertEqual(entity_dict, {'objectClass' : 'virtualPersonStuffThing', 'key' : '!value', 'foo' : '42', 'bar' : 'False'})
def test_get_dn_resolves_container_mapping(self):
self.ldap_loader.container_mapping = test.fixtures['ldap_loader_container_mapping']
self.ldap_loader.container_mapping.append({'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': 'cn', 'target': 'foo=bar'})
records = test.fixtures['ldap_syntax_loader_test_loading']
dn = self.ldap_loader._get_dn(records[0], 'update')
self.assertEqual(dn, 'cn=erat,foo=existent')
dn = self.ldap_loader._get_dn(records[1], 'update')
self.assertEqual(dn, 'cn=sagittis,foo=existent')
dn = self.ldap_loader._get_dn(records[2], 'update')
self.assertEqual(dn, 'cn=nunc,foo=bar')
dn = self.ldap_loader._get_dn(records[2], 'delete')
self.assertEqual(dn, 'cn=nunc,foo=trash')
dn = self.ldap_loader._get_dn(records[1], 'insert')
self.assertEqual(dn, 'sn=velit,foo=new')
self.ldap_loader.container_mapping.remove({'operation': ['insert', 'update', 'delete'],'condition': True, 'rdn': 'cn', 'target': 'foo=bar'})
dn = self.ldap_loader._get_dn(records[2], 'update')
self.assertEqual(dn, None)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
...@@ -333,7 +333,7 @@ class UuidTransformer(AbstractTransformer): ...@@ -333,7 +333,7 @@ class UuidTransformer(AbstractTransformer):
if isinstance(self.source, str) and self.entity is not None: if isinstance(self.source, str) and self.entity is not None:
self.source = NameResolver(self.entity, 'source', self.source)() self.source = NameResolver(self.entity, 'source', self.source)()
else: else:
raise Exception("Source is of type string and entity is None, so the NameResolver cannot find a correspinding container.") raise Exception("Source is of type string and entity is None, so the NameResolver cannot find a corresponding container.")
if self.mapping is not {}: if self.mapping is not {}:
mapped_identifiers = [x if x not in self.mapping.keys() else self.mapping[x] for x in self.source.identifiers] mapped_identifiers = [x if x not in self.mapping.keys() else self.mapping[x] for x in self.source.identifiers]
self.result = Container(name = self.name, self.result = Container(name = self.name,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment