clean up user creation automation

pull/11/head
nathaniel smith 2017-02-16 00:49:26 -08:00
parent 55562572ae
commit d0c79a5159
5 changed files with 84 additions and 76 deletions

View File

@ -2,21 +2,27 @@
_Being an adminstrative and user-signup tool for [https://tilde.town]_. _Being an adminstrative and user-signup tool for [https://tilde.town]_.
## Features ## Features, present and future
* User signup form (✓) * (✓) User signup form
* with client-side key generation (only server-side key **validation** at this point) * eventually with client-side key generation (only server-side key
* Guestbook (✓) **validation** at this point)
* Helpdesk (✓) * (✓) Guestbook
* User account management (admin only) * (✓) Helpdesk
* (✓) User account management
* Start/stop services * Start/stop services
* Cost reporting using AWS * Cost reporting using AWS
* Status monitoring * Status monitoring
## Libraries ## Requirements
* Django 1.10 * Python 3.5+
* Python 3.4+ * 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 ## Authors

View File

@ -1,4 +1,7 @@
#!/usr/bin/env python3 #!/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 import sys
KEYFILE_PATH = '/home/{}/.ssh/authorized_keys2' KEYFILE_PATH = '/home/{}/.ssh/authorized_keys2'

View File

@ -5,7 +5,7 @@
* autoconf? * autoconf?
* python3-dev * python3-dev
* postgresql-server-dev-9.5 * postgresql-server-dev-9.5
* python 3.4+ * python 3.5+
* postgresql * postgresql
* virtualenv * virtualenv
* nginx * nginx
@ -14,9 +14,11 @@
* create ttadmin user * create ttadmin user
* ttadmin db user (or just rely on ident..?) / database created * 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: * 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 * 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 * run django app as wsgi container through gunicorn as the ttadmin user with venv active

View File

@ -4,7 +4,7 @@ from setuptools import setup
setup( setup(
name='tildetown-admin', name='tildetown-admin',
version='1.0.0', version='1.1.0',
description='administrative webapp for tilde.town', description='administrative webapp for tilde.town',
url='https://github.com/nathanielksmith/prosaic', url='https://github.com/nathanielksmith/prosaic',
author='vilmibm shaksfrpease', author='vilmibm shaksfrpease',
@ -18,6 +18,7 @@ setup(
install_requires = ['Django==1.10.2', install_requires = ['Django==1.10.2',
'sshpubkeys==2.2.0', 'sshpubkeys==2.2.0',
'psycopg2==2.6.2', 'psycopg2==2.6.2',
'requests==2.12.5'], 'requests==2.12.5',
'gunicorn==19.6.0'],
include_package_data = True, include_package_data = True,
) )

View File

@ -1,10 +1,10 @@
import os import os
import re import re
from subprocess import run, CalledProcessError, PIPE from subprocess import run, CalledProcessError
from tempfile import TemporaryFile from tempfile import TemporaryFile
from django.db.models import Model 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.dispatch import receiver
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import TextField, BooleanField, CharField, ForeignKey from django.db.models import TextField, BooleanField, CharField, ForeignKey
@ -18,22 +18,10 @@ SSH_TYPE_CHOICES = (
('ssh-dss', 'ssh-dss',), ('ssh-dss', 'ssh-dss',),
) )
KEYFILE_HEADER = """Hi! This file is automatically managed by tilde.town. You KEYFILE_HEADER = """########## GREETINGS! ##########
probably shouldn't change it. If you want to add more public keys that's # Hi! This file is automatically managed by tilde.town. You
totally fine: you can put them in ~/.ssh/authorized_keys""" # probably shouldn't change it. If you want to add more public keys that's
TMP_PATH = '/tmp/ttadmin' # totally fine: you can put them in ~/.ssh/authorized_keys"""
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()
class Townie(User): class Townie(User):
@ -66,50 +54,56 @@ class Townie(User):
# managing concrete system state # 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) assert(self.reviewed)
dot_ssh_path = '/home/{}/.ssh'.format(self.username) 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: _guarded_run(['sudo',
# TODO handle rename case either with update fields or a rename action 'adduser',
# Add the user '--quiet',
result = run(['sudo', '--shell={}'.format(self.shell),
'adduser', '--gecos="{}"'.format(self.displayname),
'--quiet', '--disabled-password',
'--shell={}'.format(self.shell), self.username,])
'--gecos="{}"'.format(self.displayname),
'--disabled-password', # Create .ssh
self.username,], _guarded_run(['sudo',
check=True, '--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): def generate_authorized_keys(self):
"""returns a string suitable for writing out to an authorized_keys """returns a string suitable for writing out to an authorized_keys
file""" file"""
content = KEYFILE_HEADER content = KEYFILE_HEADER
pubkeys = Pubkey.objects.filter(townie=self) pubkeys = Pubkey.objects.filter(townie=self)
for key in pubkeys: for key in pubkeys:
if key.startswith('ssh-'): if key.key.startswith('ssh-'):
content += '\n {}'.format(key.key) content += '\n{}'.format(key.key)
else: else:
content += '\n{} {}'.format(key.key_type, key.key) content += '\n{} {}'.format(key.key_type, key.key)
@ -133,21 +127,23 @@ def on_townie_pre_save(sender, instance, **kwargs):
return return
if not existing[0].reviewed and instance.reviewed == True: if not existing[0].reviewed and instance.reviewed == True:
instance.create_on_disk()
instance.send_welcome_email() instance.send_welcome_email()
@receiver(post_save, sender=Townie) def _guarded_run(cmd_args, **run_args):
def post_save_reconcile(sender, instance, **kwargs): try:
if not instance.reviewed: run(cmd_args,
return check=True,
instance.reconcile(ENSURE_PRESENT) **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): # development notes!
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)
# what the puppet module does: # what the puppet module does:
# * creates user account # * creates user account