From 5c7b3ede3ffa957835c75838e97f7f0e2849e419 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 23 Jul 2013 14:11:16 +0200 Subject: [PATCH] management: add comands to import user and roles from W.C.S. fixes #3103 --- README | 91 ++++++++++ portail_citoyen/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/load-wcs-roles.py | 81 +++++++++ .../management/commands/load-wcs-users.py | 170 ++++++++++++++++++ 5 files changed, 342 insertions(+) create mode 100644 portail_citoyen/management/__init__.py create mode 100644 portail_citoyen/management/commands/__init__.py create mode 100644 portail_citoyen/management/commands/load-wcs-roles.py create mode 100644 portail_citoyen/management/commands/load-wcs-users.py diff --git a/README b/README index 222a490..769155c 100644 --- a/README +++ b/README @@ -46,3 +46,94 @@ to launch start with $, other lines are expected output):: Quit the server with CONTROL-C. The application is now usable at http://localhost:8000/ + +Migrating users from W.C.S. to portail-citoyen +---------------------------------------------- + +You must use the management command named load-wcs-users. You must first link +your W.C.S. instance to the portail-citoyen as a new SAML 2.0 service provider. +The command accept the following options:: + + + --provider= + + The name of the SAML 2.0 service provider in the portail citoyen configuratio. + This option is mandatory. + + --mapping=: + + Map a user field identifier from W.C.S. to a local user model field. This + option is mandatory. + + --extra= + + The path to a W.C.S. extra package to load (AuQuotidien for example). + + --verbose + + Display information logged on the console + + --debug + + display debugging information on the console + + --fake + + Launch a dry run, i.e. do not save new user and new federation + + --help-mapping + + Display the list of field available in the local user model and in the + W.C.S. user model + + --allow-not-mapped + + Do not fail when a W.C.S. user field is not mapped to a local user field + + +Importing role from W.C.S. into portail-citoyen +----------------------------------------------- + +Your must first decide on a prefix for naming roles on the portail-citoyen +side, default is 'AuQuo::'. Then federated roles must be actived and the +prefix be configured in the `site-options.cfg` file of the W.C.S. host +directory like this:: + + [options] + saml2_use_role = true + saml2_role_prefix = MyPrefix:: + +Then you can use the `load-wcs-roles` command which accept the following options:: + + --url= + + URL of the W.C.S. instance from which we want to import roles (mandatory) + + --prefix + + The chosen federated role name prefix (mandatory) + + --orig + + The web-service identifier configured in `site-options.cfg` (mandatory) + + --key + + The web-service signature secret configured in `site-options.cfg` (mandatory) + + --email + + The email of the admin user used to access the fole list (mandatory) + + --deleted-orphaned-roles + + If actived delete federation roles starting with the given prefix which + does not exist on W.C.S. side. + + --verbose + + Display logs on console + + --debug + + Display logs on console and set log level to DEBUG diff --git a/portail_citoyen/management/__init__.py b/portail_citoyen/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/portail_citoyen/management/commands/__init__.py b/portail_citoyen/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/portail_citoyen/management/commands/load-wcs-roles.py b/portail_citoyen/management/commands/load-wcs-roles.py new file mode 100644 index 0000000..9c250a0 --- /dev/null +++ b/portail_citoyen/management/commands/load-wcs-roles.py @@ -0,0 +1,81 @@ +import random +import string +import locale +from optparse import make_option +import os.path +import logging +import urlparse +import re + +try: + from django.contrib.auth import get_user_model +except ImportError: + from django.contrib.auth.models import User + get_user_model = lambda: User +from django.core.management.base import BaseCommand, CommandError, NoArgsCommand +from django.db import transaction +from django.utils.http import urlencode +from django.contrib.auth.models import Group + +from authentic2.saml.models import LibertyProvider, LibertyFederation +from data_source_plugin.signature import sign_url + +import requests + +logger = logging.getLogger() + +class Command(BaseCommand): + args = '' + help = '''Load W.C.S. roles''' + + option_list = BaseCommand.option_list + ( + make_option('--url', action="store"), + make_option('--prefix', action="store", default='AuQuo::'), + make_option('--orig', action="store"), + make_option('--key', action="store"), + make_option('--email', action="store"), + make_option('--delete-orphaned-roles', action="store_true", + default=False), + make_option('--debug', action="store_true"), + make_option('--verbose', action="store_true"), + ) + + @transaction.commit_on_success + def handle(self, *args, **options): + locale.setlocale(locale.LC_ALL, '') + if options['verbose'] or options['debug']: + handler = logging.StreamHandler() + handler.setLevel(level=logging.DEBUG if options['debug'] else logging.INFO) + logger.addHandler(handler) + for key in ('url', 'prefix', 'orig', 'key', 'email'): + if not options.get(key): + raise CommandError('the --%s option must be provided' % key) + url = urlparse.urljoin(options['url'], '/roles') + url += '?' + urlencode({ 'format': 'json', 'orig': options['orig'], 'email': options['email']}) + logger.debug('signing using key %r', options['key']) + url = sign_url(url, options['key']) + logger.debug('sending GET to %s', url) + response = requests.get(url) + logger.debug('got response %r', response.content) + json_response = response.json() + if json_response.get('err') == 1: + raise CommandError('Web Service error: %s' % json_response['err_class']) + prefix = options['prefix'].decode('utf-8') + if not prefix: + raise CommandError('the prefix %s is invalid' % options['prefix']) + # clean ending + prefix = re.sub(':+$', '', prefix) + default_groups = [{'text': 'Admin'},{'text': 'BackOffice'}] + all_groups = set() + for role in json_response.get('data', [])+default_groups: + role_name = role['text'] + group_name = u'{0}::{1}'.format(prefix, role_name) + group, created = Group.objects.get_or_create(name=group_name) + all_groups.add(group.id) + if created: + logger.info('created role %s', group.name) + if options['delete_orphaned_roles']: + for group in Group.objects.exclude(id__in=all_groups) \ + .filter(name__startswith=prefix+'::'): + logger.info('deleted orphaned role %s', group.name) + group.delete() diff --git a/portail_citoyen/management/commands/load-wcs-users.py b/portail_citoyen/management/commands/load-wcs-users.py new file mode 100644 index 0000000..4ad5fda --- /dev/null +++ b/portail_citoyen/management/commands/load-wcs-users.py @@ -0,0 +1,170 @@ +import random +import string +import locale +from optparse import make_option +import os.path +import logging + +try: + from django.contrib.auth import get_user_model +except ImportError: + from django.contrib.auth.models import User + get_user_model = lambda: User +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from authentic2.saml.models import LibertyProvider, LibertyFederation + +import wcs +import publisher +import ConfigParser +from qommon.ident.password_accounts import PasswordAccount +from wcs.users import User as WcsUser +from wcs.formdata import get_dict_with_varnames +import lasso + +logger = logging.getLogger() + +class Command(BaseCommand): + args = '' + help = '''Load W.C.S. users''' + + option_list = BaseCommand.option_list + ( + make_option('--provider', action="store"), + make_option("--extra", action="append", default=[]), + make_option('--verbose', action="store_true"), + make_option('--fake', action="store_true"), + make_option('--debug', action="store_true"), + make_option('--allow-not-mapped', action="store_true"), + make_option('--help-mapping', action="store_true"), + make_option('--mapping', action="append", default=[]), + ) + + hashing_algo_mapping = { + 'sha': 'sha1', + } + + @transaction.commit_on_success + def handle(self, *args, **options): + locale.setlocale(locale.LC_ALL, '') + if options['verbose'] or options['debug']: + handler = logging.StreamHandler() + handler.setLevel(level=logging.DEBUG if options['debug'] else logging.INFO) + logger.addHandler(handler) + options['mapping'] = map(lambda s: s.split(':'), options['mapping']) + for wcs_data_path in args: + if not os.path.isdir(wcs_data_path): + raise CommandError('path %s does not exist', wcs_data_path) + self.handle_path(wcs_data_path, options) + + def generate_name_id(self): + # example: _9903A47299512211F49F9E7931183761 + alpha = string.ascii_uppercase + string.digits + name_id = ''.join(random.choice(alpha) for i in xrange(32)) + return '_%s' % name_id + + def handle_path(self, path, options): + logger.debug('==> %s' % path) + User = get_user_model() + publisher.WcsPublisher.configure(ConfigParser.RawConfigParser()) + for extra in options['extra']: + publisher.WcsPublisher.register_extra_dir(extra) + pub = publisher.WcsPublisher.create_publisher() + pub.app_dir = path + pub.set_config() + formdef = WcsUser.get_formdef() + wcs_user_fields = get_dict_with_varnames(formdef.fields, {}).keys() + user_fields = User._meta.get_all_field_names() + if options.get('help_mapping', False): + print 'List of W.C.S. user fields:' + for wcs_user_field in wcs_user_fields: + print '-', wcs_user_field + print 'List of portail-citoyen user fields:' + for user_field in user_fields: + print '-', user_field + return + for wcs_user_field, user_field in options.get('mapping', []): + if wcs_user_field not in wcs_user_fields: + raise CommandError('wcs user field %r unknown' % wcs_user_field) + if user_field not in user_fields: + raise CommandError('idp user field %r unknown' % user_field) + not_mapped = False + mapping_dict = dict(options.get('mapping', ())) + for wcs_user_field in wcs_user_fields: + if wcs_user_field not in mapping_dict: + print 'W.C.S. user field %r not mapped' % wcs_user_field) + not_mapped = True + if not_mapped and not options['allow_not_mapped']: + raise CommandError('Some W.C.S. user fields are not mapped ! Aborting.') + try: + provider = LibertyProvider.objects.get(name=options['provider']) + except LibertyProvider.DoesNotExist: + raise CommandError('provider %s does not exist' % options['provider']) + to_save = [] + to_store = [] + for password_account in PasswordAccount.values(): + new_user = None + new_federation = None + wcs_user = password_account.get_user() + if not wcs_user: + logger.info('no wcs user for password account %s' % password_account) + continue + try: + new_federation = LibertyFederation.objects.get( + sp_id=provider.entity_id, + name_id_content__in=getattr(wcs_user, 'name_identifiers', []) or []) + new_user = new_federation.user + logger.info('wcs account %r already linked to idp account %r, updating' % (password_account.id, new_federation.user.username)) + except LibertyFederation.DoesNotExist: + pass + if User.objects.filter(username=password_account.id).exists(): + if options['verbose']: + logger.info('wcs account %r cannot be linked as homonym account %r:%s exists' % (password_account.id, + federation.user.username, federation.user.id)) + continue + algo = password_account.hashing_algo + algo = self.hashing_algo_mapping.get(algo, algo) + if algo: + new_password = '%s$$%s' % (algo, password_account.password) + if not new_user: + if User.objects.filter(username=password_account.id).exists(): + logger.info('wcs account %r already exists in db' % (password_account.id,)) + continue + new_user = User(username=password_account.id) + wcs_user_data = get_dict_with_varnames(formdef.fields, wcs_user.form_data) + for wcs_user_field, user_field in options['mapping']: + value = wcs_user_data.get(wcs_user_field) + if not value: + continue + logger.info('setting %s to %s' % (user_field, value)) + setattr(new_user, user_field, str(value).decode('utf-8')) + if algo: + new_user.password = new_password + else: + new_user.set_password(password_account.password) + new_user.save() + name_id = self.generate_name_id() + if not new_federation: + new_federation = LibertyFederation.objects.create(user=new_user, + sp_id=provider.entity_id, + idp_id='', + name_id_qualifier=name_id, + name_id_sp_name_qualifier=provider.entity_id, + name_id_format=lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, + name_id_content=name_id, + name_id_sp_provided_id='') + wcs_user.name_identifiers.append(name_id) + logger.info('created new link %s %s' % (password_account.id, wcs_user.name_identifiers)) + to_store.append(wcs_user) + if options['fake']: + raise CommandError('Fake...') + for user in to_store: + user.store() + + + + + + + +