Improved validation errors
This commit is contained in:
parent
25df6505bb
commit
6864e462bf
34
TODO.md
34
TODO.md
|
@ -12,7 +12,6 @@ TODO ====
|
|||
|
||||
* add `BackendLog` retry action
|
||||
* PHPbBckendMiixin with get_php_ini
|
||||
* Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]`
|
||||
* webmail identities and addresses
|
||||
* user.roles.mailbox its awful when combined with addresses:
|
||||
* 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)
|
||||
|
||||
* 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
|
||||
|
|
|
@ -17,7 +17,7 @@ def create_account_creation_form():
|
|||
help_text=_("Designates whether to creates an enabled or disabled related system user. "
|
||||
"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)
|
||||
field_name = 'create_%s' % model._meta.model_name
|
||||
label = _("Create %s") % model._meta.verbose_name
|
||||
|
@ -35,9 +35,10 @@ def create_account_creation_form():
|
|||
except KeyError:
|
||||
# Previous validation error
|
||||
return
|
||||
errors = {}
|
||||
systemuser_model = Account.main_systemuser.field.rel.to
|
||||
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:
|
||||
model = get_model(model)
|
||||
kwargs = {
|
||||
|
@ -45,9 +46,13 @@ def create_account_creation_form():
|
|||
}
|
||||
if model.objects.filter(**kwargs).exists():
|
||||
verbose_name = model._meta.verbose_name
|
||||
raise forms.ValidationError(
|
||||
_("A %s with this name already exists") % verbose_name
|
||||
)
|
||||
field_name = 'create_%s' % model._meta.model_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):
|
||||
account.save(active_systemuser=self.cleaned_data['enable_systemuser'])
|
||||
|
||||
|
|
|
@ -41,8 +41,10 @@ class BillContact(models.Model):
|
|||
def clean(self):
|
||||
self.vat = self.vat.strip()
|
||||
self.city = self.city.strip()
|
||||
validators.validate_vat(self.vat, self.country)
|
||||
validators.validate_zipcode(self.zipcode, self.country)
|
||||
validators.all_valid({
|
||||
'vat': (validators.validate_vat, self.vat, self.country),
|
||||
'zipcode': (validators.validate_zipcode, self.zipcode, self.country)
|
||||
})
|
||||
|
||||
|
||||
class BillManager(models.Manager):
|
||||
|
|
|
@ -66,12 +66,18 @@ class Contact(models.Model):
|
|||
self.address = self.address.strip()
|
||||
self.city = self.city.strip()
|
||||
self.country = self.country.strip()
|
||||
errors = {}
|
||||
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:
|
||||
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:
|
||||
validators.validate_zipcode(self.zipcode, self.country)
|
||||
try:
|
||||
validators.validate_zipcode(self.zipcode, self.country)
|
||||
except ValidationError, error:
|
||||
errors['zipcode'] = error
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
||||
accounts.register(Contact)
|
||||
|
|
|
@ -21,8 +21,9 @@ class CreateDomainAdminForm(forms.ModelForm):
|
|||
# Fake an account to make django validation happy
|
||||
account_model = self.fields['account']._queryset.model
|
||||
cleaned_data['account'] = account_model()
|
||||
msg = _("An account should be provided for top domain names")
|
||||
raise ValidationError(msg)
|
||||
raise ValidationError({
|
||||
'account': _("An account should be provided for top domain names."),
|
||||
})
|
||||
cleaned_data['account'] = top.account
|
||||
return cleaned_data
|
||||
|
||||
|
@ -67,6 +68,7 @@ class CreateDomainAdminForm(forms.ModelForm):
|
|||
# self.save_formset(request, form, formset, change=change)
|
||||
|
||||
|
||||
# TODO do it in admin
|
||||
class RecordInlineFormSet(forms.models.BaseInlineFormSet):
|
||||
def clean(self):
|
||||
""" Checks if everything is consistent """
|
||||
|
|
|
@ -14,7 +14,7 @@ class Domain(models.Model):
|
|||
help_text=_("Domain or subdomain name."))
|
||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||
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,
|
||||
help_text=_("Serial number"))
|
||||
|
||||
|
@ -41,6 +41,10 @@ class Domain(models.Model):
|
|||
# don't cache, don't replace by top_id
|
||||
return not bool(self.top)
|
||||
|
||||
@property
|
||||
def subdomains(self):
|
||||
return Domain.objects.filter(name__regex='\.%s$' % self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return 'http://%s' % self.name
|
||||
|
||||
|
@ -50,7 +54,7 @@ class Domain(models.Model):
|
|||
|
||||
def get_subdomains(self):
|
||||
""" 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):
|
||||
return type(self).get_top_domain(self.name)
|
||||
|
@ -140,8 +144,8 @@ class Domain(models.Model):
|
|||
update = True
|
||||
super(Domain, self).save(*args, **kwargs)
|
||||
if update:
|
||||
domains = Domain.objects.exclude(pk=self.pk)
|
||||
for domain in domains.filter(name__endswith=self.name):
|
||||
for domain in self.subdomains.exclude(pk=self.pk):
|
||||
# queryset.update() is not used because we want to trigger backend to delete ex-topdomains
|
||||
domain.top = self
|
||||
domain.save(update_fields=['top'])
|
||||
|
||||
|
@ -182,7 +186,7 @@ class Record(models.Model):
|
|||
""" validates record value based on its type """
|
||||
# validate value
|
||||
self.value = self.value.lower().strip()
|
||||
mapp = {
|
||||
choices = {
|
||||
self.MX: validators.validate_mx_record,
|
||||
self.NS: validators.validate_zone_label,
|
||||
self.A: validate_ipv4_address,
|
||||
|
@ -192,7 +196,10 @@ class Record(models.Model):
|
|||
self.SRV: validators.validate_srv_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):
|
||||
return self.ttl or settings.DOMAINS_DEFAULT_TTL
|
||||
|
|
|
@ -45,7 +45,9 @@ class MailboxForm(forms.ModelForm):
|
|||
filtering = self.cleaned_data['filtering']
|
||||
custom_filtering = self.cleaned_data['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
|
||||
|
||||
|
||||
|
@ -69,4 +71,4 @@ class AddressForm(forms.ModelForm):
|
|||
def clean(self):
|
||||
cleaned_data = super(AddressForm, self).clean()
|
||||
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."))
|
||||
|
|
|
@ -117,7 +117,7 @@ class Transaction(models.Model):
|
|||
if not self.pk:
|
||||
amount = self.bill.transactions.exclude(state=self.REJECTED).amount()
|
||||
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):
|
||||
assert self.state == self.WAITTING_PROCESSING
|
||||
|
|
|
@ -39,6 +39,35 @@ class ServiceHandler(plugins.Plugin):
|
|||
choices = super(ServiceHandler, cls).get_plugin_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):
|
||||
if not self.model:
|
||||
return self.content_type
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.utils.functional import cached_property
|
|||
from django.utils.module_loading import autodiscover_modules
|
||||
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.models import queryset
|
||||
|
||||
|
@ -51,7 +51,7 @@ class ContractedPlan(models.Model):
|
|||
def clean(self):
|
||||
if not self.pk and not self.plan.allow_multiples:
|
||||
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):
|
||||
|
@ -243,34 +243,12 @@ class Service(models.Model):
|
|||
|
||||
def clean(self):
|
||||
self.description = self.description.strip()
|
||||
content_type = self.handler.get_content_type()
|
||||
if self.content_type != content_type:
|
||||
ct = str(content_type)
|
||||
raise ValidationError(_("Content type must be equal to '%s'.") % ct)
|
||||
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)
|
||||
|
||||
validators.all_valid({
|
||||
'content_type': (self.handler.validate_content_type, self),
|
||||
'match': (self.handlers.validate_match, self),
|
||||
'metric': (self.handlers.validate_metric, self),
|
||||
})
|
||||
|
||||
def get_pricing_period(self):
|
||||
if self.pricing_period == self.BILLING_PERIOD:
|
||||
return self.billing_period
|
||||
|
|
|
@ -54,7 +54,7 @@ class WebAppServiceMixin(object):
|
|||
return {
|
||||
'user': webapp.get_username(),
|
||||
'group': webapp.get_groupname(),
|
||||
'app_name': webapp.name,
|
||||
'app_name': webapp.get_name(),
|
||||
'type': webapp.type,
|
||||
'app_path': webapp.get_path().rstrip('/'),
|
||||
'banner': self.get_banner(),
|
||||
|
|
|
@ -77,8 +77,13 @@ class WebAppOption(models.Model):
|
|||
""" validates name and value according to WEBAPPS_OPTIONS """
|
||||
__, regex = settings.WEBAPPS_OPTIONS[self.name]
|
||||
if not re.match(regex, self.value):
|
||||
msg = _("'%s' does not match %s")
|
||||
raise ValidationError(msg % (self.value, regex))
|
||||
raise ValidationError({
|
||||
'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
|
||||
params={
|
||||
'value': self.value,
|
||||
'regex': regex
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
services.register(WebApp)
|
||||
|
|
|
@ -24,7 +24,7 @@ WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_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', {
|
||||
|
@ -190,7 +190,7 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
|
|||
),
|
||||
'PHP-suhosin.executor.include.whitelist': (
|
||||
_("PHP - suhosin.executor.include.whitelist"),
|
||||
r'^(upload|phar)$'
|
||||
r'.*$'
|
||||
),
|
||||
'PHP-upload_max_filesize': (
|
||||
_("PHP - upload_max_filesize"),
|
||||
|
|
|
@ -37,6 +37,7 @@ class Apache2Backend(ServiceController):
|
|||
SuexecUserGroup {{ user }} {{ group }}\
|
||||
{% for line in extra_conf.splitlines %}
|
||||
{{ line | safe }}{% endfor %}
|
||||
IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f]
|
||||
</VirtualHost>"""
|
||||
))
|
||||
apache_conf = apache_conf.render(Context(context))
|
||||
|
|
|
@ -82,8 +82,13 @@ class WebsiteOption(models.Model):
|
|||
""" validates name and value according to WEBSITES_WEBSITEOPTIONS """
|
||||
__, regex = settings.WEBSITES_OPTIONS[self.name]
|
||||
if not re.match(regex, self.value):
|
||||
msg = _("'%s' does not match %s")
|
||||
raise ValidationError(msg % (self.value, regex))
|
||||
raise ValidationError({
|
||||
'value': ValidationError(_("'%(value)s' does not match %(regex)s."),
|
||||
params={
|
||||
'value': self.value,
|
||||
'regex': regex
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
class Content(models.Model):
|
||||
|
|
|
@ -12,6 +12,18 @@ from IPy import IP
|
|||
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):
|
||||
msg = _("%s is not a valid IPv4 address") % value
|
||||
try:
|
||||
|
|
Loading…
Reference in a new issue