models.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. # -*- coding: utf-8 -*-
  2. from django.db import models
  3. from django.core.exceptions import ValidationError
  4. from netfields import InetAddressField, NetManager
  5. import ldapdb.models
  6. from ldapdb.models.fields import CharField, ListField
  7. from coin.models import CoinLdapSyncModel
  8. from coin.offers.models import OfferSubscription
  9. from coin import utils
  10. def validate_v4(address):
  11. if address.version != 4:
  12. raise ValidationError('{} is not an IPv4 address'.format(address))
  13. def validate_v6(address):
  14. if address.version != 6:
  15. raise ValidationError('{} is not an IPv6 address'.format(address))
  16. def str_or_none(obj):
  17. return str(obj) if obj else None
  18. def validate_backend_type(subscription):
  19. """Ensures that the subscription we link to has the right backend
  20. type. This is a bit ugly, as validators are not meant for
  21. database-level sanity checks, but this is the cost of doing genericity
  22. the way we do.
  23. Note that this validator should not be needed for most cases, as the
  24. "limit_choices_to" parameter of the "administrative_subscription"
  25. OneToOneField automatically limits the available choices on forms.
  26. But it does not protect us if we fiddle manually with the database:
  27. better safe than sorry.
  28. """
  29. if OfferSubscription.objects.get(pk=subscription).offer.backend != 'openvpn_ldap':
  30. raise ValidationError('Administrative subscription must have a "openvpn_ldap" backend.')
  31. class VPNSubscription(CoinLdapSyncModel):
  32. administrative_subscription = models.OneToOneField(
  33. 'offers.OfferSubscription',
  34. related_name='openvpn_ldap',
  35. # Only consider VPN subscriptions.
  36. # TODO: also filter out subscriptions that already have a OneToOne
  37. # relation. Simply adding "'openvpn_ldap': None" to the
  38. # dictionary does not work well, as it will also remove ourselves
  39. # from the list when editing the object in the admin. Using a Q()
  40. # query does not solve the problem, as there is no way to
  41. # reference ourselves. The proper solution would be to create a
  42. # custom form.
  43. limit_choices_to={'offer__backend': 'openvpn_ldap'},
  44. validators=[validate_backend_type])
  45. activated = models.BooleanField(default=False)
  46. login = models.CharField(max_length=50, unique=True)
  47. password = models.CharField(max_length=256)
  48. ipv4_endpoint = InetAddressField(validators=[validate_v4], blank=True,
  49. null=True)
  50. ipv6_endpoint = InetAddressField(validators=[validate_v6], blank=True,
  51. null=True)
  52. comment = models.CharField(blank=True, max_length=512)
  53. objects = NetManager()
  54. def get_subnets(self, version):
  55. subnets = self.administrative_subscription.ip_subnet.all()
  56. return [subnet for subnet in subnets if subnet.inet.version == version]
  57. def sync_to_ldap(self, creation):
  58. if creation:
  59. config = LdapVPNConfig()
  60. else:
  61. config = LdapVPNConfig.objects.get(pk=self.login)
  62. config.login = config.sn = self.login
  63. config.password = self.password
  64. config.active = 'yes' if self.activated else 'no'
  65. config.ipv4_endpoint = str_or_none(self.ipv4_endpoint)
  66. config.ipv6_endpoint = str_or_none(self.ipv6_endpoint)
  67. config.ranges_v4 = [str(s) for s in self.get_subnets(4)]
  68. config.ranges_v6 = [str(s) for s in self.get_subnets(6)]
  69. config.save()
  70. def delete_from_ldap(self):
  71. LdapVPNConfig.objects.get(pk=self.login).delete()
  72. def generate_endpoints(self, v4=True, v6=True):
  73. """Generate IP endpoints in one of the attributed IP subnets. If there is
  74. no available subnet for a given address family, then no endpoint
  75. is generated for this address family. If there already is an
  76. endpoint, do nothing.
  77. Returns True if an endpoint was generated.
  78. TODO: this should be factored for other technologies (DSL, etc)
  79. """
  80. subnets = self.administrative_subscription.ip_subnet.all()
  81. updated = False
  82. if v4 and self.ipv4_endpoint is None:
  83. subnets_v4 = [s for s in subnets if s.inet.version == 4]
  84. if len(subnets_v4) > 0:
  85. self.ipv4_endpoint = subnets_v4[0].inet.ip
  86. updated = True
  87. if v6 and self.ipv6_endpoint is None:
  88. subnets_v6 = [s for s in subnets if s.inet.version == 6]
  89. if len(subnets_v6) > 0:
  90. # With v6, we choose the second host of the subnet (cafe::1)
  91. gen = subnets_v6[0].inet.iter_hosts()
  92. gen.next()
  93. self.ipv6_endpoint = gen.next()
  94. updated = True
  95. return updated
  96. def check_endpoints(self):
  97. """Check that the IP endpoints are included in one of the attributed IP
  98. subnets.
  99. """
  100. subnets = self.administrative_subscription.ip_subnet.all()
  101. for endpoint in [self.ipv4_endpoint, self.ipv6_endpoint]:
  102. if endpoint:
  103. if not any([endpoint in subnet.inet for subnet in subnets]):
  104. raise ValidationError("Endpoint {} is not in an attributed range".format(endpoint))
  105. def clean(self):
  106. # Hash password if needed
  107. self.password = utils.ldap_hash(self.password)
  108. # If saving for the first time and IP endpoints are not specified,
  109. # generate them automatically.
  110. if self.pk is None:
  111. self.generate_endpoints()
  112. self.check_endpoints()
  113. def __unicode__(self):
  114. return 'VPN ' + self.login
  115. class LdapVPNConfig(ldapdb.models.Model):
  116. # TODO: déplacer ligne suivante dans settings.py
  117. base_dn = "ou=vpn,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
  118. object_classes = ['person', 'organizationalPerson', 'inetOrgPerson',
  119. 'top', 'radiusprofile']
  120. login = CharField(db_column='cn', primary_key=True, max_length=255)
  121. sn = CharField(db_column='sn', max_length=255)
  122. password = CharField(db_column='userPassword', max_length=255)
  123. active = CharField(db_column='dialupAccess', max_length=3)
  124. ipv4_endpoint = CharField(db_column='radiusFramedIPAddress', max_length=16)
  125. ipv6_endpoint = CharField(db_column='postalAddress', max_length=40)
  126. ranges_v4 = ListField(db_column='radiusFramedRoute')
  127. ranges_v6 = ListField(db_column='registeredAddress')
  128. def __unicode__(self):
  129. return self.login
  130. class Meta:
  131. managed = False # Indique à South de ne pas gérer le model LdapUser