def login(): """ This is the login route and processing of information """ # Calls the new function in order to refresh the auth_source list login_form = forms.LoginForm.new() if current_user.is_authenticated: return redirect(url_for('common.index')) else: if request.method == 'GET': return render_template('login.html', title='PostMaster Login', loginForm=login_form) elif login_form.validate_on_submit(): username = login_form.admin.username login_user(login_form.admin, remember=False) clear_lockout_fields_on_user(username) json_logger( 'auth', username, 'The administrator "{0}" logged in successfully'.format( username)) return redirect(request.args.get('next') or url_for( 'common.index')) else: wtforms_errors = get_wtforms_errors(login_form) if wtforms_errors: flash(wtforms_errors) return redirect(url_for('common.login'))
def delete_user(user_id): """ Deletes a user by ID in VirtualUsers, and returns HTTP 204 on success """ user = VirtualUsers.query.get_or_404(user_id) aliases = VirtualAliases.query.filter_by(destination=user.email).all() if aliases: for alias in aliases: db.session.delete(alias) db.session.delete(user) try: db.session.commit() json_logger( 'audit', current_user.username, 'The user "{0}" was deleted successfully'.format(user.email)) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in delete_user: {0}'.format(str(e))) raise GenericError('The user could not be deleted') finally: db.session.close() return {}, 204
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: minPwdLength = int(Configs.query.filter_by( setting='Minimum Password Length').first().value) if len(json['password']) < minPwdLength: raise ValidationError( 'The password must be at least {0} characters long'.format( minPwdLength)) user.password = VirtualUsers().encrypt_password(json['password']) auditMessage = '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, auditMessage) 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 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() json_logger( 'error', current_user.username, 'The following error occurred in update_config: {0}'.format(str( e))) raise GenericError('The configuration could not be updated') finally: db.session.close() return {}, 200
def login(): """ This is the login route and processing of information """ # Calls the new function in order to refresh the auth_source list login_form = forms.LoginForm.new() if current_user.is_authenticated: return redirect(url_for('common.index')) else: if request.method == 'GET': return render_template('login.html', title='PostMaster Login', loginForm=login_form) elif login_form.validate_on_submit(): username = login_form.admin.username login_user(login_form.admin, remember=False) clear_lockout_fields_on_user(username) log_msg = ('The administrator "{0}" logged in successfully'.format( username)) json_logger('auth', username, log_msg) return redirect( request.args.get('next') or url_for('common.index')) else: wtforms_errors = get_wtforms_errors(login_form) if wtforms_errors: flash(wtforms_errors) return redirect(url_for('common.login'))
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() json_logger( "error", current_user.username, "The following error occurred in verify_qrcode: {0}".format(str(e)), ) 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 twofactor_disable(admin_id): """ Disable 2 factor using API. Enabling 2 factor from this route is not possible. """ admin = Admins.query.get_or_404(admin_id) status = request.get_json(force=True).get("enabled") if status: if status.lower() == "false": admin.otp_active = False try: db.session.add(admin) db.session.commit() except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( "error", current_user.username, "The following error occurred in twofactor_disable: {0}".format(str(e)), ) raise GenericError("The administrator could not be updated") return {}, 200 elif status.lower() == "true": raise GenericError("Cannot enable 2 factor from this route - see docs") raise GenericError("An invalid parameter was supplied")
def qrcode(admin_id): """ Presents the user with a QR code to scan to setup 2 factor authentication """ # render qrcode for FreeTOTP admin = Admins.query.get_or_404(admin_id) if admin.id != current_user.id: raise GenericError("You may not view other admin's QR codes") if admin.otp_active: return ("", 204) admin.generate_otp_secret() try: db.session.add(admin) db.session.commit() except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger("error", current_user.username, "The following error occurred in qrcode: {0}".format(str(e))) raise GenericError("The administrator could not be updated") url = pyqrcode.create(admin.get_totp_uri()) stream = BytesIO() url.svg(stream, scale=5) return ( stream.getvalue(), 200, { "Content-Type": "image/svg+xml", "Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0", }, )
def twofactor_disable(admin_id): """ Disable 2 factor using API. Enabling 2 factor from this route is not possible. """ admin = Admins.query.get_or_404(admin_id) status = request.get_json(force=True).get('enabled') if status: if status.lower() == "false": admin.otp_active = False try: db.session.add(admin) db.session.commit() except ValidationError as e: raise e except Exception as e: db.session.rollback() log_msg = ('The following error occurred in twofactor_disable:' ' {0}'.format(str(e))) json_logger('error', current_user.username, log_msg) raise GenericError('The administrator could not be updated') return {}, 200 elif status.lower() == 'true': raise GenericError( 'Cannot enable 2 factor from this route - see docs') raise GenericError('An invalid parameter was supplied')
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 qrcode(admin_id): """ Presents the user with a QR code to scan to setup 2 factor authentication """ # render qrcode for FreeTOTP admin = Admins.query.get_or_404(admin_id) if admin.id != current_user.id: raise GenericError('You may not view other admin\'s QR codes') if admin.otp_active: return '', 204 admin.generate_otp_secret() try: db.session.add(admin) db.session.commit() except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in qrcode: {0}'.format(str(e))) raise GenericError('The administrator could not be updated') url = pyqrcode.create(admin.get_totp_uri()) stream = BytesIO() url.svg(stream, scale=5) return stream.getvalue(), 200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '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")
def get_ldap_object(self, sam_account_name, attributes=None): """ Returns an LDAP object based on the sAMAccountName and the desired attributes """ search_filter = '(sAMAccountName={0})'.format(sam_account_name) ldap_object = self.search(search_filter, attributes)[0] # Check if a user was returned if 'dn' in ldap_object: if attributes and 'attributes' in ldap_object: for attribute in attributes: if attribute not in ldap_object['attributes']: log_msg = ('The object with account name "{0}" was ' 'found in LDAP, but the attribute "{1}" ' 'was not'.format( sam_account_name, attribute)) json_logger('error', self.get_loggedin_user(), log_msg) error_msg = ('There was an error searching the LDAP ' 'server. Please try again.') raise ADException(error_msg) return ldap_object log_msg = ('The object with account name "{0}" could not be found in ' 'LDAP'.format(sam_account_name)) json_logger('error', self.get_loggedin_user(), log_msg) raise ADException( 'There was an error searching the LDAP server. Please try again.')
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 login(self, username, password): """ Uses the supplied username and password to bind to LDAP and returns a boolean """ bind_username = self.parse_username_input(username) self.ldap_connection.user = str(bind_username) self.ldap_connection.password = str(password) if self.ldap_connection.bind(): return True json_logger( 'auth', bind_username, 'The administrator "{0}" entered an incorrect username or password via LDAP'.format(bind_username)) raise ADException('The username or password was incorrect')
def login(self, username, password): """ Uses the supplied username and password to bind to LDAP and returns a boolean """ bind_username = self.parse_username_input(username) self.ldap_connection.user = str(bind_username) self.ldap_connection.password = str(password) if self.ldap_connection.bind(): return True log_msg = ('The administrator "{0}" entered an incorrect username or ' 'password via LDAP'.format(bind_username)) json_logger('auth', bind_username, log_msg) raise ADException('The username or password was incorrect')
def __init__(self): """ The constructor that initializes the ldap_connection object """ ldap_enabled = models.Configs().query.filter_by( setting='Enable LDAP Authentication').first() if ldap_enabled is not None and ldap_enabled.value == 'True': ldap_server = models.Configs().query.filter_by( setting='AD Server LDAP String').first() domain = models.Configs().query.filter_by( setting='AD Domain').first() ldap_admin_group = models.Configs().query.filter_by( setting='AD PostMaster Group').first() ldap_auth_method = models.Configs().query.filter_by( setting='LDAP Authentication Method').first() ldap_server_regex = r'LDAP[S]?:\/\/(.*?)\:\d+' if ldap_server is not None and re.search( ldap_server_regex, ldap_server.value, re.IGNORECASE): self.ldap_server = ldap_server.value if domain is not None and ldap_admin_group is not None: self.domain = domain.value self.ldap_admin_group = ldap_admin_group.value ad_server = ldap3.Server( ldap_server.value, allowed_referral_hosts=[('*', False)], connect_timeout=3) # Use NTLMv2 authentication so that credentials aren't sent # in the clear if LDAPS is not used self.ldap_connection = ldap3.Connection( ad_server, auto_referrals=False, authentication=ldap_auth_method.value.upper()) try: self.ldap_connection.open() except ldap3.core.exceptions.LDAPSocketOpenError: log_msg = ('The LDAP server "{0}" could not be ' 'contacted'.format(self.ldap_server)) json_logger('error', 'NA', log_msg) raise ADException( 'The LDAP server could not be contacted') else: log_msg = 'The LDAP Admin Group is not configured properly' json_logger('error', 'NA', log_msg) raise ADException( 'LDAP authentication is not properly configured') else: log_msg = ('The LDAP server string is not configured or ' 'isn\'t properly formatted') json_logger('error', 'NA', log_msg) raise ADException('The LDAP server could not be contacted') else: log_msg = ('An LDAP authentication attempt was made but it is ' 'currently disabled') json_logger('error', 'NA', log_msg) raise ADException('LDAP authentication is not enabled')
def search(self, search_filter, attributes=None, search_scope=ldap3.SUBTREE): """ Returns LDAP objects based on the search_filter and the desired attributes """ # Check if the ldap_connection is in a logged in state if self.ldap_connection.bound: if self.ldap_connection.search( self.base_dn, search_filter, search_scope=search_scope, attributes=attributes): # Check if anything was returned if len(self.ldap_connection.response): return self.ldap_connection.response json_logger( 'error', self.get_loggedin_user(), 'The LDAP object could not be found using the search filter "{0}"'.format(search_filter)) raise ADException('There was an error searching the LDAP server. Please try again.') else: raise ADException('You must be logged into LDAP to search')
def get_primary_group_dn_of_user(self, sam_account_name): """ Returns the distinguished name of the primary group of the user """ user = self.get_ldap_object(sam_account_name, ['primaryGroupID']) primary_group_id = str(user['attributes']['primaryGroupID']) domain_search = self.search('(objectClass=domainDNS)', ['objectSid'], ldap3.BASE) if 'dn' in domain_search[0]: if domain_search[0] and 'attributes' in domain_search[0] and 'objectSid' in domain_search[0]['attributes']: domain_sid = domain_search[0]['attributes']['objectSid'] group_search_filter = '(&(objectClass=group)(objectSid={0}-{1}))'.format(domain_sid, primary_group_id) group_search = self.search(group_search_filter) if 'dn' in group_search[0]: return group_search[0]['dn'] json_logger('error', self.get_loggedin_user(), 'The objectSid of the domain could not be found') raise ADException('There was an error searching the LDAP server. Please try again.')
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 new_admin(): """ Creates a new admin user in Admins, and returns HTTP 201 on success """ admin = Admins().from_json(request.get_json(force=True)) db.session.add(admin) try: db.session.commit() json_logger( "audit", current_user.username, 'The administrator "{0}" was created successfully'.format(admin.username) ) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger("error", current_user.username, "The following error occurred in new_admin: {0}".format(str(e))) raise GenericError("The admininstrator could not be created") finally: db.session.close() return {}, 201
def validate_wtforms_password(form, field): """ Validates the password from a wtforms object """ username = form.username.data password = form.password.data two_factor = form.two_factor.data if form.two_factor.data else None try: if form.auth_source.data == 'PostMaster User': admin = models.Admins.query.filter_by(username=username, source='local').first() if admin: if admin.is_unlocked(): if bcrypt.check_password_hash(admin.password, password): if admin.otp_active: if not admin.verify_totp(two_factor): raise WtfStopValidation('The two factor authentication code is incorrect') form.admin = admin return else: increment_failed_login(username) else: raise WtfStopValidation('The user is currently locked out. Please try logging in again later.') json_logger( 'auth', username, 'The administrator "{0}" entered an incorrect username or password'.format( username)) raise WtfStopValidation('The username or password was incorrect') else: ad_object = AD() if ad_object.login(username, password): if ad_object.check_group_membership(): friendly_username = ad_object.get_loggedin_user() display_name = ad_object.get_loggedin_user_display_name() if not models.Admins.query.filter_by(username=friendly_username, source='ldap').first(): add_ldap_user_to_db(friendly_username, display_name) admin = models.Admins.query.filter_by(username=friendly_username, source='ldap').first() if admin.otp_active: if not admin.verify_totp(two_factor): raise WtfStopValidation('The two factor authentication code is incorrect') form.admin = admin except ADException as e: raise WtfStopValidation(e.message)
def check_group_membership(self): """ Checks the group membership of the logged on user. This will return True if the user is a member of the Administrator group set in the database """ username = self.get_loggedin_user() if self.check_nested_group_membership(self.ldap_admin_group, username): return True admin_group_dn = self.get_distinguished_name(self.ldap_admin_group) # If the user was not a member of the group, check to see if the admin group is the primary group # of the user which is not included in memberOf (this is typically Domain Users) primary_group_dn = self.get_primary_group_dn_of_user(username) if admin_group_dn == primary_group_dn: return True json_logger( 'auth', username, ('The LDAP user "{0}" authenticated but the login failed because they weren\'t ' 'a PostMaster administrator').format(username)) raise ADException('The user account is not authorized to login to PostMaster')
def new_alias(): """ Creates a new alias in VirtualAliases, and returns HTTP 201 on success """ alias = VirtualAliases().from_json(request.get_json(force=True)) db.session.add(alias) try: db.session.commit() json_logger( 'audit', current_user.username, 'The alias "{0}" was created successfully'.format(alias.source)) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in new_alias: {0}'.format(str(e))) raise GenericError('The alias could not be created') finally: db.session.close() return {}, 201
def get_ldap_object(self, sam_account_name, attributes=None): """ Returns an LDAP object based on the sAMAccountName and the desired attributes """ search_filter = '(sAMAccountName={0})'.format(sam_account_name) ldap_object = self.search(search_filter, attributes)[0] # Check if a user was returned if 'dn' in ldap_object: if attributes and 'attributes' in ldap_object: for attribute in attributes: if attribute not in ldap_object['attributes']: json_logger( 'error', self.get_loggedin_user(), ('The object with account name "{0}" was found in LDAP, but the attribute "{1}" was' ' not'.format(sam_account_name, attribute))) raise ADException('There was an error searching the LDAP server. Please try again.') return ldap_object json_logger( 'error', self.get_loggedin_user(), 'The object with account name "{0}" could not be found in LDAP'.format(sam_account_name)) raise ADException('There was an error searching the LDAP server. Please try again.')
def new_user(): """ Creates a new user in VirtualUsers, and returns HTTP 201 on success """ user = VirtualUsers().from_json(request.get_json(force=True)) db.session.add(user) try: db.session.commit() json_logger( 'audit', current_user.username, 'The user "{0}" was created successfully'.format(user.email)) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in new_user: {0}'.format(str(e))) raise GenericError('The user could not be created') finally: db.session.close() return {}, 201
def delete_alias(alias_id): """ Deletes an alias by ID in VirtualAliases, and returns HTTP 204 on success """ alias = VirtualAliases.query.get_or_404(alias_id) db.session.delete(alias) try: db.session.commit() json_logger( 'audit', current_user.username, 'The alias "{0}" was deleted successfully'.format(alias.source)) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in delete_alias: {0}'.format(str(e))) raise GenericError('The alias could not be deleted') finally: db.session.close() return {}, 204
def delete_admin(admin_id): """ Deletes an admin user by ID in Admins, and returns HTTP 204 on success """ admin = Admins.query.get_or_404(admin_id) if db.session.query(Admins).count() == 1: raise GenericError("There needs to be at least one adminstrator") db.session.delete(admin) try: db.session.commit() json_logger( "audit", current_user.username, 'The administrator "{0}" was deleted successfully'.format(admin.username) ) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger("error", current_user.username, "The following error occurred in delete_admin: {0}".format(str(e))) raise GenericError("The administrator could not be deleted") finally: db.session.close() return {}, 204
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 delete_domain(domain_id): """ Deletes a domain by ID in VirtualDomains, and returns HTTP 204 on success """ domain = VirtualDomains.query.get_or_404(domain_id) db.session.delete(domain) try: db.session.commit() json_logger( 'audit', current_user.username, 'The domain "{0}" was deleted successfully'.format(domain.name)) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in delete_domain: {0}'.format(str( e))) raise GenericError('The domain could not be deleted') finally: db.session.close() return {}, 204
def new_admin(): """ Creates a new admin user in Admins, and returns HTTP 201 on success """ admin = Admins().from_json(request.get_json(force=True)) db.session.add(admin) try: db.session.commit() json_logger( 'audit', current_user.username, 'The administrator "{0}" was created successfully'.format( admin.username)) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in new_admin: {0}'.format(str(e))) raise GenericError('The admininstrator could not be created') finally: db.session.close() return {}, 201
def search(self, search_filter, attributes=None, search_scope=ldap3.SUBTREE): """ Returns LDAP objects based on the search_filter and the desired attributes """ # Check if the ldap_connection is in a logged in state if self.ldap_connection.bound: if self.ldap_connection.search( self.base_dn, search_filter, search_scope=search_scope, attributes=attributes): # Check if anything was returned if len(self.ldap_connection.response): return self.ldap_connection.response log_msg = ('The LDAP object could not be found using the search ' 'filter "{0}"'.format(search_filter)) json_logger('error', self.get_loggedin_user(), log_msg) error_msg = ('There was an error searching the LDAP server. ' 'Please try again.') raise ADException(error_msg) else: raise ADException('You must be logged into LDAP to search')
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 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: newSource = json['source'].lower() if VirtualAliases().validate_source(newSource): auditMessage = 'The alias "{0}" had their source changed to "{1}"'.format( alias.source, newSource) alias.source = newSource db.session.add(alias) elif 'destination' in json: newDestination = json['destination'].lower() if VirtualAliases().validate_destination(newDestination): auditMessage = 'The alias "{0}" had their destination changed to "{1}"'.format( alias.source, newDestination) alias.destination = newDestination 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, auditMessage) 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 update_admin(admin_id): """ Updates an admin user by ID in Admins, and returns HTTP 200 on success """ auditMessage = "" admin = Admins.query.get_or_404(admin_id) json = request.get_json(force=True) if "username" in json: newUsername = json["username"].lower() if Admins.query.filter_by(username=newUsername).first() is None: auditMessage = 'The administrator "{0}" had their username changed to "{1}"'.format( admin.username, newUsername ) admin.username = newUsername else: ValidationError("The username supplied already exists") elif "password" in json: admin.set_password(json["password"]) auditMessage = 'The administrator "{0}" had their password changed'.format(admin.username) elif "name" in json: auditMessage = '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, auditMessage) 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 check_group_membership(self): """ Checks the group membership of the logged on user. This will return True if the user is a member of the Administrator group set in the database """ username = self.get_loggedin_user() if self.check_nested_group_membership(self.ldap_admin_group, username): return True admin_group_dn = self.get_distinguished_name(self.ldap_admin_group) # If the user was not a member of the group, check to see if the admin # group is the primary group of the user which is not included in # memberOf (this is typically Domain Users) primary_group_dn = self.get_primary_group_dn_of_user(username) if admin_group_dn == primary_group_dn: return True log_msg = ('The LDAP user "{0}" authenticated but the login failed ' 'because they weren\'t a PostMaster administrator' .format(username)) json_logger('auth', username, log_msg) raise ADException( 'The user account is not authorized to login to PostMaster')
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 get_primary_group_dn_of_user(self, sam_account_name): """ Returns the distinguished name of the primary group of the user """ user = self.get_ldap_object(sam_account_name, ['primaryGroupID']) primary_group_id = str(user['attributes']['primaryGroupID']) domain_search = self.search( '(objectClass=domainDNS)', ['objectSid'], ldap3.BASE) if 'dn' in domain_search[0]: if domain_search[0] and 'attributes' in domain_search[0] \ and 'objectSid' in domain_search[0]['attributes']: domain_sid = domain_search[0]['attributes']['objectSid'] group_search_filter = \ ('(&(objectClass=group)(objectSid={0}-{1}))' .format(domain_sid, primary_group_id)) group_search = self.search(group_search_filter) if 'dn' in group_search[0]: return group_search[0]['dn'] json_logger('error', self.get_loggedin_user(), 'The objectSid of the domain could not be found') raise ADException( 'There was an error searching the LDAP server. Please try again.')
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 delete_admin(admin_id): """ Deletes an admin user by ID in Admins, and returns HTTP 204 on success """ admin = Admins.query.get_or_404(admin_id) if db.session.query(Admins).count() == 1: raise GenericError('There needs to be at least one adminstrator') db.session.delete(admin) try: db.session.commit() json_logger( 'audit', current_user.username, 'The administrator "{0}" was deleted successfully'.format( admin.username)) except ValidationError as e: raise e except Exception as e: db.session.rollback() json_logger( 'error', current_user.username, 'The following error occurred in delete_admin: {0}'.format(str(e))) raise GenericError('The administrator could not be deleted') finally: db.session.close() return {}, 204
def __init__(self): """ The constructor that initializes the ldap_connection object """ ldap_enabled = models.Configs().query.filter_by(setting='Enable LDAP Authentication').first() if ldap_enabled is not None and ldap_enabled.value == 'True': ldap_server = models.Configs().query.filter_by(setting='AD Server LDAP String').first() domain = models.Configs().query.filter_by(setting='AD Domain').first() ldap_admin_group = models.Configs().query.filter_by(setting='AD PostMaster Group').first() ldap_auth_method = models.Configs().query.filter_by(setting='LDAP Authentication Method').first() if ldap_server is not None and re.search(r'LDAP[S]?:\/\/(.*?)\:\d+', ldap_server.value, re.IGNORECASE): self.ldap_server = ldap_server.value if domain is not None and ldap_admin_group is not None: self.domain = domain.value self.ldap_admin_group = ldap_admin_group.value ad_server = ldap3.Server( ldap_server.value, allowed_referral_hosts=[('*', False)], connect_timeout=3) # Use NTLMv2 authentication so that credentials aren't set in the clear if LDAPS is not used self.ldap_connection = ldap3.Connection( ad_server, auto_referrals=False, authentication=ldap_auth_method.value.upper()) try: self.ldap_connection.open() except ldap3.core.exceptions.LDAPSocketOpenError: json_logger( 'error', 'NA', 'The LDAP server "{0}" could not be contacted'.format(self.ldap_server)) raise ADException('The LDAP server could not be contacted') else: json_logger('error', 'NA', 'The LDAP Admin Group is not configured properly') raise ADException('LDAP authentication is not properly configured') else: json_logger('error', 'NA', 'The LDAP server string is not configured or isn\'t properly formatted') raise ADException('The LDAP server could not be contacted') else: json_logger('error', 'NA', 'An LDAP authentication attempt was made but it is currently disabled') raise ADException('LDAP authentication is not enabled')