Skip to content

Commit

Permalink
Merge pull request #178 from mynameisrufus/feature/rfc3062
Browse files Browse the repository at this point in the history
Support for rfc3062 Password Modify, closes #163
  • Loading branch information
jch committed Jan 8, 2016
2 parents 67d8311 + e63134e commit 987c522
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 4 deletions.
1 change: 1 addition & 0 deletions Contributors.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ Contributions since:
* David J. Lee (DavidJLee)
* Cody Cutrer (ccutrer)
* WoodsBagotAndreMarquesLee
* Rufus Post (mynameisrufus)
1 change: 1 addition & 0 deletions lib/net/ber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ module Net # :nodoc:
# <tr><th>CHARACTER STRING</th><th>C</th><td>29: 61 (0x3d, 0b00111101)</td></tr>
# <tr><th>BMPString</th><th>P</th><td>30: 30 (0x1e, 0b00011110)</td></tr>
# <tr><th>BMPString</th><th>C</th><td>30: 62 (0x3e, 0b00111110)</td></tr>
# <tr><th>ExtendedResponse</th><th>C</th><td>107: 139 (0x8b, 0b010001011)</td></tr>
# </table>
module BER
VERSION = Net::LDAP::VERSION
Expand Down
53 changes: 51 additions & 2 deletions lib/net/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions lib/net/ldap/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion lib/net/ldap/pdu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/openldap/slapd.conf.ldif
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions test/integration/test_password_modify.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 987c522

Please sign in to comment.