Improved validation errors

This commit is contained in:
root 2014-11-05 20:22:01 +00:00
parent 25df6505bb
commit 6864e462bf
16 changed files with 145 additions and 59 deletions

34
TODO.md
View file

@ -12,7 +12,6 @@ TODO ====
* add `BackendLog` retry action * add `BackendLog` retry action
* PHPbBckendMiixin with get_php_ini * PHPbBckendMiixin with get_php_ini
* Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]`
* webmail identities and addresses * webmail identities and addresses
* user.roles.mailbox its awful when combined with addresses: * user.roles.mailbox its awful when combined with addresses:
* address.mailboxes filter by account is crap in admin and api * address.mailboxes filter by account is crap in admin and api
@ -180,3 +179,36 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* Databases.User add reverse M2M databases widget (like mailbox.addresses) * Databases.User add reverse M2M databases widget (like mailbox.addresses)
* One domain zone validation for each save, not one per subdomain, maybe on modeladmin.save_related? prevent save on model_related, and save it on save_related() * One domain zone validation for each save, not one per subdomain, maybe on modeladmin.save_related? prevent save on model_related, and save it on save_related()
* Change permissions periodically on the web server, to ensure security
* Apache RLimit ?
* Patch suexec: if user mismatch, check user belongs to suexecusergroup group
* fuck suexec http://www.litespeedtech.com/support/forum/threads/solved-cloudlinux-php-lsapi-say-no-to-suexec.5812/
* http://mail-archives.apache.org/mod_mbox/httpd-dev/201409.mbox/%3C5411FFBE.9050506@loginroot.com%3E ??
* Root owned logs on user's home ?
* Secondary user home in /home/secondaryuser and simlink to /home/main/webapps/app so it can have private storage?
* Grant permissions like in webfaction
* Secondaryusers home should be under mainuser home. i.e. /home/mainuser/webapps/seconduser_webapp/
* Make one dedicated CGI user for each account only for CGI execution (fpm/fcgid). Different from the files owner, and without W permissions, so attackers can not inject backdors and malware.
* In most cases we can prevent the creation of files for the CGI users, preventing attackers to upload and executing PHPShells.
* Make main systemuser able to write/read everything on its home, including stuff created by the CGI user and secondary users
* Prevent users from accessing other users home while at the same time allow access Apache/fcgid/fpm and secondary users (x)
* public_html/webapps directory with root owner and permissions
* resource min max allocation with validation
* mailman needs both aliases when address_name is provided (default messages and bounces and all)
* specify field on ValidationError under model.clean() of form.clean(): ValidationError({'bark_volume': ["Must be louder!",]}
* And raise ValidationError once at the end collecting all errors at once

View file

@ -17,7 +17,7 @@ def create_account_creation_form():
help_text=_("Designates whether to creates an enabled or disabled related system user. " help_text=_("Designates whether to creates an enabled or disabled related system user. "
"Notice that a related system user will be always created.")) "Notice that a related system user will be always created."))
}) })
for model, key, kwargs, help_text in settings.ACCOUNTS_CREATE_RELATED: for model, __, kwargs, help_text in settings.ACCOUNTS_CREATE_RELATED:
model = get_model(model) model = get_model(model)
field_name = 'create_%s' % model._meta.model_name field_name = 'create_%s' % model._meta.model_name
label = _("Create %s") % model._meta.verbose_name label = _("Create %s") % model._meta.verbose_name
@ -35,9 +35,10 @@ def create_account_creation_form():
except KeyError: except KeyError:
# Previous validation error # Previous validation error
return return
errors = {}
systemuser_model = Account.main_systemuser.field.rel.to systemuser_model = Account.main_systemuser.field.rel.to
if systemuser_model.objects.filter(username=account.username).exists(): if systemuser_model.objects.filter(username=account.username).exists():
raise forms.ValidationError(_("A system user with this name already exists")) errors['username'] = _("A system user with this name already exists.")
for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
model = get_model(model) model = get_model(model)
kwargs = { kwargs = {
@ -45,9 +46,13 @@ def create_account_creation_form():
} }
if model.objects.filter(**kwargs).exists(): if model.objects.filter(**kwargs).exists():
verbose_name = model._meta.verbose_name verbose_name = model._meta.verbose_name
raise forms.ValidationError( field_name = 'create_%s' % model._meta.model_name
_("A %s with this name already exists") % verbose_name errors[field] = ValidationError(
) _("A %(type)s with this name already exists."),
params={'type': verbose_name})
if errors:
raise ValidationError(errors)
def save_model(self, account): def save_model(self, account):
account.save(active_systemuser=self.cleaned_data['enable_systemuser']) account.save(active_systemuser=self.cleaned_data['enable_systemuser'])

View file

@ -41,8 +41,10 @@ class BillContact(models.Model):
def clean(self): def clean(self):
self.vat = self.vat.strip() self.vat = self.vat.strip()
self.city = self.city.strip() self.city = self.city.strip()
validators.validate_vat(self.vat, self.country) validators.all_valid({
validators.validate_zipcode(self.zipcode, self.country) 'vat': (validators.validate_vat, self.vat, self.country),
'zipcode': (validators.validate_zipcode, self.zipcode, self.country)
})
class BillManager(models.Manager): class BillManager(models.Manager):

View file

@ -66,12 +66,18 @@ class Contact(models.Model):
self.address = self.address.strip() self.address = self.address.strip()
self.city = self.city.strip() self.city = self.city.strip()
self.country = self.country.strip() self.country = self.country.strip()
errors = {}
if self.address and not (self.city and self.zipcode and self.country): if self.address and not (self.city and self.zipcode and self.country):
raise ValidationError(_("City, zipcode and country must be provided when address is provided.")) errors['__all__'] = _("City, zipcode and country must be provided when address is provided.")
if self.zipcode and not self.country: if self.zipcode and not self.country:
raise ValidationError(_("Country must be provided when zipcode is provided.")) errors['country'] = _("Country must be provided when zipcode is provided.")
elif self.zipcode and self.country: elif self.zipcode and self.country:
try:
validators.validate_zipcode(self.zipcode, self.country) validators.validate_zipcode(self.zipcode, self.country)
except ValidationError, error:
errors['zipcode'] = error
if errors:
raise ValidationError(errors)
accounts.register(Contact) accounts.register(Contact)

View file

@ -21,8 +21,9 @@ class CreateDomainAdminForm(forms.ModelForm):
# Fake an account to make django validation happy # Fake an account to make django validation happy
account_model = self.fields['account']._queryset.model account_model = self.fields['account']._queryset.model
cleaned_data['account'] = account_model() cleaned_data['account'] = account_model()
msg = _("An account should be provided for top domain names") raise ValidationError({
raise ValidationError(msg) 'account': _("An account should be provided for top domain names."),
})
cleaned_data['account'] = top.account cleaned_data['account'] = top.account
return cleaned_data return cleaned_data
@ -67,6 +68,7 @@ class CreateDomainAdminForm(forms.ModelForm):
# self.save_formset(request, form, formset, change=change) # self.save_formset(request, form, formset, change=change)
# TODO do it in admin
class RecordInlineFormSet(forms.models.BaseInlineFormSet): class RecordInlineFormSet(forms.models.BaseInlineFormSet):
def clean(self): def clean(self):
""" Checks if everything is consistent """ """ Checks if everything is consistent """

View file

@ -14,7 +14,7 @@ class Domain(models.Model):
help_text=_("Domain or subdomain name.")) help_text=_("Domain or subdomain name."))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='domains', blank=True, help_text=_("Automatically selected for subdomains.")) related_name='domains', blank=True, help_text=_("Automatically selected for subdomains."))
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains', editable=False) top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set', editable=False)
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial,
help_text=_("Serial number")) help_text=_("Serial number"))
@ -41,6 +41,10 @@ class Domain(models.Model):
# don't cache, don't replace by top_id # don't cache, don't replace by top_id
return not bool(self.top) return not bool(self.top)
@property
def subdomains(self):
return Domain.objects.filter(name__regex='\.%s$' % self.name)
def get_absolute_url(self): def get_absolute_url(self):
return 'http://%s' % self.name return 'http://%s' % self.name
@ -50,7 +54,7 @@ class Domain(models.Model):
def get_subdomains(self): def get_subdomains(self):
""" proxy method, needed for input validation, see helpers.domain_for_validation """ """ proxy method, needed for input validation, see helpers.domain_for_validation """
return self.origin.subdomains.all() return self.origin.subdomain_set.all()
def get_top(self): def get_top(self):
return type(self).get_top_domain(self.name) return type(self).get_top_domain(self.name)
@ -140,8 +144,8 @@ class Domain(models.Model):
update = True update = True
super(Domain, self).save(*args, **kwargs) super(Domain, self).save(*args, **kwargs)
if update: if update:
domains = Domain.objects.exclude(pk=self.pk) for domain in self.subdomains.exclude(pk=self.pk):
for domain in domains.filter(name__endswith=self.name): # queryset.update() is not used because we want to trigger backend to delete ex-topdomains
domain.top = self domain.top = self
domain.save(update_fields=['top']) domain.save(update_fields=['top'])
@ -182,7 +186,7 @@ class Record(models.Model):
""" validates record value based on its type """ """ validates record value based on its type """
# validate value # validate value
self.value = self.value.lower().strip() self.value = self.value.lower().strip()
mapp = { choices = {
self.MX: validators.validate_mx_record, self.MX: validators.validate_mx_record,
self.NS: validators.validate_zone_label, self.NS: validators.validate_zone_label,
self.A: validate_ipv4_address, self.A: validate_ipv4_address,
@ -192,7 +196,10 @@ class Record(models.Model):
self.SRV: validators.validate_srv_record, self.SRV: validators.validate_srv_record,
self.SOA: validators.validate_soa_record, self.SOA: validators.validate_soa_record,
} }
mapp[self.type](self.value) try:
choices[self.type](self.value)
except ValidationError, error:
raise ValidationError({'value': error})
def get_ttl(self): def get_ttl(self):
return self.ttl or settings.DOMAINS_DEFAULT_TTL return self.ttl or settings.DOMAINS_DEFAULT_TTL

View file

@ -45,7 +45,9 @@ class MailboxForm(forms.ModelForm):
filtering = self.cleaned_data['filtering'] filtering = self.cleaned_data['filtering']
custom_filtering = self.cleaned_data['custom_filtering'] custom_filtering = self.cleaned_data['custom_filtering']
if filtering == self._meta.model.CUSTOM and not custom_filtering: if filtering == self._meta.model.CUSTOM and not custom_filtering:
raise forms.ValidationError(_("You didn't provide any custom filtering")) raise forms.ValidationError({
'custom_filtering': _("You didn't provide any custom filtering.")
})
return custom_filtering return custom_filtering
@ -69,4 +71,4 @@ class AddressForm(forms.ModelForm):
def clean(self): def clean(self):
cleaned_data = super(AddressForm, self).clean() cleaned_data = super(AddressForm, self).clean()
if not cleaned_data.get('mailboxes', True) and not cleaned_data['forward']: if not cleaned_data.get('mailboxes', True) and not cleaned_data['forward']:
raise forms.ValidationError(_("Mailboxes or forward address should be provided")) raise forms.ValidationError(_("Mailboxes or forward address should be provided."))

View file

@ -117,7 +117,7 @@ class Transaction(models.Model):
if not self.pk: if not self.pk:
amount = self.bill.transactions.exclude(state=self.REJECTED).amount() amount = self.bill.transactions.exclude(state=self.REJECTED).amount()
if amount >= self.bill.total: if amount >= self.bill.total:
raise ValidationError(_("New transactions can not be allocated for this bill")) raise ValidationError(_("New transactions can not be allocated for this bill."))
def mark_as_processed(self): def mark_as_processed(self):
assert self.state == self.WAITTING_PROCESSING assert self.state == self.WAITTING_PROCESSING

View file

@ -39,6 +39,35 @@ class ServiceHandler(plugins.Plugin):
choices = super(ServiceHandler, cls).get_plugin_choices() choices = super(ServiceHandler, cls).get_plugin_choices()
return [('', _("Default"))] + choices return [('', _("Default"))] + choices
def validate_content_type(self, service):
pass
def validate_match(self, service):
if not service.match:
raise ValidationError(_("Match should be provided."))
try:
obj = service.content_type.model_class().objects.all()[0]
except IndexError:
return
try:
bool(self.matches(obj))
except Exception, exception:
name = type(exception).__name__
message = exception.message
raise ValidationError(': '.join((name, message)))
def validate_metric(self, service):
try:
obj = service.content_type.model_class().objects.all()[0]
except IndexError:
return
try:
bool(self.get_metric(obj))
except Exception, exception:
name = type(exception).__name__
message = exception.message
raise ValidationError(': '.join((name, message)))
def get_content_type(self): def get_content_type(self):
if not self.model: if not self.model:
return self.content_type return self.content_type

View file

@ -9,7 +9,7 @@ from django.utils.functional import cached_property
from django.utils.module_loading import autodiscover_modules from django.utils.module_loading import autodiscover_modules
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import caches, services, accounts from orchestra.core import caches, services, accounts, validators
from orchestra.core.validators import validate_name from orchestra.core.validators import validate_name
from orchestra.models import queryset from orchestra.models import queryset
@ -51,7 +51,7 @@ class ContractedPlan(models.Model):
def clean(self): def clean(self):
if not self.pk and not self.plan.allow_multiples: if not self.pk and not self.plan.allow_multiples:
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists(): if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():
raise ValidationError("A contracted plan for this account already exists") raise ValidationError("A contracted plan for this account already exists.")
class RateQuerySet(models.QuerySet): class RateQuerySet(models.QuerySet):
@ -243,33 +243,11 @@ class Service(models.Model):
def clean(self): def clean(self):
self.description = self.description.strip() self.description = self.description.strip()
content_type = self.handler.get_content_type() validators.all_valid({
if self.content_type != content_type: 'content_type': (self.handler.validate_content_type, self),
ct = str(content_type) 'match': (self.handlers.validate_match, self),
raise ValidationError(_("Content type must be equal to '%s'.") % ct) 'metric': (self.handlers.validate_metric, self),
if not self.match: })
raise ValidationError(_("Match should be provided"))
try:
obj = content_type.model_class().objects.all()[0]
except IndexError:
pass
else:
attr = None
try:
bool(self.handler.matches(obj))
except Exception as exception:
attr = "Matches"
try:
metric = self.handler.get_metric(obj)
if metric is not None:
int(metric)
except Exception as exception:
attr = "Get metric"
if attr is not None:
name = type(exception).__name__
message = exception.message
msg = "{0} {1}: {2}".format(attr, name, message)
raise ValidationError(msg)
def get_pricing_period(self): def get_pricing_period(self):
if self.pricing_period == self.BILLING_PERIOD: if self.pricing_period == self.BILLING_PERIOD:

View file

@ -54,7 +54,7 @@ class WebAppServiceMixin(object):
return { return {
'user': webapp.get_username(), 'user': webapp.get_username(),
'group': webapp.get_groupname(), 'group': webapp.get_groupname(),
'app_name': webapp.name, 'app_name': webapp.get_name(),
'type': webapp.type, 'type': webapp.type,
'app_path': webapp.get_path().rstrip('/'), 'app_path': webapp.get_path().rstrip('/'),
'banner': self.get_banner(), 'banner': self.get_banner(),

View file

@ -77,8 +77,13 @@ class WebAppOption(models.Model):
""" validates name and value according to WEBAPPS_OPTIONS """ """ validates name and value according to WEBAPPS_OPTIONS """
__, regex = settings.WEBAPPS_OPTIONS[self.name] __, regex = settings.WEBAPPS_OPTIONS[self.name]
if not re.match(regex, self.value): if not re.match(regex, self.value):
msg = _("'%s' does not match %s") raise ValidationError({
raise ValidationError(msg % (self.value, regex)) 'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
params={
'value': self.value,
'regex': regex
}),
})
services.register(WebApp) services.register(WebApp)

View file

@ -24,7 +24,7 @@ WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH',
WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH', WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH',
'/home/httpd/fcgid/%(user)s-%(app_name)s-wrapper') '/home/httpd/fcgid/%(user)s/%(app_name)s-wrapper')
WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', { WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', {
@ -190,7 +190,7 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
), ),
'PHP-suhosin.executor.include.whitelist': ( 'PHP-suhosin.executor.include.whitelist': (
_("PHP - suhosin.executor.include.whitelist"), _("PHP - suhosin.executor.include.whitelist"),
r'^(upload|phar)$' r'.*$'
), ),
'PHP-upload_max_filesize': ( 'PHP-upload_max_filesize': (
_("PHP - upload_max_filesize"), _("PHP - upload_max_filesize"),

View file

@ -37,6 +37,7 @@ class Apache2Backend(ServiceController):
SuexecUserGroup {{ user }} {{ group }}\ SuexecUserGroup {{ user }} {{ group }}\
{% for line in extra_conf.splitlines %} {% for line in extra_conf.splitlines %}
{{ line | safe }}{% endfor %} {{ line | safe }}{% endfor %}
IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f]
</VirtualHost>""" </VirtualHost>"""
)) ))
apache_conf = apache_conf.render(Context(context)) apache_conf = apache_conf.render(Context(context))

View file

@ -82,8 +82,13 @@ class WebsiteOption(models.Model):
""" validates name and value according to WEBSITES_WEBSITEOPTIONS """ """ validates name and value according to WEBSITES_WEBSITEOPTIONS """
__, regex = settings.WEBSITES_OPTIONS[self.name] __, regex = settings.WEBSITES_OPTIONS[self.name]
if not re.match(regex, self.value): if not re.match(regex, self.value):
msg = _("'%s' does not match %s") raise ValidationError({
raise ValidationError(msg % (self.value, regex)) 'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
params={
'value': self.value,
'regex': regex
}),
})
class Content(models.Model): class Content(models.Model):

View file

@ -12,6 +12,18 @@ from IPy import IP
from ..utils.python import import_class from ..utils.python import import_class
def all_valid(kwargs):
""" helper function to merge multiple validators at once """
errors = {}
for field, validator in kwargs.iteritems():
try:
validator[0](*validator[1:])
except ValidationError, error:
errors[field] = error
if errors:
raise ValidationError(errors)
def validate_ipv4_address(value): def validate_ipv4_address(value):
msg = _("%s is not a valid IPv4 address") % value msg = _("%s is not a valid IPv4 address") % value
try: try: