Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

makeservices: add makexmpp script #112

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a more specific subclass of Exception?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a huge deal, since this is only seen by the emailed problem report in the root@ mailing list. We'll see the line number which is more important anyways.


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.')
dkess marked this conversation as resolved.
Show resolved Hide resolved

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