# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings import ldapdb.models from ldapdb.models.fields import CharField, ListField from coin.mixins import CoinLdapSyncMixin from coin.configuration.models import Configuration from coin import validation, utils # Implementationo of a simple Radius backend for DSL, stored in LDAP. # Settings # - LDAP branch class RadiusGroup(models.Model): """This implements a notion of "Radius group", which is mostly useful to support multiple backhaul operators (i.e. each object provides settings for a specific backhaul operator). For now, it only changes the realm of the login stored in LDAP, but it might also be used in the future to implement different Radius profile for each group (different L2TP server, etc). One possible usage could be to load-balance users (possibly on the same backhaul infrastracture) to different L2TP servers by placing them in different Radius groups. """ name = models.CharField(max_length=256, verbose_name=_("group name")) realm = models.CharField(max_length=50, verbose_name=_("radius realm"), help_text=_('Example: "fdn.nerim"')) suffix = models.CharField(max_length=50, blank=True, verbose_name=_("suffix"), help_text=_('Optional suffix added to the login, as a kind of "sub-realm". Example: "%gnd"')) comment = models.CharField(max_length=256, blank=True, verbose_name=_("comment")) def __unicode__(self): return "{} ({}@{})".format(self.name, self.suffix, self.realm) class Meta: verbose_name = _("radius group") verbose_name_plural = _("radius groups") class DSLConfiguration(CoinLdapSyncMixin, Configuration): """TODO: make sure that the (full) login never changes, because it's a LDAP primary key. Or at least delete the old object and create a new one when the login changes. """ url_namespace = "dsl" phone_number = models.CharField(max_length=20, verbose_name=_('phone number'), help_text=_("Phone number associated to the DSL line")) activated = models.BooleanField(default=False, verbose_name=_('activated')) radius_group = models.ForeignKey(RadiusGroup, verbose_name=_("radius group"), help_text=_("Group (i.e. backhaul) to use")) # TODO: introduce forbidden characters (and read RFC2865) login = models.CharField(max_length=50, unique=True, blank=True, verbose_name=_("login"), help_text=_("Leave empty for automatic generation")) password = models.CharField(max_length=256, blank=True, verbose_name=_("password"), help_text=_("Will be stored in cleartext! Automatically generated if empty")) def full_login(self): """Login with realm""" return "{}{}@{}".format(self.login, self.radius_group.suffix, self.radius_group.realm) def clean(self): # Generate DSL login, of the form "login-dslX". if not self.login: username = self.offersubscription.member.username dsl_lines = DSLConfiguration.objects.filter(offersubscription__member__username=username) # This is the list of existing DSL logins for this user. logins = [dsl.login for dsl in dsl_lines] # 100 DSL lines ought to be enough for anybody. for login in ["{}-dsl{}".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("Impossible de générer un login DSL") # Generate password: 8 lowercase letters. We don't really care # about security, since 1/ we store it in cleartext 2/ to # bruteforce it, you must have a DSL line on the same backhaul # infrastructure. On the other hand, it must be easy to copy it # by hand to configure a modem. if not self.password: self.password = utils.generate_weak_password(8) # This method is part of the general configuration interface. def subnet_event(self): self.sync_to_ldap(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 = LdapDSLConfig() else: config = LdapDSLConfig.objects.get(pk=self.full_login()) config.login = self.full_login() config.cleartext_password = self.password config.password = utils.ldap_hash(self.password) config.active = 'yes' if self.activated else 'no' v4_subnets = self.get_subnets(4) v6_subnets = self.get_subnets(6) # Instead of having an explicit IPv4 endpoint in the model, we # simply take the first IPv4 subnet (which we assume is a /32) if len(v4_subnets) > 0: config.ipv4_endpoint = str(v4_subnets[0].inet.ip) else: config.ipv4_endpoint = None config.ranges_v4 = [str(s) for s in v4_subnets] if len(v6_subnets) > 0: config.ranges_v6 = [str(s) for s in v6_subnets] else: # This field is multi-valued, but mandatory... Hack hack hack. config.ranges_v6 = ["fd20:fd79:5eb6:5cc5::/64"] config.save() def delete_from_ldap(self): LdapDSLConfig.objects.get(pk=self.full_login()).delete() def __unicode__(self): return self.full_login() class Meta: verbose_name = _("DSL line") verbose_name_plural = _("DSL lines") class LdapDSLConfig(ldapdb.models.Model): base_dn = settings.DSL_CONF_BASE_DN # "ou=radius,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR" object_classes = [b'top', b'radiusObjectProfile', b'radiusprofile', b'ipHost'] login = CharField(db_column=b'cn', primary_key=True, max_length=255) password = CharField(db_column=b'userPassword', max_length=255) cleartext_password = CharField(db_column=b'description', max_length=255) active = CharField(db_column=b'dialupAccess', max_length=3) ipv4_endpoint = CharField(db_column=b'radiusFramedIPAddress', max_length=16) ranges_v4 = ListField(db_column=b'radiusFramedRoute') # This field is multi-valued, but mandatory... ranges_v6 = ListField(db_column=b'ipHostNumber') def __unicode__(self): return self.login class Meta: managed = False # Indique à South de ne pas gérer le model LdapUser