models.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. from django.db import models
  4. from django.utils.translation import ugettext_lazy as _
  5. from django.conf import settings
  6. import ldapdb.models
  7. from ldapdb.models.fields import CharField, ListField
  8. from coin.mixins import CoinLdapSyncMixin
  9. from coin.configuration.models import Configuration
  10. from coin import validation, utils
  11. # Implementationo of a simple Radius backend for DSL, stored in LDAP.
  12. # Settings
  13. # - LDAP branch
  14. class RadiusGroup(models.Model):
  15. """This implements a notion of "Radius group", which is mostly useful to
  16. support multiple backhaul operators (i.e. each object provides
  17. settings for a specific backhaul operator).
  18. For now, it only changes the realm of the login stored in LDAP, but it
  19. might also be used in the future to implement different Radius profile
  20. for each group (different L2TP server, etc). One possible usage could
  21. be to load-balance users (possibly on the same backhaul
  22. infrastracture) to different L2TP servers by placing them in different
  23. Radius groups.
  24. """
  25. name = models.CharField(max_length=256, verbose_name=_("group name"))
  26. realm = models.CharField(max_length=50,
  27. verbose_name=_("radius realm"),
  28. help_text=_('Example: "fdn.nerim"'))
  29. suffix = models.CharField(max_length=50, blank=True,
  30. verbose_name=_("suffix"),
  31. help_text=_('Optional suffix added to the login, as a kind of "sub-realm". Example: "%gnd"'))
  32. comment = models.CharField(max_length=256, blank=True,
  33. verbose_name=_("comment"))
  34. def __unicode__(self):
  35. return "{} ({}@{})".format(self.name, self.suffix, self.realm)
  36. class Meta:
  37. verbose_name = _("radius group")
  38. verbose_name_plural = _("radius groups")
  39. class DSLConfiguration(CoinLdapSyncMixin, Configuration):
  40. """TODO: make sure that the (full) login never changes, because it's a
  41. LDAP primary key. Or at least delete the old object and create a new
  42. one when the login changes.
  43. """
  44. url_namespace = "dsl_ldap"
  45. phone_number = models.CharField(max_length=20,
  46. verbose_name=_('phone number'),
  47. help_text=_("Phone number associated to the DSL line"))
  48. activated = models.BooleanField(default=False, verbose_name=_('activated'))
  49. radius_group = models.ForeignKey(RadiusGroup, verbose_name=_("radius group"),
  50. help_text=_("Group (i.e. backhaul) to use"))
  51. # TODO: introduce forbidden characters (and read RFC2865)
  52. login = models.CharField(max_length=50, unique=True, blank=True,
  53. verbose_name=_("login"),
  54. help_text=_("Leave empty for automatic generation"))
  55. password = models.CharField(max_length=256, blank=True,
  56. verbose_name=_("password"),
  57. help_text=_("Will be stored in cleartext! Automatically generated if empty"))
  58. def full_login(self):
  59. """Login with realm"""
  60. return "{}{}@{}".format(self.login, self.radius_group.suffix,
  61. self.radius_group.realm)
  62. def clean(self):
  63. # Generate DSL login, of the form "login-dslX".
  64. if not self.login:
  65. username = self.offersubscription.member.username
  66. dsl_lines = DSLConfiguration.objects.filter(offersubscription__member__username=username)
  67. # This is the list of existing DSL logins for this user.
  68. logins = [dsl.login for dsl in dsl_lines]
  69. # 100 DSL lines ought to be enough for anybody.
  70. for login in ["{}-dsl{}".format(username, k) for k in range(1, 101)]:
  71. if login not in logins:
  72. self.login = login
  73. break
  74. # We may have failed.
  75. if not self.login:
  76. ValidationError("Impossible de générer un login DSL")
  77. # Generate password: 8 lowercase letters. We don't really care
  78. # about security, since 1/ we store it in cleartext 2/ to
  79. # bruteforce it, you must have a DSL line on the same backhaul
  80. # infrastructure. On the other hand, it must be easy to copy it
  81. # by hand to configure a modem.
  82. if not self.password:
  83. self.password = utils.generate_weak_password(8)
  84. # This method is part of the general configuration interface.
  85. def subnet_event(self):
  86. self.sync_to_ldap(False)
  87. def get_subnets(self, version):
  88. subnets = self.ip_subnet.all()
  89. return [subnet for subnet in subnets if subnet.inet.version == version]
  90. def sync_to_ldap(self, creation, *args, **kwargs):
  91. if creation:
  92. config = LdapDSLConfig()
  93. else:
  94. config = LdapDSLConfig.objects.get(pk=self.full_login())
  95. config.login = self.full_login()
  96. config.cleartext_password = self.password
  97. config.password = utils.ldap_hash(self.password)
  98. config.active = 'yes' if self.activated else 'no'
  99. v4_subnets = self.get_subnets(4)
  100. v6_subnets = self.get_subnets(6)
  101. # Instead of having an explicit IPv4 endpoint in the model, we
  102. # simply take the first IPv4 subnet (which we assume is a /32)
  103. if len(v4_subnets) > 0:
  104. config.ipv4_endpoint = str(v4_subnets[0].inet.ip)
  105. else:
  106. config.ipv4_endpoint = None
  107. config.ranges_v4 = [str(s) for s in v4_subnets]
  108. if len(v6_subnets) > 0:
  109. config.ranges_v6 = [str(s) for s in v6_subnets]
  110. else:
  111. # This field is multi-valued, but mandatory... Hack hack hack.
  112. config.ranges_v6 = ["fd20:fd79:5eb6:5cc5::/64"]
  113. config.save()
  114. def delete_from_ldap(self):
  115. LdapDSLConfig.objects.get(pk=self.full_login()).delete()
  116. def __unicode__(self):
  117. return self.full_login()
  118. class Meta:
  119. verbose_name = _("DSL line")
  120. verbose_name_plural = _("DSL lines")
  121. class LdapDSLConfig(ldapdb.models.Model):
  122. base_dn = settings.DSL_CONF_BASE_DN # "ou=radius,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  123. object_classes = [b'top', b'radiusObjectProfile', b'radiusprofile', b'ipHost']
  124. login = CharField(db_column=b'cn', primary_key=True, max_length=255)
  125. password = CharField(db_column=b'userPassword', max_length=255)
  126. cleartext_password = CharField(db_column=b'description', max_length=255)
  127. active = CharField(db_column=b'dialupAccess', max_length=3)
  128. ipv4_endpoint = CharField(db_column=b'radiusFramedIPAddress', max_length=16)
  129. ranges_v4 = ListField(db_column=b'radiusFramedRoute')
  130. # This field is multi-valued, but mandatory...
  131. ranges_v6 = ListField(db_column=b'ipHostNumber')
  132. def __unicode__(self):
  133. return self.login
  134. class Meta:
  135. managed = False # Indique à South de ne pas gérer le model LdapUser