Improved webapps and saas validation

This commit is contained in:
Marc Aymerich 2015-03-25 17:04:44 +00:00
parent dd84217320
commit c55cff9a37
8 changed files with 95 additions and 116 deletions

23
TODO.md
View file

@ -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

View file

@ -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'

View file

@ -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)

View file

@ -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()

View file

@ -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) "
"<b>DEPRECATED as of PHP 5.3.0</b>.")
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) <b>DEPRECATED as of PHP 5.3.0</b>.")
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) <b>DEPRECATED as of PHP 5.3.0</b>")
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

View file

@ -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:

View file

@ -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:

View file

@ -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