From efb60e42822a8350c4916855c0ba68c2da67dd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 20 Aug 2019 10:03:47 +0100 Subject: [PATCH 1/4] Extract new password in passwd_s (#246) --- Lib/ldap/extop/__init__.py | 1 + Lib/ldap/extop/passwd.py | 33 ++++++++++++++++++++++++++++++++ Lib/ldap/ldapobject.py | 15 +++++++++++---- Tests/t_ldapobject.py | 39 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 Lib/ldap/extop/passwd.py diff --git a/Lib/ldap/extop/__init__.py b/Lib/ldap/extop/__init__.py index 874166d9..39e653a9 100644 --- a/Lib/ldap/extop/__init__.py +++ b/Lib/ldap/extop/__init__.py @@ -65,3 +65,4 @@ def decodeResponseValue(self,value): # Import sub-modules from ldap.extop.dds import * +from ldap.extop.passwd import PasswordModifyResponse diff --git a/Lib/ldap/extop/passwd.py b/Lib/ldap/extop/passwd.py new file mode 100644 index 00000000..0a8346a8 --- /dev/null +++ b/Lib/ldap/extop/passwd.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +ldap.extop.passwd - Classes for Password Modify extended operation +(see RFC 3062) + +See https://www.python-ldap.org/ for details. +""" + +from ldap.extop import ExtendedResponse + +# Imports from pyasn1 +from pyasn1.type import namedtype, univ, tag +from pyasn1.codec.der import decoder + + +class PasswordModifyResponse(ExtendedResponse): + responseName = None + + class PasswordModifyResponseValue(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.OptionalNamedType( + 'genPasswd', + univ.OctetString().subtype( + implicitTag=tag.Tag(tag.tagClassContext, + tag.tagFormatSimple, 0) + ) + ) + ) + + def decodeResponseValue(self, value): + respValue, _ = decoder.decode(value, asn1Spec=self.PasswordModifyResponseValue()) + self.genPasswd = bytes(respValue.getComponentByName('genPasswd')) + return self.genPasswd diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index a92b0886..2e9674dc 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -27,7 +27,7 @@ from ldap.schema import SCHEMA_ATTRS from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples -from ldap.extop import ExtendedRequest,ExtendedResponse +from ldap.extop import ExtendedRequest,ExtendedResponse,PasswordModifyResponse from ldap.compat import reraise from ldap import LDAPError @@ -656,9 +656,16 @@ def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): newpw = self._bytesify_input('newpw', newpw) return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) - def passwd_s(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): - msgid = self.passwd(user,oldpw,newpw,serverctrls,clientctrls) - return self.extop_result(msgid,all=1,timeout=self.timeout) + def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None): + msgid = self.passwd(user, oldpw, newpw, serverctrls, clientctrls) + respoid, respvalue = self.extop_result(msgid, all=1, timeout=self.timeout) + + if respoid != PasswordModifyResponse.responseName: + raise ldap.PROTOCOL_ERROR("Unexpected OID %s in extended response!" % respoid) + if respvalue: + respvalue = PasswordModifyResponse(PasswordModifyResponse.responseName, respvalue) + + return respoid, respvalue def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None): """ diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 24711b21..5b162380 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -687,6 +687,45 @@ def test_async_search_no_such_object_exception_contains_message_id(self): self._ldap_conn.result() self.assertEqual(cm.exception.args[0]["msgid"], msgid) + def test_passwd_s(self): + l = self._ldap_conn + + # first, create a user to change password on + dn = "cn=PasswordTest," + self.server.suffix + result, pmsg, msgid, ctrls = l.add_ext_s( + dn, + [ + ('objectClass', b'person'), + ('sn', b'PasswordTest'), + ('cn', b'PasswordTest'), + ('userPassword', b'initial'), + ] + ) + self.assertEqual(result, ldap.RES_ADD) + self.assertIsInstance(msgid, int) + self.assertEqual(pmsg, []) + self.assertEqual(ctrls, []) + + # try changing password with a wrong old-pw + with self.assertRaises(ldap.UNWILLING_TO_PERFORM): + l.passwd_s(dn, "bogus", "ignored") + + # have the server generate a new random pw + respoid, respvalue = l.passwd_s(dn, "initial", None) + self.assertEqual(respoid, None) + + password = respvalue.genPasswd + self.assertIsInstance(password, bytes) + if PY2: + password = password.decode('utf-8') + + # try changing password back + respoid, respvalue = l.passwd_s(dn, password, "initial") + self.assertEqual(respoid, None) + self.assertEqual(respvalue, None) + + l.delete_s(dn) + class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): """ From c812258768516fcc0da3046bd9ef3ef0e945bd0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 20 Aug 2019 10:56:38 +0100 Subject: [PATCH 2/4] Update documentation --- Doc/reference/ldap.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 1d1b025e..248a52ee 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -963,7 +963,7 @@ and wait for and return with the server's result, or with .. py:method:: LDAPObject.passwd(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> int -.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> None +.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> (respoid, respvalue) Perform a ``LDAP Password Modify Extended Operation`` operation on the entry specified by *user*. @@ -974,6 +974,11 @@ and wait for and return with the server's result, or with of the specified *user* which is sometimes used when a user changes his own password. + ``respoid`` is always :py:const:`None`. ``respvalue`` is also + :py:const:`None` unless *newpw* was :py:const:`None`, this requests + that the server generates a new random password. With :py:meth:`passwd_s()` + method, this password is available through ``respvalue.genPasswd``. + *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. The asynchronous version returns the initiated message id. @@ -983,6 +988,7 @@ and wait for and return with the server's result, or with .. seealso:: :rfc:`3062` - LDAP Password Modify Extended Operation + :py:mod:`ldap.extop.passwd` From f6dac66cdfe8083d3926cde359ad9a9a065191f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 20 Aug 2019 11:48:47 +0100 Subject: [PATCH 3/4] Doc for extract_newpw --- Doc/reference/ldap.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 248a52ee..06ecb906 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -963,7 +963,7 @@ and wait for and return with the server's result, or with .. py:method:: LDAPObject.passwd(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> int -.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> (respoid, respvalue) +.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None] [, extract_newpw=False]]]) -> (respoid, respvalue) Perform a ``LDAP Password Modify Extended Operation`` operation on the entry specified by *user*. @@ -974,10 +974,12 @@ and wait for and return with the server's result, or with of the specified *user* which is sometimes used when a user changes his own password. - ``respoid`` is always :py:const:`None`. ``respvalue`` is also - :py:const:`None` unless *newpw* was :py:const:`None`, this requests - that the server generates a new random password. With :py:meth:`passwd_s()` - method, this password is available through ``respvalue.genPasswd``. + *respoid* is always :py:const:`None`. *respvalue* is also + :py:const:`None` unless *newpw* was :py:const:`None`. This requests that + the server generate a new random password. If *extract_newpw* is + :py:const:`True`, this password is a bytes object available through + ``respvalue.genPasswd``, otherwise *respvalue* is the raw ASN.1 response + (this is deprecated and only for backwards compatibility). *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`. From 2550717ba440f8fc56a600d79665b9477f4f2b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Kuzn=C3=ADk?= Date: Tue, 20 Aug 2019 11:50:09 +0100 Subject: [PATCH 4/4] Impl for extract_newpw --- Lib/ldap/ldapobject.py | 4 ++-- Tests/t_ldapobject.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index 2e9674dc..ab654e17 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -656,13 +656,13 @@ def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): newpw = self._bytesify_input('newpw', newpw) return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) - def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None): + def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None, extract_newpw=False): msgid = self.passwd(user, oldpw, newpw, serverctrls, clientctrls) respoid, respvalue = self.extop_result(msgid, all=1, timeout=self.timeout) if respoid != PasswordModifyResponse.responseName: raise ldap.PROTOCOL_ERROR("Unexpected OID %s in extended response!" % respoid) - if respvalue: + if extract_newpw and respvalue: respvalue = PasswordModifyResponse(PasswordModifyResponse.responseName, respvalue) return respoid, respvalue diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 5b162380..1ec00280 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -711,7 +711,7 @@ def test_passwd_s(self): l.passwd_s(dn, "bogus", "ignored") # have the server generate a new random pw - respoid, respvalue = l.passwd_s(dn, "initial", None) + respoid, respvalue = l.passwd_s(dn, "initial", None, extract_newpw=True) self.assertEqual(respoid, None) password = respvalue.genPasswd