Added systemusers home/directory support
This commit is contained in:
parent
5b54a0d28b
commit
d2b96dac40
5
TODO.md
5
TODO.md
|
@ -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?)
|
||||||
|
|
|
@ -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 _
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -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_base_home(self):
|
||||||
|
context = {
|
||||||
|
'username': self.username,
|
||||||
|
}
|
||||||
|
return settings.SYSTEMUSERS_HOME % context
|
||||||
|
|
||||||
def get_home(self):
|
def get_home(self):
|
||||||
if self.is_main:
|
return os.path.join(self.home or self.get_base_home(), self.directory)
|
||||||
context = {
|
|
||||||
'username': self.username,
|
|
||||||
}
|
|
||||||
basehome = settings.SYSTEMUSERS_HOME % context
|
|
||||||
else:
|
|
||||||
basehome = self.account.main_systemuser.get_home()
|
|
||||||
return os.path.join(basehome, self.home)
|
|
||||||
|
|
||||||
|
|
||||||
services.register(SystemUser)
|
services.register(SystemUser)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue