Random fixes

This commit is contained in:
Marc Aymerich 2014-10-30 16:34:02 +00:00
parent d0c7c760af
commit c4e8c07311
25 changed files with 469 additions and 205 deletions

View File

@ -160,3 +160,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* prevent adding local email addresses on account.contacts account.email
* Resource monitoring without ROUTE alert or explicit error
* Domain validation has to be done with injected records and subdomains

View File

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('systemusers', '__first__'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and ./-/_ only.', unique=True, max_length=64, verbose_name='username', validators=[django.core.validators.RegexValidator(b'^[\\w.-]+$', 'Enter a valid username.', b'invalid')])),
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
('email', models.EmailField(help_text='Used for password recovery', max_length=75, verbose_name='email address')),
('type', models.CharField(default=b'INDIVIDUAL', max_length=32, verbose_name='type', choices=[(b'INDIVIDUAL', 'Individual'), (b'ASSOCIATION', 'Association'), (b'CUSTOMER', 'Customer'), (b'STAFF', 'Staff')])),
('language', models.CharField(default=b'ca', max_length=2, verbose_name='language', choices=[(b'ca', 'Catalan'), (b'es', 'Spanish'), (b'en', 'English')])),
('comments', models.TextField(max_length=256, verbose_name='comments', blank=True)),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('main_systemuser', models.ForeignKey(related_name='accounts_main', to='systemusers.SystemUser', null=True)),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
]

View File

@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='account',
name='first_name',
),
migrations.RemoveField(
model_name='account',
name='last_name',
),
migrations.AddField(
model_name='account',
name='full_name',
field=models.CharField(default='', max_length=30, verbose_name='full name'),
preserve_default=False,
),
migrations.AddField(
model_name='account',
name='short_name',
field=models.CharField(default='', max_length=30, verbose_name='short name', blank=True),
preserve_default=False,
),
]

View File

@ -14,13 +14,13 @@ from . import settings
class Account(auth.AbstractBaseUser):
username = models.CharField(_("username"), max_length=64, unique=True,
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')])
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
related_name='accounts_main')
short_name = models.CharField(_("short name"), max_length=30, blank=True)
full_name = models.CharField(_("full name"), max_length=30)
related_name='accounts_main', editable=False)
short_name = models.CharField(_("short name"), max_length=64, blank=True)
full_name = models.CharField(_("full name"), max_length=256)
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE)

View File

@ -15,7 +15,7 @@ ACCOUNTS_DEFAULT_TYPE = getattr(settings, 'ACCOUNTS_DEFAULT_TYPE', 'INDIVIDUAL')
ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', (
('en', _('English')),
('EN', _('English')),
))
@ -23,7 +23,7 @@ ACCOUNTS_SYSTEMUSER_MODEL = getattr(settings, 'ACCOUNTS_SYSTEMUSER_MODEL',
'systemusers.SystemUser')
ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en')
ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'EN')
ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1)

View File

@ -1,6 +1,6 @@
from dateutil.relativedelta import relativedelta
from django.core.validators import ValidationError
from django.core.validators import ValidationError, RegexValidator
from django.db import models
from django.template import loader, Context
from django.utils import timezone
@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.models import Account
from orchestra.apps.contacts.models import Contact
from orchestra.core import accounts
from orchestra.core import accounts, validators
from orchestra.utils.html import html_to_pdf
from . import settings
@ -24,8 +24,11 @@ class BillContact(models.Model):
address = models.TextField(_("address"))
city = models.CharField(_("city"), max_length=128,
default=settings.BILLS_CONTACT_DEFAULT_CITY)
zipcode = models.PositiveIntegerField(_("zip code"))
zipcode = models.CharField(_("zip code"), max_length=10,
validators=[RegexValidator(r'^[0-9A-Z]{3,10}$',
_("Enter a valid zipcode."), 'invalid')])
country = models.CharField(_("country"), max_length=20,
choices=settings.BILLS_CONTACT_COUNTRIES,
default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)
vat = models.CharField(_("VAT number"), max_length=64)
@ -35,6 +38,12 @@ class BillContact(models.Model):
def get_name(self):
return self.name or self.account.get_full_name()
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)
class BillManager(models.Manager):
def get_queryset(self):

View File

@ -1,4 +1,5 @@
from django.conf import settings
from django_countries import data
BILLS_NUMBER_LENGTH = getattr(settings, 'BILLS_NUMBER_LENGTH', 4)
@ -57,4 +58,9 @@ BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')
BILLS_CONTACT_DEFAULT_CITY = getattr(settings, 'BILLS_CONTACT_DEFAULT_CITY', 'Barcelona')
BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'Spain')
BILLS_CONTACT_COUNTRIES = getattr(settings, 'BILLS_CONTACT_COUNTRIES', data.COUNTRIES)
BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'ES')

View File

@ -1,12 +1,17 @@
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.core import accounts
from orchestra.core import accounts, validators
from orchestra.models.fields import MultiSelectField
from . import settings
validate_phone = lambda p: validators.validate_phone(p, settings.CONTACTS_DEFAULT_COUNTRY)
class ContactQuerySet(models.QuerySet):
def filter(self, *args, **kwargs):
usages = kwargs.pop('email_usages', [])
@ -37,14 +42,17 @@ class Contact(models.Model):
email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True,
choices=EMAIL_USAGES,
default=settings.CONTACTS_DEFAULT_EMAIL_USAGES)
phone = models.CharField(_("phone"), max_length=32, blank=True)
phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True)
phone = models.CharField(_("phone"), max_length=32, blank=True,
validators=[validate_phone])
phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True,
validators=[validate_phone])
address = models.TextField(_("address"), blank=True)
city = models.CharField(_("city"), max_length=128, blank=True,
default=settings.CONTACTS_DEFAULT_CITY)
zipcode = models.PositiveIntegerField(_("zip code"), null=True, blank=True)
city = models.CharField(_("city"), max_length=128, blank=True)
zipcode = models.CharField(_("zip code"), max_length=10, blank=True,
validators=[RegexValidator(r'^[0-9,A-Z]{3,10}$',
_("Enter a valid zipcode."), 'invalid')])
country = models.CharField(_("country"), max_length=20, blank=True,
default=settings.CONTACTS_DEFAULT_COUNTRY)
choices=settings.CONTACTS_COUNTRIES)
def __unicode__(self):
return self.short_name
@ -57,6 +65,12 @@ class Contact(models.Model):
self.address = self.address.strip()
self.city = self.city.strip()
self.country = self.country.strip()
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."))
if self.zipcode and not self.country:
raise ValidationError(_("Country must be provided when zipcode is provided."))
elif self.zipcode and self.country:
validators.validate_zipcode(self.zipcode, self.country)
accounts.register(Contact)

View File

@ -1,4 +1,5 @@
from django.conf import settings
from django_countries import data
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
@ -9,7 +10,7 @@ CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES
CONTACTS_DEFAULT_CITY = getattr(settings, 'CONTACTS_DEFAULT_CITY', 'Barcelona')
CONTACTS_DEFAULT_PROVINCE = getattr(settings, 'CONTACTS_DEFAULT_PROVINCE', 'Barcelona')
CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', data.COUNTRIES)
CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'Spain')
CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'ES')

View File

@ -2,8 +2,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address,
validate_hostname, validate_ascii)
from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii
from orchestra.utils.python import AttrDict
from . import settings, validators, utils
@ -11,11 +10,11 @@ from . import settings, validators, utils
class Domain(models.Model):
name = models.CharField(_("name"), max_length=256, unique=True,
validators=[validate_hostname, validators.validate_allowed_domain],
validators=[validators.validate_domain_name, validators.validate_allowed_domain],
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')
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains', editable=False)
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial,
help_text=_("Serial number"))

View File

@ -4,6 +4,7 @@ import re
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.core.validators import validate_hostname
from orchestra.utils import paths
from orchestra.utils.system import run
@ -23,6 +24,15 @@ def validate_allowed_domain(value):
raise ValidationError(_("This domain name is not allowed"))
def validate_domain_name(value):
# SRV records may use '_' in the domain name
value = value.lstrip('*.').replace('_', '')
try:
validate_hostname(value)
except ValidationError:
raise ValidationError(_("Not a valid domain name."))
def validate_zone_interval(value):
try:
int(value)

View File

@ -24,6 +24,11 @@ class PaymentMethod(plugins.Plugin):
plugins.append(import_class(cls))
return plugins
@classmethod
def clean_data(cls, data):
""" model clean """
return data
def get_form(self):
self.form.plugin = self
self.form.plugin_field = 'method'

View File

@ -24,24 +24,12 @@ class SEPADirectDebitForm(PluginDataForm):
name = forms.CharField(max_length=128, label=_("Name"),
widget=forms.TextInput(attrs={'size': '50'}))
def clean_iban(self):
return self.cleaned_data['iban'].strip()
def clean_name(self):
return self.cleaned_data['name'].strip()
class SEPADirectDebitSerializer(serializers.Serializer):
iban = serializers.CharField(label='IBAN', validators=[IBANValidator()],
min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34)
name = serializers.CharField(label=_("Name"), max_length=128)
def clean_iban(self, attrs, source):
return attrs[source].strip()
def clean_name(self, attrs, source):
return attrs[source].strip()
class SEPADirectDebit(PaymentMethod):
verbose_name = _("SEPA Direct Debit")
@ -56,6 +44,13 @@ class SEPADirectDebit(PaymentMethod):
return _("This bill will been automatically charged to your bank account "
" with IBAN number<br><strong>%s</strong>.") % source.number
@classmethod
def clean_data(cls, data):
data['iban'] = data['iban'].strip()
data['name'] = data['name'].strip()
IBANValidator()(data['iban'])
return data
@classmethod
def process(cls, transactions):
debts = []

View File

@ -50,6 +50,9 @@ class PaymentSource(models.Model):
def get_due_delta(self):
return self.method_class().due_delta
def clean(self):
self.data = self.method_class().clean_data(self.data)
class TransactionQuerySet(models.QuerySet):
group_by = group_by

View File

@ -23,7 +23,7 @@ class SystemUserQuerySet(models.QuerySet):
class SystemUser(models.Model):
""" System users """
username = models.CharField(_("username"), max_length=64, unique=True,
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')])
password = models.CharField(_("password"), max_length=128)

View File

@ -61,6 +61,14 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', {
})
WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {})
for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems():
if value is None:
WEBAPPS_TYPES.pop(webapp_type, None)
else:
WEBAPPS_TYPES[webapp_type] = value
WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5')
@ -74,51 +82,91 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
# PHP
'enabled_functions': (
_("PHP - Enabled functions"),
r'^[\w.,-]+$'
),
'PHP-register_globals': (
_("PHP - Register globals"),
r'^(On|Off|on|off)$'
r'^[\w\.,-]+$'
),
'PHP-allow_url_include': (
_("PHP - Allow URL include"),
r'^(On|Off|on|off)$'
),
'PHP-allow_url_fopen': (
_("PHP - allow_url_fopen"),
r'^(On|Off|on|off)$'
),
'PHP-auto_append_file': (
_("PHP - Auto append file"),
r'^none$'
r'^[\w\.,-/]+$'
),
'PHP-auto_prepend_file': (
_("PHP - Auto prepend file"),
r'^[\w\.,-/]+$'
),
'PHP-date.timezone': (
_("PHP - date.timezone"),
r'^\w+/\w+$'
),
'PHP-default_socket_timeout': (
_("PHP - Default socket timeout"),
r'P^[0-9][0-9]?[0-9]?$'
r'^[0-9]{1,3}$'
),
'PHP-display_errors': (
_("PHP - Display errors"),
r'^(On|Off|on|off)$'
),
'PHP-extension': (
_("PHP - Extension"),
r'^[^ ]+$'
),
'PHP-magic_quotes_gpc': (
_("PHP - Magic quotes GPC"),
r'^(On|Off|on|off)$'
),
'PHP-magic_quotes_runtime': (
_("PHP - Magic quotes runtime"),
r'^(On|Off|on|off)$'
),
'PHP-magic_quotes_sybase': (
_("PHP - Magic quotes sybase"),
r'^(On|Off|on|off)$'
),
'PHP-max_execution_time': (
_("PHP - Max execution time"),
r'^[0-9][0-9]?[0-9]?$'
r'^[0-9]{1,3}$'
),
'PHP-max_input_time': (
_("PHP - Max input time"),
r'^[0-9][0-9]?[0-9]?$'
r'^[0-9]{1,3}$'
),
'PHP-memory_limit': (
_("PHP - Memory limit"),
r'^[0-9][0-9]?[0-9]?M$'
r'^[0-9]{1,3}M$'
),
'PHP-mysql.connect_timeout': (
_("PHP - Mysql connect timeout"),
r'^[0-9][0-9]?[0-9]?$'
r'^([0-9]){1,3}$'
),
'PHP-output_buffering': (
_("PHP - output_buffering"),
r'^(On|Off|on|off)$'
),
'PHP-register_globals': (
_("PHP - Register globals"),
r'^(On|Off|on|off)$'
),
'PHP-post_max_size': (
_("PHP - Post max size"),
r'^[0-9][0-9]?M$'
r'^[0-9]{1,3}M$'
),
'PHP-sendmail_path': (
_("PHP - sendmail_path"),
r'^[^ ]+$'
),
'PHP-session.bug_compat_warn': (
_("PHP - session.bug_compat_warn"),
r'^(On|Off|on|off)$'
),
'PHP-session.auto_start': (
_("PHP - session.auto_start"),
r'^(On|Off|on|off)$'
),
'PHP-safe_mode': (
_("PHP - Safe mode"),
@ -126,32 +174,48 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
),
'PHP-suhosin.post.max_vars': (
_("PHP - Suhosin post max vars"),
r'^[0-9][0-9]?[0-9]?[0-9]?$'
r'^[0-9]{1,4}$'
),
'PHP-suhosin.request.max_vars': (
_("PHP - Suhosin request max vars"),
r'^[0-9][0-9]?[0-9]?[0-9]?$'
r'^[0-9]{1,4}$'
),
'PHP-suhosin.session.encrypt': (
_("PHP - suhosin.session.encrypt"),
r'^(On|Off|on|off)$'
),
'PHP-suhosin.simulation': (
_("PHP - Suhosin simulation"),
r'^(On|Off|on|off)$'
),
'PHP-suhosin.executor.include.whitelist': (
_("PHP - suhosin.executor.include.whitelist"),
r'^(upload|phar)$'
),
'PHP-upload_max_filesize': (
_("PHP - upload_max_filesize"),
r'^[0-9]{1,3}M$'
),
'PHP-zend_extension': (
_("PHP - zend_extension"),
r'^[^ ]+$'
),
# FCGID
'FcgidIdleTimeout': (
_("FCGI - Idle timeout"),
r'^[0-9][0-9]?[0-9]?$'
r'^[0-9]{1,3}$'
),
'FcgidBusyTimeout': (
_("FCGI - Busy timeout"),
r'^[0-9][0-9]?[0-9]?$'
r'^[0-9]{1,3}$'
),
'FcgidConnectTimeout': (
_("FCGI - Connection timeout"),
r'^[0-9][0-9]?[0-9]?$'
r'^[0-9]{1,3}$'
),
'FcgidIOTimeout': (
_("FCGI - IO timeout"),
r'^[0-9][0-9]?[0-9]?$'
r'^[0-9]{1,3}$'
),
})

View File

@ -24,6 +24,7 @@ class Apache2Backend(ServiceController):
if site.protocol is 'https':
extra_conf += self.get_ssl(site)
extra_conf += self.get_security(site)
extra_conf += self.get_redirect(site)
context['extra_conf'] = extra_conf
apache_conf = Template(textwrap.dedent("""\
@ -89,7 +90,7 @@ class Apache2Backend(ServiceController):
<Directory %(app_path)s>
Options +ExecCGI
AddHandler fcgid-script .php
FcgidWrapper %(fcgid_path)s
FcgidWrapper %(fcgid_path)s\
""" % context)
for option in content.webapp.options.filter(name__startswith='Fcgid'):
fcgid += " %s %s\n" % (option.name, option.value)
@ -101,10 +102,12 @@ class Apache2Backend(ServiceController):
custom_cert = site.options.filter(name='ssl')
if custom_cert:
cert = tuple(custom_cert[0].value.split())
# TODO separate directtives?
directives = textwrap.dedent("""\
SSLEngine on
SSLCertificateFile %s
SSLCertificateKeyFile %s""" % cert
SSLCertificateKeyFile %s\
""" % cert
)
return directives
@ -112,14 +115,24 @@ class Apache2Backend(ServiceController):
directives = ''
for rules in site.options.filter(name='sec_rule_remove'):
for rule in rules.value.split():
directives += "SecRuleRemoveById %i" % int(rule)
directives += "SecRuleRemoveById %i\n" % int(rule)
for modsecurity in site.options.filter(name='sec_rule_off'):
directives += textwrap.dedent("""\
<LocationMatch %s>
SecRuleEngine Off
</LocationMatch>
</LocationMatch>\
""" % modsecurity.value)
if directives:
directives = '<IfModule mod_security2.c>\n%s\n</IfModule>' % directives
return directives
def get_redirect(self, site):
directives = ''
for redirect in site.options.filter(name='redirect'):
if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect.value):
directives += "RedirectMatch %s" % redirect.value
else:
directives += "Redirect %s" % redirect.value
return directives
def get_protections(self, site):

View File

@ -17,25 +17,34 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*')
WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain')
# TODO ssl ca, ssl cert, ssl key
WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', {
# { name: ( verbose_name, validation_regex ) }
'directory_protection': (
_("HTTPD - Directory protection"),
r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$'
),
'redirection': (
'redirect': (
_("HTTPD - Redirection"),
r'^.*\s+.*$'
r'^(permanent\s[^ ]+|[^ ]+)\s[^ ]+$'
),
'ssl': (
_("HTTPD - SSL"),
r'^.*\s+.*$'
'ssl_ca': (
_("HTTPD - SSL CA"),
r'^[^ ]+$'
),
'ssl_cert': (
_("HTTPD - SSL cert"),
r'^[^ ]+$'
),
'ssl_key': (
_("HTTPD - SSL key"),
r'^[^ ]+$'
),
'sec_rule_remove': (
_("HTTPD - SecRuleRemoveById"),
r'^[0-9,\s]+$'
r'^[0-9\s]+$'
),
'sec_rule_off': (
'sec_engine': (
_("HTTPD - Disable Modsecurity"),
r'^[\w/_]+$'
),

View File

@ -155,7 +155,10 @@ function install_requirements () {
lxml==3.3.5 \
python-dateutil==2.2 \
django-iban==0.3.0 \
requests"
requests \
phonenumbers \
django-countries \
django-localflavor"
if $testing; then
APT="${APT} \

View File

@ -1,12 +1,16 @@
import re
import crack
import localflavor
import phonenumbers
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from IPy import IP
from ..utils.python import import_class
def validate_ipv4_address(value):
msg = _("%s is not a valid IPv4 address") % value
@ -18,7 +22,6 @@ def validate_ipv4_address(value):
raise ValidationError(msg)
def validate_ipv6_address(value):
msg = _("%s is not a valid IPv6 address") % value
try:
@ -61,11 +64,12 @@ def validate_hostname(hostname):
http://stackoverflow.com/a/2532344
"""
if len(hostname) > 255:
return False
if hostname[-1] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
raise ValidationError(_("Too long for a hostname."))
hostname = hostname.rstrip('.')
allowed = re.compile('(?!-)[A-Z\d-]{1,63}(?<!-)$', re.IGNORECASE)
for name in hostname.split('.'):
if not allowed.match(name):
raise ValidationError(_("Not a valid hostname (%s).") % name)
def validate_password(value):
@ -78,3 +82,40 @@ def validate_password(value):
def validate_url_path(value):
if not re.match(r'^\/[/.a-zA-Z0-9-_]*$', value):
raise ValidationError(_('"%s" is not a valid URL path.') % value)
def validate_vat(vat, country):
field = 'localflavor.{lower}.forms.{upper}IdentityCardNumberField'.format(
lower=country.lower(),
upper=country.upper()
)
try:
field = import_class(field)
except (ImportError, AttributeError, ValueError):
pass
else:
field().clean(vat)
def validate_zipcode(zipcode, country):
field = 'localflavor.{lower}.forms.{upper}PostalCodeField'.format(
lower=country.lower(),
upper=country.upper()
)
try:
field = import_class(field)
except (ImportError, AttributeError, ValueError):
pass
else:
field().clean(zipcode)
def validate_phone(value, country):
""" local phone number or international """
msg = _("Not a valid %s nor international phone number.") % country
try:
number = phonenumbers.parse(value, country)
except phonenumbers.phonenumberutil.NumberParseException:
raise ValidationError(msg)
if not phonenumbers.is_valid_number(number):
raise ValidationError(msg)

View File

@ -29,6 +29,10 @@ class MultiSelectField(models.CharField):
def to_python(self, value):
if value is not None:
if isinstance(value, list) and value[0].startswith('('):
# Workaround unknown bug on default model values
# [u"('SUPPORT'", u" 'ADMIN'", u" 'BILLING'", u" 'TECH'", u" 'ADDS'", u" 'EMERGENCY')"]
value = list(eval(', '.join(value)))
return value if isinstance(value, list) else value.split(',')
return ''
@ -36,7 +40,7 @@ class MultiSelectField(models.CharField):
super(MultiSelectField, self).contribute_to_class(cls, name)
if self.choices:
def func(self, field=name, choices=dict(self.choices)):
','.join([ choices.get(value, value) for value in getattr(self, field) ])
return ','.join([ choices.get(value, value) for value in getattr(self, field) ])
setattr(cls, 'get_%s_display' % self.name, func)
def validate(self, value, model_instance):

View File

@ -21,11 +21,11 @@ def check_root(func):
return wrapped
class _AttributeString(str):
class _AttributeUnicode(unicode):
""" Simple string subclass to allow arbitrary attribute access. """
@property
def stdout(self):
return str(self)
return unicode(self)
def make_async(fd):
@ -43,7 +43,7 @@ def read_async(fd):
if e.errno != errno.EAGAIN:
raise e
else:
return ''
return u''
def run(command, display=False, error_codes=[0], silent=False, stdin=''):
@ -60,8 +60,8 @@ def run(command, display=False, error_codes=[0], silent=False, stdin=''):
make_async(p.stdout)
make_async(p.stderr)
stdout = str()
stderr = str()
stdout = unicode()
stderr = unicode()
# Async reading of stdout and sterr
while True:
@ -77,15 +77,15 @@ def run(command, display=False, error_codes=[0], silent=False, stdin=''):
if display and stderrPiece:
sys.stderr.write(stderrPiece)
stdout += stdoutPiece
stderr += stderrPiece
stdout += stdoutPiece.decode("utf8")
stderr += stderrPiece.decode("utf8")
returnCode = p.poll()
if returnCode != None:
break
out = _AttributeString(stdout.strip())
err = _AttributeString(stderr.strip())
out = _AttributeUnicode(stdout.strip())
err = _AttributeUnicode(stderr.strip())
p.stdout.close()
p.stderr.close()

View File

@ -1,6 +1,99 @@
import re
import glob
import sys
import errno
import fcntl
import getpass
import os
import re
import select
import subprocess
import textwrap
class _AttributeString(str):
""" Simple string subclass to allow arbitrary attribute access. """
@property
def stdout(self):
return str(self)
def make_async(fd):
""" Helper function to add the O_NONBLOCK flag to a file descriptor """
fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)
def read_async(fd):
"""
Helper function to read some data from a file descriptor, ignoring EAGAIN errors
"""
try:
return fd.read()
except IOError, e:
if e.errno != errno.EAGAIN:
raise e
else:
return ''
def run(command, display=False, error_codes=[0], silent=False, stdin=''):
""" Subprocess wrapper for running commands """
if display:
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
p = subprocess.Popen(command, shell=True, executable='/bin/bash',
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
p.stdin.write(stdin)
p.stdin.close()
make_async(p.stdout)
make_async(p.stderr)
stdout = str()
stderr = str()
# Async reading of stdout and sterr
while True:
# Wait for data to become available
select.select([p.stdout, p.stderr], [], [])
# Try reading some data from each
stdoutPiece = read_async(p.stdout)
stderrPiece = read_async(p.stderr)
if display and stdoutPiece:
sys.stdout.write(stdoutPiece)
if display and stderrPiece:
sys.stderr.write(stderrPiece)
stdout += stdoutPiece
stderr += stderrPiece
returnCode = p.poll()
if returnCode != None:
break
out = _AttributeString(stdout.strip())
err = _AttributeString(stderr.strip())
p.stdout.close()
p.stderr.close()
out.failed = False
out.return_code = returnCode
out.stderr = err
if p.returncode not in error_codes:
out.failed = True
msg = "\nrun() encountered an error (return code %s) while executing '%s'\n"
msg = msg % (p.returncode, command)
if display:
sys.stderr.write("\n\033[1;31mCommandError: %s %s\033[m\n" % (msg, err))
if not silent:
raise AttributeError("%s %s %s" % (msg, err, out))
out.succeeded = not out.failed
return out
print "from orchestra.apps.accounts.models import Account"
print "from orchestra.apps.domains.models import Domain"
@ -8,10 +101,24 @@ print "from orchestra.apps.webapps.models import WebApp"
print "from orchestra.apps.websites.models import Website, Content"
def print_webapp(context):
print textwrap.dedent("""\
try:
webapp = WebApp.objects.get(account=account, name='%(name)s')
except:"
webapp = WebApp.objects.create(account=account, name='%(name)s', type='%(type)s')
else:
webapp.type = '%(type)s'
webapp.save()"
Content.objects.get_or_create(website=website, webapp=webapp, path='%(path)s')
""" % context
)
for conf in glob.glob('/etc/apache2/sites-enabled/*'):
username = conf.split('/')[-1].split('.')[0]
with open(conf, 'rb') as conf:
print "account = Account.objects.get(user__username='%s')" % username
print "account = Account.objects.get(username='%s')" % username
for line in conf.readlines():
line = line.strip()
if line.startswith('<VirtualHost'):
@ -20,43 +127,64 @@ for conf in glob.glob('/etc/apache2/sites-enabled/*'):
apps = []
if line.endswith(':443>'):
port = 443
wrapper_root = None
webalizer = False
webappname = None
elif line.startswith("DocumentRoot"):
__, path = line.split()
webappname = path.rstrip('/').split('/')[-1]
if webappname == 'public_html':
webappname = ''
elif line.startswith("ServerName"):
domain = line.split()[1]
name = domain
__, domain = line.split()
sitename = domain
domains.append("'%s'" % domain)
elif line.startswith("ServerAlias"):
for domain in line.split()[1:]:
domains.append("'%s'" % domain)
elif line.startswith("Alias /fcgi-bin/"):
fcgid = line.split('/')[-1] or line.split('/')[-2]
fcgid = fcgid.split('-')[0]
apps.append((name, fcgid, '/'))
__, __, wrapper_root = line.split()
elif line.startswith('Action php-fcgi'):
__, __, wrapper_name = line.split()
wrapper_name = wrapper_name.split('/')[-1]
elif line.startswith("Alias /webalizer"):
apps.append(('webalizer', 'webalizer', '/webalizer'))
webalizer = True
elif line == '</VirtualHost>':
if port == 443:
name += '-ssl'
print "# SITE"
print "website, __ = Website.objects.get_or_create(name='%s', account=account, port=%d)" % (name, port)
domains = ', '.join(domains)
print "for domain in [%s]:" % str(domains)
print " try:"
print " domain = Domain.objects.get(name=domain)"
print " except:"
print " domain = Domain.objects.create(name=domain, account=account)"
print " else:"
print " domain.account = account"
print " domain.save()"
print " website.domains.add(domain)"
print ""
for name, type, path in apps:
print "try:"
print " webapp = WebApp.objects.get(account=account, name='%s')" % name
print "except:"
print " webapp = WebApp.objects.create(account=account, name='%s', type='%s')" % (name, type)
print "else:"
print " webapp.type = '%s'" % type
print " webapp.save()"
print ""
print "Content.objects.get_or_create(website=website, webapp=webapp, path='%s')" % path
sitename += '-ssl'
context = {
'sitename': sitename,
'port': port,
'domains': ', '.join(domains),
}
print textwrap.dedent("""\
# SITE"
website, __ = Website.objects.get_or_create(name='%(sitename)s', account=account, port=%(port)d)
for domain in [%(domains)s]:
try:
domain = Domain.objects.get(name=domain)
except:
domain = Domain.objects.create(name=domain, account=account)
else:
domain.account = account
domain.save()
website.domains.add(domain)
""" % context)
if wrapper_root:
wrapper = os.join(wrapper_root, wrapper_name)
fcgid = run('grep "^\s*exec " %s' % wrapper).stdout
type = fcgid.split()[1].split('/')[-1].split('-')[0]
for option in fcgid.split('-d'):
print option
print_webapp({
'name': webappname,
'path': '/',
'type': type,
})
if webalizer:
print_webapp({
'name': 'webalizer-%s' % sitename,
'path': '/webalizer',
'type': 'webalizer',
})
print '\n'

View File

@ -1,34 +1,64 @@
#!/bin/bash
VIRTUALTABLE="/etc/postfix/virtusertable"
VIRTUALTABLE=${1-"/etc/postfix/virtusertable"}
echo "from orchestra.apps.users import User"
echo "from orchestra.apps.users.roles.mailbox import Address, Mailbox"
echo "from orchestra.apps.domains import Domain"
echo "from orchestra.apps.accounts.models import Account"
echo "from orchestra.apps.mailboxes.models import Address, Mailbox"
echo "from orchestra.apps.domains.models import Domain"
echo "main_account = Account.objects.get(id=1)"
cat "$VIRTUALTABLE"|grep -v "^\s*$"|while read line; do
NAME=$(echo "$line" | awk {'print $1'} | cut -d'@' -f1)
DOMAIN=$(echo "$line" | awk {'print $1'} | cut -d'@' -f2)
DESTINATION=$(echo "$line" | awk '{$1=""; print $0}' | sed -e 's/^ *//' -e 's/ *$//')
echo "domain = Domain.objects.get(name='$DOMAIN')"
echo "mailboxes = []"
echo "account = main_account"
NEW_DESTINATION=""
for PLACE in $DESTINATION; do
if [[ ! $(echo $PLACE | grep '@') ]]; then
echo "try:"
echo " user = User.objects.get(username='$PLACE')"
echo "except:"
echo " print 'User $PLACE does not exists'"
if [[ $(grep "^${PLACE}:" /etc/shadow) ]]; then
PASSWORD=$(grep "^${PLACE}:" /etc/shadow | cut -d':' -f2)
echo "if account == main_account and domain.account != main_account:"
echo " account = domain.account"
echo "else:"
echo " mailbox, __ = Mailbox.objects.get_or_create(user=user)"
echo " if user.account_id != 1:"
echo " user.account=domain.account"
echo " user.save()"
echo ""
echo " try:"
echo " account = Account.objects.get(username='${PLACE}')"
echo " except:"
echo " pass"
echo "mailboxes.append(('${PLACE}', '${PASSWORD}'))"
else
NEW_DESTINATION="${NEW_DESTINATION} ${PLACE}"
fi
else
NEW_DESTINATION="${NEW_DESTINATION} ${PLACE}"
fi
done
echo "address, __ = Address.objects.get_or_create(name='$NAME', domain=domain)"
echo "address.account=domain.account"
echo "address.destination='$DESTINATION'"
echo "address.save()"
echo "for mailbox, password in mailboxes:"
echo " mailbox = mailbox.strip()"
echo " try:"
echo " mailbox = Mailbox.objects.get(username=mailbox)"
echo " except:"
echo " mailbox = Mailbox(username=mailbox, password=password, account=account)"
echo " try:"
echo " mailbox.full_clean()"
echo " except:"
echo " sys.stderr.write('cleaning')"
echo " else:"
echo " mailbox.save()"
echo " else:"
echo " if mailbox.account != account:"
echo " sys.stderr.write('%s != %s' % (mailbox.account, account))"
echo " if domain.account != account:"
echo " sys.stderr.write('%s != %s' % (domain.account, account))"
echo " address = Address(name='${NAME}', domain=domain, account=account, destination='${NEW_DESTINATION}')"
echo " try:"
echo " address.full_clean()"
echo " except:"
echo " sys.stderr.write('cleaning address')"
echo " else:"
echo " address.save()"
echo " domain = None"
done