From e63134e3142a7e9e515c3b8e1695dbb43c56bccb Mon Sep 17 00:00:00 2001 From: Rufus Post Date: Mon, 15 Dec 2014 17:26:12 +1100 Subject: [PATCH] Support for rfc3062 Password Modify, closes #163 This implements the password modify extended request http://tools.ietf.org/html/rfc3062 --- Contributors.rdoc | 1 + lib/net/ber.rb | 1 + lib/net/ldap.rb | 53 +++++++++++++++- lib/net/ldap/connection.rb | 45 +++++++++++++ lib/net/ldap/pdu.rb | 26 +++++++- test/fixtures/openldap/slapd.conf.ldif | 2 +- test/integration/test_password_modify.rb | 80 ++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 test/integration/test_password_modify.rb 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 STRINGC29: 61 (0x3d, 0b00111101) # BMPStringP30: 30 (0x1e, 0b00011110) # BMPStringC30: 62 (0x3e, 0b00111110) + # ExtendedResponseC107: 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