models.py 6.52 KB
Newer Older
1
from django.contrib.auth.models import AbstractUser
2
from django.contrib.auth.models import UserManager as BaseUserManager
3 4 5 6 7 8 9 10 11 12 13
from ldapdb.models.fields import CharField, ListField
import ldap
import ldapdb.models
import logging
import pyasn1.type.univ
import pyasn1.type.namedtype
import pyasn1.codec.ber.encoder
from django.db import connections, router
from django.utils.crypto import salted_hmac

log = logging.getLogger(__name__)
14

15

16
class UserManager(BaseUserManager):
17 18 19 20 21 22 23 24 25 26

    def create_user(self, username, email=None, password=None, **extra_fields):
        """Create regular users in LDAP, and with no Django password."""

        super(UserManager, self).create_user(username, None, None, **extra_fields)

    def create_superuser(self, username, email=None, password=None, **extra_fields):
        """Create superusers with a Django password."""

        super(UserManager, self).create_superuser(username, None, None, **extra_fields)
27 28 29


class User(AbstractUser):
30

31
    objects = UserManager()
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
    REQUIRED_FIELDS = ['email']

    def __init__(self, *args, **kwargs):

        return super(User, self).__init__(*args, **kwargs)

    def validate_unique(self, exclude=None):

        return super(User, self).validate_unique(exclude)

    def get_ldap(self):
        return LdapPerson.objects.get(uid=self.get_username())

    def has_ldap(self):
        result = LdapPerson.objects.filter(uid=self.get_username())
        return len(result) == 1

    def create_ldap(self):
        username = self.get_username()
        LdapPerson.objects.create(uid=username, cn=username, sn=username)

    def set_ldap_password(self, raw_password):
        ldap_person = self.get_ldap()
        ldap_person.change_password(raw_password)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):

        # save django user
        super(User, self).save(force_insert, force_update, using, update_fields)

        # create LDAP user (if required)
        if not self.has_ldap():
            self.create_ldap()

        # force null password (will use LDAP password)
        self.set_unusable_password()

    def set_password(self, raw_password):

        # force null password (will use LDAP password)
        self.set_unusable_password()

        # create LDAP user (if required)
        if self.get_username():

            if not self.has_ldap():
                self.create_ldap()

            # set LDAP password
            self.set_ldap_password(raw_password)

    def get_session_auth_hash(self):
        """
        Return an HMAC of the password field.
        """
        key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
        return salted_hmac(key_salt, self.get_username()).hexdigest()  #FIXME: should use LDAP password value!


class LdapGroup(ldapdb.models.Model):
    """
    Class for representing an LDAP group entry.
    """

    class Meta:
        verbose_name = "LDAP group"
        verbose_name_plural = "LDAP groups"

    # LDAP meta-data
    base_dn = "dc=comms,dc=nodomain"
    object_classes = ['groupOfNames', ]

    # LDAP group attributes
    cn = CharField(db_column='cn', max_length=200, primary_key=True)
    description = CharField(db_column='description', max_length=200)
    members = ListField(db_column='member')

    def __str__(self):
        return self.name

    def __unicode__(self):
        return self.name


class LdapPerson(ldapdb.models.Model):
    """
    Class for representing an LDAP person entry.
    """

    class Meta:
        verbose_name = "LDAP person"
        verbose_name_plural = "LDAP people"

    # LDAP meta-data
    base_dn = "ou=people,dc=comms,dc=nodomain"
    object_classes = ['inetOrgPerson', 'organizationalPerson', 'person', ]

    # Minimal attributes
    uid = CharField(db_column='uid', max_length=200, primary_key=True)
    cn = CharField(db_column='cn', max_length=200)
    description = CharField(db_column='description', max_length=200)
    sn = CharField(db_column='sn', max_length=200)

    def __str__(self):
        return self.uid

    def __unicode__(self):
        return self.uid

    def change_password(self, raw_password, using=None):

        # dig into the ldapdb primitives
        using = using or router.db_for_write(self.__class__, instance=self)
        connection = connections[using]
        cursor = connection._cursor()

        # call pyldap_orm password modification
        cursor.connection.extop_s(PasswordModify(self.dn, raw_password))


# The following code taken from https://github.com/asyd/pyldap_orm/blob/master/pyldap_orm/controls.py
# Copyright 2016 Bruno Bonfils
# SPDX-License-Identifier: Apache-2.0 (no NOTICE file)


class PasswordModify(ldap.extop.ExtendedRequest):
    """
    Implements RFC 3062, LDAP Password Modify Extended Operation
    Reference: https://www.ietf.org/rfc/rfc3062.txt
    """

    def __init__(self, identity, new, current=None):
        self.requestName = '1.3.6.1.4.1.4203.1.11.1'
        self.identity = identity
        self.new = new
        self.current = current

    def encodedRequestValue(self):
        request = self.PasswdModifyRequestValue()
        request.setComponentByName('userIdentity', self.identity)
        if self.current is not None:
            request.setComponentByName('oldPasswd', self.current)
        request.setComponentByName('newPasswd', self.new)
        return pyasn1.codec.ber.encoder.encode(request)

    class PasswdModifyRequestValue(pyasn1.type.univ.Sequence):
        """
        PyASN1 representation of:
            PasswdModifyRequestValue ::= SEQUENCE {
            userIdentity    [0]  OCTET STRING OPTIONAL
            oldPasswd       [1]  OCTET STRING OPTIONAL
            newPasswd       [2]  OCTET STRING OPTIONAL }
        """
        componentType = pyasn1.type.namedtype.NamedTypes(
            pyasn1.type.namedtype.OptionalNamedType(
                'userIdentity',
                pyasn1.type.univ.OctetString().subtype(
                    implicitTag=pyasn1.type.tag.Tag(pyasn1.type.tag.tagClassContext, pyasn1.type.tag.tagFormatSimple, 0)
                )),
            pyasn1.type.namedtype.OptionalNamedType(
                'oldPasswd',
                pyasn1.type.univ.OctetString().subtype(
                    implicitTag=pyasn1.type.tag.Tag(pyasn1.type.tag.tagClassContext, pyasn1.type.tag.tagFormatSimple, 1)
                )),
            pyasn1.type.namedtype.OptionalNamedType(
                'newPasswd',
                pyasn1.type.univ.OctetString().subtype(
                    implicitTag=pyasn1.type.tag.Tag(pyasn1.type.tag.tagClassContext, pyasn1.type.tag.tagFormatSimple, 2)
                )),
)