Source code for accre.bots.password_resetter

"""
Robot to be run periodically from cron and handle RT user password
reset tickets that come from the website. This semi-automates the steps
of changing the password to a temporary generated password in LDAP and
emailing the user about the change.

User intervention is required to confirm that the ticket is valid
before going ahead with the reset.

Tickets are identified as password reset requests if the first comment
contains at least one noun and at least one verb from the lists in this
module, or if a further comment includes the special token
"account bot reset password". Administrators can mark unidentified tickets
by adding that text in a comment.
"""
from collections import namedtuple
import re

import urllib3

from accre.account_management import VUNetIDValidator, ClusterPasswordResetter
from accre.config import get_config
from accre.database import VandyAdminDBClient
from accre.ldap import ACCRELDAP
from accre.logger import get_logger
from accre.rt_requests import Requestor


CONFIG = get_config()
LOGGER = get_logger()

#: Password reset nouns for ticket identification
PW_RESET_NOUNS = {
    'password', 'passphrase', 'login', 'passwd', 'creds', 'credentials'
}
#: Password reset verbs for ticket identification
PW_RESET_VERBS = {
    'change', 'forgot', 'forgotten', 'reset', 'lost', 'expired', 'expire'
}

NOPUNC = re.compile(r'[^\w\s]')

PWRequest = namedtuple('PWRequest', ['ticket', 'vunetid', 'email', 'priority'])


[docs]def run_password_resetter(): """ CLI endpoint to run the password resetter, searching through all RT tickets and performing appropriate steps for handling the request. """ # Suppress certificate warnings from RT (TODO: fix the cert!) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) pwr = PasswordResetter() pwr.run()
[docs]class PasswordResetter: """ Object to handle password resets by calling the ``run`` method. """ def __init__(self): self.rt = Requestor() self.dbclient = VandyAdminDBClient()
[docs] def run(self): """ Run the password resetter on all identified requests. """ for request in self._get_open_requests(): if request.priority == '0': self._get_approval(request) elif request.priority == '2': self._perform_reset(request)
def _get_open_requests(self): """ Identify and retrieve all tickets that are believed to be password reset requests. Returns a list of PWRequest objects for the identified tickets. """ queue = CONFIG['account-management']['rt-reset-queue'] tids_subjects = self.rt.search_tickets( "(Status='new' or Status='open') " "and Queue='{0}' and Owner='nobody'".format(queue) ) LOGGER.info( "{0} unowned open tickets found in the '{1}' queue." .format(len(tids_subjects), queue) ) users = [] for tid, subject in tids_subjects.items(): prediction = False try: # Make sure this originated from the helpdesk by checking for # account bot json content first, skip ticket if missing tinfo = self.rt.ticket_account_bot_json(tid) if not tinfo: continue # check the subject text next prediction = _check_for_reset(subject) # inspect the first comment next if not prediction: first_history = self.rt.ticket_first_history_text(tid) prediction = _check_for_reset(first_history) # finally, look for instructions to the robot if not prediction: body = self.rt.ticket_histories_text(tid) words = ','.join( [NOPUNC.sub('',s).lower() for s in body.split()] ) prediction = 'account,bot,reset,password' in words if prediction: priority = self.rt.ticket_properties(tid).get('Priority') users.append(PWRequest( ticket=tid, priority=priority, vunetid=tinfo.get('vunetid', ''), email=tinfo.get('email', '') )) except Exception: LOGGER.exception(f'Failed to interpret ticket {tid}') return users def _get_approval(self, request): """ Get approval from admin for this request: check VUNetID and email for validity and send comment to ticket with explanation. """ user = request.vunetid errors = [] if not self.dbclient.user_exists(user): errors.append( 'User {0} does not exist in the database'.format(user) ) elif not self.dbclient.user_info(user)['active']: errors.append( 'User {0} is not an active user in the database'.format(user) ) with ACCRELDAP() as lclient: if not lclient.exists(user): errors.append( 'User {0} is not in ACCRE LDAP'.format(user) ) with VUNetIDValidator() as vclient: if not vclient.exists(user) or vclient.is_locked(user): errors.append( 'User {0} has a locked or missing VUNetID'.format(user) ) print(vclient.exists(user)) if vclient.exists(user): vandy_email = vclient.info(user)['vanderbilt_email'] if vandy_email != request.email: errors.append( 'User {0} requested email {1} for reset, but ' 'their VUNetID associated email is {2}.' .format(user, request.email, vandy_email) ) msg = ( 'This ticket was identified as a password reset ticket and ' 'a password reset will be attempted if the ticket remains ' 'unowned and the priority is set to 2. \n\n' ) if errors: LOGGER.warning( 'User {0} in password request for ticket {1} failed ' 'validation, errors: {2}' .format(user, request.ticket, errors) ) msg += ( 'User {0} in password request for ticket {1} failed ' 'validation, the following errors occurred: \n\n{2}' .format(user, request.ticket, '\n '.join(errors)) ) msg += ( ' \n\n If you are sure that this request is correct, ' 'even thought this failure has occurred, then set priority ' 'to 2 to continue with the reset.' ) self.rt.ticket_add_comment(request.ticket, msg) self.rt.ticket_change_priority(request.ticket, 99) return ok_msg = ( "User {0} found in LDAP and email address {1} verified." .format(user, request.email) ) LOGGER.info(ok_msg) msg += ok_msg msg += ( ' \n\n If you are sure that this request is correct, ' 'then set priority to 2 to continue with the reset.' ) self.rt.ticket_add_comment(request.ticket, msg) self.rt.ticket_change_priority(request.ticket, 1) def _perform_reset(self, request): """ Reset the user password and send email. """ try: cpr = ClusterPasswordResetter( vunetid=request.vunetid, ticket=request.ticket, email=request.email, ) cpr.reset_cluster_password() except Exception as e: LOGGER.exception( 'Resetting password for user {0} failed.' .format(request.vunetid, request.ticket) ) msg = ( 'Failed to reset password for user {0}. The following ' 'exception occurred, see auditor system log for further ' 'information: \n\n{1}'.format(request.vunetid, str(e)) ) self.rt.ticket_add_comment(request.ticket, msg) self.rt.ticket_change_priority(request.ticket, 99) return msg = 'Password reset for user {0}.'.format(request.vunetid) LOGGER.info(msg) self.rt.ticket_add_comment(request.ticket, msg) self.rt.ticket_change_priority(request.ticket, 3)
def _check_for_reset(text): """ Check text for a reset request """ content = [NOPUNC.sub('',s).lower() for s in text.split()] has_verb = any(word in PW_RESET_VERBS for word in content) has_noun = any(word in PW_RESET_NOUNS for word in content) if has_verb and has_noun: return True return False