Cleanup of Dataset and Entry.

master
Austin Ziegler 2010-03-27 14:49:14 -04:00
parent 20f494a875
commit 06ea324055
4 changed files with 343 additions and 319 deletions

View File

@ -1041,7 +1041,7 @@ class Net::LDAP
:attributes => [ :namingContexts, :supportedLdapVersion,
:altServer, :supportedControl, :supportedExtension,
:supportedFeatures, :supportedSASLMechanisms])
(rs and rs.first) or Entry.new
(rs and rs.first) or Net::LDAP::Entry.new
end
# Return the root Subschema record from the LDAP server as a
@ -1072,16 +1072,16 @@ class Net::LDAP
rs = search(:ignore_server_caps => true, :base => "",
:scope => SearchScope_BaseObject,
:attributes => [:subschemaSubentry])
return Entry.new unless (rs and rs.first)
return Net::LDAP::Entry.new unless (rs and rs.first)
subschema_name = rs.first.subschemasubentry
return Entry.new unless (subschema_name and subschema_name.first)
return Net::LDAP::Entry.new unless (subschema_name and subschema_name.first)
rs = search(:ignore_server_caps => true, :base => subschema_name.first,
:scope => SearchScope_BaseObject,
:filter => "objectclass=subschema",
:attributes => [:objectclasses, :attributetypes])
(rs and rs.first) or Entry.new
(rs and rs.first) or Net::LDAP::Entry.new
end
#--

View File

@ -20,80 +20,155 @@
#
#---------------------------------------------------------------------------
module Net
class LDAP
class Dataset < Hash
attr_reader :comments
##
# An LDAP Dataset. Used primarily as an intermediate format for converting
# to and from LDIF strings and Net::LDAP::Entry objects.
class Net::LDAP::Dataset < Hash
##
# Dataset object comments.
attr_reader :comments
class IOFilter
def initialize(io)
@io = io
end
def gets
s = @io.gets
s.chomp if s
class << self
class ChompedIO #:nodoc:
def initialize(io)
@io = io
end
def gets
s = @io.gets
s.chomp if s
end
end
##
# Reads an object that returns data line-wise (using #gets) and parses
# LDIF data into a Dataset object.
def read_ldif(io) #:yields: entry-type, value Used mostly for debugging.
ds = Net::LDAP::Dataset.new
io = ChompedIO.new(io)
line = io.gets
dn = nil
while line
new_line = io.gets
if new_line =~ /^[\s]+/
line << " " << $'
else
nextline = new_line
if line =~ /^#/
ds.comments << line
yield :comment, line if block_given?
elsif line =~ /^dn:[\s]*/i
dn = $'
ds[dn] = Hash.new { |k,v| k[v] = [] }
yield :dn, dn if block_given?
elsif line.empty?
dn = nil
yield :end, nil if block_given?
elsif line =~ /^([^:]+):([\:]?)[\s]*/
# $1 is the attribute name
# $2 is a colon iff the attr-value is base-64 encoded
# $' is the attr-value
# Avoid the Base64 class because not all Ruby versions have it.
attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
ds[dn][$1.downcase.to_sym] << attrvalue
yield :attr, [$1.downcase.to_sym, attrvalue] if block_given?
end
line = nextline
end
end
def self.read_ldif io
ds = Dataset.new
io = IOFilter.new(io)
ds
end
line = io.gets
dn = nil
##
# Creates a Dataset object from an Entry object. Used mostly to assist
# with the conversion of
def from_entry(entry)
dataset = Net::LDAP::Dataset.new
hash = { }
entry.each_attribute do |attribute, value|
next if attribute == :dn
hash[attribute] = value
end
dataset[entry.dn] = hash
dataset
end
end
while line
new_line = io.gets
if new_line =~ /^[\s]+/
line << " " << $'
def initialize(*args, &block) #:nodoc:
super
@comments = []
end
##
# Outputs an LDAP Dataset as an array of strings representing LDIF
# entries.
def to_ldif
ary = []
ary += @comments unless @comments.empty?
keys.sort.each do |dn|
ary << "dn: #{dn}"
attributes = self[dn].keys.map { |attr| attr.to_s }.sort
attributes.each do |attr|
self[dn][attr.to_sym].each do |value|
if attr == "userpassword" or value_is_binary?(value)
value = [value].pack("m").chomp.gsub(/\n/m, "\n ")
ary << "#{attr}:: #{value}"
else
nextline = new_line
if line =~ /^\#/
ds.comments << line
elsif line =~ /^dn:[\s]*/i
dn = $'
ds[dn] = Hash.new {|k,v| k[v] = []}
elsif line.length == 0
dn = nil
elsif line =~ /^([^:]+):([\:]?)[\s]*/
# $1 is the attribute name
# $2 is a colon iff the attr-value is base-64 encoded
# $' is the attr-value
# Avoid the Base64 class because not all Ruby versions have it.
attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
ds[dn][$1.downcase.intern] << attrvalue
end
line = nextline
ary << "#{attr}: #{value}"
end
end
ds
end
ary << ""
end
block_given? and ary.each { |line| yield line}
def initialize
@comments = []
ary
end
##
# Outputs an LDAP Dataset as an LDIF string.
def to_ldif_string
to_ldif.join("\n")
end
##
# Convert the parsed LDIF objects to Net::LDAP::Entry objects.
def to_entries
ary = []
keys.each do |dn|
entry = Net::LDAP::Entry.new(dn)
self[dn].each do |attr, value|
entry[attr] = value
end
ary << entry
end
ary
end
def to_ldif
ary = []
ary += (@comments || [])
keys.sort.each do |dn|
ary << "dn: #{dn}"
self[dn].keys.map {|sym| sym.to_s}.sort.each do |attr|
self[dn][attr.intern].each {|val| ary << "#{attr}: #{val}" }
end
ary << ""
end
block_given? and ary.each {|line| yield line}
ary
end
end
end
# This is an internal convenience method to determine if a value requires
# base64-encoding before conversion to LDIF output. The standard approach
# in most LDAP tools is to check whether the value is a password, or if
# the first or last bytes are non-printable. Microsoft Active Directory,
# on the other hand, sometimes sends values that are binary in the middle.
#
# In the worst cases, this could be a nasty performance killer, which is
# why we handle the simplest cases first. Ideally, we would also test the
# first/last byte, but it's a bit harder to do this in a way that's
# compatible with both 1.8.6 and 1.8.7.
def value_is_binary?(value)
value = value.to_s
return true if value[0] == ?: or value[0] == ?<
value.each_byte { |byte| return true if (byte < 32) || (byte > 126) }
false
end
private :value_is_binary?
end
require 'net/ldap/entry' unless defined? Net::LDAP::Entry

View File

@ -21,246 +21,188 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
#---------------------------------------------------------------------------
##
# Objects of this class represent individual entries in an LDAP directory.
# User code generally does not instantiate this class. Net::LDAP#search
# provides objects of this class to user code, either as block parameters or
# as return values.
#
# In LDAP-land, an "entry" is a collection of attributes that are uniquely
# and globally identified by a DN ("Distinguished Name"). Attributes are
# identified by short, descriptive words or phrases. Although a directory is
# free to implement any attribute name, most of them follow rigorous
# standards so that the range of commonly-encountered attribute names is not
# large.
#
# An attribute name is case-insensitive. Most directories also restrict the
# range of characters allowed in attribute names. To simplify handling
# attribute names, Net::LDAP::Entry internally converts them to a standard
# format. Therefore, the methods which take attribute names can take Strings
# or Symbols, and work correctly regardless of case or capitalization.
#
# An attribute consists of zero or more data items called <i>values.</i> An
# entry is the combination of a unique DN, a set of attribute names, and a
# (possibly-empty) array of values for each attribute.
#
# Class Net::LDAP::Entry provides convenience methods for dealing with LDAP
# entries. In addition to the methods documented below, you may access
# individual attributes of an entry simply by giving the attribute name as
# the name of a method call. For example:
#
# ldap.search( ... ) do |entry|
# puts "Common name: #{entry.cn}"
# puts "Email addresses:"
# entry.mail.each {|ma| puts ma}
# end
#
# If you use this technique to access an attribute that is not present in a
# particular Entry object, a NoMethodError exception will be raised.
#
#--
# Ugly problem to fix someday: We key off the internal hash with a canonical
# form of the attribute name: convert to a string, downcase, then take the
# symbol. Unfortunately we do this in at least three places. Should do it in
# ONE place.
class Net::LDAP::Entry
##
# This constructor is not generally called by user code.
def initialize(dn = nil) #:nodoc:
@myhash = {}
@myhash[:dn] = [dn]
end
module Net
class LDAP
##
# Use the LDIF format for Marshal serialization.
def _dump(depth) #:nodoc:
to_ldif
end
##
# Use the LDIF format for Marshal serialization.
def self._load(entry) #:nodoc:
from_single_ldif_string(entry)
end
# Objects of this class represent individual entries in an LDAP directory.
# User code generally does not instantiate this class. Net::LDAP#search
# provides objects of this class to user code, either as block parameters or
# as return values.
class << self
##
# Converts a single LDIF entry string into an Entry object. Useful for
# Marshal serialization. If a string with multiple LDIF entries is
# provided, an exception will be raised.
def from_single_ldif_string(ldif)
ds = Net::LDAP::Dataset.read_ldif(::StringIO.new(ldif))
return nil if ds.empty?
raise Net::LDAP::LdapError, "Too many LDIF entries" unless ds.size == 1
entry = ds.to_entries.first
return nil if entry.dn.nil?
entry
end
##
# Canonicalizes an LDAP attribute name as a \Symbol. The name is
# lowercased and, if present, a trailing equals sign is removed.
def attribute_name(name)
name = name.to_s.downcase
name = name[0..-2] if name[-1] == ?=
name.to_sym
end
end
##
# Sets or replaces the array of values for the provided attribute. The
# attribute name is canonicalized prior to assignment.
#
# In LDAP-land, an "entry" is a collection of attributes that are uniquely
# and globally identified by a DN ("Distinguished Name"). Attributes are
# identified by short, descriptive words or phrases. Although a directory is
# free to implement any attribute name, most of them follow rigorous
# standards so that the range of commonly-encountered attribute names is not
# large.
# When an attribute is set using this, that attribute is now made
# accessible through methods as well.
#
# An attribute name is case-insensitive. Most directories also restrict the
# range of characters allowed in attribute names. To simplify handling
# attribute names, Net::LDAP::Entry internally converts them to a standard
# format. Therefore, the methods which take attribute names can take Strings
# or Symbols, and work correctly regardless of case or capitalization.
# entry = Net::LDAP::Entry.new("dc=com")
# entry.foo # => NoMethodError
# entry["foo"] = 12345 # => [12345]
# entry.foo # => [12345]
def []=(name, value)
@myhash[self.class.attribute_name(name)] = Kernel::Array(value)
end
##
# Reads the array of values for the provided attribute. The attribute name
# is canonicalized prior to reading. Returns an empty array if the
# attribute does not exist.
def [](name)
name = self.class.attribute_name(name)
@myhash[name] || []
end
##
# Returns the first distinguished name (dn) of the Entry as a \String.
def dn
self[:dn].first.to_s
end
##
# Returns an array of the attribute names present in the Entry.
def attribute_names
@myhash.keys
end
##
# Accesses each of the attributes present in the Entry.
#
# An attribute consists of zero or more data items called <i>values.</i> An
# entry is the combination of a unique DN, a set of attribute names, and a
# (possibly-empty) array of values for each attribute.
#
# Class Net::LDAP::Entry provides convenience methods for dealing with LDAP
# entries. In addition to the methods documented below, you may access
# individual attributes of an entry simply by giving the attribute name as
# the name of a method call. For example:
#
# ldap.search( ... ) do |entry|
# puts "Common name: #{entry.cn}"
# puts "Email addresses:"
# entry.mail.each {|ma| puts ma}
# end
#
# If you use this technique to access an attribute that is not present in a
# particular Entry object, a NoMethodError exception will be raised.
#
#--
# Ugly problem to fix someday: We key off the internal hash with a canonical
# form of the attribute name: convert to a string, downcase, then take the
# symbol. Unfortunately we do this in at least three places. Should do it in
# ONE place.
#
class Entry
# This constructor is not generally called by user code.
#
def initialize dn = nil # :nodoc:
@myhash = {}
@myhash[:dn] = [dn]
end
def _dump depth
to_ldif
end
class << self
def _load entry
from_single_ldif_string entry
end
end
#--
# Discovered bug, 26Aug06: I noticed that we're not converting the
# incoming value to an array if it isn't already one.
def []=(name, value) # :nodoc:
sym = attribute_name(name)
value = [value] unless value.is_a?(Array)
@myhash[sym] = value
end
#--
# We have to deal with this one as we do with []= because this one and not
# the other one gets called in formulations like entry["CN"] << cn.
#
def [](name) # :nodoc:
name = attribute_name(name) unless name.is_a?(Symbol)
@myhash[name] || []
end
# Returns the dn of the Entry as a String.
def dn
self[:dn][0].to_s
end
# Returns an array of the attribute names present in the Entry.
def attribute_names
@myhash.keys
end
# Accesses each of the attributes present in the Entry.
# Calls a user-supplied block with each attribute in turn,
# passing two arguments to the block: a Symbol giving
# the name of the attribute, and a (possibly empty)
# Array of data values.
#
def each
if block_given?
attribute_names.each {|a|
attr_name,values = a,self[a]
yield attr_name, values
}
end
end
alias_method :each_attribute, :each
# Converts the Entry to a String, representing the
# Entry's attributes in LDIF format.
#--
def to_ldif
ary = []
ary << "dn: #{dn}\n"
v2 = "" # temp value, save on GC
each_attribute do |k,v|
unless k == :dn
v.each {|v1|
v2 = if (k == :userpassword) || is_attribute_value_binary?(v1)
": #{Base64.encode64(v1).chomp.gsub(/\n/m,"\n ")}"
else
" #{v1}"
end
ary << "#{k}:#{v2}\n"
}
end
end
ary << "\n"
ary.join
end
#--
# TODO, doesn't support broken lines.
# It generates a SINGLE Entry object from an incoming LDIF stream which is
# of course useless for big LDIF streams that encode many objects.
#
# DO NOT DOCUMENT THIS METHOD UNTIL THESE RESTRICTIONS ARE LIFTED.
#
# As it is, it's useful for unmarshalling objects that we create, but not
# for reading arbitrary LDIF files. Eventually, we should have a class
# method that parses large LDIF streams into individual LDIF blocks
# (delimited by blank lines) and passes them here.
#
class << self
def from_single_ldif_string ldif
entry = Entry.new
entry[:dn] = []
ldif.split(/\r?\n/m).each {|line|
break if line.length == 0
if line =~ /\A([\w]+):(:?)[\s]*/
entry[$1] <<= if $2 == ':'
Base64.decode64($')
else
$'
end
end
}
entry.dn ? entry : nil
end
end
#--
# Part of the support for getter and setter style access to attributes.
#
def respond_to?(sym)
name = attribute_name(sym)
return true if valid_attribute?(name)
return super
end
#--
# Supports getter and setter style access for all the attributes that this
# entry holds.
#
def method_missing sym, *args, &block # :nodoc:
name = attribute_name(sym)
if valid_attribute? name
if setter?(sym) && args.size == 1
value = args.first
value = [value] unless value.instance_of?(Array)
self[name]= value
return value
elsif args.empty?
return self[name]
end
end
super
end
def write
end
private
#--
# Internal convenience method. It seems like the standard
# approach in most LDAP tools to base64 encode an attribute
# value if its first or last byte is nonprintable, or if
# it's a password. But that turns out to be not nearly good
# enough. There are plenty of A/D attributes that are binary
# in the middle. This is probably a nasty performance killer.
def is_attribute_value_binary? value
v = value.to_s
v.each_byte {|byt|
return true if (byt < 32) || (byt > 126)
# Calls a user-supplied block with each attribute in turn, passing two
# arguments to the block: a Symbol giving the name of the attribute, and a
# (possibly empty) \Array of data values.
def each # :yields: attribute-name, data-values-array
if block_given?
attribute_names.each {|a|
attr_name,values = a,self[a]
yield attr_name, values
}
if v[0..0] == ':' or v[0..0] == '<'
return true
end
end
alias_method :each_attribute, :each
##
# Converts the Entry to an LDIF-formatted String
def to_ldif
Net::LDAP::Dataset.from_entry(self).to_ldif_string
end
def respond_to?(sym) #:nodoc:
return true if valid_attribute?(self.class.attribute_name(sym))
return super
end
def method_missing(sym, *args, &block) #:nodoc:
name = self.class.attribute_name(sym)
if valid_attribute?(name )
if setter?(sym) && args.size == 1
value = args.first
value = Array(value)
self[name]= value
return value
elsif args.empty?
return self[name]
end
false
end
# Returns the symbol that can be used to access the attribute that
# sym_or_str designates.
#
def attribute_name(sym_or_str)
str = sym_or_str.to_s.downcase
# Does str match 'something='? Still only returns :something
return str[0...-1].to_sym if str.size>1 && str[-1] == ?=
return str.to_sym
end
# Given a valid attribute symbol, returns true.
#
def valid_attribute?(attr_name)
attribute_names.include?(attr_name)
end
def setter?(sym)
sym.to_s[-1] == ?=
end
end # class Entry
super
end
end # class LDAP
end # module Net
# Given a valid attribute symbol, returns true.
def valid_attribute?(attr_name)
attribute_names.include?(attr_name)
end
private :valid_attribute?
# Returns true if the symbol ends with an equal sign.
def setter?(sym)
sym.to_s[-1] == ?=
end
private :setter?
end # class Entry
require 'net/ldap/dataset' unless defined? Net::LDAP::Dataset

View File

@ -7,53 +7,60 @@ require 'digest/sha1'
require 'base64'
class TestLdif < Test::Unit::TestCase
TestLdifFilename = "#{File.dirname(__FILE__)}/testdata.ldif"
def test_empty_ldif
ds = Net::LDAP::Dataset.read_ldif( StringIO.new )
assert_equal( true, ds.empty? )
ds = Net::LDAP::Dataset.read_ldif(StringIO.new)
assert_equal(true, ds.empty?)
end
def test_ldif_with_comments
str = ["# Hello from LDIF-land", "# This is an unterminated comment"]
io = StringIO.new( str[0] + "\r\n" + str[1] )
ds = Net::LDAP::Dataset::read_ldif( io )
assert_equal( str, ds.comments )
io = StringIO.new(str[0] + "\r\n" + str[1])
ds = Net::LDAP::Dataset::read_ldif(io)
assert_equal(str, ds.comments)
end
def test_ldif_with_password
psw = "goldbricks"
hashed_psw = "{SHA}" + Base64::encode64(Digest::SHA1.digest(psw)).chomp
ldif_encoded = Base64::encode64( hashed_psw ).chomp
ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n" ))
ldif_encoded = Base64::encode64(hashed_psw).chomp
ds = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n"))
recovered_psw = ds["Goldbrick"][:userpassword].shift
assert_equal( hashed_psw, recovered_psw )
assert_equal(hashed_psw, recovered_psw)
end
def test_ldif_with_continuation_lines
ds = Net::LDAP::Dataset::read_ldif( StringIO.new( "dn: abcdefg\r\n hijklmn\r\n\r\n" ))
assert_equal( true, ds.has_key?( "abcdefg hijklmn" ))
ds = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n"))
assert_equal(true, ds.has_key?("abcdefg hijklmn"))
end
# TODO, INADEQUATE. We need some more tests
# to verify the content.
def test_ldif
File.open( TestLdifFilename, "r" ) {|f|
ds = Net::LDAP::Dataset::read_ldif( f )
assert_equal( 13, ds.length )
File.open(TestLdifFilename, "r") {|f|
ds = Net::LDAP::Dataset::read_ldif(f)
assert_equal(13, ds.length)
}
end
# TODO, need some tests.
# Must test folded lines and base64-encoded lines as well as normal ones.
#def test_to_ldif
# File.open( TestLdifFilename, "r" ) {|f|
# ds = Net::LDAP::Dataset::read_ldif( f )
# ds.to_ldif
# assert_equal( true, false ) # REMOVE WHEN WE HAVE SOME TESTS HERE.
# }
#end
def test_to_ldif
data = File.open(TestLdifFilename, "rb") { |f| f.read }
io = StringIO.new(data)
entries = data.grep(/^dn:\s*/) { $'.chomp }
dn_entries = entries.dup
ds = Net::LDAP::Dataset::read_ldif(io) { |type, value|
case type
when :dn
assert_equal(dn_entries.first, value)
dn_entries.shift
end
}
assert_equal(entries.size, ds.size)
assert_equal(entries.sort, ds.to_ldif.grep(/^dn:\s*/) { $'.chomp })
end
end