Esempio n. 1
0
def test_get_by_id(superuser):
    """Get user by ID."""
    user = User('*****@*****.**', 'Foo Bar')
    user.save_as(superuser)

    retrieved = User.get_by_id(user.id)
    assert retrieved == user
Esempio n. 2
0
def test_update_last_login_does_not_update_modified_at(superuser):
    """Test modified date."""
    user = User('*****@*****.**', 'Hello World')
    user.save_as(superuser)
    first_modified_at = user.modified_at

    # Update 'last_login_at' timestamp.
    user.update_last_login()

    # Initial 'modified_at' is still the same.
    assert first_modified_at == user.modified_at
Esempio n. 3
0
def test_modified_at_defaults_to_current_datetime(superuser):
    """Test modified date."""
    user = User('*****@*****.**', 'Wrong Name')
    user.save_as(superuser)
    first_modified_at = user.modified_at

    assert abs((first_modified_at - user.created_at).total_seconds()) < 10

    user.full_name = 'Correct Name'
    user.save_as(user)

    # Initial 'modified_at' has been overwritten.
    assert first_modified_at != user.modified_at
Esempio n. 4
0
def test_created_by_and_modified_by_is_updated(superuser):
    """Test created/modified by."""
    user = User(email='*****@*****.**', full_name='Foo Bar')
    user.save_as(superuser)
    assert user.created_by_id == superuser.id
    assert user.created_by == superuser
    assert user.modified_by_id == superuser.id
    assert user.modified_by == superuser

    # User updates something in their profile.
    user.update_as(user, commit=True, full_name='Bar Foo')
    assert user.created_by == superuser
    assert user.modified_by == user
Esempio n. 5
0
def test_login_attempt_purge(superuser):
    """Test purging login attempts for username/IP combo."""
    user = User('*****@*****.**', 'Foo fooson')
    user.save_as(superuser)

    foo_login_attempt = FailedLoginAttempt('*****@*****.**', '127.0.0.1')
    foo_login_attempt.save()
    bar_login_attempt = FailedLoginAttempt('bar', '127.0.0.1')
    bar_login_attempt.save()

    attempts = FailedLoginAttempt.query.all()
    assert len(attempts) == 2
    user.update_last_login_internal(commit=True, remote_address='127.0.0.1')

    attempts = FailedLoginAttempt.query.all()
    assert len(attempts) == 1
    assert bar_login_attempt in attempts
Esempio n. 6
0
def test_password_can_contain_utf8_chars(superuser):
    """Test that passwords can contain utf-8 characters."""
    password = '******' * 40  # test a really long password, just to make sure.
    user = User(email='*****@*****.**', full_name='Foo Bar', password=password)
    user.save_as(superuser)
    assert user.check_password(password)
Esempio n. 7
0
def test_password_defaults_to_a_random_one(superuser):
    """Test empty password field is assigned some random password, instead of being set to null."""
    user = User(email='*****@*****.**', full_name='Foo Bar')
    user.save_as(superuser)
    assert user.password is not None
Esempio n. 8
0
def test_tos_approved_at_defaults_to_null(superuser):
    """Test Terms of Service datetime field is not populated automatically."""
    user = User(email='*****@*****.**', full_name='Foo Bar')
    user.save_as(superuser)
    assert user.tos_approved_at is None
Esempio n. 9
0
def test_created_at_defaults_to_datetime(superuser):
    """Test creation date."""
    user = User(email='*****@*****.**', full_name='Foo Bar')
    user.save_as(superuser)
    assert bool(user.created_at)
    assert isinstance(user.created_at, datetime)
Esempio n. 10
0
def import_data(verbose, admin_email, wipe_permissions, send_password_resets):
    """Read data from Voyager dump and BibDB API to create DB entities.

    Creates:
        - collections
        - user accounts for collection managers
        - permissions between the two

    """
    import requests
    from flask_babel import gettext

    from .collection.forms import RegisterForm as CollectionRegisterForm
    from .collection.models import Collection
    from .permission.models import Permission
    from .user.forms import RegisterForm as UserRegisterForm
    from .user.models import PasswordReset, User

    def _get_collection_details_from_bibdb(code):
        raw_bibdb_api_data = json.loads(requests.get(
            'https://bibdb.libris.kb.se/api/lib?level=brief&sigel={}'
            .format(code)).content.decode('utf-8'))
        if raw_bibdb_api_data['query']['operation'] == 'sigel {}'.format(code):
            if verbose:
                print('Fetched details for sigel %r' % code)
            else:
                click.echo('.', nl=False)
        else:
            if not verbose:
                click.echo('x', nl=False)
            raise AssertionError('Lookup failed for sigel %r' % code)

        bibdb_api_data = None
        for chunk in raw_bibdb_api_data['libraries']:
            if chunk['sigel'] == code:
                if bibdb_api_data is not None:
                    raise AssertionError('Duplicate results for sigel %r' % code)
                bibdb_api_data = chunk
        if not bibdb_api_data:
            raise AssertionError('Zero results for sigel %r' % code)

        if bibdb_api_data['type'] in {'library', 'bibliography'}:
            category = bibdb_api_data['type']
        else:
            category = 'uncategorized'

        if bibdb_api_data['dept']:
            friendly_name = '%s, %s' % (bibdb_api_data['name'], bibdb_api_data['dept'])
        else:
            friendly_name = bibdb_api_data['name']

        assert bibdb_api_data['alive'] in {True, False}

        collection = {
            'friendly_name': friendly_name,
            'code': bibdb_api_data['sigel'],
            'category': category,
            'is_active': bibdb_api_data['alive'],
            'replaces': bibdb_api_data['sigel_old'],
            'replaced_by': bibdb_api_data['sigel_new']
        }

        if bibdb_api_data['date_created']:
            collection['created_at'] = \
                dt.datetime.strptime(bibdb_api_data['date_created'], '%Y-%m-%dT%H:%M:%S')

        return collection

    def _get_voyager_data():
        raw_voyager_sigels_and_locations = requests.get(
            'https://github.com/libris/xl_auth/files/1513982/171129_KB--sigel_locations.txt'
        ).content.decode('latin-1').splitlines()
        voyager_sigels_and_collections = dict()
        voyager_main_sigels, voyager_location_sigels = set(), set()
        for voyager_row in raw_voyager_sigels_and_locations:
            voyager_sigel, voyager_location = voyager_row.split(',')
            assert voyager_sigel and voyager_location
            voyager_main_sigels.add(voyager_sigel)
            voyager_location_sigels.add(voyager_location)
            if voyager_sigel == 'SEK' and voyager_location != 'SEK':
                continue  # Don't add all the collections under SEK super cataloger.
            if voyager_sigel in voyager_sigels_and_collections:
                voyager_sigels_and_collections[voyager_sigel].add(voyager_location)
            else:
                voyager_sigels_and_collections[voyager_sigel] = {voyager_location}
        print('voyager_main_sigels:', len(voyager_main_sigels), '/',
              'voyager_location_sigels:', len(voyager_location_sigels))
        print('(voyager_main_sigels | voyager_location_sigels):',
              len(voyager_main_sigels | voyager_location_sigels))

        return {
            'sigel_to_collections': voyager_sigels_and_collections,
            'sigels': voyager_main_sigels,
            'collections': voyager_location_sigels
        }

    def _get_bibdb_cataloging_admins():
        raw_bibdb_sigels_and_cataloging_admins = requests.get(
            'https://libris.kb.se/libinfo/library_konreg.jsp').content.decode('utf-8').splitlines()

        registering_bibdb_sigels, bibdb_cataloging_admins = set(), set()
        bibdb_sigels_and_cataloging_admins = dict()
        bibdb_cataloging_admins_and_sigels = dict()
        bibdb_cataloging_admin_emails_and_names = dict()
        for bibdb_row in raw_bibdb_sigels_and_cataloging_admins:
            try:
                bibdb_sigel, cataloging_admin_name, cataloging_admin_email = bibdb_row.split(',')
            except ValueError as err:
                print('ValueError: %s / bibdb_row: %r' % (err, bibdb_row))
                continue
            cataloging_admin_email = cataloging_admin_email.lower()
            bibdb_cataloging_admin_emails_and_names[cataloging_admin_email] = cataloging_admin_name
            assert bibdb_sigel != ''
            registering_bibdb_sigels.add(bibdb_sigel)
            if not cataloging_admin_email:
                continue

            bibdb_cataloging_admins.add(cataloging_admin_email)
            bibdb_sigels_and_cataloging_admins[bibdb_sigel] = cataloging_admin_email

            if cataloging_admin_email in bibdb_cataloging_admins_and_sigels:
                bibdb_cataloging_admins_and_sigels[cataloging_admin_email].add(bibdb_sigel)
            else:
                bibdb_cataloging_admins_and_sigels[cataloging_admin_email] = {bibdb_sigel}

        print('registering_bibdb_sigels:', len(registering_bibdb_sigels), '/',
              'bibdb_cataloging_admins:', len(bibdb_cataloging_admins))

        return {
            'sigel_to_cataloging_admins': bibdb_sigels_and_cataloging_admins,
            'cataloging_admin_to_sigels': bibdb_cataloging_admins_and_sigels,
            'cataloging_admin_emails_to_names': bibdb_cataloging_admin_emails_and_names,
            'sigels': registering_bibdb_sigels,
            'cataloging_admins': bibdb_cataloging_admins,
        }

    def _get_bibdb_sigels_not_in_voyager(bibdb_sigels, voyager_sigels):
        unknown_sigels = set()
        for bibdb_sigel in bibdb_sigels:
            if bibdb_sigel not in voyager_sigels:
                unknown_sigels.add(bibdb_sigel)
        return unknown_sigels

    def _generate_xl_auth_cataloging_admins_and_collections(bibdb_cataloging_admin_to_sigels,
                                                            voyager_sigel_to_collections):
        pre_total, post_total = 0, 0
        voyager_sigels_unknown_in_bibdb = set()
        xl_auth_cataloging_admins = dict()
        xl_auth_collections = dict()
        # Prepare permissions for cataloging admins.
        for cataloging_admin, sigels in bibdb_cataloging_admin_to_sigels.items():
            pre_total += len(sigels)
            xl_auth_cataloging_admins[cataloging_admin] = set()
            for sigel in sigels:
                # Fetch details if necessary.
                if sigel not in xl_auth_collections:
                    xl_auth_collections[sigel] = _get_collection_details_from_bibdb(sigel)

                xl_auth_cataloging_admins[cataloging_admin].add(sigel)

                if sigel in bibdb_sigels_unknown_in_voyager:
                    continue  # Don't attempt resolving sigels that does not exist in Voyager.

                # Add additional sigels from Voyager sigel-to-"sub-sigel" mapping.
                for voyager_collection in voyager_sigel_to_collections[sigel]:
                    if voyager_collection not in xl_auth_collections:
                        try:
                            # Fetch details if necessary.
                            xl_auth_collections[voyager_collection] = \
                                _get_collection_details_from_bibdb(voyager_collection)
                        except AssertionError as err:
                            voyager_sigels_unknown_in_bibdb.add(voyager_collection)
                            if verbose:
                                print(err)
                            continue
                    xl_auth_cataloging_admins[cataloging_admin].add(voyager_collection)

            post_total += len(xl_auth_cataloging_admins[cataloging_admin])

        print('\npre_total:', pre_total, '/ post_total:', post_total)
        print('voyager_sigels_unknown_in_bibdb:', voyager_sigels_unknown_in_bibdb)

        resolved_bibdb_refs = set()
        unresolved_bibdb_refs = set()
        print('before-replaces-lookups:', len(xl_auth_collections))
        for _ in range(10):
            for _, details in deepcopy(xl_auth_collections).items():
                for old_new_ref in {'replaces', 'replaced_by'}:
                    if details[old_new_ref] and details[old_new_ref] not in xl_auth_collections:
                        try:
                            xl_auth_collections[details[old_new_ref]] = \
                                _get_collection_details_from_bibdb(details[old_new_ref])
                            resolved_bibdb_refs.add(details[old_new_ref])
                        except AssertionError as err:
                            unresolved_bibdb_refs.add(details[old_new_ref])
                            print(err)
        print('after-replaces-lookups:', len(xl_auth_collections))

        print('resolved_bibdb_refs:', resolved_bibdb_refs)
        print('unresolved_bibdb_refs:', unresolved_bibdb_refs)

        return {
            'collections': xl_auth_collections,
            'cataloging_admins': xl_auth_cataloging_admins
        }

    def _get_manually_added_permissions():
        emails_and_collection_codes = requests.get(
            'https://docs.google.com/spreadsheets/d/e/2PACX-1vT2TjS_L9_J5LJztfKWo0UxQD-RCZo3bheFIH'
            'Ouz2Gu-aGcd7IrlDzHDmQ2yL726z0BnSc47vasL0l3/pub?gid=0&single=true&output=tsv'
        ).content.decode('utf-8').splitlines()

        manual_additions = []
        for add_row in emails_and_collection_codes[1:]:
            add_email, add_code, _ = add_row.split('\t')
            manual_additions.append((add_email.strip(), add_code.strip()))

        return manual_additions

    def _get_manually_deleted_permissions():
        emails_and_collection_codes = requests.get(
            'https://docs.google.com/spreadsheets/d/e/2PACX-1vT2TjS_L9_J5LJztfKWo0UxQD-RCZo3bheFIH'
            'Ouz2Gu-aGcd7IrlDzHDmQ2yL726z0BnSc47vasL0l3/pub?gid=518641812&single=true&output=tsv'
        ).content.decode('utf-8').splitlines()

        manual_deletions = []
        for del_row in emails_and_collection_codes[1:]:
            del_email, del_code, _ = del_row.split('\t')
            manual_deletions.append((del_email.strip(), del_code.strip()))

        return manual_deletions

    # Get admin user
    admin = User.get_by_email(admin_email)

    # Gather data.
    voyager = _get_voyager_data()
    bibdb = _get_bibdb_cataloging_admins()

    bibdb_sigels_unknown_in_voyager = \
        _get_bibdb_sigels_not_in_voyager(bibdb['sigels'], voyager['sigels'])
    print('bibdb_sigels_unknown_in_voyager:', bibdb_sigels_unknown_in_voyager)

    # Compile it into xl_auth-compatible model.
    xl_auth = _generate_xl_auth_cataloging_admins_and_collections(
        bibdb['cataloging_admin_to_sigels'], voyager['sigel_to_collections'])

    # Store collections.
    for collection, details in deepcopy(xl_auth['collections']).items():
        with current_app.test_request_context():
            collection_form = CollectionRegisterForm(admin, code=details['code'],
                                                     friendly_name=details['friendly_name'])
            collection_form.validate()
        if collection_form.code.errors or collection_form.friendly_name.errors:
            for code_error in collection_form.code.errors:
                print('collection %r: %s' % (collection, code_error))
            for friendly_name_error in collection_form.friendly_name.errors:
                print('friendly_name %r: %s' % (details['friendly_name'], friendly_name_error))
            del xl_auth['collections'][collection]
            continue

        collection = Collection.get_by_code(code=details['code'])
        if collection:
            if collection.is_active != details['is_active']:
                collection.is_active = details['is_active']
                collection.save_as(admin)
                print('corrected collection %r: is_active=%s'
                      % (collection.code, collection.is_active))
        else:
            collection = Collection(**details)
            if collection.created_at:
                collection.modified_at = collection.created_at
                collection.modified_by = collection.created_by = admin
                collection.save(preserve_modified=True)
            else:
                collection.save_as(admin)

    # Store users.
    for email, full_name in deepcopy(bibdb['cataloging_admin_emails_to_names']).items():
        if email not in bibdb['cataloging_admins']:
            del bibdb['cataloging_admin_emails_to_names'][email]
            continue

        with current_app.test_request_context():
            user_form = UserRegisterForm(None, username=email, full_name=full_name)
            user_form.validate()
        if gettext('Email already registered') in user_form.username.errors:
            pass
        elif user_form.username.errors or user_form.full_name.errors:
            print('validation failed for %s <%s>' % (full_name, email))
            for username_error in user_form.username.errors:
                print('email %r: %s' % (email, username_error))
            for full_name_error in user_form.full_name.errors:
                print('full_name %r: %s' % (full_name, full_name_error))
            del bibdb['cataloging_admin_emails_to_names'][email]
            continue

        user = User.get_by_email(email)
        if not user:
            user = User(email=email, full_name=full_name, is_active=False)
            if send_password_resets:  # Requires SERVER_NAME and PREFERRED_URL_SCHEME env vars.
                with current_app.test_request_context():
                    password_reset = PasswordReset(user)
                    password_reset.send_email(account_registration_from_user=admin)
                    user.save_as(admin)
                    password_reset.save()
                print('Added inactive user %r (password reset email sent).' % email)
            else:
                user.save_as(admin)
                print('Added inactive user %r (no password reset).' % email)

    old_permissions = Permission.query.all()
    current_permissions, new_permissions, removed_permissions = [], [], []

    # Store permissions.
    for email, collections in xl_auth['cataloging_admins'].items():
        user = User.get_by_email(email)
        if not user:
            continue

        for code in collections:
            collection = Collection.get_by_code(code)
            if not collection:
                print('Collection %r does not exist' % code)
                continue
            permission = Permission.query.filter_by(user_id=user.id,
                                                    collection_id=collection.id).first()
            if permission:
                current_permissions.append(permission)
            elif collection.is_active:  # No creating permissions on inactive collections.
                if user.email == '*****@*****.**':
                    permission = Permission.create_as(admin, user=user, collection=collection,
                                                      registrant=True,
                                                      cataloger=collection.code == 'Utb2',
                                                      cataloging_admin=False)
                else:
                    permission = Permission.create_as(admin, user=user, collection=collection,
                                                      registrant=True, cataloger=True,
                                                      cataloging_admin=True)
                new_permissions.append(permission)

    # Apply manual additions.
    for email, code in _get_manually_added_permissions():
        user = User.get_by_email(email)
        if not user:
            print('Cannot add permission manually; user %r does not exist' % email)
            continue

        collection = Collection.get_by_code(code)
        if not collection:
            print('Cannot add permission manually, collection %r does not exist' % code)
            continue

        permission = Permission.query.filter_by(user_id=user.id,
                                                collection_id=collection.id).first()
        if permission:
            current_permissions.append(permission)
            if verbose:
                print('Manual permission for %r on %r already exists.' % (email, code))
        else:
            permission = Permission.create_as(admin, user=user, collection=collection,
                                              registrant=True, cataloger=True,
                                              cataloging_admin=True)
            new_permissions.append(permission)
            if verbose:
                print('Manually added permissions for %r on %r.' % (email, code))

    # Apply manual deletions.
    for email, code in _get_manually_deleted_permissions():
        user = User.get_by_email(email)
        if not user:
            print('Cannot delete permission manually; user %r does not exist' % email)
            continue

        collection = Collection.get_by_code(code)
        if not collection:
            print('Cannot delete permission manually, collection %r does not exist' % code)
            continue

        permission = Permission.query.filter_by(user_id=user.id,
                                                collection_id=collection.id).first()
        if permission:
            permission.delete()
            removed_permissions.append(permission)
            if verbose:
                print('Manually deleted permissions for %r on %r.' % (email, code))
        else:
            current_permissions.append(permission)
            if verbose:
                print('Cannot manually deleted permissions for %r on %r; does not exist.'
                      % (email, code))

    # Optionally wipe permissions not deduced from controlled sources (BibDB, Voyager, manual),
    # but only if created by admin account. And also existing permissions on inactive collections.
    for permission in old_permissions:
        if not permission.collection.is_active:
            print('Existing permission for %r on inactive collection %r (deleting=%s).'
                  % (permission.user.email, permission.collection.code, wipe_permissions))
            if wipe_permissions:
                permission.delete()
            continue
        elif permission in current_permissions and permission not in removed_permissions:
            continue
        else:
            if permission.created_by != admin:
                print('Unknown permission for %r on %r, created by %r (deleting=False).'
                      % (permission.user.email, permission.collection.code,
                         permission.created_by.email))
                continue

            print('Permission for %r on %r not found during import (deleting=%s).'
                  % (permission.user.email, permission.collection.code, wipe_permissions))
            if wipe_permissions:
                permission.delete()