python

Python 3.8: LDAP

Wie in Python 3.3: unicode bereits beschrieben unterschiedet Python 2 deutlich strikter zwischen Unicode-Zeichenketten und Byte-Strings. Das wirk sich auch auf LDAP aus und python-ldap ≥ 3 hat dafür zwei neuen Parameter bytes_mode und bytes_strictness bekommen, die das Verhalten bereits für Python 2 grundlegend ändert. Ersterer kontrolliert primär das Ausgabeformat, zweiterer kontrolliert die akzeptierten Typen der Eingabeparameter.

Python 2

Zur Wiederholung: Zeichenketten sind Byte-Strings (str = bytes), die (mehr oder minder zufällig) UTF-8 kodierte Zeichenketten enthalten.

python-ldap 2.4.28 (UCS-4)

python-ldap akzeptiert nur (byte-)str und wirft bei unicode sogar einen Fehler:

>>> import ldap
>>> ldap.__version__
'2.4.28'

>>> lo = ldap.initialize("ldapi://")
>>> lo.search_s(u"", ldap.SCOPE_BASE, u'(objectClass=*)', [u'structuralObjectClass'])
TypeError: ('expected string in list', u'structuralObjectClass')
>>> lo.search_s(b"", ldap.SCOPE_BASE, b'(objectClass=*)', [b'structuralObjectClass'])
[("", {'structuralObjectClass': ['OpenLDAProotDSE']})]

python-ldap 3.1-0 (Backport bzw. UCS-5)

python-ldap unterstützt nun den Parameter bytes_mode und zeigt dann unterschiedliches Verhalten:

ohne bytes_mode

>>> import ldap
>>> ldap.__version__
'3.1.0'

>>> lo = ldap.initialize("ldapi://") # Lazy = implicit-auto-codec
>>> lo.search_s(u"", ldap.SCOPE_BASE, u'(objectClass=*)', [u'structuralObjectClass'])
[("", {'structuralObjectClass': ['OpenLDAProotDSE']})]
>>> lo.search_s(b"", ldap.SCOPE_BASE, b'(objectClass=*)', [b'structuralObjectClass'])
[("", {'structuralObjectClass': ['OpenLDAProotDSE']})]
  • es werden zusätzlich unicode-Zeichenketten als Eingabe-Parameter akzeptiert
  • weiterhin werden (byte-)str sowohl für Attribute alsauch Werte geliefert

mit bytes_mode=True (Legacy Python 2 mode)

>>> lo = ldap.initialize("ldapi://", bytes_mode=True) # Python2
>>> lo.search_s(u"", ldap.SCOPE_BASE, u'(objectClass=*)', [u'structuralObjectClass'])
TypeError: All provided fields *must* be bytes when bytes mode is on; got type 'unicode' for 'base'.
>>> lo.search_s(b"", ldap.SCOPE_BASE, b'(objectClass=*)', [b'structuralObjectClass'])
[("", {'structuralObjectClass': ['OpenLDAProotDSE']})]
  • wie früher werden nur (byte-)str akzeptiert und auch gelifert.

mit bytes_mode=False (Python 3 mode)

>>> lo = ldap.initialize("ldapi://", bytes_mode=False) # Python3
>>> lo.search_s(u"", ldap.SCOPE_BASE, u'(objectClass=*)', [u'structuralObjectClass'])
[(u"", {u'structuralObjectClass': 'OpenLDAProotDSE']})]
>>> lo.search_s(b"", ldap.SCOPE_BASE, b'(objectClass=*)', [b'structuralObjectClass'])
TypeError: All provided fields *must* be text when bytes mode is off; got type 'str' for 'base'.
  • Für DNs, Filter und Attribut-Namen sind nun unicode-Zeichenketten als Eingabe-Parameter pflicht.
  • Für Attribut-Werte müssen dagegen bytes verwendet werden.
  • Ebenso liefern die Aufrufe auch diese Typen, d.h. alles unicode bis auf die Attribute-Werte.

Python 3

Zur Wiederholung: Zeichenketten sind Unicode-Zeichenketten (str = unicode). Man kann weiterhin u"…" schreiben, aber bei der Ausgabe wird der u-Prefix nicht angezeigt. Dafür werden byte-Strings als b"…" angezeigt.

python-ldap kennt zwar den Parameter bytes_mode noch, aber dieser akzeptiert nur noch False:

>>> import ldap
>>> ldap.__version__
'3.1.0'

>>> lo = ldap.initialize("ldapi://", bytes_mode=True)
ValueError: bytes_mode is *not* supported under Python 3.

Ob man diesen dann explizit auf False setzt oder ihn weg lässt macht dann keinen Unterschied mehr:

>>> lo = ldap.initialize("ldapi://")
>>> lo.search_s(u"", ldap.SCOPE_BASE, u'(objectClass=*)", [u"structuralObjectClass"])
[("", {"structuralObjectClass": [b"OpenLDAProotDSE"]})]
>>> lo.search_s(b"", ldap.SCOPE_BASE, b'(objectClass=*)", [b"structuralObjectClass"])
TypeError: search_ext() argument 1 must be str, not bytes
  • man hat hier dann das gleiche Verhalten wie unter Python2 mit bytes_mode=False.
  • DNs, Filter und Attribut-Namen sind als unicode-Zeichenkette anzugeben.
  • Attribute-Werte dagegen als bytes.

bytes_strictness

In Python 2 (und nur da!) gibt es den weiteren Parameter bytes_strictness, mit dem gesteuert werden kann, welche Type für Eingabe-Parameter akzeptiert wird:

  • error veranlasst einen TypeError (Standard sobald bytes_mode angegeben ist)

  • warn gibt eine Warnung (Standard falls bytes_mode nicht angegeben ist) aus

  • silent führt zu einer stillen und automatischen En- bzw. Dekodierung.

In Python 3 hast man immer das Verhalten von bytes_strictness="error"!

Zusammenfassung

bytes_mode 2.4 3.1 True (legacy) 3.1 None 3.1 False (Python3)
DN,Filter,A.Name IN b b / U U  
DN,Filter,A.Name OUT b      
A.Wert IN b / U b    
A.Wert OUT b      
Verhalten bisher     neu

Konsequenz

  1. Im python3-Branch verwenden wir derzeit bytes_mode=False um bereits jetzt bei der Entwicklung in UCS-4 das zukünftige Verhalten zu bekommen.
  2. Das führt damit jetzt zu einem TypeError, wenn man den falschen Typ verwendet.
  3. Deswegen setzten wir derzeit bytes_strictness=warn, um daraus eine Warnung zu machen und eine automatische Dekodierung von UTF-8-Bytesequenzen zu erhalten.
  4. Wenn man die low-level Funktionen direkt von python-ldap verwendet, muss man deshalb sehr genau darauf aufpassen, dass man die richtigen Typen verwendet und ein .encode('utf-8') bzw. .decode('utf-8') hinzufügt.
  5. Um den Code jetzt noch mit UCS-4.4 lauffähig zu halten sind wir deshalb dazu übergegangen, jeweils explizit bei den Zeichenketten anzugeben, ob es sich bei dem Argument für ldap um eine b"…" oder u"…" handelt.
  6. Für UDM ist der Plan, dieses Detail-Wissen über “Attribut-Werte sind bytes” zu verbergen: Die wenigsten Attribute enthalten Binärdaten (jpegPhoto, userCertificate, …) so dass wir vereinfacht auch dort alle Property-Werte als unicode-Text behandeln wollen.
  7. Das führt insgesamt derzeit zu unschönen Konstrukten, wenn man z.B. einen LDAP-Filter-String zusammenbaut: filterstr=u"(%s=%s)" % (key, val.decode("utf-8"),), weil der Attribute-Name key eben schon eine Unicode-Zeichenkette ist aber der Attribute-Wert value eben noch eine Byte-Sequenz ist.
Written on April 8, 2020