diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index 1d1b025..06ecb90 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] [, extract_newpw=False]]]) -> (respoid, respvalue) Perform a ``LDAP Password Modify Extended Operation`` operation on the entry specified by *user*. @@ -974,6 +974,13 @@ 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 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`. The asynchronous version returns the initiated message id. @@ -983,6 +990,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` diff --git a/Lib/ldap/extop/__init__.py b/Lib/ldap/extop/__init__.py index 874166d..39e653a 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 0000000..0a8346a --- /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 a92b088..ab654e1 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, 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 extract_newpw and 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 24711b2..1ec0028 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, extract_newpw=True) + 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): """