Пример #1
0
def change_password_with_old_password(db: DatabaseHandler, email: str,
                                      old_password: str, new_password: str,
                                      new_password_repeat: str) -> None:
    """Change password by entering old password."""

    email = decode_object_from_bytes_if_needed(email)
    old_password = decode_object_from_bytes_if_needed(old_password)
    new_password = decode_object_from_bytes_if_needed(new_password)
    new_password_repeat = decode_object_from_bytes_if_needed(
        new_password_repeat)

    # Check if user exists
    try:
        user_info(db=db, email=email)
    except Exception:
        raise McAuthChangePasswordException(
            'User with email address "%s" does not exist.' % email)

    if old_password == new_password:
        raise McAuthChangePasswordException(
            'Old and new passwords are the same.')

    # Validate old password; fetch the hash from the database again because that hash might be outdated (e.g. if the
    # password has been changed already)
    db_password_old = db.query(
        """
        SELECT auth_users_id,
               email,
               password_hash
        FROM auth_users
        WHERE email = %(email)s
        LIMIT 1
    """, {
            'email': email
        }).hash()
    if db_password_old is None or len(db_password_old) == 0:
        raise McAuthChangePasswordException(
            'Unable to find the user in the database.')

    # Validate the password
    try:
        login_with_email_password(db=db, email=email, password=old_password)
    except Exception as ex:
        raise McAuthChangePasswordException(
            "Unable to log in with old password: %s" % str(ex))

    # Execute the change
    try:
        change_password(db=db,
                        email=email,
                        new_password=new_password,
                        new_password_repeat=new_password_repeat)
    except Exception as ex:
        raise McAuthChangePasswordException("Unable to change password: %s" %
                                            str(ex))
    def test_login_with_api_key_inactive_user(self):
        """Inactive user logging in with API key."""

        db = connect_to_db()

        email = '*****@*****.**'
        password = '******'
        full_name = 'Test user login'
        ip_address = '1.2.3.4'

        add_user(
            db=db,
            new_user=NewUser(
                email=email,
                full_name=full_name,
                notes='Test test test',
                role_ids=[1],
                active=False,
                password=password,
                password_repeat=password,
                activation_url='https://activate.com/activate',
            ),
        )

        user = user_info(db=db, email=email)
        assert user
        global_api_key = user.global_api_key()

        with pytest.raises(McAuthLoginException) as ex:
            login_with_api_key(db=db,
                               api_key=global_api_key,
                               ip_address=ip_address)

        # Make sure the error message explicitly states that login failed due to user not being active
        assert 'not active' in str(ex)
Пример #3
0
def send_user_activation_token(db: DatabaseHandler,
                               email: str,
                               activation_link: str) -> None:
    """Prepare for activation by emailing the activation token."""

    email = decode_object_from_bytes_if_needed(email)
    activation_link = decode_object_from_bytes_if_needed(activation_link)

    # Check if user exists
    try:
        user = user_info(db=db, email=email)
        full_name = user.full_name()

    except Exception as ex:
        log.warning("Unable to fetch user profile for user '%s': %s" % (email, str(ex),))
        full_name = 'Nonexistent user'

    # If user was not found, send an email to a random address anyway to avoid timing attack
    full_activation_link = _generate_user_activation_token(db=db, email=email, activation_link=activation_link)
    if not full_activation_link:
        log.warning("Unable to generate full activation link for email '%s'" % email)
        email = '*****@*****.**'
        full_activation_link = 'activation link'

    message = AuthActivationNeededMessage(
        to=email,
        full_name=full_name,
        activation_url=full_activation_link,
    )
    if not send_email(message):
        raise McAuthRegisterException('The user was created, but I was unable to send you an activation email.')
Пример #4
0
def change_password_with_reset_token(db: DatabaseHandler, email: str,
                                     password_reset_token: str,
                                     new_password: str,
                                     new_password_repeat: str) -> None:
    """Change password with a password token sent by email."""

    email = decode_object_from_bytes_if_needed(email)
    password_reset_token = decode_object_from_bytes_if_needed(
        password_reset_token)
    new_password = decode_object_from_bytes_if_needed(new_password)
    new_password_repeat = decode_object_from_bytes_if_needed(
        new_password_repeat)

    if not password_reset_token:
        raise McAuthChangePasswordException('Password reset token is empty.')

    # Check if user exists
    try:
        user_info(db=db, email=email)
    except Exception:
        raise McAuthChangePasswordException(
            'User with email address "%s" does not exist.' % email)

    # Validate the token once more (was pre-validated in controller)
    if not password_reset_token_is_valid(
            db=db, email=email, password_reset_token=password_reset_token):
        raise McAuthChangePasswordException('Password reset token is invalid.')

    # Execute the change
    try:
        change_password(db=db,
                        email=email,
                        new_password=new_password,
                        new_password_repeat=new_password_repeat)
    except Exception as ex:
        raise McAuthChangePasswordException("Unable to change password: %s" %
                                            str(ex))

    # Unset the password reset token
    db.query(
        """
        UPDATE auth_users
        SET password_reset_token_hash = NULL
        WHERE email = %(email)s
    """, {'email': email})
Пример #5
0
def delete_user(db: DatabaseHandler, email: str) -> None:
    """Delete user."""

    email = decode_object_from_bytes_if_needed(email)

    if not email:
        raise McAuthProfileException('Email address is empty.')

    # Check if user exists
    try:
        user_info(db=db, email=email)
    except Exception:
        raise McAuthProfileException("User with email address '%s' does not exist." % email)

    # Delete the user (PostgreSQL's relation will take care of 'auth_users_roles_map')
    db.query("""
        DELETE FROM auth_users
        WHERE email = %(email)s
    """, {'email': email})
Пример #6
0
def regenerate_api_key(db: DatabaseHandler, email: str) -> None:
    """Regenerate API key -- creates new non-IP limited API key, removes all IP-limited API keys."""

    email = decode_object_from_bytes_if_needed(email)

    if not email:
        raise McAuthProfileException('Email address is empty.')

    # Check if user exists
    try:
        user = user_info(db=db, email=email)
    except Exception:
        raise McAuthProfileException(
            "User with email address '%s' does not exist." % email)

    db.begin()

    # Purge all IP-limited API keys
    db.query(
        """
        DELETE FROM auth_user_api_keys
        WHERE ip_address IS NOT NULL
          AND auth_users_id = (
            SELECT auth_users_id
            FROM auth_users
            WHERE email = %(email)s
          )
    """, {'email': email})

    # Regenerate non-IP limited API key
    db.query(
        """
        UPDATE auth_user_api_keys

        -- DEFAULT points to a generation function
        SET api_key = DEFAULT

        WHERE ip_address IS NULL
          AND auth_users_id = (
            SELECT auth_users_id
            FROM auth_users
            WHERE email = %(email)s
          )
    """, {'email': email})

    message = AuthAPIKeyResetMessage(to=email, full_name=user.full_name())
    if not send_email(message):
        db.rollback()
        raise McAuthProfileException(
            "Unable to send email about reset API key.")

    db.commit()
Пример #7
0
def regenerate_api_key(db: DatabaseHandler, email: str) -> None:
    """Regenerate API key -- creates new non-IP limited API key, removes all IP-limited API keys."""

    email = decode_object_from_bytes_if_needed(email)

    if not email:
        raise McAuthProfileException('Email address is empty.')

    # Check if user exists
    try:
        user = user_info(db=db, email=email)
    except Exception:
        raise McAuthProfileException(
            "User with email address '%s' does not exist." % email)

    db.begin()

    # Purge all API keys
    db.query(
        """
        DELETE FROM auth_user_api_keys
        WHERE auth_users_id = %(auth_users_id)s
    """, {'auth_users_id': user.user_id()})

    # Regenerate non-IP limited API key
    db.query(
        """
        INSERT INTO auth_user_api_keys (
            auth_users_id,
            api_key,
            ip_address
        )
        VALUES (
            %(auth_users_id)s,

            -- DEFAULT points to a generation function
            DEFAULT,

            NULL
        )
    """, {'auth_users_id': user.user_id()})

    message = AuthAPIKeyResetMessage(to=email, full_name=user.full_name())
    if not send_email(message):
        db.rollback()
        raise McAuthProfileException(
            "Unable to send email about reset API key.")

    db.commit()
Пример #8
0
def test_user_info():
    db = connect_to_db()

    email = '*****@*****.**'
    full_name = 'Test user info'
    notes = 'Test test test'
    weekly_requests_limit = 123
    weekly_requested_items_limit = 456
    max_topic_stories = 789

    add_user(
        db=db,
        new_user=NewUser(
            email=email,
            full_name=full_name,
            has_consented=True,
            notes=notes,
            role_ids=[1],
            active=True,
            password='******',
            password_repeat='user_info',
            activation_url='',  # user is active, no need for activation URL
            resource_limits=Resources(
                weekly_requests=weekly_requests_limit,
                weekly_requested_items=weekly_requested_items_limit,
                max_topic_stories=max_topic_stories,
            ),
        ),
    )

    user = user_info(db=db, email=email)

    assert isinstance(user, CurrentUser)
    assert user.user_id()
    assert user.email() == email
    assert user.full_name() == full_name
    assert user.notes() == notes
    assert user.resource_limits()
    assert user.resource_limits().weekly_requests() == weekly_requests_limit
    assert user.resource_limits().weekly_requested_items(
    ) == weekly_requested_items_limit
    assert user.resource_limits().max_topic_stories() == max_topic_stories
    assert user.active()
    assert user.has_consented()
    assert user.created_date()
    assert __looks_like_iso8601_date(user.created_date())
    assert user.global_api_key()
    assert user.password_hash()
    assert user.has_role('admin')
Пример #9
0
def activate_user_via_token(db: DatabaseHandler, email: str,
                            activation_token: str) -> None:
    """Change password with a password token sent by email."""

    email = decode_object_from_bytes_if_needed(email)
    activation_token = decode_object_from_bytes_if_needed(activation_token)

    if not email:
        raise McAuthRegisterException("Email is empty.")
    if not activation_token:
        raise McAuthRegisterException('Password reset token is empty.')

    # Validate the token once more (was pre-validated in controller)
    if not password_reset_token_is_valid(
            db=db, email=email, password_reset_token=activation_token):
        raise McAuthRegisterException('Activation token is invalid.')

    db.begin()

    # Set the password hash
    db.query(
        """
        UPDATE auth_users
        SET active = TRUE
        WHERE email = %(email)s
    """, {'email': email})

    # Unset the password reset token
    db.query(
        """
        UPDATE auth_users
        SET password_reset_token_hash = NULL
        WHERE email = %(email)s
    """, {'email': email})

    user = user_info(db=db, email=email)

    message = AuthActivatedMessage(to=email, full_name=user.full_name())
    if not send_email(message):
        db.rollback()
        raise McAuthRegisterException(
            "Unable to send email about an activated user.")

    db.commit()
Пример #10
0
def all_users(db: DatabaseHandler) -> List[CurrentUser]:
    """Fetch and return a list of users and their roles."""

    # Start a transaction so that the list of users doesn't change while we run separate queries with user_info()
    db.begin()

    user_emails = db.query("""
        SELECT email
        FROM auth_users
        ORDER BY auth_users_id
    """).flat()

    users = []

    for email in user_emails:
        users.append(user_info(db=db, email=email))

    db.commit()

    return users
Пример #11
0
def login_with_email_password(db: DatabaseHandler, email: str, password: str, ip_address: str = None) -> CurrentUser:
    """Log in with username and password; raise on unsuccessful login."""

    email = decode_object_from_bytes_if_needed(email)
    password = decode_object_from_bytes_if_needed(password)

    if not (email and password):
        raise McAuthLoginException("Email and password must be defined.")

    # Try-except block because we don't want to reveal the specific reason why the login has failed
    try:

        user = user_info(db=db, email=email)

        # Check if user has tried to log in unsuccessfully before and now is trying
        # again too fast
        if __user_is_trying_to_login_too_soon(db=db, email=email):
            raise McAuthLoginException(
                "User '%s' is trying to log in too soon after the last unsuccessful attempt." % email
            )

        if not password_hash_is_valid(password_hash=user.password_hash(), password=password):
            raise McAuthLoginException("Password for user '%s' is invalid." % email)

    except Exception as ex:
        log.info(
            "Login failed for %(email)s, will delay any successive login attempt for %(delay)d seconds: %(exc)s" % {
                'email': email,
                'delay': __POST_UNSUCCESSFUL_LOGIN_DELAY,
                'exc': str(ex),
            }
        )

        # Set the unsuccessful login timestamp
        # (TIMESTAMP 'now' returns "current transaction's start time", so using LOCALTIMESTAMP instead)
        db.query("""
            UPDATE auth_users
            SET last_unsuccessful_login_attempt = LOCALTIMESTAMP
            WHERE email = %(email)s
        """, {'email': email})

        # It might make sense to time.sleep() here for the duration of $POST_UNSUCCESSFUL_LOGIN_DELAY seconds to prevent
        # legitimate users from trying to log in too fast. However, when being actually brute-forced through multiple
        # HTTP connections, this approach might end up creating a lot of processes that would time.sleep() and take up
        # memory.
        #
        # So, let's return the error page ASAP and hope that a legitimate user won't be able to reenter his / her
        # password before the $POST_UNSUCCESSFUL_LOGIN_DELAY amount of seconds pass.

        # Don't give out a specific reason for the user to not be able to find
        # out which user emails are registered
        raise McAuthLoginException("User '%s' was not found or password is incorrect." % email)

    if not user.active():
        raise McAuthLoginException("User with email '%s' is not active." % email)

    # Reset password reset token (if any)
    db.query("""
        UPDATE auth_users
        SET password_reset_token_hash = NULL
        WHERE email = %(email)s
          AND password_reset_token_hash IS NOT NULL
    """, {'email': email})

    if ip_address:
        if not user.api_key_for_ip_address(ip_address):
            db.create(
                table='auth_user_api_keys',
                insert_hash={
                    'auth_users_id': user.user_id(),
                    'ip_address': ip_address,
                })

            # Fetch user again
            user = user_info(db=db, email=email)

            if not user.api_key_for_ip_address(ip_address):
                raise McAuthLoginException("Unable to create per-IP API key for IP %s" % ip_address)

    return user
Пример #12
0
def login_with_api_key(db: DatabaseHandler, api_key: str, ip_address: str) -> CurrentUser:
    """Fetch user object for the API key. Only active users are fetched."""

    api_key = decode_object_from_bytes_if_needed(api_key)
    ip_address = decode_object_from_bytes_if_needed(ip_address)

    if not api_key:
        raise McAuthLoginException("API key is undefined.")

    if not ip_address:
        # Even if provided API key is the global one, we want the IP address
        raise McAuthLoginException("IP address is undefined.")

    api_key_user = db.query("""
        SELECT auth_users.email
        FROM auth_users
            INNER JOIN auth_user_api_keys
                ON auth_users.auth_users_id = auth_user_api_keys.auth_users_id
        WHERE
            (
                auth_user_api_keys.api_key = %(api_key)s AND
                (
                    auth_user_api_keys.ip_address IS NULL
                    OR
                    auth_user_api_keys.ip_address = %(ip_address)s
                )
            )

        GROUP BY auth_users.auth_users_id,
                 auth_users.email
        ORDER BY auth_users.auth_users_id
        LIMIT 1
    """, {
        'api_key': api_key,
        'ip_address': ip_address,
    }).hash()

    if api_key_user is None or len(api_key_user) == 0:
        raise McAuthLoginException("Unable to find user for API key '%s' and IP address '%s'" % (api_key, ip_address,))

    email = api_key_user['email']

    # Check if user has tried to log in unsuccessfully before and now is trying again too fast
    if __user_is_trying_to_login_too_soon(db=db, email=email):
        raise McAuthLoginException(
            "User '%s' is trying to log in too soon after the last unsuccessful attempt." % email
        )

    user = user_info(db=db, email=email)

    # Reset password reset token (if any)
    db.query("""
        UPDATE auth_users
        SET password_reset_token_hash = NULL
        WHERE email = %(email)s
          AND password_reset_token_hash IS NOT NULL
    """, {'email': email})

    if not user.active():
        raise McAuthLoginException("User '%s' for API key '%s' is not active." % (email, api_key,))

    return user
Пример #13
0
def update_user(db: DatabaseHandler, user_updates: ModifyUser) -> None:
    """Update an existing user."""

    if not user_updates:
        raise McAuthProfileException("Existing user is undefined.")

    # Check if user exists
    try:
        user = user_info(db=db, email=user_updates.email())
    except Exception:
        raise McAuthProfileException('User with email address "%s" does not exist.' % user_updates.email())

    db.begin()

    if user_updates.full_name() is not None:
        db.query("""
            UPDATE auth_users
            SET full_name = %(full_name)s
            WHERE email = %(email)s
        """, {
            'full_name': user_updates.full_name(),
            'email': user_updates.email(),
        })

    if user_updates.notes() is not None:
        db.query("""
            UPDATE auth_users
            SET notes = %(notes)s
            WHERE email = %(email)s
        """, {
            'notes': user_updates.notes(),
            'email': user_updates.email(),
        })

    if user_updates.active() is not None:
        db.query("""
            UPDATE auth_users
            SET active = %(active)s
            WHERE email = %(email)s
        """, {
            'active': bool(int(user_updates.active())),
            'email': user_updates.email(),
        })

    if user_updates.has_consented() is not None:
        db.query("""
            UPDATE auth_users
            SET has_consented = %(has_consented)s
            WHERE email = %(email)s
        """, {
            'has_consented': bool(int(user_updates.has_consented())),
            'email': user_updates.email(),
        })

    if user_updates.password() is not None:
        try:
            change_password(
                db=db,
                email=user_updates.email(),
                new_password=user_updates.password(),
                new_password_repeat=user_updates.password_repeat(),
                do_not_inform_via_email=True,
            )
        except Exception as ex:
            db.rollback()
            raise McAuthProfileException("Unable to change password: %s" % str(ex))

    resource_limits = user_updates.resource_limits()
    if resource_limits:

        if resource_limits.weekly_requests() is not None:
            db.query("""
                UPDATE auth_user_limits
                SET weekly_requests_limit = %(weekly_requests_limit)s
                WHERE auth_users_id = %(auth_users_id)s
            """, {
                'weekly_requests_limit': resource_limits.weekly_requests(),
                'auth_users_id': user.user_id(),
            })

        if resource_limits.weekly_requested_items() is not None:
            db.query("""
                UPDATE auth_user_limits
                SET weekly_requested_items_limit = %(weekly_requested_items_limit)s
                WHERE auth_users_id = %(auth_users_id)s
            """, {
                'weekly_requested_items_limit': resource_limits.weekly_requested_items(),
                'auth_users_id': user.user_id(),
            })

        if resource_limits.max_topic_stories() is not None:
            db.query("""
                UPDATE auth_user_limits
                SET max_topic_stories = %(max_topic_stories)s
                WHERE auth_users_id = %(auth_users_id)s
            """, {
                'max_topic_stories': resource_limits.max_topic_stories(),
                'auth_users_id': user.user_id(),
            })

    if user_updates.role_ids() is not None:
        db.query("""
            DELETE FROM auth_users_roles_map
            WHERE auth_users_id = %(auth_users_id)s
        """, {'auth_users_id': user.user_id()})

        for auth_roles_id in user_updates.role_ids():
            db.insert(table='auth_users_roles_map', insert_hash={
                'auth_users_id': user.user_id(),
                'auth_roles_id': auth_roles_id,
            })

    db.commit()
Пример #14
0
def add_user(db: DatabaseHandler, new_user: NewUser) -> None:
    """Add new user."""

    if not new_user:
        raise McAuthRegisterException("New user is undefined.")

    # Check if user already exists
    user_exists = db.query(
        """
        SELECT auth_users_id
        FROM auth_users
        WHERE email = %(email)s
        LIMIT 1
    """, {
            'email': new_user.email()
        }).hash()

    if user_exists is not None and 'auth_users_id' in user_exists:
        raise McAuthRegisterException("User with email '%s' already exists." %
                                      new_user.email())

    # Hash + validate the password
    try:
        password_hash = generate_secure_hash(password=new_user.password())
        if not password_hash:
            raise McAuthRegisterException("Password hash is empty.")
    except Exception as ex:
        log.error("Unable to hash a new password: {}".format(ex))
        raise McAuthRegisterException('Unable to hash a new password.')

    db.begin()

    # Create the user
    db.create(table='auth_users',
              insert_hash={
                  'email': new_user.email(),
                  'password_hash': password_hash,
                  'full_name': new_user.full_name(),
                  'notes': new_user.notes(),
                  'active': bool(int(new_user.active())),
              })

    # Fetch the user's ID
    try:
        user = user_info(db=db, email=new_user.email())
    except Exception as ex:
        db.rollback()
        raise McAuthRegisterException(
            "I've attempted to create the user but it doesn't exist: %s" %
            str(ex))

    # Create roles
    try:
        for auth_roles_id in new_user.role_ids():
            db.create(table='auth_users_roles_map',
                      insert_hash={
                          'auth_users_id': user.user_id(),
                          'auth_roles_id': auth_roles_id,
                      })
    except Exception as ex:
        raise McAuthRegisterException("Unable to create roles: %s" % str(ex))

    # Update limits (if they're defined)
    if new_user.weekly_requests_limit() is not None:
        db.query(
            """
            UPDATE auth_user_limits
            SET weekly_requests_limit = %(weekly_requests_limit)s
            WHERE auth_users_id = %(auth_users_id)s
        """, {
                'auth_users_id': user.user_id(),
                'weekly_requests_limit': new_user.weekly_requests_limit(),
            })

    if new_user.weekly_requested_items_limit() is not None:
        db.query(
            """
            UPDATE auth_user_limits
            SET weekly_requested_items_limit = %(weekly_requested_items_limit)s
            WHERE auth_users_id = %(auth_users_id)s
        """, {
                'auth_users_id':
                user.user_id(),
                'weekly_requested_items_limit':
                new_user.weekly_requested_items_limit(),
            })

    # Subscribe to newsletter
    if new_user.subscribe_to_newsletter():
        db.create(table='auth_users_subscribe_to_newsletter',
                  insert_hash={'auth_users_id': user.user_id()})

    if not new_user.active():
        send_user_activation_token(
            db=db,
            email=new_user.email(),
            activation_link=new_user.activation_url(),
            subscribe_to_newsletter=new_user.subscribe_to_newsletter(),
        )

    db.commit()
Пример #15
0
def change_password(db: DatabaseHandler,
                    email: str,
                    new_password: str,
                    new_password_repeat: str,
                    do_not_inform_via_email: bool = False) -> None:
    """Change user's password."""

    email = decode_object_from_bytes_if_needed(email)
    new_password = decode_object_from_bytes_if_needed(new_password)
    new_password_repeat = decode_object_from_bytes_if_needed(
        new_password_repeat)

    if isinstance(do_not_inform_via_email, bytes):
        do_not_inform_via_email = decode_object_from_bytes_if_needed(
            do_not_inform_via_email)

    do_not_inform_via_email = bool(int(do_not_inform_via_email))

    # Check if user exists
    try:
        user = user_info(db=db, email=email)
    except Exception:
        raise McAuthChangePasswordException(
            'User with email address "%s" does not exist.' % email)

    password_validation_message = validate_new_password(
        email=email,
        password=new_password,
        password_repeat=new_password_repeat)
    if password_validation_message:
        raise McAuthChangePasswordException("Unable to change password: %s" %
                                            password_validation_message)

    # Hash + validate the password
    try:
        password_new_hash = generate_secure_hash(password=new_password)
    except Exception as ex:
        raise McAuthChangePasswordException(
            "Unable to hash a new password: %s" % str(ex))

    if not password_new_hash:
        raise McAuthChangePasswordException(
            "Generated password hash is empty.")

    # Set the password hash
    db.query(
        """
        UPDATE auth_users
        SET password_hash = %(password_hash)s,
            active = TRUE
        WHERE email = %(email)s
    """, {
            'email': email,
            'password_hash': password_new_hash,
        })

    if not do_not_inform_via_email:

        message = AuthPasswordChangedMessage(to=email,
                                             full_name=user.full_name())
        if not send_email(message):
            raise McAuthChangePasswordException(
                'The password has been changed, but I was unable to send an email notifying you about the change.'
            )