net-ldap-0.8.0/0000755000004100000410000000000012404411554013253 5ustar www-datawww-datanet-ldap-0.8.0/Rakefile0000644000004100000410000000403612404411554014723 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require "rubygems" require 'hoe' Hoe.plugin :doofus Hoe.plugin :git Hoe.plugin :gemspec Hoe.spec 'net-ldap' do |spec| # spec.rubyforge_name = spec.name spec.developer("Francis Cianfrocca", "blackhedd@rubyforge.org") spec.developer("Emiel van de Laar", "gemiel@gmail.com") spec.developer("Rory O'Connell", "rory.ocon@gmail.com") spec.developer("Kaspar Schiess", "kaspar.schiess@absurd.li") spec.developer("Austin Ziegler", "austin@rubyforge.org") spec.developer("Michael Schaarschmidt", "michael@schaaryworks.com") spec.remote_rdoc_dir = '' spec.rsync_args << ' --exclude=statsvn/' spec.urls = %w(http://rubyldap.com/' 'https://github.com/ruby-ldap/ruby-net-ldap) spec.licenses = ['MIT'] spec.history_file = 'History.rdoc' spec.readme_file = 'README.rdoc' spec.extra_rdoc_files = FileList["*.rdoc"].to_a spec.extra_dev_deps << [ "hoe-git", "~> 1" ] spec.extra_dev_deps << [ "hoe-gemspec", "~> 1" ] spec.extra_dev_deps << [ "metaid", "~> 1" ] spec.extra_dev_deps << [ "flexmock", ">= 1.3.0" ] spec.extra_dev_deps << [ "rspec", "~> 2.0" ] spec.clean_globs << "coverage" spec.spec_extras[:required_ruby_version] = ">= 1.8.7" spec.multiruby_skip << "1.8.6" spec.multiruby_skip << "1_8_6" spec.need_tar = true end # I'm not quite ready to get rid of this, but I think "rake git:manifest" is # sufficient. namespace :old do desc "Build the manifest file from the current set of files." task :build_manifest do |t| require 'find' paths = [] Find.find(".") do |path| next if File.directory?(path) next if path =~ /\.svn/ next if path =~ /\.git/ next if path =~ /\.hoerc/ next if path =~ /\.swp$/ next if path =~ %r{coverage/} next if path =~ /~$/ paths << path.sub(%r{^\./}, '') end File.open("Manifest.txt", "w") do |f| f.puts paths.sort.join("\n") end puts paths.sort.join("\n") end end desc "Run a full set of integration and unit tests" task :cruise => [:test, :spec] # vim: syntax=ruby net-ldap-0.8.0/.gemtest0000644000004100000410000000000012404411554014712 0ustar www-datawww-datanet-ldap-0.8.0/Manifest.txt0000644000004100000410000000232412404411554015563 0ustar www-datawww-data.autotest .rspec .travis.yml Contributors.rdoc Gemfile Hacking.rdoc History.rdoc License.rdoc Manifest.txt README.rdoc Rakefile autotest/discover.rb lib/net-ldap.rb lib/net/ber.rb lib/net/ber/ber_parser.rb lib/net/ber/core_ext.rb lib/net/ber/core_ext/array.rb lib/net/ber/core_ext/bignum.rb lib/net/ber/core_ext/false_class.rb lib/net/ber/core_ext/fixnum.rb lib/net/ber/core_ext/string.rb lib/net/ber/core_ext/true_class.rb lib/net/ldap.rb lib/net/ldap/dataset.rb lib/net/ldap/dn.rb lib/net/ldap/entry.rb lib/net/ldap/filter.rb lib/net/ldap/instrumentation.rb lib/net/ldap/password.rb lib/net/ldap/pdu.rb lib/net/ldap/version.rb lib/net/snmp.rb net-ldap.gemspec spec/integration/ssl_ber_spec.rb spec/spec.opts spec/spec_helper.rb spec/unit/ber/ber_spec.rb spec/unit/ber/core_ext/array_spec.rb spec/unit/ber/core_ext/string_spec.rb spec/unit/ldap/dn_spec.rb spec/unit/ldap/entry_spec.rb spec/unit/ldap/filter_parser_spec.rb spec/unit/ldap/filter_spec.rb spec/unit/ldap/search_spec.rb spec/unit/ldap_spec.rb test/common.rb test/test_entry.rb test/test_filter.rb test/test_ldap_connection.rb test/test_ldif.rb test/test_password.rb test/test_rename.rb test/test_snmp.rb test/testdata.ldif testserver/ldapserver.rb testserver/testdata.ldif net-ldap-0.8.0/Gemfile0000644000004100000410000000004612404411554014546 0ustar www-datawww-datasource 'https://rubygems.org' gemspec net-ldap-0.8.0/.autotest0000644000004100000410000000037312404411554015127 0ustar www-datawww-datarequire 'rubygems' #require 'redgreen/autotest' require 'autotest/timestamp' Autotest.add_hook :initialize do |autotest| %w{.git .hg .DS_Store ._* tmp log doc}.each do |exception| autotest.add_exception(exception) end end # vim: syntax=ruby net-ldap-0.8.0/History.rdoc0000644000004100000410000002236512404411554015575 0ustar www-datawww-data=== Net::LDAP 0.5.0 / 2013-07-22 * Major changes: * Required Ruby version is >=1.9.3 * Major enhancements: * Added alias dereferencing (@ngwilson) * BER now unescapes characters that are already escaped in the source string (@jzinn) * BerIdentifiedString will now fall back to ASCII-8 encoding if the source Ruby object cannot be encoded in UTF-8 (@lfu) * Bug fixes: * Fixed nil variable error when following a reference response (@cmdrclueless) * Fixed FilterParser unable to parse multibyte strings (@satoryu) * Return ConverterNotFound when dealing with a potentially corrupt data response (@jamuc) === Net::LDAP 0.3.1 / 2012-02-15 * Bug Fixes: * Bundler should now work again === Net::LDAP 0.3.0 / 2012-02-14 * Major changes: * Now uses UTF-8 strings instead of ASCII-8 per the LDAP RFC * Major Enhancements: * Adding continuation reference processing * Bug Fixes: * Fixes usupported object type #139 * Fixes Net::LDAP namespace errors * Return nil instead of an empty array if the search fails === Net::LDAP 0.2.2 / 2011-03-26 * Bug Fixes: * Fixed the call to Net::LDAP.modify_ops from Net::LDAP#modify. === Net::LDAP 0.2.1 / 2011-03-23 * Bug Fixes: * Net::LDAP.modify_ops was broken and is now fixed. === Net::LDAP 0.2 / 2011-03-22 * Major Enhancements: * Net::LDAP::Filter changes: * Filters can only be constructed using our custom constructors (eq, ge, etc.). Cleaned up the code to reflect the private new. * Fixed #to_ber to output a BER representation for :ne filters. Simplified the BER construction for substring matching. * Added Filter.join(left, right), Filter.intersect(left, right), and Filter.negate(filter) to match Filter#&, Filter#|, and Filter#~@ to prevent those operators from having problems with the private new. * Added Filter.present and Filter.present? aliases for the method previously only known as Filter.pres. * Added Filter.escape to escape strings for use in filters, based on rfc4515. * Added Filter.equals, Filter.begins, Filter.ends and Filter.contains, which automatically escape input for use in a filter string. * Cleaned up Net::LDAP::Filter::FilterParser to handle branches better. Fixed some of the regular expressions to be more canonically defined. * Correctly handles single-branch branches. * Cleaned up the string representation of Filter objects. * Added experimental support for RFC4515 extensible matching (e.g., "(cn:caseExactMatch:=Fred Flintstone)"); provided by "nowhereman". * Net::LDAP::DN class representing an automatically escaping/unescaping distinguished name for LDAP queries. * Minor Enhancements: * SSL capabilities will be enabled or disabled based on whether we can load OpenSSL successfully or not. * Moved the core class extensions extensions from being in the Net::LDAP hierarchy to the Net::BER hierarchy as most of the methods therein are related to BER-encoding values. This will make extracting Net::BER from Net::LDAP easier in the future. * Added some unit tests for the BER core extensions. * Paging controls are only sent where they are supported. * Documentation Changes: * Core class extension methods under Net::BER. * Extensive changes to Net::BER documentation. * Cleaned up some rdoc oddities, suppressed empty documentation sections where possible. * Added a document describing how to contribute to Net::LDAP most effectively. * Added a document recognizing contributors to Net::LDAP. * Extended unit testing: * Added some unit tests for the BER core extensions. * The LDIF test data file was split for Ruby 1.9 regexp support. * Added a cruisecontrol.rb task. * Converted some test/unit tests to specs. * Code clean-up: * Made the formatting of code consistent across all files. * Removed Net::BER::BERParser::TagClasses as it does not appear to be used. * Replaced calls to #to_a with calls to Kernel#Array; since Ruby 1.8.3, the default #to_a implementation has been deprecated and should be replaced either with calls to Kernel#Array or [value].flatten(1). * Modified #add and #modify to return a Pdu#result_code instead of a Pdu#result. This may be changed in Net::LDAP 1.0 to return the full Pdu#result, but if we do so, it will be that way for all LDAP calls involving Pdu objects. * Renamed Net::LDAP::Psw to Net::LDAP::Password with a corresponding filename change. * Removed the stub file lib/net/ldif.rb and class Net::LDIF. * Project Management: * Changed the license from Ruby + GPL to MIT with the agreement of the original author (Francis Cianfrocca) and the named contributors. Versions prior to 0.2.0 are still available under the Ruby + GPL license. === Net::LDAP 0.1.1 / 2010-03-18 * Fixing a critical problem with sockets. === Net::LDAP 0.1 / 2010-03-17 * Small fixes throughout, more to come. * Ruby 1.9 support added. * Ruby 1.8.6 and below support removed. If we can figure out a compatible way to reintroduce this, we will. * New maintainers, new project repository location. Please see the README.txt. === Net::LDAP 0.0.5 / 2009-03-xx * 13 minor enhancements: * Added Net::LDAP::Entry#to_ldif * Supported rootDSE searches with a new API. * Added [preliminary (still undocumented) support for SASL authentication. * Supported several constructs from the server side of the LDAP protocol. * Added a "consuming" String#read_ber! method. * Added some support for SNMP data-handling. * Belatedly added a patch contributed by Kouhei Sutou last October. The patch adds start_tls support. * Added Net::LDAP#search_subschema_entry * Added Net::LDAP::Filter#parse_ber, which constructs Net::LDAP::Filter objects directly from BER objects that represent search filters in LDAP SearchRequest packets. * Added Net::LDAP::Filter#execute, which allows arbitrary processing based on LDAP filters. * Changed Net::LDAP::Entry so it can be marshalled and unmarshalled. Thanks to an anonymous feature requester who only left the name "Jammy." * Added support for binary values in Net::LDAP::Entry LDIF conversions and marshalling. * Migrated to 'hoe' as the new project droid. * 14 bugs fixed: * Silenced some annoying warnings in filter.rb. Thanks to "barjunk" for pointing this out. * Some fairly extensive performance optimizations in the BER parser. * Fixed a bug in Net::LDAP::Entry::from_single_ldif_string noticed by Matthias Tarasiewicz. * Removed an erroneous LdapError value, noticed by Kouhei Sutou. * Supported attributes containing blanks (cn=Babs Jensen) to Filter#construct. Suggested by an anonymous Rubyforge user. * Added missing syntactic support for Filter ANDs, NOTs and a few other things. * Extended support for server-reported error messages. This was provisionally added to Net::LDAP#add, and eventually will be added to other methods. * Fixed bug in Net::LDAP#bind. We were ignoring the passed-in auth parm. Thanks to Kouhei Sutou for spotting it. * Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou for the patch. * Applied an additional patch from Kouhei. * Allowed comma in filter strings, suggested by Kouhei. * 04Sep07, Changed four error classes to inherit from StandardError rather Exception, in order to be friendlier to irb. Suggested by Kouhei. * Ensure connections are closed. Thanks to Kristian Meier. * Minor bug fixes here and there. === Net::LDAP 0.0.4 / 2006-08-15 * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for providing the rationale for this. * Added a much-expanded set of special characters to the parser for RFC-2254 filters. Thanks to Andre Nathan. * Changed Net::LDAP#search so you can pass it a filter in string form. The conversion to a Net::LDAP::Filter now happens automatically. * Implemented Net::LDAP#bind_as (preliminary and subject to change). Thanks for Simon Claret for valuable suggestions and for helping test. * Fixed bug in Net::LDAP#open that was preventing #open from being called more than one on a given Net::LDAP object. === Net::LDAP 0.0.3 / 2006-07-26 * Added simple TLS encryption. Thanks to Garett Shulman for suggestions and for helping test. === Net::LDAP 0.0.2 / 2006-07-12 * Fixed malformation in distro tarball and gem. * Improved documentation. * Supported "paged search control." * Added a range of API improvements. * Thanks to Andre Nathan, andre@digirati.com.br, for valuable suggestions. * Added support for LE and GE search filters. * Added support for Search referrals. * Fixed a regression with openldap 2.2.x and higher caused by the introduction of RFC-2696 controls. Thanks to Andre Nathan for reporting the problem. * Added support for RFC-2254 filter syntax. === Net::LDAP 0.0.1 / 2006-05-01 * Initial release. * Client functionality is near-complete, although the APIs are not guaranteed and may change depending on feedback from the community. * We're internally working on a Ruby-based implementation of a full-featured, production-quality LDAP server, which will leverage the underlying LDAP and BER functionality in Net::LDAP. * Please tell us if you would be interested in seeing a public release of the LDAP server. * Grateful acknowledgement to Austin Ziegler, who reviewed this code and provided the release framework, including minitar. net-ldap-0.8.0/License.rdoc0000644000004100000410000000246712404411554015517 0ustar www-datawww-data== License This software is available under the terms of the MIT license. Copyright 2006–2011 by Francis Cianfrocca and other contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. === Notice of License Change Versions prior to 0.2 were under Ruby's dual license with the GNU GPL. With this release (0.2), Net::LDAP is now under the MIT license. net-ldap-0.8.0/.rspec0000644000004100000410000000004012404411554014362 0ustar www-datawww-data--colour --format documentation net-ldap-0.8.0/spec/0000755000004100000410000000000012404411554014205 5ustar www-datawww-datanet-ldap-0.8.0/spec/spec.opts0000644000004100000410000000003112404411554016040 0ustar www-datawww-data--format specdoc --colournet-ldap-0.8.0/spec/spec_helper.rb0000644000004100000410000000077212404411554017031 0ustar www-datawww-datarequire 'net/ldap' RSpec.configure do |config| config.mock_with :flexmock def raw_string(s) # Conveniently, String#b only needs to be called when it exists s.respond_to?(:b) ? s.b : s end end class MockInstrumentationService def initialize @events = {} end def instrument(event, payload) result = yield(payload) @events[event] ||= [] @events[event] << [payload, result] result end def subscribe(event) @events[event] ||= [] @events[event] end end net-ldap-0.8.0/spec/integration/0000755000004100000410000000000012404411554016530 5ustar www-datawww-datanet-ldap-0.8.0/spec/integration/ssl_ber_spec.rb0000644000004100000410000000167212404411554021526 0ustar www-datawww-datarequire 'spec_helper' require 'net/ldap' require 'timeout' describe "BER serialisation (SSL)" do # Transmits str to #to and reads it back from #from. # def transmit(str) Timeout::timeout(1) do to.write(str) to.close from.read end end attr_reader :to, :from before(:each) do @from, @to = IO.pipe # The production code operates on sockets, which do need #connect called # on them to work. Pipes are more robust for this test, so we'll skip # the #connect call since it fails. flexmock(OpenSSL::SSL::SSLSocket). new_instances.should_receive(:connect => nil) @to = Net::LDAP::Connection.wrap_with_ssl(to) @from = Net::LDAP::Connection.wrap_with_ssl(from) end it "should transmit strings" do transmit('foo').should == 'foo' end it "should correctly transmit numbers" do to.write 1234.to_ber from.read_ber.should == 1234 end endnet-ldap-0.8.0/spec/unit/0000755000004100000410000000000012404411554015164 5ustar www-datawww-datanet-ldap-0.8.0/spec/unit/ldap_spec.rb0000644000004100000410000001727212404411554017454 0ustar www-datawww-datarequire 'spec_helper' describe Net::LDAP do describe "initialize" do context "when instrumentation is configured" do before do @connection = flexmock(:connection, :close => true) flexmock(Net::LDAP::Connection).should_receive(:new).and_return(@connection) @service = MockInstrumentationService.new end subject do Net::LDAP.new \ :server => "test.mocked.com", :port => 636, :force_no_page => true, # so server capabilities are not queried :instrumentation_service => @service end it "should instrument bind" do events = @service.subscribe "bind.net_ldap" bind_result = flexmock(:bind_result, :success? => true) @connection.should_receive(:bind).with(Hash).and_return(bind_result) subject.bind.should be_true payload, result = events.pop result.should be_true payload[:bind].should == bind_result end it "should instrument search" do events = @service.subscribe "search.net_ldap" @connection.should_receive(:bind).and_return(flexmock(:bind_result, :result_code => 0)) @connection.should_receive(:search).with(Hash, Proc). yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")). and_return(flexmock(:search_result, :success? => true, :result_code => 0)) subject.search(:filter => "(uid=user1)").should be_true payload, result = events.pop result.should == [entry] payload[:result].should == [entry] payload[:filter].should == "(uid=user1)" end end end end describe Net::LDAP::Connection do describe "initialize" do context "when host is not responding" do before(:each) do flexmock(TCPSocket). should_receive(:new).and_raise(Errno::ECONNREFUSED) end it "should raise LdapError" do lambda { Net::LDAP::Connection.new( :server => 'test.mocked.com', :port => 636) }.should raise_error(Net::LDAP::LdapError) end end context "when host is blocking the port" do before(:each) do flexmock(TCPSocket). should_receive(:new).and_raise(SocketError) end it "should raise LdapError" do lambda { Net::LDAP::Connection.new( :server => 'test.mocked.com', :port => 636) }.should raise_error(Net::LDAP::LdapError) end end context "on other exceptions" do before(:each) do flexmock(TCPSocket). should_receive(:new).and_raise(NameError) end it "should rethrow the exception" do lambda { Net::LDAP::Connection.new( :server => 'test.mocked.com', :port => 636) }.should raise_error(NameError) end end end context "populate error messages" do before do @tcp_socket = flexmock(:connection) @tcp_socket.should_receive(:write) flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket) end subject { Net::LDAP::Connection.new(:server => 'test.mocked.com', :port => 636) } it "should get back error messages if operation fails" do ber = Net::BER::BerIdentifiedArray.new([53, "", "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1"]) ber.ber_identifier = Net::LDAP::PDU::ModifyResponse @tcp_socket.should_receive(:read_ber).and_return([2, ber]) result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) result.should be_failure result.error_message.should == "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1" end it "shouldn't get back error messages if operation succeeds" do ber = Net::BER::BerIdentifiedArray.new([0, "", ""]) ber.ber_identifier = Net::LDAP::PDU::ModifyResponse @tcp_socket.should_receive(:read_ber).and_return([2, ber]) result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) result.should be_success result.error_message.should == "" end end context "instrumentation" do before do @tcp_socket = flexmock(:connection) # handle write @tcp_socket.should_receive(:write) # return this mock flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket) @service = MockInstrumentationService.new end subject do Net::LDAP::Connection.new(:server => 'test.mocked.com', :port => 636, :instrumentation_service => @service) end it "should publish a write.net_ldap_connection event" do ber = Net::BER::BerIdentifiedArray.new([0, "", ""]) ber.ber_identifier = Net::LDAP::PDU::BindResult read_result = [2, ber] @tcp_socket.should_receive(:read_ber).and_return(read_result) events = @service.subscribe "write.net_ldap_connection" result = subject.bind(method: :anon) result.should be_success # a write event payload, result = events.pop payload.should have_key(:result) payload.should have_key(:content_length) end it "should publish a read.net_ldap_connection event" do ber = Net::BER::BerIdentifiedArray.new([0, "", ""]) ber.ber_identifier = Net::LDAP::PDU::BindResult read_result = [2, ber] @tcp_socket.should_receive(:read_ber).and_return(read_result) events = @service.subscribe "read.net_ldap_connection" result = subject.bind(method: :anon) result.should be_success # a read event payload, result = events.pop payload.should have_key(:result) result.should == read_result end it "should publish a bind.net_ldap_connection event" do ber = Net::BER::BerIdentifiedArray.new([0, "", ""]) ber.ber_identifier = Net::LDAP::PDU::BindResult bind_result = [2, ber] @tcp_socket.should_receive(:read_ber).and_return(bind_result) events = @service.subscribe "bind.net_ldap_connection" result = subject.bind(method: :anon) result.should be_success # a read event payload, result = events.pop payload.should have_key(:result) result.should be_success end it "should publish a search.net_ldap_connection event" do # search data search_data_ber = Net::BER::BerIdentifiedArray.new([2, [ "uid=user1,ou=OrgUnit2,ou=OrgUnitTop,dc=openldap,dc=ghe,dc=local", [ ["uid", ["user1"]] ] ]]) search_data_ber.ber_identifier = Net::LDAP::PDU::SearchReturnedData search_data = [2, search_data_ber] # search result (end of results) search_result_ber = Net::BER::BerIdentifiedArray.new([0, "", ""]) search_result_ber.ber_identifier = Net::LDAP::PDU::SearchResult search_result = [2, search_result_ber] @tcp_socket.should_receive(:read_ber).and_return(search_data). and_return(search_result) events = @service.subscribe "search.net_ldap_connection" result = subject.search(filter: "(uid=user1)") result.should be_success # a search event payload, result = events.pop payload.should have_key(:result) payload.should have_key(:filter) payload[:filter].to_s.should == "(uid=user1)" result.should be_truthy end end end net-ldap-0.8.0/spec/unit/ldap/0000755000004100000410000000000012404411554016104 5ustar www-datawww-datanet-ldap-0.8.0/spec/unit/ldap/search_spec.rb0000644000004100000410000000252112404411554020710 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- describe Net::LDAP, "search method" do class FakeConnection def search(args) OpenStruct.new(:result_code => 1, :message => "error", :success? => false) end end before(:each) do @service = MockInstrumentationService.new @connection = Net::LDAP.new :instrumentation_service => @service @connection.instance_variable_set(:@open_connection, FakeConnection.new) end context "when :return_result => true" do it "should return nil upon error" do result_set = @connection.search(:return_result => true) result_set.should be_nil end end context "when :return_result => false" do it "should return false upon error" do result = @connection.search(:return_result => false) result.should be_false end end context "When :return_result is not given" do it "should return nil upon error" do result_set = @connection.search result_set.should be_nil end end context "when instrumentation_service is configured" do it "should publish a search.net_ldap event" do events = @service.subscribe "search.net_ldap" @connection.search :filter => "test" payload, result = events.pop payload.should have_key(:result) payload.should have_key(:filter) payload[:filter].should == "test" end end end net-ldap-0.8.0/spec/unit/ldap/filter_spec.rb0000644000004100000410000000721712404411554020737 0ustar www-datawww-datarequire 'spec_helper' describe Net::LDAP::Filter do describe "<- .ex(attr, value)" do context "('foo', 'bar')" do attr_reader :filter before(:each) do @filter = Net::LDAP::Filter.ex('foo', 'bar') end it "should convert to 'foo:=bar'" do filter.to_s.should == '(foo:=bar)' end it "should survive roundtrip via to_s/from_rfc2254" do Net::LDAP::Filter.from_rfc2254(filter.to_s).should == filter end it "should survive roundtrip conversion to/from ber" do ber = filter.to_ber Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)).should == filter end end context "various legal inputs" do [ '(o:dn:=Ace Industry)', '(:dn:2.4.8.10:=Dino)', '(cn:dn:1.2.3.4.5:=John Smith)', '(sn:dn:2.4.6.8.10:=Barbara Jones)', '(&(sn:dn:2.4.6.8.10:=Barbara Jones))' ].each do |filter_str| context "from_rfc2254(#{filter_str.inspect})" do attr_reader :filter before(:each) do @filter = Net::LDAP::Filter.from_rfc2254(filter_str) end it "should decode into a Net::LDAP::Filter" do filter.should be_an_instance_of(Net::LDAP::Filter) end it "should survive roundtrip conversion to/from ber" do ber = filter.to_ber Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)).should == filter end end end end end describe "<- .construct" do it "should accept apostrophes in filters (regression)" do Net::LDAP::Filter.construct("uid=O'Keefe").to_rfc2254.should == "(uid=O'Keefe)" end end describe "convenience filter constructors" do def eq(attribute, value) described_class.eq(attribute, value) end describe "<- .equals(attr, val)" do it "should delegate to .eq with escaping" do described_class.equals('dn', 'f*oo').should == eq('dn', 'f\2Aoo') end end describe "<- .begins(attr, val)" do it "should delegate to .eq with escaping" do described_class.begins('dn', 'f*oo').should == eq('dn', 'f\2Aoo*') end end describe "<- .ends(attr, val)" do it "should delegate to .eq with escaping" do described_class.ends('dn', 'f*oo').should == eq('dn', '*f\2Aoo') end end describe "<- .contains(attr, val)" do it "should delegate to .eq with escaping" do described_class.contains('dn', 'f*oo').should == eq('dn', '*f\2Aoo*') end end end describe "<- .escape(str)" do it "should escape nul, *, (, ) and \\" do Net::LDAP::Filter.escape("\0*()\\").should == "\\00\\2A\\28\\29\\5C" end end context 'with a well-known BER string' do ber = raw_string("\xa4\x2d" \ "\x04\x0b" "objectclass" \ "\x30\x1e" \ "\x80\x08" "foo" "*\\" "bar" \ "\x81\x08" "foo" "*\\" "bar" \ "\x82\x08" "foo" "*\\" "bar") describe "<- .to_ber" do [ "foo" "\\2A\\5C" "bar", "foo" "\\2a\\5c" "bar", "foo" "\\2A\\5c" "bar", "foo" "\\2a\\5C" "bar" ].each do |escaped| it 'unescapes escaped characters' do filter = Net::LDAP::Filter.eq("objectclass", "#{escaped}*#{escaped}*#{escaped}") filter.to_ber.should == ber end end end describe '<- .parse_ber' do it 'escapes characters' do escaped = Net::LDAP::Filter.escape("foo" "*\\" "bar") filter = Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) filter.to_s.should == "(objectclass=#{escaped}*#{escaped}*#{escaped})" end end end end net-ldap-0.8.0/spec/unit/ldap/filter_parser_spec.rb0000644000004100000410000000172512404411554022311 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe Net::LDAP::Filter::FilterParser do describe "#parse" do context "Given ASCIIs as filter string" do let(:filter_string) { "(cn=name)" } specify "should generate filter object" do expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter end end context "Given string including multibyte chars as filter string" do let(:filter_string) { "(cn=名前)" } specify "should generate filter object" do expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter end end context "Given string including colons ':'" do let(:filter_string) { "(ismemberof=cn=edu:berkeley:app:calmessages:deans,ou=campus groups,dc=berkeley,dc=edu)" } specify "should generate filter object" do expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter end end end end net-ldap-0.8.0/spec/unit/ldap/entry_spec.rb0000644000004100000410000000211112404411554020577 0ustar www-datawww-datarequire 'spec_helper' describe Net::LDAP::Entry do attr_reader :entry before(:each) do @entry = Net::LDAP::Entry.from_single_ldif_string( %Q{dn: something foo: foo barAttribute: bar } ) end describe "entry access" do it "should always respond to #dn" do entry.should respond_to(:dn) end context "<- #foo" do it "should respond_to?" do entry.should respond_to(:foo) end it "should return 'foo'" do entry.foo.should == ['foo'] end end context "<- #Foo" do it "should respond_to?" do entry.should respond_to(:Foo) end it "should return 'foo'" do entry.foo.should == ['foo'] end end context "<- #foo=" do it "should respond_to?" do entry.should respond_to(:foo=) end it "should set 'foo'" do entry.foo= 'bar' entry.foo.should == ['bar'] end end context "<- #fOo=" do it "should return 'foo'" do entry.fOo= 'bar' entry.fOo.should == ['bar'] end end end endnet-ldap-0.8.0/spec/unit/ldap/dn_spec.rb0000644000004100000410000000367612404411554020060 0ustar www-datawww-datarequire 'spec_helper' require 'net/ldap/dn' describe Net::LDAP::DN do describe "<- .construct" do attr_reader :dn before(:each) do @dn = Net::LDAP::DN.new('cn', ',+"\\<>;', 'ou=company') end it "should construct a Net::LDAP::DN" do dn.should be_an_instance_of(Net::LDAP::DN) end it "should escape all the required characters" do dn.to_s.should == 'cn=\\,\\+\\"\\\\\\<\\>\\;,ou=company' end end describe "<- .to_a" do context "parsing" do { 'cn=James, ou=Company\\,\\20LLC' => ['cn','James','ou','Company, LLC'], 'cn = \ James , ou = "Comp\28ny" ' => ['cn',' James','ou','Comp(ny'], '1.23.4= #A3B4D5 ,ou=Company' => ['1.23.4','#A3B4D5','ou','Company'], }.each do |key, value| context "(#{key})" do attr_reader :dn before(:each) do @dn = Net::LDAP::DN.new(key) end it "should decode into a Net::LDAP::DN" do dn.should be_an_instance_of(Net::LDAP::DN) end it "should return the correct array" do dn.to_a.should == value end end end end context "parsing bad input" do [ 'cn=James,', 'cn=#aa aa', 'cn="James', 'cn=J\ames', 'cn=\\', '1.2.d=Value', 'd1.2=Value', ].each do |value| context "(#{value})" do attr_reader :dn before(:each) do @dn = Net::LDAP::DN.new(value) end it "should decode into a Net::LDAP::DN" do dn.should be_an_instance_of(Net::LDAP::DN) end it "should raise an error on parsing" do lambda { dn.to_a }.should raise_error end end end end end describe "<- .escape(str)" do it "should escape ,, +, \", \\, <, >, and ;" do Net::LDAP::DN.escape(',+"\\<>;').should == '\\,\\+\\"\\\\\\<\\>\\;' end end end net-ldap-0.8.0/spec/unit/ber/0000755000004100000410000000000012404411554015734 5ustar www-datawww-datanet-ldap-0.8.0/spec/unit/ber/core_ext/0000755000004100000410000000000012404411554017544 5ustar www-datawww-datanet-ldap-0.8.0/spec/unit/ber/core_ext/string_spec.rb0000644000004100000410000000321212404411554022407 0ustar www-datawww-datarequire 'spec_helper' require 'metaid' describe String, "when extended with BER core extensions" do describe "<- #read_ber! (consuming read_ber method)" do context "when passed an ldap bind request and some extra data" do attr_reader :str, :result before(:each) do @str = raw_string("0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus UNCONSUMED") @result = str.read_ber!(Net::LDAP::AsnSyntax) end it "should correctly parse the ber message" do result.should == [1, [3, "Administrator", "ad_is_bogus"]] end it "should leave unconsumed part of message in place" do str.should == " UNCONSUMED" end context "if an exception occurs during #read_ber" do attr_reader :initial_value before(:each) do stub_exception_class = Class.new(StandardError) @initial_value = raw_string("0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus") @str = initial_value.dup # Defines a string io = StringIO.new(initial_value) io.meta_def :read_ber do |syntax| read raise stub_exception_class end flexmock(StringIO).should_receive(:new).and_return(io) begin str.read_ber!(Net::LDAP::AsnSyntax) rescue stub_exception_class # EMPTY ON PURPOSE else raise "The stub code should raise an exception!" end end it "should not modify string" do str.should == initial_value end end end end end net-ldap-0.8.0/spec/unit/ber/core_ext/array_spec.rb0000644000004100000410000000142712404411554022225 0ustar www-datawww-datarequire 'spec_helper' require 'metaid' describe Array, "when extended with BER core extensions" do it "should correctly convert a control code array" do control_codes = [] control_codes << ['1.2.3'.to_ber, true.to_ber].to_ber_sequence control_codes << ['1.7.9'.to_ber, false.to_ber].to_ber_sequence control_codes = control_codes.to_ber_sequence res = [['1.2.3', true],['1.7.9',false]].to_ber_control res.should eq(control_codes) end it "should wrap the array in another array if a nested array is not passed" do result1 = ['1.2.3', true].to_ber_control result2 = [['1.2.3', true]].to_ber_control result1.should eq(result2) end it "should return an empty string if an empty array is passed" do [].to_ber_control.should be_empty end end net-ldap-0.8.0/spec/unit/ber/ber_spec.rb0000644000004100000410000001102112404411554020036 0ustar www-datawww-datarequire 'spec_helper' require 'net/ber' require 'net/ldap' describe "BER encoding of" do RSpec::Matchers.define :properly_encode_and_decode do match do |given| given.to_ber.read_ber.should == given end end context "arrays" do it "should properly encode/decode []" do [].should properly_encode_and_decode end it "should properly encode/decode [1,2,3]" do ary = [1,2,3] encoded_ary = ary.map { |el| el.to_ber }.to_ber encoded_ary.read_ber.should == ary end end context "booleans" do it "should encode true" do true.to_ber.should == "\x01\x01\x01" end it "should encode false" do false.to_ber.should == "\x01\x01\x00" end end context "numbers" do # Sample based { 0 => raw_string("\x02\x01\x00"), 1 => raw_string("\x02\x01\x01"), 127 => raw_string("\x02\x01\x7F"), 128 => raw_string("\x02\x01\x80"), 255 => raw_string("\x02\x01\xFF"), 256 => raw_string("\x02\x02\x01\x00"), 65535 => raw_string("\x02\x02\xFF\xFF"), 65536 => raw_string("\x02\x03\x01\x00\x00"), 16_777_215 => raw_string("\x02\x03\xFF\xFF\xFF"), 0x01000000 => raw_string("\x02\x04\x01\x00\x00\x00"), 0x3FFFFFFF => raw_string("\x02\x04\x3F\xFF\xFF\xFF"), 0x4FFFFFFF => raw_string("\x02\x04\x4F\xFF\xFF\xFF"), # Some odd samples... 5 => raw_string("\002\001\005"), 500 => raw_string("\002\002\001\364"), 50_000 => raw_string("\x02\x02\xC3P"), 5_000_000_000 => raw_string("\002\005\001*\005\362\000") }.each do |number, expected_encoding| it "should encode #{number} as #{expected_encoding.inspect}" do number.to_ber.should == expected_encoding end end # Round-trip encoding: This is mostly to be sure to cover Bignums well. context "when decoding with #read_ber" do it "should correctly handle powers of two" do 100.times do |p| n = 2 << p n.should properly_encode_and_decode end end it "should correctly handle powers of ten" do 100.times do |p| n = 5 * 10**p n.should properly_encode_and_decode end end end end if "Ruby 1.9".respond_to?(:encoding) context "strings" do it "should properly encode UTF-8 strings" do "\u00e5".force_encoding("UTF-8").to_ber.should == raw_string("\x04\x02\xC3\xA5") end it "should properly encode strings encodable as UTF-8" do "teststring".encode("US-ASCII").to_ber.should == "\x04\nteststring" end it "should properly encode binary data strings using to_ber_bin" do # This is used for searching for GUIDs in Active Directory ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").to_ber_bin.should == raw_string("\x04\x10" + "j1\xB4\xA1*\xA2zA\xAC\xA9`?'\xDDQ\x16") end it "should not fail on strings that can not be converted to UTF-8" do error = Encoding::UndefinedConversionError lambda {"\x81".to_ber }.should_not raise_exception(error) end end end end describe "BER decoding of" do context "numbers" do it "should decode #{"\002\001\006".inspect} (6)" do "\002\001\006".read_ber(Net::LDAP::AsnSyntax).should == 6 end it "should decode #{"\004\007testing".inspect} ('testing')" do "\004\007testing".read_ber(Net::LDAP::AsnSyntax).should == 'testing' end it "should decode an ldap bind request" do "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus". read_ber(Net::LDAP::AsnSyntax).should == [1, [3, "Administrator", "ad_is_bogus"]] end end end describe Net::BER::BerIdentifiedString do describe "initialize" do subject { Net::BER::BerIdentifiedString.new(data) } context "binary data" do let(:data) { ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").force_encoding("ASCII-8BIT") } its(:valid_encoding?) { should be_true } specify { subject.encoding.name.should == "ASCII-8BIT" } end context "ascii data in UTF-8" do let(:data) { "some text".force_encoding("UTF-8") } its(:valid_encoding?) { should be_true } specify { subject.encoding.name.should == "UTF-8" } end context "UTF-8 data in UTF-8" do let(:data) { ["e4b8ad"].pack("H*").force_encoding("UTF-8") } its(:valid_encoding?) { should be_true } specify { subject.encoding.name.should == "UTF-8" } end end end net-ldap-0.8.0/README.rdoc0000644000004100000410000000266212404411554015067 0ustar www-datawww-data= Net::LDAP for Ruby {}[https://travis-ci.org/github/ruby-net-ldap] == Description Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services. Net::LDAP is written completely in Ruby with no external dependencies. It supports most LDAP client features and a subset of server features as well. Net::LDAP has been tested against modern popular LDAP servers including OpenLDAP and Active Directory. The current release is mostly compliant with earlier versions of the IETF LDAP RFCs (2251–2256, 2829–2830, 3377, and 3771). Our roadmap for Net::LDAP 1.0 is to gain full client compliance with the most recent LDAP RFCs (4510–4519, plus portions of 4520–4532). == Where * {GitHub}[https://github.com/ruby-ldap/ruby-net-ldap] * {ruby-ldap@googlegroups.com}[http://groups.google.com/group/ruby-ldap] == Synopsis See Net::LDAP for documentation and usage samples. == Requirements Net::LDAP requires a Ruby 1.9.3 compatible interpreter or better. == Install Net::LDAP is a pure Ruby library. It does not require any external libraries. You can install the RubyGems version of Net::LDAP available from the usual sources. gem install net-ldap Simply require either 'net-ldap' or 'net/ldap'. :include: Contributors.rdoc :include: License.rdoc net-ldap-0.8.0/.travis.yml0000644000004100000410000000023012404411554015357 0ustar www-datawww-datalanguage: ruby rvm: - 1.9.3 - 2.0.0 - jruby-19mode - rbx-19mode matrix: allow_failures: - rvm: jruby-19mode script: bundle exec rake spec net-ldap-0.8.0/lib/0000755000004100000410000000000012404411554014021 5ustar www-datawww-datanet-ldap-0.8.0/lib/net-ldap.rb0000644000004100000410000000006212404411554016050 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'net/ldap' net-ldap-0.8.0/lib/net/0000755000004100000410000000000012404411554014607 5ustar www-datawww-datanet-ldap-0.8.0/lib/net/snmp.rb0000644000004100000410000001451612404411554016120 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'net/ldap/version' # :stopdoc: module Net class SNMP VERSION = Net::LDAP::VERSION AsnSyntax = Net::BER.compile_syntax({ :application => { :primitive => { 1 => :integer, # Counter32, (RFC2578 sec 2) 2 => :integer, # Gauge32 or Unsigned32, (RFC2578 sec 2) 3 => :integer # TimeTicks32, (RFC2578 sec 2) }, :constructed => { } }, :context_specific => { :primitive => { }, :constructed => { 0 => :array, # GetRequest PDU (RFC1157 pgh 4.1.2) 1 => :array, # GetNextRequest PDU (RFC1157 pgh 4.1.3) 2 => :array # GetResponse PDU (RFC1157 pgh 4.1.4) } } }) # SNMP 32-bit counter. # Defined in RFC1155 (Structure of Mangement Information), section 6. # A 32-bit counter is an ASN.1 application [1] implicit unsigned integer # with a range from 0 to 2^^32 - 1. class Counter32 def initialize value @value = value end def to_ber @value.to_ber_application(1) end end # SNMP 32-bit gauge. # Defined in RFC1155 (Structure of Mangement Information), section 6. # A 32-bit counter is an ASN.1 application [2] implicit unsigned integer. # This is also indistinguishable from Unsigned32. (Need to alias them.) class Gauge32 def initialize value @value = value end def to_ber @value.to_ber_application(2) end end # SNMP 32-bit timer-ticks. # Defined in RFC1155 (Structure of Mangement Information), section 6. # A 32-bit counter is an ASN.1 application [3] implicit unsigned integer. class TimeTicks32 def initialize value @value = value end def to_ber @value.to_ber_application(3) end end end class SnmpPdu class Error < StandardError; end PduTypes = [ :get_request, :get_next_request, :get_response, :set_request, :trap ] ErrorStatusCodes = { # Per RFC1157, pgh 4.1.1 0 => "noError", 1 => "tooBig", 2 => "noSuchName", 3 => "badValue", 4 => "readOnly", 5 => "genErr" } class << self def parse ber_object n = new n.send :parse, ber_object n end end attr_reader :version, :community, :pdu_type, :variables, :error_status attr_accessor :request_id, :error_index def initialize args={} @version = args[:version] || 0 @community = args[:community] || "public" @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value. @error_status = args[:error_status] || 0 @error_index = args[:error_index] || 0 @variables = args[:variables] || [] end #-- def parse ber_object begin parse_ber_object ber_object rescue Error # Pass through any SnmpPdu::Error instances raise $! rescue # Wrap any basic parsing error so it becomes a PDU-format error raise Error.new( "snmp-pdu format error" ) end end private :parse def parse_ber_object ber_object send :version=, ber_object[0].to_i send :community=, ber_object[1].to_s data = ber_object[2] case (app_tag = data.ber_identifier & 31) when 0 send :pdu_type=, :get_request parse_get_request data when 1 send :pdu_type=, :get_next_request # This PDU is identical to get-request except for the type. parse_get_request data when 2 send :pdu_type=, :get_response # This PDU is identical to get-request except for the type, # the error_status and error_index values are meaningful, # and the fact that the variable bindings will be non-null. parse_get_response data else raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) end end private :parse_ber_object #-- # Defined in RFC1157, pgh 4.1.2. def parse_get_request data send :request_id=, data[0].to_i # data[1] is error_status, always zero. # data[2] is error_index, always zero. send :error_status=, 0 send :error_index=, 0 data[3].each {|n,v| # A variable-binding, of which there may be several, # consists of an OID and a BER null. # We're ignoring the null, we might want to verify it instead. unless v.is_a?(Net::BER::BerIdentifiedNull) raise Error.new(" invalid variable-binding in get-request" ) end add_variable_binding n, nil } end private :parse_get_request #-- # Defined in RFC1157, pgh 4.1.4 def parse_get_response data send :request_id=, data[0].to_i send :error_status=, data[1].to_i send :error_index=, data[2].to_i data[3].each {|n,v| # A variable-binding, of which there may be several, # consists of an OID and a BER null. # We're ignoring the null, we might want to verify it instead. add_variable_binding n, v } end private :parse_get_response def version= ver unless [0,2].include?(ver) raise Error.new("unknown snmp-version: #{ver}") end @version = ver end def pdu_type= t unless PduTypes.include?(t) raise Error.new("unknown pdu-type: #{t}") end @pdu_type = t end def error_status= es unless ErrorStatusCodes.has_key?(es) raise Error.new("unknown error-status: #{es}") end @error_status = es end def community= c @community = c.to_s end #-- # Syntactic sugar def add_variable_binding name, value=nil @variables ||= [] @variables << [name, value] end def to_ber_string [ version.to_ber, community.to_ber, pdu_to_ber_string ].to_ber_sequence end #-- # Helper method that returns a PDU payload in BER form, # depending on the PDU type. def pdu_to_ber_string case pdu_type when :get_request [ request_id.to_ber, error_status.to_ber, error_index.to_ber, [ @variables.map {|n,v| [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence } ].to_ber_sequence ].to_ber_contextspecific(0) when :get_next_request [ request_id.to_ber, error_status.to_ber, error_index.to_ber, [ @variables.map {|n,v| [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence } ].to_ber_sequence ].to_ber_contextspecific(1) when :get_response [ request_id.to_ber, error_status.to_ber, error_index.to_ber, [ @variables.map {|n,v| [n.to_ber_oid, v.to_ber].to_ber_sequence } ].to_ber_sequence ].to_ber_contextspecific(2) else raise Error.new( "unknown pdu-type: #{pdu_type}" ) end end private :pdu_to_ber_string end end # :startdoc: net-ldap-0.8.0/lib/net/ber.rb0000644000004100000410000003034412404411554015710 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'net/ldap/version' module Net # :nodoc: ## # == Basic Encoding Rules (BER) Support Module # # Much of the text below is cribbed from Wikipedia: # http://en.wikipedia.org/wiki/Basic_Encoding_Rules # # The ITU Specification is also worthwhile reading: # http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf # # The Basic Encoding Rules were the original rules laid out by the ASN.1 # standard for encoding abstract information into a concrete data stream. # The rules, collectively referred to as a transfer syntax in ASN.1 # parlance, specify the exact octet sequences which are used to encode a # given data item. The syntax defines such elements as: the # representations for basic data types, the structure of length # information, and the means for defining complex or compound types based # on more primitive types. The BER syntax, along with two subsets of BER # (the Canonical Encoding Rules and the Distinguished Encoding Rules), are # defined by the ITU-T's X.690 standards document, which is part of the # ASN.1 document series. # # == Encoding # The BER format specifies a self-describing and self-delimiting format # for encoding ASN.1 data structures. Each data element is encoded as a # type identifier, a length description, the actual data elements, and # where necessary, an end-of-content marker. This format allows a receiver # to decode the ASN.1 information from an incomplete stream, without # requiring any pre-knowledge of the size, content, or semantic meaning of # the data. # # # # == Protocol Data Units (PDU) # Protocols are defined with schema represented in BER, such that a PDU # consists of cascaded type-length-value encodings. # # === Type Tags # BER type tags are represented as single octets (bytes). The lower five # bits of the octet are tag identifier numbers and the upper three bits of # the octet are used to distinguish the type as native to ASN.1, # application-specific, context-specific, or private. See # Net::BER::TAG_CLASS and Net::BER::ENCODING_TYPE for more information. # # If Class is set to Universal (0b00______), the value is of a type native # to ASN.1 (e.g. INTEGER). The Application class (0b01______) is only # valid for one specific application. Context_specific (0b10______) # depends on the context and private (0b11_______) can be defined in # private specifications # # If the primitive/constructed bit is zero (0b__0_____), it specifies that # the value is primitive like an INTEGER. If it is one (0b__1_____), the # value is a constructed value that contains type-length-value encoded # types like a SET or a SEQUENCE. # # === Defined Universal (ASN.1 Native) Types # There are a number of pre-defined universal (native) types. # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
NamePrimitive
Constructed
Number
EOC (End-of-Content)P0: 0 (0x0, 0b00000000)
BOOLEANP1: 1 (0x01, 0b00000001)
INTEGERP2: 2 (0x02, 0b00000010)
BIT STRINGP3: 3 (0x03, 0b00000011)
BIT STRINGC3: 35 (0x23, 0b00100011)
OCTET STRINGP4: 4 (0x04, 0b00000100)
OCTET STRINGC4: 36 (0x24, 0b00100100)
NULLP5: 5 (0x05, 0b00000101)
OBJECT IDENTIFIERP6: 6 (0x06, 0b00000110)
Object DescriptorP7: 7 (0x07, 0b00000111)
EXTERNALC8: 40 (0x28, 0b00101000)
REAL (float)P9: 9 (0x09, 0b00001001)
ENUMERATEDP10: 10 (0x0a, 0b00001010)
EMBEDDED PDVC11: 43 (0x2b, 0b00101011)
UTF8StringP12: 12 (0x0c, 0b00001100)
UTF8StringC12: 44 (0x2c, 0b00101100)
RELATIVE-OIDP13: 13 (0x0d, 0b00001101)
SEQUENCE and SEQUENCE OFC16: 48 (0x30, 0b00110000)
SET and SET OFC17: 49 (0x31, 0b00110001)
NumericStringP18: 18 (0x12, 0b00010010)
NumericStringC18: 50 (0x32, 0b00110010)
PrintableStringP19: 19 (0x13, 0b00010011)
PrintableStringC19: 51 (0x33, 0b00110011)
T61StringP20: 20 (0x14, 0b00010100)
T61StringC20: 52 (0x34, 0b00110100)
VideotexStringP21: 21 (0x15, 0b00010101)
VideotexStringC21: 53 (0x35, 0b00110101)
IA5StringP22: 22 (0x16, 0b00010110)
IA5StringC22: 54 (0x36, 0b00110110)
UTCTimeP23: 23 (0x17, 0b00010111)
UTCTimeC23: 55 (0x37, 0b00110111)
GeneralizedTimeP24: 24 (0x18, 0b00011000)
GeneralizedTimeC24: 56 (0x38, 0b00111000)
GraphicStringP25: 25 (0x19, 0b00011001)
GraphicStringC25: 57 (0x39, 0b00111001)
VisibleStringP26: 26 (0x1a, 0b00011010)
VisibleStringC26: 58 (0x3a, 0b00111010)
GeneralStringP27: 27 (0x1b, 0b00011011)
GeneralStringC27: 59 (0x3b, 0b00111011)
UniversalStringP28: 28 (0x1c, 0b00011100)
UniversalStringC28: 60 (0x3c, 0b00111100)
CHARACTER STRINGP29: 29 (0x1d, 0b00011101)
CHARACTER STRINGC29: 61 (0x3d, 0b00111101)
BMPStringP30: 30 (0x1e, 0b00011110)
BMPStringC30: 62 (0x3e, 0b00111110)
module BER VERSION = Net::LDAP::VERSION ## # Used for BER-encoding the length and content bytes of a Fixnum integer # values. MAX_FIXNUM_SIZE = 0.size ## # BER tag classes are kept in bits seven and eight of the tag type # octet. # # # # # # # #
BitmaskDefinition
0b00______Universal (ASN.1 Native) Types
0b01______Application Types
0b10______Context-Specific Types
0b11______Private Types
TAG_CLASS = { :universal => 0b00000000, # 0 :application => 0b01000000, # 64 :context_specific => 0b10000000, # 128 :private => 0b11000000, # 192 } ## # BER encoding type is kept in bit 6 of the tag type octet. # # # # # #
BitmaskDefinition
0b__0_____Primitive
0b__1_____Constructed
ENCODING_TYPE = { :primitive => 0b00000000, # 0 :constructed => 0b00100000, # 32 } ## # Accepts a hash of hashes describing a BER syntax and converts it into # a byte-keyed object for fast BER conversion lookup. The resulting # "compiled" syntax is used by Net::BER::BERParser. # # This method should be called only by client classes of Net::BER (e.g., # Net::LDAP and Net::SNMP) and not by clients of those classes. # # The hash-based syntax uses TAG_CLASS keys that contain hashes of # ENCODING_TYPE keys that contain tag numbers with object type markers. # # : => { # : => { # => # }, # }, # # === Permitted Object Types # :string:: A string value, represented as BerIdentifiedString. # :integer:: An integer value, represented with Fixnum. # :oid:: An Object Identifier value; see X.690 section # 8.19. Currently represented with a standard array, # but may be better represented as a # BerIdentifiedOID object. # :array:: A sequence, represented as BerIdentifiedArray. # :boolean:: A boolean value, represented as +true+ or +false+. # :null:: A null value, represented as BerIdentifiedNull. # # === Example # Net::LDAP defines its ASN.1 BER syntax something like this: # # class Net::LDAP # AsnSyntax = Net::BER.compile_syntax({ # :application => { # :primitive => { # 2 => :null, # }, # :constructed => { # 0 => :array, # # ... # }, # }, # :context_specific => { # :primitive => { # 0 => :string, # # ... # }, # :constructed => { # 0 => :array, # # ... # }, # } # }) # end # # NOTE:: For readability and formatting purposes, Net::LDAP and its # siblings actually construct their syntaxes more deliberately, # as shown below. Since a hash is passed in the end in any case, # the format does not matter. # # primitive = { 2 => :null } # constructed = { # 0 => :array, # # ... # } # application = { # :primitive => primitive, # :constructed => constructed # } # # primitive = { # 0 => :string, # # ... # } # constructed = { # 0 => :array, # # ... # } # context_specific = { # :primitive => primitive, # :constructed => constructed # } # AsnSyntax = Net::BER.compile_syntax(:application => application, # :context_specific => context_specific) def self.compile_syntax(syntax) # TODO 20100327 AZ: Should we be allocating an array of 256 values # that will either be +nil+ or an object type symbol, or should we # allocate an empty Hash since unknown values return +nil+ anyway? out = [ nil ] * 256 syntax.each do |tag_class_id, encodings| tag_class = TAG_CLASS[tag_class_id] encodings.each do |encoding_id, classes| encoding = ENCODING_TYPE[encoding_id] object_class = tag_class + encoding classes.each do |number, object_type| out[object_class + number] = object_type end end end out end end end class Net::BER::BerError < RuntimeError; end ## # An Array object with a BER identifier attached. class Net::BER::BerIdentifiedArray < Array attr_accessor :ber_identifier def initialize(*args) super end end ## # A BER object identifier. class Net::BER::BerIdentifiedOid attr_accessor :ber_identifier def initialize(oid) if oid.is_a?(String) oid = oid.split(/\./).map {|s| s.to_i } end @value = oid end def to_ber to_ber_oid end def to_ber_oid @value.to_ber_oid end def to_s @value.join(".") end def to_arr @value.dup end end ## # A String object with a BER identifier attached. class Net::BER::BerIdentifiedString < String attr_accessor :ber_identifier def initialize args super args # LDAP uses UTF-8 encoded strings self.encode('UTF-8') if self.respond_to?(:encoding) rescue self end end module Net::BER ## # A BER null object. class BerIdentifiedNull attr_accessor :ber_identifier def to_ber "\005\000" end end ## # The default BerIdentifiedNull object. Null = Net::BER::BerIdentifiedNull.new end require 'net/ber/core_ext' net-ldap-0.8.0/lib/net/ldap.rb0000644000004100000410000021026712404411554016064 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'ostruct' module Net # :nodoc: class LDAP begin require 'openssl' ## # Set to +true+ if OpenSSL is available and LDAPS is supported. HasOpenSSL = true rescue LoadError # :stopdoc: HasOpenSSL = false # :startdoc: end end end require 'socket' require 'net/ber' require 'net/ldap/pdu' require 'net/ldap/filter' require 'net/ldap/dataset' require 'net/ldap/password' require 'net/ldap/entry' require 'net/ldap/instrumentation' require 'net/ldap/version' # == Quick-start for the Impatient # === Quick Example of a user-authentication against an LDAP directory: # # require 'rubygems' # require 'net/ldap' # # ldap = Net::LDAP.new # ldap.host = your_server_ip_address # ldap.port = 389 # ldap.auth "joe_user", "opensesame" # if ldap.bind # # authentication succeeded # else # # authentication failed # end # # # === Quick Example of a search against an LDAP directory: # # require 'rubygems' # require 'net/ldap' # # ldap = Net::LDAP.new :host => server_ip_address, # :port => 389, # :auth => { # :method => :simple, # :username => "cn=manager, dc=example, dc=com", # :password => "opensesame" # } # # filter = Net::LDAP::Filter.eq("cn", "George*") # treebase = "dc=example, dc=com" # # ldap.search(:base => treebase, :filter => filter) do |entry| # puts "DN: #{entry.dn}" # entry.each do |attribute, values| # puts " #{attribute}:" # values.each do |value| # puts " --->#{value}" # end # end # end # # p ldap.get_operation_result # # # == A Brief Introduction to LDAP # # We're going to provide a quick, informal introduction to LDAP terminology # and typical operations. If you're comfortable with this material, skip # ahead to "How to use Net::LDAP." If you want a more rigorous treatment of # this material, we recommend you start with the various IETF and ITU # standards that relate to LDAP. # # === Entities # LDAP is an Internet-standard protocol used to access directory servers. # The basic search unit is the entity, which corresponds to a person # or other domain-specific object. A directory service which supports the # LDAP protocol typically stores information about a number of entities. # # === Principals # LDAP servers are typically used to access information about people, but # also very often about such items as printers, computers, and other # resources. To reflect this, LDAP uses the term entity, or less # commonly, principal, to denote its basic data-storage unit. # # === Distinguished Names # In LDAP's view of the world, an entity is uniquely identified by a # globally-unique text string called a Distinguished Name, originally # defined in the X.400 standards from which LDAP is ultimately derived. Much # like a DNS hostname, a DN is a "flattened" text representation of a string # of tree nodes. Also like DNS (and unlike Java package names), a DN # expresses a chain of tree-nodes written from left to right in order from # the most-resolved node to the most-general one. # # If you know the DN of a person or other entity, then you can query an # LDAP-enabled directory for information (attributes) about the entity. # Alternatively, you can query the directory for a list of DNs matching a # set of criteria that you supply. # # === Attributes # # In the LDAP view of the world, a DN uniquely identifies an entity. # Information about the entity is stored as a set of Attributes. An # attribute is a text string which is associated with zero or more values. # Most LDAP-enabled directories store a well-standardized range of # attributes, and constrain their values according to standard rules. # # A good example of an attribute is sn, which stands for "Surname." # This attribute is generally used to store a person's surname, or last # name. Most directories enforce the standard convention that an entity's # sn attribute have exactly one value. In LDAP jargon, that # means that sn must be present and single-valued. # # Another attribute is mail, which is used to store email # addresses. (No, there is no attribute called "email, " perhaps because # X.400 terminology predates the invention of the term email.) # mail differs from sn in that most directories permit any # number of values for the mail attribute, including zero. # # === Tree-Base # We said above that X.400 Distinguished Names are globally unique. # In a manner reminiscent of DNS, LDAP supposes that each directory server # contains authoritative attribute data for a set of DNs corresponding to a # specific sub-tree of the (notional) global directory tree. This subtree is # generally configured into a directory server when it is created. It # matters for this discussion because most servers will not allow you to # query them unless you specify a correct tree-base. # # Let's say you work for the engineering department of Big Company, Inc., # whose internet domain is bigcompany.com. You may find that your # departmental directory is stored in a server with a defined tree-base of # ou=engineering, dc=bigcompany, dc=com # You will need to supply this string as the tree-base when querying # this directory. (Ou is a very old X.400 term meaning "organizational # unit." Dc is a more recent term meaning "domain component.") # # === LDAP Versions # (stub, discuss v2 and v3) # # === LDAP Operations # The essential operations are: #bind, #search, #add, #modify, #delete, and # #rename. # # ==== Bind # #bind supplies a user's authentication credentials to a server, which in # turn verifies or rejects them. There is a range of possibilities for # credentials, but most directories support a simple username and password # authentication. # # Taken by itself, #bind can be used to authenticate a user against # information stored in a directory, for example to permit or deny access to # some other resource. In terms of the other LDAP operations, most # directories require a successful #bind to be performed before the other # operations will be permitted. Some servers permit certain operations to be # performed with an "anonymous" binding, meaning that no credentials are # presented by the user. (We're glossing over a lot of platform-specific # detail here.) # # ==== Search # Calling #search against the directory involves specifying a treebase, a # set of search filters, and a list of attribute values. The filters # specify ranges of possible values for particular attributes. Multiple # filters can be joined together with AND, OR, and NOT operators. A server # will respond to a #search by returning a list of matching DNs together # with a set of attribute values for each entity, depending on what # attributes the search requested. # # ==== Add # #add specifies a new DN and an initial set of attribute values. If the # operation succeeds, a new entity with the corresponding DN and attributes # is added to the directory. # # ==== Modify # #modify specifies an entity DN, and a list of attribute operations. # #modify is used to change the attribute values stored in the directory for # a particular entity. #modify may add or delete attributes (which are lists # of values) or it change attributes by adding to or deleting from their # values. Net::LDAP provides three easier methods to modify an entry's # attribute values: #add_attribute, #replace_attribute, and # #delete_attribute. # # ==== Delete # #delete specifies an entity DN. If it succeeds, the entity and all its # attributes is removed from the directory. # # ==== Rename (or Modify RDN) # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP # protocol. It responds to the often-arising need to change the DN of an # entity without discarding its attribute values. In earlier LDAP versions, # the only way to do this was to delete the whole entity and add it again # with a different DN. # # #rename works by taking an "old" DN (the one to change) and a "new RDN, " # which is the left-most part of the DN string. If successful, #rename # changes the entity DN so that its left-most node corresponds to the new # RDN given in the request. (RDN, or "relative distinguished name, " denotes # a single tree-node as expressed in a DN, which is a chain of tree nodes.) # # == How to use Net::LDAP # To access Net::LDAP functionality in your Ruby programs, start by # requiring the library: # # require 'net/ldap' # # If you installed the Gem version of Net::LDAP, and depending on your # version of Ruby and rubygems, you _may_ also need to require rubygems # explicitly: # # require 'rubygems' # require 'net/ldap' # # Most operations with Net::LDAP start by instantiating a Net::LDAP object. # The constructor for this object takes arguments specifying the network # location (address and port) of the LDAP server, and also the binding # (authentication) credentials, typically a username and password. Given an # object of class Net:LDAP, you can then perform LDAP operations by calling # instance methods on the object. These are documented with usage examples # below. # # The Net::LDAP library is designed to be very disciplined about how it # makes network connections to servers. This is different from many of the # standard native-code libraries that are provided on most platforms, which # share bloodlines with the original Netscape/Michigan LDAP client # implementations. These libraries sought to insulate user code from the # workings of the network. This is a good idea of course, but the practical # effect has been confusing and many difficult bugs have been caused by the # opacity of the native libraries, and their variable behavior across # platforms. # # In general, Net::LDAP instance methods which invoke server operations make # a connection to the server when the method is called. They execute the # operation (typically binding first) and then disconnect from the server. # The exception is Net::LDAP#open, which makes a connection to the server # and then keeps it open while it executes a user-supplied block. # Net::LDAP#open closes the connection on completion of the block. class Net::LDAP include Net::LDAP::Instrumentation class LdapError < StandardError; end SearchScope_BaseObject = 0 SearchScope_SingleLevel = 1 SearchScope_WholeSubtree = 2 SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree ] DerefAliases_Never = 0 DerefAliases_Search = 1 DerefAliases_Find = 2 DerefAliases_Always = 3 DerefAliasesArray = [ DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always ] primitive = { 2 => :null } # UnbindRequest body constructed = { 0 => :array, # BindRequest 1 => :array, # BindResponse 2 => :array, # UnbindRequest 3 => :array, # SearchRequest 4 => :array, # SearchData 5 => :array, # SearchResult 6 => :array, # ModifyRequest 7 => :array, # ModifyResponse 8 => :array, # AddRequest 9 => :array, # AddResponse 10 => :array, # DelRequest 11 => :array, # DelResponse 12 => :array, # ModifyRdnRequest 13 => :array, # ModifyRdnResponse 14 => :array, # CompareRequest 15 => :array, # CompareResponse 16 => :array, # AbandonRequest 19 => :array, # SearchResultReferral 24 => :array, # Unsolicited Notification } application = { :primitive => primitive, :constructed => constructed, } primitive = { 0 => :string, # password 1 => :string, # Kerberos v4 2 => :string, # Kerberos v5 3 => :string, # SearchFilter-extensible 4 => :string, # SearchFilter-extensible 7 => :string, # serverSaslCreds } constructed = { 0 => :array, # RFC-2251 Control and Filter-AND 1 => :array, # SearchFilter-OR 2 => :array, # SearchFilter-NOT 3 => :array, # Seach referral 4 => :array, # unknown use in Microsoft Outlook 5 => :array, # SearchFilter-GE 6 => :array, # SearchFilter-LE 7 => :array, # serverSaslCreds 9 => :array, # SearchFilter-extensible } context_specific = { :primitive => primitive, :constructed => constructed, } AsnSyntax = Net::BER.compile_syntax(:application => application, :context_specific => context_specific) DefaultHost = "127.0.0.1" DefaultPort = 389 DefaultAuth = { :method => :anonymous } DefaultTreebase = "dc=com" DefaultForceNoPage = false StartTlsOid = "1.3.6.1.4.1.1466.20037" ResultStrings = { 0 => "Success", 1 => "Operations Error", 2 => "Protocol Error", 3 => "Time Limit Exceeded", 4 => "Size Limit Exceeded", 10 => "Referral", 12 => "Unavailable crtical extension", 14 => "saslBindInProgress", 16 => "No Such Attribute", 17 => "Undefined Attribute Type", 19 => "Constraint Violation", 20 => "Attribute or Value Exists", 32 => "No Such Object", 34 => "Invalid DN Syntax", 48 => "Inappropriate Authentication", 49 => "Invalid Credentials", 50 => "Insufficient Access Rights", 51 => "Busy", 52 => "Unavailable", 53 => "Unwilling to perform", 65 => "Object Class Violation", 68 => "Entry Already Exists" } module LDAPControls PAGED_RESULTS = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696 SORT_REQUEST = "1.2.840.113556.1.4.473" SORT_RESPONSE = "1.2.840.113556.1.4.474" DELETE_TREE = "1.2.840.113556.1.4.805" end def self.result2string(code) #:nodoc: ResultStrings[code] || "unknown result (#{code})" end attr_accessor :host attr_accessor :port attr_accessor :base # Instantiate an object of type Net::LDAP to perform directory operations. # This constructor takes a Hash containing arguments, all of which are # either optional or may be specified later with other methods as # described below. The following arguments are supported: # * :host => the LDAP server's IP-address (default 127.0.0.1) # * :port => the LDAP server's TCP port (default 389) # * :auth => a Hash containing authorization parameters. Currently # supported values include: {:method => :anonymous} and {:method => # :simple, :username => your_user_name, :password => your_password } # The password parameter may be a Proc that returns a String. # * :base => a default treebase parameter for searches performed against # the LDAP server. If you don't give this value, then each call to # #search must specify a treebase parameter. If you do give this value, # then it will be used in subsequent calls to #search that do not # specify a treebase. If you give a treebase value in any particular # call to #search, that value will override any treebase value you give # here. # * :encryption => specifies the encryption to be used in communicating # with the LDAP server. The value is either a Hash containing additional # parameters, or the Symbol :simple_tls, which is equivalent to # specifying the Hash {:method => :simple_tls}. There is a fairly large # range of potential values that may be given for this parameter. See # #encryption for details. # * :force_no_page => Set to true to prevent paged results even if your # server says it supports them. This is a fix for MS Active Directory # * :instrumentation_service => An object responsible for instrumenting # operations, compatible with ActiveSupport::Notifications' public API. # # Instantiating a Net::LDAP object does not result in network # traffic to the LDAP server. It simply stores the connection and binding # parameters in the object. def initialize(args = {}) @host = args[:host] || DefaultHost @port = args[:port] || DefaultPort @verbose = false # Make this configurable with a switch on the class. @auth = args[:auth] || DefaultAuth @base = args[:base] || DefaultTreebase @force_no_page = args[:force_no_page] || DefaultForceNoPage encryption args[:encryption] # may be nil if pr = @auth[:password] and pr.respond_to?(:call) @auth[:password] = pr.call end @instrumentation_service = args[:instrumentation_service] # This variable is only set when we are created with LDAP::open. All of # our internal methods will connect using it, or else they will create # their own. @open_connection = nil end # Convenience method to specify authentication credentials to the LDAP # server. Currently supports simple authentication requiring a username # and password. # # Observe that on most LDAP servers, the username is a complete DN. # However, with A/D, it's often possible to give only a user-name rather # than a complete DN. In the latter case, beware that many A/D servers are # configured to permit anonymous (uncredentialled) binding, and will # silently accept your binding as anonymous if you give an unrecognized # username. This is not usually what you want. (See # #get_operation_result.) # # Important: The password argument may be a Proc that returns a # string. This makes it possible for you to write client programs that # solicit passwords from users or from other data sources without showing # them in your code or on command lines. # # require 'net/ldap' # # ldap = Net::LDAP.new # ldap.host = server_ip_address # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", "your_psw" # # Alternatively (with a password block): # # require 'net/ldap' # # ldap = Net::LDAP.new # ldap.host = server_ip_address # psw = proc { your_psw_function } # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", psw # def authenticate(username, password) password = password.call if password.respond_to?(:call) @auth = { :method => :simple, :username => username, :password => password } end alias_method :auth, :authenticate # Convenience method to specify encryption characteristics for connections # to LDAP servers. Called implicitly by #new and #open, but may also be # called by user code if desired. The single argument is generally a Hash # (but see below for convenience alternatives). This implementation is # currently a stub, supporting only a few encryption alternatives. As # additional capabilities are added, more configuration values will be # added here. # # Currently, the only supported argument is { :method => :simple_tls }. # (Equivalently, you may pass the symbol :simple_tls all by itself, # without enclosing it in a Hash.) # # The :simple_tls encryption method encrypts all communications # with the LDAP server. It completely establishes SSL/TLS encryption with # the LDAP server before any LDAP-protocol data is exchanged. There is no # plaintext negotiation and no special encryption-request controls are # sent to the server. The :simple_tls option is the simplest, easiest # way to encrypt communications between Net::LDAP and LDAP servers. # It's intended for cases where you have an implicit level of trust in the # authenticity of the LDAP server. No validation of the LDAP server's SSL # certificate is performed. This means that :simple_tls will not produce # errors if the LDAP server's encryption certificate is not signed by a # well-known Certification Authority. If you get communications or # protocol errors when using this option, check with your LDAP server # administrator. Pay particular attention to the TCP port you are # connecting to. It's impossible for an LDAP server to support plaintext # LDAP communications and simple TLS connections on the same port. # The standard TCP port for unencrypted LDAP connections is 389, but the # standard port for simple-TLS encrypted connections is 636. Be sure you # are using the correct port. # # [Note: a future version of Net::LDAP will support the STARTTLS LDAP # control, which will enable encrypted communications on the same TCP port # used for unencrypted connections.] def encryption(args) case args when :simple_tls, :start_tls args = { :method => args } end @encryption = args end # #open takes the same parameters as #new. #open makes a network # connection to the LDAP server and then passes a newly-created Net::LDAP # object to the caller-supplied block. Within the block, you can call any # of the instance methods of Net::LDAP to perform operations against the # LDAP directory. #open will perform all the operations in the # user-supplied block on the same network connection, which will be closed # automatically when the block finishes. # # # (PSEUDOCODE) # auth = { :method => :simple, :username => username, :password => password } # Net::LDAP.open(:host => ipaddress, :port => 389, :auth => auth) do |ldap| # ldap.search(...) # ldap.add(...) # ldap.modify(...) # end def self.open(args) ldap1 = new(args) ldap1.open { |ldap| yield ldap } end # Returns a meaningful result any time after a protocol operation (#bind, # #search, #add, #modify, #rename, #delete) has completed. It returns an # #OpenStruct containing an LDAP result code (0 means success), and a # human-readable string. # # unless ldap.bind # puts "Result: #{ldap.get_operation_result.code}" # puts "Message: #{ldap.get_operation_result.message}" # end # # Certain operations return additional information, accessible through # members of the object returned from #get_operation_result. Check # #get_operation_result.error_message and # #get_operation_result.matched_dn. # #-- # Modified the implementation, 20Mar07. We might get a hash of LDAP # response codes instead of a simple numeric code. #++ def get_operation_result result = @result result = result.result if result.is_a?(Net::LDAP::PDU) os = OpenStruct.new if result.is_a?(Hash) # We might get a hash of LDAP response codes instead of a simple # numeric code. os.code = (result[:resultCode] || "").to_i os.error_message = result[:errorMessage] os.matched_dn = result[:matchedDN] elsif result os.code = result else os.code = 0 end os.message = Net::LDAP.result2string(os.code) os end # Opens a network connection to the server and then passes self # to the caller-supplied block. The connection is closed when the block # completes. Used for executing multiple LDAP operations without requiring # a separate network connection (and authentication) for each one. # Note: You do not need to log-in or "bind" to the server. This # will be done for you automatically. For an even simpler approach, see # the class method Net::LDAP#open. # # # (PSEUDOCODE) # auth = { :method => :simple, :username => username, :password => password } # ldap = Net::LDAP.new(:host => ipaddress, :port => 389, :auth => auth) # ldap.open do |ldap| # ldap.search(...) # ldap.add(...) # ldap.modify(...) # end def open # First we make a connection and then a binding, but we don't do # anything with the bind results. We then pass self to the caller's # block, where he will execute his LDAP operations. Of course they will # all generate auth failures if the bind was unsuccessful. raise Net::LDAP::LdapError, "Open already in progress" if @open_connection instrument "open.net_ldap" do |payload| begin @open_connection = Net::LDAP::Connection.new \ :host => @host, :port => @port, :encryption => @encryption, :instrumentation_service => @instrumentation_service payload[:connection] = @open_connection payload[:bind] = @open_connection.bind(@auth) yield self ensure @open_connection.close if @open_connection @open_connection = nil end end end # Searches the LDAP directory for directory entries. Takes a hash argument # with parameters. Supported parameters include: # * :base (a string specifying the tree-base for the search); # * :filter (an object of type Net::LDAP::Filter, defaults to # objectclass=*); # * :attributes (a string or array of strings specifying the LDAP # attributes to return from the server); # * :return_result (a boolean specifying whether to return a result set). # * :attributes_only (a boolean flag, defaults false) # * :scope (one of: Net::LDAP::SearchScope_BaseObject, # Net::LDAP::SearchScope_SingleLevel, # Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.) # * :size (an integer indicating the maximum number of search entries to # return. Default is zero, which signifies no limit.) # * :deref (one of: Net::LDAP::DerefAliases_Never, Net::LDAP::DerefAliases_Search, # Net::LDAP::DerefAliases_Find, Net::LDAP::DerefAliases_Always. Default is Never.) # # #search queries the LDAP server and passes each entry to the # caller-supplied block, as an object of type Net::LDAP::Entry. If the # search returns 1000 entries, the block will be called 1000 times. If the # search returns no entries, the block will not be called. # # #search returns either a result-set or a boolean, depending on the value # of the :return_result argument. The default behavior is to # return a result set, which is an Array of objects of class # Net::LDAP::Entry. If you request a result set and #search fails with an # error, it will return nil. Call #get_operation_result to get the error # information returned by # the LDAP server. # # When :return_result => false, #search will return only a # Boolean, to indicate whether the operation succeeded. This can improve # performance with very large result sets, because the library can discard # each entry from memory after your block processes it. # # treebase = "dc=example, dc=com" # filter = Net::LDAP::Filter.eq("mail", "a*.com") # attrs = ["mail", "cn", "sn", "objectclass"] # ldap.search(:base => treebase, :filter => filter, :attributes => attrs, # :return_result => false) do |entry| # puts "DN: #{entry.dn}" # entry.each do |attr, values| # puts ".......#{attr}:" # values.each do |value| # puts " #{value}" # end # end # end def search(args = {}) unless args[:ignore_server_caps] args[:paged_searches_supported] = paged_searches_supported? end args[:base] ||= @base return_result_set = args[:return_result] != false result_set = return_result_set ? [] : nil instrument "search.net_ldap", args do |payload| if @open_connection @result = @open_connection.search(args) { |entry| result_set << entry if result_set yield entry if block_given? } else begin conn = Net::LDAP::Connection.new \ :host => @host, :port => @port, :encryption => @encryption, :instrumentation_service => @instrumentation_service if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 @result = conn.search(args) { |entry| result_set << entry if result_set yield entry if block_given? } end ensure conn.close if conn end end if return_result_set (!@result.nil? && @result.result_code == 0) ? result_set : nil else @result.success? end end end # #bind connects to an LDAP server and requests authentication based on # the :auth parameter passed to #open or #new. It takes no # parameters. # # User code does not need to call #bind directly. It will be called # implicitly by the library whenever you invoke an LDAP operation, such as # #search or #add. # # It is useful, however, to call #bind in your own code when the only # operation you intend to perform against the directory is to validate a # login credential. #bind returns true or false to indicate whether the # binding was successful. Reasons for failure include malformed or # unrecognized usernames and incorrect passwords. Use # #get_operation_result to find out what happened in case of failure. # # Here's a typical example using #bind to authenticate a credential which # was (perhaps) solicited from the user of a web site: # # require 'net/ldap' # ldap = Net::LDAP.new # ldap.host = your_server_ip_address # ldap.port = 389 # ldap.auth your_user_name, your_user_password # if ldap.bind # # authentication succeeded # else # # authentication failed # p ldap.get_operation_result # end # # Here's a more succinct example which does exactly the same thing, but # collects all the required parameters into arguments: # # require 'net/ldap' # ldap = Net::LDAP.new(:host => your_server_ip_address, :port => 389) # if ldap.bind(:method => :simple, :username => your_user_name, # :password => your_user_password) # # authentication succeeded # else # # authentication failed # p ldap.get_operation_result # end # # You don't need to pass a user-password as a String object to bind. You # can also pass a Ruby Proc object which returns a string. This will cause # bind to execute the Proc (which might then solicit input from a user # with console display suppressed). The String value returned from the # Proc is used as the password. # # You don't have to create a new instance of Net::LDAP every time you # perform a binding in this way. If you prefer, you can cache the # Net::LDAP object and re-use it to perform subsequent bindings, # provided you call #auth to specify a new credential before # calling #bind. Otherwise, you'll just re-authenticate the previous user! # (You don't need to re-set the values of #host and #port.) As noted in # the documentation for #auth, the password parameter can be a Ruby Proc # instead of a String. def bind(auth = @auth) instrument "bind.net_ldap" do |payload| if @open_connection payload[:connection] = @open_connection payload[:bind] = @result = @open_connection.bind(auth) else begin conn = Connection.new \ :host => @host, :port => @port, :encryption => @encryption, :instrumentation_service => @instrumentation_service payload[:connection] = conn payload[:bind] = @result = conn.bind(auth) ensure conn.close if conn end end @result.success? end end # #bind_as is for testing authentication credentials. # # As described under #bind, most LDAP servers require that you supply a # complete DN as a binding-credential, along with an authenticator such as # a password. But for many applications (such as authenticating users to a # Rails application), you often don't have a full DN to identify the user. # You usually get a simple identifier like a username or an email address, # along with a password. #bind_as allows you to authenticate these # user-identifiers. # # #bind_as is a combination of a search and an LDAP binding. First, it # connects and binds to the directory as normal. Then it searches the # directory for an entry corresponding to the email address, username, or # other string that you supply. If the entry exists, then #bind_as will # re-bind as that user with the password (or other authenticator) # that you supply. # # #bind_as takes the same parameters as #search, with the addition of # an authenticator. Currently, this authenticator must be # :password. Its value may be either a String, or a +proc+ that # returns a String. #bind_as returns +false+ on failure. On success, it # returns a result set, just as #search does. This result set is an Array # of objects of type Net::LDAP::Entry. It contains the directory # attributes corresponding to the user. (Just test whether the return # value is logically true, if you don't need this additional information.) # # Here's how you would use #bind_as to authenticate an email address and # password: # # require 'net/ldap' # # user, psw = "joe_user@yourcompany.com", "joes_psw" # # ldap = Net::LDAP.new # ldap.host = "192.168.0.100" # ldap.port = 389 # ldap.auth "cn=manager, dc=yourcompany, dc=com", "topsecret" # # result = ldap.bind_as(:base => "dc=yourcompany, dc=com", # :filter => "(mail=#{user})", # :password => psw) # if result # puts "Authenticated #{result.first.dn}" # else # puts "Authentication FAILED." # end def bind_as(args = {}) result = false open { |me| rs = search args if rs and rs.first and dn = rs.first.dn password = args[:password] password = password.call if password.respond_to?(:call) result = rs if bind(:method => :simple, :username => dn, :password => password) end } result end # Adds a new entry to the remote LDAP server. # Supported arguments: # :dn :: Full DN of the new entry # :attributes :: Attributes of the new entry. # # The attributes argument is supplied as a Hash keyed by Strings or # Symbols giving the attribute name, and mapping to Strings or Arrays of # Strings giving the actual attribute values. Observe that most LDAP # directories enforce schema constraints on the attributes contained in # entries. #add will fail with a server-generated error if your attributes # violate the server-specific constraints. # # Here's an example: # # dn = "cn=George Smith, ou=people, dc=example, dc=com" # attr = { # :cn => "George Smith", # :objectclass => ["top", "inetorgperson"], # :sn => "Smith", # :mail => "gsmith@example.com" # } # Net::LDAP.open(:host => host) do |ldap| # ldap.add(:dn => dn, :attributes => attr) # end def add(args) instrument "add.net_ldap", args do |payload| if @open_connection @result = @open_connection.add(args) else @result = 0 begin conn = Connection.new \ :host => @host, :port => @port, :encryption => @encryption, :instrumentation_service => @instrumentation_service if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 @result = conn.add(args) end ensure conn.close if conn end end @result.success? end end # Modifies the attribute values of a particular entry on the LDAP # directory. Takes a hash with arguments. Supported arguments are: # :dn :: (the full DN of the entry whose attributes are to be modified) # :operations :: (the modifications to be performed, detailed next) # # This method returns True or False to indicate whether the operation # succeeded or failed, with extended information available by calling # #get_operation_result. # # Also see #add_attribute, #replace_attribute, or #delete_attribute, which # provide simpler interfaces to this functionality. # # The LDAP protocol provides a full and well thought-out set of operations # for changing the values of attributes, but they are necessarily somewhat # complex and not always intuitive. If these instructions are confusing or # incomplete, please send us email or create an issue on GitHub. # # The :operations parameter to #modify takes an array of # operation-descriptors. Each individual operation is specified in one # element of the array, and most LDAP servers will attempt to perform the # operations in order. # # Each of the operations appearing in the Array must itself be an Array # with exactly three elements: an operator:: must be :add, :replace, or # :delete an attribute name:: the attribute name (string or symbol) to # modify a value:: either a string or an array of strings. # # The :add operator will, unsurprisingly, add the specified values to the # specified attribute. If the attribute does not already exist, :add will # create it. Most LDAP servers will generate an error if you try to add a # value that already exists. # # :replace will erase the current value(s) for the specified attribute, if # there are any, and replace them with the specified value(s). # # :delete will remove the specified value(s) from the specified attribute. # If you pass nil, an empty string, or an empty array as the value # parameter to a :delete operation, the _entire_ _attribute_ will be # deleted, along with all of its values. # # For example: # # dn = "mail=modifyme@example.com, ou=people, dc=example, dc=com" # ops = [ # [:add, :mail, "aliasaddress@example.com"], # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]], # [:delete, :sn, nil] # ] # ldap.modify :dn => dn, :operations => ops # # (This example is contrived since you probably wouldn't add a mail # value right before replacing the whole attribute, but it shows that # order of execution matters. Also, many LDAP servers won't let you delete # SN because that would be a schema violation.) # # It's essential to keep in mind that if you specify more than one # operation in a call to #modify, most LDAP servers will attempt to # perform all of the operations in the order you gave them. This matters # because you may specify operations on the same attribute which must be # performed in a certain order. # # Most LDAP servers will _stop_ processing your modifications if one of # them causes an error on the server (such as a schema-constraint # violation). If this happens, you will probably get a result code from # the server that reflects only the operation that failed, and you may or # may not get extended information that will tell you which one failed. # #modify has no notion of an atomic transaction. If you specify a chain # of modifications in one call to #modify, and one of them fails, the # preceding ones will usually not be "rolled back, " resulting in a # partial update. This is a limitation of the LDAP protocol, not of # Net::LDAP. # # The lack of transactional atomicity in LDAP means that you're usually # better off using the convenience methods #add_attribute, # #replace_attribute, and #delete_attribute, which are are wrappers over # #modify. However, certain LDAP servers may provide concurrency # semantics, in which the several operations contained in a single #modify # call are not interleaved with other modification-requests received # simultaneously by the server. It bears repeating that this concurrency # does _not_ imply transactional atomicity, which LDAP does not provide. def modify(args) instrument "modify.net_ldap", args do |payload| if @open_connection @result = @open_connection.modify(args) else @result = 0 begin conn = Connection.new \ :host => @host, :port => @port, :encryption => @encryption, :instrumentation_service => @instrumentation_service if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 @result = conn.modify(args) end ensure conn.close if conn end 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 # violations), #add_attribute will create it with the caller-specified # values. If the attribute already exists (and there are no schema # violations), the caller-specified values will be _added_ to the values # already present. # # Returns True or False to indicate whether the operation succeeded or # failed, with extended information available by calling # #get_operation_result. See also #replace_attribute and # #delete_attribute. # # dn = "cn=modifyme, dc=example, dc=com" # ldap.add_attribute dn, :mail, "newmailaddress@example.com" def add_attribute(dn, attribute, value) modify(:dn => dn, :operations => [[:add, attribute, value]]) end # Replace the value of an attribute. #replace_attribute can be thought of # as equivalent to calling #delete_attribute followed by #add_attribute. # It 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, it will be created with the caller-specified value(s). If the # attribute does exist, its values will be _discarded_ and replaced with # the caller-specified values. # # Returns True or False to indicate whether the operation succeeded or # failed, with extended information available by calling # #get_operation_result. See also #add_attribute and #delete_attribute. # # dn = "cn=modifyme, dc=example, dc=com" # ldap.replace_attribute dn, :mail, "newmailaddress@example.com" def replace_attribute(dn, attribute, value) modify(:dn => dn, :operations => [[:replace, attribute, value]]) end # Delete an attribute and all its values. Takes the full DN of the entry # to modify, and the name (Symbol or String) of the attribute to delete. # # Returns True or False to indicate whether the operation succeeded or # failed, with extended information available by calling # #get_operation_result. See also #add_attribute and #replace_attribute. # # dn = "cn=modifyme, dc=example, dc=com" # ldap.delete_attribute dn, :mail def delete_attribute(dn, attribute) modify(:dn => dn, :operations => [[:delete, attribute, nil]]) end # Rename an entry on the remote DIS by changing the last RDN of its DN. # # _Documentation_ _stub_ def rename(args) instrument "rename.net_ldap", args do |payload| if @open_connection @result = @open_connection.rename(args) else @result = 0 begin conn = Connection.new \ :host => @host, :port => @port, :encryption => @encryption, :instrumentation_service => @instrumentation_service if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 @result = conn.rename(args) end ensure conn.close if conn end end @result.success? end end alias_method :modify_rdn, :rename # Delete an entry from the LDAP directory. Takes a hash of arguments. The # only supported argument is :dn, which must give the complete DN of the # entry to be deleted. # # Returns True or False to indicate whether the delete succeeded. Extended # status information is available by calling #get_operation_result. # # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com" # ldap.delete :dn => dn def delete(args) instrument "delete.net_ldap", args do |payload| if @open_connection @result = @open_connection.delete(args) else @result = 0 begin conn = Connection.new \ :host => @host, :port => @port, :encryption => @encryption, :instrumentation_service => @instrumentation_service if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 @result = conn.delete(args) end ensure conn.close end end @result.success? end end # Delete an entry from the LDAP directory along with all subordinate entries. # the regular delete method will fail to delete an entry if it has subordinate # entries. This method sends an extra control code to tell the LDAP server # to do a tree delete. ('1.2.840.113556.1.4.805') # # Returns True or False to indicate whether the delete succeeded. Extended # status information is available by calling #get_operation_result. # # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com" # ldap.delete_tree :dn => dn def delete_tree(args) delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]])) end # This method is experimental and subject to change. Return the rootDSE # record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if # the server doesn't return the record. #-- # cf. RFC4512 graf 5.1. # Note that the rootDSE record we return on success has an empty DN, which # is correct. On failure, the empty Entry will have a nil DN. There's no # real reason for that, so it can be changed if desired. The funky # number-disagreements in the set of attribute names is correct per the # RFC. We may be called by #search itself, which may need to determine # things like paged search capabilities. So to avoid an infinite regress, # set :ignore_server_caps, which prevents us getting called recursively. #++ def search_root_dse rs = search(:ignore_server_caps => true, :base => "", :scope => SearchScope_BaseObject, :attributes => [ :namingContexts, :supportedLdapVersion, :altServer, :supportedControl, :supportedExtension, :supportedFeatures, :supportedSASLMechanisms]) (rs and rs.first) or Net::LDAP::Entry.new end # Return the root Subschema record from the LDAP server as a # Net::LDAP::Entry, or an empty Entry if the server doesn't return the # record. On success, the Net::LDAP::Entry returned from this call will # have the attributes :dn, :objectclasses, and :attributetypes. If there # is an error, call #get_operation_result for more information. # # ldap = Net::LDAP.new # ldap.host = "your.ldap.host" # ldap.auth "your-user-dn", "your-psw" # subschema_entry = ldap.search_subschema_entry # # subschema_entry.attributetypes.each do |attrtype| # # your code # end # # subschema_entry.objectclasses.each do |attrtype| # # your code # end #-- # cf. RFC4512 section 4, particulary graff 4.4. # The :dn attribute in the returned Entry is the subschema name as # returned from the server. Set :ignore_server_caps, see the notes in # search_root_dse. #++ def search_subschema_entry rs = search(:ignore_server_caps => true, :base => "", :scope => SearchScope_BaseObject, :attributes => [:subschemaSubentry]) return Net::LDAP::Entry.new unless (rs and rs.first) subschema_name = rs.first.subschemasubentry 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 Net::LDAP::Entry.new end #-- # Convenience method to query server capabilities. # Only do this once per Net::LDAP object. # Note, we call a search, and we might be called from inside a search! # MUST refactor the root_dse call out. #++ def paged_searches_supported? # active directory returns that it supports paged results. However # it returns binary data in the rfc2696_cookie which throws an # encoding exception breaking searching. return false if @force_no_page @server_caps ||= search_root_dse @server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS) end end # class LDAP # This is a private class used internally by the library. It should not # be called by user code. class Net::LDAP::Connection #:nodoc: include Net::LDAP::Instrumentation LdapVersion = 3 MaxSaslChallenges = 10 def initialize(server) @instrumentation_service = server[:instrumentation_service] begin @conn = TCPSocket.new(server[:host], server[:port]) rescue SocketError raise Net::LDAP::LdapError, "No such address or other socket error." rescue Errno::ECONNREFUSED raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}." end if server[:encryption] setup_encryption server[:encryption] end yield self if block_given? end module GetbyteForSSLSocket def getbyte getc.ord end end module FixSSLSocketSyncClose def close super io.close end end def self.wrap_with_ssl(io) raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL ctx = OpenSSL::SSL::SSLContext.new conn = OpenSSL::SSL::SSLSocket.new(io, ctx) conn.connect # Doesn't work: # conn.sync_close = true conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte) conn.extend(FixSSLSocketSyncClose) conn end #-- # Helper method called only from new, and only after we have a # successfully-opened @conn instance variable, which is a TCP connection. # Depending on the received arguments, we establish SSL, potentially # replacing the value of @conn accordingly. Don't generate any errors here # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption # is requested and we have trouble setting it up. That includes if OpenSSL # is not set up on the machine. (Question: how does the Ruby OpenSSL # wrapper react in that case?) DO NOT filter exceptions raised by the # OpenSSL library. Let them pass back to the user. That should make it # easier for us to debug the problem reports. Presumably (hopefully?) that # will also produce recognizable errors if someone tries to use this on a # machine without OpenSSL. # # The simple_tls method is intended as the simplest, stupidest, easiest # solution for people who want nothing more than encrypted comms with the # LDAP server. It doesn't do any server-cert validation and requires # nothing in the way of key files and root-cert files, etc etc. OBSERVE: # WE REPLACE the value of @conn, which is presumed to be a connected # TCPSocket object. # # The start_tls method is supported by many servers over the standard LDAP # port. It does not require an alternative port for encrypted # communications, as with simple_tls. Thanks for Kouhei Sutou for # generously contributing the :start_tls path. #++ def setup_encryption(args) case args[:method] when :simple_tls @conn = self.class.wrap_with_ssl(@conn) # additional branches requiring server validation and peer certs, etc. # go here. when :start_tls msgid = next_msgid.to_ber request = [Net::LDAP::StartTlsOid.to_ber_contextspecific(0)].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) request_pkt = [msgid, request].to_ber_sequence write request_pkt be = read raise Net::LDAP::LdapError, "no start_tls result" if be.nil? pdu = Net::LDAP::PDU.new(be) raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil? if pdu.result_code.zero? @conn = self.class.wrap_with_ssl(@conn) else raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}" end else raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}" end end #-- # This is provided as a convenience method to make sure a connection # object gets closed without waiting for a GC to happen. Clients shouldn't # have to call it, but perhaps it will come in handy someday. #++ def close @conn.close @conn = nil end # Internal: Reads and parses data from the configured connection. # # - syntax: the BER syntax to use to parse the read data with # # Returns basic BER objects. def read(syntax = Net::LDAP::AsnSyntax) instrument "read.net_ldap_connection", :syntax => syntax do |payload| @conn.read_ber(syntax) do |id, content_length| payload[:object_type_id] = id payload[:content_length] = content_length end end end private :read # Internal: Writes the given packet to the configured connection. # # - packet: the BER data packet to write on the socket. # # Returns the return value from writing to the connection, which in some # cases is the Integer number of bytes written to the socket. def write(packet) instrument "write.net_ldap_connection" do |payload| payload[:content_length] = @conn.write(packet) end end private :write def next_msgid @msgid ||= 0 @msgid += 1 end def bind(auth) instrument "bind.net_ldap_connection" do |payload| payload[:method] = meth = auth[:method] if [:simple, :anonymous, :anon].include?(meth) bind_simple auth elsif meth == :sasl bind_sasl(auth) elsif meth == :gss_spnego bind_gss_spnego(auth) else raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})" end end end #-- # Implements a simple user/psw authentication. Accessed by calling #bind # with a method of :simple or :anonymous. #++ def bind_simple(auth) user, psw = if auth[:method] == :simple [auth[:username] || auth[:dn], auth[:password]] else ["", ""] end raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) msgid = next_msgid.to_ber request = [LdapVersion.to_ber, user.to_ber, psw.to_ber_contextspecific(0)].to_ber_appsequence(0) request_pkt = [msgid, request].to_ber_sequence write request_pkt (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" pdu end #-- # Required parameters: :mechanism, :initial_credential and # :challenge_response # # Mechanism is a string value that will be passed in the SASL-packet's # "mechanism" field. # # Initial credential is most likely a string. It's passed in the initial # BindRequest that goes to the server. In some protocols, it may be empty. # # Challenge-response is a Ruby proc that takes a single parameter and # returns an object that will typically be a string. The # challenge-response block is called when the server returns a # BindResponse with a result code of 14 (saslBindInProgress). The # challenge-response block receives a parameter containing the data # returned by the server in the saslServerCreds field of the LDAP # BindResponse packet. The challenge-response block may be called multiple # times during the course of a SASL authentication, and each time it must # return a value that will be passed back to the server as the credential # data in the next BindRequest packet. #++ def bind_sasl(auth) mech, cred, chall = auth[:mechanism], auth[:initial_credential], auth[:challenge_response] raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall) n = 0 loop { msgid = next_msgid.to_ber sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) request_pkt = [msgid, request].to_ber_sequence write request_pkt (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" return pdu unless pdu.result_code == 14 # saslBindInProgress raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges) cred = chall.call(pdu.result_server_sasl_creds) } raise Net::LDAP::LdapError, "why are we here?" end private :bind_sasl #-- # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to # integrate it without introducing an external dependency. # # This authentication method is accessed by calling #bind with a :method # parameter of :gss_spnego. It requires :username and :password # attributes, just like the :simple authentication method. It performs a # GSS-SPNEGO authentication with the server, which is presumed to be a # Microsoft Active Directory. #++ def bind_gss_spnego(auth) require 'ntlm' user, psw = [auth[:username] || auth[:dn], auth[:password]] raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) nego = proc { |challenge| t2_msg = NTLM::Message.parse(challenge) t3_msg = t2_msg.response({ :user => user, :password => psw }, { :ntlmv2 => true }) t3_msg.serialize } bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO", :initial_credential => NTLM::Message::Type1.new.serialize, :challenge_response => nego) end private :bind_gss_spnego #-- # Allow the caller to specify a sort control # # The format of the sort control needs to be: # # :sort_control => ["cn"] # just a string # or # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false) # or # :sort_control => ["givenname","sn"] #multiple strings or arrays # def encode_sort_controls(sort_definitions) return sort_definitions unless sort_definitions sort_control_values = sort_definitions.map do |control| control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder control[0] = String(control[0]).to_ber, control[1] = String(control[1]).to_ber, control[2] = (control[2] == true).to_ber control.to_ber_sequence end sort_control = [ Net::LDAP::LDAPControls::SORT_REQUEST.to_ber, false.to_ber, sort_control_values.to_ber_sequence.to_s.to_ber ].to_ber_sequence end #-- # Alternate implementation, this yields each search entry to the caller as # it are received. # # TODO: certain search parameters are hardcoded. # TODO: if we mis-parse the server results or the results are wrong, we # can block forever. That's because we keep reading results until we get a # type-5 packet, which might never come. We need to support the time-limit # in the protocol. #++ def search(args = {}) search_filter = (args && args[:filter]) || Net::LDAP::Filter.eq("objectclass", "*") search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String) search_base = (args && args[:base]) || "dc=example, dc=com" search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber} return_referrals = args && args[:return_referrals] == true sizelimit = (args && args[:size].to_i) || 0 raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0 paged_searches_supported = (args && args[:paged_searches_supported]) attributes_only = (args and args[:attributes_only] == true) scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope) sort_control = encode_sort_controls(args.fetch(:sort_controls){ false }) deref = args[:deref] || Net::LDAP::DerefAliases_Never raise Net::LDAP::LdapError.new( "invalid alias dereferencing value" ) unless Net::LDAP::DerefAliasesArray.include?(deref) # An interesting value for the size limit would be close to A/D's # built-in page limit of 1000 records, but openLDAP newer than version # 2.2.0 chokes on anything bigger than 126. You get a silent error that # is easily visible by running slapd in debug mode. Go figure. # # Changed this around 06Sep06 to support a caller-specified search-size # limit. Because we ALWAYS do paged searches, we have to work around the # problem that it's not legal to specify a "normal" sizelimit (in the # body of the search request) that is larger than the page size we're # requesting. Unfortunately, I have the feeling that this will break # with LDAP servers that don't support paged searches!!! # # (Because we pass zero as the sizelimit on search rounds when the # remaining limit is larger than our max page size of 126. In these # cases, I think the caller's search limit will be ignored!) # # CONFIRMED: This code doesn't work on LDAPs that don't support paged # searches when the size limit is larger than 126. We're going to have # to do a root-DSE record search and not do a paged search if the LDAP # doesn't support it. Yuck. rfc2696_cookie = [126, ""] result_pdu = nil n_results = 0 instrument "search.net_ldap_connection", :filter => search_filter, :base => search_base, :scope => scope, :limit => sizelimit, :sort => sort_control, :referrals => return_referrals, :deref => deref, :attributes => search_attributes do |payload| loop do # should collect this into a private helper to clarify the structure query_limit = 0 if sizelimit > 0 if paged_searches_supported query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - n_results) : 0) else query_limit = sizelimit end end request = [ search_base.to_ber, scope.to_ber_enumerated, deref.to_ber_enumerated, query_limit.to_ber, # size limit 0.to_ber, attributes_only.to_ber, search_filter.to_ber, search_attributes.to_ber_sequence ].to_ber_appsequence(3) # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory # this breaks when calling to_ber. (Can't force binary data to UTF-8) # we have to disable paging (even though server supports it) to get around this... controls = [] controls << [ Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber, # Criticality MUST be false to interoperate with normal LDAPs. false.to_ber, rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber ].to_ber_sequence if paged_searches_supported controls << sort_control if sort_control controls = controls.empty? ? nil : controls.to_ber_contextspecific(0) pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence write pkt result_pdu = nil controls = [] while (be = read) && (pdu = Net::LDAP::PDU.new(be)) case pdu.app_tag when Net::LDAP::PDU::SearchReturnedData n_results += 1 yield pdu.search_entry if block_given? when Net::LDAP::PDU::SearchResultReferral if return_referrals if block_given? se = Net::LDAP::Entry.new se[:search_referrals] = (pdu.search_referrals || []) yield se end end when Net::LDAP::PDU::SearchResult result_pdu = pdu controls = pdu.result_controls if return_referrals && pdu.result_code == 10 if block_given? se = Net::LDAP::Entry.new se[:search_referrals] = (pdu.search_referrals || []) yield se end end break else raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}" end end # count number of pages of results payload[:page_count] ||= 0 payload[:page_count] += 1 # When we get here, we have seen a type-5 response. If there is no # error AND there is an RFC-2696 cookie, then query again for the next # page of results. If not, we're done. Don't screw this up or we'll # break every search we do. # # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't # that have a parameter of AsnSyntax? Does this just accidentally # work? According to RFC-2696, the value expected in this position is # of type OCTET STRING, covered in the default syntax supported by # read_ber, so I guess we're ok. more_pages = false if result_pdu.result_code == 0 and controls controls.each do |c| if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS # just in case some bogus server sends us more than 1 of these. more_pages = false if c.value and c.value.length > 0 cookie = c.value.read_ber[1] if cookie and cookie.length > 0 rfc2696_cookie[1] = cookie more_pages = true end end end end end break unless more_pages end # loop # track total result count payload[:result_count] = n_results result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search") end # instrument end MODIFY_OPERATIONS = { #:nodoc: :add => 0, :delete => 1, :replace => 2 } def self.modify_ops(operations) ops = [] if operations operations.each { |op, attrib, values| # TODO, fix the following line, which gives a bogus error if the # opcode is invalid. op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set values = [ attrib.to_s.to_ber, values ].to_ber_sequence ops << [ op_ber, values ].to_ber } end ops end #-- # TODO: need to support a time limit, in case the server fails to respond. # TODO: We're throwing an exception here on empty DN. Should return a # proper error instead, probaby from farther up the chain. # TODO: If the user specifies a bogus opcode, we'll throw a confusing # error here ("to_ber_enumerated is not defined on nil"). #++ def modify(args) modify_dn = args[:dn] or raise "Unable to modify empty DN" ops = self.class.modify_ops args[:operations] request = [ modify_dn.to_ber, ops.to_ber_sequence ].to_ber_appsequence(6) pkt = [ next_msgid.to_ber, request ].to_ber_sequence write pkt (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::ModifyResponse) or raise Net::LDAP::LdapError, "response missing or invalid" 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 # rather than a simple result number. This is experimental, and eventually # we'll want to do this with all the others. The point is to have access # to the error message and the matched-DN returned by the server. #++ def add(args) add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN" add_attrs = [] a = args[:attributes] and a.each { |k, v| add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence } request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) pkt = [next_msgid.to_ber, request].to_ber_sequence write pkt (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::AddResponse) or raise Net::LDAP::LdapError, "response missing or invalid" pdu end #-- # TODO: need to support a time limit, in case the server fails to respond. #++ def rename(args) old_dn = args[:olddn] or raise "Unable to rename empty DN" new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" delete_attrs = args[:delete_attributes] ? true : false new_superior = args[:new_superior] request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber] request << new_superior.to_ber_contextspecific(0) unless new_superior == nil pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence write pkt (be = read) && (pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == Net::LDAP::PDU::ModifyRDNResponse) or raise Net::LDAP::LdapError.new( "response missing or invalid" ) pdu end #-- # TODO, need to support a time limit, in case the server fails to respond. #++ def delete(args) dn = args[:dn] or raise "Unable to delete empty DN" controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later request = dn.to_s.to_ber_application_string(10) pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence write pkt (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::DeleteResponse) or raise Net::LDAP::LdapError, "response missing or invalid" pdu end end # class Connection net-ldap-0.8.0/lib/net/ldap/0000755000004100000410000000000012404411554015527 5ustar www-datawww-datanet-ldap-0.8.0/lib/net/ldap/pdu.rb0000644000004100000410000002055412404411554016652 0ustar www-datawww-data# -*- 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 def error_message result[:errorMessage] || "" 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 def status result_code == 0 ? :success : :failure end def success? status == :success end def failure? !success? 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] } parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == 10 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 net-ldap-0.8.0/lib/net/ldap/password.rb0000644000004100000410000000234612404411554017723 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'digest/sha1' require 'digest/md5' require 'base64' require 'securerandom' class Net::LDAP::Password class << self # Generate a password-hash suitable for inclusion in an LDAP attribute. # Pass a hash type as a symbol (:md5, :sha, :ssha) 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: # * maybe salted-md5 # * Should we provide sha1 as a synonym for sha1? I vote no because then # should you also provide ssha1 for symmetry? # attribute_value = "" def generate(type, str) case type when :md5 attribute_value = '{MD5}' + Base64.encode64(Digest::MD5.digest(str)).chomp! when :sha attribute_value = '{SHA}' + Base64.encode64(Digest::SHA1.digest(str)).chomp! when :ssha salt = SecureRandom.random_bytes(16) attribute_value = '{SSHA}' + Base64.encode64(Digest::SHA1.digest(str + salt) + salt).chomp! else raise Net::LDAP::LdapError, "Unsupported password-hash type (#{type})" end return attribute_value end end end net-ldap-0.8.0/lib/net/ldap/filter.rb0000644000004100000410000006444612404411554017357 0ustar www-datawww-data# -*- 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 # :filter 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, :bineq ] 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 # mail. 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 sAMAccountName: # # 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 not supported due to limitations in the # underlying LDAP protocol. This example selects any entry with a # mail 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 a binary comparison. # this prevents the search data from being forced into a UTF-8 string. # # This is primarily used for Microsoft Active Directory to compare # GUID values. # # # for guid represented as hex charecters # guid = "6a31b4a12aa27a41aca9603f27dd5116" # guid_bin = [guid].pack("H*") # f = Net::LDAP::Filter.bineq("objectGUID", guid_bin) # # This filter does not perform any escaping. def bineq(attribute, value) new(:bineq, 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 # Filter.join(left, right) is the same as left & # right. # # # Selects only entries that have an objectclass attribute. # x = Net::LDAP::Filter.present("objectclass") # # Selects only entries that have a mail 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 # Filter.intersect(left, right) is the same as left | # right. # # # Selects only entries that have an objectclass attribute. # x = Net::LDAP::Filter.present("objectclass") # # Selects only entries that have a mail 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 Fitler.negate(filter) i s the same # as ~filter. # # # Selects only entries that do not have an objectclass # # 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 += escape(b) when 0x81 # context-specific primitive 0, SubstringFilter "any" str += "*#{escape(b)}" when 0x82 # context-specific primitive 0, SubstringFilter "final" str += "*#{escape(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 objectclass attribute. # x = Net::LDAP::Filter.present("objectclass") # # Selects only entries that have a mail 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 objectclass attribute. # x = Net::LDAP::Filter.present("objectclass") # # Selects only entries that have a mail 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 objectclass # # 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, :bineq "#{@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 = unescape(ary.shift).to_ber_contextspecific(0) end if ary.last.empty? last = nil ary.pop else last = unescape(ary.pop).to_ber_contextspecific(2) end seq = ary.map { |e| unescape(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 :bineq # make sure data is not forced to UTF-8 [@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3) 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 (&(test1)(test2) 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 (&(test1)(test2) 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\xC3\x80-\xCA\xAF]|[^\x00-\x7F]|\\[a-fA-F\d]{2})+/u) # 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 net-ldap-0.8.0/lib/net/ldap/version.rb0000644000004100000410000000007012404411554017536 0ustar www-datawww-datamodule Net class LDAP VERSION = "0.8.0" end end net-ldap-0.8.0/lib/net/ldap/instrumentation.rb0000644000004100000410000000123412404411554021317 0ustar www-datawww-datamodule Net::LDAP::Instrumentation attr_reader :instrumentation_service private :instrumentation_service # Internal: Instrument a block with the defined instrumentation service. # # Yields the event payload if a block is given. # # Skips instrumentation if no service is set. # # Returns the return value of the block. def instrument(event, payload = {}) payload = (payload || {}).dup if instrumentation_service instrumentation_service.instrument(event, payload) do |payload| payload[:result] = yield(payload) if block_given? end else yield(payload) if block_given? end end private :instrument end net-ldap-0.8.0/lib/net/ldap/dataset.rb0000644000004100000410000001012412404411554017477 0ustar www-datawww-data# -*- 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 =~ /^ / 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 net-ldap-0.8.0/lib/net/ldap/entry.rb0000644000004100000410000001354112404411554017221 0ustar www-datawww-data# -*- 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 values. 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 ## # Read the first value for the provided attribute. The attribute name # is canonicalized prior to reading. Returns nil if the attribute does # not exist. def first(name) self[name].first 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, include_all = false) #: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 net-ldap-0.8.0/lib/net/ldap/dn.rb0000644000004100000410000001471512404411554016465 0ustar www-datawww-data# -*- 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 net-ldap-0.8.0/lib/net/ber/0000755000004100000410000000000012404411554015357 5ustar www-datawww-datanet-ldap-0.8.0/lib/net/ber/core_ext.rb0000644000004100000410000000204012404411554017510 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'net/ber/ber_parser' # :stopdoc: class IO include Net::BER::BERParser end class StringIO include Net::BER::BERParser end if defined? ::OpenSSL class OpenSSL::SSL::SSLSocket include Net::BER::BERParser end end # :startdoc: module Net::BER::Extensions # :nodoc: end require 'net/ber/core_ext/string' # :stopdoc: class String include Net::BER::BERParser include Net::BER::Extensions::String end require 'net/ber/core_ext/array' # :stopdoc: class Array include Net::BER::Extensions::Array end # :startdoc: require 'net/ber/core_ext/bignum' # :stopdoc: class Bignum include Net::BER::Extensions::Bignum end # :startdoc: require 'net/ber/core_ext/fixnum' # :stopdoc: class Fixnum include Net::BER::Extensions::Fixnum end # :startdoc: require 'net/ber/core_ext/true_class' # :stopdoc: class TrueClass include Net::BER::Extensions::TrueClass end # :startdoc: require 'net/ber/core_ext/false_class' # :stopdoc: class FalseClass include Net::BER::Extensions::FalseClass end # :startdoc: net-ldap-0.8.0/lib/net/ber/core_ext/0000755000004100000410000000000012404411554017167 5ustar www-datawww-datanet-ldap-0.8.0/lib/net/ber/core_ext/string.rb0000644000004100000410000000403612404411554021025 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'stringio' ## # BER extensions to the String class. module Net::BER::Extensions::String ## # Converts a string to a BER string. Universal octet-strings are tagged # with 0x04, but other values are possible depending on the context, so we # let the caller give us one. # # User code should call either #to_ber_application_string or # #to_ber_contextspecific. def to_ber(code = 0x04) raw_string = raw_utf8_encoded [code].pack('C') + raw_string.length.to_ber_length_encoding + raw_string end ## # Converts a string to a BER string but does *not* encode to UTF-8 first. # This is required for proper representation of binary data for Microsoft # Active Directory def to_ber_bin(code = 0x04) [code].pack('C') + length.to_ber_length_encoding + self end def raw_utf8_encoded if self.respond_to?(:encode) # Strings should be UTF-8 encoded according to LDAP. # However, the BER code is not necessarily valid UTF-8 begin self.encode('UTF-8').force_encoding('ASCII-8BIT') rescue Encoding::UndefinedConversionError self rescue Encoding::ConverterNotFoundError self rescue Encoding::InvalidByteSequenceError self end else self end end private :raw_utf8_encoded ## # Creates an application-specific BER string encoded value with the # provided syntax code value. def to_ber_application_string(code) to_ber(0x40 + code) end ## # Creates a context-specific BER string encoded value with the provided # syntax code value. def to_ber_contextspecific(code) to_ber(0x80 + code) end ## # Nondestructively reads a BER object from this string. def read_ber(syntax = nil) StringIO.new(self).read_ber(syntax) end ## # Destructively reads a BER object from the string. def read_ber!(syntax = nil) io = StringIO.new(self) result = io.read_ber(syntax) self.slice!(0...io.pos) return result end def reject_empty_ber_arrays self.gsub(/0\000/n,'') end end net-ldap-0.8.0/lib/net/ber/core_ext/false_class.rb0000644000004100000410000000033212404411554021771 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- ## # BER extensions to +false+. module Net::BER::Extensions::FalseClass ## # Converts +false+ to the BER wireline representation of +false+. def to_ber "\001\001\000" end end net-ldap-0.8.0/lib/net/ber/core_ext/true_class.rb0000644000004100000410000000055412404411554021664 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- ## # BER extensions to +true+. module Net::BER::Extensions::TrueClass ## # Converts +true+ to the BER wireline representation of +true+. def to_ber # 20100319 AZ: Note that this may not be the completely correct value, # per some test documentation. We need to determine the truth of this. "\001\001\001" end end net-ldap-0.8.0/lib/net/ber/core_ext/array.rb0000644000004100000410000000700612404411554020635 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- ## # BER extensions to the Array class. module Net::BER::Extensions::Array ## # Converts an Array to a BER sequence. All values in the Array are # expected to be in BER format prior to calling this method. def to_ber(id = 0) # The universal sequence tag 0x30 is composed of the base tag value # (0x10) and the constructed flag (0x20). to_ber_seq_internal(0x30 + id) end alias_method :to_ber_sequence, :to_ber ## # Converts an Array to a BER set. All values in the Array are expected to # be in BER format prior to calling this method. def to_ber_set(id = 0) # The universal set tag 0x31 is composed of the base tag value (0x11) # and the constructed flag (0x20). to_ber_seq_internal(0x31 + id) end ## # Converts an Array to an application-specific sequence, assigned a tag # value that is meaningful to the particular protocol being used. All # values in the Array are expected to be in BER format pr prior to calling # this method. #-- # Implementor's note 20100320(AZ): RFC 4511 (the LDAPv3 protocol) as well # as earlier RFCs 1777 and 2559 seem to indicate that LDAP only has # application constructed sequences (0x60). However, ldapsearch sends some # context-specific constructed sequences (0xA0); other clients may do the # same. This behaviour appears to violate the RFCs. In real-world # practice, we may need to change calls of #to_ber_appsequence to # #to_ber_contextspecific for full LDAP server compatibility. # # This note probably belongs elsewhere. #++ def to_ber_appsequence(id = 0) # The application sequence tag always starts from the application flag # (0x40) and the constructed flag (0x20). to_ber_seq_internal(0x60 + id) end ## # Converts an Array to a context-specific sequence, assigned a tag value # that is meaningful to the particular context of the particular protocol # being used. All values in the Array are expected to be in BER format # prior to calling this method. def to_ber_contextspecific(id = 0) # The application sequence tag always starts from the context flag # (0x80) and the constructed flag (0x20). to_ber_seq_internal(0xa0 + id) end ## # The internal sequence packing routine. All values in the Array are # expected to be in BER format prior to calling this method. def to_ber_seq_internal(code) s = self.join [code].pack('C') + s.length.to_ber_length_encoding + s end private :to_ber_seq_internal ## # SNMP Object Identifiers (OID) are special arrays #-- # 20100320 AZ: I do not think that this method should be in BER, since # this appears to be SNMP-specific. This should probably be subsumed by a # proper SNMP OID object. #++ def to_ber_oid ary = self.dup first = ary.shift raise Net::BER::BerError, "Invalid OID" unless [0, 1, 2].include?(first) first = first * 40 + ary.shift ary.unshift first oid = ary.pack("w*") [6, oid.length].pack("CC") + oid end ## # Converts an array into a set of ber control codes # The expected format is [[control_oid, criticality, control_value(optional)]] # [['1.2.840.113556.1.4.805',true]] # def to_ber_control #if our array does not contain at least one array then wrap it in an array before going forward ary = self[0].kind_of?(Array) ? self : [self] ary = ary.collect do |control_sequence| control_sequence.collect{|element| element.to_ber}.to_ber_sequence.reject_empty_ber_arrays end ary.to_ber_sequence.reject_empty_ber_arrays end end net-ldap-0.8.0/lib/net/ber/core_ext/bignum.rb0000644000004100000410000000106512404411554020777 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- ## # BER extensions to the Bignum class. module Net::BER::Extensions::Bignum ## # Converts a Bignum to an uncompressed BER integer. def to_ber result = [] # NOTE: Array#pack's 'w' is a BER _compressed_ integer. We need # uncompressed BER integers, so we're not using that. See also: # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/228864 n = self while n > 0 b = n & 0xff result << b n = n >> 8 end "\002" + ([result.size] + result.reverse).pack('C*') end end net-ldap-0.8.0/lib/net/ber/core_ext/fixnum.rb0000644000004100000410000000341612404411554021026 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- ## # Ber extensions to the Fixnum class. module Net::BER::Extensions::Fixnum ## # Converts the fixnum to BER format. def to_ber "\002#{to_ber_internal}" end ## # Converts the fixnum to BER enumerated format. def to_ber_enumerated "\012#{to_ber_internal}" end ## # Converts the fixnum to BER length encodining format. def to_ber_length_encoding if self <= 127 [self].pack('C') else i = [self].pack('N').sub(/^[\0]+/,"") [0x80 + i.length].pack('C') + i end end ## # Generate a BER-encoding for an application-defined INTEGER. Examples of # such integers are SNMP's Counter, Gauge, and TimeTick types. def to_ber_application(tag) [0x40 + tag].pack("C") + to_ber_internal end ## # Used to BER-encode the length and content bytes of a Fixnum. Callers # must prepend the tag byte for the contained value. def to_ber_internal # CAUTION: Bit twiddling ahead. You might want to shield your eyes or # something. # Looks for the first byte in the fixnum that is not all zeroes. It does # this by masking one byte after another, checking the result for bits # that are left on. size = Net::BER::MAX_FIXNUM_SIZE while size > 1 break if (self & (0xff << (size - 1) * 8)) > 0 size -= 1 end # Store the size of the fixnum in the result result = [size] # Appends bytes to result, starting with higher orders first. Extraction # of bytes is done by right shifting the original fixnum by an amount # and then masking that with 0xff. while size > 0 # right shift size - 1 bytes, mask with 0xff result << ((self >> ((size - 1) * 8)) & 0xff) size -= 1 end result.pack('C*') end private :to_ber_internal end net-ldap-0.8.0/lib/net/ber/ber_parser.rb0000644000004100000410000001224612404411554020035 0ustar www-datawww-data# -*- ruby encoding: utf-8 -*- require 'stringio' # Implements Basic Encoding Rules parsing to be mixed into types as needed. module Net::BER::BERParser primitive = { 1 => :boolean, 2 => :integer, 4 => :string, 5 => :null, 6 => :oid, 10 => :integer, 13 => :string # (relative OID) } constructed = { 16 => :array, 17 => :array } universal = { :primitive => primitive, :constructed => constructed } primitive = { 10 => :integer } context = { :primitive => primitive } # The universal, built-in ASN.1 BER syntax. BuiltinSyntax = Net::BER.compile_syntax(:universal => universal, :context_specific => context) ## # This is an extract of our BER object parsing to simplify our # understanding of how we parse basic BER object types. def parse_ber_object(syntax, id, data) # Find the object type from either the provided syntax lookup table or # the built-in syntax lookup table. # # This exceptionally clever bit of code is verrrry slow. object_type = (syntax && syntax[id]) || BuiltinSyntax[id] # == is expensive so sort this so the common cases are at the top. if object_type == :string s = Net::BER::BerIdentifiedString.new(data || "") s.ber_identifier = id s elsif object_type == :integer j = 0 data.each_byte { |b| j = (j << 8) + b } j elsif object_type == :oid # See X.690 pgh 8.19 for an explanation of this algorithm. # This is potentially not good enough. We may need a # BerIdentifiedOid as a subclass of BerIdentifiedArray, to # get the ber identifier and also a to_s method that produces # the familiar dotted notation. oid = data.unpack("w*") f = oid.shift g = if f < 40 [0, f] elsif f < 80 [1, f - 40] else # f - 80 can easily be > 80. What a weird optimization. [2, f - 80] end oid.unshift g.last oid.unshift g.first # Net::BER::BerIdentifiedOid.new(oid) oid elsif object_type == :array seq = Net::BER::BerIdentifiedArray.new seq.ber_identifier = id sio = StringIO.new(data || "") # Interpret the subobject, but note how the loop is built: # nil ends the loop, but false (a valid BER value) does not! while (e = sio.read_ber(syntax)) != nil seq << e end seq elsif object_type == :boolean data != "\000" elsif object_type == :null n = Net::BER::BerIdentifiedNull.new n.ber_identifier = id n else raise Net::BER::BerError, "Unsupported object type: id=#{id}" end end private :parse_ber_object ## # This is an extract of how our BER object length parsing is done to # simplify the primary call. This is defined in X.690 section 8.1.3. # # The BER length will either be a single byte or up to 126 bytes in # length. There is a special case of a BER length indicating that the # content-length is undefined and will be identified by the presence of # two null values (0x00 0x00). # # # # # # # # # # # # # # # # # # # # # # #
RangeLength
0x00 -- 0x7f
0b00000000 -- 0b01111111
0 - 127 bytes
0x80
0b10000000
Indeterminate (end-of-content marker required)
0x81 -- 0xfe
0b10000001 -- 0b11111110
1 - 126 bytes of length as an integer value
0xff
0b11111111
Illegal (reserved for future expansion)
# #-- # This has been modified from the version that was previously inside # #read_ber to handle both the indeterminate terminator case and the # invalid BER length case. Because the "lengthlength" value was not used # inside of #read_ber, we no longer return it. def read_ber_length n = getbyte if n <= 0x7f n elsif n == 0x80 -1 elsif n == 0xff raise Net::BER::BerError, "Invalid BER length 0xFF detected." else v = 0 read(n & 0x7f).each_byte do |b| v = (v << 8) + b end v end end private :read_ber_length ## # Reads a BER object from the including object. Requires that #getbyte is # implemented on the including object and that it returns a Fixnum value. # Also requires #read(bytes) to work. # # Yields the object type `id` and the data `content_length` if a block is # given. This is namely to support instrumentation. # # This does not work with non-blocking I/O. def read_ber(syntax = nil) # TODO: clean this up so it works properly with partial packets coming # from streams that don't block when we ask for more data (like # StringIOs). At it is, this can throw TypeErrors and other nasties. id = getbyte or return nil # don't trash this value, we'll use it later content_length = read_ber_length yield id, content_length if block_given? if -1 == content_length raise Net::BER::BerError, "Indeterminite BER content length not implemented." else data = read(content_length) end parse_ber_object(syntax, id, data) end end net-ldap-0.8.0/Contributors.rdoc0000644000004100000410000000074712404411554016631 0ustar www-datawww-data== Contributors Net::LDAP was originally developed by: * Francis Cianfrocca (garbagecat) Contributions since: * Emiel van de Laar (emiel) * Rory O'Connell (roryo) * Kaspar Schiess (kschiess) * Austin Ziegler (halostatue) * Dimitrij Denissenko (dim) * James Hewitt (jamstah) * Kouhei Sutou (kou) * Lars Tobias Skjong-Børsting (larstobi) * Rory O'Connell (roryo) * Tony Headford (tonyheadford) * Derek Harmel (derekharmel) * Erik Hetzner (egh) * nowhereman * David J. Lee (DavidJLee) net-ldap-0.8.0/metadata.yml0000644000004100000410000001404312404411554015560 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: net-ldap version: !ruby/object:Gem::Version version: 0.8.0 platform: ruby authors: - Francis Cianfrocca - Emiel van de Laar - Rory O'Connell - Kaspar Schiess - Austin Ziegler - Michael Schaarschmidt autorequire: bindir: bin cert_chain: [] date: 2014-09-10 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rdoc requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '4.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '4.0' - !ruby/object:Gem::Dependency name: hoe-git requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1' - !ruby/object:Gem::Dependency name: hoe-gemspec requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1' - !ruby/object:Gem::Dependency name: metaid requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1' - !ruby/object:Gem::Dependency name: flexmock requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.3.0 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.3.0 - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.0' - !ruby/object:Gem::Dependency name: hoe requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '3.10' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '3.10' description: |- Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services. Net::LDAP is written completely in Ruby with no external dependencies. It supports most LDAP client features and a subset of server features as well. Net::LDAP has been tested against modern popular LDAP servers including OpenLDAP and Active Directory. The current release is mostly compliant with earlier versions of the IETF LDAP RFCs (2251–2256, 2829–2830, 3377, and 3771). Our roadmap for Net::LDAP 1.0 is to gain full client compliance with the most recent LDAP RFCs (4510–4519, plus portions of 4520–4532). email: - blackhedd@rubyforge.org - gemiel@gmail.com - rory.ocon@gmail.com - kaspar.schiess@absurd.li - austin@rubyforge.org - michael@schaaryworks.com executables: [] extensions: [] extra_rdoc_files: - Contributors.rdoc - Hacking.rdoc - History.rdoc - License.rdoc - Manifest.txt - README.rdoc files: - ".autotest" - ".gemtest" - ".rspec" - ".travis.yml" - Contributors.rdoc - Gemfile - Hacking.rdoc - History.rdoc - License.rdoc - Manifest.txt - README.rdoc - Rakefile - autotest/discover.rb - lib/net-ldap.rb - lib/net/ber.rb - lib/net/ber/ber_parser.rb - lib/net/ber/core_ext.rb - lib/net/ber/core_ext/array.rb - lib/net/ber/core_ext/bignum.rb - lib/net/ber/core_ext/false_class.rb - lib/net/ber/core_ext/fixnum.rb - lib/net/ber/core_ext/string.rb - lib/net/ber/core_ext/true_class.rb - lib/net/ldap.rb - lib/net/ldap/dataset.rb - lib/net/ldap/dn.rb - lib/net/ldap/entry.rb - lib/net/ldap/filter.rb - lib/net/ldap/instrumentation.rb - lib/net/ldap/password.rb - lib/net/ldap/pdu.rb - lib/net/ldap/version.rb - lib/net/snmp.rb - net-ldap.gemspec - spec/integration/ssl_ber_spec.rb - spec/spec.opts - spec/spec_helper.rb - spec/unit/ber/ber_spec.rb - spec/unit/ber/core_ext/array_spec.rb - spec/unit/ber/core_ext/string_spec.rb - spec/unit/ldap/dn_spec.rb - spec/unit/ldap/entry_spec.rb - spec/unit/ldap/filter_parser_spec.rb - spec/unit/ldap/filter_spec.rb - spec/unit/ldap/search_spec.rb - spec/unit/ldap_spec.rb - test/common.rb - test/test_entry.rb - test/test_filter.rb - test/test_ldap_connection.rb - test/test_ldif.rb - test/test_password.rb - test/test_rename.rb - test/test_snmp.rb - test/testdata.ldif - testserver/ldapserver.rb - testserver/testdata.ldif homepage: http://rubyldap.com/' licenses: - MIT metadata: {} post_install_message: rdoc_options: - "--main" - README.rdoc require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.8.7 required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.2.2 signing_key: specification_version: 4 summary: Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services test_files: - test/test_entry.rb - test/test_filter.rb - test/test_ldap_connection.rb - test/test_ldif.rb - test/test_password.rb - test/test_rename.rb - test/test_snmp.rb net-ldap-0.8.0/test/0000755000004100000410000000000012404411554014232 5ustar www-datawww-datanet-ldap-0.8.0/test/test_snmp.rb0000644000004100000410000000736612404411554016607 0ustar www-datawww-data# $Id: testsnmp.rb 231 2006-12-21 15:09:29Z blackhedd $ require 'common' require 'net/snmp' class TestSnmp < Test::Unit::TestCase def self.raw_string(s) # Conveniently, String#b only needs to be called when it exists s.respond_to?(:b) ? s.b : s end SnmpGetRequest = raw_string("0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000") SnmpGetResponse = raw_string("0+\002\001\000\004\006public\242\036\002\002'\017\002\001\000\002\001\0000\0220\020\006\b+\006\001\002\001\001\001\000\004\004test") SnmpGetRequestXXX = raw_string("0'\002\001\000\004\006xxxxxx\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000") def test_invalid_packet data = "xxxx" assert_raise(Net::BER::BerError) { ary = data.read_ber(Net::SNMP::AsnSyntax) } end # The method String#read_ber! added by Net::BER consumes a well-formed BER # object from the head of a string. If it doesn't find a complete, # well-formed BER object, it returns nil and leaves the string unchanged. # If it finds an object, it returns the object and removes it from the # head of the string. This is good for handling partially-received data # streams, such as from network connections. def _test_consume_string data = "xxx" assert_equal(nil, data.read_ber!) assert_equal("xxx", data) data = SnmpGetRequest + "!!!" ary = data.read_ber!(Net::SNMP::AsnSyntax) assert_equal("!!!", data) assert ary.is_a?(Array) assert ary.is_a?(Net::BER::BerIdentifiedArray) end def test_weird_packet assert_raise(Net::SnmpPdu::Error) { Net::SnmpPdu.parse("aaaaaaaaaaaaaa") } end def test_get_request data = SnmpGetRequest.dup pkt = data.read_ber(Net::SNMP::AsnSyntax) assert pkt.is_a?(Net::BER::BerIdentifiedArray) assert_equal(48, pkt.ber_identifier) # Constructed [0], signifies GetRequest pdu = Net::SnmpPdu.parse(pkt) assert_equal(:get_request, pdu.pdu_type) assert_equal(16170, pdu.request_id) # whatever was in the test data. 16170 is not magic. assert_equal([[[1, 3, 6, 1, 2, 1, 1, 1, 0], nil]], pdu.variables) assert_equal(pdu.to_ber_string, SnmpGetRequest) end def test_empty_pdu pdu = Net::SnmpPdu.new assert_raise(Net::SnmpPdu::Error) { pdu.to_ber_string } end def test_malformations pdu = Net::SnmpPdu.new pdu.version = 0 pdu.version = 2 assert_raise(Net::SnmpPdu::Error) { pdu.version = 100 } pdu.pdu_type = :get_request pdu.pdu_type = :get_next_request pdu.pdu_type = :get_response pdu.pdu_type = :set_request pdu.pdu_type = :trap assert_raise(Net::SnmpPdu::Error) { pdu.pdu_type = :something_else } end def test_make_response pdu = Net::SnmpPdu.new pdu.version = 0 pdu.community = "public" pdu.pdu_type = :get_response pdu.request_id = 9999 pdu.error_status = 0 pdu.error_index = 0 pdu.add_variable_binding [1, 3, 6, 1, 2, 1, 1, 1, 0], "test" assert_equal(SnmpGetResponse, pdu.to_ber_string) end def test_make_bad_response pdu = Net::SnmpPdu.new assert_raise(Net::SnmpPdu::Error) {pdu.to_ber_string} pdu.pdu_type = :get_response pdu.request_id = 999 pdu.to_ber_string # Not specifying variables doesn't create an error. (Maybe it should?) end def test_snmp_integers c32 = Net::SNMP::Counter32.new(100) assert_equal("A\001d", c32.to_ber) g32 = Net::SNMP::Gauge32.new(100) assert_equal("B\001d", g32.to_ber) t32 = Net::SNMP::TimeTicks32.new(100) assert_equal("C\001d", t32.to_ber) end def test_community data = SnmpGetRequestXXX.dup ary = data.read_ber(Net::SNMP::AsnSyntax) pdu = Net::SnmpPdu.parse(ary) assert_equal("xxxxxx", pdu.community) end end net-ldap-0.8.0/test/test_password.rb0000644000004100000410000000054112404411554017460 0ustar www-datawww-data# $Id: testpsw.rb 72 2006-04-24 21:58:14Z blackhedd $ require 'common' class TestPassword < Test::Unit::TestCase def test_psw assert_equal( "{MD5}xq8jwrcfibi0sZdZYNkSng==", Net::LDAP::Password.generate( :md5, "cashflow" )) assert_equal( "{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", Net::LDAP::Password.generate( :sha, "cashflow" )) end end net-ldap-0.8.0/test/test_ldap_connection.rb0000644000004100000410000000157712404411554020767 0ustar www-datawww-datarequire 'common' class TestLDAP < Test::Unit::TestCase def test_modify_ops_delete args = { :operations => [ [ :delete, "mail" ] ] } result = Net::LDAP::Connection.modify_ops(args[:operations]) expected = [ "0\r\n\x01\x010\b\x04\x04mail1\x00" ] assert_equal(expected, result) end def test_modify_ops_add args = { :operations => [ [ :add, "mail", "testuser@example.com" ] ] } result = Net::LDAP::Connection.modify_ops(args[:operations]) expected = [ "0#\n\x01\x000\x1E\x04\x04mail1\x16\x04\x14testuser@example.com" ] assert_equal(expected, result) end def test_modify_ops_replace args = { :operations =>[ [ :replace, "mail", "testuser@example.com" ] ] } result = Net::LDAP::Connection.modify_ops(args[:operations]) expected = [ "0#\n\x01\x020\x1E\x04\x04mail1\x16\x04\x14testuser@example.com" ] assert_equal(expected, result) end end net-ldap-0.8.0/test/test_rename.rb0000644000004100000410000000535712404411554017077 0ustar www-datawww-datarequire 'common' # Commented out since it assumes you have a live LDAP server somewhere. This # will be migrated to the integration specs, as soon as they are ready. =begin class TestRename < Test::Unit::TestCase HOST= '10.10.10.71' PORT = 389 BASE = "o=test" AUTH = { :method => :simple, :username => "cn=testadmin,#{BASE}", :password => 'password' } BASIC_USER = "cn=jsmith,ou=sales,#{BASE}" RENAMED_USER = "cn=jbrown,ou=sales,#{BASE}" MOVED_USER = "cn=jsmith,ou=marketing,#{BASE}" RENAMED_MOVED_USER = "cn=jjones,ou=marketing,#{BASE}" def setup # create the entries we're going to manipulate Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| if ldap.add(:dn => "ou=sales,#{BASE}", :attributes => { :ou => "sales", :objectclass => "organizationalUnit" }) puts "Add failed: #{ldap.get_operation_result.message} - code: #{ldap.get_operation_result.code}" end ldap.add(:dn => "ou=marketing,#{BASE}", :attributes => { :ou => "marketing", :objectclass => "organizationalUnit" }) ldap.add(:dn => BASIC_USER, :attributes => { :cn => "jsmith", :objectclass => "inetOrgPerson", :sn => "Smith" }) end end def test_rename_entry dn = nil Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| ldap.rename(:olddn => BASIC_USER, :newrdn => "cn=jbrown") ldap.search(:base => RENAMED_USER) do |entry| dn = entry.dn end end assert_equal(RENAMED_USER, dn) end def test_move_entry dn = nil Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| ldap.rename(:olddn => BASIC_USER, :newrdn => "cn=jsmith", :new_superior => "ou=marketing,#{BASE}") ldap.search(:base => MOVED_USER) do |entry| dn = entry.dn end end assert_equal(MOVED_USER, dn) end def test_move_and_rename_entry dn = nil Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| ldap.rename(:olddn => BASIC_USER, :newrdn => "cn=jjones", :new_superior => "ou=marketing,#{BASE}") ldap.search(:base => RENAMED_MOVED_USER) do |entry| dn = entry.dn end end assert_equal(RENAMED_MOVED_USER, dn) end def teardown # delete the entries # note: this doesn't always completely clear up on eDirectory as objects get locked while # the rename/move is being completed on the server and this prevents the delete from happening Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| ldap.delete(:dn => BASIC_USER) ldap.delete(:dn => RENAMED_USER) ldap.delete(:dn => MOVED_USER) ldap.delete(:dn => RENAMED_MOVED_USER) ldap.delete(:dn => "ou=sales,#{BASE}") ldap.delete(:dn => "ou=marketing,#{BASE}") end end end =end net-ldap-0.8.0/test/testdata.ldif0000644000004100000410000000625312404411554016711 0ustar www-datawww-data# $Id: testdata.ldif 50 2006-04-17 17:57:33Z blackhedd $ # # This is test-data for an LDAP server in LDIF format. # dn: dc=bayshorenetworks,dc=com objectClass: dcObject objectClass: organization o: Bayshore Networks LLC dc: bayshorenetworks dn: cn=Manager,dc=bayshorenetworks,dc=com objectClass: organizationalrole cn: Manager dn: ou=people,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: people dn: ou=privileges,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: privileges dn: ou=roles,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: roles dn: ou=office,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: office dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com cn: Bob Fosse mail: nogoodnik@steamheat.net sn: Fosse ou: people objectClass: top objectClass: inetorgperson objectClass: authorizedperson hasAccessRole: uniqueIdentifier=engineer,ou=roles hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles hasAccessRole: uniqueIdentifier=workorder_user,ou=roles hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com cn: Gwen Verdon mail: elephant@steamheat.net sn: Verdon ou: people objectClass: top objectClass: inetorgperson objectClass: authorizedperson hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles hasAccessRole: uniqueIdentifier=engineer,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com uniqueIdentifier: engineering ou: privileges objectClass: accessPrivilege dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com uniqueIdentifier: engineer ou: roles objectClass: accessRole hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com uniqueIdentifier: ldapadmin ou: roles objectClass: accessRole dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com uniqueIdentifier: ldapsuperadmin ou: roles objectClass: accessRole dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com cn: Sid Sorokin mail: catperson@steamheat.net sn: Sorokin ou: people objectClass: top objectClass: inetorgperson objectClass: authorizedperson hasAccessRole: uniqueIdentifier=engineer,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles hasAccessRole: uniqueIdentifier=workorder_user,ou=roles net-ldap-0.8.0/test/test_entry.rb0000644000004100000410000000320312404411554016755 0ustar www-datawww-datarequire 'common' =begin class TestEntry < Test::Unit::TestCase Commented out until I can make it a spec. context "An instance of Entry" do setup do @entry = Net::LDAP::Entry.new 'cn=Barbara,o=corp' end should "be initialized with the DN" do assert_equal 'cn=Barbara,o=corp', @entry.dn end should 'return an empty array when accessing a nonexistent attribute (index lookup)' do assert_equal [], @entry['sn'] end should 'return an empty array when accessing a nonexistent attribute (method call)' do assert_equal [], @entry.sn end should 'create an attribute on assignment (index lookup)' do @entry['sn'] = 'Jensen' assert_equal ['Jensen'], @entry['sn'] end should 'create an attribute on assignment (method call)' do @entry.sn = 'Jensen' assert_equal ['Jensen'], @entry.sn end should 'have attributes accessible by index lookup' do @entry['sn'] = 'Jensen' assert_equal ['Jensen'], @entry['sn'] end should 'have attributes accessible using a Symbol as the index' do @entry[:sn] = 'Jensen' assert_equal ['Jensen'], @entry[:sn] end should 'have attributes accessible by method call' do @entry['sn'] = 'Jensen' assert_equal ['Jensen'], @entry.sn end should 'ignore case of attribute names' do @entry['sn'] = 'Jensen' assert_equal ['Jensen'], @entry.sn assert_equal ['Jensen'], @entry.Sn assert_equal ['Jensen'], @entry.SN assert_equal ['Jensen'], @entry['sn'] assert_equal ['Jensen'], @entry['Sn'] assert_equal ['Jensen'], @entry['SN'] end end end =end net-ldap-0.8.0/test/common.rb0000644000004100000410000000010112404411554016037 0ustar www-datawww-data# Add 'lib' to load path. require 'test/unit' require 'net/ldap' net-ldap-0.8.0/test/test_ldif.rb0000644000004100000410000000503012404411554016532 0ustar www-datawww-data# $Id: testldif.rb 61 2006-04-18 20:55:55Z blackhedd $ require 'common' 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?) 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) 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")) recovered_psw = ds["Goldbrick"][:userpassword].shift 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?("abcdefghijklmn")) end def test_ldif_with_continuation_lines_and_extra_whitespace ds1 = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n")) assert_equal(true, ds1.has_key?("abcdefg hijklmn")) ds2 = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: abcdefg\r\n hij klmn\r\n\r\n")) assert_equal(true, ds2.has_key?("abcdefghij klmn")) end def test_ldif_tab_is_not_continuation ds = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: key\r\n\tnotcontinued\r\n\r\n")) assert_equal(true, ds.has_key?("key")) 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) } end # Must test folded lines and base64-encoded lines as well as normal ones. def test_to_ldif data = File.open(TestLdifFilename, "rb") { |f| f.read } io = StringIO.new(data) # added .lines to turn to array because 1.9 doesn't have # .grep on basic strings entries = data.lines.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 net-ldap-0.8.0/test/test_filter.rb0000644000004100000410000001202412404411554017102 0ustar www-datawww-datarequire 'common' class TestFilter < Test::Unit::TestCase Filter = Net::LDAP::Filter def test_bug_7534_rfc2254 assert_equal("(cn=Tim Wizard)", Filter.from_rfc2254("(cn=Tim Wizard)").to_rfc2254) end def test_invalid_filter_string assert_raises(Net::LDAP::LdapError) { Filter.from_rfc2254("") } end def test_invalid_filter assert_raises(Net::LDAP::LdapError) { # This test exists to prove that our constructor blocks unknown filter # types. All filters must be constructed using helpers. Filter.__send__(:new, :xx, nil, nil) } end def test_to_s assert_equal("(uid=george *)", Filter.eq("uid", "george *").to_s) end def test_convenience_filters assert_equal("(uid=\\2A)", Filter.equals("uid", "*").to_s) assert_equal("(uid=\\28*)", Filter.begins("uid", "(").to_s) assert_equal("(uid=*\\29)", Filter.ends("uid", ")").to_s) assert_equal("(uid=*\\5C*)", Filter.contains("uid", "\\").to_s) end def test_c2 assert_equal("(uid=george *)", Filter.from_rfc2254("uid=george *").to_rfc2254) assert_equal("(uid:=george *)", Filter.from_rfc2254("uid:=george *").to_rfc2254) assert_equal("(uid=george*)", Filter.from_rfc2254(" ( uid = george* ) ").to_rfc2254) assert_equal("(!(uid=george*))", Filter.from_rfc2254("uid!=george*").to_rfc2254) assert_equal("(uid<=george*)", Filter.from_rfc2254("uid <= george*").to_rfc2254) assert_equal("(uid>=george*)", Filter.from_rfc2254("uid>=george*").to_rfc2254) assert_equal("(&(uid=george*)(mail=*))", Filter.from_rfc2254("(& (uid=george* ) (mail=*))").to_rfc2254) assert_equal("(|(uid=george*)(mail=*))", Filter.from_rfc2254("(| (uid=george* ) (mail=*))").to_rfc2254) assert_equal("(!(mail=*))", Filter.from_rfc2254("(! (mail=*))").to_rfc2254) end def test_filter_with_single_clause assert_equal("(cn=name)", Net::LDAP::Filter.construct("(&(cn=name))").to_s) end def test_filters_from_ber [ Net::LDAP::Filter.eq("objectclass", "*"), Net::LDAP::Filter.pres("objectclass"), Net::LDAP::Filter.eq("objectclass", "ou"), Net::LDAP::Filter.ge("uid", "500"), Net::LDAP::Filter.le("uid", "500"), (~ Net::LDAP::Filter.pres("objectclass")), (Net::LDAP::Filter.pres("objectclass") & Net::LDAP::Filter.pres("ou")), (Net::LDAP::Filter.pres("objectclass") & Net::LDAP::Filter.pres("ou") & Net::LDAP::Filter.pres("sn")), (Net::LDAP::Filter.pres("objectclass") | Net::LDAP::Filter.pres("ou") | Net::LDAP::Filter.pres("sn")), Net::LDAP::Filter.eq("objectclass", "*aaa"), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb"), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*ccc"), Net::LDAP::Filter.eq("objectclass", "aaa*bbb"), Net::LDAP::Filter.eq("objectclass", "aaa*bbb*ccc"), Net::LDAP::Filter.eq("objectclass", "abc*def*1111*22*g"), Net::LDAP::Filter.eq("objectclass", "*aaa*"), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*"), Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*ccc*"), Net::LDAP::Filter.eq("objectclass", "aaa*"), Net::LDAP::Filter.eq("objectclass", "aaa*bbb*"), Net::LDAP::Filter.eq("objectclass", "aaa*bbb*ccc*"), ].each do |ber| f = Net::LDAP::Filter.parse_ber(ber.to_ber.read_ber(Net::LDAP::AsnSyntax)) assert(f == ber) assert_equal(f.to_ber, ber.to_ber) end end def test_ber_from_rfc2254_filter [ Net::LDAP::Filter.construct("objectclass=*"), Net::LDAP::Filter.construct("objectclass=ou"), Net::LDAP::Filter.construct("uid >= 500"), Net::LDAP::Filter.construct("uid <= 500"), Net::LDAP::Filter.construct("(!(uid=*))"), Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*))"), Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*)(sn=*))"), Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*))"), Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*)(sn=*))"), Net::LDAP::Filter.construct("objectclass=*aaa"), Net::LDAP::Filter.construct("objectclass=*aaa*bbb"), Net::LDAP::Filter.construct("objectclass=*aaa bbb"), Net::LDAP::Filter.construct("objectclass=*aaa bbb"), Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc"), Net::LDAP::Filter.construct("objectclass=aaa*bbb"), Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc"), Net::LDAP::Filter.construct("objectclass=abc*def*1111*22*g"), Net::LDAP::Filter.construct("objectclass=*aaa*"), Net::LDAP::Filter.construct("objectclass=*aaa*bbb*"), Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc*"), Net::LDAP::Filter.construct("objectclass=aaa*"), Net::LDAP::Filter.construct("objectclass=aaa*bbb*"), Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc*"), ].each do |ber| f = Net::LDAP::Filter.parse_ber(ber.to_ber.read_ber(Net::LDAP::AsnSyntax)) assert(f == ber) assert_equal(f.to_ber, ber.to_ber) end end end net-ldap-0.8.0/autotest/0000755000004100000410000000000012404411554015123 5ustar www-datawww-datanet-ldap-0.8.0/autotest/discover.rb0000644000004100000410000000004412404411554017264 0ustar www-datawww-dataAutotest.add_discovery { "rspec2" } net-ldap-0.8.0/Hacking.rdoc0000644000004100000410000000452012404411554015471 0ustar www-datawww-data= Hacking on Net::LDAP We welcome your contributions to Net::LDAP. We accept most contributions, but there are ways to increase the chance of your patch being accepted quickly. == Licensing Net::LDAP 0.2 and later are be licensed under an MIT-style license; any contributions after 2010-04-20 must be under this license to be accepted. == Formatting * Your patches should be formatted like the rest of Net::LDAP. * We use a text wrap of 76–78 characters, especially for documentation contents. * Operators should have spaces around them. * Method definitions should have parentheses around arguments (and no parentheses if there are no arguments). * Indentation should be kept as flat as possible; this may mean being more explicit with constants. We welcome your contributions to Net::LDAP. To increase the chances of your patches being accepted, we recommend that you follow the guidelines below: == Documentation * Documentation: {net-ldap}[http://rubydoc.info/gems/net-ldap] It is very important that, if you add new methods or objects, your code is well-documented. The purpose of the changes should be clearly described so that even if this is a feature we do not use, we can understand its purpose. We also encourage documentation-only contributions that improve the documentation of Net::LDAP. We encourage you to provide a good summary of your as a modification to +History.rdoc+, and if you're not yet named as a contributor, include a modification to +Contributors.rdoc+ to add yourself. == Tests The Net::LDAP team uses RSpec for unit testing; all changes must have rspec tests for any new or changed features. Your changes should have been tested against at least one real LDAP server; the current tests are not sufficient to find all possible bugs. It's unlikely that they will ever be sufficient given the variations in LDAP server behaviour. If you're introducing a new feature, it would be preferred for you to provide us with a sample LDIF data file for importing into LDAP servers for testing. == Development Dependencies Net::LDAP uses several libraries during development, all of which can be installed using RubyGems. * *hoe* * *hoe-git* * *metaid* * *rspec* * *flexmock* == Participation * GitHub: {ruby-ldap/ruby-net-ldap}[https://github.com/ruby-ldap/ruby-net-ldap/] * Group: {ruby-ldap}[http://groups.google.com/group/ruby-ldap] net-ldap-0.8.0/net-ldap.gemspec0000644000004100000410000001071712404411554016332 0ustar www-datawww-data# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'net/ldap/version' Gem::Specification.new do |s| s.name = %q{net-ldap} s.version = Net::LDAP::VERSION s.license = "MIT" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Francis Cianfrocca", "Emiel van de Laar", "Rory O'Connell", "Kaspar Schiess", "Austin Ziegler", "Michael Schaarschmidt"] s.date = %q{2012-02-28} s.description = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services. Net::LDAP is written completely in Ruby with no external dependencies. It supports most LDAP client features and a subset of server features as well. Net::LDAP has been tested against modern popular LDAP servers including OpenLDAP and Active Directory. The current release is mostly compliant with earlier versions of the IETF LDAP RFCs (2251-2256, 2829-2830, 3377, and 3771). Our roadmap for Net::LDAP 1.0 is to gain full client compliance with the most recent LDAP RFCs (4510-4519, plutions of 4520-4532).} s.email = ["blackhedd@rubyforge.org", "gemiel@gmail.com", "rory.ocon@gmail.com", "kaspar.schiess@absurd.li", "austin@rubyforge.org"] s.extra_rdoc_files = ["Manifest.txt", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "README.rdoc"] s.files = [".autotest", ".rspec", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "Manifest.txt", "README.rdoc", "Rakefile", "autotest/discover.rb", "lib/net-ldap.rb", "lib/net/ber.rb", "lib/net/ber/ber_parser.rb", "lib/net/ber/core_ext.rb", "lib/net/ber/core_ext/array.rb", "lib/net/ber/core_ext/bignum.rb", "lib/net/ber/core_ext/false_class.rb", "lib/net/ber/core_ext/fixnum.rb", "lib/net/ber/core_ext/string.rb", "lib/net/ber/core_ext/true_class.rb", "lib/net/ldap.rb", "lib/net/ldap/dataset.rb", "lib/net/ldap/dn.rb", "lib/net/ldap/entry.rb", "lib/net/ldap/filter.rb", "lib/net/ldap/instrumentation.rb", "lib/net/ldap/password.rb", "lib/net/ldap/pdu.rb", "lib/net/snmp.rb", "net-ldap.gemspec", "spec/integration/ssl_ber_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/unit/ber/ber_spec.rb", "spec/unit/ber/core_ext/string_spec.rb", "spec/unit/ldap/dn_spec.rb", "spec/unit/ldap/entry_spec.rb", "spec/unit/ldap/filter_spec.rb", "spec/unit/ldap_spec.rb", "test/common.rb", "test/test_entry.rb", "test/test_filter.rb", "test/test_ldap_connection.rb", "test/test_ldif.rb", "test/test_password.rb", "test/test_rename.rb", "test/test_snmp.rb", "test/testdata.ldif", "testserver/ldapserver.rb", "testserver/testdata.ldif", "lib/net/ldap/version.rb"] s.homepage = %q{http://github.com/ruby-ldap/ruby-net-ldap} s.rdoc_options = ["--main", "README.rdoc"] s.require_paths = ["lib"] s.required_ruby_version = Gem::Requirement.new(">= 1.8.7") s.rubyforge_project = %q{net-ldap} s.rubygems_version = %q{1.5.2} s.summary = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services} s.test_files = ["test/test_entry.rb", "test/test_filter.rb", "test/test_ldap_connection.rb", "test/test_ldif.rb", "test/test_password.rb", "test/test_rename.rb", "test/test_snmp.rb"] if s.respond_to? :specification_version then s.specification_version = 3 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q, ["~> 1"]) s.add_development_dependency(%q, ["~> 1"]) s.add_development_dependency(%q, ["~> 1"]) s.add_development_dependency(%q, [">= 1.3.0"]) s.add_development_dependency(%q, ["~> 2.0"]) s.add_development_dependency(%q, [">= 2.9.1"]) else s.add_dependency(%q, ["~> 1"]) s.add_dependency(%q, ["~> 1"]) s.add_dependency(%q, ["~> 1"]) s.add_dependency(%q, [">= 1.3.0"]) s.add_dependency(%q, ["~> 2.0"]) s.add_dependency(%q, [">= 2.9.1"]) end else s.add_dependency(%q, ["~> 1"]) s.add_dependency(%q, ["~> 1"]) s.add_dependency(%q, ["~> 1"]) s.add_dependency(%q, [">= 1.3.0"]) s.add_dependency(%q, ["~> 2.0"]) s.add_dependency(%q, [">= 2.9.1"]) end end net-ldap-0.8.0/testserver/0000755000004100000410000000000012404411554015461 5ustar www-datawww-datanet-ldap-0.8.0/testserver/ldapserver.rb0000644000004100000410000001331012404411554020153 0ustar www-datawww-data# $Id$ # # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. # Gmail account: garbagecat10. # # This is an LDAP server intended for unit testing of Net::LDAP. # It implements as much of the protocol as we have the stomach # to implement but serves static data. Use ldapsearch to test # this server! # # To make this easier to write, we use the Ruby/EventMachine # reactor library. # #------------------------------------------------ module LdapServer LdapServerAsnSyntax = { :application => { :constructed => { 0 => :array, # LDAP BindRequest 3 => :array # LDAP SearchRequest }, :primitive => { 2 => :string, # ldapsearch sends this to unbind } }, :context_specific => { :primitive => { 0 => :string, # simple auth (password) 7 => :string # present filter }, :constructed => { 3 => :array # equality filter }, } } def post_init $logger.info "Accepted LDAP connection" @authenticated = false end def receive_data data @data ||= ""; @data << data while pdu = @data.read_ber!(LdapServerAsnSyntax) begin handle_ldap_pdu pdu rescue $logger.error "closing connection due to error #{$!}" close_connection end end end def handle_ldap_pdu pdu tag_id = pdu[1].ber_identifier case tag_id when 0x60 handle_bind_request pdu when 0x63 handle_search_request pdu when 0x42 # bizarre thing, it's a null object (primitive application-2) # sent by ldapsearch to request an unbind (or a kiss-off, not sure which) close_connection_after_writing else $logger.error "received unknown packet-type #{tag_id}" close_connection_after_writing end end def handle_bind_request pdu # TODO, return a proper LDAP error instead of blowing up on version error if pdu[1][0] != 3 send_ldap_response 1, pdu[0].to_i, 2, "", "We only support version 3" elsif pdu[1][1] != "cn=bigshot,dc=bayshorenetworks,dc=com" send_ldap_response 1, pdu[0].to_i, 48, "", "Who are you?" elsif pdu[1][2].ber_identifier != 0x80 send_ldap_response 1, pdu[0].to_i, 7, "", "Keep it simple, man" elsif pdu[1][2] != "opensesame" send_ldap_response 1, pdu[0].to_i, 49, "", "Make my day" else @authenticated = true send_ldap_response 1, pdu[0].to_i, 0, pdu[1][1], "I'll take it" end end #-- # Search Response ::= # CHOICE { # entry [APPLICATION 4] SEQUENCE { # objectName LDAPDN, # attributes SEQUENCE OF SEQUENCE { # AttributeType, # SET OF AttributeValue # } # }, # resultCode [APPLICATION 5] LDAPResult # } def handle_search_request pdu unless @authenticated # NOTE, early exit. send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?" return end treebase = pdu[1][0] if treebase != "dc=bayshorenetworks,dc=com" send_ldap_response 5, pdu[0].to_i, 32, "", "unknown treebase" return end msgid = pdu[0].to_i.to_ber # pdu[1][7] is the list of requested attributes. # If it's an empty array, that means that *all* attributes were requested. requested_attrs = if pdu[1][7].length > 0 pdu[1][7].map {|a| a.downcase} else :all end filters = pdu[1][6] if filters.length == 0 # NOTE, early exit. send_ldap_response 5, pdu[0].to_i, 53, "", "No filter specified" end # TODO, what if this returns nil? filter = Net::LDAP::Filter.parse_ldap_filter( filters ) $ldif.each {|dn, entry| if filter.match( entry ) attrs = [] entry.each {|k, v| if requested_attrs == :all or requested_attrs.include?(k.downcase) attrvals = v.map {|v1| v1.to_ber}.to_ber_set attrs << [k.to_ber, attrvals].to_ber_sequence end } appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) pkt = [msgid.to_ber, appseq].to_ber_sequence send_data pkt end } send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?" end def send_ldap_response pkt_tag, msgid, code, dn, text send_data( [msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag) ].to_ber ) end end #------------------------------------------------ # Rather bogus, a global method, which reads a HARDCODED filename # parses out LDIF data. It will be used to serve LDAP queries out of this server. # def load_test_data ary = File.readlines( "./testdata.ldif" ) hash = {} while line = ary.shift and line.chomp! if line =~ /^dn:[\s]*/i dn = $' hash[dn] = {} while attr = ary.shift and attr.chomp! and attr =~ /^([\w]+)[\s]*:[\s]*/ hash[dn][$1.downcase] ||= [] hash[dn][$1.downcase] << $' end end end hash end #------------------------------------------------ if __FILE__ == $0 require 'rubygems' require 'eventmachine' require 'logger' $logger = Logger.new $stderr $logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP." $:.unshift "../lib" $ldif = load_test_data require 'net/ldap' EventMachine.run { $logger.info "starting LDAP server on 127.0.0.1 port 3890" EventMachine.start_server "127.0.0.1", 3890, LdapServer EventMachine.add_periodic_timer 60, proc {$logger.info "heartbeat"} } end net-ldap-0.8.0/testserver/testdata.ldif0000644000004100000410000000617112404411554020137 0ustar www-datawww-data# $Id$ # # This is test-data for an LDAP server in LDIF format. # dn: dc=bayshorenetworks,dc=com objectClass: dcObject objectClass: organization o: Bayshore Networks LLC dc: bayshorenetworks dn: cn=Manager,dc=bayshorenetworks,dc=com objectClass: organizationalrole cn: Manager dn: ou=people,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: people dn: ou=privileges,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: privileges dn: ou=roles,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: roles dn: ou=office,dc=bayshorenetworks,dc=com objectClass: organizationalunit ou: office dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com cn: Bob Fosse mail: nogoodnik@steamheat.net sn: Fosse ou: people objectClass: top objectClass: inetorgperson objectClass: authorizedperson hasAccessRole: uniqueIdentifier=engineer,ou=roles hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles hasAccessRole: uniqueIdentifier=workorder_user,ou=roles hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com cn: Gwen Verdon mail: elephant@steamheat.net sn: Verdon ou: people objectClass: top objectClass: inetorgperson objectClass: authorizedperson hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles hasAccessRole: uniqueIdentifier=engineer,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com uniqueIdentifier: engineering ou: privileges objectClass: accessPrivilege dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com uniqueIdentifier: engineer ou: roles objectClass: accessRole hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com uniqueIdentifier: ldapadmin ou: roles objectClass: accessRole dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com uniqueIdentifier: ldapsuperadmin ou: roles objectClass: accessRole dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com cn: Sid Sorokin mail: catperson@steamheat.net sn: Sorokin ou: people objectClass: top objectClass: inetorgperson objectClass: authorizedperson hasAccessRole: uniqueIdentifier=engineer,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles hasAccessRole: uniqueIdentifier=workorder_user,ou=roles