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]_.
## 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

View File

@ -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'

View File

@ -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

View File

@ -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,
)

View File

@ -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,40 +54,46 @@ 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',
_guarded_run(['sudo',
'adduser',
'--quiet',
'--shell={}'.format(self.shell),
'--gecos="{}"'.format(self.displayname),
'--disabled-password',
self.username,],
check=True,
)
self.username,])
# Create .ssh
run(['sudo', '--user={}'.format(self.username), 'mkdir', dot_ssh_path])
_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)
run(['sudo',
_guarded_run(['sudo',
'--user={}'.format(self.username),
'/opt/bin/create_keyfile.py',
self.username],
stdin=fp
stdin=fp,
)
def generate_authorized_keys(self):
@ -108,8 +102,8 @@ class Townie(User):
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