Beispiel #1
0
def update_user(user_id):
    """ Updates a user by ID in VirtualUsers, and returns HTTP 200 on success
    """
    user = VirtualUsers.query.get_or_404(user_id)
    json = request.get_json(force=True)

    if 'password' in json:
        min_pwd_length = int(
            Configs.query.filter_by(
                setting='Minimum Password Length').first().value)
        if len(json['password']) < min_pwd_length:
            raise ValidationError(
                'The password must be at least {0} characters long'.format(
                    min_pwd_length))
        user.password = VirtualUsers().encrypt_password(json['password'])
        audit_msg = 'The user "{0}" had their password changed'.format(
            user.email)
        db.session.add(user)
    else:
        raise ValidationError('The password was not supplied in the request')
    try:
        db.session.commit()
        json_logger('audit', current_user.username, audit_msg)
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        json_logger(
            'error', current_user.username,
            'The following error occurred in update_user: {0}'.format(str(e)))
        raise GenericError('The user could not be updated')
    finally:
        db.session.close()
    return {}, 200
Beispiel #2
0
 def from_json(self, json):
     if not json.get('name', None):
         raise ValidationError('The domain name was not specified')
     if self.query.filter_by(name=json['name']).first() is None:
         self.name = json['name'].lower()
     else:
         raise ValidationError(
             'The domain "{0}" already exists'.format(json['name'].lower()))
     return self
Beispiel #3
0
def get_logs_dict(num_lines=50, reverse_order=False):
    """
    Returns the JSON formatted log file as a dict
    """
    log_path = app.config.get('LOG_LOCATION')
    if log_path and os.path.exists(log_path):
        log_file = open(log_path, mode='r+')

        try:
            mmap_handler = mmap(log_file.fileno(), 0)
        except ValueError as e:
            if str(e) == 'cannot mmap an empty file':
                # If the file is empty, return empty JSON
                return {'items': [], }
            else:
                raise ValidationError(
                    'There was an error opening "{0}"'.format(
                        os.getcwd().replace('\\', '/') + '/' + log_path))

        new_line_count = 0
        # Assigns current_char to the last character of the file
        current_char = mmap_handler.size() - 1

        # If the file ends in a new line, add 1 more line to process
        # for mmap_handler[current_char:].splitlines() later on
        if mmap_handler[current_char] == '\n':
            num_lines += 1

        # While the number of lines iterated is less than numLines
        # and the beginning of the file hasn't been reached
        while new_line_count < num_lines and current_char > 0:
            # If a new line character is found, this means
            # the current line has ended
            if mmap_handler[current_char] == '\n':
                new_line_count += 1
            # Subtract from the charcter count to read the previous character
            current_char -= 1

        # If the beginning of the file hasn't been reached,
        # strip the preceeding new line character
        if current_char > 0:
            current_char += 2

        # Create the list
        logs = mmap_handler[current_char:].splitlines()

        # Close the log file
        mmap_handler.close()
        log_file.close()

        if reverse_order:
            logs = list(reversed(logs))

        return {'items': [loads(log.decode('utf-8')) for log in logs], }
    else:
        raise ValidationError('The log file could not be found')
Beispiel #4
0
 def ldap_user_from_json(self, json):
     if not json.get('username', None):
         raise ValidationError('The username was not specified')
     if not json.get('name', None):
         raise ValidationError('The name was not specified')
     if self.query.filter_by(username=json['username']).first() is not None:
         raise ValidationError('"{0}" already exists'.format(
             json['username'].lower()))
     self.username = json['username']
     self.name = json['name']
     self.source = 'ldap'
     self.active = True
     return self
Beispiel #5
0
    def set_password(self, new_password):
        """ Sets the password for an admin.
        """
        if not self.id:
            raise ValidationError('An admin is not associated with the object')

        min_pwd_length = int(Configs.query.filter_by(
            setting='Minimum Password Length').first().value)
        if len(new_password) < min_pwd_length:
            error_msg = ('The password must be at least {0} characters long'
                         .format(min_pwd_length))
            raise ValidationError(error_msg)

        self.password = bcrypt.generate_password_hash(new_password)
Beispiel #6
0
def update_config(config_id):
    """ Updates a config by ID in Configs, and returns HTTP 200 on success
    """
    config = Configs.query.get_or_404(config_id)
    json = request.get_json(force=True)

    if 'value' in json and is_config_update_valid(config.setting,
                                                  json['value'], config.regex):
        audit_message = (
            'The setting "{0}" was set from "{1}" to "{2}"'.format(
                config.setting, config.value, json['value']))
        config.value = json['value']
        db.session.add(config)
    else:
        raise ValidationError('An invalid setting value was supplied')

    try:
        db.session.commit()
        json_logger('audit', current_user.username, audit_message)
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        log_msg = ('The following error occurred in update_config: {0}'.format(
            str(e)))
        json_logger('error', current_user.username, log_msg)
        raise GenericError('The configuration could not be updated')
    finally:
        db.session.close()
    return {}, 200
Beispiel #7
0
def verify_qrcode(admin_id):
    """ Verifies if the 2 factor token provided is correct

        This will enable 2 factor for a user.
    """
    admin = Admins.query.get_or_404(admin_id)
    if request.get_json(force=True).get('code'):
        if not admin.otp_secret:
            raise GenericError(
                "The 2 Factor Secret has not been generated yet")
        if admin.verify_totp(request.get_json(force=True).get('code')):
            if not admin.otp_active:
                audit_message = ('The administrator "{0}" enabled 2 '
                                 'factor'.format(admin.username))
                admin.otp_active = True
                try:
                    db.session.add(admin)
                    db.session.commit()
                    json_logger('audit', current_user.username, audit_message)
                except ValidationError as e:
                    raise e
                except Exception as e:
                    db.session.rollback()
                    log_msg = ('The following error occurred in verify_qrcode:'
                               ' {0}'.format(str(e)))
                    json_logger('error', current_user.username, log_msg)
                    raise GenericError(
                        'The administrator could not be updated')
            return {}, 200
        else:
            raise GenericError("An invalid code was supplied")
    else:
        raise ValidationError("The code was not supplied")
Beispiel #8
0
def json_logger(category, admin, message):
    """ Takes a category (typically error or audit), a log message and the
    responsible user. It then appends it with an ISO 8601 UTC timestamp to a
    JSON formatted log file
    """
    log_path = app.config.get('LOG_LOCATION')
    if log_path and ((category == 'error') or
                     (category == 'audit' and maildb_auditing_enabled()) or
                     (category == 'auth' and login_auditing_enabled())):
        try:
            with open(log_path, mode='a+') as log_file:
                log_file.write("{}\n".format(
                    json.dumps(
                        {
                            'category': category,
                            'message': message,
                            'admin': admin,
                            'timestamp': datetime.utcnow().isoformat() + 'Z'
                        },
                        sort_keys=True)))
                log_file.close()
        except IOError:
            error_msg = ('The log could not be written to "{0}". Verify that '
                         'the path exists and is writeable.'.format(
                             path.abspath(log_path)))
            raise ValidationError(error_msg)
Beispiel #9
0
 def from_json(self, json):
     """ Returns a database row from JSON input
     """
     if not json.get('setting', None):
         raise ValidationError('The setting was not specified')
     if not json.get('value', None):
         raise ValidationError('The value of the setting was not specified')
     if not json.get('regex', None):
         raise ValidationError(
             'The regex for valid setting values was not specified')
     if self.query.filter_by(setting=json['setting']).first() is not None:
         raise ValidationError('The setting "{0}" already exists'.format(
             json['setting']))
     self.setting = json['setting']
     self.value = json['value']
     self.regex = json['regex']
     return self
Beispiel #10
0
 def from_json(self, json):
     if not json.get('source', None):
         raise ValidationError('The source email was not specified')
     if not json.get('destination', None):
         raise ValidationError('The destination email was not specified')
     if self.query.filter_by(
             source=json['source'],
             destination=json['destination']).first() is not None:
         raise ValidationError('"{0}" to "{1}" already exists!'.format(
             json['source'], json['destination']))
     if self.validate_source(json['source'].lower()):
         self.source = json['source'].lower()
     if self.validate_destination(json['destination'].lower()):
         self.destination = json['destination'].lower()
         self.domain_id = VirtualDomains.query.filter_by(name=search(
             '(?<=@).*$', self.destination).group(0)).first().id
     return self
Beispiel #11
0
def update_admin(admin_id):
    """ Updates an admin user by ID in Admins, and returns HTTP 200 on success
    """
    audit_msg = ''
    admin = Admins.query.get_or_404(admin_id)
    json = request.get_json(force=True)

    if 'username' in json:
        new_user_name = json['username'].lower()
        if Admins.query.filter_by(username=new_user_name).first() is None:
            audit_msg = ('The administrator "{0}" had their username changed '
                         'to "{1}"'.format(admin.username, new_user_name))
            admin.username = new_user_name
        else:
            ValidationError('The username supplied already exists')
    elif 'password' in json:
        admin.set_password(json['password'])
        audit_msg = (
            'The administrator "{0}" had their password changed'.format(
                admin.username))
    elif 'name' in json:
        audit_msg = (
            'The administrator "{0}" had their name changed to "{1}"'.format(
                admin.username, admin.name))
        admin.name = json['name']
    else:
        raise ValidationError(
            'The username, password, or name was not supplied in the request')

    try:
        db.session.add(admin)
        db.session.commit()
        json_logger('audit', current_user.username, audit_msg)
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        json_logger(
            'error', current_user.username,
            'The following error occurred in update_admin: {0}'.format(str(e)))
        raise GenericError('The administrator could not be updated')
    finally:
        db.session.close()
    return {}, 200
Beispiel #12
0
 def validate_destination(destination):
     if match('.*@.*[.].*$', destination):
         destination_domain = search('(?<=@).*$', destination).group(0)
         if VirtualDomains.query.filter_by(
                 name=destination_domain).first() is None:
             raise ValidationError(
                 'The domain "{0}" is not managed by this database'.format(
                     destination_domain))
         if VirtualUsers.query.filter_by(
                 email=destination).first() is not None:
             return True
         else:
             error_msg = ('The destination "{0}" is not a current email '
                          'address'.format(destination))
             raise ValidationError(error_msg)
     else:
         raise ValidationError(
             'The destination "{0}" is not in a valid email format'.format(
                 destination))
Beispiel #13
0
    def is_unlocked(self):
        """ Returns a boolean based on if the admin is unlocked.
        """
        if not self.id:
            raise ValidationError('An admin is not associated with the object')

        if self.unlock_date and self.unlock_date > datetime.utcnow():
            return False

        return True
Beispiel #14
0
    def clear_lockout_fields(self):
        """ Clears the lockout fields (failed_attempts, last_failed_date,
        unlock_date) on an admin.
        """
        if not self.id:
            raise ValidationError('An admin is not associated with the object')

        # Only clear out the lockout fields if the admin is not an LDAP user
        if self.source == 'local':
            self.failed_attempts = 0
            self.last_failed_date = None
            self.unlock_date = None
Beispiel #15
0
 def validate_source(source):
     if match('.*@.*[.].*$', source):
         source_domain = search('(?<=@).*$', source).group(0)
         if VirtualAliases.query.filter_by(
                 source=source).first() is not None:
             raise ValidationError(
                 'The source alias "{0}" already exists'.format(source))
         if VirtualUsers.query.filter_by(email=source).first() is not None:
             error_msg = ('The source alias "{0}" is an existing email '
                          'address'.format(source))
             raise ValidationError(error_msg)
         if VirtualDomains.query.filter_by(
                 name=source_domain).first() is None:
             raise ValidationError(
                 'The domain "{0}" is not managed by this database'.format(
                     source_domain))
         return True
     else:
         raise ValidationError(
             'The source "{0}" is not in a valid email format'.format(
                 source))
Beispiel #16
0
 def from_json(self, json):
     if not json.get('email', None):
         raise ValidationError('The email address was not specified')
     if not json.get('password', None):
         raise ValidationError('The password was not specified')
     min_pwd_len = int(Configs.query.filter_by(
         setting='Minimum Password Length').first().value)
     if len(json['password']) < min_pwd_len:
         raise ValidationError(
             'The password must be at least {0} characters long'.format(
                 min_pwd_len))
     if self.query.filter_by(
             email=json['email'].lower()).first() is not None:
         raise ValidationError('"{0}" already exists!'.format(
             json['email'].lower()))
     # Checks if the domain can be extracted and if the email is at least
     # somewhat in the right format
     if search('(?<=@).*$', json['email']) and match('.*@.*[.].*$',
                                                     json['email']):
         domain = search('(?<=@).*$', json['email'].lower()).group(0)
         if VirtualDomains.query.filter_by(name=domain).first() is not None:
             self.domain_id = VirtualDomains.query.filter_by(
                 name=domain).first().id
             self.email = json['email'].lower()
             self.password = self.encrypt_password(json['password'])
         else:
             raise ValidationError(
                 'The domain "{0}" is not managed by this database'.format(
                     domain))
     else:
         raise ValidationError(
             '"{0}" is not a valid email address'.format(
                 json['email'].lower()))
     return self
Beispiel #17
0
def is_config_update_valid(setting, value, valid_value_regex):
    """ A helper function for the update_config function on the
    /configs/<int:config_id> PATCH route. A bool is returned based on if the
    user's input is valid.
    """
    if match(valid_value_regex, value):
        if setting == 'Login Auditing' or setting == 'Mail Database Auditing':
            log_path = app.config.get('LOG_LOCATION')
            if not log_path or not is_file_writeable(log_path):
                error_msg = ('The log could not be written to "{0}". Verify '
                             'that the path exists and is writeable.'
                             .format(os.path.abspath(log_path)))
                raise ValidationError(error_msg)

        elif setting == 'Enable LDAP Authentication':
            ldap_string = Configs.query.filter_by(
                setting='AD Server LDAP String').first().value
            ad_domain = Configs.query.filter_by(
                setting='AD Domain').first().value
            ad_group = Configs.query.filter_by(
                setting='AD PostMaster Group').first().value

            if not (ldap_string and ad_domain and ad_group):
                error_msg = ('The LDAP settings must be configured before '
                             'LDAP authentication is enabled')
                raise ValidationError(error_msg)

        elif setting == 'AD Server LDAP String' or setting == 'AD Domain' \
                or setting == 'AD PostMaster Group':
            ldap_enabled = Configs.query.filter_by(
                setting='Enable LDAP Authentication').first().value

            if ldap_enabled == 'True' and not value:
                error_msg = ('LDAP authentication must be disabled when '
                             'deleting LDAP configuration items')
                raise ValidationError(error_msg)

        return True
    else:
        if setting in ['Minimum Password Length', 'Account Lockout Threshold']:
            error_msg = ('An invalid value was supplied. The value must be '
                         'between 0-25.')
            raise ValidationError(error_msg)
        elif setting in ['Account Lockout Duration in Minutes',
                         'Reset Account Lockout Counter in Minutes']:
            error_msg = ('An invalid value was supplied. The value must be '
                         'between 1-99.')
            raise ValidationError(error_msg)
        elif setting == 'LDAP Authentication Method':
            error_msg = ('An invalid value was supplied. The value must be '
                         'either "NTLM" or "SIMPLE"')
            raise ValidationError(error_msg)

        raise ValidationError('An invalid setting value was supplied')
Beispiel #18
0
def clear_lockout_fields_on_user(username):
    """ Clears the lockout fields (failed_attempts, last_failed_date, unlock_date) on a user. This is used
    to unlock a user, or when a user logs in successfully.
    """
    admin = models.Admins.query.filter_by(username=username).first()

    if not admin:
        raise ValidationError('The admin does not exist in the database.')

    try:
        admin.clear_lockout_fields()
        db.session.add(admin)
        db.session.commit()
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        json_logger(
            'error', username,
            'The following error occurred when try to clear out the lockout fields: {0}'.format(str(e)))
        ValidationError('A database error occurred. Please try again.', 'error')
    finally:
        db.session.close()
Beispiel #19
0
    def from_json(self, json):
        if not json.get('username', None):
            raise ValidationError('The username was not specified')
        if not json.get('password', None):
            raise ValidationError('The password was not specified')
        if not json.get('name', None):
            raise ValidationError('The name was not specified')
        if self.query.filter_by(username=json['username']).first() is not None:
            raise ValidationError('"{0}" already exists'.format(
                json['username'].lower()))
        min_pwd_length = int(Configs.query.filter_by(
            setting='Minimum Password Length').first().value)
        if len(json['password']) < min_pwd_length:
            error_msg = ('The password must be at least {0} characters long'
                         .format(min_pwd_length))
            raise ValidationError(error_msg)

        self.password = bcrypt.generate_password_hash(json['password'])
        self.username = json['username'].lower()
        self.name = json['name']
        self.source = 'local'
        self.active = True
        return self
Beispiel #20
0
def reset_admin_password(username, new_password):
    """ Resets an admin's password with one supplied
    """
    admin = models.Admins.query.filter_by(username=username).first()

    if not admin:
        raise ValidationError('The admin does not exist in the database.')

    admin.set_password(new_password)

    try:
        db.session.add(admin)
        db.session.commit()
        json_logger('audit', 'CLI', ('The administrator "{0}" had their password changed via the CLI'.format(username)))
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        json_logger(
            'error', username,
            'The following error occurred when try to reset an admin\'s password: {0}'.format(str(e)))
        ValidationError('A database error occurred. Please try again.', 'error')
    finally:
        db.session.close()
Beispiel #21
0
def increment_failed_login(username):
    """ Increments the failed_attempts value, updates the last_failed_date value, and sets the unlock_date value
    if necessary on the admin
    """
    admin = models.Admins.query.filter_by(username=username).first()
    if not admin:
        raise ValidationError('The admin does not exist in the database.')

    account_lockout_threshold = int(models.Configs.query.filter_by(setting='Account Lockout Threshold').first().value)
    reset_account_lockout_counter = int(models.Configs.query.filter_by(
        setting='Reset Account Lockout Counter in Minutes').first().value)
    account_lockout_duration = int(models.Configs.query.filter_by(
        setting='Account Lockout Duration in Minutes').first().value)

    admin.increment_failed_login(account_lockout_threshold, reset_account_lockout_counter, account_lockout_duration)

    try:
        db.session.add(admin)
        db.session.commit()

        # If the account is locked out, log an audit message
        if not admin.is_unlocked():
            audit_message = '"{0}" is now locked out and will be unlocked in {1} minute(s)'.format(
                username, account_lockout_duration)
            json_logger('audit', username, audit_message)
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        json_logger(
            'error', username,
            'The following error occurred when incrementing the failed login attempts field on "{0}": {1}'.format(
                username, str(e)))
        ValidationError('A database error occurred. Please try again.', 'error')
    finally:
        db.session.close()
Beispiel #22
0
def update_alias(alias_id):
    """ Updates an alias by ID in VirtualAliases, and returns HTTP 200 on
    success
    """
    alias = VirtualAliases.query.get_or_404(alias_id)
    json = request.get_json(force=True)

    if 'source' in json:
        new_source = json['source'].lower()
        if VirtualAliases().validate_source(new_source):
            audit_msg = (
                'The alias "{0}" had their source changed to "{1}"'.format(
                    alias.source, new_source))
            alias.source = new_source
            db.session.add(alias)
    elif 'destination' in json:
        new_destination = json['destination'].lower()
        if VirtualAliases().validate_destination(new_destination):
            audit_msg = ('The alias "{0}" had their destination changed to'
                         ' "{1}"'.format(alias.source, new_destination))
            alias.destination = new_destination
            db.session.add(alias)
    else:
        raise ValidationError(
            'The source or destination was not supplied in the request')

    try:
        db.session.commit()
        json_logger('audit', current_user.username, audit_msg)
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        json_logger(
            'error', current_user.username,
            'The following error occurred in update_alias: {0}'.format(str(e)))
        raise GenericError('The alias could not be updated')
    finally:
        db.session.close()
    return {}, 200
Beispiel #23
0
def add_ldap_user_to_db(username, display_name):
    """ Adds an LDAP user stub in the Admins table of the database for flask_login
    """
    try:
        new_admin = models.Admins().ldap_user_from_json({
            'username': username,
            'name': display_name
        })
        db.session.add(new_admin)
        db.session.commit()
        json_logger('audit', username,
                    '"{0}" was added as an LDAP admin to the database"'.format(username))
    except ValidationError as e:
        raise e
    except Exception as e:
        db.session.rollback()
        json_logger(
            'error', username,
            'The following error occurred when adding an LDAP admin: {0}'.format(str(e)))
        ValidationError('A database error occurred. Please try again.', 'error')
    finally:
        db.session.close()
Beispiel #24
0
    def increment_failed_login(self, account_lockout_threshold,
                               reset_account_lockout_counter,
                               account_lockout_duration):
        """ Increments the failed_attempts value, updates the last_failed_date
        value, and sets the unlock_date value if applicable on the admin
        object.
        """
        now = datetime.utcnow()

        if not self.id:
            raise ValidationError('An admin is not associated with the object')

        # Only increment the failed login count if the admin is not an LDAP
        # user
        if self.source == 'local':
            # If the last failed attempt was before the current time minus the
            # minutes configured to reset the account lockout counter, then
            # the failed attempts should be set to 1 again
            if self.last_failed_date and self.last_failed_date < \
                    (now - timedelta(minutes=reset_account_lockout_counter)):
                self.failed_attempts = 1
                self.unlock_date = None
            else:
                # If the admin has never failed a login attempt, the failed
                # attempts column will be null
                if self.failed_attempts:
                    self.failed_attempts += 1
                else:
                    self.failed_attempts = 1

                # Only try to lockout the user if the account lockout
                # threshold is greater than 0, otherwise account lockouts are
                # disabled
                if account_lockout_threshold != 0 \
                        and self.failed_attempts >= account_lockout_threshold:
                    self.unlock_date = now + timedelta(
                        minutes=account_lockout_duration)

            self.last_failed_date = now