From 55562572ae66e7f7d8ccd83bdba455b0eb7b2d27 Mon Sep 17 00:00:00 2001 From: nathaniel smith Date: Wed, 15 Feb 2017 23:25:39 -0800 Subject: [PATCH] oof, get user creation working --- scripts/abc | 4 ++ scripts/create_keyfile.py | 14 ++++ ttadmin/users/models.py | 133 +++++++++++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 scripts/abc create mode 100755 scripts/create_keyfile.py diff --git a/scripts/abc b/scripts/abc new file mode 100644 index 0000000..6f84751 --- /dev/null +++ b/scripts/abc @@ -0,0 +1,4 @@ +foobar +some stuff +yeahh +hm diff --git a/scripts/create_keyfile.py b/scripts/create_keyfile.py new file mode 100755 index 0000000..1ae11ed --- /dev/null +++ b/scripts/create_keyfile.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys + +KEYFILE_PATH = '/home/{}/.ssh/authorized_keys2' + + +def main(argv): + username = argv[1] + with open(KEYFILE_PATH.format(username), 'w') as f: + f.write(sys.stdin.read()) + + +if __name__ == '__main__': + exit(main(sys.argv)) diff --git a/ttadmin/users/models.py b/ttadmin/users/models.py index af1b6cc..267e7e0 100644 --- a/ttadmin/users/models.py +++ b/ttadmin/users/models.py @@ -1,7 +1,10 @@ +import os import re +from subprocess import run, CalledProcessError, PIPE +from tempfile import TemporaryFile from django.db.models import Model -from django.db.models.signals import pre_save +from django.db.models.signals import pre_save, post_save, post_delete from django.dispatch import receiver from django.contrib.auth.models import User from django.db.models import TextField, BooleanField, CharField, ForeignKey @@ -15,6 +18,23 @@ 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() + class Townie(User): """Both an almost normal Django User as well as an abstraction over a @@ -44,6 +64,58 @@ class Townie(User): self.username, self.email)) + # managing concrete system state + + def reconcile(self, ensure): + 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, + ) + + # 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) + else: + content += '\n{} {}'.format(key.key_type, key.key) + + return content + + class Pubkey(Model): key_type = CharField(max_length=50, blank=False, @@ -62,3 +134,62 @@ def on_townie_pre_save(sender, instance, **kwargs): if not existing[0].reviewed and instance.reviewed == True: 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) + +@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) + +# what the puppet module does: +# * creates user account +# * creates home directory +# * creates authorized_keys2 +# * adds user to group 'tilde' (why?) +# * sets shell +# * creates .ssh directory +# * creates .irssi directory +# * creates templatized .irssi config file (irssi isn't even default anymore...) +# * creates hardcoded .twurlrc file (why not skel?) +# +# some of this stuff is pointless and the actually required stuff is: +# * create user account (useradd) +# * create home dir (useradd) +# * create .ssh/authorized_keys2 (need functions for this +# * set shell (chsh) +# * create .twurlrc (just use /etc/skel) + +# other things to consider: +# * what happens when a user wants their name changed? +# * it looks like usermod -l and a mv of the home dir can change a user's username. +# * would hook this into the pre_save signal to note a username change +# * what happens when a user is marked as not reviewed? +# * does this signal user deletion? Or does literal Townie deletion signal +# "needs to be removed from disk"? I think it makes the most sense for the +# latter to imply full user deletion. +# * I honestly can't even think of a reason to revert a user to "not reviewed" +# and perhaps it's best to just not make that possible. for now, though, I +# think I can ignore it. +# * what happens when a user needs to be banned? +# * the Townie should be deleted via post_delete signal +# * what are things about a user that might change in django and require changes on disk? +# * username +# * displayname (only if i start using this?) +# * ssh key +# +# how should this code be structured? +# * within the Townie model, hardcoded +# * outside the Townie model, procedurally +# * within an abstract class +# +# for now my gut says to implement stuff hardcoded in the Townie class but with +# an eye towards generalizing the pattern in some base class for other +# resources as needed.