Mercurial > hg > savane-forge
view src/savane/backend/auth_ldif_export.py @ 119:a34e97e27050
LDIF export: more efficient (direct MySQL access) and cleaner (queries in the models)
author | Sylvain Beucler <beuc@beuc.net> |
---|---|
date | Sun, 02 Aug 2009 23:38:26 +0200 |
parents | cd5e4c45265b |
children | 8d4b08714c90 |
line wrap: on
line source
# Replicate users and groups to an OpenLDAP directory # Copyright (C) 2009 Sylvain Beucler # # This file is part of Savane. # # Savane is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # Savane 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Recommended indexes: # index uid,uidNumber,gidNumber,memberUid,shadowExpire eq # TODO: most settings are hard-coded and need to be made configurable # - base: dc=savannah,dc=gnu,dc=org # - users ou: "users" # - groups ou: "groups" # - create 'organization' and 'organizationalUnit' objects? # - min uid: 1000 # - min gid: 1000 # - default group: cn=svusers / gid=1000 # - loginShell: /usr/local/bin/sv_membersh import sys import codecs import base64, binascii from django.db import connection, models import savane.svmain.models as svmain_models # Convert stdout to UTF-8 - if the stdout is redirected to a file # sys.stdout.encoding is autodetected as 'None' and you get the # obnoxious UnicodeEncodeError python error. sys.stdout = codecs.getwriter('UTF-8')(sys.stdout) print """dn: dc=savannah,dc=gnu,dc=org objectClass: top objectClass: dcObject objectClass: organization o: GNU dc: savannah structuralObjectClass: organization dn: ou=users,dc=savannah,dc=gnu,dc=org ou: users objectClass: organizationalUnit objectClass: top structuralObjectClass: organizationalUnit dn: ou=groups,dc=savannah,dc=gnu,dc=org ou: groups objectClass: organizationalUnit objectClass: top structuralObjectClass: organizationalUnit """ # Add user admin/admin # (REMOVE WHEN TESTING IS DONE!) print """ dn: cn=admin,dc=savannah,dc=gnu,dc=org objectClass: simpleSecurityObject objectClass: organizationalRole cn: admin description: LDAP administrator userPassword:: e2NyeXB0fWt0YVZ1TFNDaEg0Wi4= structuralObjectClass: organizationalRole """ import MySQLdb, settings MySQLdb.charset = 'UTF-8' conn = MySQLdb.connect(user=settings.DATABASE_USER, passwd=settings.DATABASE_PASSWORD, db=settings.DATABASE_NAME, use_unicode=True) # Alternatively: #from django.db import connection #connection.cursor() # establish connection - well looks like it does #conn = connection.connection # MySQL-specific connection - now using mysqldb ## # Users ## max_uid = svmain_models.ExtendedUser.objects.all().aggregate(models.Max('uidNumber'))['uidNumber__max'] if max_uid < 1000: max_uid = 1000 users_with_group = {} group_users = {} svmain_models.Membership.query_active_memberships_raw(conn, ('group_id', 'username')) res = conn.store_result() for row in res.fetch_row(maxrows=0, how=1): users_with_group[row['username']] = 1 if group_users.has_key(row['group_id']): group_users[row['group_id']].append(row['username']) else: group_users[row['group_id']] = [row['username'],] user_saves = [] svmain_models.ExtendedUser.query_active_users_raw(conn, ('username', 'first_name', 'last_name', 'email', 'password', 'uidNumber', 'gidNumber')) res = conn.store_result() for row in res.fetch_row(maxrows=0): (username, first_name, last_name, email, password, uidNumber, gidNumber) = row #if uidNumber == 0: # either non-assigned, or mistakenly assigned to root if uidNumber < 1000: # either non-assigned, or mistakenly assigned to privileged user max_uid = max_uid + 1 user_saves.append((username, max_uid)) uidNumber = max_uid cleanup = [first_name, last_name, email] for i in range(0, len(cleanup)): cleanup[i] = cleanup[i].replace('\n', ' ') cleanup[i] = cleanup[i].replace('\r', ' ') cleanup[i] = cleanup[i].strip() (first_name, last_name, email) = cleanup ldap_password = '{CRYPT}!' # default = unusable password if users_with_group.has_key(username): if password.startswith('sha1$'): # Django-specific algorithm: it sums 5-char-salt+pass instead # of SSHA's pass+4-bytes-salt, so we can't store it in LDAP - # /me curses django devs pass elif password.startswith('md5$$'): # MD5 without salt algo, empty, hash_hex = password.split('$') if (len(hash_hex) == 32): # filter out empty or disabled passwords ldap_password = "{MD5}" + base64.b64encode(binascii.a2b_hex(hash_hex)) elif password.startswith('md5$'): # md5$salt$ vs. {SMD5} is similar to sha1$salt$ vs. {SSHA} - # cf. above pass elif password.startswith('crypt$'): # glibc crypt has improved algorithms, but where salt contains # three '$'s, which Django doesn't support (since '$' is # already the salt field separator). So this is only weak, # passwd-style (not shadow-style) crypt. algo, salt_hex, hash_hex = password.split('$') # salt_hex is 2-chars long and already prepended to hash_hex ldap_password = "{CRYPT}" + base64.b64encode(binascii.a2b_hex(hash_hex)) elif '$' not in password: # MD5 without salt, alternate Django syntax hash_hex = password if (len(hash_hex) == 32): # filter out empty or disabled passwords ldap_password = "{MD5}" + base64.b64encode(binascii.a2b_hex(hash_hex)) # Object classes: # - posixAccount: base class for libnss-ldap/pam-ldap support # - shadowAccount: for shadowExpire # - inetOrgPerson: for mail and givenName, and structural class print u""" dn: uidNumber=%(uidNumber)d,ou=users,dc=savannah,dc=gnu,dc=org uid: %(username)s cn:: %(full_name)s sn:: %(last_name)s mail: %(email)s userPassword: %(ldap_password)s uidNumber: %(uidNumber)d gidNumber: %(gidNumber)d homeDirectory: %(homedir)s loginShell: /usr/local/bin/sv_membersh objectClass: shadowAccount objectClass: posixAccount objectClass: inetOrgPerson objectClass: top structuralObjectClass: inetOrgPerson""" % { 'username' : username, 'full_name' : base64.b64encode((first_name + ' ' + last_name).encode('UTF-8')), 'last_name' : base64.b64encode((last_name or '-').encode('UTF-8')), 'email' : email, 'ldap_password' : ldap_password, 'uidNumber' : uidNumber, 'gidNumber' : 1000, 'homedir' : '/home/' + username[:1] + '/' + username[:2] + '/' + username, } # non-mandatory fields - slapadd doesn't accept empty fields apparently if len(first_name) > 0: print "givenName::" + base64.b64encode(first_name.encode('UTF-8')) # disallow login for users that are not part of any group if not users_with_group.has_key(username): # shadowExpire is a timestamp - avoid 0 as it may be # interpreted as 'no expiration' print "shadowExpire: 10" ## # Groups ## max_gid = svmain_models.ExtendedGroup.objects.all().aggregate(models.Max('gidNumber'))['gidNumber__max'] if max_gid < 1000: max_gid = 1000 # Create base 'svusers' group print u""" dn: cn=svusers,ou=groups,dc=savannah,dc=gnu,dc=org cn: svusers gidNumber: 1000 objectClass: posixGroup objectClass: top structuralObjectClass: posixGroup""" # Dump groups group_saves = [] svmain_models.ExtendedGroup.query_active_groups_raw(conn, ('group_id', 'name', 'gidNumber')) res = conn.store_result() #for group in svmain_models.ExtendedGroup.objects.only('name'): for row in res.fetch_row(maxrows=0): (group_id, name, gidNumber) = row if gidNumber < 1000: # either non-assigned, or mistakenly assigned to privileged user max_gid = max_gid + 1 group_saves.append((group_id, max_gid)) gidNumber = max_gid print u""" dn: cn=%(name)s,ou=groups,dc=savannah,dc=gnu,dc=org cn: %(name)s gidNumber: %(gidNumber)s objectClass: posixGroup objectClass: top structuralObjectClass: posixGroup""" % { 'name' : name, 'gidNumber' : gidNumber, } if group_users.has_key(group_id): for username in group_users[group_id]: print "memberUid: " + username # TODO # - user_saves # - group_saves # with multi-line UPDATEs