diff --git a/scripts/doublons-cd91/authentic_fusion_local.py b/scripts/doublons-cd91/authentic_fusion_local.py new file mode 100644 index 0000000..1c3fd90 --- /dev/null +++ b/scripts/doublons-cd91/authentic_fusion_local.py @@ -0,0 +1,107 @@ +import collections +import json +import logging +import sys + +from django.contrib.auth import get_user_model +from django.db import connection, transaction +from django.utils import timezone + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +fh = logging.FileHandler('authentic_fusion.log') +fh.setLevel(logging.DEBUG) + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) + +logger.addHandler(fh) +logger.addHandler(ch) + +User = get_user_model() + +domain_url = '' +if hasattr(connection, 'tenant') and hasattr(connection.tenant, 'domain_url'): + domain_url = 'https://%s' % connection.tenant.domain_url + + +logger.info('=== Starting fusion at %s ===', timezone.now().strftime('%Y-%m-%dT%H:%M:%S')) + + +agent_users = ( + User.objects.filter(email__icontains='@cd-essonne.fr', is_active=True) + .distinct() + .order_by('first_name') + .prefetch_related('saml_identifiers') +) + +agent_users_by_email = collections.defaultdict(list) +for user in agent_users: + agent_users_by_email[user.email.lower()].append(user) + + +def get_user_detail(user): + return f'{user.get_full_name()} {user.email} {user.uuid} {domain_url}{user.get_absolute_url()}' + + +users_to_keep = [] +for email, users in agent_users_by_email.items(): + if len(users) == 1: + continue + + if len(users) == 3: + logger.info('* SKIPPING USER %s (more than 2 duplicates)', get_user_detail(users[0])) + continue + + if users[0].saml_identifiers.all() and users[1].saml_identifiers.all(): + logger.info('* SKIPPING USER %s (duplicates are both saml accounts)', get_user_detail(users[0])) + continue + + if not users[0].saml_identifiers.all() and not users[1].saml_identifiers.all(): + logger.info('* SKIPPING USER %s (duplicates are both local accounts)', get_user_detail(users[0])) + continue + + roles = {} + for user in users: + for role in user.roles.all(): + roles[role.id] = role + + user_to_keep = [x for x in users if x.saml_identifiers.all()][0] + users_to_disable = [x for x in users if not x.saml_identifiers.all()] + + user_to_keep._roles_to_add = roles + user_to_keep._duplicated_users = users_to_disable + users_to_keep.append(user_to_keep) + + +def do_fusion(users): + disabled_users_uuid_by_user_uuid = collections.defaultdict(list) + for user in users: + logger.info('* Processing user %s', get_user_detail(user)) + + for role in sorted(user._roles_to_add.values(), key=lambda x: x.name.lower()): + logger.info('Adding role %s', role) + user.roles.add(role) + + for duplicated_user in user._duplicated_users: + logger.info('Disabling duplicate %s', get_user_detail(duplicated_user)) + disabled_users_uuid_by_user_uuid[user.uuid].append(duplicated_user.uuid) + duplicated_user.mark_as_inactive(reason='Désactivation automatique des doublons') + + result = json.dumps(disabled_users_uuid_by_user_uuid) + logger.info('Result %s', result) + + with open('authentic_fusion_result.json', 'w') as f: + f.write(result) + + +try: + with transaction.atomic(): + do_fusion(users_to_keep) + + if len(sys.argv) < 2 or sys.argv[1] != '--proceed=true': + raise ValueError + logger.info('=== Success ===') +except ValueError: + logger.info('=== Did nothing ===') diff --git a/scripts/doublons-cd91/test_authentic_fusion.py b/scripts/doublons-cd91/test_authentic_fusion.py index 5b4f86a..280b36d 100644 --- a/scripts/doublons-cd91/test_authentic_fusion.py +++ b/scripts/doublons-cd91/test_authentic_fusion.py @@ -159,3 +159,167 @@ def test_authentic_fusion(db, caplog): saml_user_recent_connection_no_roles_2.refresh_from_db() assert saml_user_recent_connection_no_roles_2.is_active is True assert set(saml_user_recent_connection_no_roles_2.roles.all()) == {role1, role2} + + +@pytest.mark.freeze_time('2022-04-19 14:00') +def test_authentic_fusion_local_account(db, caplog): + role1 = Role.objects.create(name='role1') + role2 = Role.objects.create(name='role2') + role3 = Role.objects.create(name='role3') + + # duplicated users, but not agents, should ignore + User.objects.create(first_name='Normal', last_name='User', email='normal.user@gmail.com') + User.objects.create(first_name='Normal', last_name='User', email='normal.user@gmail.com') + + # two local duplicates, should ignore + User.objects.create(first_name='Agent', last_name='No SAML', email='agent.no.saml@cd-essonne.fr') + User.objects.create(first_name='Agent', last_name='No SAML', email='agent.no.saml@cd-essonne.fr') + + # three local duplicates, should ignore + User.objects.create(first_name='Agent', last_name='3 dups', email='Agent3dups@cd-essonne.fr') + User.objects.create(first_name='Agent', last_name='3 dups', email='aGent3dups@cd-essonne.fr') + User.objects.create(first_name='Agent', last_name='3 dups', email='agEnt3dups@cd-essonne.fr') + + # agent with saml link, no duplicate, should ignore + issuer = Issuer.objects.create(entity_id='https://idp1.example.com/', slug='idp1') + saml_user = User.objects.create( + first_name='Agent', + last_name='No duplicate', + email='agent.no.dup@cd-essonne.fr', + ) + deactivated_saml_user = User.objects.create( + first_name='Agent', + last_name='No duplicate (deactivated)', + email='agent.no.dup@cd-essonne.fr', + is_active=False, + ) + UserSAMLIdentifier.objects.create(user=saml_user, issuer=issuer, name_id='anodup') + UserSAMLIdentifier.objects.create(user=deactivated_saml_user, issuer=issuer, name_id='adeact') + + # two saml duplicates, should ignore + saml_user_old_connection = User.objects.create( + id=42, + uuid='uuid:42/agent@cd-essonne.fr', + first_name='Agent', + last_name='Duplicated', + email='agent@cd-essonne.fr', + last_login=now() - datetime.timedelta(days=10), + ) + UserSAMLIdentifier.objects.create(user=saml_user_old_connection, issuer=issuer, name_id='aduplicated') + saml_user_old_connection_no_roles = User.objects.create( + id=43, + uuid='uuid:43/agent@cd-essonne.fr', + first_name='Agent', + last_name='Duplicated', + email='agent@cd-Essonne.fr', + last_login=now() - datetime.timedelta(days=5), + ) + UserSAMLIdentifier.objects.create( + user=saml_user_old_connection_no_roles, issuer=issuer, name_id='Aduplicated' + ) + + # local and saml duplicates, should process + saml_user = User.objects.create( + id=45, + uuid='uuid:45/agent2@cd-essonne.fr', + first_name='Agent', + last_name='Duplicated 2', + email='agent2@cd-essonne.fr', + ) + UserSAMLIdentifier.objects.create(user=saml_user, issuer=issuer, name_id='Aduplicated2') + + local_user = User.objects.create( + id=46, + uuid='uuid:46/agent2@cd-essonne.fr', + first_name='Agent', + last_name='Duplicated 2', + email='agent2@cd-essonne.fr', + ) + local_user.roles.add(role2) + + # again + local_user2 = User.objects.create( + id=47, + uuid='uuid:47/agent3@cd-essonne.fr', + first_name='Agent', + last_name='Duplicated 3', + email='agent3@cd-essonne.fr', + ) + local_user.roles.add(role1) + + saml_user2 = User.objects.create( + id=48, + uuid='uuid:48/agent3@cd-essonne.fr', + first_name='Agent', + last_name='Duplicated 3', + email='agent3@cd-essonne.fr', + ) + UserSAMLIdentifier.objects.create(user=saml_user2, issuer=issuer, name_id='Aduplicated3') + saml_user2.roles.add(role2) + + assert User.objects.count() == 15 + assert User.objects.filter(is_active=True).count() == 14 + + call_command('runscript', 'tests/authentic_fusion_local.py') + + log_messages = caplog.messages + assert log_messages == [ + '=== Starting fusion at 2022-04-19T14:00:00 ===', + '* SKIPPING USER Agent Duplicated agent@cd-essonne.fr ' + 'uuid:42/agent@cd-essonne.fr /manage/users/42/ (duplicates are both saml ' + 'accounts)', + '* SKIPPING USER Agent No SAML agent.no.saml@cd-essonne.fr ' + '06231a68f4bc483293faad5fb3105ddd /manage/users/544/ (duplicates are both ' + 'local accounts)', + '* SKIPPING USER Agent 3 dups Agent3dups@cd-essonne.fr ' + '653c422dd6d94654a0c1bd6cd708a93c /manage/users/546/ (more than 2 duplicates)', + '* Processing user Agent Duplicated 2 agent2@cd-essonne.fr ' + 'uuid:45/agent2@cd-essonne.fr /manage/users/45/', + 'Adding role role1', + 'Adding role role2', + 'Disabling duplicate Agent Duplicated 2 agent2@cd-essonne.fr ' + 'uuid:46/agent2@cd-essonne.fr /manage/users/46/', + '* Processing user Agent Duplicated 3 agent3@cd-essonne.fr ' + 'uuid:48/agent3@cd-essonne.fr /manage/users/48/', + 'Adding role role2', + 'Disabling duplicate Agent Duplicated 3 agent3@cd-essonne.fr ' + 'uuid:47/agent3@cd-essonne.fr /manage/users/47/', + 'Result {"uuid:45/agent2@cd-essonne.fr": ["uuid:46/agent2@cd-essonne.fr"], ' + '"uuid:48/agent3@cd-essonne.fr": ["uuid:47/agent3@cd-essonne.fr"]}', + '=== Did nothing ===', + ] + + # no changes in db + assert User.objects.count() == 15 + assert User.objects.filter(is_active=True).count() == 14 + assert saml_user_recent_connection_no_roles_2.roles.count() == 0 + + caplog.clear() + call_command('runscript', 'tests/authentic_fusion_local.py', '--proceed=true') + + assert log_messages[:-1] == caplog.messages[:-1] + assert caplog.messages[-1] == '=== Success ===' + + assert User.objects.count() == 11 + assert User.objects.filter(is_active=True).count() == 7 + + assert User.objects.filter(email='normal.user@gmail.com', is_active=True).count() == 2 + assert User.objects.filter(email='agent.no.saml@cd-essonne.fr', is_active=True).count() == 2 + assert User.objects.filter(email='agent.no.dup@cd-essonne.fr', is_active=True).count() == 1 + + saml_user_old_connection.refresh_from_db() + assert saml_user_old_connection.is_active is False + + saml_user_old_connection_no_roles.refresh_from_db() + assert saml_user_old_connection_no_roles.is_active is False + + saml_user_recent_connection.refresh_from_db() + assert saml_user_recent_connection.is_active is True + assert set(saml_user_recent_connection.roles.all()) == {role1, role2, role3} + + saml_user_old_connection_2.refresh_from_db() + assert saml_user_old_connection_2.is_active is False + + saml_user_recent_connection_no_roles_2.refresh_from_db() + assert saml_user_recent_connection_no_roles_2.is_active is True + assert set(saml_user_recent_connection_no_roles_2.roles.all()) == {role1, role2}