systemusers functional tests passing
This commit is contained in:
parent
647bc43a5a
commit
56ee1ba4a3
|
@ -52,6 +52,7 @@ class AccountAdmin(auth.UserAdmin, ExtendedModelAdmin):
|
||||||
add_form = AccountCreationForm
|
add_form = AccountCreationForm
|
||||||
form = AccountChangeForm
|
form = AccountChangeForm
|
||||||
filter_horizontal = ()
|
filter_horizontal = ()
|
||||||
|
change_readonly_fields = ('username',)
|
||||||
change_form_template = 'admin/accounts/account/change_form.html'
|
change_form_template = 'admin/accounts/account/change_form.html'
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
|
|
|
@ -24,21 +24,13 @@ class AccountCreationForm(auth.forms.UserCreationForm):
|
||||||
|
|
||||||
|
|
||||||
class AccountChangeForm(forms.ModelForm):
|
class AccountChangeForm(forms.ModelForm):
|
||||||
username = forms.CharField(required=False)
|
|
||||||
password = auth.forms.ReadOnlyPasswordHashField(label=_("Password"),
|
password = auth.forms.ReadOnlyPasswordHashField(label=_("Password"),
|
||||||
help_text=_("Raw passwords are not stored, so there is no way to see "
|
help_text=_("Raw passwords are not stored, so there is no way to see "
|
||||||
"this user's password, but you can change the password "
|
"this user's password, but you can change the password "
|
||||||
"using <a href=\"password/\">this form</a>."))
|
"using <a href=\"password/\">this form</a>."))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AccountChangeForm, self).__init__(*args, **kwargs)
|
|
||||||
account = kwargs.get('instance')
|
|
||||||
username = '<b style="font-size:small">%s</b>' % account.username
|
|
||||||
self.fields['username'].widget = ReadOnlyWidget(username)
|
|
||||||
self.fields['password'].initial = account.password
|
|
||||||
|
|
||||||
def clean_password(self):
|
def clean_password(self):
|
||||||
# Regardless of what the user provides, return the initial value.
|
# Regardless of what the user provides, return the initial value.
|
||||||
# This is done here, rather than on the field, because the
|
# This is done here, rather than on the field, because the
|
||||||
# field does not have access to the initial value
|
# field does not have access to the initial value
|
||||||
return self.fields['password'].initial
|
return self.initial["password"]
|
||||||
|
|
|
@ -41,7 +41,7 @@ def execute(operations):
|
||||||
scripts = {}
|
scripts = {}
|
||||||
cache = {}
|
cache = {}
|
||||||
for operation in operations:
|
for operation in operations:
|
||||||
logger.info("Queued %s" % str(operation))
|
logger.debug("Queued %s" % str(operation))
|
||||||
servers = router.get_servers(operation, cache=cache)
|
servers = router.get_servers(operation, cache=cache)
|
||||||
for server in servers:
|
for server in servers:
|
||||||
key = (server, operation.backend)
|
key = (server, operation.backend)
|
||||||
|
@ -72,6 +72,7 @@ def execute(operations):
|
||||||
logger.info("Executed %s" % str(operation))
|
logger.info("Executed %s" % str(operation))
|
||||||
operation.log = execution.log
|
operation.log = execution.log
|
||||||
operation.save()
|
operation.save()
|
||||||
logger.info(execution.log.stderr)
|
logger.debug(execution.log.stdout)
|
||||||
|
logger.debug(execution.log.stderr)
|
||||||
logs.append(execution.log)
|
logs.append(execution.log)
|
||||||
return logs
|
return logs
|
||||||
|
|
|
@ -37,8 +37,6 @@ def BashSSH(backend, log, server, cmds):
|
||||||
log.save(update_fields=['state'])
|
log.save(update_fields=['state'])
|
||||||
return
|
return
|
||||||
transport = ssh.get_transport()
|
transport = ssh.get_transport()
|
||||||
channel = transport.open_session()
|
|
||||||
|
|
||||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||||
sftp.put(path, "%s.remote" % path)
|
sftp.put(path, "%s.remote" % path)
|
||||||
sftp.close()
|
sftp.close()
|
||||||
|
@ -51,9 +49,11 @@ def BashSSH(backend, log, server, cmds):
|
||||||
cmd = (
|
cmd = (
|
||||||
"[[ $(md5sum %(path)s|awk {'print $1'}) == %(digest)s ]] && bash %(path)s\n"
|
"[[ $(md5sum %(path)s|awk {'print $1'}) == %(digest)s ]] && bash %(path)s\n"
|
||||||
"RETURN_CODE=$?\n"
|
"RETURN_CODE=$?\n"
|
||||||
# "rm -fr %(path)s\n"
|
# TODO "rm -fr %(path)s\n"
|
||||||
"exit $RETURN_CODE" % context
|
"exit $RETURN_CODE" % context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
channel = transport.open_session()
|
||||||
channel.exec_command(cmd)
|
channel.exec_command(cmd)
|
||||||
if True: # TODO if not async
|
if True: # TODO if not async
|
||||||
log.stdout += channel.makefile('rb', -1).read().decode('utf-8')
|
log.stdout += channel.makefile('rb', -1).read().decode('utf-8')
|
||||||
|
|
|
@ -22,7 +22,7 @@ class SystemUserBackend(ServiceController):
|
||||||
if [[ $( id %(username)s ) ]]; then
|
if [[ $( id %(username)s ) ]]; then
|
||||||
usermod %(username)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
|
usermod %(username)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
|
||||||
else
|
else
|
||||||
useradd %(username)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
|
useradd %(username)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
|
||||||
usermod -a -G %(username)s %(mainusername)s
|
usermod -a -G %(username)s %(mainusername)s
|
||||||
fi
|
fi
|
||||||
mkdir -p %(home)s
|
mkdir -p %(home)s
|
||||||
|
@ -35,10 +35,14 @@ class SystemUserBackend(ServiceController):
|
||||||
self.append("killall -u %(username)s || true" % context)
|
self.append("killall -u %(username)s || true" % context)
|
||||||
self.append("userdel %(username)s || true" % context)
|
self.append("userdel %(username)s || true" % context)
|
||||||
self.append("groupdel %(username)s || true" % context)
|
self.append("groupdel %(username)s || true" % context)
|
||||||
|
if user.is_main:
|
||||||
|
# TODO delete instead of this shit
|
||||||
|
context['deleted'] = context['home'][:-1]+'.deleted'
|
||||||
|
self.append("mv %(home)s %(deleted)s" % context)
|
||||||
|
|
||||||
def get_groups(self, user):
|
def get_groups(self, user):
|
||||||
if user.is_main:
|
if user.is_main:
|
||||||
return user.account.systemusers.exclude(id=user.id).values_list('username', flat=True)
|
return user.account.systemusers.exclude(username=user.username).values_list('username', flat=True)
|
||||||
groups = list(user.groups.values_list('username', flat=True))
|
groups = list(user.groups.values_list('username', flat=True))
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
|
@ -48,9 +52,8 @@ class SystemUserBackend(ServiceController):
|
||||||
'password': user.password if user.active else '*%s' % user.password,
|
'password': user.password if user.active else '*%s' % user.password,
|
||||||
'shell': user.shell,
|
'shell': user.shell,
|
||||||
'mainusername': user.username if user.is_main else user.account.username,
|
'mainusername': user.username if user.is_main else user.account.username,
|
||||||
|
'home': user.get_home()
|
||||||
}
|
}
|
||||||
basehome = settings.SYSTEMUSERS_HOME % context
|
|
||||||
context['home'] = os.path.join(basehome, user.home)
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
from orchestra.apps.accounts.models import Account
|
from orchestra.apps.accounts.models import Account
|
||||||
from orchestra.core.validators import validate_password
|
from orchestra.core.validators import validate_password
|
||||||
|
|
||||||
|
from .models import SystemUser
|
||||||
|
|
||||||
|
|
||||||
# TODO orchestra.UserCretionForm
|
# TODO orchestra.UserCretionForm
|
||||||
class UserCreationForm(auth.forms.UserCreationForm):
|
class UserCreationForm(auth.forms.UserCreationForm):
|
||||||
|
@ -16,10 +18,12 @@ class UserCreationForm(auth.forms.UserCreationForm):
|
||||||
# Since model.clean() will check this, this is redundant,
|
# Since model.clean() will check this, this is redundant,
|
||||||
# but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth
|
# but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth
|
||||||
username = self.cleaned_data["username"]
|
username = self.cleaned_data["username"]
|
||||||
account_model = self._meta.model.account.field.rel.to
|
try:
|
||||||
if account_model.objects.filter(username=username).exists():
|
SystemUser._default_manager.get(username=username)
|
||||||
raise forms.ValidationError(self.error_messages['duplicate_username'])
|
except SystemUser.DoesNotExist:
|
||||||
return username
|
return username
|
||||||
|
raise forms.ValidationError(self.error_messages['duplicate_username'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO orchestra.UserCretionForm
|
# TODO orchestra.UserCretionForm
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import os
|
||||||
|
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
|
@ -49,7 +51,26 @@ class SystemUser(models.Model):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def active(self):
|
def active(self):
|
||||||
|
a = type(self).account.field.model
|
||||||
|
try:
|
||||||
return self.is_active and self.account.is_active
|
return self.is_active and self.account.is_active
|
||||||
|
except type(self).account.field.rel.to.DoesNotExist:
|
||||||
|
return self.is_active
|
||||||
|
|
||||||
|
def get_home(self):
|
||||||
|
if self.is_main:
|
||||||
|
context = {
|
||||||
|
'username': self.username,
|
||||||
|
}
|
||||||
|
basehome = settings.SYSTEMUSERS_HOME % context
|
||||||
|
else:
|
||||||
|
basehome = self.account.systemusers.get(is_main=True).get_home()
|
||||||
|
basehome = basehome.replace('/./', '/')
|
||||||
|
home = os.path.join(basehome, self.home)
|
||||||
|
# Chrooting
|
||||||
|
home = home.split('/')
|
||||||
|
home.insert(-2, '.')
|
||||||
|
return '/'.join(home)
|
||||||
|
|
||||||
|
|
||||||
## TODO user deletion and group handling.
|
## TODO user deletion and group handling.
|
||||||
|
|
|
@ -4,13 +4,13 @@ from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
SYSTEMUSERS_SHELLS = getattr(settings, 'SYSTEMUSERS_SHELLS', (
|
SYSTEMUSERS_SHELLS = getattr(settings, 'SYSTEMUSERS_SHELLS', (
|
||||||
('/bin/false', _("No shell, FTP only")),
|
('/dev/null', _("No shell, FTP only")),
|
||||||
('/bin/rsync', _("No shell, SFTP/RSYNC only")),
|
('/bin/rssh', _("No shell, SFTP/RSYNC only")),
|
||||||
('/bin/bash', "/bin/bash"),
|
('/bin/bash', "/bin/bash"),
|
||||||
('/bin/sh', "/bin/sh"),
|
('/bin/sh', "/bin/sh"),
|
||||||
))
|
))
|
||||||
|
|
||||||
SYSTEMUSERS_DEFAULT_SHELL = getattr(settings, 'SYSTEMUSERS_DEFAULT_SHELL', '/bin/false')
|
SYSTEMUSERS_DEFAULT_SHELL = getattr(settings, 'SYSTEMUSERS_DEFAULT_SHELL', '/dev/null')
|
||||||
|
|
||||||
|
|
||||||
SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', '/home/%(username)s')
|
SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', '/home/%(username)s')
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
import ftplib
|
||||||
|
import re
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.conf import settings
|
import paramiko
|
||||||
|
from django.conf import settings as djsettings
|
||||||
|
from django.core.management.base import CommandError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from selenium.webdriver.support.select import Select
|
from selenium.webdriver.support.select import Select
|
||||||
|
|
||||||
|
@ -9,7 +13,7 @@ from orchestra.apps.orchestration.models import Server, Route
|
||||||
from orchestra.utils.system import run
|
from orchestra.utils.system import run
|
||||||
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii
|
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii
|
||||||
|
|
||||||
from ... import backends
|
from ... import backends, settings
|
||||||
from ...models import SystemUser
|
from ...models import SystemUser
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,12 +31,16 @@ class SystemUserMixin(object):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(SystemUserMixin, self).setUp()
|
super(SystemUserMixin, self).setUp()
|
||||||
self.add_route()
|
self.add_route()
|
||||||
|
djsettings.DEBUG = True
|
||||||
|
|
||||||
def add_route(self):
|
def add_route(self):
|
||||||
master = Server.objects.create(name=self.MASTER_ADDR)
|
master = Server.objects.create(name=self.MASTER_ADDR)
|
||||||
backend = backends.SystemUserBackend.get_name()
|
backend = backends.SystemUserBackend.get_name()
|
||||||
Route.objects.create(backend=backend, match=True, host=master)
|
Route.objects.create(backend=backend, match=True, host=master)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def add(self):
|
def add(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -45,54 +53,154 @@ class SystemUserMixin(object):
|
||||||
def disable(self):
|
def disable(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_group(self, username, groupname):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def validate_user(self, username):
|
||||||
|
idcmd = r("id %s" % username)
|
||||||
|
self.assertEqual(0, idcmd.return_code)
|
||||||
|
user = SystemUser.objects.get(username=username)
|
||||||
|
groups = list(user.groups.values_list('username', flat=True))
|
||||||
|
groups.append(user.username)
|
||||||
|
idgroups = idcmd.stdout.strip().split(' ')[2]
|
||||||
|
idgroups = re.findall(r'\d+\((\w+)\)', idgroups)
|
||||||
|
self.assertEqual(set(groups), set(idgroups))
|
||||||
|
|
||||||
|
def validate_delete(self, username):
|
||||||
|
self.assertRaises(SystemUser.DoesNotExist, SystemUser.objects.get, username=username)
|
||||||
|
self.assertRaises(CommandError, run, 'id %s' % username, display=False)
|
||||||
|
self.assertRaises(CommandError, run, 'grep "^%s:" /etc/groups' % username, display=False)
|
||||||
|
self.assertRaises(CommandError, run, 'grep "^%s:" /etc/passwd' % username, display=False)
|
||||||
|
self.assertRaises(CommandError, run, 'grep "^%s:" /etc/shadow' % username, display=False)
|
||||||
|
|
||||||
|
def validate_ftp(self, username, password):
|
||||||
|
connection = ftplib.FTP(self.MASTER_ADDR)
|
||||||
|
connection.login(user=username, passwd=password)
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
def validate_sftp(self, username, password):
|
||||||
|
transport = paramiko.Transport((self.MASTER_ADDR, 22))
|
||||||
|
transport.connect(username=username, password=password)
|
||||||
|
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||||
|
sftp.listdir()
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
def validate_ssh(self, username, password):
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(self.MASTER_ADDR, username=username, password=password)
|
||||||
|
transport = ssh.get_transport()
|
||||||
|
channel = transport.open_session()
|
||||||
|
channel.exec_command('ls')
|
||||||
|
self.assertEqual(0, channel.recv_exit_status())
|
||||||
|
channel.close()
|
||||||
|
|
||||||
def test_create_systemuser(self):
|
def test_create_systemuser(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%spppP001' % random_ascii(5)
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
self.add(username, password)
|
self.add(username, password)
|
||||||
self.addCleanup(partial(self.delete, username))
|
self.addCleanup(partial(self.delete, username))
|
||||||
self.assertEqual(0, r("id %s" % username).return_code)
|
self.validate_user(username)
|
||||||
# TODO test group membership and everything
|
|
||||||
|
def test_ftp(self):
|
||||||
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
self.add(username, password, shell='/dev/null')
|
||||||
|
self.addCleanup(partial(self.delete, username))
|
||||||
|
self.assertRaises(paramiko.AuthenticationException, self.validate_sftp, username, password)
|
||||||
|
self.assertRaises(paramiko.AuthenticationException, self.validate_ssh, username, password)
|
||||||
|
|
||||||
|
def test_sftp(self):
|
||||||
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
self.add(username, password, shell='/bin/rssh')
|
||||||
|
self.addCleanup(partial(self.delete, username))
|
||||||
|
self.validate_sftp(username, password)
|
||||||
|
self.assertRaises(AssertionError, self.validate_ssh, username, password)
|
||||||
|
|
||||||
|
def test_ssh(self):
|
||||||
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
self.add(username, password, shell='/bin/bash')
|
||||||
|
self.addCleanup(partial(self.delete, username))
|
||||||
|
self.validate_ssh(username, password)
|
||||||
|
|
||||||
def test_delete_systemuser(self):
|
def test_delete_systemuser(self):
|
||||||
username = '%s_systemuser' % random_ascii(10)
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
password = '@!?%sppppP001' % random_ascii(5)
|
password = '@!?%sppppP001' % random_ascii(5)
|
||||||
self.add(username, password)
|
self.add(username, password)
|
||||||
self.assertEqual(0, r("id %s" % username).return_code)
|
self.validate_user(username)
|
||||||
self.delete(username)
|
self.delete(username)
|
||||||
self.assertEqual(1, r("id %s" % username, error_codes=[0,1]).return_code)
|
self.validate_delete(username)
|
||||||
|
|
||||||
def test_update_systemuser(self):
|
def test_add_group_systemuser(self):
|
||||||
pass
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
# TODO
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
self.add(username, password)
|
||||||
|
self.addCleanup(partial(self.delete, username))
|
||||||
|
self.validate_user(username)
|
||||||
|
username2 = '%s_systemuser' % random_ascii(10)
|
||||||
|
password2 = '@!?%spppP001' % random_ascii(5)
|
||||||
|
self.add(username2, password2)
|
||||||
|
self.addCleanup(partial(self.delete, username2))
|
||||||
|
self.validate_user(username2)
|
||||||
|
self.add_group(username, username2)
|
||||||
|
user = SystemUser.objects.get(username=username)
|
||||||
|
groups = list(user.groups.values_list('username', flat=True))
|
||||||
|
self.assertIn(username2, groups)
|
||||||
|
self.validate_user(username)
|
||||||
|
|
||||||
def test_disable_systemuser(self):
|
def test_disable_systemuser(self):
|
||||||
pass
|
username = '%s_systemuser' % random_ascii(10)
|
||||||
# TODO
|
password = '@!?%spppP001' % random_ascii(5)
|
||||||
|
self.add(username, password, shell='/dev/null')
|
||||||
|
self.addCleanup(partial(self.delete, username))
|
||||||
|
self.validate_ftp(username, password)
|
||||||
|
self.disable(username)
|
||||||
|
self.validate_user(username)
|
||||||
|
self.assertRaises(ftplib.error_perm, self.validate_ftp, username, password)
|
||||||
|
|
||||||
# TODO test with ftp and ssh clients?
|
|
||||||
|
|
||||||
class RESTSystemUserMixin(SystemUserMixin):
|
class RESTSystemUserMixin(SystemUserMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RESTSystemUserMixin, self).setUp()
|
super(RESTSystemUserMixin, self).setUp()
|
||||||
self.rest_login()
|
self.rest_login()
|
||||||
|
# create main user
|
||||||
|
self.save(self.account.username)
|
||||||
|
self.addCleanup(partial(self.delete, self.account.username))
|
||||||
|
|
||||||
def add(self, username, password):
|
def add(self, username, password, shell='/dev/null'):
|
||||||
self.rest.systemusers.create(username=username, password=password)
|
self.rest.systemusers.create(username=username, password=password, shell=shell)
|
||||||
|
|
||||||
def delete(self, username):
|
def delete(self, username):
|
||||||
user = self.rest.systemusers.retrieve(username=username).get()
|
user = self.rest.systemusers.retrieve(username=username).get()
|
||||||
user.delete()
|
user.delete()
|
||||||
|
|
||||||
def update(self):
|
def add_group(self, username, groupname):
|
||||||
pass
|
user = self.rest.systemusers.retrieve(username=username).get()
|
||||||
|
group = self.rest.systemusers.retrieve(username=groupname).get()
|
||||||
|
user.groups.append(group) # TODO how to do it with the api?
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
def disable(self, username):
|
||||||
|
user = self.rest.systemusers.retrieve(username=username).get()
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
def save(self, username):
|
||||||
|
user = self.rest.systemusers.retrieve(username=username).get()
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
class AdminSystemUserMixin(SystemUserMixin):
|
class AdminSystemUserMixin(SystemUserMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(AdminSystemUserMixin, self).setUp()
|
super(AdminSystemUserMixin, self).setUp()
|
||||||
self.admin_login()
|
self.admin_login()
|
||||||
|
# create main user
|
||||||
|
self.save(self.account.username)
|
||||||
|
self.addCleanup(partial(self.delete, self.account.username))
|
||||||
|
|
||||||
def add(self, username, password):
|
def add(self, username, password, shell='/dev/null'):
|
||||||
url = self.live_server_url + reverse('admin:systemusers_systemuser_add')
|
url = self.live_server_url + reverse('admin:systemusers_systemuser_add')
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
|
|
||||||
|
@ -108,21 +216,52 @@ class AdminSystemUserMixin(SystemUserMixin):
|
||||||
account_select = Select(account_input)
|
account_select = Select(account_input)
|
||||||
account_select.select_by_value(str(self.account.pk))
|
account_select.select_by_value(str(self.account.pk))
|
||||||
|
|
||||||
|
shell_input = self.selenium.find_element_by_id('id_shell')
|
||||||
|
shell_select = Select(shell_input)
|
||||||
|
shell_select.select_by_value(shell)
|
||||||
|
|
||||||
username_field.submit()
|
username_field.submit()
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
def delete(self, username):
|
def delete(self, username):
|
||||||
user = SystemUser.objects.get(username=username)
|
user = SystemUser.objects.get(username=username)
|
||||||
url = self.live_server_url + reverse('admin:systemusers_systemuser_delete', args=(user.pk,))
|
delete = reverse('admin:systemusers_systemuser_delete', args=(user.pk,))
|
||||||
|
url = self.live_server_url + delete
|
||||||
self.selenium.get(url)
|
self.selenium.get(url)
|
||||||
confirmation = self.selenium.find_element_by_name('post')
|
confirmation = self.selenium.find_element_by_name('post')
|
||||||
confirmation.submit()
|
confirmation.submit()
|
||||||
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
def disable(self, username):
|
def disable(self, username):
|
||||||
pass
|
user = SystemUser.objects.get(username=username)
|
||||||
|
change = reverse('admin:systemusers_systemuser_change', args=(user.pk,))
|
||||||
|
url = self.live_server_url + change
|
||||||
|
self.selenium.get(url)
|
||||||
|
is_active = self.selenium.find_element_by_id('id_is_active')
|
||||||
|
is_active.click()
|
||||||
|
save = self.selenium.find_element_by_name('_save')
|
||||||
|
save.submit()
|
||||||
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
def update(self):
|
def add_group(self, username, groupname):
|
||||||
pass
|
user = SystemUser.objects.get(username=username)
|
||||||
|
change = reverse('admin:systemusers_systemuser_change', args=(user.pk,))
|
||||||
|
url = self.live_server_url + change
|
||||||
|
self.selenium.get(url)
|
||||||
|
groups = self.selenium.find_element_by_id('id_groups_add_all_link')
|
||||||
|
groups.click()
|
||||||
|
save = self.selenium.find_element_by_name('_save')
|
||||||
|
save.submit()
|
||||||
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
|
def save(self, username):
|
||||||
|
user = SystemUser.objects.get(username=username)
|
||||||
|
change = reverse('admin:systemusers_systemuser_change', args=(user.pk,))
|
||||||
|
url = self.live_server_url + change
|
||||||
|
self.selenium.get(url)
|
||||||
|
save = self.selenium.find_element_by_name('_save')
|
||||||
|
save.submit()
|
||||||
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
|
|
||||||
class RESTSystemUserTest(RESTSystemUserMixin, BaseLiveServerTestCase):
|
class RESTSystemUserTest(RESTSystemUserMixin, BaseLiveServerTestCase):
|
||||||
|
@ -160,3 +299,20 @@ class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase):
|
||||||
self.addCleanup(account.delete)
|
self.addCleanup(account.delete)
|
||||||
self.assertNotEqual(url, self.selenium.current_url)
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
self.assertEqual(0, r("id %s" % account.username).return_code)
|
self.assertEqual(0, r("id %s" % account.username).return_code)
|
||||||
|
|
||||||
|
def test_delete_account(self):
|
||||||
|
home = self.account.systemusers.get(is_main=True).get_home()
|
||||||
|
|
||||||
|
delete = reverse('admin:accounts_account_delete', args=(self.account.pk,))
|
||||||
|
url = self.live_server_url + delete
|
||||||
|
self.selenium.get(url)
|
||||||
|
confirmation = self.selenium.find_element_by_name('post')
|
||||||
|
confirmation.submit()
|
||||||
|
self.assertNotEqual(url, self.selenium.current_url)
|
||||||
|
|
||||||
|
self.assertRaises(CommandError, run, 'ls %s' % home, display=False)
|
||||||
|
|
||||||
|
# Recreate a fucking fake account for test cleanup
|
||||||
|
self.account = self.create_account(username=self.account.username, superuser=True)
|
||||||
|
self.selenium.delete_all_cookies()
|
||||||
|
self.admin_login()
|
||||||
|
|
|
@ -46,7 +46,7 @@ def read_async(fd):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def run(command, display=True, error_codes=[0], silent=True, stdin=''):
|
def run(command, display=True, error_codes=[0], silent=False, stdin=''):
|
||||||
""" Subprocess wrapper for running commands """
|
""" Subprocess wrapper for running commands """
|
||||||
if display:
|
if display:
|
||||||
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
|
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
|
||||||
|
@ -96,9 +96,10 @@ def run(command, display=True, error_codes=[0], silent=True, stdin=''):
|
||||||
out.failed = True
|
out.failed = True
|
||||||
msg = "\nrun() encountered an error (return code %s) while executing '%s'\n"
|
msg = "\nrun() encountered an error (return code %s) while executing '%s'\n"
|
||||||
msg = msg % (p.returncode, command)
|
msg = msg % (p.returncode, command)
|
||||||
|
if display:
|
||||||
sys.stderr.write("\n\033[1;31mCommandError: %s %s\033[m\n" % (msg, err))
|
sys.stderr.write("\n\033[1;31mCommandError: %s %s\033[m\n" % (msg, err))
|
||||||
if not silent:
|
if not silent:
|
||||||
raise CommandError("\n%s\n %s\n" % (msg, err))
|
raise CommandError("%s %s" % (msg, err))
|
||||||
|
|
||||||
out.succeeded = not out.failed
|
out.succeeded = not out.failed
|
||||||
return out
|
return out
|
||||||
|
|
|
@ -53,7 +53,8 @@ class AppDependencyMixin(object):
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(TestCase, AppDependencyMixin):
|
class BaseTestCase(TestCase, AppDependencyMixin):
|
||||||
def create_account(self, superuser=False):
|
def create_account(self, username='', superuser=False):
|
||||||
|
if not username:
|
||||||
username = '%s_superaccount' % random_ascii(5)
|
username = '%s_superaccount' % random_ascii(5)
|
||||||
password = 'orchestra'
|
password = 'orchestra'
|
||||||
if superuser:
|
if superuser:
|
||||||
|
@ -75,9 +76,11 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
|
||||||
cls.vdisplay.stop()
|
cls.vdisplay.stop()
|
||||||
super(BaseLiveServerTestCase, cls).tearDownClass()
|
super(BaseLiveServerTestCase, cls).tearDownClass()
|
||||||
|
|
||||||
def create_account(self, superuser=False):
|
def create_account(self, username='', superuser=False):
|
||||||
|
if not username:
|
||||||
username = '%s_superaccount' % random_ascii(5)
|
username = '%s_superaccount' % random_ascii(5)
|
||||||
password = 'orchestra'
|
password = 'orchestra'
|
||||||
|
self.account_password = password
|
||||||
if superuser:
|
if superuser:
|
||||||
return Account.objects.create_superuser(username, password=password, email='orchestra@orchestra.org')
|
return Account.objects.create_superuser(username, password=password, email='orchestra@orchestra.org')
|
||||||
return Account.objects.create_user(username, password=password, email='orchestra@orchestra.org')
|
return Account.objects.create_user(username, password=password, email='orchestra@orchestra.org')
|
||||||
|
@ -101,4 +104,4 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
|
||||||
))
|
))
|
||||||
|
|
||||||
def rest_login(self):
|
def rest_login(self):
|
||||||
self.rest.login(username=self.ACCOUNT_USERNAME, password=self.ACCOUNT_PASSWORD)
|
self.rest.login(username=self.account.username, password=self.account_password)
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
Restricted Shell for SCP and Rsync
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. apt-get install rssh
|
||||||
|
|
||||||
|
2. Enable desired programs
|
||||||
|
```bash
|
||||||
|
sed -i "s/^#allowscp/allowscp/" /etc/rssh.conf
|
||||||
|
sed -i "s/^#allowrsync/allowrsync/" /etc/rssh.conf
|
||||||
|
sed -i "s/^#allowsftp/allowsftp/" /etc/rssh.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Enable the shell
|
||||||
|
```bash
|
||||||
|
ln -s /usr/local/bin/rssh /bin/rssh
|
||||||
|
echo /bin/rssh >> /etc/shells
|
||||||
|
```
|
|
@ -15,9 +15,12 @@ VsFTPd with System Users
|
||||||
sed -i "s/#write_enable=YES/write_enable=YES" /etc/vsftpd.conf
|
sed -i "s/#write_enable=YES/write_enable=YES" /etc/vsftpd.conf
|
||||||
sed -i "s/#chroot_local_user=YES/chroot_local_user=YES/" /etc/vsftpd.conf
|
sed -i "s/#chroot_local_user=YES/chroot_local_user=YES/" /etc/vsftpd.conf
|
||||||
|
|
||||||
echo '/bin/false' >> /etc/shells
|
echo '/dev/null' >> /etc/shells
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# TODO https://www.benscobie.com/fixing-500-oops-vsftpd-refusing-to-run-with-writable-root-inside-chroot/#comment-2051
|
||||||
|
Define option passwd_chroot_enable=yes in configuration file and change in /etc/passwd file user home directory from «/home/user» to «/home/./user» (w/o quotes).
|
||||||
|
|
||||||
|
|
||||||
3. Apply changes
|
3. Apply changes
|
||||||
```bash
|
```bash
|
||||||
|
|
Loading…
Reference in New Issue