123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- # -*- 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
|