management: add comands to import user and roles from W.C.S.
fixes #3103
This commit is contained in:
parent
977c855147
commit
5c7b3ede3f
91
README
91
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=<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
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue