Compare commits

..

No commits in common. "master" and "add-key-for-user" have entirely different histories.

35 changed files with 120 additions and 650 deletions

8
.gitignore vendored
View File

@ -3,11 +3,3 @@
__pycache__
*.pyc
*.egg-info
.bash_history
.viminfo
ttadmin/settings_live.py
ttadmin/static
venv/
build/
dist/

View File

@ -10,14 +10,13 @@ _Being an adminstrative tool written in Django for <https://tilde.town>_.
* [x] Helpdesk
* [x] User account management
* [ ] Start/stop services
* [ ] Cost reporting from DO
* [ ] Cost reporting from AWS
* [ ] Status monitoring
## Requirements
* Python 3.5+
* PostgreSQL 9+
* Ubuntu or Debian
## Installation / setup

0
scripts/create_keyfile.py 100755 → 100644
View File

View File

@ -1,56 +0,0 @@
#!/usr/bin/env python3
"""This script wraps the usermod command to allow user account renames via
sudoers."""
import os
import sys
import subprocess
def rename_user(old_username, new_username):
"""Given an old and a new username, renames user on disk with usermod.
Raises if the usermod call fails."""
args = [
'pkill',
'-u',
old_username]
subprocess.run(args, check=False)
# Rename user
args = [
'usermod',
'-l',
new_username,
'-m',
'-d',
os.path.join('/home', new_username),
old_username
]
subprocess.run(args, check=True)
# Rename their group
args = [
'groupmod',
'-n',
new_username,
old_username
]
subprocess.run(args, check=True)
def main(argv):
if len(argv) < 3:
print('[rename_user] Too few arguments passed.', file=sys.stderr)
return 1
try:
rename_user(argv[1], argv[2])
except subprocess.CalledProcessError as e:
print('[rename_user] {}'.format(e), file=sys.stderr)
return 2
return 0
if __name__ == '__main__':
exit(main(sys.argv))

View File

@ -1,12 +0,0 @@
#!/bin/bash
PORT=8888
INSTALL_ROOT=/town/src/tildetown-admin
APP_ROOT=$INSTALL_ROOT/ttadmin
VENV=/town/venvs/ttadmin
source $VENV/bin/activate
export DJANGO_SETTINGS_MODULE=settings_live
cd $APP_ROOT
gunicorn -t120 -b0.0.0.0:$PORT ttadmin.wsgi

View File

@ -15,13 +15,10 @@
* 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/`.
* copy `rename_user.py` from `scripts/` and put it in `/tilde/bin/`.
* `chmod u+x /opt/bin/create_keyfile.py``
* `chmod o+x /opt/bin/create_keyfile.py``
* add to sudoers:
```
ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/bin/mkdir,/opt/bin/create_keyfile.py,/tilde/bin/rename_user.py
```
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

@ -15,11 +15,12 @@ setup(
'License :: OSI Approved :: Affero GNU General Public License v3 (AGPLv3)',
],
packages=['ttadmin'],
install_requires = ['Django==1.11.29',
install_requires = ['Django==1.10.2',
'sshpubkeys==2.2.0',
'psycopg2-binary==2.8.5',
'psycopg2-binary==2.7.4',
'requests==2.12.5',
'gunicorn==19.6.0',
'Mastodon.py==1.4.5',
'tweepy==3.7.0'],
'Mastodon.py==1.1.1',
'tweepy==3.5.0'],
include_package_data = True,
)

View File

@ -4,15 +4,15 @@ from random import shuffle
from django.core.exceptions import ValidationError
from django.forms import ChoiceField
CAPTCHA_CHOICES = [('number', 'zorp borp'),
('hey', 'quop bop'),
('never', 'NO, I AM NOT A ROBOT'),
('eleven', 'crackle zop'),
('twelve', '*rusty screech*'),
('eighty', 'mother, give me legs')]
CAPTCHA_CHOICES = [('two', 'zorp borp'),
('three', 'quop bop'),
('four', 'NO, I AM NOT A ROBOT'),
('five', 'crackle zop'),
('six', '*rusty screech*'),
('seven', 'mother, give me legs')]
shuffle(CAPTCHA_CHOICES)
CAPTCHA_CHOICES.insert(0, ('one', 'beep boop'),)
NOT_A_ROBOT = 'never'
NOT_A_ROBOT = 'four'
def validate_captcha(captcha):
if captcha != NOT_A_ROBOT:

View File

@ -1,26 +1,32 @@
import logging
from smtplib import SMTP_SSL, SMTPException
from email.message import EmailMessage
import requests
from django.conf import settings
logger = logging.getLogger()
FROM='root@tilde.town'
def send_email(to, body, subject='a message from tilde.town'):
"""Sends an email using external SMTP. Logs on failure."""
em = EmailMessage()
em['Subject'] = subject
em['From'] = 'root@tilde.town'
em['To'] = to
em.set_content(body)
try:
with SMTP_SSL(port=settings.SMTP_PORT, host=settings.SMTP_HOST) as smtp:
smtp.login('root@tilde.town', settings.SMTP_PASSWORD)
smtp.send_message(em)
smtp.quit()
except SMTPException as e:
logger.error(f'failed to send email "{subject}" to {to}: {e}')
return False
def send_email(to, body, subject='a message from tilde.town', frum=FROM,):
"""Sends an email using mailgun. Logs on failure."""
response = requests.post(
settings.MAILGUN_URL,
auth=('api', settings.MAILGUN_KEY),
data={
'from': frum,
'to': to,
'subject': subject,
'text': body
}
)
return True
success = response.status_code == 200
if not success:
logger.error('{}: failed to send email "{}" to {}'.format(
response.status_code,
subject,
to))
return success

View File

@ -20,7 +20,6 @@
</head>
<body>
<h1>tilde.town guestbook</h1>
<p><em>don't try to post urls. it won't work.</em></p>
<marquee>~*~*~*~*say hello*~*~*~*~</marquee>
<form class="tilde" action="{% url 'guestbook:guestbook' %}" method="post">
{% csrf_token %}

View File

@ -1,5 +1,3 @@
import re
from django.shortcuts import redirect
from django.views.generic import TemplateView
from django.views.generic.edit import FormView
@ -7,8 +5,6 @@ from django.views.generic.edit import FormView
from .forms import GuestbookForm
from .models import GuestbookMessage
SUSPICIOUS_RE = re.compile(r'https?://')
class GuestbookView(FormView):
form_class = GuestbookForm
@ -21,7 +17,5 @@ class GuestbookView(FormView):
def form_valid(self, form):
del form.cleaned_data['captcha']
if SUSPICIOUS_RE.search(form.cleaned_data['msg']) != None:
return redirect('guestbook:guestbook')
t = GuestbookMessage.objects.create(**form.cleaned_data)
return redirect('guestbook:guestbook')

View File

@ -1,44 +1,7 @@
from django.contrib import admin
from django.forms import ModelForm
from .models import Ticket, Note
class ImmutableNoteInline(admin.TabularInline):
model = Note
extra = 1
max_num = 0
fields = ('author', 'created', 'body')
readonly_fields = ('author', 'created', 'body')
can_delete = False
ordering = ('created',)
class NewNoteInline(admin.StackedInline):
model = Note
extra = 0
fields = ('body',)
def get_queryset(self, request):
queryset = super().get_queryset(request)
return queryset.none()
from .models import Ticket
@admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin):
inlines = [ImmutableNoteInline, NewNoteInline]
readonly_fields = ('submitted', 'issue_type')
list_display = ('submitted', 'issue_status', 'assigned', 'issue_type', 'name', 'email',)
list_filter = ('issue_status', 'issue_type', 'assigned')
fields = ('submitted', 'name', 'email', 'assigned', 'issue_status', 'issue_type', 'issue_text')
def save_related(self, request, form, formsets, change):
# THIS IS EXTREMELY BOOTLEG AND MAY BREAK IF MORE INLINES ARE ADDED TO THIS ADMIN.
for formset in formsets:
if len(formset.forms) == 1:
# It's probably the add new note form (i hope).
note_form = formset.forms[0]
note_form.instance.author = request.user
note_form.instance.save()
note_form.save(commit=False)
note_form.save_m2m()
return super().save_related(request, form, formsets, change)
list_display = ('issue_status', 'issue_type', 'name', 'email')
fields = ('name', 'email', 'issue_status', 'issue_type', 'issue_text')

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-01-19 18:28
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('help', '0003_auto_20171110_2323'),
]
operations = [
migrations.AddField(
model_name='ticket',
name='submitted',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-16 02:43
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0015_remove_townie_reviewed'),
('help', '0004_ticket_submitted'),
]
operations = [
migrations.CreateModel(
name='Note',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('body', models.TextField()),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Townie')),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='help.Ticket')),
],
),
]

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-16 02:58
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('help', '0005_note'),
]
operations = [
migrations.AlterField(
model_name='note',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-18 22:21
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('help', '0006_auto_20190716_0258'),
]
operations = [
migrations.AddField(
model_name='ticket',
name='assigned',
field=models.ForeignKey(help_text='Assign this ticket to an admin or unassign it.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-18 22:23
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('help', '0007_ticket_assigned'),
]
operations = [
migrations.AlterField(
model_name='ticket',
name='assigned',
field=models.ForeignKey(blank=True, help_text='Assign this ticket to an admin or unassign it.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,5 +1,4 @@
from django.contrib.auth.models import User
from django.db.models import Model, TextField, EmailField, CharField, DateTimeField, ForeignKey
from django.db.models import Model, TextField, EmailField, CharField
ISSUE_TYPE_CHOICES = (
('logging_in', 'help logging in'),
@ -19,7 +18,6 @@ ISSUE_STATUS_CHOICES = (
class Ticket(Model):
submitted = DateTimeField(auto_now_add=True)
name = CharField(blank=False, null=False, max_length=100)
email = EmailField(blank=False, null=False)
issue_type = CharField(choices=ISSUE_TYPE_CHOICES,
@ -32,17 +30,6 @@ class Ticket(Model):
null=False,
max_length=50,
default=ISSUE_STATUS_CHOICES[0][0])
assigned = ForeignKey(User, blank=True, null=True, help_text="Assign this ticket to an admin or unassign it.")
def __str__(self):
return '{} from {}'.format(self.issue_type, self.name)
class Note(Model):
created = DateTimeField(auto_now_add=True)
body = TextField(blank=False, null=False)
author = ForeignKey(User)
ticket = ForeignKey(Ticket)
def __str__(self):
return "admin note"

View File

@ -6,7 +6,7 @@ To run this For Real, you'll want to:
* set a different SECRET_KEY
* change the password for the database or delete the password and use ident
* change DEBUG to False
* set smtp password
* set mailgun api info
"""
import os
@ -101,9 +101,8 @@ STATIC_URL = '/static/'
# Not used during local development, but used in staging+live environments
STATIC_ROOT = 'static'
SMTP_PORT=465
SMTP_HOST="smtp.zoho.com"
SMTP_PASSWORD="OVERWRITE THIS"
MAILGUN_URL = "OVERWRITE THIS"
MAILGUN_KEY = "OVERWRITE THIS"
# Mastodon credentials
MASTO_CLIENT_ID = "OVERWRITE THIS"

View File

@ -2,8 +2,8 @@ from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
# url(r'^help/', include('help.urls')),
url(r'^help/', include('help.urls')),
url(r'^users/', include('users.urls')),
# url(r'^guestbook/', include('guestbook.urls')),
url(r'^guestbook/', include('guestbook.urls')),
url(r'^admin/', admin.site.urls),
]

View File

@ -9,27 +9,19 @@ class PubkeyInline(admin.TabularInline):
model = Pubkey
extra = 1
def bulk_accept(madmin, req, qs):
def bulk_review(madmin, req, qs):
for townie in qs:
townie.state = Townie.ACCEPTED
townie.reviewed = True
townie.save()
post_users_to_social(qs)
bulk_accept.short_description = 'mark selected townies as accepted'
def bulk_reject(madmin, req, qs):
for townie in qs:
townie.state = Townie.REJECTED
townie.save()
bulk_reject.short_description = 'mark selected townies as rejected'
bulk_review.short_description = 'mark selected townies as reviewed'
@admin.register(Townie)
class TownieAdmin(admin.ModelAdmin):
inlines = [PubkeyInline]
list_display = ('username', 'state', 'email')
readonly_fields = ('reasons', 'plans', 'socials')
ordering = ('state',)
list_display = ('username', 'reviewed', 'email')
ordering = ('reviewed',)
exclude = ('first_name', 'last_name', 'password', 'groups', 'user_permissions', 'last_login', 'is_staff', 'is_active', 'is_superuser')
actions = (bulk_accept, bulk_reject,)
actions = (bulk_review,)
search_fields = ('username', 'email', 'displayname')

View File

@ -12,9 +12,9 @@ submission_throttle = {}
throttle_submission = throttler(submission_throttle)
USERNAME_RE = re.compile(r'^[a-z][a-z0-9_]+$')
USERNAME_MIN_LENGTH = 2
DISPLAY_NAME_RE = re.compile(r"^[a-zA-Z0-9_\-']+$")
USERNAME_RE = re.compile(r'[a-z][a-z0-9_]+')
USERNAME_MIN_LENGTH = 3
DISPLAY_NAME_RE = re.compile(r"[a-zA-Z0-9_\-']+")
DISPLAY_MIN_LENGTH = 2
@ -23,7 +23,7 @@ def validate_username(username):
raise ValidationError('Username too short.')
if not USERNAME_RE.match(username):
raise ValidationError('Username must be all lowercase, start with a letter, and only use the _ special character')
duplicate = Townie.objects.filter(username=username).exclude(state=Townie.REJECTED).count()
duplicate = Townie.objects.filter(username=username).count()
if duplicate > 0:
raise ValidationError('Username already in use :(')
@ -34,7 +34,6 @@ def validate_displayname(display_name):
if not DISPLAY_NAME_RE.match(display_name):
raise ValidationError("Valid characters: a-z, A-Z, 0-9, -, _, and '.")
def validate_pubkey(pubkey):
# TODO see if I can get the type out
key = ssh.SSHKey(pubkey, strict_mode=False, skip_option_parsing=True)
@ -53,63 +52,32 @@ class TownieForm(Form):
validators=(validate_username,),
help_text='lowercase and no spaces. underscore ok',
label='username')
email = EmailField(
help_text='only used to message you about your account and nothing else.',
label='e-mail')
displayname = CharField(
validators=(validate_displayname,),
help_text='100% optional. pseudonyms welcome.',
label='display name',
required=False)
referral = CharField(
required=False,
label='did a townie refer you? put their handle here.',
help_text="this is optional and just helps us when reviewing your application.")
reasons = CharField(
widget=Textarea,
required=True,
label='what interests you about tilde.town?',
help_text="""
What about this intentional community intrigues you and makes you want to be a part of it?
""".strip())
plans = CharField(
widget=Textarea,
required=True,
label='what sort of things do you want to do on tilde.town?',
help_text="""
Do you want to socialize? Make something? Learn stuff?
""".strip())
socials = CharField(
widget=Textarea,
required=False,
label='where else are you online?',
help_text="""Optional, but if you're comfortable sharing with us some links to other online
spaces you're in (like twitter, mastodon, neocities, or whatever) we'd love to get to know
you when reviewing your application.
""".strip())
label='what interests you about tilde.town?',
help_text='This is a totally optional place for you to tell us what excites you about getting a ~ account. This is mainly just so we can all feel warm fuzzies.')
captcha = CaptchaField()
pubkey = CharField(
widget=Textarea,
validators=(validate_pubkey,),
label='SSH public key',
help_text='if this is not a thing you are familiar with, that\'s okay! you can make one <a href="/users/keymachine">here</a> or read <a href="https://tilde.town/wiki/getting-started/ssh.html">our guide</a> to learn more.')
help_text='if this is not a thing you are familiar with, that\'s okay! you can make one <a href="/users/keymachine">here</a> or read <a href="https://tilde.town/~wiki/ssh.html">our guide</a> to learn more.')
pubkey_type = ChoiceField(
choices=SSH_TYPE_CHOICES,
label='SSH public key type',
help_text="unless you know what you're doing you can leave this be.")
aup = BooleanField(
label="i agree to the town's acceptable use policy",
help_text='please read our <a href="https://tilde.town/wiki/conduct.html">code of conduct</a> and click this box if you agree.')
label='i super agree to our acceptable use policy',
help_text='please read our <a href="https://tilde.town/~wiki/conduct.html">code of conduct</a> and click this box if you agree.')
def clean(self):
result = super().clean()

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-01-19 18:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0009_auto_20170114_0757'),
]
operations = [
migrations.AlterField(
model_name='pubkey',
name='key_type',
field=models.CharField(choices=[('ssh-rsa', 'ssh-rsa'), ('ssh-dss', 'ssh-dss'), ('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256')], max_length=50),
),
]

View File

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-04-16 01:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0010_auto_20190119_1828'),
]
operations = [
migrations.AddField(
model_name='townie',
name='plans',
field=models.TextField(blank=True, default=''),
),
migrations.AddField(
model_name='townie',
name='referral',
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name='townie',
name='socials',
field=models.TextField(blank=True, default=''),
),
]

View File

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-10 16:22
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0011_auto_20190416_0126'),
]
operations = [
migrations.AddField(
model_name='townie',
name='notes',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='townie',
name='referral',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-16 01:48
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0012_auto_20190710_1622'),
]
operations = [
migrations.AddField(
model_name='townie',
name='state',
field=models.CharField(choices=[('3_rejected', 'Rejected'), ('2_accepted', 'Accepted'), ('0_unreviewed', 'Unreviewed'), ('4_permaban', 'Permanently Banned'), ('1_tempban', 'Temporarily Banned')], default='0_unreviewed', max_length=20),
),
migrations.AlterField(
model_name='townie',
name='notes',
field=models.TextField(blank=True, help_text='Use this field to share information about this user (reviewed or not) for other admins to see', null=True),
),
]

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-16 01:51
from __future__ import unicode_literals
from django.db import migrations
def set_state(apps, _):
Townie = apps.get_model('users', 'Townie')
for townie in Townie.objects.all():
if townie.reviewed:
townie.state = '2_accepted'
townie.save()
class Migration(migrations.Migration):
dependencies = [
('users', '0013_auto_20190716_0148'),
]
operations = [
migrations.RunPython(set_state)
]

View File

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2019-07-16 02:10
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0014_auto_20190716_0151'),
]
operations = [
migrations.RemoveField(
model_name='townie',
name='reviewed',
),
]

View File

@ -4,7 +4,7 @@ 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
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
@ -19,7 +19,6 @@ SSH_TYPE_CHOICES = (
('ssh-rsa', 'ssh-rsa',),
('ssh-dss', 'ssh-dss',),
('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256'),
('ssh-ed25519', 'ssh-ed25519'),
)
DEFAULT_INDEX_PATH = '/etc/skel/public_html/index.html'
@ -33,7 +32,7 @@ else:
KEYFILE_HEADER = """########## GREETINGS! ##########
# Hi! This file is automatically managed by tilde.town. You
# seriously shouldn't change it. If you want to add more public keys that's
# 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"""
@ -43,57 +42,25 @@ class Townie(User):
class Meta:
verbose_name = 'Townie'
verbose_name_plural = 'Townies'
# the actual values here have a leading int for sorting :(
UNREVIEWED = '0_unreviewed'
TEMPBAN = '1_tempban'
ACCEPTED = '2_accepted'
REJECTED = '3_rejected'
PERMABAN = '4_permaban'
STATE_CHOICES = (
(REJECTED, 'Rejected'),
(ACCEPTED, 'Accepted'),
(UNREVIEWED, 'Unreviewed'),
(PERMABAN, 'Permanently Banned'),
(TEMPBAN, 'Temporarily Banned'),
)
shell = CharField(max_length=50, default="/bin/bash")
state = CharField(max_length=20, choices=STATE_CHOICES, default=UNREVIEWED)
reviewed = BooleanField(default=False)
reasons = TextField(blank=True, null=False, default='')
plans = TextField(blank=True, null=False, default='')
socials = TextField(blank=True, null=False, default='')
referral = CharField(max_length=100, null=True, blank=True)
displayname = CharField(max_length=100, blank=False, null=False)
notes = TextField(blank=True, null=True,
help_text='Use this field to share information about this user (reviewed or not) for other admins to see')
@property
def accepted(self):
return self.ACCEPTED == self.state
@property
def unreviewed(self):
return self.UNREVIEWED == self.state
@property
def home(self):
return os.path.join('/home', self.username)
def generate_gift(self):
command = '/town/bin/generate_welcome_present.sh'
error = _guarded_run(['sudo', command, self.username])
if error:
logger.error(error)
return
def send_welcome_email(self):
def send_welcome_email(self, admin_name='vilmibm'):
welcome_tmpl = get_template('users/welcome_email.txt')
context = {
'username': self.username,
'admin_name': 'vilmibm',
'admin_name': admin_name,
}
text = welcome_tmpl.render(context)
success = send_email(self.email, text, subject='tilde.town!')
from_address = '{}@tilde.town'.format(admin_name)
success = send_email(self.email, text, subject='tilde.town!',
frum=from_address)
if not success:
Ticket.objects.create(name='system',
email='root@tilde.town',
@ -117,7 +84,7 @@ class Townie(User):
"""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.accepted)
assert(self.reviewed)
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
error = _guarded_run(['sudo',
@ -128,7 +95,7 @@ class Townie(User):
'--disabled-password',
self.username])
if error:
logger.error(error)
logging.error(error)
return
error = _guarded_run(['sudo',
@ -138,7 +105,7 @@ class Townie(User):
self.username])
if error:
logger.error(error)
logging.error(error)
return
# Create .ssh
@ -147,7 +114,7 @@ class Townie(User):
'mkdir',
dot_ssh_path])
if error:
logger.error(error)
logging.error(error)
return
def write_authorized_keys(self):
@ -167,114 +134,54 @@ class Townie(User):
fp.seek(0)
error = _guarded_run(['sudo',
'--user={}'.format(self.username),
'/town/src/tildetown-admin/scripts/create_keyfile.py',
'/opt/bin/create_keyfile.py',
self.username],
stdin=fp)
if error:
logger.error(error)
logging.error(error)
def generate_authorized_keys(self):
"""returns a string suitable for writing out to an authorized_keys
file"""
content = KEYFILE_HEADER
for pubkey in self.pubkey_set.all():
prefix = pubkey.key.split(' ')
prefix = prefix[0] if len(prefix) > 0 else None
if prefix in [p[0] for p in SSH_TYPE_CHOICES]:
if pubkey.key.startswith('ssh-'):
content += '\n{}'.format(pubkey.key)
else:
content += '\n{} {}'.format(pubkey.key_type, pubkey.key)
content += '\n{} {}'.format(key.key_type, pubkey.key)
return content
def rename_on_disk(self, old_username):
"""Assuming that this instance has a new name set, renames this user on
disk with self.username."""
# TODO use systemd thing to end their session
error = _guarded_run([
'sudo',
'/town/src/tildetown-admin/scripts/rename_user.py',
old_username,
self.username])
if error:
logger.error(error)
return
logger.info('Renamed {} to {}'.format(old_username, self.username))
# send user an email
rename_tmpl = get_template('users/rename_email.txt')
context = {
'old_username': old_username,
'new_username': self.username
}
text = rename_tmpl.render(context)
success = send_email(self.email, text, subject='Your tilde.town user has been renamed!')
if not success:
Ticket.objects.create(name='system',
email='root@tilde.town',
issue_type='other',
issue_text='was not able to send rename email to {} ({})'.format(
self.username,
self.email))
class Pubkey(Model):
key_type = CharField(max_length=50,
blank=False,
null=False,
choices=SSH_TYPE_CHOICES)
choices=SSH_TYPE_CHOICES,
)
key = TextField(blank=False, null=False)
townie = ForeignKey(Townie)
@receiver(post_save, sender=Pubkey)
def on_pubkey_post_save(sender, instance, **kwargs):
# Ensure we're checking the townie as it exists at the point of pubkey
# save. If a user is being reviewed, we'll write their key file in the
# townie pre save.
townie = Townie.objects.filter(username=instance.townie.username)
if not townie:
return
townie = townie[0]
if townie.accepted:
townie.write_authorized_keys()
@receiver(pre_save, sender=Townie)
def on_townie_pre_save(sender, instance, **kwargs):
if instance.id is None:
logger.info('Signup from {}'.format(instance.username))
existing = Townie.objects.filter(username=instance.username)
if not existing:
# we're making a new Townie; this means someone just signed up. We
# don't care at all about their state on disk.
return
existing = Townie.objects.get(id=instance.id)
existing = existing[0]
# See if we need to create the user on disk.
if existing.unreviewed and instance.accepted:
logger.info('Creating user {} on disk.'.format(instance.username))
try:
needs_creation = not existing.reviewed and instance.reviewed == True
regen_keyfile = needs_creation or set(existing.pubkey_set.all()) != set(instance.pubkey_set.all())
if needs_creation:
instance.create_on_disk()
instance.write_authorized_keys()
except Exception as e:
logger.error('Failed syncing user {} to disk: {}'.format(instance.username, e))
else:
instance.send_welcome_email()
instance.generate_gift()
return
else:
# This user state transition is currently undefined. In the future, we can check for things
# like bans/unbans and then take the appropriate action.
return
# See if this user needs a rename on disk
logger.info('checking for rename {} vs {}'.format(
existing.username, instance.username))
if existing.username != instance.username:
logger.info('username do not match, going to rename')
instance.rename_on_disk(existing.username)
if regen_keyfile:
instance.write_authorized_keys()
def _guarded_run(cmd_args, **run_args):
@ -293,3 +200,22 @@ def _guarded_run(cmd_args, **run_args):
issue_text='error while running {}: {}'.format(
cmd_args, e))
return e
# 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

View File

@ -104,7 +104,7 @@
For more information about where to save keys, how to
use them, and how to use terminals (on all platforms),
check out the <a
href="https://tilde.town/wiki/ssh.html">tilde.town ssh
href="https://tilde.town/~wiki/ssh.html">tilde.town ssh
primer</a>.
</p>
</td>

View File

@ -1,13 +0,0 @@
hi!
you requested a new username on tilde.town. This process required logging you
out and killing any active processes you had running. sorry if this caused any
confusion or inconvenience.
old username: {{old_username}}
new username: {{new_username}}
you'll use this when ssh'ing into the town.
best,
~vilmibm

View File

@ -11,7 +11,6 @@
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% if signups_enabled %}
<form class="tilde" action="{% url 'users:signup' %}" method="post">
{% csrf_token %}
<table id="signup">
@ -41,11 +40,5 @@
<input type="submit" value="sign up <3" />
</div>
</form>
{% else %}
<p>
Town signups are currently disabled, but will resume in the future. If you can't wait for an account, you
can check out some <a href="https://tildeverse.org/">similar servers</a>.
</p>
{% endif %}
</body>
</html>

View File

@ -12,22 +12,23 @@
<h1>thanks for signing up for <a href="https://tilde.town">tilde.town!</a>!</h1>
<p>you'll get an email when your account is live. a human reads over
each new account and manually activates it, so it might take 1-3
days. if you think your account has been overlooked, file a
<a href="https://cgi.tilde.town/help/tickets">ticket</a>.
days. if you think your account has been overlooked, tweet
<a href="https://twitter.com/tildetown">@tildetown</a> or file a
<a href="https://tilde.town/helpdesk">ticket</a>.
</p>
<table>
<tr>
<td>
<h2>in the meantime, check out some of our projects...</h2>
<ul>
<li><a href="https://tilde.town/wiki/">our wiki</a></li>
<li><a href="https://tilde.town/~bear/where.html">tilde map</a></li>
<li><a href="https://github.com/tildetown/zine/raw/master/issue_1/zine.pdf">zine</a></li>
<li><a href="http://tilde.town/~wiki/">our wiki</a></li>
<li><a href="https://tilde.town/~karlen/">no one will ever read this but</a></li>
<li><a href="https://tilde.town/~kc/blackout/">black out</a></li>
<li><a href="https://tilde.town/~subtransience/machinecode/index.html">the machine room</a></li>
</ul>
<h2>or a <a href="https://cgi.tilde.town/users/random">random page</a></h2>
<h2>or a <a href="https://tilde.town/cgi/random">random page</a></h2>
</td>
<td style="padding:1em">
<a href="http://giphy.com/gifs/cyndipop-golden-girls-bea-arthur-ToMjGpK80QLT7KLWPLO">

View File

@ -1,6 +1,6 @@
Welcome to tilde.town, ~{{username}}!
Please take a moment to review our code of conduct: https://tilde.town/wiki/conduct.html
Please take a moment to review our code of conduct: https://tilde.town/~wiki/conduct.html
and login with:

View File

@ -12,18 +12,10 @@ from django.views.generic.edit import FormView
from .forms import TownieForm
from .models import Townie, Pubkey
SIGNUPS_ENABLED = True
class SignupView(FormView):
form_class = TownieForm
template_name = 'users/signup.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['signups_enabled'] = SIGNUPS_ENABLED
return ctx
@transaction.atomic
def form_valid(self, form):
del form.cleaned_data['captcha']