From c55cff9a377bdfdbf59a5b99eba5a53aa95ec3a8 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Wed, 25 Mar 2015 17:04:44 +0000 Subject: [PATCH] Improved webapps and saas validation --- TODO.md | 23 +---- orchestra/apps/saas/admin.py | 4 +- orchestra/apps/saas/backends/gitlab.py | 41 +++++--- orchestra/apps/saas/models.py | 6 ++ orchestra/apps/webapps/options.py | 113 ++++++++++------------- orchestra/apps/webapps/types/__init__.py | 10 -- orchestra/apps/webapps/types/php.py | 10 +- orchestra/apps/websites/directives.py | 4 - 8 files changed, 95 insertions(+), 116 deletions(-) diff --git a/TODO.md b/TODO.md index 6dbe10f0..70324e94 100644 --- a/TODO.md +++ b/TODO.md @@ -195,23 +195,20 @@ Php binaries should have this format: /usr/bin/php5.2-cgi * Orchestra global search box on the header, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields -* contain error on plugin missing key (plugin dissabled): NOP, fail hard is better than silently, perhaps fail at starttime? apploading +* contain error on plugin missing key (plugin dissabled): NOP, fail hard is better than silently, perhaps fail at starttime? apploading machinary * contact.alternative_phone on a phone.tooltip, email:to * better validate options and directives (url locations, filesystem paths, etc..) -* filter php deprecated options out based on version * make sure that you understand the risks * full support for deactivation of services/accounts - * Display admin.is_active (disabled account/order by) - + * Display admin.is_active (disabled account special icon and order by support) * lock resource monitoring - * -EXecCGI in common CMS upload locations /wp-upload/upload/uploads * cgi user / pervent shell access @@ -219,14 +216,6 @@ Php binaries should have this format: /usr/bin/php5.2-cgi * disable anonymized list options (mailman) -* webapps directory protection and disable excecgi - -* php-fpm disable execCGI - -* SuexecUserGroup needs to be per app othewise wrapper/fpm user can't be correct - -* wprdess-mu saas app that create a Website object???? - * tags = GenericRelation(TaggedItem, related_query_name='bookmarks') * make home for all systemusers (/home/username) and fix monitors @@ -243,23 +232,17 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl * normurlpath '' return '/' -* rename webapps.type to something more generic - * initial configuration of multisite sas apps with password stored in DATA * webapps installation complete, passowrd protected * saas.initial_password autogenerated (ok because its random and not user provided) vs saas.password /change_Form provided + send email with initial_password -* disable saas apps - * more robust backend error handling, continue executing but exit code > 0 if failure, replace exit_code=0; do_sometging || exit_code=1 * saas require unique emails? connect to backend server to find out because they change * automaitcally set passwords and email users? -* website directives uniquenes validation on serializers - -* gitlab store id, username changes +* website directives uniquenes validation on serializers diff --git a/orchestra/apps/saas/admin.py b/orchestra/apps/saas/admin.py index ff09b933..d8670899 100644 --- a/orchestra/apps/saas/admin.py +++ b/orchestra/apps/saas/admin.py @@ -11,8 +11,8 @@ from .services import SoftwareService class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): - list_display = ('name', 'service', 'display_site_domain', 'account_link') - list_filter = ('service',) + list_display = ('name', 'service', 'display_site_domain', 'account_link', 'is_active') + list_filter = ('service', 'is_active') change_readonly_fields = ('service',) plugin = SoftwareService plugin_field = 'service' diff --git a/orchestra/apps/saas/backends/gitlab.py b/orchestra/apps/saas/backends/gitlab.py index ee7232b7..db101644 100644 --- a/orchestra/apps/saas/backends/gitlab.py +++ b/orchestra/apps/saas/backends/gitlab.py @@ -22,9 +22,10 @@ class GitLabSaaSBackend(ServiceController): user_id = saas.data['user_id'] return self.get_base_url() + '/users/%i' % user_id - def validate_response(self, response, status_codes): + def validate_response(self, response, *status_codes): if response.status_code not in status_codes: raise RuntimeError("[%i] %s" % (response.status_code, response.content)) + return json.loads(response.content) def authenticate(self): login_url = self.get_base_url() + '/session' @@ -33,8 +34,8 @@ class GitLabSaaSBackend(ServiceController): 'password': settings.SAAS_GITLAB_ROOT_PASSWORD, } response = requests.post(login_url, data=data) - self.validate_response(response, [201]) - token = json.loads(response.content)['private_token'] + session = self.validate_response(response, 201) + token = session['private_token'] self.headers = { 'PRIVATE-TOKEN': token, } @@ -49,9 +50,7 @@ class GitLabSaaSBackend(ServiceController): 'name': saas.account.get_full_name(), } response = requests.post(user_url, data=data, headers=self.headers) - self.validate_response(response, [201]) - print response.content - user = json.loads(response.content) + user = self.validate_response(response, 201) saas.data['user_id'] = user['id'] # Using queryset update to avoid triggering backends with the post_save signal type(saas).objects.filter(pk=saas.pk).update(data=saas.data) @@ -60,19 +59,32 @@ class GitLabSaaSBackend(ServiceController): def change_password(self, saas, server): self.authenticate() user_url = self.get_user_url(saas) - data = { - 'password': saas.password, - } - response = requests.patch(user_url, data=data, headers=self.headers) - self.validate_response(response, [200]) - print json.dumps(json.loads(response.content), indent=4) + response = requests.get(user_url, headers=self.headers) + user = self.validate_response(response, 200) + user = json.loads(response.content) + user['password'] = saas.password + response = requests.put(user_url, data=user, headers=self.headers) + user = self.validate_response(response, 200) + print json.dumps(user, indent=4) + + def set_state(self, saas, server): + # TODO http://feedback.gitlab.com/forums/176466-general/suggestions/4098632-add-administrative-api-call-to-block-users + return + self.authenticate() + user_url = self.get_user_url(saas) + response = requests.get(user_url, headers=self.headers) + user = self.validate_response(response, 200) + user['state'] = 'active' if saas.active else 'blocked', + response = requests.patch(user_url, data=user, headers=self.headers) + user = self.validate_response(response, 200) + print json.dumps(user, indent=4) def delete_user(self, saas, server): self.authenticate() user_url = self.get_user_url(saas) response = requests.delete(user_url, headers=self.headers) - self.validate_response(response, [200, 404]) - print json.dumps(json.loads(response.content), indent=4) + user = self.validate_response(response, 200, 404) + print json.dumps(user, indent=4) def _validate_creation(self, saas, server): """ checks if a saas object is valid for creation on the server side """ @@ -96,6 +108,7 @@ class GitLabSaaSBackend(ServiceController): self.append(self.change_password, saas) else: self.append(self.create_user, saas) + self.append(self.set_state, saas) def delete(self, saas): self.append(self.delete_user, saas) diff --git a/orchestra/apps/saas/models.py b/orchestra/apps/saas/models.py index 6b33b767..d58ee537 100644 --- a/orchestra/apps/saas/models.py +++ b/orchestra/apps/saas/models.py @@ -19,6 +19,8 @@ class SaaS(models.Model): validators=[validators.validate_username]) account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='saas') + is_active = models.BooleanField(_("active"), default=True, + help_text=_("Designates whether this service should be treated as active. ")) data = JSONField(_("data"), default={}, help_text=_("Extra information dependent of each service.")) @@ -41,6 +43,10 @@ class SaaS(models.Model): """ Per request lived service_instance """ return self.service_class(self) + @cached_property + def active(self): + return self.is_active and self.account.is_active + def clean(self): self.data = self.service_instance.clean_data() diff --git a/orchestra/apps/webapps/options.py b/orchestra/apps/webapps/options.py index 9a0ab84c..84b6f0ea 100644 --- a/orchestra/apps/webapps/options.py +++ b/orchestra/apps/webapps/options.py @@ -48,6 +48,20 @@ class AppOption(Plugin): }) +class PHPAppOption(AppOption): + deprecated = None + group = AppOption.PHP + + def validate(self): + super(PHPAppOption, self).validate() + if self.deprecated: + php_version = self.instance.webapp.type_instance.get_php_version() + if php_version and php_version > self.deprecated: + raise ValidationError( + _("This option is deprecated since PHP version %s.") % str(self.deprecated) + ) + + class PublicRoot(AppOption): name = 'public-root' verbose_name = _("Public root") @@ -77,193 +91,171 @@ class Processes(AppOption): group = AppOption.PROCESS -class PHPEnabledFunctions(AppOption): +class PHPEnabledFunctions(PHPAppOption): name = 'enabled_functions' verbose_name = _("Enabled functions") help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS) regex = r'^[\w\.,-]+$' - group = AppOption.PHP -class PHPAllowURLInclude(AppOption): +class PHPAllowURLInclude(PHPAppOption): name = 'allow_url_include' verbose_name = _("Allow URL include") help_text = _("Allows the use of URL-aware fopen wrappers with include, include_once, require, " "require_once (On or Off).") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPAllowURLFopen(AppOption): +class PHPAllowURLFopen(PHPAppOption): name = 'allow_url_fopen' verbose_name = _("Allow URL fopen") help_text = _("Enables the URL-aware fopen wrappers that enable accessing URL object like files (On or Off).") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPAutoAppendFile(AppOption): +class PHPAutoAppendFile(PHPAppOption): name = 'auto_append_file' verbose_name = _("Auto append file") help_text = _("Specifies the name of a file that is automatically parsed after the main file.") regex = r'^[\w\.,-/]+$' - group = AppOption.PHP -class PHPAutoPrependFile(AppOption): +class PHPAutoPrependFile(PHPAppOption): name = 'auto_prepend_file' verbose_name = _("Auto prepend file") help_text = _("Specifies the name of a file that is automatically parsed before the main file.") regex = r'^[\w\.,-/]+$' - group = AppOption.PHP -class PHPDateTimeZone(AppOption): +class PHPDateTimeZone(PHPAppOption): name = 'date.timezone' verbose_name = _("date.timezone") help_text = _("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London').") regex = r'^\w+/\w+$' - group = AppOption.PHP -class PHPDefaultSocketTimeout(AppOption): +class PHPDefaultSocketTimeout(PHPAppOption): name = 'default_socket_timeout' verbose_name = _("Default socket timeout") help_text = _("Number between 0 and 999.") regex = r'^[0-9]{1,3}$' - group = AppOption.PHP -class PHPDisplayErrors(AppOption): +class PHPDisplayErrors(PHPAppOption): name = 'display_errors' verbose_name = _("Display errors") help_text = _("Determines whether errors should be printed to the screen as part of the output or " "if they should be hidden from the user (On or Off).") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPExtension(AppOption): +class PHPExtension(PHPAppOption): name = 'extension' verbose_name = _("Extension") regex = r'^[^ ]+$' - group = AppOption.PHP -class PHPMagicQuotesGPC(AppOption): +class PHPMagicQuotesGPC(PHPAppOption): name = 'magic_quotes_gpc' verbose_name = _("Magic quotes GPC") help_text = _("Sets the magic_quotes state for GPC (Get/Post/Cookie) operations (On or Off) " "DEPRECATED as of PHP 5.3.0.") regex = r'^(On|Off|on|off)$' - deprecated=5.3 - group = AppOption.PHP + deprecated = 5.3 -class PHPMagicQuotesRuntime(AppOption): +class PHPMagicQuotesRuntime(PHPAppOption): name = 'magic_quotes_runtime' verbose_name = _("Magic quotes runtime") help_text = _("Functions that return data from any sort of external source will have quotes escaped " "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0.") regex = r'^(On|Off|on|off)$' - deprecated=5.3 - group = AppOption.PHP + deprecated = 5.3 -class PHPMaginQuotesSybase(AppOption): +class PHPMaginQuotesSybase(PHPAppOption): name = 'magic_quotes_sybase' verbose_name = _("Magic quotes sybase") help_text = _("Single-quote is escaped with a single-quote instead of a backslash (On or Off).") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPMaxExecutonTime(AppOption): +class PHPMaxExecutonTime(PHPAppOption): name = 'max_execution_time' verbose_name = _("Max execution time") help_text = _("Maximum time in seconds a script is allowed to run before it is terminated by " "the parser (Integer between 0 and 999).") regex = r'^[0-9]{1,3}$' - group = AppOption.PHP -class PHPMaxInputTime(AppOption): +class PHPMaxInputTime(PHPAppOption): name = 'max_input_time' verbose_name = _("Max input time") help_text = _("Maximum time in seconds a script is allowed to parse input data, like POST and GET " "(Integer between 0 and 999).") regex = r'^[0-9]{1,3}$' - group = AppOption.PHP -class PHPMaxInputVars(AppOption): +class PHPMaxInputVars(PHPAppOption): name = 'max_input_vars' verbose_name = _("Max input vars") help_text = _("How many input variables may be accepted (limit is applied to $_GET, $_POST " "and $_COOKIE superglobal separately) (Integer between 0 and 9999).") regex = r'^[0-9]{1,4}$' - group = AppOption.PHP -class PHPMemoryLimit(AppOption): +class PHPMemoryLimit(PHPAppOption): name = 'memory_limit' verbose_name = _("Memory limit") help_text = _("This sets the maximum amount of memory in bytes that a script is allowed to allocate " "(Value between 0M and 999M).") regex = r'^[0-9]{1,3}M$' - group = AppOption.PHP -class PHPMySQLConnectTimeout(AppOption): +class PHPMySQLConnectTimeout(PHPAppOption): name = 'mysql.connect_timeout' verbose_name = _("Mysql connect timeout") help_text = _("Number between 0 and 999.") regex = r'^([0-9]){1,3}$' - group = AppOption.PHP -class PHPOutputBuffering(AppOption): +class PHPOutputBuffering(PHPAppOption): name = 'output_buffering' verbose_name = _("Output buffering") help_text = _("Turn on output buffering (On or Off).") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPRegisterGlobals(AppOption): +class PHPRegisterGlobals(PHPAppOption): name = 'register_globals' verbose_name = _("Register globals") help_text = _("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) " "variables as global variables (On or Off).") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPPostMaxSize(AppOption): +class PHPPostMaxSize(PHPAppOption): name = 'post_max_size' verbose_name = _("Post max size") help_text = _("Sets max size of post data allowed (Value between 0M and 999M).") regex = r'^[0-9]{1,3}M$' - group = AppOption.PHP -class PHPSendmailPath(AppOption): +class PHPSendmailPath(PHPAppOption): name = 'sendmail_path' verbose_name = _("sendmail_path") help_text = _("Where the sendmail program can be found.") regex = r'^[^ ]+$' - group = AppOption.PHP -class PHPSessionBugCompatWarn(AppOption): +class PHPSessionBugCompatWarn(PHPAppOption): name = 'session.bug_compat_warn' verbose_name = _("session.bug_compat_warn") help_text = _("Enables an PHP bug on session initialization for legacy behaviour (On or Off).") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPSessionAutoStart(AppOption): +class PHPSessionAutoStart(PHPAppOption): name = 'session.auto_start' verbose_name = _("session.auto_start") help_text = _("Specifies whether the session module starts a session automatically on request " @@ -272,72 +264,63 @@ class PHPSessionAutoStart(AppOption): group = AppOption.PHP -class PHPSafeMode(AppOption): +class PHPSafeMode(PHPAppOption): name = 'safe_mode' verbose_name = _("Safe mode") help_text = _("Whether to enable PHP's safe mode (On or Off) DEPRECATED as of PHP 5.3.0") regex = r'^(On|Off|on|off)$' deprecated=5.3 - group = AppOption.PHP -class PHPSuhosinPostMaxVars(AppOption): +class PHPSuhosinPostMaxVars(PHPAppOption): name = 'suhosin.post.max_vars' verbose_name = _("Suhosin POST max vars") help_text = _("Number between 0 and 9999.") regex = r'^[0-9]{1,4}$' - group = AppOption.PHP -class PHPSuhosinGetMaxVars(AppOption): +class PHPSuhosinGetMaxVars(PHPAppOption): name = 'suhosin.get.max_vars' verbose_name = _("Suhosin GET max vars") help_text = _("Number between 0 and 9999.") regex = r'^[0-9]{1,4}$' - group = AppOption.PHP -class PHPSuhosinRequestMaxVars(AppOption): +class PHPSuhosinRequestMaxVars(PHPAppOption): name = 'suhosin.request.max_vars' verbose_name = _("Suhosin request max vars") help_text = _("Number between 0 and 9999.") regex = r'^[0-9]{1,4}$' - group = AppOption.PHP -class PHPSuhosinSessionEncrypt(AppOption): +class PHPSuhosinSessionEncrypt(PHPAppOption): name = 'suhosin.session.encrypt' verbose_name = _("suhosin.session.encrypt") help_text = _("On or Off") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPSuhosinSimulation(AppOption): +class PHPSuhosinSimulation(PHPAppOption): name = 'suhosin.simulation' verbose_name = _("Suhosin simulation") help_text = _("On or Off") regex = r'^(On|Off|on|off)$' - group = AppOption.PHP -class PHPSuhosinExecutorIncludeWhitelist(AppOption): +class PHPSuhosinExecutorIncludeWhitelist(PHPAppOption): name = 'suhosin.executor.include.whitelist' verbose_name = _("suhosin.executor.include.whitelist") regex = r'.*$' - group = AppOption.PHP -class PHPUploadMaxFileSize(AppOption): +class PHPUploadMaxFileSize(PHPAppOption): name = 'upload_max_filesize' verbose_name = _("upload_max_filesize") help_text = _("Value between 0M and 999M.") regex = r'^[0-9]{1,3}M$' - group = AppOption.PHP -class PHPZendExtension(AppOption): +class PHPZendExtension(PHPAppOption): name = 'zend_extension' verbose_name = _("Zend extension") regex = r'^[^ ]+$' - group = AppOption.PHP diff --git a/orchestra/apps/webapps/types/__init__.py b/orchestra/apps/webapps/types/__init__.py index 3e179b7e..3b210320 100644 --- a/orchestra/apps/webapps/types/__init__.py +++ b/orchestra/apps/webapps/types/__init__.py @@ -36,14 +36,6 @@ class AppType(plugins.Plugin): 'name': _("A WordPress blog with this name already exists."), }) - @classmethod - @cached - def get_php_options(cls): - # TODO validate php options once a php version has been selected (deprecated directives) - php_version = getattr(cls, 'php_version', 1) - php_options = AppOption.get_option_groups()[AppOption.PHP] - return [op for op in php_options if getattr(cls, 'deprecated', 99) > php_version] - @classmethod @cached def get_options(cls): @@ -52,8 +44,6 @@ class AppType(plugins.Plugin): options = [] for group in cls.option_groups: group_options = groups[group] - if group == AppOption.PHP: - group_options = cls.get_php_options() if group is None: options.insert(0, (group, group_options)) else: diff --git a/orchestra/apps/webapps/types/php.py b/orchestra/apps/webapps/types/php.py index aed56a80..97760da9 100644 --- a/orchestra/apps/webapps/types/php.py +++ b/orchestra/apps/webapps/types/php.py @@ -7,8 +7,10 @@ from rest_framework import serializers from orchestra.forms import widgets from orchestra.plugins.forms import PluginDataForm +from orchestra.utils.functional import cached from .. import settings +from ..options import AppOption from . import AppType @@ -57,6 +59,12 @@ class PHPApp(AppType): def get_detail(self): return self.instance.data.get('php_version', '') + @cached + def get_php_options(self): + php_version = self.get_php_version() + php_options = AppOption.get_option_groups()[AppOption.PHP] + return [op for op in php_options if getattr(self, 'deprecated', 999) > php_version] + def get_php_init_vars(self, merge=False): """ process php options for inclusion on php.ini @@ -72,7 +80,7 @@ class PHPApp(AppType): for webapp in webapps: if webapp.type_instance.get_php_version == php_version: options += list(webapp.options.all()) - php_options = [option.name for option in type(self).get_php_options()] + php_options = [option.name for option in self.get_php_options()] enabled_functions = set() for opt in options: if opt.name in php_options: diff --git a/orchestra/apps/websites/directives.py b/orchestra/apps/websites/directives.py index da6273e6..38fd474a 100644 --- a/orchestra/apps/websites/directives.py +++ b/orchestra/apps/websites/directives.py @@ -10,7 +10,6 @@ from orchestra.utils.python import import_class from . import settings -# TODO multiple and unique validation support in the formset class SiteDirective(Plugin): HTTPD = 'HTTPD' SEC = 'ModSecurity' @@ -141,7 +140,6 @@ class WordPressSaaS(SiteDirective): name = 'wordpress-saas' verbose_name = "WordPress SaaS" help_text = _("URL path for mounting wordpress multisite.") -# fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN group = SiteDirective.SAAS regex = r'^/[^ ]*$' unique_value = True @@ -151,7 +149,6 @@ class DokuWikiSaaS(SiteDirective): name = 'dokuwiki-saas' verbose_name = "DokuWiki SaaS" help_text = _("URL path for mounting wordpress multisite.") -# fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN group = SiteDirective.SAAS regex = r'^/[^ ]*$' unique_value = True @@ -161,7 +158,6 @@ class DrupalSaaS(SiteDirective): name = 'drupal-saas' verbose_name = "Drupdal SaaS" help_text = _("URL path for mounting wordpress multisite.") -# fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN group = SiteDirective.SAAS regex = r'^/[^ ]*$' unique_value = True