Replaced ruby-net-ldap with net-ldap 0.2.2 gem.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@8751 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2012-02-02 19:30:01 +00:00
parent d02f6a8e32
commit 73f9b825f0
71 changed files with 6200 additions and 3369 deletions

View File

@@ -0,0 +1,154 @@
# -*- ruby encoding: utf-8 -*-
##
# 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
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
ary << "#{attr}: #{value}"
end
end
end
ary << ""
end
block_given? and ary.each { |line| yield line}
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
##
# 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) # :nodoc:
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?
class << self
class ChompedIO # :nodoc:
def initialize(io)
@io = io
end
def gets
s = @io.gets
s.chomp if s
end
end
##
# 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
##
# Reads an object that returns data line-wise (using #gets) and parses
# LDIF data into a Dataset object.
def read_ldif(io)
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
ds
end
end
end
require 'net/ldap/entry' unless defined? Net::LDAP::Entry

View File

@@ -0,0 +1,225 @@
# -*- ruby encoding: utf-8 -*-
##
# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
# ("Distinguished Name") is a unique identifier for an entry within an LDAP
# directory. It is made up of a number of other attributes strung together,
# to identify the entry in the tree.
#
# Each attribute that makes up a DN needs to have its value escaped so that
# the DN is valid. This class helps take care of that.
#
# A fully escaped DN needs to be unescaped when analysing its contents. This
# class also helps take care of that.
class Net::LDAP::DN
##
# Initialize a DN, escaping as required. Pass in attributes in name/value
# pairs. If there is a left over argument, it will be appended to the dn
# without escaping (useful for a base string).
#
# Most uses of this class will be to escape a DN, rather than to parse it,
# so storing the dn as an escaped String and parsing parts as required
# with a state machine seems sensible.
def initialize(*args)
buffer = StringIO.new
args.each_index do |index|
buffer << "=" if index % 2 == 1
buffer << "," if index % 2 == 0 && index != 0
if index < args.length - 1 || index % 2 == 1
buffer << Net::LDAP::DN.escape(args[index])
else
buffer << args[index]
end
end
@dn = buffer.string
end
##
# Parse a DN into key value pairs using ASN from
# http://tools.ietf.org/html/rfc2253 section 3.
def each_pair
state = :key
key = StringIO.new
value = StringIO.new
hex_buffer = ""
@dn.each_char do |char|
case state
when :key then
case char
when 'a'..'z', 'A'..'Z' then
state = :key_normal
key << char
when '0'..'9' then
state = :key_oid
key << char
when ' ' then state = :key
else raise "DN badly formed"
end
when :key_normal then
case char
when '=' then state = :value
when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
else raise "DN badly formed"
end
when :key_oid then
case char
when '=' then state = :value
when '0'..'9', '.', ' ' then key << char
else raise "DN badly formed"
end
when :value then
case char
when '\\' then state = :value_normal_escape
when '"' then state = :value_quoted
when ' ' then state = :value
when '#' then
state = :value_hexstring
value << char
when ',' then
state = :key
yield key.string.strip, value.string.rstrip
key = StringIO.new
value = StringIO.new;
else
state = :value_normal
value << char
end
when :value_normal then
case char
when '\\' then state = :value_normal_escape
when ',' then
state = :key
yield key.string.strip, value.string.rstrip
key = StringIO.new
value = StringIO.new;
else value << char
end
when :value_normal_escape then
case char
when '0'..'9', 'a'..'f', 'A'..'F' then
state = :value_normal_escape_hex
hex_buffer = char
else state = :value_normal; value << char
end
when :value_normal_escape_hex then
case char
when '0'..'9', 'a'..'f', 'A'..'F' then
state = :value_normal
value << "#{hex_buffer}#{char}".to_i(16).chr
else raise "DN badly formed"
end
when :value_quoted then
case char
when '\\' then state = :value_quoted_escape
when '"' then state = :value_end
else value << char
end
when :value_quoted_escape then
case char
when '0'..'9', 'a'..'f', 'A'..'F' then
state = :value_quoted_escape_hex
hex_buffer = char
else
state = :value_quoted;
value << char
end
when :value_quoted_escape_hex then
case char
when '0'..'9', 'a'..'f', 'A'..'F' then
state = :value_quoted
value << "#{hex_buffer}#{char}".to_i(16).chr
else raise "DN badly formed"
end
when :value_hexstring then
case char
when '0'..'9', 'a'..'f', 'A'..'F' then
state = :value_hexstring_hex
value << char
when ' ' then state = :value_end
when ',' then
state = :key
yield key.string.strip, value.string.rstrip
key = StringIO.new
value = StringIO.new;
else raise "DN badly formed"
end
when :value_hexstring_hex then
case char
when '0'..'9', 'a'..'f', 'A'..'F' then
state = :value_hexstring
value << char
else raise "DN badly formed"
end
when :value_end then
case char
when ' ' then state = :value_end
when ',' then
state = :key
yield key.string.strip, value.string.rstrip
key = StringIO.new
value = StringIO.new;
else raise "DN badly formed"
end
else raise "Fell out of state machine"
end
end
# Last pair
if [:value, :value_normal, :value_hexstring, :value_end].include? state
yield key.string.strip, value.string.rstrip
else
raise "DN badly formed"
end
end
##
# Returns the DN as an array in the form expected by the constructor.
def to_a
a = []
self.each_pair { |key, value| a << key << value }
a
end
##
# Return the DN as an escaped string.
def to_s
@dn
end
# http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions
# for dn values. All of the following must be escaped in any normal string
# using a single backslash ('\') as escape.
ESCAPES = {
',' => ',',
'+' => '+',
'"' => '"',
'\\' => '\\',
'<' => '<',
'>' => '>',
';' => ';',
}
# Compiled character class regexp using the keys from the above hash, and
# checking for a space or # at the start, or space at the end, of the
# string.
ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
"])")
##
# Escape a string for use in a DN value
def self.escape(string)
string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
end
##
# Proxy all other requests to the string object, because a DN is mainly
# used within the library as a string
def method_missing(method, *args, &block)
@dn.send(method, *args, &block)
end
end

View File

@@ -0,0 +1,185 @@
# -*- ruby encoding: utf-8 -*-
##
# 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
##
# 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
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.
#
# When an attribute is set using this, that attribute is now made
# accessible through methods as well.
#
# 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.
#
# 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
}
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
end
super
end
# 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

@@ -0,0 +1,759 @@
# -*- ruby encoding: utf-8 -*-
##
# Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
# this class is passed to Net::LDAP#search in the parameter :filter.
#
# Net::LDAP::Filter supports the complete set of search filters available in
# LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
# This class supplants the (infamous) RFC 2254 standard notation for
# specifying LDAP search filters.
#--
# NOTE: This wording needs to change as we will be supporting LDAPv3 search
# filter strings (RFC 4515).
#++
#
# Here's how to code the familiar "objectclass is present" filter:
# f = Net::LDAP::Filter.present("objectclass")
#
# The object returned by this code can be passed directly to the
# <tt>:filter</tt> parameter of Net::LDAP#search.
#
# See the individual class and instance methods below for more examples.
class Net::LDAP::Filter
##
# Known filter types.
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex ]
def initialize(op, left, right) #:nodoc:
unless FilterTypes.include?(op)
raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
end
@op = op
@left = left
@right = right
end
class << self
# We don't want filters created except using our custom constructors.
private :new
##
# Creates a Filter object indicating that the value of a particular
# attribute must either be present or match a particular string.
#
# Specifying that an attribute is 'present' means only directory entries
# which contain a value for the particular attribute will be selected by
# the filter. This is useful in case of optional attributes such as
# <tt>mail.</tt> Presence is indicated by giving the value "*" in the
# second parameter to #eq. This example selects only entries that have
# one or more values for <tt>sAMAccountName:</tt>
#
# f = Net::LDAP::Filter.eq("sAMAccountName", "*")
#
# To match a particular range of values, pass a string as the second
# parameter to #eq. The string may contain one or more "*" characters as
# wildcards: these match zero or more occurrences of any character. Full
# regular-expressions are <i>not</i> supported due to limitations in the
# underlying LDAP protocol. This example selects any entry with a
# <tt>mail</tt> value containing the substring "anderson":
#
# f = Net::LDAP::Filter.eq("mail", "*anderson*")
#
# This filter does not perform any escaping
def eq(attribute, value)
new(:eq, attribute, value)
end
##
# Creates a Filter object indicating extensible comparison. This Filter
# object is currently considered EXPERIMENTAL.
#
# sample_attributes = ['cn:fr', 'cn:fr.eq',
# 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
# attr = sample_attributes.first # Pick an extensible attribute
# value = 'roberts'
#
# filter = "#{attr}:=#{value}" # Basic String Filter
# filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
#
# # Perform a search with the Extensible Match Filter
# Net::LDAP.search(:filter => filter)
#--
# The LDIF required to support the above examples on the OpenDS LDAP
# server:
#
# version: 1
#
# dn: dc=example,dc=com
# objectClass: domain
# objectClass: top
# dc: example
#
# dn: ou=People,dc=example,dc=com
# objectClass: organizationalUnit
# objectClass: top
# ou: People
#
# dn: uid=1,ou=People,dc=example,dc=com
# objectClass: person
# objectClass: organizationalPerson
# objectClass: inetOrgPerson
# objectClass: top
# cn:: csO0YsOpcnRz
# sn:: YsO0YiByw7Riw6lydHM=
# givenName:: YsO0Yg==
# uid: 1
#
# =Refs:
# * http://www.ietf.org/rfc/rfc2251.txt
# * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
# * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
#++
def ex(attribute, value)
new(:ex, attribute, value)
end
##
# Creates a Filter object indicating that a particular attribute value
# is either not present or does not match a particular string; see
# Filter::eq for more information.
#
# This filter does not perform any escaping
def ne(attribute, value)
new(:ne, attribute, value)
end
##
# Creates a Filter object indicating that the value of a particular
# attribute must match a particular string. The attribute value is
# escaped, so the "*" character is interpreted literally.
def equals(attribute, value)
new(:eq, attribute, escape(value))
end
##
# Creates a Filter object indicating that the value of a particular
# attribute must begin with a particular string. The attribute value is
# escaped, so the "*" character is interpreted literally.
def begins(attribute, value)
new(:eq, attribute, escape(value) + "*")
end
##
# Creates a Filter object indicating that the value of a particular
# attribute must end with a particular string. The attribute value is
# escaped, so the "*" character is interpreted literally.
def ends(attribute, value)
new(:eq, attribute, "*" + escape(value))
end
##
# Creates a Filter object indicating that the value of a particular
# attribute must contain a particular string. The attribute value is
# escaped, so the "*" character is interpreted literally.
def contains(attribute, value)
new(:eq, attribute, "*" + escape(value) + "*")
end
##
# Creates a Filter object indicating that a particular attribute value
# is greater than or equal to the specified value.
def ge(attribute, value)
new(:ge, attribute, value)
end
##
# Creates a Filter object indicating that a particular attribute value
# is less than or equal to the specified value.
def le(attribute, value)
new(:le, attribute, value)
end
##
# Joins two or more filters so that all conditions must be true. Calling
# <tt>Filter.join(left, right)</tt> is the same as <tt>left &
# right</tt>.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet both conditions above.
# z = Net::LDAP::Filter.join(x, y)
def join(left, right)
new(:and, left, right)
end
##
# Creates a disjoint comparison between two or more filters. Selects
# entries where either the left or right side are true. Calling
# <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
# right</tt>.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet either condition above.
# z = x | y
def intersect(left, right)
new(:or, left, right)
end
##
# Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
# as <tt>~filter</tt>.
#
# # Selects only entries that do not have an <tt>objectclass</tt>
# # attribute.
# x = ~Net::LDAP::Filter.present("objectclass")
def negate(filter)
new(:not, filter, nil)
end
##
# This is a synonym for #eq(attribute, "*"). Also known as #present and
# #pres.
def present?(attribute)
eq(attribute, "*")
end
alias_method :present, :present?
alias_method :pres, :present?
# http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1
# charset for filters. All of the following must be escaped in any normal
# string using a single backslash ('\') as escape.
#
ESCAPES = {
"\0" => '00', # NUL = %x00 ; null character
'*' => '2A', # ASTERISK = %x2A ; asterisk ("*")
'(' => '28', # LPARENS = %x28 ; left parenthesis ("(")
')' => '29', # RPARENS = %x29 ; right parenthesis (")")
'\\' => '5C', # ESC = %x5C ; esc (or backslash) ("\")
}
# Compiled character class regexp using the keys from the above hash.
ESCAPE_RE = Regexp.new(
"[" +
ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
"]")
##
# Escape a string for use in an LDAP filter
def escape(string)
string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
end
##
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter
# object. The incoming BER object most likely came to us by parsing an
# LDAP searchRequest PDU. See also the comments under #to_ber, including
# the grammar snippet from the RFC.
#--
# We're hardcoding the BER constants from the RFC. These should be
# broken out insto constants.
def parse_ber(ber)
case ber.ber_identifier
when 0xa0 # context-specific constructed 0, "and"
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
when 0xa1 # context-specific constructed 1, "or"
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
when 0xa2 # context-specific constructed 2, "not"
~parse_ber(ber.first)
when 0xa3 # context-specific constructed 3, "equalityMatch"
if ber.last == "*"
else
eq(ber.first, ber.last)
end
when 0xa4 # context-specific constructed 4, "substring"
str = ""
final = false
ber.last.each { |b|
case b.ber_identifier
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
str += b
when 0x81 # context-specific primitive 0, SubstringFilter "any"
str += "*#{b}"
when 0x82 # context-specific primitive 0, SubstringFilter "final"
str += "*#{b}"
final = true
end
}
str += "*" unless final
eq(ber.first.to_s, str)
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
ge(ber.first.to_s, ber.last.to_s)
when 0xa6 # context-specific constructed 6, "lessOrEqual"
le(ber.first.to_s, ber.last.to_s)
when 0x87 # context-specific primitive 7, "present"
# call to_s to get rid of the BER-identifiedness of the incoming string.
present?(ber.to_s)
when 0xa9 # context-specific constructed 9, "extensible comparison"
raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2
# Reassembles the extensible filter parts
# (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
type = value = dn = rule = nil
ber.each do |element|
case element.ber_identifier
when 0x81 then rule=element
when 0x82 then type=element
when 0x83 then value=element
when 0x84 then dn='dn'
end
end
attribute = ''
attribute << type if type
attribute << ":#{dn}" if dn
attribute << ":#{rule}" if rule
ex(attribute, value)
else
raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
end
end
##
# Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
# to a Net::LDAP::Filter.
def construct(ldap_filter_string)
FilterParser.parse(ldap_filter_string)
end
alias_method :from_rfc2254, :construct
alias_method :from_rfc4515, :construct
##
# Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
# object.
#--
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
# filter types. Could pull them out into a constant.
#++
def parse_ldap_filter(obj)
case obj.ber_identifier
when 0x87 # present. context-specific primitive 7.
eq(obj.to_s, "*")
when 0xa3 # equalityMatch. context-specific constructed 3.
eq(obj[0], obj[1])
else
raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
end
end
end
##
# Joins two or more filters so that all conditions must be true.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet both conditions above.
# z = x & y
def &(filter)
self.class.join(self, filter)
end
##
# Creates a disjoint comparison between two or more filters. Selects
# entries where either the left or right side are true.
#
# # Selects only entries that have an <tt>objectclass</tt> attribute.
# x = Net::LDAP::Filter.present("objectclass")
# # Selects only entries that have a <tt>mail</tt> attribute that begins
# # with "George".
# y = Net::LDAP::Filter.eq("mail", "George*")
# # Selects only entries that meet either condition above.
# z = x | y
def |(filter)
self.class.intersect(self, filter)
end
##
# Negates a filter.
#
# # Selects only entries that do not have an <tt>objectclass</tt>
# # attribute.
# x = ~Net::LDAP::Filter.present("objectclass")
def ~@
self.class.negate(self)
end
##
# Equality operator for filters, useful primarily for constructing unit tests.
def ==(filter)
# 20100320 AZ: We need to come up with a better way of doing this. This
# is just nasty.
str = "[@op,@left,@right]"
self.instance_eval(str) == filter.instance_eval(str)
end
def to_raw_rfc2254
case @op
when :ne
"!(#{@left}=#{@right})"
when :eq
"#{@left}=#{@right}"
when :ex
"#{@left}:=#{@right}"
when :ge
"#{@left}>=#{@right}"
when :le
"#{@left}<=#{@right}"
when :and
"&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
when :or
"|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
when :not
"!(#{@left.to_raw_rfc2254})"
end
end
##
# Converts the Filter object to an RFC 2254-compatible text format.
def to_rfc2254
"(#{to_raw_rfc2254})"
end
def to_s
to_rfc2254
end
##
# Converts the filter to BER format.
#--
# Filter ::=
# CHOICE {
# and [0] SET OF Filter,
# or [1] SET OF Filter,
# not [2] Filter,
# equalityMatch [3] AttributeValueAssertion,
# substrings [4] SubstringFilter,
# greaterOrEqual [5] AttributeValueAssertion,
# lessOrEqual [6] AttributeValueAssertion,
# present [7] AttributeType,
# approxMatch [8] AttributeValueAssertion,
# extensibleMatch [9] MatchingRuleAssertion
# }
#
# SubstringFilter ::=
# SEQUENCE {
# type AttributeType,
# SEQUENCE OF CHOICE {
# initial [0] LDAPString,
# any [1] LDAPString,
# final [2] LDAPString
# }
# }
#
# MatchingRuleAssertion ::=
# SEQUENCE {
# matchingRule [1] MatchingRuleId OPTIONAL,
# type [2] AttributeDescription OPTIONAL,
# matchValue [3] AssertionValue,
# dnAttributes [4] BOOLEAN DEFAULT FALSE
# }
#
# Matching Rule Suffixes
# Less than [.1] or .[lt]
# Less than or equal to [.2] or [.lte]
# Equality [.3] or [.eq] (default)
# Greater than or equal to [.4] or [.gte]
# Greater than [.5] or [.gt]
# Substring [.6] or [.sub]
#
#++
def to_ber
case @op
when :eq
if @right == "*" # presence test
@left.to_s.to_ber_contextspecific(7)
elsif @right =~ /[*]/ # substring
# Parsing substrings is a little tricky. We use String#split to
# break a string into substrings delimited by the * (star)
# character. But we also need to know whether there is a star at the
# head and tail of the string, so we use a limit parameter value of
# -1: "If negative, there is no limit to the number of fields
# returned, and trailing null fields are not suppressed."
#
# 20100320 AZ: This is much simpler than the previous verison. Also,
# unnecessary regex escaping has been removed.
ary = @right.split(/[*]+/, -1)
if ary.first.empty?
first = nil
ary.shift
else
first = ary.shift.to_ber_contextspecific(0)
end
if ary.last.empty?
last = nil
ary.pop
else
last = ary.pop.to_ber_contextspecific(2)
end
seq = ary.map { |e| e.to_ber_contextspecific(1) }
seq.unshift first if first
seq.push last if last
[@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
else # equality
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
end
when :ex
seq = []
unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
raise Net::LDAP::LdapError, "Bad attribute #{@left}"
end
type, dn, rule = $1, $2, $4
seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
seq.to_ber_contextspecific(9)
when :ge
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
when :le
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
when :ne
[self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
when :and
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
when :or
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
when :not
[@left.to_ber].to_ber_contextspecific(2)
end
end
##
# Perform filter operations against a user-supplied block. This is useful
# when implementing an LDAP directory server. The caller's block will be
# called with two arguments: first, a symbol denoting the "operation" of
# the filter; and second, an array consisting of arguments to the
# operation. The user-supplied block (which is MANDATORY) should perform
# some desired application-defined processing, and may return a
# locally-meaningful object that will appear as a parameter in the :and,
# :or and :not operations detailed below.
#
# A typical object to return from the user-supplied block is an array of
# Net::LDAP::Filter objects.
#
# These are the possible values that may be passed to the user-supplied
# block:
# * :equalityMatch (the arguments will be an attribute name and a value
# to be matched);
# * :substrings (two arguments: an attribute name and a value containing
# one or more "*" characters);
# * :present (one argument: an attribute name);
# * :greaterOrEqual (two arguments: an attribute name and a value to be
# compared against);
# * :lessOrEqual (two arguments: an attribute name and a value to be
# compared against);
# * :and (two or more arguments, each of which is an object returned
# from a recursive call to #execute, with the same block;
# * :or (two or more arguments, each of which is an object returned from
# a recursive call to #execute, with the same block; and
# * :not (one argument, which is an object returned from a recursive
# call to #execute with the the same block.
def execute(&block)
case @op
when :eq
if @right == "*"
yield :present, @left
elsif @right.index '*'
yield :substrings, @left, @right
else
yield :equalityMatch, @left, @right
end
when :ge
yield :greaterOrEqual, @left, @right
when :le
yield :lessOrEqual, @left, @right
when :or, :and
yield @op, (@left.execute(&block)), (@right.execute(&block))
when :not
yield @op, (@left.execute(&block))
end || []
end
##
# This is a private helper method for dealing with chains of ANDs and ORs
# that are longer than two. If BOTH of our branches are of the specified
# type of joining operator, then return both of them as an array (calling
# coalesce recursively). If they're not, then return an array consisting
# only of self.
def coalesce(operator) #:nodoc:
if @op == operator
[@left.coalesce(operator), @right.coalesce(operator)]
else
[self]
end
end
##
#--
# We got a hash of attribute values.
# Do we match the attributes?
# Return T/F, and call match recursively as necessary.
#++
def match(entry)
case @op
when :eq
if @right == "*"
l = entry[@left] and l.length > 0
else
l = entry[@left] and l = Array(l) and l.index(@right)
end
else
raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
end
end
##
# Converts escaped characters (e.g., "\\28") to unescaped characters
# ("(").
def unescape(right)
right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
end
private :unescape
##
# Parses RFC 2254-style string representations of LDAP filters into Filter
# object hierarchies.
class FilterParser #:nodoc:
##
# The constructed filter.
attr_reader :filter
class << self
private :new
##
# Construct a filter tree from the provided string and return it.
def parse(ldap_filter_string)
new(ldap_filter_string).filter
end
end
def initialize(str)
require 'strscan' # Don't load strscan until we need it.
@filter = parse(StringScanner.new(str))
raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
end
##
# Parse the string contained in the StringScanner provided. Parsing
# tries to parse a standalone expression first. If that fails, it tries
# to parse a parenthesized expression.
def parse(scanner)
parse_filter_branch(scanner) or parse_paren_expression(scanner)
end
private :parse
##
# Join ("&") and intersect ("|") operations are presented in branches.
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
# test1 and test2. Each of these is parsed separately and then pushed
# into a branch array for filter merging using the parent operation.
#
# This method parses the branch text out into an array of filter
# objects.
def parse_branches(scanner)
branches = []
while branch = parse_paren_expression(scanner)
branches << branch
end
branches
end
private :parse_branches
##
# Join ("&") and intersect ("|") operations are presented in branches.
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
# test1 and test2. Each of these is parsed separately and then pushed
# into a branch array for filter merging using the parent operation.
#
# This method calls #parse_branches to generate the branch list and then
# merges them into a single Filter tree by calling the provided
# operation.
def merge_branches(op, scanner)
filter = nil
branches = parse_branches(scanner)
if branches.size >= 1
filter = branches.shift
while not branches.empty?
filter = filter.__send__(op, branches.shift)
end
end
filter
end
private :merge_branches
def parse_paren_expression(scanner)
if scanner.scan(/\s*\(\s*/)
expr = if scanner.scan(/\s*\&\s*/)
merge_branches(:&, scanner)
elsif scanner.scan(/\s*\|\s*/)
merge_branches(:|, scanner)
elsif scanner.scan(/\s*\!\s*/)
br = parse_paren_expression(scanner)
~br if br
else
parse_filter_branch(scanner)
end
if expr and scanner.scan(/\s*\)\s*/)
expr
end
end
end
private :parse_paren_expression
##
# This parses a given expression inside of parentheses.
def parse_filter_branch(scanner)
scanner.scan(/\s*/)
if token = scanner.scan(/[-\w:.]*[\w]/)
scanner.scan(/\s*/)
if op = scanner.scan(/<=|>=|!=|:=|=/)
scanner.scan(/\s*/)
if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!'\s]|\\[a-fA-F\d]{2})+/)
# 20100313 AZ: Assumes that "(uid=george*)" is the same as
# "(uid=george* )". The standard doesn't specify, but I can find
# no examples that suggest otherwise.
value.strip!
case op
when "="
Net::LDAP::Filter.eq(token, value)
when "!="
Net::LDAP::Filter.ne(token, value)
when "<="
Net::LDAP::Filter.le(token, value)
when ">="
Net::LDAP::Filter.ge(token, value)
when ":="
Net::LDAP::Filter.ex(token, value)
end
end
end
end
end
private :parse_filter_branch
end # class Net::LDAP::FilterParser
end # class Net::LDAP::Filter

View File

@@ -0,0 +1,31 @@
# -*- ruby encoding: utf-8 -*-
require 'digest/sha1'
require 'digest/md5'
class Net::LDAP::Password
class << self
# Generate a password-hash suitable for inclusion in an LDAP attribute.
# Pass a hash type (currently supported: :md5 and :sha) and a plaintext
# password. This function will return a hashed representation.
#
#--
# STUB: This is here to fulfill the requirements of an RFC, which
# one?
#
# TODO, gotta do salted-sha and (maybe)salted-md5. Should we provide
# sha1 as a synonym for sha1? I vote no because then should you also
# provide ssha1 for symmetry?
def generate(type, str)
digest, digest_name = case type
when :md5
[Digest::MD5.new, 'MD5']
when :sha
[Digest::SHA1.new, 'SHA']
else
raise Net::LDAP::LdapError, "Unsupported password-hash type (#{type})"
end
digest << str.to_s
return "{#{digest_name}}#{[digest.digest].pack('m').chomp }"
end
end
end

View File

@@ -0,0 +1,256 @@
# -*- ruby encoding: utf-8 -*-
require 'ostruct'
##
# Defines the Protocol Data Unit (PDU) for LDAP. An LDAP PDU always looks
# like a BER SEQUENCE with at least two elements: an INTEGER message ID
# number and an application-specific SEQUENCE. Some LDAPv3 packets also
# include an optional third element, a sequence of "controls" (see RFC 2251
# section 4.1.12 for more information).
#
# The application-specific tag in the sequence tells us what kind of packet
# it is, and each kind has its own format, defined in RFC-1777.
#
# Observe that many clients (such as ldapsearch) do not necessarily enforce
# the expected application tags on received protocol packets. This
# implementation does interpret the RFC strictly in this regard, and it
# remains to be seen whether there are servers out there that will not work
# well with our approach.
#
# Currently, we only support controls on SearchResult.
class Net::LDAP::PDU
class Error < RuntimeError; end
##
# This message packet is a bind request.
BindRequest = 0
BindResult = 1
UnbindRequest = 2
SearchRequest = 3
SearchReturnedData = 4
SearchResult = 5
ModifyResponse = 7
AddResponse = 9
DeleteResponse = 11
ModifyRDNResponse = 13
SearchResultReferral = 19
ExtendedRequest = 23
ExtendedResponse = 24
##
# The LDAP packet message ID.
attr_reader :message_id
alias_method :msg_id, :message_id
##
# The application protocol format tag.
attr_reader :app_tag
attr_reader :search_entry
attr_reader :search_referrals
attr_reader :search_parameters
attr_reader :bind_parameters
##
# Returns RFC-2251 Controls if any.
attr_reader :ldap_controls
alias_method :result_controls, :ldap_controls
# Messy. Does this functionality belong somewhere else?
def initialize(ber_object)
begin
@message_id = ber_object[0].to_i
# Grab the bottom five bits of the identifier so we know which type of
# PDU this is.
#
# This is safe enough in LDAP-land, but it is recommended that other
# approaches be taken for other protocols in the case that there's an
# app-specific tag that has both primitive and constructed forms.
@app_tag = ber_object[1].ber_identifier & 0x1f
@ldap_controls = []
rescue Exception => ex
raise Net::LDAP::PDU::Error, "LDAP PDU Format Error: #{ex.message}"
end
case @app_tag
when BindResult
parse_bind_response(ber_object[1])
when SearchReturnedData
parse_search_return(ber_object[1])
when SearchResultReferral
parse_search_referral(ber_object[1])
when SearchResult
parse_ldap_result(ber_object[1])
when ModifyResponse
parse_ldap_result(ber_object[1])
when AddResponse
parse_ldap_result(ber_object[1])
when DeleteResponse
parse_ldap_result(ber_object[1])
when ModifyRDNResponse
parse_ldap_result(ber_object[1])
when SearchRequest
parse_ldap_search_request(ber_object[1])
when BindRequest
parse_bind_request(ber_object[1])
when UnbindRequest
parse_unbind_request(ber_object[1])
when ExtendedResponse
parse_ldap_result(ber_object[1])
else
raise LdapPduError.new("unknown pdu-type: #{@app_tag}")
end
parse_controls(ber_object[2]) if ber_object[2]
end
##
# Returns a hash which (usually) defines the members :resultCode,
# :errorMessage, and :matchedDN. These values come directly from an LDAP
# response packet returned by the remote peer. Also see #result_code.
def result
@ldap_result || {}
end
##
# This returns an LDAP result code taken from the PDU, but it will be nil
# if there wasn't a result code. That can easily happen depending on the
# type of packet.
def result_code(code = :resultCode)
@ldap_result and @ldap_result[code]
end
##
# Return serverSaslCreds, which are only present in BindResponse packets.
#--
# Messy. Does this functionality belong somewhere else? We ought to
# refactor the accessors of this class before they get any kludgier.
def result_server_sasl_creds
@ldap_result && @ldap_result[:serverSaslCreds]
end
def parse_ldap_result(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]
}
end
private :parse_ldap_result
##
# A Bind Response may have an additional field, ID [7], serverSaslCreds,
# per RFC 2251 pgh 4.2.3.
def parse_bind_response(sequence)
sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP Bind Response length."
parse_ldap_result(sequence)
@ldap_result[:serverSaslCreds] = sequence[3] if sequence.length >= 4
@ldap_result
end
private :parse_bind_response
# Definition from RFC 1777 (we're handling application-4 here).
#
# Search Response ::=
# CHOICE {
# entry [APPLICATION 4] SEQUENCE {
# objectName LDAPDN,
# attributes SEQUENCE OF SEQUENCE {
# AttributeType,
# SET OF AttributeValue
# }
# },
# resultCode [APPLICATION 5] LDAPResult
# }
#
# We concoct a search response that is a hash of the returned attribute
# values.
#
# NOW OBSERVE CAREFULLY: WE ARE DOWNCASING THE RETURNED ATTRIBUTE NAMES.
#
# This is to make them more predictable for user programs, but it may not
# be a good idea. Maybe this should be configurable.
def parse_search_return(sequence)
sequence.length >= 2 or raise Net::LDAP::PDU::Error, "Invalid Search Response length."
@search_entry = Net::LDAP::Entry.new(sequence[0])
sequence[1].each { |seq| @search_entry[seq[0]] = seq[1] }
end
private :parse_search_return
##
# A search referral is a sequence of one or more LDAP URIs. Any number of
# search-referral replies can be returned by the server, interspersed with
# normal replies in any order.
#--
# Until I can think of a better way to do this, we'll return the referrals
# as an array. It'll be up to higher-level handlers to expose something
# reasonable to the client.
def parse_search_referral(uris)
@search_referrals = uris
end
private :parse_search_referral
##
# Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting
# of an OID, a boolean criticality flag defaulting FALSE, and an OPTIONAL
# Octet String. If only two fields are given, the second one may be either
# criticality or data, since criticality has a default value. Someday we
# may want to come back here and add support for some of more-widely used
# controls. RFC-2696 is a good example.
def parse_controls(sequence)
@ldap_controls = sequence.map do |control|
o = OpenStruct.new
o.oid, o.criticality, o.value = control[0], control[1], control[2]
if o.criticality and o.criticality.is_a?(String)
o.value = o.criticality
o.criticality = false
end
o
end
end
private :parse_controls
# (provisional, must document)
def parse_ldap_search_request(sequence)
s = OpenStruct.new
s.base_object, s.scope, s.deref_aliases, s.size_limit, s.time_limit,
s.types_only, s.filter, s.attributes = sequence
@search_parameters = s
end
private :parse_ldap_search_request
# (provisional, must document)
def parse_bind_request sequence
s = OpenStruct.new
s.version, s.name, s.authentication = sequence
@bind_parameters = s
end
private :parse_bind_request
# (provisional, must document)
# UnbindRequest has no content so this is a no-op.
def parse_unbind_request(sequence)
nil
end
private :parse_unbind_request
end
module Net
##
# Handle renamed constants Net::LdapPdu (Net::LDAP::PDU) and
# Net::LdapPduError (Net::LDAP::PDU::Error).
def self.const_missing(name) #:nodoc:
case name.to_s
when "LdapPdu"
warn "Net::#{name} has been deprecated. Use Net::LDAP::PDU instead."
Net::LDAP::PDU
when "LdapPduError"
warn "Net::#{name} has been deprecated. Use Net::LDAP::PDU::Error instead."
Net::LDAP::PDU::Error
when 'LDAP'
else
super
end
end
end # module Net