Improved ACL support

This commit is contained in:
Marc Aymerich 2015-05-12 12:38:40 +00:00
parent b8c815edf2
commit b5ede03858
23 changed files with 248 additions and 87 deletions

View File

@ -354,3 +354,5 @@ make django admin taskstate uncollapse fucking traceback, ( if exists ?)
resorce monitoring more efficient, less mem an better queries for calc current data resorce monitoring more efficient, less mem an better queries for calc current data
# best_price rating method # best_price rating method
# select contact with one result: redirect

View File

@ -58,7 +58,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
add_form = AccountCreationForm add_form = AccountCreationForm
form = UserChangeForm form = UserChangeForm
filter_horizontal = () filter_horizontal = ()
change_readonly_fields = ('username', 'main_systemuser_link') change_readonly_fields = ('username', 'main_systemuser_link', 'is_active')
change_form_template = 'admin/accounts/account/change_form.html' change_form_template = 'admin/accounts/account/change_form.html'
actions = [disable, list_contacts, service_report, SendEmail(), delete_related_services] actions = [disable, list_contacts, service_report, SendEmail(), delete_related_services]
change_view_actions = [disable, service_report] change_view_actions = [disable, service_report]
@ -139,8 +139,14 @@ class AccountListAdmin(AccountAdmin):
'original_model': original_model, 'original_model': original_model,
} }
context.update(extra_context or {}) context.update(extra_context or {})
return super(AccountListAdmin, self).changelist_view(request, response = super(AccountListAdmin, self).changelist_view(request, extra_context=context)
extra_context=context) if hasattr(response, 'context_data'):
# user has submitted a change list change, we redirect directly to the add view
# if there is only one result
queryset = response.context_data['cl'].queryset
if len(queryset) == 1:
return HttpResponseRedirect('../?account=%i' % queryset[0].pk)
return response
class AccountAdminMixin(object): class AccountAdminMixin(object):
@ -283,8 +289,7 @@ class AccountAdminMixin(object):
request_copy.pop('account') request_copy.pop('account')
request.GET = request_copy request.GET = request_copy
context.update(extra_context or {}) context.update(extra_context or {})
return super(AccountAdminMixin, self).changelist_view(request, return super(AccountAdminMixin, self).changelist_view(request, extra_context=context)
extra_context=context)
class SelectAccountAdminMixin(AccountAdminMixin): class SelectAccountAdminMixin(AccountAdminMixin):

View File

@ -177,7 +177,12 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
def commit(self): def commit(self):
""" ideally slave should be restarted after master """ """ ideally slave should be restarted after master """
self.append('if [[ $UPDATED == 1 ]]; then { sleep 1 && service bind9 reload; } & fi') self.append(textwrap.dedent("""\
if [[ $UPDATED == 1 ]]; then
nohup bash -c 'sleep 1 && service bind9 reload' &> /dev/null &
fi
""")
)
def get_context(self, domain): def get_context(self, domain):
context = { context = {

View File

@ -252,6 +252,7 @@ class Record(models.Model):
def clean(self): def clean(self):
""" validates record value based on its type """ """ validates record value based on its type """
# validate value # validate value
if self.type != self.TXT:
self.value = self.value.lower().strip() self.value = self.value.lower().strip()
choices = { choices = {
self.MX: validators.validate_mx_record, self.MX: validators.validate_mx_record,

View File

@ -124,5 +124,5 @@ def validate_zone(zone):
if check.exit_code == 127: if check.exit_code == 127:
logger.error("Cannot validate domain zone: %s not installed." % checkzone) logger.error("Cannot validate domain zone: %s not installed." % checkzone)
elif check.exit_code == 1: elif check.exit_code == 1:
errors = re.compile(r'zone.*: (.*)').findall(check.stdout)[:-1] errors = re.compile(r'zone.*: (.*)').findall(check.stdout.decode('utf8'))[:-1]
raise ValidationError(', '.join(errors)) raise ValidationError(', '.join(errors))

View File

@ -78,7 +78,7 @@ class MailmanBackend(MailmanVirtualDomainBackend):
Includes <tt>MailmanVirtualDomainBackend</tt> Includes <tt>MailmanVirtualDomainBackend</tt>
""" """
verbose_name = "Mailman" verbose_name = "Mailman"
addresses = [ address_suffixes = [
'', '',
'-admin', '-admin',
'-bounces', '-bounces',
@ -99,9 +99,12 @@ class MailmanBackend(MailmanVirtualDomainBackend):
def get_virtual_aliases(self, context): def get_virtual_aliases(self, context):
aliases = ['# %(banner)s' % context] aliases = ['# %(banner)s' % context]
for address in self.addresses: for suffix in self.address_suffixes:
context['address'] = address context['suffix'] = suffix
aliases.append("%(address_name)s%(address)s@%(domain)s\t%(name)s%(address)s" % context) # Because mailman doesn't properly handle lists aliases we need two virtual aliases
aliases.append("%(address_name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context)
# And another with the original list name; Mailman generates links with it
aliases.append("%(name)s%(suffix)s@%(domain)s\t%(name)s%(suffix)s" % context)
return '\n'.join(aliases) return '\n'.join(aliases)
def save(self, mail_list): def save(self, mail_list):
@ -122,7 +125,7 @@ class MailmanBackend(MailmanVirtualDomainBackend):
UPDATED_VIRTUAL_ALIAS=1 UPDATED_VIRTUAL_ALIAS=1
else else
if [[ ! $(grep '^\s*%(address_name)s@%(address_domain)s\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then if [[ ! $(grep '^\s*%(address_name)s@%(address_domain)s\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then
sed -i -e '/^.*\s%(name)s\(%(address_regex)s\)\s*$/d' \\ sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s -e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s
echo "${aliases}" >> %(virtual_alias)s echo "${aliases}" >> %(virtual_alias)s
UPDATED_VIRTUAL_ALIAS=1 UPDATED_VIRTUAL_ALIAS=1
@ -159,7 +162,7 @@ class MailmanBackend(MailmanVirtualDomainBackend):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.exclude_virtual_alias_domain(context) self.exclude_virtual_alias_domain(context)
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
sed -i -e '/^.*\s%(name)s\(%(address_regex)s\)\s*$/d' \\ sed -i -e '/^.*\s%(name)s\(%(suffixes_regex)s\)\s*$/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s""") % context -e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s""") % context
) )
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
@ -201,7 +204,7 @@ class MailmanBackend(MailmanVirtualDomainBackend):
'domain': mail_list.address_domain or settings.LISTS_DEFAULT_DOMAIN, 'domain': mail_list.address_domain or settings.LISTS_DEFAULT_DOMAIN,
'address_name': mail_list.get_address_name(), 'address_name': mail_list.get_address_name(),
'address_domain': mail_list.address_domain, 'address_domain': mail_list.address_domain,
'address_regex': '\|'.join(self.addresses), 'suffixes_regex': '\|'.join(self.address_suffixes),
'admin': mail_list.admin_email, 'admin': mail_list.admin_email,
'mailman_root': settings.LISTS_MAILMAN_ROOT_DIR, 'mailman_root': settings.LISTS_MAILMAN_ROOT_DIR,
}) })

View File

@ -48,12 +48,16 @@ class SieveFilteringMixin(object):
class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController): class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController):
""" """
Assumes that all system users on this servers all mail accounts. Assumes that all system users on this servers all mail accounts.
If you want to have system users AND mailboxes on the same server you should consider using virtual mailboxes If you want to have system users AND mailboxes on the same server you should consider using virtual mailboxes.
Supports quota allocation via <tt>resources.disk.allocated</tt>.
""" """
SHELL = '/dev/null' SHELL = '/dev/null'
verbose_name = _("UNIX maildir user") verbose_name = _("UNIX maildir user")
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
doc_settings = (settings,
('MAILBOXES_USE_ACCOUNT_AS_GROUP',)
)
def save(self, mailbox): def save(self, mailbox):
context = self.get_context(mailbox) context = self.get_context(mailbox)
@ -89,7 +93,7 @@ class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController):
context = self.get_context(mailbox) context = self.get_context(mailbox)
self.append('mv %(home)s %(home)s.deleted || exit_code=$?' % context) self.append('mv %(home)s %(home)s.deleted || exit_code=$?' % context)
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
{ sleep 2 && killall -u %(user)s -s KILL; } & nohup bash -c '{ sleep 2 && killall -u %(user)s -s KILL; }' &> /dev/null &
killall -u %(user)s || true killall -u %(user)s || true
userdel %(user)s || true userdel %(user)s || true
groupdel %(user)s || true""") % context groupdel %(user)s || true""") % context
@ -98,7 +102,7 @@ class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController):
def get_context(self, mailbox): def get_context(self, mailbox):
context = { context = {
'user': mailbox.name, 'user': mailbox.name,
'group': mailbox.name, 'group': mailbox.account.username if settings.MAILBOXES_USE_ACCOUNT_AS_GROUP else mailbox.name,
'name': mailbox.name, 'name': mailbox.name,
'password': mailbox.password if mailbox.active else '*%s' % mailbox.password, 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
'home': mailbox.get_home(), 'home': mailbox.get_home(),
@ -147,7 +151,7 @@ class DovecotPostfixPasswdVirtualUserBackend(SieveFilteringMixin, ServiceControl
def delete(self, mailbox): def delete(self, mailbox):
context = self.get_context(mailbox) context = self.get_context(mailbox)
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
{ sleep 2 && killall -u %(uid)s -s KILL; } & nohup bash -c 'sleep 2 && killall -u %(uid)s -s KILL' &> /dev/null &
killall -u %(uid)s || true killall -u %(uid)s || true
sed -i '/^%(user)s:.*/d' %(passwd_path)s sed -i '/^%(user)s:.*/d' %(passwd_path)s
sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s
@ -224,10 +228,10 @@ class PostfixAddressVirtualDomainBackend(ServiceController):
domain = context['domain'] domain = context['domain']
if domain.name != context['local_domain'] and self.is_local_domain(domain): if domain.name != context['local_domain'] and self.is_local_domain(domain):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
[[ $(grep '^\s*%(domain)s\s*$' %(virtual_alias_domains)s) ]] || { if [[ ! $(grep '^\s*%(domain)s\s*$' %(virtual_alias_domains)s) ]]; then
echo '%(domain)s' >> %(virtual_alias_domains)s echo '%(domain)s' >> %(virtual_alias_domains)s
UPDATED_VIRTUAL_ALIAS_DOMAINS=1 UPDATED_VIRTUAL_ALIAS_DOMAINS=1
}""") % context fi""") % context
) )
def is_last_domain(self, domain): def is_last_domain(self, domain):
@ -237,9 +241,10 @@ class PostfixAddressVirtualDomainBackend(ServiceController):
domain = context['domain'] domain = context['domain']
if self.is_last_domain(domain): if self.is_last_domain(domain):
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
sed -i '/^%(domain)s\s*/d;{!q0;q1}' %(virtual_alias_domains)s && \\ if [[ $(grep '^%(domain)s\s*$' %(virtual_alias_domains)s) ]]; then
sed -i '/^%(domain)s\s*/d' %(virtual_alias_domains)s
UPDATED_VIRTUAL_ALIAS_DOMAINS=1 UPDATED_VIRTUAL_ALIAS_DOMAINS=1
""") % context fi""") % context
) )
def save(self, address): def save(self, address):
@ -307,9 +312,10 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend):
else: else:
logger.warning("Address %i is empty" % address.pk) logger.warning("Address %i is empty" % address.pk)
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
sed -i '/^%(email)s\s/d;{!q0;q1}' %(virtual_alias_maps)s && \\ if [[ $(grep '^%(email)s\s' %(virtual_alias_maps)s) ]]; then
sed -i '/^%(email)s\s/d' %(virtual_alias_maps)s
UPDATED_VIRTUAL_ALIAS_MAPS=1 UPDATED_VIRTUAL_ALIAS_MAPS=1
""") % context fi""") % context
) )
# Virtual mailbox stuff # Virtual mailbox stuff
# destination = [] # destination = []

View File

@ -45,6 +45,12 @@ MAILBOXES_SIEVETEST_BIN_PATH = Setting('MAILBOXES_SIEVETEST_BIN_PATH',
) )
MAILBOXES_USE_ACCOUNT_AS_GROUP = Setting('MAILBOXES_USE_ACCOUNT_AS_GROUP',
False,
help_text="Group used for system user based mailboxes. If <tt>False</tt> mailbox.name will be used as group."
)
MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH = Setting('MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH', MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH = Setting('MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH',
'/etc/postfix/virtual_mailboxes' '/etc/postfix/virtual_mailboxes'
) )

View File

@ -87,9 +87,9 @@ def message_user(request, logs):
if log.state != log.EXCEPTION: if log.state != log.EXCEPTION:
# EXCEPTION logs are not stored on the database # EXCEPTION logs are not stored on the database
ids.append(log.pk) ids.append(log.pk)
if log.state in (log.SUCCESS, log.NOTHING): if log.is_success:
successes += 1 successes += 1
elif log.state in (log.RECEIVED, log.STARTED): elif not log.has_finished:
async += 1 async += 1
async_ids.append(log.id) async_ids.append(log.id)
errors = total-successes-async errors = total-successes-async

View File

@ -79,7 +79,7 @@ class Command(BaseCommand):
route, __ = key route, __ = key
backend, operations = value backend, operations = value
servers.append(str(route.host)) servers.append(str(route.host))
self.stdout.write('# Execute on %s' % server.name) self.stdout.write('# Execute on %s' % route.host)
for method, commands in backend.scripts: for method, commands in backend.scripts:
script = '\n'.join(commands) script = '\n'.join(commands)
self.stdout.write(script) self.stdout.write(script)

View File

@ -28,7 +28,7 @@ def keep_log(execute, log, operations):
log = kwargs['log'] log = kwargs['log']
try: try:
log = execute(*args, **kwargs) log = execute(*args, **kwargs)
if log.state != log.SUCCESS: if not log.is_success:
send_report(execute, args, log) send_report(execute, args, log)
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()

View File

@ -96,6 +96,10 @@ class BackendLog(models.Model):
def has_finished(self): def has_finished(self):
return self.state not in (self.STARTED, self.RECEIVED) return self.state not in (self.STARTED, self.RECEIVED)
@property
def is_success(self):
return self.state in (self.SUCCESS, self.NOTHING)
def backend_class(self): def backend_class(self):
return ServiceBackend.get_backend(self.backend) return ServiceBackend.get_backend(self.backend)

View File

@ -12,13 +12,16 @@ from . import settings
class UNIXUserBackend(ServiceController): class UNIXUserBackend(ServiceController):
""" """
Basic UNIX system user/group support based on <tt>useradd</tt>, <tt>usermod</tt>, <tt>userdel</tt> and <tt>groupdel</tt>. Basic UNIX system user/group support based on <tt>useradd</tt>, <tt>usermod</tt>, <tt>userdel</tt> and <tt>groupdel</tt>.
Autodetects and uses ACL if available, for better permission management.
""" """
verbose_name = _("UNIX user") verbose_name = _("UNIX user")
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
actions = ('save', 'delete', 'set_permission', 'validate_path') actions = ('save', 'delete', 'set_permission', 'validate_path_exists')
doc_settings = (settings, doc_settings = (settings, (
('SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', 'SYSTEMUSERS_MOVE_ON_DELETE_PATH') 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS',
) 'SYSTEMUSERS_MOVE_ON_DELETE_PATH',
'SYSTEMUSERS_FORBIDDEN_PATHS'
))
def save(self, user): def save(self, user):
context = self.get_context(user) context = self.get_context(user)
@ -33,16 +36,25 @@ class UNIXUserBackend(ServiceController):
else else
useradd %(user)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s useradd %(user)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
fi fi
mkdir -p %(home)s
chmod 750 %(home)s
chown %(user)s:%(user)s %(home)s""") % context
)
if context['home'] != context['base_home']:
self.append(textwrap.dedent("""
mkdir -p %(base_home)s mkdir -p %(base_home)s
chmod 750 %(base_home)s chmod 750 %(base_home)s
chown %(user)s:%(user)s %(base_home)s""") % context chown %(user)s:%(user)s %(base_home)s""") % context
) )
if context['home'] != context['base_home']:
self.append(textwrap.dedent("""
if [[ $(mount | grep "^$(df %(home)s|grep '^/')\s" | grep acl) ]]; then
chown %(mainuser)s:%(mainuser)s %(home)s
# Home access
setfacl -m u:%(user)s:--x '%(mainuser_home)s'
# Grant perms to future files within the directory
setfacl -m d:u:%(user)s:rwx %(home)s
# Grant access to main user
setfacl -m d:u:%(mainuser)s:rwx %(home)s
else
chmod g+rxw %(home)s
chown %(user)s:%(user)s %(home)s
fi""") % context
)
for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS: for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
context['member'] = member context['member'] = member
self.append('usermod -a -G %(user)s %(member)s || exit_code=$?' % context) self.append('usermod -a -G %(user)s %(member)s || exit_code=$?' % context)
@ -54,7 +66,7 @@ class UNIXUserBackend(ServiceController):
if not context['user']: if not context['user']:
return return
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
{ sleep 2 && killall -u %(user)s -s KILL; } & nohup bash -c 'sleep 2 && killall -u %(user)s -s KILL' &> /dev/null &
killall -u %(user)s || true killall -u %(user)s || true
userdel %(user)s || exit_code=$? userdel %(user)s || exit_code=$?
groupdel %(group)s || exit_code=$? groupdel %(group)s || exit_code=$?
@ -71,15 +83,12 @@ class UNIXUserBackend(ServiceController):
'perm_action': user.set_perm_action, 'perm_action': user.set_perm_action,
'perm_home': user.set_perm_base_home, 'perm_home': user.set_perm_base_home,
'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension), 'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension),
'exclude': '',
}) })
exclude_acl = [] exclude_acl = []
for exclude in settings.SYSTEMUSERS_EXLUDE_ACL_PATHS: for exclude in settings.SYSTEMUSERS_FORBIDDEN_PATHS:
context['exclude'] = exclude context['exclude_acl'] = exclude
exclude_acl.append('-not -path "%(perm_home)s/%(exclude)s"' % context) exclude_acl.append('-not -path "%(perm_to)s/%(exclude_acl)s"' % context)
if exclude_acl: context['exclude_acl'] = ' \\\n -a '.join(exclude_acl) if exclude_acl else ''
context['exclude'] = ' \\\n -a '.join(exclude_acl)
if user.set_perm_perms == 'read-write': if user.set_perm_perms == 'read-write':
context['perm_perms'] = 'rwx' if user.set_perm_action == 'grant' else '---' context['perm_perms'] = 'rwx' if user.set_perm_action == 'grant' else '---'
elif user.set_perm_perms == 'read-only': elif user.set_perm_perms == 'read-only':
@ -91,9 +100,9 @@ class UNIXUserBackend(ServiceController):
# Home access # Home access
setfacl -m u:%(user)s:--x '%(perm_home)s' setfacl -m u:%(user)s:--x '%(perm_home)s'
# Grant perms to existing and future files # Grant perms to existing and future files
find '%(perm_to)s' %(exclude)s \\ find '%(perm_to)s' %(exclude_acl)s \\
-exec setfacl -m u:%(user)s:%(perm_perms)s {} \\; -exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;
find '%(perm_to)s' -type d %(exclude)s \\ find '%(perm_to)s' -type d %(exclude_acl)s \\
-exec setfacl -m d:u:%(user)s:%(perm_perms)s {} \\; -exec setfacl -m d:u:%(user)s:%(perm_perms)s {} \\;
# Account group as the owner of new files # Account group as the owner of new files
chmod g+s '%(perm_to)s' chmod g+s '%(perm_to)s'
@ -102,28 +111,27 @@ class UNIXUserBackend(ServiceController):
if not user.is_main: if not user.is_main:
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
# Grant access to main user # Grant access to main user
find '%(perm_to)s' -type d %(exclude)s \\ find '%(perm_to)s' -type d %(exclude_acl)s \\
-exec setfacl -m d:u:%(mainuser)s:rwx {} \\; -exec setfacl -m d:u:%(mainuser)s:rwx {} \\;
""") % context """) % context
) )
elif user.set_perm_action == 'revoke': elif user.set_perm_action == 'revoke':
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
# Revoke permissions # Revoke permissions
find '%(perm_to)s' %(exclude)s \\ find '%(perm_to)s' %(exclude_acl)s \\
-exec setfacl -m u:%(user)s:%(perm_perms)s {} \\; -exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;
""") % context """) % context
) )
else: else:
raise NotImplementedError() raise NotImplementedError()
def validate_path(self, user): def validate_path_exists(self, user):
context = { context = {
'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension) 'path': user.path_to_validate,
} }
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
if [[ ! -e '%(perm_to)s' ]]; then if [[ ! -e '%(path)s' ]]; then
echo "%(perm_to)s path does not exists." >&2 echo "%(path)s path does not exists." >&2
exit 1
fi fi
""") % context """) % context
) )
@ -143,6 +151,7 @@ class UNIXUserBackend(ServiceController):
'mainuser': user.username if user.is_main else user.account.username, 'mainuser': user.username if user.is_main else user.account.username,
'home': user.get_home(), 'home': user.get_home(),
'base_home': user.get_base_home(), 'base_home': user.get_base_home(),
'mainuser_home': user.main.get_home(),
} }
context['deleted_home'] = settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH % context context['deleted_home'] = settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH % context
return replace(context, "'", '"') return replace(context, "'", '"')

View File

@ -4,12 +4,11 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ngettext, ugettext_lazy as _ from django.utils.translation import ngettext, ugettext_lazy as _
from orchestra.contrib.orchestration import Operation
from orchestra.forms import UserCreationForm, UserChangeForm from orchestra.forms import UserCreationForm, UserChangeForm
from . import settings from . import settings
from .models import SystemUser from .models import SystemUser
from .validators import validate_home from .validators import validate_home, validate_path_exists
class SystemUserFormMixin(object): class SystemUserFormMixin(object):
@ -66,11 +65,13 @@ class SystemUserFormMixin(object):
def clean(self): def clean(self):
super(SystemUserFormMixin, self).clean() super(SystemUserFormMixin, self).clean()
home = self.cleaned_data.get('home') cleaned_data = self.cleaned_data
home = cleaned_data.get('home')
if home and self.MOCK_USERNAME in home: if home and self.MOCK_USERNAME in home:
username = self.cleaned_data.get('username', '') username = cleaned_data.get('username', '')
self.cleaned_data['home'] = home.replace(self.MOCK_USERNAME, username) cleaned_data['home'] = home.replace(self.MOCK_USERNAME, username)
validate_home(self.instance, self.cleaned_data, self.account) validate_home(self.instance, cleaned_data, self.account)
return cleaned_data
class SystemUserCreationForm(SystemUserFormMixin, UserCreationForm): class SystemUserCreationForm(SystemUserFormMixin, UserCreationForm):
@ -111,14 +112,11 @@ class PermissionForm(forms.Form):
def clean(self): def clean(self):
cleaned_data = super(PermissionForm, self).clean() cleaned_data = super(PermissionForm, self).clean()
user = self.instance path = os.path.join(cleaned_data['base_home'], cleaned_data['home_extension'])
user.set_perm_action = cleaned_data['set_action'] try:
user.set_perm_base_home = cleaned_data['base_home'] validate_path_exists(self.instance, path)
user.set_perm_home_extension = cleaned_data['home_extension'] except ValidationError as err:
user.set_perm_perms = cleaned_data['permissions']
log = Operation.execute_action(user, 'validate_path')[0]
if 'path does not exists' in log.stderr:
raise ValidationError({ raise ValidationError({
'home_extension': log.stderr, 'home_extension': err,
}) })
return cleaned_data return cleaned_data

View File

@ -1,3 +1,4 @@
import fnmatch
import os import os
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
@ -64,6 +65,10 @@ 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
@cached_property
def main(self):
return self.account.main_systemuser
@property @property
def has_shell(self): def has_shell(self):
return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS
@ -84,16 +89,20 @@ class SystemUser(models.Model):
if self.home: if self.home:
self.home = os.path.normpath(self.home) self.home = os.path.normpath(self.home)
if self.directory: if self.directory:
directory_error = None self.directory = os.path.normpath(self.directory)
dir_errors = []
if self.has_shell: if self.has_shell:
directory_error = _("Directory with shell users can not be specified.") dir_errors.append(_("Directory with shell users can not be specified."))
elif self.account_id and self.is_main: elif self.account_id and self.is_main:
directory_error = _("Directory with main system users can not be specified.") dir_errors.append(_("Directory with main system users can not be specified."))
elif self.home == self.get_base_home(): elif self.home == self.get_base_home():
directory_error = _("Directory on the user's base home is not allowed.") dir_errors.append(_("Directory on the user's base home is not allowed."))
if directory_error: for pattern in settings.SYSTEMUSERS_FORBIDDEN_PATHS:
if fnmatch.fnmatch(self.directory, pattern):
dir_errors.append(_("Provided directory is forbidden."))
if dir_errors:
raise ValidationError({ raise ValidationError({
'directory': directory_error, 'directory': [ValidationError(error) for error in dir_errors]
}) })
if self.has_shell and self.home and self.home != self.get_base_home(): if self.has_shell and self.home and self.home != self.get_base_home():
raise ValidationError({ raise ValidationError({

View File

@ -60,8 +60,8 @@ SYSTEMUSERS_MOVE_ON_DELETE_PATH = Setting('SYSTEMUSERS_MOVE_ON_DELETE_PATH',
) )
SYSTEMUSERS_EXLUDE_ACL_PATHS = Setting('SYSTEMUSERS_EXLUDE_ACL_PATHS', SYSTEMUSERS_FORBIDDEN_PATHS = Setting('SYSTEMUSERS_FORBIDDEN_PATHS',
(), (),
help_text=("Exlude ACL operations on provided globs, relative to user's home.<br>" help_text=("Exlude ACL operations or home locations on provided globs, relative to user's home.<br>"
"e.g. ('logs', 'logs/apache*', 'webapps')"), "e.g. ('logs', 'logs/apache*', 'webapps')"),
) )

View File

@ -0,0 +1,74 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls static utils %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/hide-inline-id.css" %}" />
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% if obj %}
&rsaquo; <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
&rsaquo; {{ action_name }}
{% elif add %}
&rsaquo; <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
&rsaquo; {{ action_name }}
{% else %}
&rsaquo; {{ action_name }} multiple objects
{% endif %}
</div>
{% endblock %}
{% block content %}
<div>
<div style="margin:20px;">
Set permissions for {% for user in queryset %}{{ user.username }}{% if not forloop.last %}, {% endif %}{% endfor %} system user(s).
<ul>{{ display_objects | unordered_list }}</ul>
<form action="" method="post">{% csrf_token %}
<fieldset class="module aligned wide">
{{ form.non_field_errors }}
<div class="form-row ">
{{ form.set_action.errors }}
<label for="{{ form.set_action.id_for_label }}">{{ form.set_action.label }}:</label>
{{ form.set_action }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.set_action.help_text|safe }}</p>
</div>
<div class="form-row ">
<div class="field-box field-base_home">
{{ form.base_home.errors }}
<label for="{{ form.base_home.id_for_label }}">{{ form.base_home.label }}:</label>
{{ form.base_home }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.base_home.help_text|safe }}</p>
</div>
<div class="field-box field-user_extension">
{{ form.home_extension.errors }}
<label for="{{ form.home_extension.id_for_label }}"></label>
{{ form.home_extension }}
<p class="help">{{ form.home_extension.help_text|safe }}</p>
</div>
</div>
<div class="form-row ">
{{ form.permissions.errors }}
<label for="{{ form.base_path.id_for_label }}">{{ form.permissions.label }}:</label>
{{ form.permissions }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<p class="help">{{ form.permissions.help_text|safe }}</p>
</div>
</fieldset>
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="{{ action_value }}" />
<input type="hidden" name="post" value="{{ post_value|default:'generic_confirmation' }}" />
<input type="submit" value="{{ submit_value|default:_("Save") }}" />
</div>
</form>
{% endblock %}

View File

@ -2,6 +2,15 @@ import os
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from orchestra.contrib.orchestration import Operation
def validate_path_exists(user, path, ):
user.path_to_validate = path
log = Operation.execute_action(user, 'validate_path_exists')[0]
if 'path does not exists' in log.stderr:
raise ValidationError(log.stderr)
def validate_home(user, data, account): def validate_home(user, data, account):
""" validates home based on account and data['shell'] """ """ validates home based on account and data['shell'] """
@ -25,3 +34,11 @@ def validate_home(user, data, account):
raise ValidationError({ raise ValidationError({
'home': _("Not a valid home directory.") 'home': _("Not a valid home directory.")
}) })
if 'directory' in data and data['directory']:
path = os.path.join(data['home'], data['directory'])
try:
validate_path_exists(user, path)
except ValidationError as err:
raise ValidationError({
'directory': err,
})

View File

@ -29,14 +29,13 @@ class WebAppServiceMixin(object):
if context['under_construction_path']: if context['under_construction_path']:
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
if [[ $CREATED == 1 && ! $(ls -A %(app_path)s) ]]; then if [[ $CREATED == 1 && ! $(ls -A %(app_path)s) ]]; then
{ # Async wait for other backends to do their thing or cp under construction
# Wait for other backends to do their thing or cp under construction nohup bash -c '
sleep 10 sleep 10
if [[ ! $(ls -A %(app_path)s) ]]; then if [[ ! $(ls -A %(app_path)s) ]]; then
cp -r %(under_construction_path)s %(app_path)s cp -r %(under_construction_path)s %(app_path)s
chown -R %(user)s:%(group)s %(app_path)s chown -R %(user)s:%(group)s %(app_path)s
fi fi' &> /dev/null &
} &
fi""") % context fi""") % context
) )

View File

@ -127,6 +127,9 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
mv /dev/shm/restart.apache2 /dev/shm/restart.apache2.locked mv /dev/shm/restart.apache2 /dev/shm/restart.apache2.locked
} }
state="$(grep -v "$backend" /dev/shm/restart.apache2.locked)" || is_last=1 state="$(grep -v "$backend" /dev/shm/restart.apache2.locked)" || is_last=1
[[ $is_last -eq 0 ]] && {
echo "$state" | grep -v ' RESTART$' || is_last=1
}
if [[ $is_last -eq 1 ]]; then if [[ $is_last -eq 1 ]]; then
if [[ $UPDATED_APACHE -eq 1 || "$state" =~ .*RESTART$ ]]; then if [[ $UPDATED_APACHE -eq 1 || "$state" =~ .*RESTART$ ]]; then
service apache2 status && service apache2 reload || service apache2 start service apache2 status && service apache2 reload || service apache2 start

View File

@ -1,3 +1,4 @@
import os
import textwrap import textwrap
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -41,11 +42,19 @@ class WordPressBackend(WebAppServiceMixin, ServiceController):
if (count(glob("%(app_path)s/*")) > 1) { if (count(glob("%(app_path)s/*")) > 1) {
die("App directory not empty."); die("App directory not empty.");
} }
exc('mkdir -p %(app_path)s'); shell_exec("mkdir -p %(app_path)s
exc('rm -f %(app_path)s/index.html'); rm -f %(app_path)s/index.html
exc('wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate | tar -xzvf - -C %(app_path)s --strip-components=1'); filename=\\$(wget https://wordpress.org/latest.tar.gz --server-response --spider --no-check-certificate 2>&1 | grep filename | cut -d'=' -f2)
exc('mkdir %(app_path)s/wp-content/uploads'); mkdir -p %(cms_cache_dir)s
exc('chmod 750 %(app_path)s/wp-content/uploads'); if [ \\$(basename \\$(readlink %(cms_cache_dir)s/wordpress) 2> /dev/null ) != \\$filename ]; then
wget https://wordpress.org/latest.tar.gz -O - --no-check-certificate | tee %(cms_cache_dir)s/\\$filename | tar -xzvf - -C %(app_path)s --strip-components=1
rm -f %(cms_cache_dir)s/wordpress
ln -s %(cms_cache_dir)s/\\$filename %(cms_cache_dir)s/wordpress
else
tar -xzvf %(cms_cache_dir)s/wordpress -C %(app_path)s --strip-components=1
fi
mkdir %(app_path)s/wp-content/uploads
chmod 750 %(app_path)s/wp-content/uploads");
$config_file = file('%(app_path)s/' . 'wp-config-sample.php'); $config_file = file('%(app_path)s/' . 'wp-config-sample.php');
$secret_keys = file_get_contents('https://api.wordpress.org/secret-key/1.1/salt/'); $secret_keys = file_get_contents('https://api.wordpress.org/secret-key/1.1/salt/');
@ -124,5 +133,6 @@ class WordPressBackend(WebAppServiceMixin, ServiceController):
'db_host': settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST, 'db_host': settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST,
'email': webapp.account.email, 'email': webapp.account.email,
'title': "%s blog's" % webapp.account.get_full_name(), 'title': "%s blog's" % webapp.account.get_full_name(),
'cms_cache_dir': os.path.normpath(settings.WEBAPPS_CMS_CACHE_DIR)
}) })
return replace(context, '"', "'") return replace(context, '"', "'")

View File

@ -261,3 +261,10 @@ WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = Setting('WEBAPPS_DEFAULT_MYSQL_DATABASE_HO
WEBAPPS_MOVE_ON_DELETE_PATH = Setting('WEBAPPS_MOVE_ON_DELETE_PATH', WEBAPPS_MOVE_ON_DELETE_PATH = Setting('WEBAPPS_MOVE_ON_DELETE_PATH',
'' ''
) )
WEBAPPS_CMS_CACHE_DIR = Setting('WEBAPPS_CMS_CACHE_DIR',
'/tmp/orchestra_cms_cache',
help_text="Server-side cache directori for CMS tarballs.",
)

View File

@ -148,6 +148,9 @@ class Apache2Backend(ServiceController):
mv /dev/shm/restart.apache2 /dev/shm/restart.apache2.locked mv /dev/shm/restart.apache2 /dev/shm/restart.apache2.locked
} }
state="$(grep -v "$backend" /dev/shm/restart.apache2.locked)" || is_last=1 state="$(grep -v "$backend" /dev/shm/restart.apache2.locked)" || is_last=1
[[ $is_last -eq 0 ]] && {
echo "$state" | grep -v ' RESTART$' || is_last=1
}
if [[ $is_last -eq 1 ]]; then if [[ $is_last -eq 1 ]]; then
if [[ $UPDATED_APACHE -eq 1 || "$state" =~ .*RESTART$ ]]; then if [[ $UPDATED_APACHE -eq 1 || "$state" =~ .*RESTART$ ]]; then
service apache2 status && service apache2 reload || service apache2 start service apache2 status && service apache2 reload || service apache2 start