Skip to content

Commit 9a91bbd

Browse files
authored
Extract new password in passwd_s
Fixes: python-ldap#246 python-ldap#299
1 parent c803bfc commit 9a91bbd

5 files changed

Lines changed: 93 additions & 5 deletions

File tree

Doc/reference/ldap.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ and wait for and return with the server's result, or with
963963

964964
.. py:method:: LDAPObject.passwd(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> int
965965
966-
.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> None
966+
.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None] [, extract_newpw=False]]]) -> (respoid, respvalue)
967967
968968
Perform a ``LDAP Password Modify Extended Operation`` operation
969969
on the entry specified by *user*.
@@ -974,6 +974,13 @@ and wait for and return with the server's result, or with
974974
of the specified *user* which is sometimes used when a user changes
975975
his own password.
976976

977+
*respoid* is always :py:const:`None`. *respvalue* is also
978+
:py:const:`None` unless *newpw* was :py:const:`None`. This requests that
979+
the server generate a new random password. If *extract_newpw* is
980+
:py:const:`True`, this password is a bytes object available through
981+
``respvalue.genPasswd``, otherwise *respvalue* is the raw ASN.1 response
982+
(this is deprecated and only for backwards compatibility).
983+
977984
*serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`.
978985

979986
The asynchronous version returns the initiated message id.
@@ -983,6 +990,7 @@ and wait for and return with the server's result, or with
983990
.. seealso::
984991

985992
:rfc:`3062` - LDAP Password Modify Extended Operation
993+
:py:mod:`ldap.extop.passwd`
986994

987995

988996

Lib/ldap/extop/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,4 @@ def decodeResponseValue(self,value):
6565

6666
# Import sub-modules
6767
from ldap.extop.dds import *
68+
from ldap.extop.passwd import PasswordModifyResponse

Lib/ldap/extop/passwd.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
ldap.extop.passwd - Classes for Password Modify extended operation
4+
(see RFC 3062)
5+
6+
See https://www.python-ldap.org/ for details.
7+
"""
8+
9+
from ldap.extop import ExtendedResponse
10+
11+
# Imports from pyasn1
12+
from pyasn1.type import namedtype, univ, tag
13+
from pyasn1.codec.der import decoder
14+
15+
16+
class PasswordModifyResponse(ExtendedResponse):
17+
responseName = None
18+
19+
class PasswordModifyResponseValue(univ.Sequence):
20+
componentType = namedtype.NamedTypes(
21+
namedtype.OptionalNamedType(
22+
'genPasswd',
23+
univ.OctetString().subtype(
24+
implicitTag=tag.Tag(tag.tagClassContext,
25+
tag.tagFormatSimple, 0)
26+
)
27+
)
28+
)
29+
30+
def decodeResponseValue(self, value):
31+
respValue, _ = decoder.decode(value, asn1Spec=self.PasswordModifyResponseValue())
32+
self.genPasswd = bytes(respValue.getComponentByName('genPasswd'))
33+
return self.genPasswd

Lib/ldap/ldapobject.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from ldap.schema import SCHEMA_ATTRS
2929
from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples
30-
from ldap.extop import ExtendedRequest,ExtendedResponse
30+
from ldap.extop import ExtendedRequest,ExtendedResponse,PasswordModifyResponse
3131
from ldap.compat import reraise
3232

3333
from ldap import LDAPError
@@ -656,9 +656,16 @@ def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None):
656656
newpw = self._bytesify_input('newpw', newpw)
657657
return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
658658

659-
def passwd_s(self,user,oldpw,newpw,serverctrls=None,clientctrls=None):
660-
msgid = self.passwd(user,oldpw,newpw,serverctrls,clientctrls)
661-
return self.extop_result(msgid,all=1,timeout=self.timeout)
659+
def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None, extract_newpw=False):
660+
msgid = self.passwd(user, oldpw, newpw, serverctrls, clientctrls)
661+
respoid, respvalue = self.extop_result(msgid, all=1, timeout=self.timeout)
662+
663+
if respoid != PasswordModifyResponse.responseName:
664+
raise ldap.PROTOCOL_ERROR("Unexpected OID %s in extended response!" % respoid)
665+
if extract_newpw and respvalue:
666+
respvalue = PasswordModifyResponse(PasswordModifyResponse.responseName, respvalue)
667+
668+
return respoid, respvalue
662669

663670
def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None):
664671
"""

Tests/t_ldapobject.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,45 @@ def test_async_search_no_such_object_exception_contains_message_id(self):
687687
self._ldap_conn.result()
688688
self.assertEqual(cm.exception.args[0]["msgid"], msgid)
689689

690+
def test_passwd_s(self):
691+
l = self._ldap_conn
692+
693+
# first, create a user to change password on
694+
dn = "cn=PasswordTest," + self.server.suffix
695+
result, pmsg, msgid, ctrls = l.add_ext_s(
696+
dn,
697+
[
698+
('objectClass', b'person'),
699+
('sn', b'PasswordTest'),
700+
('cn', b'PasswordTest'),
701+
('userPassword', b'initial'),
702+
]
703+
)
704+
self.assertEqual(result, ldap.RES_ADD)
705+
self.assertIsInstance(msgid, int)
706+
self.assertEqual(pmsg, [])
707+
self.assertEqual(ctrls, [])
708+
709+
# try changing password with a wrong old-pw
710+
with self.assertRaises(ldap.UNWILLING_TO_PERFORM):
711+
l.passwd_s(dn, "bogus", "ignored")
712+
713+
# have the server generate a new random pw
714+
respoid, respvalue = l.passwd_s(dn, "initial", None, extract_newpw=True)
715+
self.assertEqual(respoid, None)
716+
717+
password = respvalue.genPasswd
718+
self.assertIsInstance(password, bytes)
719+
if PY2:
720+
password = password.decode('utf-8')
721+
722+
# try changing password back
723+
respoid, respvalue = l.passwd_s(dn, password, "initial")
724+
self.assertEqual(respoid, None)
725+
self.assertEqual(respvalue, None)
726+
727+
l.delete_s(dn)
728+
690729

691730
class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
692731
"""

0 commit comments

Comments
 (0)