Skip to content

Commit

Permalink
makeservices: add makexmpp script
Browse files Browse the repository at this point in the history
  • Loading branch information
dkess committed Jun 7, 2019
1 parent 9e27fc2 commit 8e0385c
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 0 deletions.
6 changes: 6 additions & 0 deletions makeservices/makexmpp
Original file line number Diff line number Diff line change
@@ -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
250 changes: 250 additions & 0 deletions makeservices/makexmpp-real
Original file line number Diff line number Diff line change
@@ -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 an unprivileged account.
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.readfp(open(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 XMPPUserPassword(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, newuser, newpassword):
sleekxmpp.ClientXMPP.__init__(self, 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 = XMPPUserPassword(
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()

0 comments on commit 8e0385c

Please sign in to comment.