add some python 3 support in the LDAP and AD backends

python-ldap talks in bytes,
as the rest of ldapcherry talks in unicode utf-8:
* everything passed to python-ldap must be converted to bytes
* everything coming from python-ldap must be converted to unicode

The previous statement was true for python-ldap < version 3.X.
With versions > 3.0.0 and python 3, it gets tricky,
some parts of python-ldap takes string, specially the filters/escaper.

so we have now:
*_byte_p2 (unicode -> bytes conversion for python 2)
*_byte_p3 (unicode -> bytes conversion for python 3)
*_byte_p23 (unicode -> bytes conversion for python AND 3)
This commit is contained in:
kakwa 2019-02-09 16:08:18 +01:00
parent 979d4eeda8
commit 10747cff93
2 changed files with 121 additions and 67 deletions

View File

@ -15,6 +15,7 @@ import ldapcherry.backend
from ldapcherry.exceptions import UserDoesntExist, GroupDoesntExist from ldapcherry.exceptions import UserDoesntExist, GroupDoesntExist
import os import os
import re import re
import sys
class CaFileDontExist(Exception): class CaFileDontExist(Exception):
@ -129,11 +130,11 @@ class Backend(ldapcherry.backend.backendLdap.Backend):
self.dn_user_attr = 'cn' self.dn_user_attr = 'cn'
self.key = 'sAMAccountName' self.key = 'sAMAccountName'
self.objectclasses = [ self.objectclasses = [
'top', self._byte_p23('top'),
'person', self._byte_p23('person'),
'organizationalPerson', self._byte_p23('organizationalPerson'),
'user', self._byte_p23('user'),
'posixAccount', self._byte_p23('posixAccount'),
] ]
self.group_attrs = { self.group_attrs = {
'member': "%(dn)s" 'member': "%(dn)s"
@ -142,16 +143,25 @@ class Backend(ldapcherry.backend.backendLdap.Backend):
self.attrlist = [] self.attrlist = []
self.group_attrs_keys = [] self.group_attrs_keys = []
for a in attrslist: for a in attrslist:
self.attrlist.append(self._str(a)) self.attrlist.append(self._byte_p2(a))
if self._str('cn') not in self.attrlist: if self._byte_p2('cn') not in self.attrlist:
raise MissingAttr() raise MissingAttr()
if self._str('unicodePwd') not in self.attrlist: if self._byte_p2('unicodePwd') not in self.attrlist:
raise MissingAttr() raise MissingAttr()
if sys.version < '3':
@staticmethod
def _tobyte(in_int):
return str(in_int)
else:
@staticmethod
def _tobyte(in_int):
return in_int.to_bytes(4, byteorder='big')
def _search_group(self, searchfilter, groupdn): def _search_group(self, searchfilter, groupdn):
searchfilter = self._str(searchfilter) searchfilter = self._byte_p2(searchfilter)
ldap_client = self._bind() ldap_client = self._bind()
try: try:
r = ldap_client.search_s( r = ldap_client.search_s(
@ -183,22 +193,24 @@ class Backend(ldapcherry.backend.backendLdap.Backend):
ldap_client = self._bind() ldap_client = self._bind()
if by_cn: if by_cn:
dn = self._str('CN=%(cn)s,%(user_dn)s' % { dn = self._byte_p2('CN=%(cn)s,%(user_dn)s' % {
'cn': name, 'cn': name,
'user_dn': self.userdn 'user_dn': self.userdn
}) })
else: else:
dn = self._str(name) dn = self._byte_p2(name)
attrs = {} attrs = {}
attrs['unicodePwd'] = self._modlist(self._str(password_value)) attrs['unicodePwd'] = self._modlist(self._byte_p2(password_value))
ldif = modlist.modifyModlist({'unicodePwd': 'tmp'}, attrs) ldif = modlist.modifyModlist({'unicodePwd': 'tmp'}, attrs)
ldap_client.modify_s(dn, ldif) ldap_client.modify_s(dn, ldif)
del(attrs['unicodePwd']) del(attrs['unicodePwd'])
attrs['UserAccountControl'] = self._modlist(str(NORMAL_ACCOUNT)) attrs['UserAccountControl'] = self._modlist(
self._tobyte(NORMAL_ACCOUNT)
)
ldif = modlist.modifyModlist({'UserAccountControl': 'tmp'}, attrs) ldif = modlist.modifyModlist({'UserAccountControl': 'tmp'}, attrs)
ldap_client.modify_s(dn, ldif) ldap_client.modify_s(dn, ldif)
@ -212,7 +224,7 @@ class Backend(ldapcherry.backend.backendLdap.Backend):
if 'unicodePwd' in attrs: if 'unicodePwd' in attrs:
password = attrs['unicodePwd'] password = attrs['unicodePwd']
del(attrs['unicodePwd']) del(attrs['unicodePwd'])
userdn = self._get_user(self._str(username), NO_ATTR) userdn = self._get_user(self._byte_p2(username), NO_ATTR)
self._set_password(userdn, password, False) self._set_password(userdn, password, False)
super(Backend, self).set_attrs(username, attrs) super(Backend, self).set_attrs(username, attrs)
@ -226,7 +238,7 @@ class Backend(ldapcherry.backend.backendLdap.Backend):
def get_groups(self, username): def get_groups(self, username):
username = ldap.filter.escape_filter_chars(username) username = ldap.filter.escape_filter_chars(username)
userdn = self._get_user(self._str(username), NO_ATTR) userdn = self._get_user(self._byte_p2(username), NO_ATTR)
searchfilter = self.group_filter_tmpl % { searchfilter = self.group_filter_tmpl % {
'userdn': userdn, 'userdn': userdn,
@ -246,7 +258,7 @@ class Backend(ldapcherry.backend.backendLdap.Backend):
) )
for entry in groups: for entry in groups:
ret.append(entry[1]['cn'][0]) ret.append(self._uni(entry[1]['cn'][0]))
return ret return ret
def auth(self, username, password): def auth(self, username, password):
@ -256,8 +268,8 @@ class Backend(ldapcherry.backend.backendLdap.Backend):
ldap_client = self._connect() ldap_client = self._connect()
try: try:
ldap_client.simple_bind_s( ldap_client.simple_bind_s(
self._str(binddn), self._byte_p2(binddn),
self._str(password) self._byte_p2(password)
) )
except ldap.INVALID_CREDENTIALS: except ldap.INVALID_CREDENTIALS:
ldap_client.unbind_s() ldap_client.unbind_s()

View File

@ -76,7 +76,7 @@ class Backend(ldapcherry.backend.Backend):
# objectclasses parameter is a coma separated list in configuration # objectclasses parameter is a coma separated list in configuration
# split it to get a real list, and convert it to bytes # split it to get a real list, and convert it to bytes
for o in re.split(r'\W+', self.get_param('objectclasses')): for o in re.split(r'\W+', self.get_param('objectclasses')):
self.objectclasses.append(self._str(o)) self.objectclasses.append(self._byte_p23(o))
self.group_attrs = {} self.group_attrs = {}
self.group_attrs_keys = set([]) self.group_attrs_keys = set([])
for param in config: for param in config:
@ -89,7 +89,7 @@ class Backend(ldapcherry.backend.Backend):
self.attrlist = [] self.attrlist = []
for a in attrslist: for a in attrslist:
self.attrlist.append(self._str(a)) self.attrlist.append(self._byte_p2(a))
# exception handler (mainly to log something meaningful) # exception handler (mainly to log something meaningful)
def _exception_handler(self, e): def _exception_handler(self, e):
@ -302,7 +302,7 @@ class Backend(ldapcherry.backend.Backend):
user_filter = self.user_filter_tmpl % { user_filter = self.user_filter_tmpl % {
'username': self._uni(username) 'username': self._uni(username)
} }
r = self._search(self._str(user_filter), attrs, self.userdn) r = self._search(self._byte_p2(user_filter), attrs, self.userdn)
if len(r) == 0: if len(r) == 0:
return None return None
@ -319,23 +319,56 @@ class Backend(ldapcherry.backend.Backend):
# as the rest of ldapcherry talks in unicode utf-8: # as the rest of ldapcherry talks in unicode utf-8:
# * everything passed to python-ldap must be converted to bytes # * everything passed to python-ldap must be converted to bytes
# * everything coming from python-ldap must be converted to unicode # * everything coming from python-ldap must be converted to unicode
#
# The previous statement was true for python-ldap < version 3.X.
# With versions > 3.0.0 and python 3, it gets tricky,
# some parts of python-ldap takes string, specially the filters/escaper.
#
# so we have now:
# *_byte_p2 (unicode -> bytes conversion for python 2)
# *_byte_p3 (unicode -> bytes conversion for python 3)
# *_byte_p23 (unicode -> bytes conversion for python AND 3)
def _byte_p23(self, s):
"""unicode -> bytes conversion"""
if s is None:
return None
return s.encode('utf-8')
if sys.version < '3': if sys.version < '3':
def _str(self, s): def _byte_p2(self, s):
"""unicode -> bytes conversion""" """unicode -> bytes conversion (python 2)"""
if s is None: if s is None:
return None return None
return s.encode('utf-8') return s.encode('utf-8')
def _byte_p3(self, s):
"""pass through (does something in python 3)"""
return s
def _uni(self, s): def _uni(self, s):
"""bytes -> unicode conversion""" """bytes -> unicode conversion"""
if s is None: if s is None:
return None return None
return s.decode('utf-8', 'ignore') return s.decode('utf-8', 'ignore')
def attrs_pretreatment(self, attrs):
attrs_srt = {}
for a in attrs:
attrs_srt[self._byte_p2(a)] = self._modlist(
self._byte_p2(attrs[a])
)
return attrs_srt
else: else:
def _str(self, s): def _byte_p2(self, s):
"""unicode -> bytes conversion""" """pass through (does something in python 2)"""
return s return s
def _byte_p3(self, s):
"""unicode -> bytes conversion"""
if s is None:
return None
return s.encode('utf-8')
def _uni(self, s): def _uni(self, s):
"""bytes -> unicode conversion""" """bytes -> unicode conversion"""
if s is None: if s is None:
@ -345,16 +378,24 @@ class Backend(ldapcherry.backend.Backend):
else: else:
return s return s
def attrs_pretreatment(self, attrs):
attrs_srt = {}
for a in attrs:
attrs_srt[self._byte_p2(a)] = self._modlist(
self._byte_p3(attrs[a])
)
return attrs_srt
def auth(self, username, password): def auth(self, username, password):
"""Authentication of a user""" """Authentication of a user"""
binddn = self._get_user(self._str(username), NO_ATTR) binddn = self._get_user(self._byte_p2(username), NO_ATTR)
if binddn is not None: if binddn is not None:
ldap_client = self._connect() ldap_client = self._connect()
try: try:
ldap_client.simple_bind_s( ldap_client.simple_bind_s(
self._str(binddn), self._byte_p2(binddn),
self._str(password) self._byte_p2(password)
) )
except ldap.INVALID_CREDENTIALS: except ldap.INVALID_CREDENTIALS:
ldap_client.unbind_s() ldap_client.unbind_s()
@ -368,36 +409,31 @@ class Backend(ldapcherry.backend.Backend):
@staticmethod @staticmethod
def _modlist(in_attr): def _modlist(in_attr):
return in_attr return in_attr
else: else:
@staticmethod @staticmethod
def _modlist(in_attr): def _modlist(in_attr):
return [in_attr] return [in_attr]
def attrs_pretreatment(self, attrs):
attrs_str = {}
for a in attrs:
attrs_str[self._str(a)] = self._modlist(self._str(attrs[a]))
return attrs_str
def add_user(self, attrs): def add_user(self, attrs):
"""add a user""" """add a user"""
ldap_client = self._bind() ldap_client = self._bind()
# encoding crap # encoding crap
attrs_str = self.attrs_pretreatment(attrs) attrs_srt = self.attrs_pretreatment(attrs)
attrs_str[self._str('objectClass')] = self.objectclasses attrs_srt[self._byte_p2('objectClass')] = self.objectclasses
# construct is DN # construct is DN
dn = \ dn = \
self._str(self.dn_user_attr) + \ self._byte_p2(self.dn_user_attr) + \
self._str('=') + \ self._byte_p2('=') + \
self._str(ldap.dn.escape_dn_chars( self._byte_p2(ldap.dn.escape_dn_chars(
attrs[self.dn_user_attr] attrs[self.dn_user_attr]
) )
) + \ ) + \
self._str(',') + \ self._byte_p2(',') + \
self._str(self.userdn) self._byte_p2(self.userdn)
# gen the ldif first add_s and add the user # gen the ldif first add_s and add the user
ldif = modlist.addModlist(attrs_str) ldif = modlist.addModlist(attrs_srt)
try: try:
ldap_client.add_s(dn, ldif) ldap_client.add_s(dn, ldif)
except ldap.ALREADY_EXISTS as e: except ldap.ALREADY_EXISTS as e:
@ -411,7 +447,7 @@ class Backend(ldapcherry.backend.Backend):
"""delete a user""" """delete a user"""
ldap_client = self._bind() ldap_client = self._bind()
# recover the user dn # recover the user dn
dn = self._str(self._get_user(self._str(username), NO_ATTR)) dn = self._byte_p2(self._get_user(self._byte_p2(username), NO_ATTR))
# delete # delete
if dn is not None: if dn is not None:
ldap_client.delete_s(dn) ldap_client.delete_s(dn)
@ -423,15 +459,15 @@ class Backend(ldapcherry.backend.Backend):
def set_attrs(self, username, attrs): def set_attrs(self, username, attrs):
""" set user attributes""" """ set user attributes"""
ldap_client = self._bind() ldap_client = self._bind()
tmp = self._get_user(self._str(username), ALL_ATTRS) tmp = self._get_user(self._byte_p2(username), ALL_ATTRS)
if tmp is None: if tmp is None:
raise UserDoesntExist(username, self.backend_name) raise UserDoesntExist(username, self.backend_name)
dn = self._str(tmp[0]) dn = self._byte_p2(tmp[0])
old_attrs = tmp[1] old_attrs = tmp[1]
for attr in attrs: for attr in attrs:
bcontent = self._str(attrs[attr]) bcontent = self._byte_p2(attrs[attr])
battr = self._str(attr) battr = self._byte_p2(attr)
new = {battr: self._modlist(bcontent)} new = {battr: self._modlist(self._byte_p3(bcontent))}
# if attr is dn entry, use rename # if attr is dn entry, use rename
if attr.lower() == self.dn_user_attr.lower(): if attr.lower() == self.dn_user_attr.lower():
ldap_client.rename_s( ldap_client.rename_s(
@ -448,10 +484,12 @@ class Backend(ldapcherry.backend.Backend):
if type(old_attrs[attr]) is list: if type(old_attrs[attr]) is list:
tmp = [] tmp = []
for value in old_attrs[attr]: for value in old_attrs[attr]:
tmp.append(self._str(value)) tmp.append(self._byte_p2(value))
bold_value = tmp bold_value = tmp
else: else:
bold_value = self._modlist(self._str(old_attrs[attr])) bold_value = self._modlist(
self._byte_p3(old_attrs[attr])
)
old = {battr: bold_value} old = {battr: bold_value}
# attribute is not set, just add it # attribute is not set, just add it
else: else:
@ -469,19 +507,19 @@ class Backend(ldapcherry.backend.Backend):
def add_to_groups(self, username, groups): def add_to_groups(self, username, groups):
ldap_client = self._bind() ldap_client = self._bind()
# recover dn of the user and his attributes # recover dn of the user and his attributes
tmp = self._get_user(self._str(username), ALL_ATTRS) tmp = self._get_user(self._byte_p2(username), ALL_ATTRS)
dn = tmp[0] dn = tmp[0]
attrs = tmp[1] attrs = tmp[1]
attrs['dn'] = dn attrs['dn'] = dn
self._normalize_group_attrs(attrs) self._normalize_group_attrs(attrs)
dn = self._str(tmp[0]) dn = self._byte_p2(tmp[0])
# add user to all groups # add user to all groups
for group in groups: for group in groups:
group = self._str(group) group = self._byte_p2(group)
# iterate on group membership attributes # iterate on group membership attributes
for attr in self.group_attrs: for attr in self.group_attrs:
# fill the content template # fill the content template
content = self._str(self.group_attrs[attr] % attrs) content = self._byte_p2(self.group_attrs[attr] % attrs)
self._logger( self._logger(
severity=logging.DEBUG, severity=logging.DEBUG,
msg="%(backend)s: adding user '%(user)s'" msg="%(backend)s: adding user '%(user)s'"
@ -497,12 +535,14 @@ class Backend(ldapcherry.backend.Backend):
) )
ldif = modlist.modifyModlist( ldif = modlist.modifyModlist(
{}, {},
{attr: self._modlist(content)} {attr: self._modlist(self._byte_p3(content))}
) )
try: try:
print(ldif)
print(group)
ldap_client.modify_s(group, ldif) ldap_client.modify_s(group, ldif)
# if already member, not a big deal, just log it and continue # if already member, not a big deal, just log it and continue
except ldap.TYPE_OR_VALUE_EXISTS as e: except (ldap.TYPE_OR_VALUE_EXISTS, ldap.ALREADY_EXISTS) as e:
self._logger( self._logger(
severity=logging.INFO, severity=logging.INFO,
msg="%(backend)s: user '%(user)s'" msg="%(backend)s: user '%(user)s'"
@ -526,19 +566,19 @@ class Backend(ldapcherry.backend.Backend):
# it follows the same logic than add_to_groups # it follows the same logic than add_to_groups
# but with MOD_DELETE # but with MOD_DELETE
ldap_client = self._bind() ldap_client = self._bind()
tmp = self._get_user(self._str(username), ALL_ATTRS) tmp = self._get_user(self._byte_p2(username), ALL_ATTRS)
if tmp is None: if tmp is None:
raise UserDoesntExist(username, self.backend_name) raise UserDoesntExist(username, self.backend_name)
dn = tmp[0] dn = tmp[0]
attrs = tmp[1] attrs = tmp[1]
attrs['dn'] = dn attrs['dn'] = dn
self._normalize_group_attrs(attrs) self._normalize_group_attrs(attrs)
dn = self._str(tmp[0]) dn = self._byte_p2(tmp[0])
for group in groups: for group in groups:
group = self._str(group) group = self._byte_p2(group)
for attr in self.group_attrs: for attr in self.group_attrs:
content = self._str(self.group_attrs[attr] % attrs) content = self._byte_p2(self.group_attrs[attr] % attrs)
ldif = [(ldap.MOD_DELETE, attr, content)] ldif = [(ldap.MOD_DELETE, attr, self._byte_p3(content))]
try: try:
ldap_client.modify_s(group, ldif) ldap_client.modify_s(group, ldif)
except ldap.NO_SUCH_ATTRIBUTE as e: except ldap.NO_SUCH_ATTRIBUTE as e:
@ -561,7 +601,9 @@ class Backend(ldapcherry.backend.Backend):
def search(self, searchstring): def search(self, searchstring):
"""Search users""" """Search users"""
# escape special char to avoid injection # escape special char to avoid injection
searchstring = ldap.filter.escape_filter_chars(self._str(searchstring)) searchstring = ldap.filter.escape_filter_chars(
self._byte_p2(searchstring)
)
# fill the search string template # fill the search string template
searchfilter = self.search_filter_tmpl % { searchfilter = self.search_filter_tmpl % {
'searchstring': searchstring 'searchstring': searchstring
@ -586,7 +628,7 @@ class Backend(ldapcherry.backend.Backend):
def get_user(self, username): def get_user(self, username):
"""Gest a specific user""" """Gest a specific user"""
ret = {} ret = {}
tmp = self._get_user(self._str(username), ALL_ATTRS) tmp = self._get_user(self._byte_p2(username), ALL_ATTRS)
if tmp is None: if tmp is None:
raise UserDoesntExist(username, self.backend_name) raise UserDoesntExist(username, self.backend_name)
attrs_tmp = tmp[1] attrs_tmp = tmp[1]
@ -600,7 +642,7 @@ class Backend(ldapcherry.backend.Backend):
def get_groups(self, username): def get_groups(self, username):
"""Get all groups of a user""" """Get all groups of a user"""
username = ldap.filter.escape_filter_chars(self._str(username)) username = ldap.filter.escape_filter_chars(self._byte_p2(username))
userdn = self._get_user(username, NO_ATTR) userdn = self._get_user(username, NO_ATTR)
searchfilter = self.group_filter_tmpl % { searchfilter = self.group_filter_tmpl % {
@ -611,5 +653,5 @@ class Backend(ldapcherry.backend.Backend):
groups = self._search(searchfilter, NO_ATTR, self.groupdn) groups = self._search(searchfilter, NO_ATTR, self.groupdn)
ret = [] ret = []
for entry in groups: for entry in groups:
ret.append(entry[0]) ret.append(self._uni(entry[0]))
return ret return ret