From 2a1d407dc48bbfc444a78e49baac34a344747dea Mon Sep 17 00:00:00 2001 From: Daniel Kessler Date: Thu, 6 Jun 2019 17:51:52 -0700 Subject: [PATCH] makeservices: add makexmpp script Co-authored-by: Kevin Peng --- makeservices/makexmpp | 6 + makeservices/makexmpp-real | 250 +++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100755 makeservices/makexmpp create mode 100755 makeservices/makexmpp-real diff --git a/makeservices/makexmpp b/makeservices/makexmpp new file mode 100755 index 0000000..10b0eea --- /dev/null +++ b/makeservices/makexmpp @@ -0,0 +1,6 @@ +#!/bin/bash -eu +if [[ "$(hostname)" != "tsunami" && "$(hostname)" != "dev-tsunami" ]]; then + echo -e '\033[1;31mYou must run this command on tsunami.\033[0m' + exit 1 +fi +sudo -u ocfmakexmpp /opt/share/utils/makeservices/makexmpp-real diff --git a/makeservices/makexmpp-real b/makeservices/makexmpp-real new file mode 100755 index 0000000..778f5a0 --- /dev/null +++ b/makeservices/makexmpp-real @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +Creates an XMPP account with the same name as the username of the user who +runs this program. + +To prevent the user from taking control of this program and e.g., causing the +program to dump core and reveal the Prosody admin credentials, this program +should be run under a separate account from that of the user. + +The password set for this new account is randomly generated; it is not +user-selectable. Ths is to prevent compromised user accounts from also +compromising the shell account password. Note that the XMPP server should also +prevent the user from later changing the password. + +The admin credentials are read from a config file; the path to it is stored in +the CONF_FILE global. This file must be readable only by the setuid user! The +Python configparser module is used to parse the config file, so the format is +basically the Windows INI format. + +Most of the code in this file was adapted from a SleekXMPP example program, see +https://github.com/fritzy/SleekXMPP/blob/master/examples/admin_commands.py +""" +import os +import random +import string +import sys +from configparser import ConfigParser +from textwrap import dedent + +import sleekxmpp +from ocflib.misc.mail import send_problem_report + + +CONF_FILE = '/opt/share/makexmpp/makexmpp.conf' +JID_DOMAIN = 'ocf.berkeley.edu' + +PW_LENGTH = 24 + + +def read_config(): + """Fetches the server admin JID and password from the config in + '/opt/share/makeservices'.""" + conf = ConfigParser() + conf.read(CONF_FILE) + admin_jid = conf.get('makexmpp', 'jid') + admin_pw = conf.get('makexmpp', 'passwd') + return admin_jid, admin_pw + + +def generate_password(length): + r = random.SystemRandom() + return ''.join( + r.choice(string.ascii_letters + string.digits) + for _ in range(length) + ) + + +def intro_prompt(): + print(dedent( + """ + This program will create an XMPP account, if one does not already exist. + A randomly-generated password will be generated for this account and + displayed on-screen. Please make sure you are in an environment where + nobody else will see it when it appears. + + You can always re-run this command to reset the password if you lose it. + + If you are ready to continue, type 'yes'. + Typing anything other than yes will abort this script. + """ + )) + return input('Continue? ') == 'yes' + + +class XMPPUserPasswordClient(sleekxmpp.ClientXMPP): + def __init__(self, jid, password, newuser, newpassword): + super().__init__(jid, password) + + self._newuser = newuser + self._newjid = newuser + '@' + JID_DOMAIN + self._newpassword = newpassword + + self.add_event_handler('session_start', self.start) + + def start(self, event): + """ + Process the session_start event. We first try to change the user's + password. If the account does not exist, we then create it. + + `event` is an empty dictionary. The session_start event does not + provide any additional data. + """ + + def command_error(iq, session): + self['xep_0050'].terminate_command(session) + self.disconnect() + + condition = iq['error']['condition'] + errtext = iq['error']['text'] + raise Exception('{}: {}'.format(condition, errtext)) + + def adduser_coda(iq, session): + errors = [ + note + for note in iq['command']['notes'] + if note[0] != 'info' + ] + if not errors: + # No errors mean the user was created successfully + print(dedent(""" + Your XMPP account has been created. + + For instructions on using your account, please visit + + https://www.ocf.berkeley.edu/docs/services/xmpp/ + + If you run into trouble, contact us at + + help@ocf.berkeley.edu + """)) + + self.disconnect() + else: + self.disconnect() + raise Exception(iq['command']['notes']) + + def adduser_form(iq, session): + form = iq['command']['form'] + + answers = { + 'FORM_TYPE': form['fields']['FORM_TYPE']['value'], + 'accountjid': self._newjid, + 'password': self._newpassword, + 'password-verify': self._newpassword, + } + + form['type'] = 'submit' + form['values'] = answers + + session['next'] = adduser_coda + session['payload'] = form + + self['xep_0050'].complete_command(session) + + def changepassword_coda(iq, session): + errors = [ + note + for note in iq['command']['notes'] + if note[0] != 'info' + ] + if not errors: + # No errors means the password was changed successfully + print(dedent(""" + Your XMPP account {} already exists. Its password was reset. + + For details on how to connect to XMPP, visit + + https://www.ocf.berkeley.edu/docs/services/xmpp/ + + If you run into trouble using your account, contact us at + + help@ocf.berkeley.edu + """).format(self._newjid)) + + self.disconnect() + elif errors == [('error', 'User does not exist')]: + # Account does not exist, create it + self['xep_0133'].add_user(session={ + 'next': adduser_form, + 'error': command_error + }) + else: + self.disconnect() + raise Exception(iq['command']['notes']) + + def changepassword_form(iq, session): + form = iq['command']['form'] + + answers = { + 'FORM_TYPE': form['fields']['FORM_TYPE']['value'], + 'accountjid': self._newjid, + 'password': self._newpassword, + } + + form['type'] = 'submit' + form['values'] = answers + + session['next'] = changepassword_coda + session['payload'] = form + + self['xep_0050'].complete_command(session) + + self['xep_0133'].change_user_password(session={ + 'next': changepassword_form, + 'error': command_error + }) + + +def main(): + try: + username = os.environ.get('SUDO_USER') + + if not username: + raise RuntimeError('Unable to read SUDO_USER.') + + # Read config file. + admin_jid, admin_pw = read_config() + + # Check whether the script should proceed. + if not intro_prompt(): + print('>>> Aborted by user request.') + return + + newpassword = generate_password(PW_LENGTH) + + # Initialize the XMPP connection and register the service admin plugin. + xmpp = XMPPUserPasswordClient( + admin_jid, + admin_pw, + username, + newpassword, + ) + xmpp.register_plugin('xep_0133') # Service Administration + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(reattempt=False): + xmpp.process(block=True) + else: + raise Exception('Unable to connect to XMPP server.') + + print('>>> Your XMPP account password is: {}'.format(newpassword)) + except Exception as ex: + send_problem_report(dedent( + """\ + Fatal error for user '{}' + + {}: {}\ + """ + ).format(username, ex.__class__.__name__, ex)) + print(dedent( + """ + A fatal error was encountered during program execution. + OCF staff have been notified of the problem. + """ + )) + sys.exit(1) + + +if __name__ == '__main__': + main()