From b5ede038587b64e4b8693269f1bd3773c386c747 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Tue, 12 May 2015 12:38:40 +0000 Subject: [PATCH] Improved ACL support --- TODO.md | 2 + orchestra/contrib/accounts/admin.py | 15 ++-- orchestra/contrib/domains/backends.py | 7 +- orchestra/contrib/domains/models.py | 3 +- orchestra/contrib/domains/validators.py | 2 +- orchestra/contrib/lists/backends.py | 17 +++-- orchestra/contrib/mailboxes/backends.py | 26 ++++--- orchestra/contrib/mailboxes/settings.py | 6 ++ orchestra/contrib/orchestration/helpers.py | 4 +- .../management/commands/orchestrate.py | 2 +- orchestra/contrib/orchestration/manager.py | 2 +- orchestra/contrib/orchestration/models.py | 4 + orchestra/contrib/systemusers/backends.py | 63 +++++++++------- orchestra/contrib/systemusers/forms.py | 26 +++---- orchestra/contrib/systemusers/models.py | 21 ++++-- orchestra/contrib/systemusers/settings.py | 4 +- .../systemuser/set_permission.html | 74 +++++++++++++++++++ orchestra/contrib/systemusers/validators.py | 17 +++++ .../contrib/webapps/backends/__init__.py | 7 +- orchestra/contrib/webapps/backends/php.py | 3 + .../contrib/webapps/backends/wordpress.py | 20 +++-- orchestra/contrib/webapps/settings.py | 7 ++ orchestra/contrib/websites/backends/apache.py | 3 + 23 files changed, 248 insertions(+), 87 deletions(-) create mode 100644 orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html diff --git a/TODO.md b/TODO.md index d1724296..d8dfef36 100644 --- a/TODO.md +++ b/TODO.md @@ -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 # best_price rating method + +# select contact with one result: redirect diff --git a/orchestra/contrib/accounts/admin.py b/orchestra/contrib/accounts/admin.py index 2d5e4e6c..e0a7a61e 100644 --- a/orchestra/contrib/accounts/admin.py +++ b/orchestra/contrib/accounts/admin.py @@ -58,7 +58,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) add_form = AccountCreationForm form = UserChangeForm 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' actions = [disable, list_contacts, service_report, SendEmail(), delete_related_services] change_view_actions = [disable, service_report] @@ -139,8 +139,14 @@ class AccountListAdmin(AccountAdmin): 'original_model': original_model, } context.update(extra_context or {}) - return super(AccountListAdmin, self).changelist_view(request, - extra_context=context) + response = super(AccountListAdmin, self).changelist_view(request, 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): @@ -283,8 +289,7 @@ class AccountAdminMixin(object): request_copy.pop('account') request.GET = request_copy context.update(extra_context or {}) - return super(AccountAdminMixin, self).changelist_view(request, - extra_context=context) + return super(AccountAdminMixin, self).changelist_view(request, extra_context=context) class SelectAccountAdminMixin(AccountAdminMixin): diff --git a/orchestra/contrib/domains/backends.py b/orchestra/contrib/domains/backends.py index c666ade3..99c2b04a 100644 --- a/orchestra/contrib/domains/backends.py +++ b/orchestra/contrib/domains/backends.py @@ -177,7 +177,12 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend): def commit(self): """ 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): context = { diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index f73743bb..7fae0ba1 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -252,7 +252,8 @@ class Record(models.Model): def clean(self): """ validates record value based on its type """ # validate value - self.value = self.value.lower().strip() + if self.type != self.TXT: + self.value = self.value.lower().strip() choices = { self.MX: validators.validate_mx_record, self.NS: validators.validate_zone_label, diff --git a/orchestra/contrib/domains/validators.py b/orchestra/contrib/domains/validators.py index a5d786d4..e3e8114e 100644 --- a/orchestra/contrib/domains/validators.py +++ b/orchestra/contrib/domains/validators.py @@ -124,5 +124,5 @@ def validate_zone(zone): if check.exit_code == 127: logger.error("Cannot validate domain zone: %s not installed." % checkzone) 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)) diff --git a/orchestra/contrib/lists/backends.py b/orchestra/contrib/lists/backends.py index c4174644..f50aeeef 100644 --- a/orchestra/contrib/lists/backends.py +++ b/orchestra/contrib/lists/backends.py @@ -78,7 +78,7 @@ class MailmanBackend(MailmanVirtualDomainBackend): Includes MailmanVirtualDomainBackend """ verbose_name = "Mailman" - addresses = [ + address_suffixes = [ '', '-admin', '-bounces', @@ -99,9 +99,12 @@ class MailmanBackend(MailmanVirtualDomainBackend): def get_virtual_aliases(self, context): aliases = ['# %(banner)s' % context] - for address in self.addresses: - context['address'] = address - aliases.append("%(address_name)s%(address)s@%(domain)s\t%(name)s%(address)s" % context) + for suffix in self.address_suffixes: + context['suffix'] = suffix + # 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) def save(self, mail_list): @@ -122,7 +125,7 @@ class MailmanBackend(MailmanVirtualDomainBackend): UPDATED_VIRTUAL_ALIAS=1 else 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 echo "${aliases}" >> %(virtual_alias)s UPDATED_VIRTUAL_ALIAS=1 @@ -159,7 +162,7 @@ class MailmanBackend(MailmanVirtualDomainBackend): context = self.get_context(mail_list) self.exclude_virtual_alias_domain(context) 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 ) self.append(textwrap.dedent(""" @@ -201,7 +204,7 @@ class MailmanBackend(MailmanVirtualDomainBackend): 'domain': mail_list.address_domain or settings.LISTS_DEFAULT_DOMAIN, 'address_name': mail_list.get_address_name(), 'address_domain': mail_list.address_domain, - 'address_regex': '\|'.join(self.addresses), + 'suffixes_regex': '\|'.join(self.address_suffixes), 'admin': mail_list.admin_email, 'mailman_root': settings.LISTS_MAILMAN_ROOT_DIR, }) diff --git a/orchestra/contrib/mailboxes/backends.py b/orchestra/contrib/mailboxes/backends.py index 30c48aeb..e007c9f1 100644 --- a/orchestra/contrib/mailboxes/backends.py +++ b/orchestra/contrib/mailboxes/backends.py @@ -48,12 +48,16 @@ class SieveFilteringMixin(object): class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController): """ 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 resources.disk.allocated. """ SHELL = '/dev/null' verbose_name = _("UNIX maildir user") model = 'mailboxes.Mailbox' + doc_settings = (settings, + ('MAILBOXES_USE_ACCOUNT_AS_GROUP',) + ) def save(self, mailbox): context = self.get_context(mailbox) @@ -89,7 +93,7 @@ class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController): context = self.get_context(mailbox) self.append('mv %(home)s %(home)s.deleted || exit_code=$?' % context) 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 userdel %(user)s || true groupdel %(user)s || true""") % context @@ -98,7 +102,7 @@ class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController): def get_context(self, mailbox): context = { 'user': mailbox.name, - 'group': mailbox.name, + 'group': mailbox.account.username if settings.MAILBOXES_USE_ACCOUNT_AS_GROUP else mailbox.name, 'name': mailbox.name, 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password, 'home': mailbox.get_home(), @@ -147,7 +151,7 @@ class DovecotPostfixPasswdVirtualUserBackend(SieveFilteringMixin, ServiceControl def delete(self, mailbox): context = self.get_context(mailbox) 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 sed -i '/^%(user)s:.*/d' %(passwd_path)s sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s @@ -224,10 +228,10 @@ class PostfixAddressVirtualDomainBackend(ServiceController): domain = context['domain'] if domain.name != context['local_domain'] and self.is_local_domain(domain): 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 UPDATED_VIRTUAL_ALIAS_DOMAINS=1 - }""") % context + fi""") % context ) def is_last_domain(self, domain): @@ -237,9 +241,10 @@ class PostfixAddressVirtualDomainBackend(ServiceController): domain = context['domain'] if self.is_last_domain(domain): 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 - """) % context + fi""") % context ) def save(self, address): @@ -307,9 +312,10 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend): else: logger.warning("Address %i is empty" % address.pk) 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 - """) % context + fi""") % context ) # Virtual mailbox stuff # destination = [] diff --git a/orchestra/contrib/mailboxes/settings.py b/orchestra/contrib/mailboxes/settings.py index bd59983a..3cbc3f39 100644 --- a/orchestra/contrib/mailboxes/settings.py +++ b/orchestra/contrib/mailboxes/settings.py @@ -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 False mailbox.name will be used as group." +) + + MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH = Setting('MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH', '/etc/postfix/virtual_mailboxes' ) diff --git a/orchestra/contrib/orchestration/helpers.py b/orchestra/contrib/orchestration/helpers.py index c0c0f8e4..e93748f1 100644 --- a/orchestra/contrib/orchestration/helpers.py +++ b/orchestra/contrib/orchestration/helpers.py @@ -87,9 +87,9 @@ def message_user(request, logs): if log.state != log.EXCEPTION: # EXCEPTION logs are not stored on the database ids.append(log.pk) - if log.state in (log.SUCCESS, log.NOTHING): + if log.is_success: successes += 1 - elif log.state in (log.RECEIVED, log.STARTED): + elif not log.has_finished: async += 1 async_ids.append(log.id) errors = total-successes-async diff --git a/orchestra/contrib/orchestration/management/commands/orchestrate.py b/orchestra/contrib/orchestration/management/commands/orchestrate.py index 33cf0d0b..d4a35ee3 100644 --- a/orchestra/contrib/orchestration/management/commands/orchestrate.py +++ b/orchestra/contrib/orchestration/management/commands/orchestrate.py @@ -79,7 +79,7 @@ class Command(BaseCommand): route, __ = key backend, operations = value 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: script = '\n'.join(commands) self.stdout.write(script) diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py index 3136353e..8e3390c2 100644 --- a/orchestra/contrib/orchestration/manager.py +++ b/orchestra/contrib/orchestration/manager.py @@ -28,7 +28,7 @@ def keep_log(execute, log, operations): log = kwargs['log'] try: log = execute(*args, **kwargs) - if log.state != log.SUCCESS: + if not log.is_success: send_report(execute, args, log) except Exception as e: trace = traceback.format_exc() diff --git a/orchestra/contrib/orchestration/models.py b/orchestra/contrib/orchestration/models.py index a5c735d9..328176a9 100644 --- a/orchestra/contrib/orchestration/models.py +++ b/orchestra/contrib/orchestration/models.py @@ -96,6 +96,10 @@ class BackendLog(models.Model): def has_finished(self): 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): return ServiceBackend.get_backend(self.backend) diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index 8a92ebf7..13179a0a 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -12,13 +12,16 @@ from . import settings class UNIXUserBackend(ServiceController): """ Basic UNIX system user/group support based on useradd, usermod, userdel and groupdel. + Autodetects and uses ACL if available, for better permission management. """ verbose_name = _("UNIX user") model = 'systemusers.SystemUser' - actions = ('save', 'delete', 'set_permission', 'validate_path') - doc_settings = (settings, - ('SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', 'SYSTEMUSERS_MOVE_ON_DELETE_PATH') - ) + actions = ('save', 'delete', 'set_permission', 'validate_path_exists') + doc_settings = (settings, ( + 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', + 'SYSTEMUSERS_MOVE_ON_DELETE_PATH', + 'SYSTEMUSERS_FORBIDDEN_PATHS' + )) def save(self, user): context = self.get_context(user) @@ -33,15 +36,24 @@ class UNIXUserBackend(ServiceController): else useradd %(user)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s fi - mkdir -p %(home)s - chmod 750 %(home)s - chown %(user)s:%(user)s %(home)s""") % context + mkdir -p %(base_home)s + chmod 750 %(base_home)s + chown %(user)s:%(user)s %(base_home)s""") % context ) if context['home'] != context['base_home']: self.append(textwrap.dedent(""" - mkdir -p %(base_home)s - chmod 750 %(base_home)s - chown %(user)s:%(user)s %(base_home)s""") % context + 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: context['member'] = member @@ -54,7 +66,7 @@ class UNIXUserBackend(ServiceController): if not context['user']: return 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 userdel %(user)s || exit_code=$? groupdel %(group)s || exit_code=$? @@ -71,15 +83,12 @@ class UNIXUserBackend(ServiceController): 'perm_action': user.set_perm_action, 'perm_home': user.set_perm_base_home, 'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension), - 'exclude': '', }) - exclude_acl = [] - for exclude in settings.SYSTEMUSERS_EXLUDE_ACL_PATHS: - context['exclude'] = exclude - exclude_acl.append('-not -path "%(perm_home)s/%(exclude)s"' % context) - if exclude_acl: - context['exclude'] = ' \\\n -a '.join(exclude_acl) + for exclude in settings.SYSTEMUSERS_FORBIDDEN_PATHS: + context['exclude_acl'] = exclude + exclude_acl.append('-not -path "%(perm_to)s/%(exclude_acl)s"' % context) + context['exclude_acl'] = ' \\\n -a '.join(exclude_acl) if exclude_acl else '' if user.set_perm_perms == 'read-write': context['perm_perms'] = 'rwx' if user.set_perm_action == 'grant' else '---' elif user.set_perm_perms == 'read-only': @@ -91,9 +100,9 @@ class UNIXUserBackend(ServiceController): # Home access setfacl -m u:%(user)s:--x '%(perm_home)s' # 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 {} \\; - 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 {} \\; # Account group as the owner of new files chmod g+s '%(perm_to)s' @@ -102,28 +111,27 @@ class UNIXUserBackend(ServiceController): if not user.is_main: self.append(textwrap.dedent("""\ # 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 {} \\; """) % context ) elif user.set_perm_action == 'revoke': self.append(textwrap.dedent("""\ # Revoke permissions - find '%(perm_to)s' %(exclude)s \\ + find '%(perm_to)s' %(exclude_acl)s \\ -exec setfacl -m u:%(user)s:%(perm_perms)s {} \\; """) % context ) else: raise NotImplementedError() - def validate_path(self, user): + def validate_path_exists(self, user): 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("""\ - if [[ ! -e '%(perm_to)s' ]]; then - echo "%(perm_to)s path does not exists." >&2 - exit 1 + if [[ ! -e '%(path)s' ]]; then + echo "%(path)s path does not exists." >&2 fi """) % context ) @@ -143,6 +151,7 @@ class UNIXUserBackend(ServiceController): 'mainuser': user.username if user.is_main else user.account.username, 'home': user.get_home(), 'base_home': user.get_base_home(), + 'mainuser_home': user.main.get_home(), } context['deleted_home'] = settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH % context return replace(context, "'", '"') diff --git a/orchestra/contrib/systemusers/forms.py b/orchestra/contrib/systemusers/forms.py index 825cbd74..6340444e 100644 --- a/orchestra/contrib/systemusers/forms.py +++ b/orchestra/contrib/systemusers/forms.py @@ -4,12 +4,11 @@ from django import forms from django.core.exceptions import ValidationError from django.utils.translation import ngettext, ugettext_lazy as _ -from orchestra.contrib.orchestration import Operation from orchestra.forms import UserCreationForm, UserChangeForm from . import settings from .models import SystemUser -from .validators import validate_home +from .validators import validate_home, validate_path_exists class SystemUserFormMixin(object): @@ -66,11 +65,13 @@ class SystemUserFormMixin(object): def clean(self): 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: - username = self.cleaned_data.get('username', '') - self.cleaned_data['home'] = home.replace(self.MOCK_USERNAME, username) - validate_home(self.instance, self.cleaned_data, self.account) + username = cleaned_data.get('username', '') + cleaned_data['home'] = home.replace(self.MOCK_USERNAME, username) + validate_home(self.instance, cleaned_data, self.account) + return cleaned_data class SystemUserCreationForm(SystemUserFormMixin, UserCreationForm): @@ -111,14 +112,11 @@ class PermissionForm(forms.Form): def clean(self): cleaned_data = super(PermissionForm, self).clean() - user = self.instance - user.set_perm_action = cleaned_data['set_action'] - user.set_perm_base_home = cleaned_data['base_home'] - user.set_perm_home_extension = cleaned_data['home_extension'] - user.set_perm_perms = cleaned_data['permissions'] - log = Operation.execute_action(user, 'validate_path')[0] - if 'path does not exists' in log.stderr: + path = os.path.join(cleaned_data['base_home'], cleaned_data['home_extension']) + try: + validate_path_exists(self.instance, path) + except ValidationError as err: raise ValidationError({ - 'home_extension': log.stderr, + 'home_extension': err, }) return cleaned_data diff --git a/orchestra/contrib/systemusers/models.py b/orchestra/contrib/systemusers/models.py index 90aaa929..ca3f2df1 100644 --- a/orchestra/contrib/systemusers/models.py +++ b/orchestra/contrib/systemusers/models.py @@ -1,3 +1,4 @@ +import fnmatch import os 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.username == self.username + @cached_property + def main(self): + return self.account.main_systemuser + @property def has_shell(self): return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS @@ -84,16 +89,20 @@ class SystemUser(models.Model): if self.home: self.home = os.path.normpath(self.home) if self.directory: - directory_error = None + self.directory = os.path.normpath(self.directory) + dir_errors = [] 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: - 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(): - directory_error = _("Directory on the user's base home is not allowed.") - if directory_error: + dir_errors.append(_("Directory on the user's base home is not allowed.")) + 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({ - '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(): raise ValidationError({ diff --git a/orchestra/contrib/systemusers/settings.py b/orchestra/contrib/systemusers/settings.py index 8a42463e..33efff1c 100644 --- a/orchestra/contrib/systemusers/settings.py +++ b/orchestra/contrib/systemusers/settings.py @@ -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.
" + help_text=("Exlude ACL operations or home locations on provided globs, relative to user's home.
" "e.g. ('logs', 'logs/apache*', 'webapps')"), ) diff --git a/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html new file mode 100644 index 00000000..8570b74f --- /dev/null +++ b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html @@ -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 }} + + +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
+
+ Set permissions for {% for user in queryset %}{{ user.username }}{% if not forloop.last %}, {% endif %}{% endfor %} system user(s). +
    {{ display_objects | unordered_list }}
+
{% csrf_token %} +
+ {{ form.non_field_errors }} +
+ {{ form.set_action.errors }} + + {{ form.set_action }}{% for x in ""|ljust:"50" %} {% endfor %} +

{{ form.set_action.help_text|safe }}

+
+
+
+ {{ form.base_home.errors }} + + {{ form.base_home }}{% for x in ""|ljust:"50" %} {% endfor %} +

{{ form.base_home.help_text|safe }}

+
+
+ {{ form.home_extension.errors }} + + {{ form.home_extension }} +

{{ form.home_extension.help_text|safe }}

+
+
+
+ {{ form.permissions.errors }} + + {{ form.permissions }}{% for x in ""|ljust:"50" %} {% endfor %} +

{{ form.permissions.help_text|safe }}

+
+
+
+ {% for obj in queryset %} + + {% endfor %} + + + +
+
+{% endblock %} + diff --git a/orchestra/contrib/systemusers/validators.py b/orchestra/contrib/systemusers/validators.py index 54670cbe..95e9714c 100644 --- a/orchestra/contrib/systemusers/validators.py +++ b/orchestra/contrib/systemusers/validators.py @@ -2,6 +2,15 @@ import os 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): """ validates home based on account and data['shell'] """ @@ -25,3 +34,11 @@ def validate_home(user, data, account): raise ValidationError({ '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, + }) diff --git a/orchestra/contrib/webapps/backends/__init__.py b/orchestra/contrib/webapps/backends/__init__.py index 1fedf217..b750b114 100644 --- a/orchestra/contrib/webapps/backends/__init__.py +++ b/orchestra/contrib/webapps/backends/__init__.py @@ -29,14 +29,13 @@ class WebAppServiceMixin(object): if context['under_construction_path']: self.append(textwrap.dedent("""\ if [[ $CREATED == 1 && ! $(ls -A %(app_path)s) ]]; then - { - # Wait for other backends to do their thing or cp under construction + # Async wait for other backends to do their thing or cp under construction + nohup bash -c ' sleep 10 if [[ ! $(ls -A %(app_path)s) ]]; then cp -r %(under_construction_path)s %(app_path)s chown -R %(user)s:%(group)s %(app_path)s - fi - } & + fi' &> /dev/null & fi""") % context ) diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py index 6d74cd25..99913353 100644 --- a/orchestra/contrib/webapps/backends/php.py +++ b/orchestra/contrib/webapps/backends/php.py @@ -127,6 +127,9 @@ class PHPBackend(WebAppServiceMixin, ServiceController): mv /dev/shm/restart.apache2 /dev/shm/restart.apache2.locked } 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 [[ $UPDATED_APACHE -eq 1 || "$state" =~ .*RESTART$ ]]; then service apache2 status && service apache2 reload || service apache2 start diff --git a/orchestra/contrib/webapps/backends/wordpress.py b/orchestra/contrib/webapps/backends/wordpress.py index e8fc6ec4..9a31cb6f 100644 --- a/orchestra/contrib/webapps/backends/wordpress.py +++ b/orchestra/contrib/webapps/backends/wordpress.py @@ -1,3 +1,4 @@ +import os import textwrap from django.utils.translation import ugettext_lazy as _ @@ -41,11 +42,19 @@ class WordPressBackend(WebAppServiceMixin, ServiceController): if (count(glob("%(app_path)s/*")) > 1) { die("App directory not empty."); } - exc('mkdir -p %(app_path)s'); - exc('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'); - exc('mkdir %(app_path)s/wp-content/uploads'); - exc('chmod 750 %(app_path)s/wp-content/uploads'); + shell_exec("mkdir -p %(app_path)s + rm -f %(app_path)s/index.html + filename=\\$(wget https://wordpress.org/latest.tar.gz --server-response --spider --no-check-certificate 2>&1 | grep filename | cut -d'=' -f2) + mkdir -p %(cms_cache_dir)s + 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'); $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, 'email': webapp.account.email, 'title': "%s blog's" % webapp.account.get_full_name(), + 'cms_cache_dir': os.path.normpath(settings.WEBAPPS_CMS_CACHE_DIR) }) return replace(context, '"', "'") diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py index 44cfff6a..707866da 100644 --- a/orchestra/contrib/webapps/settings.py +++ b/orchestra/contrib/webapps/settings.py @@ -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_CMS_CACHE_DIR = Setting('WEBAPPS_CMS_CACHE_DIR', + '/tmp/orchestra_cms_cache', + help_text="Server-side cache directori for CMS tarballs.", +) diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index c0da42ad..55e7c8f6 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -148,6 +148,9 @@ class Apache2Backend(ServiceController): mv /dev/shm/restart.apache2 /dev/shm/restart.apache2.locked } 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 [[ $UPDATED_APACHE -eq 1 || "$state" =~ .*RESTART$ ]]; then service apache2 status && service apache2 reload || service apache2 start