authentic/src/authentic2_provisionning_ldap/management/commands/provision.py

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()