clean up user creation automation
parent
55562572ae
commit
d0c79a5159
24
README.md
24
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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
5
setup.py
5
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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue