185 lines
7.5 KiB
Python
185 lines
7.5 KiB
Python
from __future__ import print_function
|
|
|
|
try:
|
|
import ldap
|
|
from ldap.dn import str2dn, dn2str
|
|
from ldap.filter import filter_format
|
|
except ImportError:
|
|
ldap = None
|
|
from ldaptools import paged
|
|
|
|
from django.core.management.base import BaseCommand
|
|
|
|
from authentic2.attributes_ng.engine import get_attributes
|
|
from authentic2 import compat, utils
|
|
|
|
from authentic2_provisionning_ldap import app_settings
|
|
|
|
|
|
ADD = 1
|
|
REPLACE = 2
|
|
DELETE = 3
|
|
|
|
|
|
|
|
class Command(BaseCommand):
|
|
can_import_django_settings = True
|
|
output_transaction = True
|
|
requires_system_checks = True
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument('target_resource', nargs='*')
|
|
parser.add_argument(
|
|
'--fake', action='store_true', default=False, help='Do nothing, just simulate'
|
|
)
|
|
parser.add_argument(
|
|
'--batch-size', action='store', type='int', default=200, help='Batch size'
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
ressources = app_settings.RESSOURCES
|
|
if options['target_resource']:
|
|
ressources = [ressource for ressource in ressources
|
|
if ressource.get('name') in options['target_resource']]
|
|
for ressource in ressources:
|
|
self.sync_ressource(ressource, **options)
|
|
|
|
def sync_ressource(self, ressource, **options):
|
|
self.sync_ldap_ressource(ressource, **options)
|
|
|
|
def add_values(self, ldap_attributes, ldap_attribute, values):
|
|
if not isinstance(values, (list, tuple)):
|
|
values = [values]
|
|
ldap_values = ldap_attributes.setdefault(ldap_attribute, [])
|
|
for value in values:
|
|
if isinstance(value, unicode):
|
|
value = value.encode('utf-8')
|
|
elif isinstance(value, str):
|
|
pass # must be well encoded
|
|
else:
|
|
raise NotImplementedError('value %r not supported' % value)
|
|
ldap_values.append(value)
|
|
|
|
def build_dn_and_filter(self, ressource, ldap_attributes):
|
|
'''Build the target record dn'''
|
|
base_dn = ressource['base_dn']
|
|
rdn_attributes = ressource['rdn_attributes']
|
|
dn = str2dn(base_dn)
|
|
rdn = []
|
|
for ldap_attribute in rdn_attributes:
|
|
values = ldap_attributes.get(ldap_attribute, [])
|
|
assert len(values) == 1, 'RDN attribute must have exactly one value %r %r' % \
|
|
(rdn_attributes, ldap_attributes)
|
|
rdn.append((ldap_attribute, values[0], 1))
|
|
dn = [rdn] + dn
|
|
return dn2str(dn), ('&', [(a,b) for a, b, c in rdn])
|
|
|
|
def format_filter(self, filters):
|
|
if isinstance(filters, basestring):
|
|
return filters
|
|
assert len(filters) == 2, 'filters %r' % (filters,)
|
|
if isinstance(filters[1], (list, tuple)):
|
|
return '(%s%s)' % (filters[0], ''.join(self.format_filter(x) for x in filters[1]))
|
|
else:
|
|
return filter_format('(%s=%%s)' % filters[0], (filters[1],))
|
|
|
|
def sync_ldap_ressource(self, ressource, **options):
|
|
verbosity = int(options['verbosity'])
|
|
fake = options['fake']
|
|
# FIXME: Check ressource well formedness
|
|
conn = paged.PagedLDAPObject(ressource['url'], retry_max=10,
|
|
retry_delay=2)
|
|
base_dn = ressource['base_dn']
|
|
use_tls = ressource.get('use_tls')
|
|
bind_dn = ressource.get('bind_dn')
|
|
bind_pw = ressource.get('bind_pw')
|
|
if use_tls:
|
|
conn.start_tls_s()
|
|
if bind_dn:
|
|
conn.simple_bind_s(bind_dn, bind_pw)
|
|
attribute_mapping = utils.lower_keys(ressource['attribute_mapping'])
|
|
static_attributes = utils.lower_keys(ressource.get('static_attributes', {}))
|
|
format_mapping = utils.lower_keys(ressource.get('format_mapping', {}))
|
|
attributes = set(attribute_mapping.keys()) | set(static_attributes.keys())
|
|
default_ctx = ressource.get('attribute_context', {})
|
|
ldap_filter = ressource.get('ldap_filter', '(objectclass=*)')
|
|
delete = ressource.get('delete', True)
|
|
User = compat.get_user_model()
|
|
qs = User.objects.filter(**ressource.get('a2_filter', {}))
|
|
todelete = set()
|
|
user_dns = set()
|
|
for batch in utils.batch(qs, options['batch_size']):
|
|
ldap_users = {}
|
|
filters = []
|
|
for user in batch:
|
|
ctx = default_ctx.copy()
|
|
ctx['user'] = user
|
|
ctx = get_attributes(ctx)
|
|
ldap_attributes = {}
|
|
for ldap_attribute, a2_attributes in attribute_mapping.iteritems():
|
|
if not isinstance(a2_attributes, (tuple, list)):
|
|
a2_attributes = [a2_attributes]
|
|
for a2_attribute in a2_attributes:
|
|
self.add_values(ldap_attributes, ldap_attribute, ctx.get(a2_attribute))
|
|
for ldap_attribute, values in static_attributes.iteritems():
|
|
self.add_values(ldap_attributes, ldap_attribute, values)
|
|
for ldap_attribute, fmt_tpls in format_mapping.iteritems():
|
|
for fmt_tpl in fmt_tpls:
|
|
self.add_values(ldap_attributes, ldap_attribute,
|
|
[fmt_tpl.format(**ctx)])
|
|
dn, filt = self.build_dn_and_filter(ressource, ldap_attributes)
|
|
user_dns.add(dn)
|
|
ldap_users[dn] = ldap_attributes
|
|
filters.append(filt)
|
|
batch_filter = ldap_filter
|
|
if filters:
|
|
batch_filter = self.format_filter(('&', (batch_filter, ('|',
|
|
filters))))
|
|
existing_dn = set()
|
|
for dn, entry in conn.paged_search_ext_s(base_dn,
|
|
ldap.SCOPE_SUBTREE,
|
|
batch_filter, list(attributes)):
|
|
entry = utils.to_dict_of_set(utils.lower_keys(entry))
|
|
if dn not in ldap_users:
|
|
todelete.add(dn)
|
|
continue
|
|
if entry == utils.to_dict_of_set(ldap_users[dn]):
|
|
# no need to update, entry is already ok
|
|
del ldap_users[dn]
|
|
continue
|
|
existing_dn.add(dn)
|
|
for dn, ldap_attributes in ldap_users.iteritems():
|
|
if dn in existing_dn:
|
|
modlist = []
|
|
for key, values in ldap_attributes:
|
|
modlist.append((ldap.MOD_REPLACE, key, values))
|
|
if not fake:
|
|
conn.modify(dn, modlist)
|
|
if verbosity > 1:
|
|
print('- Replace %s values for %s' % (dn, ', '.join(ldap_attributes.keys())))
|
|
else:
|
|
if not fake:
|
|
conn.add(dn, ldap.modlist.addModlist(ldap_attributes))
|
|
if verbosity > 1:
|
|
print('- Add %s with values for %s' % (dn, ', '.join(ldap_attributes.keys())))
|
|
# wait for results
|
|
if not fake:
|
|
for x in ldap_users:
|
|
conn.result()
|
|
for dn, entry in conn.paged_search_ext_s(base_dn,
|
|
ldap.SCOPE_SUBTREE, ldap_filter):
|
|
# ignore the basedn
|
|
if dn == base_dn:
|
|
continue
|
|
if dn not in user_dns and dn not in todelete:
|
|
if not fake:
|
|
todelete.add(dn)
|
|
if delete:
|
|
if verbosity > 1:
|
|
print('- Deleting:', ', '.join(todelete))
|
|
if not fake:
|
|
for dn in todelete:
|
|
conn.delete(dn)
|
|
for dn in todelete:
|
|
conn.result()
|