management: add comands to import user and roles from W.C.S.

fixes #3103
This commit is contained in:
Benjamin Dauvergne 2013-07-23 14:11:16 +02:00
parent 977c855147
commit 5c7b3ede3f
5 changed files with 342 additions and 0 deletions

91
README
View File

@ -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=<provider_name>
The name of the SAML 2.0 service provider in the portail citoyen configuratio.
This option is mandatory.
--mapping=<wcs_field_name>:<user_field_name>
Map a user field identifier from W.C.S. to a local user model field. This
option is mandatory.
--extra=<package_path>
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>
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

View File

View File

@ -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 = '<wcs_data_path wcs_data_path ...>'
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()

View File

@ -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 = '<wcs_data_path wcs_data_path ...>'
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()