Fields Guide
This guide covers the field types available in django-ldaporm and how to use them.
Field Types Overview
django-ldaporm provides field types that handle the conversion between Python
data types and LDAP attribute formats. Each field type maps to specific LDAP
attribute syntaxes and handles validation and conversion automatically.
Field names and LDAP attribute names
The field name is the python symbol for the field in the Python code. The LDAP attribute name is the name of the attribute in the LDAP server. The field name is used to access the field in the Python code. The LDAP attribute name is used to access read and write the attribute in the LDAP server.
If you want to use a field name that is not the same as the LDAP attribute name
(because for instance your linter hates the non-pythonic names or you want it to
be more clear to you and your fellow developers what exactly this field is), you
can use the db_column parameter to specify the LDAP attribute name. This is
supported by every field type.
class User(Model):
uid = CharField('uid', primary_key=True, max_length=50, db_column='sAMAccountName')
class Meta:
...
Field Arguments
See Field for arguments that all fields take, and
the docs for each field type for additional arguments.
Basic Field Types
CharField
Maps to LDAP string attributes:
from ldaporm.fields import CharField
class User(Model):
uid = CharField('uid', primary_key=True, max_length=50)
cn = CharField('cn', max_length=100)
description = CharField('description', max_length=200, blank=True)
class Meta:
...
# Usage
user = User(uid='john.doe', cn='John Doe')
user.save()
EmailField
Maps to LDAP email attributes with email validation:
from ldaporm import Model
from ldaporm.fields import EmailField
class User(Model):
mail = EmailField('mail', max_length=254)
altMail = EmailField('altMail', max_length=254, blank=True)
class Meta:
...
# Usage
user = User(mail='john.doe@example.com')
user.save()
BooleanField
Maps to LDAP boolean attributes:
from ldaporm import Model
from ldaporm.fields import BooleanField
class User(Model):
is_active = BooleanField('userAccountControl', default=True)
is_locked = BooleanField('lockoutTime', default=False)
class Meta:
...
# Usage
user = User(is_active=True, is_locked=False)
user.save()
IntegerField
Maps to LDAP integer attributes:
from ldaporm import Model
from ldaporm.fields import IntegerField
class User(Model):
uidNumber = IntegerField('uidNumber', null=True)
gidNumber = IntegerField('gidNumber', null=True)
class Meta:
...
# Usage
user = User(uidNumber=1000, gidNumber=100)
user.save()
DateTimeField
Maps to LDAP timestamp attributes:
from ldaporm import Model
from ldaporm.fields import DateTimeField
from django.utils import timezone
class User(Model):
created = DateTimeField('whenCreated', auto_now_add=True)
modified = DateTimeField('whenChanged', auto_now=True)
lastLogin = DateTimeField('lastLogin', null=True)
class Meta:
...
# Usage
user = User(lastLogin=timezone.now())
user.save()
DateField
Maps to LDAP date attributes:
from ldaporm import Model
from ldaporm.fields import DateField
from datetime import date
class User(Model):
birthDate = DateField('birthDate', null=True)
hireDate = DateField('hireDate', null=True)
class Meta:
...
# Usage
user = User(birthDate=date(1990, 1, 1))
user.save()
BinaryField
Maps to LDAP binary attributes:
from ldaporm import Model
from ldaporm.fields import BinaryField
class User(Model):
photo = BinaryField('jpegPhoto', null=True)
certificate = BinaryField('userCertificate', null=True)
class Meta:
...
# Usage
with open('photo.jpg', 'rb') as f:
photo_data = f.read()
user = User(photo=photo_data)
user.save()
Multi-valued Fields
CharListField
Handles LDAP attributes that can have multiple string values:
from ldaporm import Model
from ldaporm.fields import CharListField
class Group(Model):
cn = CharField('cn', primary_key=True, max_length=50)
member = CharListField('member', max_length=100)
memberUid = CharListField('memberUid', max_length=50)
class Meta:
...
# Usage
group = Group(
cn='developers',
member=['cn=john,ou=users,dc=example,dc=com', 'cn=jane,ou=users,dc=example,dc=com'],
memberUid=['john.doe', 'jane.smith']
)
group.save()
# Accessing values
print(group.member) # ['cn=john,ou=users,dc=example,dc=com', 'cn=jane,ou=users,dc=example,dc=com']
print(len(group.member)) # 2
IntegerListField
Handles LDAP attributes that can have multiple integer values:
from ldaporm import Model
from ldaporm.fields import IntegerListField
class Group(Model):
cn = CharField('cn', primary_key=True, max_length=50)
gidNumber = IntegerListField('gidNumber')
class Meta:
...
# Usage
group = Group(cn='admins', gidNumber=[1000, 1001, 1002])
group.save()
Active Directory Fields
ActiveDirectoryTimestampField
Special field for Active Directory timestamp attributes. Unlike normal LDAP servers, which typically use either unix epoch time or a string representation of the time, Active Directory uses a different format. This format, called either Active Directory timestamp, ‘Windows NT time format’, ‘Win32 FILETIME or SYSTEMTIME’ or NTFS file time, is an 18 digit number that represents the number of 100-nanosecond intervals since January 1, 1601 (UTC). This field type will convert the LDAP value to a Python datetime object.
Warning
This field type can store really large dates (thousands of years in the
future) which will cause OverflowError when converting to a
Python datetime.datetime object. This can be especially true
for the accountExpires AD attribute on user objects, which is used to
store the date and time when the account will expire.
If you have this problem in your organization, you might have to simply
store that field as an IntegerField.
from ldaporm import Model
from ldaporm.fields import ActiveDirectoryTimestampField
class ADUser(Model):
sAMAccountName = CharField('sAMAccountName', primary_key=True, max_length=50)
lastLogon = ActiveDirectoryTimestampField('lastLogon', null=True)
pwdLastSet = ActiveDirectoryTimestampField('pwdLastSet', null=True)
accountExpires = ActiveDirectoryTimestampField('accountExpires', null=True)
class Meta:
...
# Usage
user = ADUser(
sAMAccountName='john.doe',
lastLogon=timezone.now(),
pwdLastSet=timezone.now()
)
user.save()
Field Options
Common Field Parameters
All fields support these common parameters:
class User(Model):
# Primary key field
uid = CharField('uid', primary_key=True, max_length=50)
# Required field (default)
cn = CharField('cn', max_length=100)
# Optional field
description = CharField('description', max_length=200, blank=True)
# Nullable field
telephoneNumber = CharField('telephoneNumber', max_length=20, null=True)
# Field with default value
is_active = BooleanField('userAccountControl', default=True)
# Auto-managed fields
created = DateTimeField('whenCreated', auto_now_add=True)
modified = DateTimeField('whenChanged', auto_now=True)
Field Validation
Add custom validators to fields:
from ldaporm import Model
from ldaporm.fields import CharField, EmailField
from django.core.exceptions import ValidationError
def validate_uid_format(value):
if not value.isalnum():
raise ValidationError('UID must be alphanumeric')
if len(value) < 3:
raise ValidationError('UID must be at least 3 characters')
def validate_domain_email(value):
if not value.endswith('@example.com'):
raise ValidationError('Email must be from example.com domain')
class User(Model):
uid = CharField(
'uid',
primary_key=True,
max_length=50,
validators=[validate_uid_format]
)
mail = EmailField(
'mail',
max_length=254,
validators=[validate_domain_email]
)
class Meta:
...
Custom Field Types
Creating Custom Fields
You can create custom field types for special LDAP attributes:
from ldaporm import Model
from ldaporm.fields import Field, CharField, EmailField
from django.core.exceptions import ValidationError
class PhoneNumberField(Field):
"""Custom field for phone number formatting."""
def __init__(self, ldap_attribute, max_length=20, **kwargs):
super().__init__(ldap_attribute, **kwargs)
self.max_length = max_length
def to_python(self, value):
"""Convert LDAP value to Python."""
if value is None:
return None
if isinstance(value, list):
value = value[0] if value else None
if isinstance(value, bytes):
value = value.decode('utf-8')
return value
def to_db_value(self, value):
"""Convert Python value to LDAP format."""
if value is None:
return {}
# Format phone number
import re
cleaned = re.sub(r'[^\d+]', '', str(value))
if not cleaned.startswith('+'):
cleaned = '+1' + cleaned # Add country code
return {self.ldap_attribute: [cleaned.encode('utf-8')]}
def validate(self, value):
"""Validate phone number format."""
if value and not re.match(r'^\+[\d\s\-\(\)]+$', str(value)):
raise ValidationError('Invalid phone number format')
# Usage
class User(Model):
phone = PhoneNumberField('telephoneNumber', max_length=20)
mobile = PhoneNumberField('mobile', max_length=20, blank=True)
class Meta:
...
Field Inheritance
Extend existing field types:
from ldaporm import Model
from ldaporm.fields import EmailField
from django.core.exceptions import ValidationError
class DomainEmailField(EmailField):
"""Email field that only accepts specific domains."""
def __init__(self, ldap_attribute, allowed_domains=None, **kwargs):
super().__init__(ldap_attribute, **kwargs)
self.allowed_domains = allowed_domains or ['example.com']
def validate(self, value):
super().validate(value)
if value:
domain = value.split('@')[-1]
if domain not in self.allowed_domains:
raise ValidationError(
f'Email domain must be one of: {", ".join(self.allowed_domains)}'
)
# Usage
class User(Model):
work_email = DomainEmailField('mail', allowed_domains=['company.com'])
personal_email = DomainEmailField('altMail', allowed_domains=['gmail.com', 'yahoo.com'])
class Meta:
...
Field Conversion Examples
First, it is important to understand that python-ldap will return the
values for every attribute in the format of a list of bytes, whether that attribute is
multi-valued or not.
Thus in the examples below, the ldap_value will properly be represented as a
list of bytes.
LDAP to Python Conversion
Here’s how different LDAP attribute types are converted:
# String attributes
ldap_value = [b'John Doe']
python_value = CharField('cn').to_python(ldap_value) # 'John Doe'
# Boolean attributes
ldap_value = [b'TRUE']
python_value = BooleanField('isActive').to_python(ldap_value) # True
# Integer attributes
ldap_value = [b'1000']
python_value = IntegerField('uidNumber').to_python(ldap_value) # 1000
# Multi-valued attributes
ldap_value = [b'group1', b'group2', b'group3']
python_value = CharListField('memberOf').to_python(ldap_value) # ['group1', 'group2', 'group3']
Python to LDAP Conversion
Here’s how Python values are converted back to LDAP format:
# String to LDAP
python_value = 'John Doe'
ldap_value = CharField('cn').to_db_value(python_value) # {'cn': [b'John Doe']}
# Boolean to LDAP
python_value = True
ldap_value = BooleanField('isActive').to_db_value(python_value) # {'isActive': [b'TRUE']}
# Integer to LDAP
python_value = 1000
ldap_value = IntegerField('uidNumber').to_db_value(python_value) # {'uidNumber': [b'1000']}
# List to LDAP
python_value = ['group1', 'group2', 'group3']
ldap_value = CharListField('memberOf').to_db_value(python_value) # {'memberOf': [b'group1', b'group2', b'group3']}
Best Practices
Field Naming
If you want to use a different name for the python field than for the LDAP attribute, use the db_column parameter. This can be useful if you want your field names to by pythonic.
Use Python-friendly names for the field name
Be consistent with naming conventions across your models
Validation
Add appropriate validators for your use case.
Validate data at the field level when possible using field validators.
Use model-level validation for complex business rules by implementing the
cleanmethod.
Example: Complete Field Usage
Here’s a complete example showing various field types:
from ldaporm import Model
from ldaporm.fields import (
CharField,
EmailField,
BooleanField,
IntegerField,
DateTimeField,
DateField,
BinaryField,
CharListField,
ActiveDirectoryTimestampField,
)
from django.core.exceptions import ValidationError
from django.utils import timezone
from datetime import date
class Employee(Model):
# Identity fields
uid = CharField('uid', primary_key=True, max_length=50)
cn = CharField('cn', max_length=100)
sn = CharField('sn', max_length=100)
givenName = CharField('givenName', max_length=100)
# Contact information
mail = EmailField('mail', max_length=254)
telephoneNumber = CharField('telephoneNumber', max_length=20, blank=True)
mobile = CharField('mobile', max_length=20, blank=True)
# Organizational information
title = CharField('title', max_length=100, blank=True)
department = CharField('department', max_length=100, blank=True)
employeeNumber = IntegerField('employeeNumber', null=True)
# Status and dates
is_active = BooleanField('userAccountControl', default=True)
hireDate = DateField('hireDate', null=True)
birthDate = DateField('birthDate', null=True)
created = DateTimeField('whenCreated', auto_now_add=True)
modified = DateTimeField('whenChanged', auto_now=True)
# Active Directory specific
lastLogon = ActiveDirectoryTimestampField('lastLogon', null=True)
pwdLastSet = ActiveDirectoryTimestampField('pwdLastSet', null=True)
# Multi-valued attributes
memberOf = CharListField('memberOf', max_length=100)
skills = CharListField('skills', max_length=50, blank=True)
# Binary data
photo = BinaryField('jpegPhoto', null=True)
class Meta:
ldap_server = 'default'
basedn = 'ou=employees,dc=example,dc=com'
objectclass = 'person'
def clean(self):
"""Model-level validation."""
if self.hireDate and self.birthDate:
if self.hireDate < self.birthDate:
raise ValidationError('Hire date cannot be before birth date')
def get_full_name(self):
"""Return the employee's full name."""
return f"{self.givenName} {self.sn}"
def get_years_of_service(self):
"""Calculate years of service."""
if self.hireDate:
return (date.today() - self.hireDate).days // 365
return 0
def __str__(self):
return self.get_full_name()