diff --git a/Contributors.rdoc b/Contributors.rdoc
index e40b20db..137394f8 100644
--- a/Contributors.rdoc
+++ b/Contributors.rdoc
@@ -22,3 +22,4 @@ Contributions since:
* David J. Lee (DavidJLee)
* Cody Cutrer (ccutrer)
* WoodsBagotAndreMarquesLee
+* Rufus Post (mynameisrufus)
diff --git a/lib/net/ber.rb b/lib/net/ber.rb
index c34de6ba..3bc7a2ba 100644
--- a/lib/net/ber.rb
+++ b/lib/net/ber.rb
@@ -106,6 +106,7 @@ module Net # :nodoc:
#
CHARACTER STRING | C | 29: 61 (0x3d, 0b00111101) |
# BMPString | P | 30: 30 (0x1e, 0b00011110) |
# BMPString | C | 30: 62 (0x3e, 0b00111110) |
+ # ExtendedResponse | C | 107: 139 (0x8b, 0b010001011) |
#
module BER
VERSION = Net::LDAP::VERSION
diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb
index 27fd56a7..455bbd6e 100644
--- a/lib/net/ldap.rb
+++ b/lib/net/ldap.rb
@@ -323,7 +323,14 @@ class Net::LDAP
:constructed => constructed,
}
+ universal = {
+ constructed: {
+ 107 => :array #ExtendedResponse (PasswdModifyResponseValue)
+ }
+ }
+
AsnSyntax = Net::BER.compile_syntax(:application => application,
+ :universal => universal,
:context_specific => context_specific)
DefaultHost = "127.0.0.1"
@@ -332,7 +339,8 @@ class Net::LDAP
DefaultTreebase = "dc=com"
DefaultForceNoPage = false
- StartTlsOid = "1.3.6.1.4.1.1466.20037"
+ StartTlsOid = '1.3.6.1.4.1.1466.20037'
+ PasswdModifyOid = '1.3.6.1.4.1.4203.1.11.1'
# https://tools.ietf.org/html/rfc4511#section-4.1.9
# https://tools.ietf.org/html/rfc4511#appendix-A
@@ -651,8 +659,11 @@ def self.open(args)
#++
def get_operation_result
result = @result
- result = result.result if result.is_a?(Net::LDAP::PDU)
os = OpenStruct.new
+ if result.is_a?(Net::LDAP::PDU)
+ os.extended_response = result.extended_response
+ result = result.result
+ end
if result.is_a?(Hash)
# We might get a hash of LDAP response codes instead of a simple
# numeric code.
@@ -1041,6 +1052,44 @@ def modify(args)
end
end
+ # Password Modify
+ #
+ # Change existing password:
+ #
+ # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
+ # auth = {
+ # method: :simple,
+ # username: dn,
+ # password: 'passworD1'
+ # }
+ # ldap.password_modify(dn: dn,
+ # auth: auth,
+ # old_password: 'passworD1',
+ # new_password: 'passworD2')
+ #
+ # Or get the LDAP server to generate a password for you:
+ #
+ # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
+ # auth = {
+ # method: :simple,
+ # username: dn,
+ # password: 'passworD1'
+ # }
+ # ldap.password_modify(dn: dn,
+ # auth: auth,
+ # old_password: 'passworD1')
+ #
+ # ldap.get_operation_result.extended_response[0][0] #=> 'VtcgGf/G'
+ #
+ def password_modify(args)
+ instrument "modify_password.net_ldap", args do |payload|
+ @result = use_connection(args) do |conn|
+ conn.password_modify(args)
+ end
+ @result.success?
+ end
+ end
+
# Add a value to an attribute. Takes the full DN of the entry to modify,
# the name (Symbol or String) of the attribute, and the value (String or
# Array). If the attribute does not exist (and there are no schema
diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb
index e23972c4..67757323 100644
--- a/lib/net/ldap/connection.rb
+++ b/lib/net/ldap/connection.rb
@@ -539,6 +539,51 @@ def modify(args)
pdu
end
+ ##
+ # Password Modify
+ #
+ # http://tools.ietf.org/html/rfc3062
+ #
+ # passwdModifyOID OBJECT IDENTIFIER ::= 1.3.6.1.4.1.4203.1.11.1
+ #
+ # PasswdModifyRequestValue ::= SEQUENCE {
+ # userIdentity [0] OCTET STRING OPTIONAL
+ # oldPasswd [1] OCTET STRING OPTIONAL
+ # newPasswd [2] OCTET STRING OPTIONAL }
+ #
+ # PasswdModifyResponseValue ::= SEQUENCE {
+ # genPasswd [0] OCTET STRING OPTIONAL }
+ #
+ # Encoded request:
+ #
+ # 00\x02\x01\x02w+\x80\x171.3.6.1.4.1.4203.1.11.1\x81\x100\x0E\x81\x05old\x82\x05new
+ #
+ def password_modify(args)
+ dn = args[:dn]
+ raise ArgumentError, 'DN is required' if !dn || dn.empty?
+
+ ext_seq = [Net::LDAP::PasswdModifyOid.to_ber_contextspecific(0)]
+
+ unless args[:old_password].nil?
+ pwd_seq = [args[:old_password].to_ber(0x81)]
+ pwd_seq << args[:new_password].to_ber(0x82) unless args[:new_password].nil?
+ ext_seq << pwd_seq.to_ber_sequence.to_ber(0x81)
+ end
+
+ request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
+
+ message_id = next_msgid
+
+ write(request, nil, message_id)
+ pdu = queued_read(message_id)
+
+ if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
+ raise Net::LDAP::ResponseMissingError, "response missing or invalid"
+ end
+
+ pdu
+ end
+
#--
# TODO: need to support a time limit, in case the server fails to respond.
# Unlike other operation-methods in this class, we return a result hash
diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb
index f749f669..5527c1df 100644
--- a/lib/net/ldap/pdu.rb
+++ b/lib/net/ldap/pdu.rb
@@ -74,6 +74,7 @@ class Error < RuntimeError; end
attr_reader :search_referrals
attr_reader :search_parameters
attr_reader :bind_parameters
+ attr_reader :extended_response
##
# Returns RFC-2251 Controls if any.
@@ -120,7 +121,7 @@ def initialize(ber_object)
when UnbindRequest
parse_unbind_request(ber_object[1])
when ExtendedResponse
- parse_ldap_result(ber_object[1])
+ parse_extended_response(ber_object[1])
else
raise LdapPduError.new("unknown pdu-type: #{@app_tag}")
end
@@ -180,6 +181,29 @@ def parse_ldap_result(sequence)
end
private :parse_ldap_result
+ ##
+ # Parse an extended response
+ #
+ # http://www.ietf.org/rfc/rfc2251.txt
+ #
+ # Each Extended operation consists of an Extended request and an
+ # Extended response.
+ #
+ # ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
+ # requestName [0] LDAPOID,
+ # requestValue [1] OCTET STRING OPTIONAL }
+
+ def parse_extended_response(sequence)
+ sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."
+ @ldap_result = {
+ :resultCode => sequence[0],
+ :matchedDN => sequence[1],
+ :errorMessage => sequence[2]
+ }
+ @extended_response = sequence[3]
+ end
+ private :parse_extended_response
+
##
# A Bind Response may have an additional field, ID [7], serverSaslCreds,
# per RFC 2251 pgh 4.2.3.
diff --git a/test/fixtures/openldap/slapd.conf.ldif b/test/fixtures/openldap/slapd.conf.ldif
index 6ba5cf77..77a6af09 100644
--- a/test/fixtures/openldap/slapd.conf.ldif
+++ b/test/fixtures/openldap/slapd.conf.ldif
@@ -3,7 +3,7 @@ objectClass: olcGlobal
cn: config
olcPidFile: /var/run/slapd/slapd.pid
olcArgsFile: /var/run/slapd/slapd.args
-olcLogLevel: none
+olcLogLevel: -1
olcToolThreads: 1
dn: olcDatabase={-1}frontend,cn=config
diff --git a/test/integration/test_password_modify.rb b/test/integration/test_password_modify.rb
new file mode 100644
index 00000000..12583363
--- /dev/null
+++ b/test/integration/test_password_modify.rb
@@ -0,0 +1,80 @@
+require_relative '../test_helper'
+
+class TestPasswordModifyIntegration < LDAPIntegrationTestCase
+ def setup
+ super
+ @ldap.authenticate 'cn=admin,dc=rubyldap,dc=com', 'passworD1'
+
+ @dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
+
+ attrs = {
+ objectclass: %w(top inetOrgPerson organizationalPerson person),
+ uid: 'modify-password-user1',
+ cn: 'modify-password-user1',
+ sn: 'modify-password-user1',
+ mail: 'modify-password-user1@rubyldap.com',
+ userPassword: 'passworD1'
+ }
+ unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
+ assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect
+ end
+ assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
+
+ @auth = {
+ method: :simple,
+ username: @dn,
+ password: 'passworD1'
+ }
+ end
+
+ def test_password_modify
+ assert @ldap.password_modify(dn: @dn,
+ auth: @auth,
+ old_password: 'passworD1',
+ new_password: 'passworD2')
+
+ assert @ldap.get_operation_result.extended_response.nil?,
+ 'Should not have generated a new password'
+
+ refute @ldap.bind(username: @dn, password: 'passworD1', method: :simple),
+ 'Old password should no longer be valid'
+
+ assert @ldap.bind(username: @dn, password: 'passworD2', method: :simple),
+ 'New password should be valid'
+ end
+
+ def test_password_modify_generate
+ assert @ldap.password_modify(dn: @dn,
+ auth: @auth,
+ old_password: 'passworD1')
+
+ generated_password = @ldap.get_operation_result.extended_response[0][0]
+
+ assert generated_password, 'Should have generated a password'
+
+ refute @ldap.bind(username: @dn, password: 'passworD1', method: :simple),
+ 'Old password should no longer be valid'
+
+ assert @ldap.bind(username: @dn, password: generated_password, method: :simple),
+ 'New password should be valid'
+ end
+
+ def test_password_modify_generate_no_old_password
+ assert @ldap.password_modify(dn: @dn,
+ auth: @auth)
+
+ generated_password = @ldap.get_operation_result.extended_response[0][0]
+
+ assert generated_password, 'Should have generated a password'
+
+ refute @ldap.bind(username: @dn, password: 'passworD1', method: :simple),
+ 'Old password should no longer be valid'
+
+ assert @ldap.bind(username: @dn, password: generated_password, method: :simple),
+ 'New password should be valid'
+ end
+
+ def teardown
+ @ldap.delete dn: @dn
+ end
+end