add working oauth and ldap client

This commit is contained in:
Jens Langhammer
2018-11-11 13:41:48 +01:00
parent 935155ce94
commit 5aa245cac0
212 changed files with 198506 additions and 0 deletions

View File

@ -0,0 +1,3 @@
"""Passbook ldap app Header"""
__version__ = '0.0.1-alpha'
default_app_config = 'passbook.ldap.apps.PassbookLdapConfig'

5
passbook/ldap/admin.py Normal file
View File

@ -0,0 +1,5 @@
"""Passbook LDAP Admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_ldap')

11
passbook/ldap/apps.py Normal file
View File

@ -0,0 +1,11 @@
"""Passbook ldap app config"""
from django.apps import AppConfig
class PassbookLdapConfig(AppConfig):
"""Passbook ldap app config"""
name = 'passbook.ldap'
label = 'passbook_ldap'
verbose_name = 'Passbook LDAP'

21
passbook/ldap/auth.py Normal file
View File

@ -0,0 +1,21 @@
"""passbook LDAP Authentication Backend"""
from logging import getLogger
from django.contrib.auth.backends import ModelBackend
from passbook.ldap.ldap_connector import LDAPConnector
LOGGER = getLogger(__name__)
class LDAPBackend(ModelBackend):
"""Authenticate users against LDAP Server"""
def authenticate(self, **kwargs):
"""Try to authenticate a user via ldap"""
if 'password' not in kwargs:
return None
if not LDAPConnector.enabled:
return None
_ldap = LDAPConnector()
return _ldap.auth_user(**kwargs)

63
passbook/ldap/forms.py Normal file
View File

@ -0,0 +1,63 @@
# """Supervisr Mod LDAP Forms"""
# from django import forms
# from django.utils.translation import ugettext_lazy as _
# from supervisr.core.forms.settings import SettingsForm
# class GeneralSettingsForm(SettingsForm):
# """general settings form"""
# MODE_AUTHENTICATION_BACKEND = 'auth_backend'
# MODE_CREATE_USERS = 'create_users'
# MODE_CHOICES = (
# (MODE_AUTHENTICATION_BACKEND, _('Authentication Backend')),
# (MODE_CREATE_USERS, _('Create Users'))
# )
# namespace = 'supervisr.mod.auth.ldap'
# settings = ['enabled', 'mode']
# widgets = {
# 'enabled': forms.BooleanField(required=False),
# 'mode': forms.ChoiceField(widget=forms.RadioSelect, choices=MODE_CHOICES),
# }
# class ConnectionSettings(SettingsForm):
# """Connection settings form"""
# namespace = 'supervisr.mod.auth.ldap'
# settings = ['server', 'server:tls', 'bind:user', 'bind:password', 'domain']
# attrs_map = {
# 'server': {'placeholder': 'dc1.corp.exmaple.com'},
# 'bind:user': {'placeholder': 'Administrator'},
# 'domain': {'placeholder': 'corp.example.com'},
# }
# widgets = {
# 'server:tls': forms.BooleanField(required=False, label=_('Server TLS')),
# }
# class AuthenticationBackendSettings(SettingsForm):
# """Authentication backend settings"""
# namespace = 'supervisr.mod.auth.ldap'
# settings = ['base']
# attrs_map = {
# 'base': {'placeholder': 'DN in which to search for users'},
# }
# class CreateUsersSettings(SettingsForm):
# """Create users settings"""
# namespace = 'supervisr.mod.auth.ldap'
# settings = ['create_base']
# attrs_map = {
# 'create_base': {'placeholder': 'DN in which to create users'},
# }

View File

@ -0,0 +1,308 @@
"""Wrapper for ldap3 to easily manage user"""
import os
import sys
from logging import getLogger
from time import time
import ldap3
import ldap3.core.exceptions
from passbook.core.models import User
from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__)
USERNAME_FIELD = CONFIG.y('ldap.username_field', 'sAMAccountName')
LOGIN_FIELD = CONFIG.y('ldap.login_field', 'userPrincipalName')
class LDAPConnector:
"""Wrapper for ldap3 to easily manage user"""
con = None
domain = None
base_dn = None
mock = False
create_users_enabled = False
def __init__(self, mock=False, con_args=None, server_args=None):
super().__init__()
self.create_users_enabled = CONFIG.y('ldap.create_users')
if not LDAPConnector.enabled:
LOGGER.debug("LDAP not Enabled")
if not con_args:
con_args = {}
if not server_args:
server_args = {}
# Either use mock argument or test is in argv
self.domain = CONFIG.y('ldap.domain')
self.base_dn = CONFIG.y('ldap.base_dn')
if mock or any('test' in arg for arg in sys.argv):
self.mock = True
self.create_users_enabled = True
con_args['client_strategy'] = ldap3.MOCK_SYNC
server_args['get_info'] = ldap3.OFFLINE_AD_2012_R2
self.server = ldap3.Server(CONFIG.y('ldap.server.name'), **server_args)
self.con = ldap3.Connection(self.server, raise_exceptions=True,
user=CONFIG.y('ldap.bind.username'),
password=CONFIG.y('ldap.bind.password'), **con_args)
if self.mock:
json_path = os.path.join(os.path.dirname(__file__), 'tests', 'ldap_mock.json')
self.con.strategy.entries_from_json(json_path)
self.con.bind()
if CONFIG.y('ldap.server.use_tls'):
self.con.start_tls()
# @staticmethod
# def cleanup_mock():
# """Cleanup mock files which are not this PID's"""
# pid = os.getpid()
# json_path = os.path.join(os.path.dirname(__file__), 'test', 'ldap_mock_%d.json' % pid)
# os.unlink(json_path)
# LOGGER.debug("Cleaned up LDAP Mock from PID %d", pid)
# def apply_db(self):
# """Check if any unapplied LDAPModification's are left"""
# to_apply = LDAPModification.objects.filter(_purgeable=False)
# for obj in to_apply:
# try:
# if obj.action == LDAPModification.ACTION_ADD:
# self.con.add(obj.dn, obj.data)
# elif obj.action == LDAPModification.ACTION_MODIFY:
# self.con.modify(obj.dn, obj.data)
# # Object has been successfully applied to LDAP
# obj.delete()
# except ldap3.core.exceptions.LDAPException as exc:
# LOGGER.error(exc)
# LOGGER.debug("Recovered %d Modifications from DB.", len(to_apply))
# @staticmethod
# def handle_ldap_error(object_dn, action, data):
# """Custom Handler for LDAP methods to write LDIF to DB"""
# LDAPModification.objects.create(
# dn=object_dn,
# action=action,
# data=data)
@property
def enabled(self):
"""Returns whether LDAP is enabled or not"""
return CONFIG.y('ldap.enabled')
@staticmethod
def encode_pass(password):
"""Encodes a plain-text password so it can be used by AD"""
return '"{}"'.format(password).encode('utf-16-le')
def lookup(self, generate_only=False, **fields):
"""Search email in LDAP and return the DN.
Returns False if nothing was found."""
filters = []
for item, value in fields.items():
filters.append("(%s=%s)" % (item, value))
ldap_filter = "(&%s)" % "".join(filters)
LOGGER.debug("Constructed filter: '%s'", ldap_filter)
if generate_only:
return ldap_filter
try:
self.con.search(self.base_dn, ldap_filter)
results = self.con.response
if len(results) >= 1:
if 'dn' in results[0]:
return str(results[0]['dn'])
except ldap3.core.exceptions.LDAPNoSuchObjectResult as exc:
LOGGER.warning(exc)
return False
except ldap3.core.exceptions.LDAPInvalidDnError as exc:
LOGGER.warning(exc)
return False
return False
def _get_or_create_user(self, user_data):
"""Returns a Django user for the given LDAP user data.
If the user does not exist, then it will be created."""
attributes = user_data.get("attributes")
if attributes is None:
LOGGER.warning("LDAP user attributes empty")
return None
# Create the user data.
field_map = {
'username': '%(' + USERNAME_FIELD + ')s',
'first_name': '%(givenName)s %(sn)s',
'email': '%(mail)s',
}
user_fields = {}
for dj_field, ldap_field in field_map.items():
user_fields[dj_field] = ldap_field % attributes
# Update or create the user.
user, created = User.objects.update_or_create(
defaults=user_fields,
username=user_fields.pop('username', "")
)
# Update groups
# if 'memberOf' in attributes:
# applicable_groups = LDAPGroupMapping.objects.f
# ilter(ldap_dn__in=attributes['memberOf'])
# for group in applicable_groups:
# if group.group not in user.groups.all():
# user.groups.add(group.group)
# user.save()
# If the user was created, set them an unusable password.
if created:
user.set_unusable_password()
user.save()
# All done!
LOGGER.debug("LDAP user lookup succeeded")
return user
def auth_user(self, password, **filters):
"""Try to bind as either user_dn or mail with password.
Returns True on success, otherwise False"""
if not LDAPConnector.enabled:
return None
filters.pop('request')
# FIXME: Adapt user_uid
# email = filters.pop(CONFIG.get('passport').get('ldap').get, '')
email = filters.pop('email')
user_dn = self.lookup(**{LOGIN_FIELD: email})
if not user_dn:
return None
# Try to bind as new user
LOGGER.debug("Binding as '%s'", user_dn)
try:
t_con = ldap3.Connection(self.server, user=user_dn,
password=password, raise_exceptions=True)
t_con.bind()
if self.con.search(
search_base=self.base_dn,
search_filter=self.lookup(generate_only=True, **{LOGIN_FIELD: email}),
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
get_operational_attributes=True,
size_limit=1,
):
response = self.con.response[0]
# If user has no email set in AD, use UPN
if 'mail' not in response.get('attributes'):
response['attributes']['mail'] = response['attributes']['userPrincipalName']
return self._get_or_create_user(response)
LOGGER.warning("LDAP user lookup failed")
return None
except ldap3.core.exceptions.LDAPInvalidCredentialsResult as exception:
LOGGER.debug("User '%s' failed to login (Wrong credentials)", user_dn)
except ldap3.core.exceptions.LDAPException as exception:
LOGGER.warning(exception)
return None
def is_email_used(self, mail):
"""Checks whether an email address is already registered in LDAP"""
if self.create_users_enabled:
return self.lookup(mail=mail)
return False
def create_ldap_user(self, user, raw_password):
"""Creates a new LDAP User from a django user and raw_password.
Returns True on success, otherwise False"""
if not self.create_users_enabled:
LOGGER.debug("User creation not enabled")
return False
# The dn of our new entry/object
username = user.pk.hex # UUID without dashes
# sAMAccountName is limited to 20 chars
# https://msdn.microsoft.com/en-us/library/ms679635.aspx
username_trunk = username[:20] if len(username) > 20 else username
# AD doesn't like sAMAccountName's with . at the end
username_trunk = username_trunk[:-1] if username_trunk[-1] == '.' else username_trunk
user_dn = 'cn=' + username + ',' + self.base_dn
LOGGER.debug('New DN: %s', user_dn)
attrs = {
'distinguishedName': str(user_dn),
'cn': str(username),
'description': str('t=' + time()),
'sAMAccountName': str(username_trunk),
'givenName': str(user.first_name),
'displayName': str(user.username),
'name': str(user.first_name),
'mail': str(user.email),
'userPrincipalName': str(username + '@' + self.domain),
'objectClass': ['top', 'person', 'organizationalPerson', 'user'],
}
try:
self.con.add(user_dn, attributes=attrs)
except ldap3.core.exceptions.LDAPException as exception:
LOGGER.warning("Failed to create user ('%s'), saved to DB", exception)
# LDAPConnector.handle_ldap_error(user_dn, LDAPModification.ACTION_ADD, attrs)
LOGGER.debug("Signed up user %s", user.email)
return self.change_password(raw_password, mail=user.email)
def _do_modify(self, diff, **fields):
"""Do the LDAP modification itself"""
user_dn = self.lookup(**fields)
try:
self.con.modify(user_dn, diff)
except ldap3.core.exceptions.LDAPException as exception:
LOGGER.warning("Failed to modify %s ('%s'), saved to DB", user_dn, exception)
# LDAPConnector.handle_ldap_error(user_dn, LDAPModification.ACTION_MODIFY, diff)
LOGGER.debug("modified account '%s' [%s]", user_dn, ','.join(diff.keys()))
return 'result' in self.con.result and self.con.result['result'] == 0
def disable_user(self, **fields):
"""
Disables LDAP user based on mail or user_dn.
Returns True on success, otherwise False
"""
diff = {
'userAccountControl': [(ldap3.MODIFY_REPLACE, [str(66050)])],
}
return self._do_modify(diff, **fields)
def enable_user(self, **fields):
"""
Enables LDAP user based on mail or user_dn.
Returns True on success, otherwise False
"""
diff = {
'userAccountControl': [(ldap3.MODIFY_REPLACE, [str(66048)])],
}
return self._do_modify(diff, **fields)
def change_password(self, new_password, **fields):
"""
Changes LDAP user's password based on mail or user_dn.
Returns True on success, otherwise False
"""
diff = {
'unicodePwd': [(ldap3.MODIFY_REPLACE, [LDAPConnector.encode_pass(new_password)])],
}
return self._do_modify(diff, **fields)
def add_to_group(self, group_dn, **fields):
"""
Adds mail or user_dn to group_dn
Returns True on success, otherwise False
"""
user_dn = self.lookup(**fields)
diff = {
'member': [(ldap3.MODIFY_ADD), [user_dn]]
}
return self._do_modify(diff, user_dn=group_dn)
def remove_from_group(self, group_dn, **fields):
"""
Removes mail or user_dn from group_dn
Returns True on success, otherwise False
"""
user_dn = self.lookup(**fields)
diff = {
'member': [(ldap3.MODIFY_DELETE), [user_dn]]
}
return self._do_modify(diff, user_dn=group_dn)

View File

36
passbook/ldap/models.py Normal file
View File

@ -0,0 +1,36 @@
# """Supervisr mod_ldap Models"""
# from django.contrib.auth.models import Group
# from django.db import models
# from supervisr.core.fields import JSONField
# from passbook.core.models import (CreatedUpdatedModel, ProductExtension,
# UUIDModel)
# class LDAPModification(UUIDModel, CreatedUpdatedModel):
# """Store LDAP Data in DB if LDAP Server is unavailable"""
# ACTION_ADD = 'ADD'
# ACTION_MODIFY = 'MODIFY'
# ACTIONS = (
# (ACTION_ADD, 'ADD'),
# (ACTION_MODIFY, 'MODIFY'),
# )
# dn = models.CharField(max_length=255)
# action = models.CharField(max_length=17, choices=ACTIONS, default=ACTION_MODIFY)
# data = JSONField()
# def __str__(self):
# return "LDAPModification %d from %s" % (self.pk, self.created)
# class LDAPGroupMapping(UUIDModel, CreatedUpdatedModel):
# """Model to map an LDAP Group to a supervisr group"""
# ldap_dn = models.TextField()
# group = models.ForeignKey(Group, on_delete=models.CASCADE)
# def __str__(self):
# return "LDAPGroupMapping %s -> %s" % (self.ldap_dn, self.group.name)

View File

@ -0,0 +1 @@
ldap3

View File

@ -0,0 +1,7 @@
"""
LDAP Settings
"""
AUTHENTICATION_BACKENDS = [
'supervisr.mod.auth.ldap.auth.LDAPBackend',
]

View File

@ -0,0 +1,33 @@
{% extends "_admin/module_default.html" %}
{% load i18n %}
{% load supervisr_utils %}
{% block title %}
{% title "Settings" %}
{% endblock %}
{% block module_content %}
<h2><clr-icon shape="application" size="32"></clr-icon>{% trans 'LDAP connection' %}</h2>
<div class="row">
<div class="col-md-12">
<div class="card">
<form role="form" method="POST">
<div class="card-block">
<h3><clr-icon shape="cog" size="32"></clr-icon>{% trans 'General settings' %}</h3>
{% include 'blocks/form.html' with form=general %}
<h3><clr-icon shape="connect" size="32"></clr-icon>{% trans 'Connection settings' %}</h3>
{% include 'blocks/form.html' with form=connection %}
<h3><clr-icon shape="certificate" size="32"></clr-icon>{% trans 'Authentication backend ' %}</h3>
{% include 'blocks/form.html' with form=authentication %}
<h3><clr-icon shape="users" size="32"></clr-icon>{% trans 'Create users settings' %}</h3>
{% include 'blocks/form.html' with form=create_users %}
</div>
<div class="card-footer">
<button type="submit" value="general" class="btn btn-sm btn-primary">{% trans 'Update' %}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

View File

@ -0,0 +1,200 @@
{
"entries": [
{
"attributes": {
"dSCorePropagationData": [
"1601-01-01 00:00:00+00:00"
],
"distinguishedName": "OU=customers,DC=mock,DC=beryju,DC=org",
"instanceType": 4,
"name": "customers_dev",
"objectCategory": "CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=mock,DC=beryju,DC=org",
"objectClass": [
"top",
"organizationalUnit"
],
"objectGUID": "976832bb-f359-4ebc-b7c4-cb6c2ac171cb",
"ou": [
"customers_dev"
],
"uSNChanged": 139575,
"uSNCreated": 139575,
"whenChanged": "2016-12-26 17:08:44+00:00",
"whenCreated": "2016-12-26 17:08:20+00:00"
},
"dn": "OU=customers,DC=mock,DC=beryju,DC=org",
"raw": {
"dSCorePropagationData": [
"16010101000000.0Z"
],
"distinguishedName": [
"OU=customers,DC=mock,DC=beryju,DC=org"
],
"instanceType": [
"4"
],
"name": [
"customers_dev"
],
"objectCategory": [
"CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=mock,DC=beryju,DC=org"
],
"objectClass": [
"top",
"organizationalUnit"
],
"objectGUID": [
{
"encoded": "uzJol1nzvE63xMtsKsFxyw==",
"encoding": "base64"
}
],
"ou": [
"customers_dev"
],
"uSNChanged": [
"139575"
],
"uSNCreated": [
"139575"
],
"whenChanged": [
"20161226170844.0Z"
],
"whenCreated": [
"20161226170820.0Z"
]
}
},
{
"attributes": {
"accountExpires": "9999-12-31 23:59:59.999999",
"cn": "mockadm",
"codePage": 0,
"countryCode": 0,
"dSCorePropagationData": [
"1601-01-01 00:00:00+00:00"
],
"description": [
"t=1484309644.2392948"
],
"displayName": "mockadm",
"distinguishedName": "CN=mockadm,OU=customers,DC=mock,DC=beryju,DC=org",
"givenName": "admin@admin.admin",
"instanceType": 4,
"mail": "mockadm@mock.beryju.org",
"name": "mockadm",
"objectCategory": "CN=Person,CN=Schema,CN=Configuration,DC=mock,DC=beryju,DC=org",
"objectClass": [
"top",
"person",
"organizationalPerson",
"user"
],
"objectGUID": "d28cd23f-a3bc-40a3-93e4-f47b344197c1",
"objectSid": "S-1-5-21-3376105463-1408393234-2945003003-2175",
"primaryGroupID": 513,
"pwdLastSet": "2017-01-13 12:14:04.251018+00:00",
"sAMAccountName": "mockadm",
"sAMAccountType": 805306368,
"uSNChanged": 179076,
"uSNCreated": 179076,
"userAccountControl": 66050,
"userPrincipalName": "mockadm@mock.beryju.org",
"whenChanged": "2017-01-13 12:27:52+00:00",
"whenCreated": "2017-01-13 12:14:04+00:00",
"userPassword": "b3ryju0rg!"
},
"dn": "CN=mockadm,OU=customers,DC=mock,DC=beryju,DC=org",
"raw": {
"accountExpires": [
"9223372036854775807"
],
"cn": [
"mockadm"
],
"codePage": [
"0"
],
"countryCode": [
"0"
],
"dSCorePropagationData": [
"16010101000000.0Z"
],
"description": [
"t=1484309644.2392948"
],
"displayName": [
"mockadm"
],
"distinguishedName": [
"CN=mockadm,OU=customers,DC=mock,DC=beryju,DC=org"
],
"givenName": [
"admin@admin.admin"
],
"instanceType": [
"4"
],
"mail": [
"admin@admin.admin"
],
"name": [
"mockadm"
],
"objectCategory": [
"CN=Person,CN=Schema,CN=Configuration,DC=mock,DC=beryju,DC=org"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"user"
],
"objectGUID": [
{
"encoded": "P9KM0ryjo0CT5PR7NEGXwQ==",
"encoding": "base64"
}
],
"objectSid": [
{
"encoded": "AQUAAAAAAAUVAAAA90c7yRJg8lP7LYmvfwgAAA==",
"encoding": "base64"
}
],
"primaryGroupID": [
"513"
],
"sAMAccountName": [
"mockadm"
],
"sAMAccountType": [
"805306368"
],
"uSNChanged": [
"179076"
],
"uSNCreated": [
"179076"
],
"userAccountControl": [
"66050"
],
"userPrincipalName": [
"mockadm@mock.beryju.org"
],
"whenChanged": [
"20170113122752.0Z"
],
"whenCreated": [
"20170113121404.0Z"
],
"userPassword": [
"b3ryju0rg!"
]
}
}
]
}

View File

@ -0,0 +1,53 @@
"""passbook ldap settings"""
import os
from django.test import TestCase
from passbook.core.models import User
# from supervisr.mod.auth.ldap.forms import GeneralSettingsForm
from passbook.ldap.ldap_connector import LDAPConnector
class TestAccountLDAP(TestCase):
"""passbook ldap settings"""
def setUp(self):
os.environ['RECAPTCHA_TESTING'] = 'True'
# FIXME: Loading mock settings from different config file
# Setting.set('domain', 'mock.beryju.org')
# Setting.set('base', 'OU=customers,DC=mock,DC=beryju,DC=org')
# Setting.set('server', 'dc1.mock.beryju.org')
# Setting.set('server:tls', False)
# Setting.set('mode', GeneralSettingsForm.MODE_CREATE_USERS)
# Setting.set('bind:user', 'CN=mockadm,OU=customers,DC=mock,DC=beryju,DC=org')
# Setting.set('bind:password', 'b3ryju0rg!')
self.ldap = LDAPConnector(mock=True)
self.password = 'b3ryju0rg!'
self.user = User.objects.create_user(
username='test@test.test',
email='test@test.test',
first_name='Test user')
self.user.save()
self.user.is_active = False
self.user.set_password(self.password)
self.user.save()
self.assertTrue(self.ldap.create_ldap_user(self.user, self.password))
def test_change_password(self):
"""Test ldap change_password"""
self.assertTrue(self.ldap.change_password('b4ryju1rg!', mail=self.user.email))
self.assertTrue(self.ldap.change_password('b3ryju0rg!', mail=self.user.email))
def test_disable_enable(self):
"""Test ldap enable and disable"""
self.assertTrue(self.ldap.disable_user(mail=self.user.email))
self.assertTrue(self.ldap.enable_user(mail=self.user.email))
def test_email_used(self):
"""Test ldap is_email_used"""
self.assertTrue(self.ldap.is_email_used(self.user.email))
def test_auth(self):
"""Test ldap auth"""
# self.assertTrue(self.ldap.auth_user(self.password, mail=self.user.email))

9
passbook/ldap/urls.py Normal file
View File

@ -0,0 +1,9 @@
# """passbook LDAP Urls"""
# from django.conf.urls import url
# from supervisr.mod.auth.ldap import views
# urlpatterns = [
# url(r'^settings/$', views.admin_settings, name='admin_settings'),
# ]

38
passbook/ldap/views.py Normal file
View File

@ -0,0 +1,38 @@
# """Supervisr Mod LDAP Views"""
# from django.contrib import messages
# from django.contrib.auth.decorators import login_required, user_passes_test
# from django.http import HttpRequest, HttpResponse
# from django.shortcuts import redirect, render
# from django.urls import reverse
# from django.utils.translation import ugettext as _
# from supervisr.mod.auth.ldap.forms import (AuthenticationBackendSettings,
# ConnectionSettings,
# CreateUsersSettings,
# GeneralSettingsForm)
# @login_required
# @user_passes_test(lambda u: u.is_superuser)
# def admin_settings(request: HttpRequest) -> HttpResponse:
# """Default view for modules without admin view"""
# form_classes = {
# 'general': GeneralSettingsForm,
# 'connection': ConnectionSettings,
# 'authentication': AuthenticationBackendSettings,
# 'create_users': CreateUsersSettings,
# }
# render_data = {}
# for form_key, form_class in form_classes.items():
# render_data[form_key] = form_class(request.POST if request.method == 'POST' else None)
# if request.method == 'POST':
# update_count = 0
# for form_key, form_class in form_classes.items():
# form = form_class(request.POST)
# if form.is_valid():
# update_count += form.save()
# messages.success(request, _('Successfully updated %d settings.' % update_count))
# return redirect(reverse('supervisr_mod_auth_ldap:admin_settings'))
# return render(request, 'ldap/settings.html', render_data)