Added systemusers home/directory support

This commit is contained in:
Marc Aymerich 2014-11-14 14:38:06 +00:00
parent 5b54a0d28b
commit d2b96dac40
9 changed files with 167 additions and 16 deletions

View File

@ -172,3 +172,8 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* validate systemuser.home * validate systemuser.home
* webapp backend option compatibility check? * 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?)

View File

@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -1,3 +1,6 @@
import textwrap
from django import forms
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib import admin 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.apps.accounts.admin import SelectAccountAdminMixin
from orchestra.forms import UserCreationForm, UserChangeForm from orchestra.forms import UserCreationForm, UserChangeForm
from . import settings
from .actions import grant_permission from .actions import grant_permission
from .filters import IsMainListFilter from .filters import IsMainListFilter
from .models import SystemUser from .models import SystemUser
class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin): 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) list_filter = ('is_active', 'shell', IsMainListFilter)
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('username', 'password', 'account_link', 'is_active') 'fields': ('username', 'password', 'account_link', 'is_active')
}), }),
(_("System"), { (_("System"), {
'fields': ('home', 'shell', 'groups'), 'fields': ('shell', ('home', 'directory'), 'groups'),
}), }),
) )
add_fieldsets = ( add_fieldsets = (
@ -32,7 +36,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
'fields': ('account_link', 'username', 'password1', 'password2') 'fields': ('account_link', 'username', 'password1', 'password2')
}), }),
(_("System"), { (_("System"), {
'fields': ('home', 'shell', 'groups'), 'fields': ('shell', ('home', 'directory'), 'groups'),
}), }),
) )
search_fields = ['username'] search_fields = ['username']
@ -57,15 +61,56 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
display_main.short_description = _("Main") display_main.short_description = _("Main")
display_main.boolean = True 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): def get_form(self, request, obj=None, **kwargs):
""" exclude self reference on groups """
form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs) form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs)
duplicate = lambda n: (n, n)
if obj: if obj:
# Has to be done here and not in the form because of strange phenomenon # Has to be done here and not in the form because of strange phenomenon
# derived from monkeypatching formfield.widget.render on AccountAdminMinxin, # derived from monkeypatching formfield.widget.render on AccountAdminMinxin,
# don't ask. # don't ask.
formfield = form.base_fields['groups'] formfield = form.base_fields['groups']
formfield.queryset = formfield.queryset.exclude(id=obj.id) 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 = '<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 return form
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
@ -73,4 +118,5 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
return False return False
return super(SystemUserAdmin, self).has_delete_permission(request, obj=obj) return super(SystemUserAdmin, self).has_delete_permission(request, obj=obj)
admin.site.register(SystemUser, SystemUserAdmin) admin.site.register(SystemUser, SystemUserAdmin)

View File

@ -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,),
),
]

View File

@ -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,
),
]

View File

@ -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,
),
]

View File

@ -29,10 +29,12 @@ class SystemUser(models.Model):
password = models.CharField(_("password"), max_length=128) password = models.CharField(_("password"), max_length=128)
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='systemusers') related_name='systemusers')
home = models.CharField(_("home"), max_length=256, blank=True, home = models.CharField(_("home"), max_length=256, blank=False,
help_text=_("Home directory relative to account's ~main_user")) help_text=_("Starting location when login with this no-shell user."))
shell = models.CharField(_("shell"), max_length=32, directory = models.CharField(_("directory"), max_length=256, blank=True,
choices=settings.SYSTEMUSERS_SHELLS, default=settings.SYSTEMUSERS_DEFAULT_SHELL) 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, groups = models.ManyToManyField('self', blank=True, symmetrical=False,
help_text=_("A new group will be created for the user. " help_text=_("A new group will be created for the user. "
"Which additional groups would you like them to be a member of?")) "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.main_systemuser_id == self.pk
return self.account.username == self.username 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): def set_password(self, raw_password):
self.password = make_password(raw_password) self.password = make_password(raw_password)
def get_home(self): def get_base_home(self):
if self.is_main:
context = { context = {
'username': self.username, 'username': self.username,
} }
basehome = settings.SYSTEMUSERS_HOME % context return settings.SYSTEMUSERS_HOME % context
else:
basehome = self.account.main_systemuser.get_home() def get_home(self):
return os.path.join(basehome, self.home) return os.path.join(self.home or self.get_base_home(), self.directory)
services.register(SystemUser) services.register(SystemUser)

View File

@ -10,9 +10,16 @@ SYSTEMUSERS_SHELLS = getattr(settings, 'SYSTEMUSERS_SHELLS', (
('/bin/sh', "/bin/sh"), ('/bin/sh', "/bin/sh"),
)) ))
SYSTEMUSERS_DEFAULT_SHELL = getattr(settings, 'SYSTEMUSERS_DEFAULT_SHELL', '/dev/null') 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') SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', '/home/./%(username)s')