Skip to content

Commit e63134e

Browse files
Rufus Postmynameisrufus
Rufus Post
authored andcommitted
Support for rfc3062 Password Modify, closes #163
This implements the password modify extended request http://tools.ietf.org/html/rfc3062
1 parent 7b692fc commit e63134e

File tree

7 files changed

+204
-4
lines changed

7 files changed

+204
-4
lines changed

Diff for: Contributors.rdoc

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ Contributions since:
2222
* David J. Lee (DavidJLee)
2323
* Cody Cutrer (ccutrer)
2424
* WoodsBagotAndreMarquesLee
25+
* Rufus Post (mynameisrufus)

Diff for: lib/net/ber.rb

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ module Net # :nodoc:
106106
# <tr><th>CHARACTER STRING</th><th>C</th><td>29: 61 (0x3d, 0b00111101)</td></tr>
107107
# <tr><th>BMPString</th><th>P</th><td>30: 30 (0x1e, 0b00011110)</td></tr>
108108
# <tr><th>BMPString</th><th>C</th><td>30: 62 (0x3e, 0b00111110)</td></tr>
109+
# <tr><th>ExtendedResponse</th><th>C</th><td>107: 139 (0x8b, 0b010001011)</td></tr>
109110
# </table>
110111
module BER
111112
VERSION = Net::LDAP::VERSION

Diff for: lib/net/ldap.rb

+51-2
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,14 @@ class Net::LDAP
323323
:constructed => constructed,
324324
}
325325

326+
universal = {
327+
constructed: {
328+
107 => :array #ExtendedResponse (PasswdModifyResponseValue)
329+
}
330+
}
331+
326332
AsnSyntax = Net::BER.compile_syntax(:application => application,
333+
:universal => universal,
327334
:context_specific => context_specific)
328335

329336
DefaultHost = "127.0.0.1"
@@ -332,7 +339,8 @@ class Net::LDAP
332339
DefaultTreebase = "dc=com"
333340
DefaultForceNoPage = false
334341

335-
StartTlsOid = "1.3.6.1.4.1.1466.20037"
342+
StartTlsOid = '1.3.6.1.4.1.1466.20037'
343+
PasswdModifyOid = '1.3.6.1.4.1.4203.1.11.1'
336344

337345
# https://tools.ietf.org/html/rfc4511#section-4.1.9
338346
# https://tools.ietf.org/html/rfc4511#appendix-A
@@ -651,8 +659,11 @@ def self.open(args)
651659
#++
652660
def get_operation_result
653661
result = @result
654-
result = result.result if result.is_a?(Net::LDAP::PDU)
655662
os = OpenStruct.new
663+
if result.is_a?(Net::LDAP::PDU)
664+
os.extended_response = result.extended_response
665+
result = result.result
666+
end
656667
if result.is_a?(Hash)
657668
# We might get a hash of LDAP response codes instead of a simple
658669
# numeric code.
@@ -1041,6 +1052,44 @@ def modify(args)
10411052
end
10421053
end
10431054

1055+
# Password Modify
1056+
#
1057+
# Change existing password:
1058+
#
1059+
# dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
1060+
# auth = {
1061+
# method: :simple,
1062+
# username: dn,
1063+
# password: 'passworD1'
1064+
# }
1065+
# ldap.password_modify(dn: dn,
1066+
# auth: auth,
1067+
# old_password: 'passworD1',
1068+
# new_password: 'passworD2')
1069+
#
1070+
# Or get the LDAP server to generate a password for you:
1071+
#
1072+
# dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
1073+
# auth = {
1074+
# method: :simple,
1075+
# username: dn,
1076+
# password: 'passworD1'
1077+
# }
1078+
# ldap.password_modify(dn: dn,
1079+
# auth: auth,
1080+
# old_password: 'passworD1')
1081+
#
1082+
# ldap.get_operation_result.extended_response[0][0] #=> 'VtcgGf/G'
1083+
#
1084+
def password_modify(args)
1085+
instrument "modify_password.net_ldap", args do |payload|
1086+
@result = use_connection(args) do |conn|
1087+
conn.password_modify(args)
1088+
end
1089+
@result.success?
1090+
end
1091+
end
1092+
10441093
# Add a value to an attribute. Takes the full DN of the entry to modify,
10451094
# the name (Symbol or String) of the attribute, and the value (String or
10461095
# Array). If the attribute does not exist (and there are no schema

Diff for: lib/net/ldap/connection.rb

+45
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,51 @@ def modify(args)
539539
pdu
540540
end
541541

542+
##
543+
# Password Modify
544+
#
545+
# http://tools.ietf.org/html/rfc3062
546+
#
547+
# passwdModifyOID OBJECT IDENTIFIER ::= 1.3.6.1.4.1.4203.1.11.1
548+
#
549+
# PasswdModifyRequestValue ::= SEQUENCE {
550+
# userIdentity [0] OCTET STRING OPTIONAL
551+
# oldPasswd [1] OCTET STRING OPTIONAL
552+
# newPasswd [2] OCTET STRING OPTIONAL }
553+
#
554+
# PasswdModifyResponseValue ::= SEQUENCE {
555+
# genPasswd [0] OCTET STRING OPTIONAL }
556+
#
557+
# Encoded request:
558+
#
559+
# 00\x02\x01\x02w+\x80\x171.3.6.1.4.1.4203.1.11.1\x81\x100\x0E\x81\x05old\x82\x05new
560+
#
561+
def password_modify(args)
562+
dn = args[:dn]
563+
raise ArgumentError, 'DN is required' if !dn || dn.empty?
564+
565+
ext_seq = [Net::LDAP::PasswdModifyOid.to_ber_contextspecific(0)]
566+
567+
unless args[:old_password].nil?
568+
pwd_seq = [args[:old_password].to_ber(0x81)]
569+
pwd_seq << args[:new_password].to_ber(0x82) unless args[:new_password].nil?
570+
ext_seq << pwd_seq.to_ber_sequence.to_ber(0x81)
571+
end
572+
573+
request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
574+
575+
message_id = next_msgid
576+
577+
write(request, nil, message_id)
578+
pdu = queued_read(message_id)
579+
580+
if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
581+
raise Net::LDAP::ResponseMissingError, "response missing or invalid"
582+
end
583+
584+
pdu
585+
end
586+
542587
#--
543588
# TODO: need to support a time limit, in case the server fails to respond.
544589
# Unlike other operation-methods in this class, we return a result hash

Diff for: lib/net/ldap/pdu.rb

+25-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class Error < RuntimeError; end
7474
attr_reader :search_referrals
7575
attr_reader :search_parameters
7676
attr_reader :bind_parameters
77+
attr_reader :extended_response
7778

7879
##
7980
# Returns RFC-2251 Controls if any.
@@ -120,7 +121,7 @@ def initialize(ber_object)
120121
when UnbindRequest
121122
parse_unbind_request(ber_object[1])
122123
when ExtendedResponse
123-
parse_ldap_result(ber_object[1])
124+
parse_extended_response(ber_object[1])
124125
else
125126
raise LdapPduError.new("unknown pdu-type: #{@app_tag}")
126127
end
@@ -180,6 +181,29 @@ def parse_ldap_result(sequence)
180181
end
181182
private :parse_ldap_result
182183

184+
##
185+
# Parse an extended response
186+
#
187+
# http://www.ietf.org/rfc/rfc2251.txt
188+
#
189+
# Each Extended operation consists of an Extended request and an
190+
# Extended response.
191+
#
192+
# ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
193+
# requestName [0] LDAPOID,
194+
# requestValue [1] OCTET STRING OPTIONAL }
195+
196+
def parse_extended_response(sequence)
197+
sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."
198+
@ldap_result = {
199+
:resultCode => sequence[0],
200+
:matchedDN => sequence[1],
201+
:errorMessage => sequence[2]
202+
}
203+
@extended_response = sequence[3]
204+
end
205+
private :parse_extended_response
206+
183207
##
184208
# A Bind Response may have an additional field, ID [7], serverSaslCreds,
185209
# per RFC 2251 pgh 4.2.3.

Diff for: test/fixtures/openldap/slapd.conf.ldif

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ objectClass: olcGlobal
33
cn: config
44
olcPidFile: /var/run/slapd/slapd.pid
55
olcArgsFile: /var/run/slapd/slapd.args
6-
olcLogLevel: none
6+
olcLogLevel: -1
77
olcToolThreads: 1
88

99
dn: olcDatabase={-1}frontend,cn=config

Diff for: test/integration/test_password_modify.rb

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
require_relative '../test_helper'
2+
3+
class TestPasswordModifyIntegration < LDAPIntegrationTestCase
4+
def setup
5+
super
6+
@ldap.authenticate 'cn=admin,dc=rubyldap,dc=com', 'passworD1'
7+
8+
@dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
9+
10+
attrs = {
11+
objectclass: %w(top inetOrgPerson organizationalPerson person),
12+
uid: 'modify-password-user1',
13+
cn: 'modify-password-user1',
14+
sn: 'modify-password-user1',
15+
16+
userPassword: 'passworD1'
17+
}
18+
unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
19+
assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect
20+
end
21+
assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
22+
23+
@auth = {
24+
method: :simple,
25+
username: @dn,
26+
password: 'passworD1'
27+
}
28+
end
29+
30+
def test_password_modify
31+
assert @ldap.password_modify(dn: @dn,
32+
auth: @auth,
33+
old_password: 'passworD1',
34+
new_password: 'passworD2')
35+
36+
assert @ldap.get_operation_result.extended_response.nil?,
37+
'Should not have generated a new password'
38+
39+
refute @ldap.bind(username: @dn, password: 'passworD1', method: :simple),
40+
'Old password should no longer be valid'
41+
42+
assert @ldap.bind(username: @dn, password: 'passworD2', method: :simple),
43+
'New password should be valid'
44+
end
45+
46+
def test_password_modify_generate
47+
assert @ldap.password_modify(dn: @dn,
48+
auth: @auth,
49+
old_password: 'passworD1')
50+
51+
generated_password = @ldap.get_operation_result.extended_response[0][0]
52+
53+
assert generated_password, 'Should have generated a password'
54+
55+
refute @ldap.bind(username: @dn, password: 'passworD1', method: :simple),
56+
'Old password should no longer be valid'
57+
58+
assert @ldap.bind(username: @dn, password: generated_password, method: :simple),
59+
'New password should be valid'
60+
end
61+
62+
def test_password_modify_generate_no_old_password
63+
assert @ldap.password_modify(dn: @dn,
64+
auth: @auth)
65+
66+
generated_password = @ldap.get_operation_result.extended_response[0][0]
67+
68+
assert generated_password, 'Should have generated a password'
69+
70+
refute @ldap.bind(username: @dn, password: 'passworD1', method: :simple),
71+
'Old password should no longer be valid'
72+
73+
assert @ldap.bind(username: @dn, password: generated_password, method: :simple),
74+
'New password should be valid'
75+
end
76+
77+
def teardown
78+
@ldap.delete dn: @dn
79+
end
80+
end

0 commit comments

Comments
 (0)