diff --git a/README.md b/README.md index 9e78093..1525bcc 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,27 @@ _Being an adminstrative and user-signup tool for [https://tilde.town]_. -## Features +## Features, present and future - * User signup form (✓) - * with client-side key generation (only server-side key **validation** at this point) - * Guestbook (✓) - * Helpdesk (✓) - * User account management (admin only) + * (✓) User signup form + * eventually with client-side key generation (only server-side key + **validation** at this point) + * (✓) Guestbook + * (✓) Helpdesk + * (✓) User account management * Start/stop services * Cost reporting using AWS * Status monitoring -## Libraries +## Requirements - * Django 1.10 - * Python 3.4+ + * Python 3.5+ + * PostgreSQL 9+ + +## Installation / setup + +Refer to the rather rough [serversetup.md](server setup guide). just ask +~vilmibm though if you want to set this up. ## Authors diff --git a/scripts/create_keyfile.py b/scripts/create_keyfile.py index 1ae11ed..33be19b 100755 --- a/scripts/create_keyfile.py +++ b/scripts/create_keyfile.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +"""this script allows django to add public keys for a user. it's in its own +script so that a specific command can be added to the ttadmin user's sudoers +file.""" import sys KEYFILE_PATH = '/home/{}/.ssh/authorized_keys2' diff --git a/serversetup.md b/serversetup.md index aef158e..8a87e58 100644 --- a/serversetup.md +++ b/serversetup.md @@ -5,7 +5,7 @@ * autoconf? * python3-dev * postgresql-server-dev-9.5 -* python 3.4+ +* python 3.5+ * postgresql * virtualenv * nginx @@ -14,9 +14,11 @@ * create ttadmin user * ttadmin db user (or just rely on ident..?) / database created +* copy `create_keyfile.py` from `scripts/` and put it in `/opt/bin/`. +* `chmod o+x /opt/bin/create_keyfile.py`` * add to sudoers: - ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/usr/sbin/deluser,/usr/sbin/delgroup + ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/bin/mkdir,/opt/bin/create_keyfile.py * have virtualenv with python 3.5+ ready, install tildetown-admin package into it * run django app as wsgi container through gunicorn as the ttadmin user with venv active diff --git a/setup.py b/setup.py index 68f1f0c..f1b0d30 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup setup( name='tildetown-admin', - version='1.0.0', + version='1.1.0', description='administrative webapp for tilde.town', url='https://github.com/nathanielksmith/prosaic', author='vilmibm shaksfrpease', @@ -18,6 +18,7 @@ setup( install_requires = ['Django==1.10.2', 'sshpubkeys==2.2.0', 'psycopg2==2.6.2', - 'requests==2.12.5'], + 'requests==2.12.5', + 'gunicorn==19.6.0'], include_package_data = True, ) diff --git a/ttadmin/users/models.py b/ttadmin/users/models.py index 267e7e0..308a9ce 100644 --- a/ttadmin/users/models.py +++ b/ttadmin/users/models.py @@ -1,10 +1,10 @@ import os import re -from subprocess import run, CalledProcessError, PIPE +from subprocess import run, CalledProcessError from tempfile import TemporaryFile from django.db.models import Model -from django.db.models.signals import pre_save, post_save, post_delete +from django.db.models.signals import pre_save from django.dispatch import receiver from django.contrib.auth.models import User from django.db.models import TextField, BooleanField, CharField, ForeignKey @@ -18,22 +18,10 @@ SSH_TYPE_CHOICES = ( ('ssh-dss', 'ssh-dss',), ) -KEYFILE_HEADER = """Hi! This file is automatically managed by tilde.town. You -probably shouldn't change it. If you want to add more public keys that's -totally fine: you can put them in ~/.ssh/authorized_keys""" -TMP_PATH = '/tmp/ttadmin' - -ENSURE_PRESENT = 'present' -ENSURE_ABSENT = 'absent' - -def user_in_passwd(username): - """Given a username, returns either the user's line in passwd or None. - Opens and reads passwd every time. Memoize or something if this becomes an - issue.""" - with open('/etc/passwd') as passwd: - for line in passwd: - if username == line.split(':')[0]: - return line.rstrip() +KEYFILE_HEADER = """########## GREETINGS! ########## +# Hi! This file is automatically managed by tilde.town. You +# probably shouldn't change it. If you want to add more public keys that's +# totally fine: you can put them in ~/.ssh/authorized_keys""" class Townie(User): @@ -66,50 +54,56 @@ class Townie(User): # managing concrete system state - def reconcile(self, ensure): + def create_on_disk(self): + """A VERY NOT IDEMPOTENT create function. Originally, I had ambitions + to have this be idempotent and able to incrementally update a user as + needed, but decided that was overkill for now.""" assert(self.reviewed) dot_ssh_path = '/home/{}/.ssh'.format(self.username) - if ensure == ENSURE_ABSENT: - # TODO delete - # This is manual for now because it very rarely comes up and I want - # the present case to work. - pass - if ensure == ENSURE_PRESENT: - # TODO handle rename case either with update fields or a rename action - # Add the user - result = run(['sudo', - 'adduser', - '--quiet', - '--shell={}'.format(self.shell), - '--gecos="{}"'.format(self.displayname), - '--disabled-password', - self.username,], - check=True, + _guarded_run(['sudo', + 'adduser', + '--quiet', + '--shell={}'.format(self.shell), + '--gecos="{}"'.format(self.displayname), + '--disabled-password', + self.username,]) + + # Create .ssh + _guarded_run(['sudo', + '--user={}'.format(self.username), + 'mkdir', + dot_ssh_path]) + + # Write out authorized_keys file + # Why is this a call out to a python script? There's no secure way with + # sudoers to allow this code to write to a file; if this code was to be + # compromised, the ability to write arbitrary files with sudo is a TKO. + # By putting the ssh key file creation into its own script, we can just + # give sudo access for that one command to this code. + # + # We could put the other stuff from here into that script and then only + # grant sudo for the script, but then we're moving code out of this + # virtual-env contained, maintainable thing into a script. it's my + # preference to have the script be as minimal as possible. + with TemporaryFile(dir="/tmp") as fp: + fp.write(self.generate_authorized_keys().encode('utf-8')) + fp.seek(0) + _guarded_run(['sudo', + '--user={}'.format(self.username), + '/opt/bin/create_keyfile.py', + self.username], + stdin=fp, ) - # Create .ssh - run(['sudo', '--user={}'.format(self.username), 'mkdir', dot_ssh_path]) - - # Write out authorized_keys file - with TemporaryFile(dir="/tmp") as fp: - fp.write(self.generate_authorized_keys().encode('utf-8')) - fp.seek(0) - run(['sudo', - '--user={}'.format(self.username), - '/opt/bin/create_keyfile.py', - self.username], - stdin=fp - ) - def generate_authorized_keys(self): """returns a string suitable for writing out to an authorized_keys file""" content = KEYFILE_HEADER pubkeys = Pubkey.objects.filter(townie=self) for key in pubkeys: - if key.startswith('ssh-'): - content += '\n {}'.format(key.key) + if key.key.startswith('ssh-'): + content += '\n{}'.format(key.key) else: content += '\n{} {}'.format(key.key_type, key.key) @@ -133,21 +127,23 @@ def on_townie_pre_save(sender, instance, **kwargs): return if not existing[0].reviewed and instance.reviewed == True: + instance.create_on_disk() instance.send_welcome_email() -@receiver(post_save, sender=Townie) -def post_save_reconcile(sender, instance, **kwargs): - if not instance.reviewed: - return - instance.reconcile(ENSURE_PRESENT) +def _guarded_run(cmd_args, **run_args): + try: + run(cmd_args, + check=True, + **run_args) + except CalledProcessError as e: + Ticket.objects.create(name='system', + email='root@tilde.town', + issue_type='other', + issue_text='error while running {}: {}'.format( + cmd_args, e)) -@receiver(post_delete, sender=Townie) -def post_delete_reconcile(sender, instance, **kwargs): - if not instance.reviewed: - # TODO should i actually do this check? - # I might want to make it such that users can never become un-reviewed. - return - instance.reconcile(ENSURE_ABSENT) + +# development notes! # what the puppet module does: # * creates user account