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]_.
|
_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
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,40 +54,46 @@ 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
|
|
||||||
# Add the user
|
|
||||||
result = run(['sudo',
|
|
||||||
'adduser',
|
'adduser',
|
||||||
'--quiet',
|
'--quiet',
|
||||||
'--shell={}'.format(self.shell),
|
'--shell={}'.format(self.shell),
|
||||||
'--gecos="{}"'.format(self.displayname),
|
'--gecos="{}"'.format(self.displayname),
|
||||||
'--disabled-password',
|
'--disabled-password',
|
||||||
self.username,],
|
self.username,])
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create .ssh
|
# 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
|
# 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:
|
with TemporaryFile(dir="/tmp") as fp:
|
||||||
fp.write(self.generate_authorized_keys().encode('utf-8'))
|
fp.write(self.generate_authorized_keys().encode('utf-8'))
|
||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
run(['sudo',
|
_guarded_run(['sudo',
|
||||||
'--user={}'.format(self.username),
|
'--user={}'.format(self.username),
|
||||||
'/opt/bin/create_keyfile.py',
|
'/opt/bin/create_keyfile.py',
|
||||||
self.username],
|
self.username],
|
||||||
stdin=fp
|
stdin=fp,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_authorized_keys(self):
|
def generate_authorized_keys(self):
|
||||||
|
@ -108,8 +102,8 @@ class Townie(User):
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue