diff --git a/TODO.md b/TODO.md index 96ee8b20..952a4cbc 100644 --- a/TODO.md +++ b/TODO.md @@ -172,3 +172,8 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * validate systemuser.home * webapp backend option compatibility check? + + +* ServiceBackend.validate() : used for server paths validation +* ServiceBackend.grant_access() : used for granting access +* bottom line: allow arbitrary backend methods (underscore method names that are not to be executed?) diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 6e0e3f8c..277232bf 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/apps/systemusers/admin.py b/orchestra/apps/systemusers/admin.py index 9c75e892..ec1a0ff5 100644 --- a/orchestra/apps/systemusers/admin.py +++ b/orchestra/apps/systemusers/admin.py @@ -1,3 +1,6 @@ +import textwrap + +from django import forms from django.conf.urls import patterns, url from django.core.urlresolvers import reverse from django.contrib import admin @@ -11,20 +14,21 @@ from orchestra.admin.utils import wrap_admin_view from orchestra.apps.accounts.admin import SelectAccountAdminMixin from orchestra.forms import UserCreationForm, UserChangeForm +from . import settings from .actions import grant_permission from .filters import IsMainListFilter from .models import SystemUser class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin): - list_display = ('username', 'account_link', 'shell', 'home', 'display_active', 'display_main') + list_display = ('username', 'account_link', 'shell', 'display_home', 'display_active', 'display_main') list_filter = ('is_active', 'shell', IsMainListFilter) fieldsets = ( (None, { 'fields': ('username', 'password', 'account_link', 'is_active') }), (_("System"), { - 'fields': ('home', 'shell', 'groups'), + 'fields': ('shell', ('home', 'directory'), 'groups'), }), ) add_fieldsets = ( @@ -32,7 +36,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende 'fields': ('account_link', 'username', 'password1', 'password2') }), (_("System"), { - 'fields': ('home', 'shell', 'groups'), + 'fields': ('shell', ('home', 'directory'), 'groups'), }), ) search_fields = ['username'] @@ -57,15 +61,56 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende display_main.short_description = _("Main") display_main.boolean = True + def display_home(self, user): + return user.get_home() + display_home.short_description = _("Home") + display_home.admin_order_field = 'home' + def get_form(self, request, obj=None, **kwargs): - """ exclude self reference on groups """ form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs) + duplicate = lambda n: (n, n) if obj: # Has to be done here and not in the form because of strange phenomenon # derived from monkeypatching formfield.widget.render on AccountAdminMinxin, # don't ask. formfield = form.base_fields['groups'] formfield.queryset = formfield.queryset.exclude(id=obj.id) + username = obj.username + choices=( + duplicate(self.account.main_systemuser.get_home()), + duplicate(obj.get_home()), + ) + else: + username = '' + choices=( + duplicate(self.account.main_systemuser.get_home()), + duplicate(SystemUser(username=username).get_home()), + ) + form.base_fields['home'].widget = forms.Select(choices=choices) + if obj and (obj.is_main or obj.has_shell): + # hidde home option for shell users + form.base_fields['home'].widget = forms.HiddenInput() + form.base_fields['directory'].widget = forms.HiddenInput() + else: + # Some javascript for hidde home/directory inputs when convinient + form.base_fields['shell'].widget.attrs = { + 'onChange': textwrap.dedent("""\ + field = $(".form-row.field-home.field-directory"); + if ($.inArray(this.value, %s) < 0) { + field.addClass("hidden"); + } else { + field.removeClass("hidden"); + };""" % str(list(settings.SYSTEMUSERS_DISABLED_SHELLS))) + } + form.base_fields['home'].widget.attrs = { + 'onChange': textwrap.dedent("""\ + field = $(".field-box.field-directory"); + if (this.value.search("%s") > 0) { + field.addClass("hidden"); + } else { + field.removeClass("hidden"); + };""" % username) + } return form def has_delete_permission(self, request, obj=None): @@ -73,4 +118,5 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende return False return super(SystemUserAdmin, self).has_delete_permission(request, obj=obj) + admin.site.register(SystemUser, SystemUserAdmin) diff --git a/orchestra/apps/systemusers/migrations/0001_initial.py b/orchestra/apps/systemusers/migrations/0001_initial.py new file mode 100644 index 00000000..54626e32 --- /dev/null +++ b/orchestra/apps/systemusers/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings +import django.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='SystemUser', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('username', models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./-/_ only.', unique=True, max_length=64, verbose_name='username', validators=[django.core.validators.RegexValidator(b'^[\\w.-]+$', 'Enter a valid username.', b'invalid')])), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('home', models.CharField(help_text="Home directory relative to account's ~main_user", max_length=256, verbose_name='home', blank=True)), + ('shell', models.CharField(default=b'/dev/null', max_length=32, verbose_name='shell', choices=[(b'/dev/null', 'No shell, FTP only'), (b'/bin/rssh', 'No shell, SFTP/RSYNC only'), (b'/bin/bash', b'/bin/bash'), (b'/bin/sh', b'/bin/sh')])), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('account', models.ForeignKey(related_name='systemusers', verbose_name='Account', to=settings.AUTH_USER_MODEL)), + ('groups', models.ManyToManyField(help_text='A new group will be created for the user. Which additional groups would you like them to be a member of?', to='systemusers.SystemUser', blank=True)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/orchestra/apps/systemusers/migrations/0002_systemuser_relative_to_main.py b/orchestra/apps/systemusers/migrations/0002_systemuser_relative_to_main.py new file mode 100644 index 00000000..6bb47008 --- /dev/null +++ b/orchestra/apps/systemusers/migrations/0002_systemuser_relative_to_main.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('systemusers', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='systemuser', + name='relative_to_main', + field=models.BooleanField(default=False, choices=[(True, b'Hola'), (False, b'adeu')]), + preserve_default=True, + ), + ] diff --git a/orchestra/apps/systemusers/migrations/0003_auto_20141114_1340.py b/orchestra/apps/systemusers/migrations/0003_auto_20141114_1340.py new file mode 100644 index 00000000..a3a38e57 --- /dev/null +++ b/orchestra/apps/systemusers/migrations/0003_auto_20141114_1340.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('systemusers', '0002_systemuser_relative_to_main'), + ] + + operations = [ + migrations.RemoveField( + model_name='systemuser', + name='relative_to_main', + ), + migrations.AddField( + model_name='systemuser', + name='directory', + field=models.CharField(default='', max_length=256, verbose_name='directory', blank=True), + preserve_default=False, + ), + migrations.AlterField( + model_name='systemuser', + name='home', + field=models.CharField(help_text='This will be your starting location when you login with this sftp user.', max_length=256, verbose_name='home'), + preserve_default=True, + ), + ] diff --git a/orchestra/apps/systemusers/migrations/__init__.py b/orchestra/apps/systemusers/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py index 5265117f..9bfaa380 100644 --- a/orchestra/apps/systemusers/models.py +++ b/orchestra/apps/systemusers/models.py @@ -29,10 +29,12 @@ class SystemUser(models.Model): password = models.CharField(_("password"), max_length=128) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='systemusers') - home = models.CharField(_("home"), max_length=256, blank=True, - help_text=_("Home directory relative to account's ~main_user")) - shell = models.CharField(_("shell"), max_length=32, - choices=settings.SYSTEMUSERS_SHELLS, default=settings.SYSTEMUSERS_DEFAULT_SHELL) + home = models.CharField(_("home"), max_length=256, blank=False, + help_text=_("Starting location when login with this no-shell user.")) + directory = models.CharField(_("directory"), max_length=256, blank=True, + help_text=_("Optional directory relative to user's home.")) + shell = models.CharField(_("shell"), max_length=32, choices=settings.SYSTEMUSERS_SHELLS, + default=settings.SYSTEMUSERS_DEFAULT_SHELL) groups = models.ManyToManyField('self', blank=True, symmetrical=False, help_text=_("A new group will be created for the user. " "Which additional groups would you like them to be a member of?")) @@ -61,18 +63,26 @@ class SystemUser(models.Model): return self.account.main_systemuser_id == self.pk return self.account.username == self.username + @property + def has_shell(self): + return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS + + def clean(self): + if self.has_shell or self.is_main: + self.home = self.get_base_home() + self.directory = '' + def set_password(self, raw_password): self.password = make_password(raw_password) + def get_base_home(self): + context = { + 'username': self.username, + } + return settings.SYSTEMUSERS_HOME % context + def get_home(self): - if self.is_main: - context = { - 'username': self.username, - } - basehome = settings.SYSTEMUSERS_HOME % context - else: - basehome = self.account.main_systemuser.get_home() - return os.path.join(basehome, self.home) + return os.path.join(self.home or self.get_base_home(), self.directory) services.register(SystemUser) diff --git a/orchestra/apps/systemusers/settings.py b/orchestra/apps/systemusers/settings.py index 5f228058..1e18c3c0 100644 --- a/orchestra/apps/systemusers/settings.py +++ b/orchestra/apps/systemusers/settings.py @@ -10,9 +10,16 @@ SYSTEMUSERS_SHELLS = getattr(settings, 'SYSTEMUSERS_SHELLS', ( ('/bin/sh', "/bin/sh"), )) + SYSTEMUSERS_DEFAULT_SHELL = getattr(settings, 'SYSTEMUSERS_DEFAULT_SHELL', '/dev/null') +SYSTEMUSERS_DISABLED_SHELLS = getattr(settings, 'SYSTEMUSERS_DISABLED_SHELLS', ( + '/dev/null', + '/bin/rssh', +)) + + SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', '/home/./%(username)s')