diff --git a/inside/authentication.py b/inside/authentication.py index 2126cea..f6c3ddb 100644 --- a/inside/authentication.py +++ b/inside/authentication.py @@ -46,14 +46,11 @@ class InsideBackend(object): return self.user def _update_user_details(self): - sync_attrs = { - 'firstname': 'first_name', - 'lastname': 'last_name', - 'email': 'email' - } - for inside_attr, local_attr in sync_attrs.iteritems(): + sync_attrs = ['first_name', 'last_name', 'email'] + + for attr in sync_attrs: # Copy from Inside user to local Django user - setattr(self.user, local_attr, getattr(self.inside_user, inside_attr)) + setattr(self.user, attr, getattr(self.inside_user, attr)) def _update_groups(self): existing_groups = list(self.user.groups.all()) diff --git a/inside/models.py b/inside/models.py index 728b967..efc9fd7 100644 --- a/inside/models.py +++ b/inside/models.py @@ -338,8 +338,8 @@ class InsideUser(models.Model): cardno = models.IntegerField(unique=True, blank=True, null=True) username = models.CharField(unique=True, max_length=12) password = models.CharField(max_length=50) - firstname = models.CharField(max_length=120) - lastname = models.CharField(max_length=120) + first_name = models.CharField(max_length=120, db_column='firstname') + last_name = models.CharField(max_length=120, db_column='lastname') addresstype = models.CharField(max_length=3) valid_address = models.IntegerField() email = models.CharField(unique=True, max_length=120) diff --git a/neuf_kerberos/utils.py b/neuf_kerberos/utils.py index f66eaba..135beec 100644 --- a/neuf_kerberos/utils.py +++ b/neuf_kerberos/utils.py @@ -30,6 +30,10 @@ def get_kerberos_principal(username): return cleaned_output +def has_kerberos_principal(username): + return get_kerberos_principal(username) is not None + + def set_kerberos_password(username, raw_password): principal = "{}@{}".format(username, settings.KERBEROS_REALM) kadmin_query = " -p {} -w {} -q 'change_password -pw {} {}'".format( diff --git a/neuf_ldap/utils.py b/neuf_ldap/utils.py index 2f62dde..f0ce35f 100644 --- a/neuf_ldap/utils.py +++ b/neuf_ldap/utils.py @@ -1,5 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals +import pprint from django.conf import settings import logging import os @@ -25,6 +26,7 @@ def set_ldap_password(username, raw_password): user = LdapUser.objects.get(username=username) user.set_password(raw_password) except LdapUser.DoesNotExist: + # Ignore pass @@ -40,7 +42,7 @@ def ldap_username_exists(username): return len(LdapUser.objects.filter(username=username)) != 0 -def ldap_create_user(user): +def ldap_create_user(user, dry_run=False): from neuf_ldap.models import LdapUser, LdapGroup def _get_next_uid(): @@ -70,10 +72,10 @@ def ldap_create_user(user): return user_groups[0] + 1 # User - full_name = '{} {}'.format(user['firstname'], user['lastname']) + full_name = '{} {}'.format(user['first_name'], user['last_name']) user_data = { - 'first_name': user['firstname'], - 'last_name': user['lastname'], + 'first_name': user['first_name'], + 'last_name': user['last_name'], 'full_name': full_name, 'display_name': full_name, 'gecos': full_name, @@ -82,26 +84,76 @@ def ldap_create_user(user): 'id': _get_next_uid(), 'group': _get_next_user_gid(), 'home_directory': os.path.join(settings.LDAP_HOME_DIRECTORY_PREFIX, user['username']) + # TODO you are here: AttributeError: 'NoneType' object has no attribute 'startswith' + # $ python manage.py sync_active_to_ldap --dry-run -v3 } ldap_user = LdapUser(**user_data) - ldap_user.set_password(user['password'], commit=False) - ldap_user.save() + + # User password + pwd_type = None + if user.get('password') is not None: + ldap_user.set_password(user['password'], commit=False) # Raw + pwd_type = 'raw' + elif user.get('ldap_password') is not None: + ldap_user.password = user['ldap_password'] # Hashed + pwd_type = 'hashed' + else: + # No password + logger.exception("User {} has no ldap_password (hashed) or password (raw)!".format(user['username'])) + + if dry_run: + logger.debug('User saved with data: {} and password type \'{}\'.'.format( + pprint.pformat(user_data), + pwd_type)) + else: + ldap_user.save() # Add user group ldap_user_group = LdapGroup(name=user['username'], gid=user_data['group'], members=[user['username']]) - ldap_user_group.save() + if not dry_run: + ldap_user_group.save() + else: + logger.debug('User group {} created'.format(user['username'])) # Add groups ldap_groups = LdapGroup.objects.filter(name__in=user['groups']) for g in ldap_groups: if user['username'] not in g.members: g.members.append(user['username']) - g.save() + if not dry_run: + g.save() + else: + logger.debug('User {} added to group {}'.format(user['username'], g.name)) # Finito! return True +def ldap_update_user_details(inside_user, dry_run=False): + from neuf_ldap.models import LdapUser + diff_attributes = ['username', 'email', 'groups'] + ldap_user = LdapUser.objects.get(username=inside_user['username']) + + for attr in diff_attributes: + setattr(ldap_user, attr, inside_user[attr]) + + name_changed = inside_user['first_name'] != ldap_user.first_name or inside_user['last_name'] != ldap_user.last_name + if name_changed: + full_name = '{} {}'.format(inside_user['first_name'], inside_user['last_name']) + name_data = { + 'first_name': inside_user['first_name'], + 'last_name': inside_user['last_name'], + 'full_name': full_name, + 'display_name': full_name, + 'gecos': full_name, + } + for key, value in name_data.iteritems(): + setattr(ldap_user, key, value) + + if not dry_run: + ldap_user.save() + + def ldap_create_automount(username): from neuf_ldap.models import LdapAutomountHome automount = LdapAutomountHome(username=username) diff --git a/neuf_userinfo/forms.py b/neuf_userinfo/forms.py index 955f568..af36fca 100644 --- a/neuf_userinfo/forms.py +++ b/neuf_userinfo/forms.py @@ -15,12 +15,11 @@ from django.utils.translation import ugettext as _ from inside.models import InsideUser from inside.utils import set_inside_password -from neuf_kerberos.utils import set_kerberos_password -from neuf_ldap.models import LdapGroup, LdapUser +from neuf_kerberos.utils import set_kerberos_password, has_kerberos_principal from neuf_radius.utils import set_radius_password from neuf_ldap.utils import set_ldap_password, ldap_username_exists, ldap_user_group_exists from neuf_userinfo.utils import decrypt_rijndael -from neuf_userinfo.validators import PasswordValidator, UsernameValidator +from neuf_userinfo.validators import UsernameValidator, validate_password class NeufSetPasswordForm(SetPasswordForm): @@ -42,17 +41,19 @@ class NeufSetPasswordForm(SetPasswordForm): set_inside_password(username, password) # Active services - set_kerberos_password(username, password) - set_radius_password(username, password) + set_radius_password(username, password) # creates user if non-existant set_ldap_password(username, password) + if has_kerberos_principal(username): + set_kerberos_password(username, password) + return self.user # Local Django User def clean_new_password1(self): raw_password = self.cleaned_data.get('new_password1') MinLengthValidator(8)(raw_password) - PasswordValidator(raw_password) + validate_password(raw_password) return raw_password @@ -144,10 +145,18 @@ class NewUserForm(forms.Form): def clean_password(self): raw_password = self.cleaned_data['password'] password = decrypt_rijndael(settings.INSIDE_USERSYNC_ENC_KEY, raw_password) - PasswordValidator(password) + MinLengthValidator(8)(raw_password) + validate_password(password) return password def clean_groups(self): groups = self.cleaned_data['groups'] return groups.strip().split(',') + + def clean(self): + cleaned_data = super(NewUserForm, self).clean() + # Rename firstname to first_name and lastname to last_name + cleaned_data['first_name'] = cleaned_data.pop('firstname') + cleaned_data['last_name'] = cleaned_data.pop('lastname') + return cleaned_data \ No newline at end of file diff --git a/neuf_userinfo/management/commands/sync_active_to_ldap.py b/neuf_userinfo/management/commands/sync_active_to_ldap.py index f4282b7..7fbf6c4 100644 --- a/neuf_userinfo/management/commands/sync_active_to_ldap.py +++ b/neuf_userinfo/management/commands/sync_active_to_ldap.py @@ -3,69 +3,51 @@ from django.core.management.base import BaseCommand from inside.models import InsideGroup, InsideUser from neuf_ldap.models import LdapGroup, LdapUser -from neuf_ldap.utils import ldap_create_user +from neuf_ldap.utils import ldap_create_user, ldap_update_user_details + +from optparse import make_option +from neuf_userinfo.utils import add_new_user + +ACTIVE_USERS_GROUP = 'dns-aktiv' class Command(BaseCommand): help = 'Syncs users in group dns-aktiv from Inside to LDAP' + option_list = BaseCommand.option_list + ( + make_option( + '--dry-run', + action='store_true', + dest='dry_run', + default=False, + help='Dry run of syncronization, nothing is saved.' + ),) + DIFF_ATTRIBUTES = ['username', 'first_name', 'last_name', 'email', 'groups'] COUNTS = dict(create=0, update=0, in_sync=0) + options = {} def handle(self, *args, **options): + self.options = options + # Get all active user from Inside inside_users_diffable = self.get_inside_users_diffable() - if options['verbosity'] == '3': - self.stdout.write('Found {} Inside users'.format(len(inside_users_diffable))) # Get existing users in LDAP ldap_users_diffable = self.get_ldap_users_diffable() - if options['verbosity'] == '3': - self.stdout.write('Found {} LDAP users'.format(len(ldap_users_diffable))) - self.sync_users(inside_users_diffable, ldap_users_diffable, options) + # Do actual sync + self.sync_users(inside_users_diffable, ldap_users_diffable) - self.obsolete_users(inside_users_diffable, ldap_users_diffable, options) + # Are any LDAP users stale? + self.stale_ldap_users(inside_users_diffable, ldap_users_diffable) - self.log_totals(options) - # Voila + self.log_totals() - def sync_users(self, inside_users_diffable, ldap_users_diffable, options): - for username, u in inside_users_diffable.iteritems(): - # Diff by username - if username not in ldap_users_diffable: - # TODO create - # ldap_create_user(u) - self.COUNTS['create'] += 1 - if int(options['verbosity']) >= 2: - self.stdout.write('Inside user {} is not in LDAP'.format(username)) - elif not self.user_details_in_sync(u, ldap_users_diffable[username]): - # TODO update - # ldap_update_user(u) - self.COUNTS['update'] += 1 - if int(options['verbosity']) >= 2: - self.stdout.write('Inside user {} is out of sync with LDAP'.format(username)) - else: - self.COUNTS['in_sync'] += 1 - if int(options['verbosity']) == 3: - self.stdout.write('Inside user {} is in sync with LDAP'.format(username)) - - def obsolete_users(self, inside_users_diffable, ldap_users_diffable, options): - for username, u in ldap_users_diffable.iteritems(): - if username not in inside_users_diffable: - if int(options['verbosity']) >= 2: - self.stdout.write('LDAP user {} is not in Inside'.format(username)) - - def log_totals(self, options): - if self.COUNTS['create'] != 0: - self.stdout.write('{} Inside users not found in LDAP'.format(self.COUNTS['create'])) - if self.COUNTS['update'] != 0: - self.stdout.write('{} LDAP users were were update with info '.format(self.COUNTS['update'])) - if int(options['verbosity']) >= 2: - self.stdout.write('{} Inside users in sync with LDAP users '.format(self.COUNTS['in_sync'])) + # Voila! def get_inside_users_diffable(self): inside_users_diffable = {} - group = InsideGroup.objects.get(posix_group='dns-aktiv') + group = InsideGroup.objects.get(posix_group=ACTIVE_USERS_GROUP) inside_users = InsideUser.objects.filter(group_rels__group=group) for u in inside_users: @@ -73,17 +55,20 @@ class Command(BaseCommand): inside_groups = inside_groups.values_list('posix_group', flat=True) inside_users_diffable[u.ldap_username] = { 'username': u.ldap_username, - 'first_name': u.firstname, - 'last_name': u.lastname, - 'password': u.ldap_password, + 'first_name': u.first_name, + 'last_name': u.last_name, + 'ldap_password': u.ldap_password, 'email': u.email, 'groups': inside_groups } + if self.options['verbosity'] == '3': + self.stdout.write('Found {} Inside users'.format(len(inside_users_diffable))) + return inside_users_diffable def get_ldap_users_diffable(self): ldap_users_diffable = {} - ldap_active_members = LdapGroup.objects.get(name="dns-aktiv").members + ldap_active_members = LdapGroup.objects.get(name=ACTIVE_USERS_GROUP).members ldap_users = LdapUser.objects.filter(username__in=ldap_active_members) for u in ldap_users: @@ -95,8 +80,47 @@ class Command(BaseCommand): 'email': u.email, 'groups': ldap_groups } + if self.options['verbosity'] == '3': + self.stdout.write('Found {} LDAP users'.format(len(ldap_users_diffable))) + return ldap_users_diffable + def sync_users(self, inside_users_diffable, ldap_users_diffable): + for username, u in inside_users_diffable.iteritems(): + if username not in ldap_users_diffable: + # Create + add_new_user(u, dry_run=self.options['dry_run']) + + self.COUNTS['create'] += 1 + if int(self.options['verbosity']) >= 2: + self.stdout.write('Inside user {} is not in LDAP'.format(username)) + elif not self.user_details_in_sync(u, ldap_users_diffable[username]): + # Update + ldap_update_user_details(u, dry_run=self.options['dry_run']) + + self.COUNTS['update'] += 1 + if int(self.options['verbosity']) >= 2: + self.stdout.write('Inside user {} is out of sync with LDAP'.format(username)) + else: + # In sync :-) + self.COUNTS['in_sync'] += 1 + if int(self.options['verbosity']) == 3: + self.stdout.write('Inside user {} is in sync with LDAP'.format(username)) + + def stale_ldap_users(self, inside_users_diffable, ldap_users_diffable): + for username, u in ldap_users_diffable.iteritems(): + if username not in inside_users_diffable: + if int(self.options['verbosity']) >= 2: + self.stdout.write('LDAP user {} is not in Inside'.format(username)) + + def log_totals(self): + if self.COUNTS['create'] != 0: + self.stdout.write('{} Inside users not found in LDAP'.format(self.COUNTS['create'])) + if self.COUNTS['update'] != 0: + self.stdout.write('{} LDAP users were were update with info '.format(self.COUNTS['update'])) + if int(self.options['verbosity']) >= 2: + self.stdout.write('{} Inside users in sync with LDAP users '.format(self.COUNTS['in_sync'])) + def user_details_in_sync(self, inside_user, ldap_user): for attr in self.DIFF_ATTRIBUTES: if inside_user[attr] != ldap_user[attr]: diff --git a/neuf_userinfo/ssh.py b/neuf_userinfo/ssh.py index 1f491f2..518aa4c 100644 --- a/neuf_userinfo/ssh.py +++ b/neuf_userinfo/ssh.py @@ -7,7 +7,7 @@ import os logger = logging.getLogger(__name__) -def create_home_dir(username): +def create_home_dir(username, dry_run=False): """ Create homedir for username - Copy /etc/skel @@ -34,8 +34,18 @@ def create_home_dir(username): logger.error("Path '{}:{}' already exists, doing nothing.".format(host, path)) return False - run('cp -r /etc/skel {}'.format(path)) - run('chown -R {0}:{0} {1}'.format(username, path)) - run('chmod -R 700 {}'.format(path)) + cp_skel_cmd = 'cp -r /etc/skel {}'.format(path) + chown_cmd = 'chown -R {0}:{0} {1}'.format(username, path) + chmod_cmd = 'chmod -R 700 {}'.format(path) + if not dry_run: + run(cp_skel_cmd) + run(chown_cmd) + run(chmod_cmd) + else: + logger.debug('Creating homedir on {} with cmds: {} {} {}'.format( + host_string, + cp_skel_cmd, + chown_cmd, + chmod_cmd)) return True diff --git a/neuf_userinfo/utils.py b/neuf_userinfo/utils.py index fa07394..8895782 100644 --- a/neuf_userinfo/utils.py +++ b/neuf_userinfo/utils.py @@ -1,11 +1,38 @@ # coding: utf-8 from __future__ import unicode_literals import base64 + +from neuf_ldap.utils import ldap_create_user +from neuf_radius.utils import radius_create_user +from neuf_userinfo.ssh import create_home_dir import rijndael import logging logger = logging.getLogger(__name__) + +def add_new_user(user, dry_run=False): + """ + - Create new user in LDAP (username, first_name, last_name, email) + - Add to groups (usergroup, dns-alle), it not exists, create + - Set LDAP password + - Create homedir on wii + - Set RADIUS password + - (Create automount entry) + - (Create kerberos principal and set password) + """ + results = { + 'user': user, + 'ldap_user': ldap_create_user(user, dry_run=dry_run), + 'homedir': create_home_dir(user['username'], dry_run=dry_run), + # 'ldap_automount': ldap_create_automount(user['username']), # FIXME disabled + # 'kerberos_principal': kerberos_create_principal(user['username'], user['password']), # FIXME disabled + } + if user.get('password'): + results['radius'] = radius_create_user(user['username'], user['password']) + + logger.debug(results) + """ Rijndael stuff, thank you SO! Ref: http://stackoverflow.com/questions/8217269/decrypting-strings-in-python-that-were-encrypted-with-mcrypt-rijndael-256-in-php#answers-header @@ -48,4 +75,4 @@ def decrypt_rijndael(key, encoded): padded_text += r.decrypt(ciphertext[start:start+BLOCK_SIZE]) plaintext = padded_text.split('\x00', 1)[0] - return plaintext \ No newline at end of file + return plaintext diff --git a/neuf_userinfo/validators.py b/neuf_userinfo/validators.py index 6fab7f2..a07dbeb 100644 --- a/neuf_userinfo/validators.py +++ b/neuf_userinfo/validators.py @@ -1,9 +1,14 @@ +from django.core.exceptions import ValidationError from django.core.validators import RegexValidator import re -# Passwords should not contain " -password_re = re.compile(r'(?