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
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
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')
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
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)
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
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")
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)
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
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
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
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))
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
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
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))
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
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')
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()
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
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()
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()
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
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()
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