|
@@ -0,0 +1,159 @@
|
|
|
+# -*- 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_ldap"
|
|
|
+ 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
|