# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models from django.core.exceptions import ValidationError from django.conf import settings from django.core.urlresolvers import reverse from netfields import InetAddressField, NetManager import ldapdb.models from ldapdb.models.fields import CharField, ListField from coin.mixins import CoinLdapSyncMixin from coin.offers.models import OfferSubscription from coin.configuration.models import Configuration # from coin.offers.backends import ValidateBackendType from coin import utils from coin import validation class VPNConfiguration(CoinLdapSyncMixin, Configuration): url_namespace = "vpn" # backend_name = "openvpn_ldap" # administrative_subscription = models.OneToOneField( # 'offers.OfferSubscription', # related_name=backend_name, # validators=[ValidateBackendType(backend_name)]) activated = models.BooleanField(default=False, verbose_name='activé') login = models.CharField(max_length=50, unique=True, blank=True, verbose_name="identifiant", help_text="leave empty for automatic generation") password = models.CharField(max_length=256, verbose_name="mot de passe", blank=True, null=True) ipv4_endpoint = InetAddressField(validators=[validation.validate_v4], verbose_name="IPv4", blank=True, null=True, help_text="Adresse IPv4 utilisée par " "défaut sur le VPN") ipv6_endpoint = InetAddressField(validators=[validation.validate_v6], verbose_name="IPv6", blank=True, null=True, help_text="Adresse IPv6 utilisée par " "défaut sur le VPN") comment = models.CharField(blank=True, max_length=512, verbose_name="commentaire") objects = NetManager() def get_absolute_url(self): return reverse('vpn:details', args=[str(self.pk)]) # These two methods are part of the general configuration interface. def save_subnet(self, subnet, creation): self.check_endpoints(delete=True) # We potentially changed the endpoints, so we need to save. self.full_clean() self.save() def delete_subnet(self, subnet): self.save_subnet(subnet, False) def get_subnets(self, version): subnets = self.ip_subnet.all() return [subnet for subnet in subnets if subnet.inet.version == version] def sync_to_ldap(self, creation, *args, **kwargs): if creation: config = LdapVPNConfig() else: config = LdapVPNConfig.objects.get(pk=self.login) config.login = config.sn = self.login config.password = self.password config.active = 'yes' if self.activated else 'no' config.ipv4_endpoint = utils.str_or_none(self.ipv4_endpoint) config.ipv6_endpoint = utils.str_or_none(self.ipv6_endpoint) config.ranges_v4 = [str(s) for s in self.get_subnets(4)] config.ranges_v6 = [str(s) for s in self.get_subnets(6)] config.save() def delete_from_ldap(self): LdapVPNConfig.objects.get(pk=self.login).delete() def generate_endpoints(self, v4=True, v6=True): """Generate IP endpoints in one of the attributed IP subnets. If there is no available subnet for a given address family, then no endpoint is generated for this address family. If there already is an endpoint, do nothing. Returns True if an endpoint was generated. TODO: this should be factored for other technologies (DSL, etc) """ subnets = self.ip_subnet.all() updated = False if v4 and self.ipv4_endpoint is None: subnets_v4 = [s for s in subnets if s.inet.version == 4] if len(subnets_v4) > 0: self.ipv4_endpoint = subnets_v4[0].inet.ip updated = True if v6 and self.ipv6_endpoint is None: subnets_v6 = [s for s in subnets if s.inet.version == 6] if len(subnets_v6) > 0: # With v6, we choose the second host of the subnet (cafe::1) gen = subnets_v6[0].inet.iter_hosts() gen.next() self.ipv6_endpoint = gen.next() updated = True return updated def check_endpoints(self, delete=False): """Check that the IP endpoints are included in one of the attributed IP subnets. If [delete] is True, then simply delete the faulty endpoints instead of raising an exception. """ subnets = self.ip_subnet.all() is_faulty = lambda endpoint : endpoint and not any([endpoint in subnet.inet for subnet in subnets]) if is_faulty(self.ipv4_endpoint): if delete: self.ipv4_endpoint = None else: raise ValidationError("Endpoint {} is not in an attributed range".format(self.ipv4_endpoint)) if is_faulty(self.ipv6_endpoint): if delete: self.ipv6_endpoint = None else: raise ValidationError("Endpoint {} is not in an attributed range".format(self.ipv6_endpoint)) def clean(self): # Generate VPN login, of the form "login-vpnX". The resulting # login should not contain any ".", because graphite uses "." as a # separator. if not self.login: username = self.offersubscription.member.username vpns = VPNConfiguration.objects.filter(offersubscription__member__username=username) # This is the list of existing VPN logins for this user. logins = [vpn.login for vpn in vpns] # 100 VPNs ought to be enough for anybody. for login in ["{}-vpn{}".format(username, k) for k in range(1, 101)]: if login not in logins: self.login = login break # We may have failed. if not self.login: ValidationError("Unable to allocate a VPN login.") # Hash password if needed self.password = utils.ldap_hash(self.password) # If saving for the first time and IP endpoints are not specified, # generate them automatically. if self.pk is None: self.generate_endpoints() self.check_endpoints() def __unicode__(self): return 'VPN ' + self.login class Meta: verbose_name = 'VPN' class LdapVPNConfig(ldapdb.models.Model): # TODO: déplacer ligne suivante dans settings.py base_dn = settings.VPN_CONF_BASE_DN # "ou=vpn,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR" object_classes = [b'person', b'organizationalPerson', b'inetOrgPerson', b'top', b'radiusprofile'] login = CharField(db_column=b'cn', primary_key=True, max_length=255) sn = CharField(db_column=b'sn', max_length=255) password = CharField(db_column=b'userPassword', max_length=255) active = CharField(db_column=b'dialupAccess', max_length=3) ipv4_endpoint = CharField(db_column=b'radiusFramedIPAddress', max_length=16) ipv6_endpoint = CharField(db_column=b'postalAddress', max_length=40) ranges_v4 = ListField(db_column=b'radiusFramedRoute') ranges_v6 = ListField(db_column=b'registeredAddress') def __unicode__(self): return self.login class Meta: managed = False # Indique à South de ne pas gérer le model LdapUser