├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── CONTRIBUTING.md ├── Contributors.rdoc ├── Gemfile ├── Hacking.rdoc ├── History.rdoc ├── License.rdoc ├── README.rdoc ├── Rakefile ├── ci-run.sh ├── docker-compose.yml ├── lib ├── net-ldap.rb └── net │ ├── ber.rb │ ├── ber │ ├── ber_parser.rb │ ├── core_ext.rb │ └── core_ext │ │ ├── array.rb │ │ ├── false_class.rb │ │ ├── integer.rb │ │ ├── string.rb │ │ └── true_class.rb │ ├── ldap.rb │ ├── ldap │ ├── auth_adapter.rb │ ├── auth_adapter │ │ ├── gss_spnego.rb │ │ ├── sasl.rb │ │ └── simple.rb │ ├── connection.rb │ ├── dataset.rb │ ├── dn.rb │ ├── entry.rb │ ├── error.rb │ ├── filter.rb │ ├── instrumentation.rb │ ├── password.rb │ ├── pdu.rb │ └── version.rb │ └── snmp.rb ├── net-ldap.gemspec ├── script ├── changelog ├── ldap-docker ├── package └── release ├── test ├── ber │ ├── core_ext │ │ ├── test_array.rb │ │ └── test_string.rb │ └── test_ber.rb ├── fixtures │ ├── ca │ │ └── docker-ca.pem │ └── ldif │ │ ├── 06-retcode.ldif │ │ └── 50-seed.ldif ├── integration │ ├── test_add.rb │ ├── test_ber.rb │ ├── test_bind.rb │ ├── test_delete.rb │ ├── test_open.rb │ ├── test_password_modify.rb │ ├── test_return_codes.rb │ └── test_search.rb ├── support │ └── vm │ │ └── openldap │ │ └── .gitignore ├── test_auth_adapter.rb ├── test_dn.rb ├── test_entry.rb ├── test_filter.rb ├── test_filter_parser.rb ├── test_helper.rb ├── test_ldap.rb ├── test_ldap_connection.rb ├── test_ldif.rb ├── test_password.rb ├── test_rename.rb ├── test_search.rb ├── test_snmp.rb ├── test_ssl_ber.rb └── testdata.ldif └── testserver ├── ldapserver.rb └── testdata.ldif /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Test 9 | 10 | on: 11 | pull_request: 12 | push: 13 | branches: 14 | - master 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | ruby: 23 | - "3.0" 24 | - "3.1" 25 | - "3.2" 26 | - "3.3" 27 | - "3.4" 28 | - "jruby-9.4" 29 | - "truffleruby" 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Run tests with Ruby ${{ matrix.ruby }} 33 | run: docker compose run ci-${{ matrix.ruby }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | .rvmrc 4 | pkg/ 5 | doc/ 6 | publish/ 7 | Gemfile.lock 8 | .bundle 9 | bin/ 10 | .idea 11 | *.gem 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | Exclude: 5 | - 'pkg/**/*' 6 | 7 | Layout/ExtraSpacing: 8 | Enabled: false 9 | 10 | Lint/AssignmentInCondition: 11 | Enabled: false 12 | 13 | Style/ParallelAssignment: 14 | Enabled: false 15 | 16 | Style/TrailingCommaInArrayLiteral: 17 | EnforcedStyleForMultiline: comma 18 | 19 | Style/TrailingCommaInArguments: 20 | EnforcedStyleForMultiline: comma 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.1 5 | - 2.2 6 | - 2.3 7 | - 2.4 8 | - 2.5 9 | - 2.6 10 | - 2.7 11 | - jruby-9.2 12 | # optional 13 | - ruby-head 14 | - jruby-19mode 15 | - jruby-9.2 16 | - jruby-head 17 | 18 | addons: 19 | hosts: 20 | - ldap.example.org # needed for TLS verification 21 | - cert.mismatch.example.org 22 | 23 | services: 24 | - docker 25 | 26 | env: 27 | - INTEGRATION=openldap 28 | 29 | cache: bundler 30 | 31 | before_install: 32 | - gem update bundler 33 | 34 | install: 35 | - > 36 | docker run \ 37 | --hostname ldap.example.org \ 38 | --env LDAP_TLS_VERIFY_CLIENT=try \ 39 | -p 389:389 \ 40 | -p 636:636 \ 41 | -v "$(pwd)"/test/fixtures/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom \ 42 | --name openldap \ 43 | --detach \ 44 | osixia/openldap:1.3.0 \ 45 | --copy-service \ 46 | --loglevel debug \ 47 | - bundle install 48 | 49 | script: bundle exec rake ci 50 | 51 | matrix: 52 | allow_failures: 53 | - rvm: ruby-head 54 | - rvm: jruby-19mode 55 | - rvm: jruby-9.2 56 | - rvm: jruby-head 57 | fast_finish: true 58 | 59 | notifications: 60 | email: false 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | Thank you for using net-ldap. If you'd like to help, keep these guidelines in 4 | mind. 5 | 6 | ## Submitting a New Issue 7 | 8 | If you find a bug, or would like to propose an idea, file a [new issue][issues]. 9 | Include as many details as possible: 10 | 11 | - Version of net-ldap gem 12 | - LDAP server version 13 | - Queries, connection information, any other input 14 | - output or error messages 15 | 16 | ## Sending a Pull Request 17 | 18 | [Pull requests][pr] are always welcome! 19 | 20 | Check out [the project's issues list][issues] for ideas on what could be improved. 21 | 22 | Before sending, please add tests and ensure the test suite passes. 23 | 24 | To run the full suite: 25 | 26 | `bundle exec rake` 27 | 28 | To run a specific test file: 29 | 30 | `bundle exec ruby test/test_ldap.rb` 31 | 32 | To run a specific test: 33 | 34 | `bundle exec ruby test/test_ldap.rb -n test_instrument_bind` 35 | 36 | Pull requests will trigger automatic continuous integration builds on 37 | [TravisCI][travis]. To run integration tests locally, see the `test/support` 38 | folder. 39 | 40 | ## Styleguide 41 | 42 | ```ruby 43 | # 1.9+ style hashes 44 | {key: "value"} 45 | 46 | # Multi-line arguments with `\` 47 | MyClass.new \ 48 | foo: 'bar', 49 | baz: 'garply' 50 | ``` 51 | 52 | [issues]: https://github.com/ruby-ldap/ruby-net-ldap/issues 53 | [pr]: https://help.github.com/articles/using-pull-requests 54 | [travis]: https://travis-ci.org/ruby-ldap/ruby-net-ldap 55 | -------------------------------------------------------------------------------- /Contributors.rdoc: -------------------------------------------------------------------------------- 1 | == Contributors 2 | 3 | Net::LDAP was originally developed by: 4 | 5 | * Francis Cianfrocca (garbagecat) 6 | 7 | Contributions since: 8 | 9 | * Emiel van de Laar (emiel) 10 | * Rory O'Connell (roryo) 11 | * Kaspar Schiess (kschiess) 12 | * Austin Ziegler (halostatue) 13 | * Dimitrij Denissenko (dim) 14 | * James Hewitt (jamstah) 15 | * Kouhei Sutou (kou) 16 | * Lars Tobias Skjong-Børsting (larstobi) 17 | * Rory O'Connell (roryo) 18 | * Tony Headford (tonyheadford) 19 | * Derek Harmel (derekharmel) 20 | * Erik Hetzner (egh) 21 | * nowhereman 22 | * David J. Lee (DavidJLee) 23 | * Cody Cutrer (ccutrer) 24 | * WoodsBagotAndreMarquesLee 25 | * Rufus Post (mynameisrufus) 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem "debug", platform: :mri 5 | gem "flexmock", "~> 1.3" 6 | gem "rake", "~> 12.3.3" 7 | gem "rubocop", "~> 1.48" 8 | gem "test-unit" 9 | -------------------------------------------------------------------------------- /Hacking.rdoc: -------------------------------------------------------------------------------- 1 | = Hacking on Net::LDAP 2 | 3 | We welcome your contributions to Net::LDAP. We accept most contributions, but 4 | there are ways to increase the chance of your patch being accepted quickly. 5 | 6 | == Licensing 7 | 8 | Net::LDAP 0.2 and later are be licensed under an MIT-style license; any 9 | contributions after 2010-04-20 must be under this license to be accepted. 10 | 11 | == Formatting 12 | 13 | * Your patches should be formatted like the rest of Net::LDAP. 14 | * We use a text wrap of 76–78 characters, especially for documentation 15 | contents. 16 | * Operators should have spaces around them. 17 | * Method definitions should have parentheses around arguments (and no 18 | parentheses if there are no arguments). 19 | * Indentation should be kept as flat as possible; this may mean being more 20 | explicit with constants. 21 | 22 | 23 | We welcome your contributions to Net::LDAP. To increase the chances of your 24 | patches being accepted, we recommend that you follow the guidelines below: 25 | 26 | == Documentation 27 | 28 | * Documentation: {net-ldap}[http://rubydoc.info/gems/net-ldap] 29 | 30 | It is very important that, if you add new methods or objects, your code is 31 | well-documented. The purpose of the changes should be clearly described so that 32 | even if this is a feature we do not use, we can understand its purpose. 33 | 34 | We also encourage documentation-only contributions that improve the 35 | documentation of Net::LDAP. 36 | 37 | We encourage you to provide a good summary of your as a modification to 38 | +History.rdoc+, and if you're not yet named as a contributor, include a 39 | modification to +Contributors.rdoc+ to add yourself. 40 | 41 | == Tests 42 | 43 | The Net::LDAP team uses [Minitest](http://docs.seattlerb.org/minitest/) for unit 44 | testing; all changes must have tests for any new or changed features. 45 | 46 | Your changes should have been tested against at least one real LDAP server; the 47 | current tests are not sufficient to find all possible bugs. It's unlikely that 48 | they will ever be sufficient given the variations in LDAP server behaviour. 49 | 50 | If you're introducing a new feature, it would be preferred for you to provide 51 | us with a sample LDIF data file for importing into LDAP servers for testing. 52 | 53 | == Development Dependencies 54 | 55 | Net::LDAP uses several libraries during development, all of which can be 56 | installed using RubyGems. 57 | 58 | * *flexmock* 59 | 60 | == Participation 61 | 62 | * GitHub: {ruby-ldap/ruby-net-ldap}[https://github.com/ruby-ldap/ruby-net-ldap/] 63 | * Group: {ruby-ldap}[http://groups.google.com/group/ruby-ldap] 64 | -------------------------------------------------------------------------------- /History.rdoc: -------------------------------------------------------------------------------- 1 | === Net::LDAP 0.19.0 2 | * Net::LDAP::DN - Retain trailing spaces in RDN values in DNs #412 3 | * Add in ability for users to specify LDAP controls when conducting searches #411 4 | * Document connect_timeout in Constructor Details #415 5 | * Fix openssl error when using multiple hosts #417 6 | 7 | === Net::LDAP 0.18.0 8 | * Fix escaping of # and space in attrs #408 9 | * Add support to use SNI #406 10 | * Drop Ruby 2.5 and JRuby 9.2 from CI tests 11 | * Bump rubocop to 1.48.1 12 | * Update CI for TruffleRuby 22 13 | 14 | === Net::LDAP 0.17.1 15 | * Fixed shebang of bash #385 16 | * Omit some tests for now until we update our CA cert #386 17 | * Add Ruby 3.0 support #388 18 | * Add TruffleRuby 21.0.0 to CI #389 19 | * Correct a typo in an error message #391 20 | * Enable bundler caching for travis #390 21 | * Fix circular require while loading lib/net/ldap/entry.rb and lib/net/ldap/dataset.rb #392 22 | * Handle nil value in GetbyteForSSLSocket::getbyte #306 23 | 24 | === Net::LDAP 0.17.0 25 | * Added private recursive_delete as alternative to DELETE_TREE #268 26 | * Test suite updates #373 #376 #377 27 | * Use Base64.strict_encode64 and SSHA256 #303 28 | * Remove deprecated ConnectionRefusedError #366 29 | * Added method to get a duplicate of the internal Hash #286 30 | * remove a circular require #380 31 | * fix LdapServerAsnSyntax compile #379 32 | * Implement '==' operator for entries #381 33 | * fix for undefined method for write exception #383 34 | 35 | === Net::LDAP 0.16.3 36 | 37 | * Add Net::LDAP::InvalidDNError #371 38 | * Use require_relative instead of require #360 39 | * Address some warnings and fix JRuby test omissions #365 40 | * Bump rake dev dependency to 12.3 #359 41 | * Enable rubocop in ci #251 42 | * Enhance rubocop configuration and test syntax #344 43 | * CI: Drop rbx-2, uninstallable #364 44 | * Fix RuboCop warnings #312 45 | * Fix wrong error class #305 46 | * CONTRIBUTING.md: Repair link to Issues #309 47 | * Make the generate() method more idiomatic... #326 48 | * Make encode_sort_controls() more idiomatic... #327 49 | * Make the instrument() method more idiomatic... #328 50 | * Fix uninitialised Net::LDAP::LdapPduError #338 51 | * README.rdoc: Use SVG build badge #310 52 | * Update TravisCI config to inclue Ruby 2.7 #346 53 | * add explicit ** to silence Ruby 2.7 warning #342 54 | * Support parsing filters with attribute tags #345 55 | * Bump rubocop development dependency version #336 56 | * Add link to generated and hosted documentation on rubydoc #319 57 | * Fix 'uninitialized constant Net::LDAP::PDU::LdapPduError' error #317 58 | * simplify encoding logic: no more chomping required #362 59 | 60 | === Net::LDAP 0.16.2 61 | 62 | * Net::LDAP#open does not cache bind result {#334}[https://github.com/ruby-ldap/ruby-net-ldap/pull/334] 63 | * Fix CI build {#333}[https://github.com/ruby-ldap/ruby-net-ldap/pull/333] 64 | * Fix to "undefined method 'result_code'" {#308}[https://github.com/ruby-ldap/ruby-net-ldap/pull/308] 65 | * Fixed Exception: incompatible character encodings: ASCII-8BIT and UTF-8 in filter.rb {#285}[https://github.com/ruby-ldap/ruby-net-ldap/pull/285] 66 | 67 | === Net::LDAP 0.16.1 68 | 69 | * Send DN and newPassword with password_modify request {#271}[https://github.com/ruby-ldap/ruby-net-ldap/pull/271] 70 | 71 | === Net::LDAP 0.16.0 72 | 73 | * Sasl fix {#281}[https://github.com/ruby-ldap/ruby-net-ldap/pull/281] 74 | * enable TLS hostname validation {#279}[https://github.com/ruby-ldap/ruby-net-ldap/pull/279] 75 | * update rubocop to 0.42.0 {#278}[https://github.com/ruby-ldap/ruby-net-ldap/pull/278] 76 | 77 | === Net::LDAP 0.15.0 78 | 79 | * Respect connect_timeout when establishing SSL connections {#273}[https://github.com/ruby-ldap/ruby-net-ldap/pull/273] 80 | 81 | === Net::LDAP 0.14.0 82 | 83 | * Normalize the encryption parameter passed to the LDAP constructor {#264}[https://github.com/ruby-ldap/ruby-net-ldap/pull/264] 84 | * Update Docs: Net::LDAP now requires ruby >= 2 {#261}[https://github.com/ruby-ldap/ruby-net-ldap/pull/261] 85 | * fix symbol proc {#255}[https://github.com/ruby-ldap/ruby-net-ldap/pull/255] 86 | * fix trailing commas {#256}[https://github.com/ruby-ldap/ruby-net-ldap/pull/256] 87 | * fix deprecated hash methods {#254}[https://github.com/ruby-ldap/ruby-net-ldap/pull/254] 88 | * fix space after comma {#253}[https://github.com/ruby-ldap/ruby-net-ldap/pull/253] 89 | * fix space inside brackets {#252}[https://github.com/ruby-ldap/ruby-net-ldap/pull/252] 90 | * Rubocop style fixes {#249}[https://github.com/ruby-ldap/ruby-net-ldap/pull/249] 91 | * Lazy initialize Net::LDAP::Connection's internal socket {#235}[https://github.com/ruby-ldap/ruby-net-ldap/pull/235] 92 | * Support for rfc3062 Password Modify, closes #163 {#178}[https://github.com/ruby-ldap/ruby-net-ldap/pull/178] 93 | 94 | === Net::LDAP 0.13.0 95 | 96 | Avoid this release for because of an backwards incompatibility in how encryption 97 | is initialized https://github.com/ruby-ldap/ruby-net-ldap/pull/264. We did not 98 | yank it because people have already worked around it. 99 | 100 | * Set a connect_timeout for the creation of a socket {#243}[https://github.com/ruby-ldap/ruby-net-ldap/pull/243] 101 | * Update bundler before installing gems with bundler {#245}[https://github.com/ruby-ldap/ruby-net-ldap/pull/245] 102 | * Net::LDAP#encryption accepts string {#239}[https://github.com/ruby-ldap/ruby-net-ldap/pull/239] 103 | * Adds correct UTF-8 encoding to Net::BER::BerIdentifiedString {#242}[https://github.com/ruby-ldap/ruby-net-ldap/pull/242] 104 | * Remove 2.3.0-preview since ruby-head already is included {#241}[https://github.com/ruby-ldap/ruby-net-ldap/pull/241] 105 | * Drop support for ruby 1.9.3 {#240}[https://github.com/ruby-ldap/ruby-net-ldap/pull/240] 106 | * Fixed capitalization of StartTLSError {#234}[https://github.com/ruby-ldap/ruby-net-ldap/pull/234] 107 | 108 | === Net::LDAP 0.12.1 109 | 110 | * Whitespace formatting cleanup {#236}[https://github.com/ruby-ldap/ruby-net-ldap/pull/236] 111 | * Set operation result if LDAP server is not accessible {#232}[https://github.com/ruby-ldap/ruby-net-ldap/pull/232] 112 | 113 | === Net::LDAP 0.12.0 114 | 115 | * DRY up connection handling logic {#224}[https://github.com/ruby-ldap/ruby-net-ldap/pull/224] 116 | * Define auth adapters {#226}[https://github.com/ruby-ldap/ruby-net-ldap/pull/226] 117 | * add slash to attribute value filter {#225}[https://github.com/ruby-ldap/ruby-net-ldap/pull/225] 118 | * Add the ability to provide a list of hosts for a connection {#223}[https://github.com/ruby-ldap/ruby-net-ldap/pull/223] 119 | * Specify the port of LDAP server by giving INTEGRATION_PORT {#221}[https://github.com/ruby-ldap/ruby-net-ldap/pull/221] 120 | * Correctly set BerIdentifiedString values to UTF-8 {#212}[https://github.com/ruby-ldap/ruby-net-ldap/pull/212] 121 | * Raise Net::LDAP::ConnectionRefusedError when new connection is refused. {#213}[https://github.com/ruby-ldap/ruby-net-ldap/pull/213] 122 | * obscure auth password upon #inspect, added test, closes #216 {#217}[https://github.com/ruby-ldap/ruby-net-ldap/pull/217] 123 | * Fixing incorrect error class name {#207}[https://github.com/ruby-ldap/ruby-net-ldap/pull/207] 124 | * Travis update {#205}[https://github.com/ruby-ldap/ruby-net-ldap/pull/205] 125 | * Remove obsolete rbx-19mode from Travis {#204}[https://github.com/ruby-ldap/ruby-net-ldap/pull/204] 126 | * mv "sudo" from script/install-openldap to .travis.yml {#199}[https://github.com/ruby-ldap/ruby-net-ldap/pull/199] 127 | * Remove meaningless shebang {#200}[https://github.com/ruby-ldap/ruby-net-ldap/pull/200] 128 | * Fix Travis CI build {#202}[https://github.com/ruby-ldap/ruby-net-ldap/pull/202] 129 | * README.rdoc: fix travis link {#195}[https://github.com/ruby-ldap/ruby-net-ldap/pull/195] 130 | 131 | === Net::LDAP 0.11 132 | * Major enhancements: 133 | * #183 Specific errors subclassing Net::LDAP::Error 134 | * Bug fixes: 135 | * #176 Fix nil tls options 136 | * #184 Search guards against nil queued reads. Connection#unescape handles numerics 137 | * Code clean-up: 138 | * #180 Refactor connection establishment 139 | 140 | === Net::LDAP 0.10.1 141 | * Bug fixes: 142 | * Fix Integer BER encoding of signed values 143 | 144 | === Net::LDAP 0.10.0 145 | * Major enhancements: 146 | * Accept SimpleTLS/StartTLS encryption options (compatible with `OpenSSL::SSL::SSLContext#set_params`) 147 | * Bug fixes: 148 | * Parse filter strings with square and curly braces (`[]` and `{}`) 149 | * Handle connection timeout errors (`Errno::ETIMEDOUT` raised as `Net::LDAP::LdapError`) 150 | * Testing changes: 151 | * Add integration tests for StartTLS connections to OpenLDAP 152 | * Meta changes: 153 | * Update Gem release tooling (remove Hoe, use Rake) 154 | * Fix Gem release date 155 | 156 | === Net::LDAP 0.9.0 157 | * Major changes: 158 | * Dropped support for ruby 1.8.7, ruby >= 1.9.3 now required 159 | * Major enhancements: 160 | * Add support for search time limit parameter 161 | * Instrument received messages, PDU parsing 162 | * Minor enhancments: 163 | * Add support for querying ActiveDirectory capabilities from root dse 164 | * Bug fixes: 165 | * Fix reads for multiple concurrent requests with shared, open connections mixing up the results 166 | * Fix search size option 167 | * Fix BER encoding bug 168 | * Code clean-up: 169 | * Added integration test suite 170 | * Switch to minitest 171 | 172 | * Details 173 | * #150 Support querying ActiveDirectory capabilities when searching root dse 174 | * #142 Encode true as xFF 175 | * #124, #145, #146, #152 Cleanup gemspec 176 | * #138, #144 Track response messages by message id 177 | * #141 Magic number/constant cleanup 178 | * #119, #129, #130, #132, #133, #137 Integration tests 179 | * #115 Search timeout support 180 | * #140 Fix search size option 181 | * #139 Cleanup and inline documentation for Net::LDAP::Connection#search 182 | * #131 Instrumentation 183 | * #116 Refactor Connection#write 184 | * #126 Update gitignore 185 | * #128 Fix whitespace 186 | * #113, #121 Switch to minitest 187 | * #123 Base64 encoded dn 188 | * #114 Separate file for Net::LDAP::Connection 189 | * #104 Parse version spec in LDIF datasets 190 | * #106 ldap.modify doc fixes 191 | * #111 Fix test deprecations 192 | 193 | === Net::LDAP 0.5.0 / 2013-07-22 194 | * Major changes: 195 | * Required Ruby version is >=1.9.3 196 | * Major enhancements: 197 | * Added alias dereferencing (@ngwilson) 198 | * BER now unescapes characters that are already escaped in the source string (@jzinn) 199 | * BerIdentifiedString will now fall back to ASCII-8 encoding if the source Ruby object cannot be encoded in UTF-8 (@lfu) 200 | * Bug fixes: 201 | * Fixed nil variable error when following a reference response (@cmdrclueless) 202 | * Fixed FilterParser unable to parse multibyte strings (@satoryu) 203 | * Return ConverterNotFound when dealing with a potentially corrupt data response (@jamuc) 204 | 205 | === Net::LDAP 0.3.1 / 2012-02-15 206 | * Bug Fixes: 207 | * Bundler should now work again 208 | 209 | === Net::LDAP 0.3.0 / 2012-02-14 210 | * Major changes: 211 | * Now uses UTF-8 strings instead of ASCII-8 per the LDAP RFC 212 | * Major Enhancements: 213 | * Adding continuation reference processing 214 | * Bug Fixes: 215 | * Fixes usupported object type #139 216 | * Fixes Net::LDAP namespace errors 217 | * Return nil instead of an empty array if the search fails 218 | 219 | === Net::LDAP 0.2.2 / 2011-03-26 220 | * Bug Fixes: 221 | * Fixed the call to Net::LDAP.modify_ops from Net::LDAP#modify. 222 | 223 | === Net::LDAP 0.2.1 / 2011-03-23 224 | * Bug Fixes: 225 | * Net::LDAP.modify_ops was broken and is now fixed. 226 | 227 | === Net::LDAP 0.2 / 2011-03-22 228 | * Major Enhancements: 229 | * Net::LDAP::Filter changes: 230 | * Filters can only be constructed using our custom constructors (eq, ge, 231 | etc.). Cleaned up the code to reflect the private new. 232 | * Fixed #to_ber to output a BER representation for :ne filters. Simplified 233 | the BER construction for substring matching. 234 | * Added Filter.join(left, right), Filter.intersect(left, right), and 235 | Filter.negate(filter) to match Filter#&, Filter#|, and Filter#~@ to 236 | prevent those operators from having problems with the private new. 237 | * Added Filter.present and Filter.present? aliases for the method 238 | previously only known as Filter.pres. 239 | * Added Filter.escape to escape strings for use in filters, based on 240 | rfc4515. 241 | * Added Filter.equals, Filter.begins, Filter.ends and Filter.contains, 242 | which automatically escape input for use in a filter string. 243 | * Cleaned up Net::LDAP::Filter::FilterParser to handle branches better. 244 | Fixed some of the regular expressions to be more canonically defined. 245 | * Correctly handles single-branch branches. 246 | * Cleaned up the string representation of Filter objects. 247 | * Added experimental support for RFC4515 extensible matching (e.g., 248 | "(cn:caseExactMatch:=Fred Flintstone)"); provided by "nowhereman". 249 | * Net::LDAP::DN class representing an automatically escaping/unescaping 250 | distinguished name for LDAP queries. 251 | * Minor Enhancements: 252 | * SSL capabilities will be enabled or disabled based on whether we can load 253 | OpenSSL successfully or not. 254 | * Moved the core class extensions extensions from being in the Net::LDAP 255 | hierarchy to the Net::BER hierarchy as most of the methods therein are 256 | related to BER-encoding values. This will make extracting Net::BER from 257 | Net::LDAP easier in the future. 258 | * Added some unit tests for the BER core extensions. 259 | * Paging controls are only sent where they are supported. 260 | * Documentation Changes: 261 | * Core class extension methods under Net::BER. 262 | * Extensive changes to Net::BER documentation. 263 | * Cleaned up some rdoc oddities, suppressed empty documentation sections 264 | where possible. 265 | * Added a document describing how to contribute to Net::LDAP most 266 | effectively. 267 | * Added a document recognizing contributors to Net::LDAP. 268 | * Extended unit testing: 269 | * Added some unit tests for the BER core extensions. 270 | * The LDIF test data file was split for Ruby 1.9 regexp support. 271 | * Added a cruisecontrol.rb task. 272 | * Converted some test/unit tests to specs. 273 | * Code clean-up: 274 | * Made the formatting of code consistent across all files. 275 | * Removed Net::BER::BERParser::TagClasses as it does not appear to be used. 276 | * Replaced calls to #to_a with calls to Kernel#Array; since Ruby 1.8.3, the 277 | default #to_a implementation has been deprecated and should be replaced 278 | either with calls to Kernel#Array or [value].flatten(1). 279 | * Modified #add and #modify to return a Pdu#result_code instead of a 280 | Pdu#result. This may be changed in Net::LDAP 1.0 to return the full 281 | Pdu#result, but if we do so, it will be that way for all LDAP calls 282 | involving Pdu objects. 283 | * Renamed Net::LDAP::Psw to Net::LDAP::Password with a corresponding filename 284 | change. 285 | * Removed the stub file lib/net/ldif.rb and class Net::LDIF. 286 | * Project Management: 287 | * Changed the license from Ruby + GPL to MIT with the agreement of the 288 | original author (Francis Cianfrocca) and the named contributors. Versions 289 | prior to 0.2.0 are still available under the Ruby + GPL license. 290 | 291 | === Net::LDAP 0.1.1 / 2010-03-18 292 | * Fixing a critical problem with sockets. 293 | 294 | === Net::LDAP 0.1 / 2010-03-17 295 | * Small fixes throughout, more to come. 296 | * Ruby 1.9 support added. 297 | * Ruby 1.8.6 and below support removed. If we can figure out a compatible way 298 | to reintroduce this, we will. 299 | * New maintainers, new project repository location. Please see the README.txt. 300 | 301 | === Net::LDAP 0.0.5 / 2009-03-xx 302 | * 13 minor enhancements: 303 | * Added Net::LDAP::Entry#to_ldif 304 | * Supported rootDSE searches with a new API. 305 | * Added [preliminary (still undocumented) support for SASL authentication. 306 | * Supported several constructs from the server side of the LDAP protocol. 307 | * Added a "consuming" String#read_ber! method. 308 | * Added some support for SNMP data-handling. 309 | * Belatedly added a patch contributed by Kouhei Sutou last October. 310 | The patch adds start_tls support. 311 | * Added Net::LDAP#search_subschema_entry 312 | * Added Net::LDAP::Filter#parse_ber, which constructs Net::LDAP::Filter 313 | objects directly from BER objects that represent search filters in 314 | LDAP SearchRequest packets. 315 | * Added Net::LDAP::Filter#execute, which allows arbitrary processing 316 | based on LDAP filters. 317 | * Changed Net::LDAP::Entry so it can be marshalled and unmarshalled. 318 | Thanks to an anonymous feature requester who only left the name 319 | "Jammy." 320 | * Added support for binary values in Net::LDAP::Entry LDIF conversions 321 | and marshalling. 322 | * Migrated to 'hoe' as the new project droid. 323 | * 14 bugs fixed: 324 | * Silenced some annoying warnings in filter.rb. Thanks to "barjunk" 325 | for pointing this out. 326 | * Some fairly extensive performance optimizations in the BER parser. 327 | * Fixed a bug in Net::LDAP::Entry::from_single_ldif_string noticed by 328 | Matthias Tarasiewicz. 329 | * Removed an erroneous LdapError value, noticed by Kouhei Sutou. 330 | * Supported attributes containing blanks (cn=Babs Jensen) to 331 | Filter#construct. Suggested by an anonymous Rubyforge user. 332 | * Added missing syntactic support for Filter ANDs, NOTs and a few other 333 | things. 334 | * Extended support for server-reported error messages. This was provisionally 335 | added to Net::LDAP#add, and eventually will be added to other methods. 336 | * Fixed bug in Net::LDAP#bind. We were ignoring the passed-in auth parm. 337 | Thanks to Kouhei Sutou for spotting it. 338 | * Patched filter syntax to support octal \XX codes. Thanks to Kouhei Sutou 339 | for the patch. 340 | * Applied an additional patch from Kouhei. 341 | * Allowed comma in filter strings, suggested by Kouhei. 342 | * 04Sep07, Changed four error classes to inherit from StandardError rather 343 | Exception, in order to be friendlier to irb. Suggested by Kouhei. 344 | * Ensure connections are closed. Thanks to Kristian Meier. 345 | * Minor bug fixes here and there. 346 | 347 | === Net::LDAP 0.0.4 / 2006-08-15 348 | * Undeprecated Net::LDAP#modify. Thanks to Justin Forder for 349 | providing the rationale for this. 350 | * Added a much-expanded set of special characters to the parser 351 | for RFC-2254 filters. Thanks to Andre Nathan. 352 | * Changed Net::LDAP#search so you can pass it a filter in string form. 353 | The conversion to a Net::LDAP::Filter now happens automatically. 354 | * Implemented Net::LDAP#bind_as (preliminary and subject to change). 355 | Thanks for Simon Claret for valuable suggestions and for helping test. 356 | * Fixed bug in Net::LDAP#open that was preventing #open from being 357 | called more than one on a given Net::LDAP object. 358 | 359 | === Net::LDAP 0.0.3 / 2006-07-26 360 | * Added simple TLS encryption. 361 | Thanks to Garett Shulman for suggestions and for helping test. 362 | 363 | === Net::LDAP 0.0.2 / 2006-07-12 364 | * Fixed malformation in distro tarball and gem. 365 | * Improved documentation. 366 | * Supported "paged search control." 367 | * Added a range of API improvements. 368 | * Thanks to Andre Nathan, andre@digirati.com.br, for valuable 369 | suggestions. 370 | * Added support for LE and GE search filters. 371 | * Added support for Search referrals. 372 | * Fixed a regression with openldap 2.2.x and higher caused 373 | by the introduction of RFC-2696 controls. Thanks to Andre 374 | Nathan for reporting the problem. 375 | * Added support for RFC-2254 filter syntax. 376 | 377 | === Net::LDAP 0.0.1 / 2006-05-01 378 | * Initial release. 379 | * Client functionality is near-complete, although the APIs 380 | are not guaranteed and may change depending on feedback 381 | from the community. 382 | * We're internally working on a Ruby-based implementation 383 | of a full-featured, production-quality LDAP server, 384 | which will leverage the underlying LDAP and BER functionality 385 | in Net::LDAP. 386 | * Please tell us if you would be interested in seeing a public 387 | release of the LDAP server. 388 | * Grateful acknowledgement to Austin Ziegler, who reviewed 389 | this code and provided the release framework, including 390 | minitar. 391 | -------------------------------------------------------------------------------- /License.rdoc: -------------------------------------------------------------------------------- 1 | == License 2 | 3 | This software is available under the terms of the MIT license. 4 | 5 | Copyright 2006–2011 by Francis Cianfrocca and other contributors. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | === Notice of License Change 27 | 28 | Versions prior to 0.2 were under Ruby's dual license with the GNU GPL. With 29 | this release (0.2), Net::LDAP is now under the MIT license. 30 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Net::LDAP for Ruby 2 | {Gem Version}[https://badge.fury.io/rb/net-ldap] 3 | {}[https://travis-ci.org/ruby-ldap/ruby-net-ldap] 4 | 5 | == Description 6 | 7 | Net::LDAP for Ruby (also called net-ldap) implements client access for the 8 | Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for 9 | accessing distributed directory services. Net::LDAP is written completely in 10 | Ruby with no external dependencies. It supports most LDAP client features and a 11 | subset of server features as well. 12 | 13 | Net::LDAP has been tested against modern popular LDAP servers including 14 | OpenLDAP and Active Directory. The current release is mostly compliant with 15 | earlier versions of the IETF LDAP RFCs (2251–2256, 2829–2830, 3377, and 3771). 16 | Our roadmap for Net::LDAP 1.0 is to gain full client compliance with 17 | the most recent LDAP RFCs (4510–4519, plus portions of 4520–4532). 18 | 19 | == Where 20 | 21 | * {GitHub}[https://github.com/ruby-ldap/ruby-net-ldap] 22 | * {ruby-ldap@googlegroups.com}[http://groups.google.com/group/ruby-ldap] 23 | 24 | == Synopsis 25 | 26 | See {Net::LDAP on rubydoc.info}[https://www.rubydoc.info/github/ruby-ldap/ruby-net-ldap/Net/LDAP] for documentation and usage samples. 27 | 28 | == Requirements 29 | 30 | Net::LDAP requires a Ruby 2.0.0 compatible interpreter or better. 31 | 32 | == Install 33 | 34 | Net::LDAP is a pure Ruby library. It does not require any external libraries. 35 | You can install the RubyGems version of Net::LDAP available from the usual 36 | sources. 37 | 38 | gem install net-ldap 39 | 40 | Simply require either 'net-ldap' or 'net/ldap'. 41 | 42 | == Extensions 43 | 44 | This library focuses on the core LDAP RFCs referenced in the description. 45 | However, we recognize there are commonly used extensions to the spec that are 46 | useful. If there is another library which handles it, we list it here. 47 | 48 | * {resolv-srv}[https://rubygems.org/gems/resolv-srv]: Support RFC2782 SRV record lookup and failover 49 | 50 | == Develop 51 | 52 | This task will run the test suite and the 53 | {RuboCop}[https://github.com/bbatsov/rubocop] static code analyzer. 54 | 55 | rake rubotest 56 | 57 | CI takes too long? If your local box supports 58 | {Docker}[https://www.docker.com/], you can also run integration tests locally. 59 | Simply run: 60 | 61 | script/ldap-docker 62 | INTEGRATION=openldap rake test 63 | 64 | Or, use {Docker Compose}[https://docs.docker.com/compose/]. See docker-compose.yml for available Ruby versions. 65 | 66 | docker-compose run ci-2.7 67 | 68 | CAVEAT: you need to add the following line to /etc/hosts 69 | 127.0.0.1 ldap.example.org 70 | 127.0.0.1 cert.mismatch.example.org 71 | 72 | == Release 73 | 74 | This section is for gem maintainers to cut a new version of the gem. 75 | 76 | * Check out a new branch `release-VERSION` 77 | * Update lib/net/ldap/version.rb to next version number X.X.X following {semver}[http://semver.org/]. 78 | * Update `History.rdoc`. Get latest changes with `script/changelog` 79 | * Open a pull request with these changes for review 80 | * After merging, on the master branch, run `script/release` 81 | 82 | :include: Contributors.rdoc 83 | 84 | :include: License.rdoc 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | # vim: syntax=ruby 3 | 4 | require 'rake/testtask' 5 | require 'rubocop/rake_task' 6 | require 'bundler' 7 | 8 | RuboCop::RakeTask.new 9 | 10 | Rake::TestTask.new do |t| 11 | t.libs << 'test' 12 | t.test_files = FileList['test/**/test_*.rb'] 13 | t.verbose = true 14 | t.description = 'Run tests, set INTEGRATION=openldap to run integration tests, INTEGRATION_HOST and INTEGRATION_PORT are also supported' 15 | end 16 | 17 | desc 'Run tests and RuboCop (RuboCop runs on mri only)' 18 | task ci: Bundler.current_ruby.mri? ? [:test, :rubocop] : [:test] 19 | 20 | desc 'Run tests and RuboCop' 21 | task rubotest: [:test, :rubocop] 22 | 23 | task default: Bundler.current_ruby.mri? ? [:test, :rubocop] : [:test] 24 | -------------------------------------------------------------------------------- /ci-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | gem install bundler 6 | ruby -v | grep jruby && apt update && apt install -y gcc 7 | bundle check || bundle install 8 | bundle exec rake ci 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | integration_test_network: 3 | 4 | services: 5 | openldap: 6 | image: osixia/openldap:1.4.0 7 | networks: 8 | integration_test_network: 9 | aliases: 10 | - ldap.example.org 11 | - cert.mismatch.example.org 12 | environment: 13 | LDAP_TLS_VERIFY_CLIENT: "try" 14 | LDAP_SEED_INTERNAL_LDIF_PATH: "/ldif" 15 | healthcheck: 16 | test: ["CMD", "ldapsearch", "-x", "-s", "base"] 17 | interval: 60s 18 | start_period: 30s 19 | timeout: 5s 20 | retries: 1 21 | hostname: "ldap.example.org" 22 | volumes: 23 | - ./test/fixtures/ldif:/ldif:ro 24 | 25 | ci-3.0: 26 | image: ruby:3.0 27 | command: /code/ci-run.sh 28 | environment: 29 | INTEGRATION: openldap 30 | INTEGRATION_HOST: ldap.example.org 31 | depends_on: 32 | - openldap 33 | networks: 34 | integration_test_network: 35 | volumes: 36 | - .:/code 37 | working_dir: /code 38 | 39 | ci-3.1: 40 | image: ruby:3.1 41 | command: /code/ci-run.sh 42 | environment: 43 | INTEGRATION: openldap 44 | INTEGRATION_HOST: ldap.example.org 45 | depends_on: 46 | - openldap 47 | networks: 48 | integration_test_network: 49 | volumes: 50 | - .:/code 51 | working_dir: /code 52 | 53 | ci-3.2: 54 | image: ruby:3.2 55 | command: /code/ci-run.sh 56 | environment: 57 | INTEGRATION: openldap 58 | INTEGRATION_HOST: ldap.example.org 59 | depends_on: 60 | - openldap 61 | networks: 62 | integration_test_network: 63 | volumes: 64 | - .:/code 65 | working_dir: /code 66 | 67 | ci-3.3: 68 | image: ruby:3.3 69 | command: /code/ci-run.sh 70 | environment: 71 | INTEGRATION: openldap 72 | INTEGRATION_HOST: ldap.example.org 73 | depends_on: 74 | - openldap 75 | networks: 76 | integration_test_network: 77 | volumes: 78 | - .:/code 79 | working_dir: /code 80 | 81 | ci-3.4: 82 | image: ruby:3.4 83 | entrypoint: /code/ci-run.sh 84 | environment: 85 | INTEGRATION: openldap 86 | INTEGRATION_HOST: ldap.example.org 87 | depends_on: 88 | - openldap 89 | networks: 90 | integration_test_network: 91 | volumes: 92 | - .:/code 93 | working_dir: /code 94 | 95 | # https://github.com/flavorjones/truffleruby/pkgs/container/truffleruby 96 | ci-truffleruby: 97 | image: ghcr.io/flavorjones/truffleruby:stable 98 | command: /code/ci-run.sh 99 | environment: 100 | INTEGRATION: openldap 101 | INTEGRATION_HOST: ldap.example.org 102 | depends_on: 103 | - openldap 104 | networks: 105 | integration_test_network: 106 | volumes: 107 | - .:/code 108 | working_dir: /code 109 | 110 | ci-jruby-9.3: 111 | image: jruby:9.3 112 | command: /code/ci-run.sh 113 | environment: 114 | INTEGRATION: openldap 115 | INTEGRATION_HOST: ldap.example.org 116 | depends_on: 117 | - openldap 118 | networks: 119 | integration_test_network: 120 | volumes: 121 | - .:/code 122 | working_dir: /code 123 | 124 | ci-jruby-9.4: 125 | image: jruby:9.4 126 | command: /code/ci-run.sh 127 | environment: 128 | INTEGRATION: openldap 129 | INTEGRATION_HOST: ldap.example.org 130 | depends_on: 131 | - openldap 132 | networks: 133 | integration_test_network: 134 | volumes: 135 | - .:/code 136 | working_dir: /code 137 | -------------------------------------------------------------------------------- /lib/net-ldap.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require_relative 'net/ldap' 3 | -------------------------------------------------------------------------------- /lib/net/ber.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require_relative 'ldap/version' 3 | 4 | module Net # :nodoc: 5 | ## 6 | # == Basic Encoding Rules (BER) Support Module 7 | # 8 | # Much of the text below is cribbed from Wikipedia: 9 | # http://en.wikipedia.org/wiki/Basic_Encoding_Rules 10 | # 11 | # The ITU Specification is also worthwhile reading: 12 | # http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf 13 | # 14 | # The Basic Encoding Rules were the original rules laid out by the ASN.1 15 | # standard for encoding abstract information into a concrete data stream. 16 | # The rules, collectively referred to as a transfer syntax in ASN.1 17 | # parlance, specify the exact octet sequences which are used to encode a 18 | # given data item. The syntax defines such elements as: the 19 | # representations for basic data types, the structure of length 20 | # information, and the means for defining complex or compound types based 21 | # on more primitive types. The BER syntax, along with two subsets of BER 22 | # (the Canonical Encoding Rules and the Distinguished Encoding Rules), are 23 | # defined by the ITU-T's X.690 standards document, which is part of the 24 | # ASN.1 document series. 25 | # 26 | # == Encoding 27 | # The BER format specifies a self-describing and self-delimiting format 28 | # for encoding ASN.1 data structures. Each data element is encoded as a 29 | # type identifier, a length description, the actual data elements, and 30 | # where necessary, an end-of-content marker. This format allows a receiver 31 | # to decode the ASN.1 information from an incomplete stream, without 32 | # requiring any pre-knowledge of the size, content, or semantic meaning of 33 | # the data. 34 | # 35 | # 36 | # 37 | # == Protocol Data Units (PDU) 38 | # Protocols are defined with schema represented in BER, such that a PDU 39 | # consists of cascaded type-length-value encodings. 40 | # 41 | # === Type Tags 42 | # BER type tags are represented as single octets (bytes). The lower five 43 | # bits of the octet are tag identifier numbers and the upper three bits of 44 | # the octet are used to distinguish the type as native to ASN.1, 45 | # application-specific, context-specific, or private. See 46 | # Net::BER::TAG_CLASS and Net::BER::ENCODING_TYPE for more information. 47 | # 48 | # If Class is set to Universal (0b00______), the value is of a type native 49 | # to ASN.1 (e.g. INTEGER). The Application class (0b01______) is only 50 | # valid for one specific application. Context_specific (0b10______) 51 | # depends on the context and private (0b11_______) can be defined in 52 | # private specifications 53 | # 54 | # If the primitive/constructed bit is zero (0b__0_____), it specifies that 55 | # the value is primitive like an INTEGER. If it is one (0b__1_____), the 56 | # value is a constructed value that contains type-length-value encoded 57 | # types like a SET or a SEQUENCE. 58 | # 59 | # === Defined Universal (ASN.1 Native) Types 60 | # There are a number of pre-defined universal (native) types. 61 | # 62 | # 63 | # 64 | # 65 | # 66 | # 67 | # 68 | # 69 | # 70 | # 71 | # 72 | # 73 | # 74 | # 75 | # 76 | # 77 | # 78 | # 79 | # 80 | # 81 | # 82 | # 83 | # 84 | # 85 | # 86 | # 87 | # 88 | # 89 | # 90 | # 91 | # 92 | # 93 | # 94 | # 95 | # 96 | # 97 | # 98 | # 99 | # 100 | # 101 | # 102 | # 103 | # 104 | # 105 | # 106 | # 107 | # 108 | # 109 | # 110 | #
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)
ExtendedResponseC107: 139 (0x8b, 0b010001011)
111 | module BER 112 | VERSION = Net::LDAP::VERSION 113 | 114 | ## 115 | # Used for BER-encoding the length and content bytes of a Fixnum integer 116 | # values. 117 | MAX_FIXNUM_SIZE = 0.size 118 | 119 | ## 120 | # BER tag classes are kept in bits seven and eight of the tag type 121 | # octet. 122 | # 123 | # 124 | # 125 | # 126 | # 127 | # 128 | # 129 | #
BitmaskDefinition
0b00______Universal (ASN.1 Native) Types
0b01______Application Types
0b10______Context-Specific Types
0b11______Private Types
130 | TAG_CLASS = { 131 | :universal => 0b00000000, # 0 132 | :application => 0b01000000, # 64 133 | :context_specific => 0b10000000, # 128 134 | :private => 0b11000000, # 192 135 | } 136 | 137 | ## 138 | # BER encoding type is kept in bit 6 of the tag type octet. 139 | # 140 | # 141 | # 142 | # 143 | # 144 | #
BitmaskDefinition
0b__0_____Primitive
0b__1_____Constructed
145 | ENCODING_TYPE = { 146 | :primitive => 0b00000000, # 0 147 | :constructed => 0b00100000, # 32 148 | } 149 | 150 | ## 151 | # Accepts a hash of hashes describing a BER syntax and converts it into 152 | # a byte-keyed object for fast BER conversion lookup. The resulting 153 | # "compiled" syntax is used by Net::BER::BERParser. 154 | # 155 | # This method should be called only by client classes of Net::BER (e.g., 156 | # Net::LDAP and Net::SNMP) and not by clients of those classes. 157 | # 158 | # The hash-based syntax uses TAG_CLASS keys that contain hashes of 159 | # ENCODING_TYPE keys that contain tag numbers with object type markers. 160 | # 161 | # : => { 162 | # : => { 163 | # => 164 | # }, 165 | # }, 166 | # 167 | # === Permitted Object Types 168 | # :string:: A string value, represented as BerIdentifiedString. 169 | # :integer:: An integer value, represented with Fixnum. 170 | # :oid:: An Object Identifier value; see X.690 section 171 | # 8.19. Currently represented with a standard array, 172 | # but may be better represented as a 173 | # BerIdentifiedOID object. 174 | # :array:: A sequence, represented as BerIdentifiedArray. 175 | # :boolean:: A boolean value, represented as +true+ or +false+. 176 | # :null:: A null value, represented as BerIdentifiedNull. 177 | # 178 | # === Example 179 | # Net::LDAP defines its ASN.1 BER syntax something like this: 180 | # 181 | # class Net::LDAP 182 | # AsnSyntax = Net::BER.compile_syntax({ 183 | # :application => { 184 | # :primitive => { 185 | # 2 => :null, 186 | # }, 187 | # :constructed => { 188 | # 0 => :array, 189 | # # ... 190 | # }, 191 | # }, 192 | # :context_specific => { 193 | # :primitive => { 194 | # 0 => :string, 195 | # # ... 196 | # }, 197 | # :constructed => { 198 | # 0 => :array, 199 | # # ... 200 | # }, 201 | # } 202 | # }) 203 | # end 204 | # 205 | # NOTE:: For readability and formatting purposes, Net::LDAP and its 206 | # siblings actually construct their syntaxes more deliberately, 207 | # as shown below. Since a hash is passed in the end in any case, 208 | # the format does not matter. 209 | # 210 | # primitive = { 2 => :null } 211 | # constructed = { 212 | # 0 => :array, 213 | # # ... 214 | # } 215 | # application = { 216 | # :primitive => primitive, 217 | # :constructed => constructed 218 | # } 219 | # 220 | # primitive = { 221 | # 0 => :string, 222 | # # ... 223 | # } 224 | # constructed = { 225 | # 0 => :array, 226 | # # ... 227 | # } 228 | # context_specific = { 229 | # :primitive => primitive, 230 | # :constructed => constructed 231 | # } 232 | # AsnSyntax = Net::BER.compile_syntax(:application => application, 233 | # :context_specific => context_specific) 234 | def self.compile_syntax(syntax) 235 | # TODO 20100327 AZ: Should we be allocating an array of 256 values 236 | # that will either be +nil+ or an object type symbol, or should we 237 | # allocate an empty Hash since unknown values return +nil+ anyway? 238 | out = [nil] * 256 239 | syntax.each do |tag_class_id, encodings| 240 | tag_class = TAG_CLASS[tag_class_id] 241 | encodings.each do |encoding_id, classes| 242 | encoding = ENCODING_TYPE[encoding_id] 243 | object_class = tag_class + encoding 244 | classes.each do |number, object_type| 245 | out[object_class + number] = object_type 246 | end 247 | end 248 | end 249 | out 250 | end 251 | end 252 | end 253 | 254 | class Net::BER::BerError < RuntimeError; end 255 | 256 | ## 257 | # An Array object with a BER identifier attached. 258 | class Net::BER::BerIdentifiedArray < Array 259 | attr_accessor :ber_identifier 260 | 261 | def initialize(*args) 262 | super 263 | end 264 | end 265 | 266 | ## 267 | # A BER object identifier. 268 | class Net::BER::BerIdentifiedOid 269 | attr_accessor :ber_identifier 270 | 271 | def initialize(oid) 272 | if oid.is_a?(String) 273 | oid = oid.split(/\./).map(&:to_i) 274 | end 275 | @value = oid 276 | end 277 | 278 | def to_ber 279 | to_ber_oid 280 | end 281 | 282 | def to_ber_oid 283 | @value.to_ber_oid 284 | end 285 | 286 | def to_s 287 | @value.join(".") 288 | end 289 | 290 | def to_arr 291 | @value.dup 292 | end 293 | end 294 | 295 | ## 296 | # A String object with a BER identifier attached. 297 | # 298 | class Net::BER::BerIdentifiedString < String 299 | attr_accessor :ber_identifier 300 | 301 | # The binary data provided when parsing the result of the LDAP search 302 | # has the encoding 'ASCII-8BIT' (which is basically 'BINARY', or 'unknown'). 303 | # 304 | # This is the kind of a backtrace showing how the binary `data` comes to 305 | # BerIdentifiedString.new(data): 306 | # 307 | # @conn.read_ber(syntax) 308 | # -> StringIO.new(self).read_ber(syntax), i.e. included from module 309 | # -> Net::BER::BERParser.read_ber(syntax) 310 | # -> (private)Net::BER::BERParser.parse_ber_object(syntax, id, data) 311 | # 312 | # In the `#parse_ber_object` method `data`, according to its OID, is being 313 | # 'casted' to one of the Net::BER:BerIdentifiedXXX classes. 314 | # 315 | # As we are using LDAP v3 we can safely assume that the data is encoded 316 | # in UTF-8 and therefore the only thing to be done when instantiating is to 317 | # switch the encoding from 'ASCII-8BIT' to 'UTF-8'. 318 | # 319 | # Unfortunately, there are some ActiveDirectory specific attributes 320 | # (like `objectguid`) that should remain binary (do they really?). 321 | # Using the `#valid_encoding?` we can trap this cases. Special cases like 322 | # Japanese, Korean, etc. encodings might also profit from this. However 323 | # I have no clue how this encodings function. 324 | def initialize args 325 | super 326 | # 327 | # Check the encoding of the newly created String and set the encoding 328 | # to 'UTF-8' (NOTE: we do NOT change the bytes, but only set the 329 | # encoding to 'UTF-8'). 330 | return unless encoding == Encoding::BINARY 331 | current_encoding = encoding 332 | force_encoding('UTF-8') 333 | force_encoding(current_encoding) unless valid_encoding? 334 | end 335 | end 336 | 337 | module Net::BER 338 | ## 339 | # A BER null object. 340 | class BerIdentifiedNull 341 | attr_accessor :ber_identifier 342 | def to_ber 343 | "\005\000" 344 | end 345 | end 346 | 347 | ## 348 | # The default BerIdentifiedNull object. 349 | Null = Net::BER::BerIdentifiedNull.new 350 | end 351 | 352 | require_relative 'ber/core_ext' 353 | -------------------------------------------------------------------------------- /lib/net/ber/ber_parser.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require 'stringio' 3 | 4 | # Implements Basic Encoding Rules parsing to be mixed into types as needed. 5 | module Net::BER::BERParser 6 | primitive = { 7 | 1 => :boolean, 8 | 2 => :integer, 9 | 4 => :string, 10 | 5 => :null, 11 | 6 => :oid, 12 | 10 => :integer, 13 | 13 => :string # (relative OID) 14 | } 15 | constructed = { 16 | 16 => :array, 17 | 17 => :array, 18 | } 19 | universal = { :primitive => primitive, :constructed => constructed } 20 | 21 | primitive = { 10 => :integer } 22 | context = { :primitive => primitive } 23 | 24 | # The universal, built-in ASN.1 BER syntax. 25 | BuiltinSyntax = Net::BER.compile_syntax(:universal => universal, 26 | :context_specific => context) 27 | 28 | ## 29 | # This is an extract of our BER object parsing to simplify our 30 | # understanding of how we parse basic BER object types. 31 | def parse_ber_object(syntax, id, data) 32 | # Find the object type from either the provided syntax lookup table or 33 | # the built-in syntax lookup table. 34 | # 35 | # This exceptionally clever bit of code is verrrry slow. 36 | object_type = (syntax && syntax[id]) || BuiltinSyntax[id] 37 | 38 | # == is expensive so sort this so the common cases are at the top. 39 | if object_type == :string 40 | s = Net::BER::BerIdentifiedString.new(data || "") 41 | s.ber_identifier = id 42 | s 43 | elsif object_type == :integer 44 | neg = !(data.unpack("C").first & 0x80).zero? 45 | int = 0 46 | 47 | data.each_byte do |b| 48 | int = (int << 8) + (neg ? 255 - b : b) 49 | end 50 | 51 | if neg 52 | (int + 1) * -1 53 | else 54 | int 55 | end 56 | elsif object_type == :oid 57 | # See X.690 pgh 8.19 for an explanation of this algorithm. 58 | # This is potentially not good enough. We may need a 59 | # BerIdentifiedOid as a subclass of BerIdentifiedArray, to 60 | # get the ber identifier and also a to_s method that produces 61 | # the familiar dotted notation. 62 | oid = data.unpack("w*") 63 | f = oid.shift 64 | g = if f < 40 65 | [0, f] 66 | elsif f < 80 67 | [1, f - 40] 68 | else 69 | # f - 80 can easily be > 80. What a weird optimization. 70 | [2, f - 80] 71 | end 72 | oid.unshift g.last 73 | oid.unshift g.first 74 | # Net::BER::BerIdentifiedOid.new(oid) 75 | oid 76 | elsif object_type == :array 77 | seq = Net::BER::BerIdentifiedArray.new 78 | seq.ber_identifier = id 79 | sio = StringIO.new(data || "") 80 | # Interpret the subobject, but note how the loop is built: 81 | # nil ends the loop, but false (a valid BER value) does not! 82 | while (e = sio.read_ber(syntax)) != nil 83 | seq << e 84 | end 85 | seq 86 | elsif object_type == :boolean 87 | data != "\000" 88 | elsif object_type == :null 89 | n = Net::BER::BerIdentifiedNull.new 90 | n.ber_identifier = id 91 | n 92 | else 93 | raise Net::BER::BerError, "Unsupported object type: id=#{id}" 94 | end 95 | end 96 | private :parse_ber_object 97 | 98 | ## 99 | # This is an extract of how our BER object length parsing is done to 100 | # simplify the primary call. This is defined in X.690 section 8.1.3. 101 | # 102 | # The BER length will either be a single byte or up to 126 bytes in 103 | # length. There is a special case of a BER length indicating that the 104 | # content-length is undefined and will be identified by the presence of 105 | # two null values (0x00 0x00). 106 | # 107 | # 108 | # 109 | # 110 | # 111 | # 112 | # 113 | # 114 | # 115 | # 116 | # 117 | # 118 | # 119 | # 120 | # 121 | # 122 | # 123 | # 124 | # 125 | # 126 | # 127 | # 128 | #
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)
129 | # 130 | #-- 131 | # This has been modified from the version that was previously inside 132 | # #read_ber to handle both the indeterminate terminator case and the 133 | # invalid BER length case. Because the "lengthlength" value was not used 134 | # inside of #read_ber, we no longer return it. 135 | def read_ber_length 136 | n = getbyte 137 | 138 | if n <= 0x7f 139 | n 140 | elsif n == 0x80 141 | -1 142 | elsif n == 0xff 143 | raise Net::BER::BerError, "Invalid BER length 0xFF detected." 144 | else 145 | v = 0 146 | read(n & 0x7f).each_byte do |b| 147 | v = (v << 8) + b 148 | end 149 | 150 | v 151 | end 152 | end 153 | private :read_ber_length 154 | 155 | ## 156 | # Reads a BER object from the including object. Requires that #getbyte is 157 | # implemented on the including object and that it returns a Fixnum value. 158 | # Also requires #read(bytes) to work. 159 | # 160 | # Yields the object type `id` and the data `content_length` if a block is 161 | # given. This is namely to support instrumentation. 162 | # 163 | # This does not work with non-blocking I/O. 164 | def read_ber(syntax = nil) 165 | # TODO: clean this up so it works properly with partial packets coming 166 | # from streams that don't block when we ask for more data (like 167 | # StringIOs). At it is, this can throw TypeErrors and other nasties. 168 | 169 | id = getbyte or return nil # don't trash this value, we'll use it later 170 | content_length = read_ber_length 171 | 172 | yield id, content_length if block_given? 173 | 174 | if -1 == content_length 175 | raise Net::BER::BerError, 176 | "Indeterminite BER content length not implemented." 177 | end 178 | data = read(content_length) 179 | 180 | parse_ber_object(syntax, id, data) 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/net/ber/core_ext.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require_relative 'ber_parser' 3 | # :stopdoc: 4 | class IO 5 | include Net::BER::BERParser 6 | end 7 | 8 | class StringIO 9 | include Net::BER::BERParser 10 | end 11 | 12 | if defined? ::OpenSSL 13 | class OpenSSL::SSL::SSLSocket 14 | include Net::BER::BERParser 15 | end 16 | end 17 | # :startdoc: 18 | 19 | module Net::BER::Extensions # :nodoc: 20 | end 21 | 22 | require_relative 'core_ext/string' 23 | # :stopdoc: 24 | class String 25 | include Net::BER::BERParser 26 | include Net::BER::Extensions::String 27 | end 28 | 29 | require_relative 'core_ext/array' 30 | # :stopdoc: 31 | class Array 32 | include Net::BER::Extensions::Array 33 | end 34 | # :startdoc: 35 | 36 | require_relative 'core_ext/integer' 37 | # :stopdoc: 38 | class Integer 39 | include Net::BER::Extensions::Integer 40 | end 41 | # :startdoc: 42 | 43 | require_relative 'core_ext/true_class' 44 | # :stopdoc: 45 | class TrueClass 46 | include Net::BER::Extensions::TrueClass 47 | end 48 | # :startdoc: 49 | 50 | require_relative 'core_ext/false_class' 51 | # :stopdoc: 52 | class FalseClass 53 | include Net::BER::Extensions::FalseClass 54 | end 55 | # :startdoc: 56 | -------------------------------------------------------------------------------- /lib/net/ber/core_ext/array.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | ## 3 | # BER extensions to the Array class. 4 | module Net::BER::Extensions::Array 5 | ## 6 | # Converts an Array to a BER sequence. All values in the Array are 7 | # expected to be in BER format prior to calling this method. 8 | def to_ber(id = 0) 9 | # The universal sequence tag 0x30 is composed of the base tag value 10 | # (0x10) and the constructed flag (0x20). 11 | to_ber_seq_internal(0x30 + id) 12 | end 13 | alias_method :to_ber_sequence, :to_ber 14 | 15 | ## 16 | # Converts an Array to a BER set. All values in the Array are expected to 17 | # be in BER format prior to calling this method. 18 | def to_ber_set(id = 0) 19 | # The universal set tag 0x31 is composed of the base tag value (0x11) 20 | # and the constructed flag (0x20). 21 | to_ber_seq_internal(0x31 + id) 22 | end 23 | 24 | ## 25 | # Converts an Array to an application-specific sequence, assigned a tag 26 | # value that is meaningful to the particular protocol being used. All 27 | # values in the Array are expected to be in BER format pr prior to calling 28 | # this method. 29 | #-- 30 | # Implementor's note 20100320(AZ): RFC 4511 (the LDAPv3 protocol) as well 31 | # as earlier RFCs 1777 and 2559 seem to indicate that LDAP only has 32 | # application constructed sequences (0x60). However, ldapsearch sends some 33 | # context-specific constructed sequences (0xA0); other clients may do the 34 | # same. This behaviour appears to violate the RFCs. In real-world 35 | # practice, we may need to change calls of #to_ber_appsequence to 36 | # #to_ber_contextspecific for full LDAP server compatibility. 37 | # 38 | # This note probably belongs elsewhere. 39 | #++ 40 | def to_ber_appsequence(id = 0) 41 | # The application sequence tag always starts from the application flag 42 | # (0x40) and the constructed flag (0x20). 43 | to_ber_seq_internal(0x60 + id) 44 | end 45 | 46 | ## 47 | # Converts an Array to a context-specific sequence, assigned a tag value 48 | # that is meaningful to the particular context of the particular protocol 49 | # being used. All values in the Array are expected to be in BER format 50 | # prior to calling this method. 51 | def to_ber_contextspecific(id = 0) 52 | # The application sequence tag always starts from the context flag 53 | # (0x80) and the constructed flag (0x20). 54 | to_ber_seq_internal(0xa0 + id) 55 | end 56 | 57 | ## 58 | # The internal sequence packing routine. All values in the Array are 59 | # expected to be in BER format prior to calling this method. 60 | def to_ber_seq_internal(code) 61 | s = self.join 62 | [code].pack('C') + s.length.to_ber_length_encoding + s 63 | end 64 | private :to_ber_seq_internal 65 | 66 | ## 67 | # SNMP Object Identifiers (OID) are special arrays 68 | #-- 69 | # 20100320 AZ: I do not think that this method should be in BER, since 70 | # this appears to be SNMP-specific. This should probably be subsumed by a 71 | # proper SNMP OID object. 72 | #++ 73 | def to_ber_oid 74 | ary = self.dup 75 | first = ary.shift 76 | raise Net::BER::BerError, "Invalid OID" unless [0, 1, 2].include?(first) 77 | first = first * 40 + ary.shift 78 | ary.unshift first 79 | oid = ary.pack("w*") 80 | [6, oid.length].pack("CC") + oid 81 | end 82 | 83 | ## 84 | # Converts an array into a set of ber control codes 85 | # The expected format is [[control_oid, criticality, control_value(optional)]] 86 | # [['1.2.840.113556.1.4.805',true]] 87 | # 88 | def to_ber_control 89 | #if our array does not contain at least one array then wrap it in an array before going forward 90 | ary = self[0].kind_of?(Array) ? self : [self] 91 | ary = ary.collect do |control_sequence| 92 | control_sequence.collect(&:to_ber).to_ber_sequence.reject_empty_ber_arrays 93 | end 94 | ary.to_ber_sequence.reject_empty_ber_arrays 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/net/ber/core_ext/false_class.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | ## 3 | # BER extensions to +false+. 4 | module Net::BER::Extensions::FalseClass 5 | ## 6 | # Converts +false+ to the BER wireline representation of +false+. 7 | def to_ber 8 | "\001\001\000" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/net/ber/core_ext/integer.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | ## 3 | # BER extensions to the Integer class, affecting Fixnum and Bignum objects. 4 | module Net::BER::Extensions::Integer 5 | ## 6 | # Converts the Integer to BER format. 7 | def to_ber 8 | "\002#{to_ber_internal}" 9 | end 10 | 11 | ## 12 | # Converts the Integer to BER enumerated format. 13 | def to_ber_enumerated 14 | "\012#{to_ber_internal}" 15 | end 16 | 17 | ## 18 | # Converts the Integer to BER length encoding format. 19 | def to_ber_length_encoding 20 | if self <= 127 21 | [self].pack('C') 22 | else 23 | i = [self].pack('N').sub(/^[\0]+/, "") 24 | [0x80 + i.length].pack('C') + i 25 | end 26 | end 27 | 28 | ## 29 | # Generate a BER-encoding for an application-defined INTEGER. Examples of 30 | # such integers are SNMP's Counter, Gauge, and TimeTick types. 31 | def to_ber_application(tag) 32 | [0x40 + tag].pack("C") + to_ber_internal 33 | end 34 | 35 | ## 36 | # Used to BER-encode the length and content bytes of an Integer. Callers 37 | # must prepend the tag byte for the contained value. 38 | def to_ber_internal 39 | # Compute the byte length, accounting for negative values requiring two's 40 | # complement. 41 | size = 1 42 | size += 1 until (((self < 0) ? ~self : self) >> (size * 8)).zero? 43 | 44 | # Padding for positive, negative values. See section 8.5 of ITU-T X.690: 45 | # http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf 46 | 47 | # For positive integers, if most significant bit in an octet is set to one, 48 | # pad the result (otherwise it's decoded as a negative value). 49 | if self > 0 && (self & (0x80 << (size - 1) * 8)) > 0 50 | size += 1 51 | end 52 | 53 | # And for negative integers, pad if the most significant bit in the octet 54 | # is not set to one (othwerise, it's decoded as positive value). 55 | if self < 0 && (self & (0x80 << (size - 1) * 8)) == 0 56 | size += 1 57 | end 58 | 59 | # Store the size of the Integer in the result 60 | result = [size] 61 | 62 | # Appends bytes to result, starting with higher orders first. Extraction 63 | # of bytes is done by right shifting the original Integer by an amount 64 | # and then masking that with 0xff. 65 | while size > 0 66 | # right shift size - 1 bytes, mask with 0xff 67 | result << ((self >> ((size - 1) * 8)) & 0xff) 68 | size -= 1 69 | end 70 | 71 | result.pack('C*') 72 | end 73 | private :to_ber_internal 74 | end 75 | -------------------------------------------------------------------------------- /lib/net/ber/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require 'stringio' 3 | 4 | ## 5 | # BER extensions to the String class. 6 | module Net::BER::Extensions::String 7 | ## 8 | # Converts a string to a BER string. Universal octet-strings are tagged 9 | # with 0x04, but other values are possible depending on the context, so we 10 | # let the caller give us one. 11 | # 12 | # User code should call either #to_ber_application_string or 13 | # #to_ber_contextspecific. 14 | def to_ber(code = 0x04) 15 | raw_string = raw_utf8_encoded 16 | [code].pack('C') + raw_string.length.to_ber_length_encoding + raw_string 17 | end 18 | 19 | ## 20 | # Converts a string to a BER string but does *not* encode to UTF-8 first. 21 | # This is required for proper representation of binary data for Microsoft 22 | # Active Directory 23 | def to_ber_bin(code = 0x04) 24 | [code].pack('C') + length.to_ber_length_encoding + self 25 | end 26 | 27 | def raw_utf8_encoded 28 | if self.respond_to?(:encode) 29 | # Strings should be UTF-8 encoded according to LDAP. 30 | # However, the BER code is not necessarily valid UTF-8 31 | begin 32 | self.encode('UTF-8').force_encoding('ASCII-8BIT') 33 | rescue Encoding::UndefinedConversionError 34 | self 35 | rescue Encoding::ConverterNotFoundError 36 | self 37 | rescue Encoding::InvalidByteSequenceError 38 | self 39 | end 40 | else 41 | self 42 | end 43 | end 44 | private :raw_utf8_encoded 45 | 46 | ## 47 | # Creates an application-specific BER string encoded value with the 48 | # provided syntax code value. 49 | def to_ber_application_string(code) 50 | to_ber(0x40 + code) 51 | end 52 | 53 | ## 54 | # Creates a context-specific BER string encoded value with the provided 55 | # syntax code value. 56 | def to_ber_contextspecific(code) 57 | to_ber(0x80 + code) 58 | end 59 | 60 | ## 61 | # Nondestructively reads a BER object from this string. 62 | def read_ber(syntax = nil) 63 | StringIO.new(self).read_ber(syntax) 64 | end 65 | 66 | ## 67 | # Destructively reads a BER object from the string. 68 | def read_ber!(syntax = nil) 69 | io = StringIO.new(self) 70 | 71 | result = io.read_ber(syntax) 72 | self.slice!(0...io.pos) 73 | 74 | return result 75 | end 76 | 77 | def reject_empty_ber_arrays 78 | self.gsub(/0\000/n, '') 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/net/ber/core_ext/true_class.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | ## 3 | # BER extensions to +true+. 4 | module Net::BER::Extensions::TrueClass 5 | ## 6 | # Converts +true+ to the BER wireline representation of +true+. 7 | def to_ber 8 | # http://tools.ietf.org/html/rfc4511#section-5.1 9 | "\001\001\xFF".force_encoding("ASCII-8BIT") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/net/ldap/auth_adapter.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class LDAP 3 | class AuthAdapter 4 | def self.register(names, adapter) 5 | names = Array(names) 6 | @adapters ||= {} 7 | names.each do |name| 8 | @adapters[name] = adapter 9 | end 10 | end 11 | 12 | def self.[](name) 13 | a = @adapters[name] 14 | if a.nil? 15 | raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{name})" 16 | end 17 | return a 18 | end 19 | 20 | def initialize(conn) 21 | @connection = conn 22 | end 23 | 24 | def bind 25 | raise "bind method must be overwritten" 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/net/ldap/auth_adapter/gss_spnego.rb: -------------------------------------------------------------------------------- 1 | require_relative '../auth_adapter' 2 | require_relative 'sasl' 3 | 4 | module Net 5 | class LDAP 6 | module AuthAdapers 7 | #-- 8 | # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. 9 | # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to 10 | # integrate it without introducing an external dependency. 11 | # 12 | # This authentication method is accessed by calling #bind with a :method 13 | # parameter of :gss_spnego. It requires :username and :password 14 | # attributes, just like the :simple authentication method. It performs a 15 | # GSS-SPNEGO authentication with the server, which is presumed to be a 16 | # Microsoft Active Directory. 17 | #++ 18 | class GSS_SPNEGO < Net::LDAP::AuthAdapter 19 | def bind(auth) 20 | require 'ntlm' 21 | 22 | user, psw = [auth[:username] || auth[:dn], auth[:password]] 23 | raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless user && psw 24 | 25 | nego = proc do |challenge| 26 | t2_msg = NTLM::Message.parse(challenge) 27 | t3_msg = t2_msg.response({ :user => user, :password => psw }, 28 | { :ntlmv2 => true }) 29 | t3_msg.serialize 30 | end 31 | 32 | Net::LDAP::AuthAdapter::Sasl.new(@connection).bind \ 33 | :method => :sasl, 34 | :mechanism => "GSS-SPNEGO", 35 | :initial_credential => NTLM::Message::Type1.new.serialize, 36 | :challenge_response => nego 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/net/ldap/auth_adapter/sasl.rb: -------------------------------------------------------------------------------- 1 | require_relative '../auth_adapter' 2 | 3 | module Net 4 | class LDAP 5 | class AuthAdapter 6 | class Sasl < Net::LDAP::AuthAdapter 7 | MAX_SASL_CHALLENGES = 10 8 | 9 | #-- 10 | # Required parameters: :mechanism, :initial_credential and 11 | # :challenge_response 12 | # 13 | # Mechanism is a string value that will be passed in the SASL-packet's 14 | # "mechanism" field. 15 | # 16 | # Initial credential is most likely a string. It's passed in the initial 17 | # BindRequest that goes to the server. In some protocols, it may be empty. 18 | # 19 | # Challenge-response is a Ruby proc that takes a single parameter and 20 | # returns an object that will typically be a string. The 21 | # challenge-response block is called when the server returns a 22 | # BindResponse with a result code of 14 (saslBindInProgress). The 23 | # challenge-response block receives a parameter containing the data 24 | # returned by the server in the saslServerCreds field of the LDAP 25 | # BindResponse packet. The challenge-response block may be called multiple 26 | # times during the course of a SASL authentication, and each time it must 27 | # return a value that will be passed back to the server as the credential 28 | # data in the next BindRequest packet. 29 | #++ 30 | def bind(auth) 31 | mech, cred, chall = auth[:mechanism], auth[:initial_credential], 32 | auth[:challenge_response] 33 | raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless mech && cred && chall 34 | 35 | message_id = @connection.next_msgid 36 | 37 | n = 0 38 | loop do 39 | sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) 40 | request = [ 41 | Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl 42 | ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) 43 | 44 | @connection.send(:write, request, nil, message_id) 45 | pdu = @connection.queued_read(message_id) 46 | 47 | if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult 48 | raise Net::LDAP::NoBindResultError, "no bind result" 49 | end 50 | 51 | return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress 52 | raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MAX_SASL_CHALLENGES) 53 | 54 | cred = chall.call(pdu.result_server_sasl_creds) 55 | end 56 | 57 | raise Net::LDAP::SASLChallengeOverflowError, "why are we here?" 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/net/ldap/auth_adapter/simple.rb: -------------------------------------------------------------------------------- 1 | require_relative '../auth_adapter' 2 | 3 | module Net 4 | class LDAP 5 | class AuthAdapter 6 | class Simple < AuthAdapter 7 | def bind(auth) 8 | user, psw = if auth[:method] == :simple 9 | [auth[:username] || auth[:dn], auth[:password]] 10 | else 11 | ["", ""] 12 | end 13 | 14 | raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless user && psw 15 | 16 | message_id = @connection.next_msgid 17 | request = [ 18 | Net::LDAP::Connection::LdapVersion.to_ber, user.to_ber, 19 | psw.to_ber_contextspecific(0) 20 | ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) 21 | 22 | @connection.send(:write, request, nil, message_id) 23 | pdu = @connection.queued_read(message_id) 24 | 25 | if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult 26 | raise Net::LDAP::NoBindResultError, "no bind result" 27 | end 28 | 29 | pdu 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/net/ldap/dataset.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | ## 3 | # An LDAP Dataset. Used primarily as an intermediate format for converting 4 | # to and from LDIF strings and Net::LDAP::Entry objects. 5 | class Net::LDAP::Dataset < Hash 6 | ## 7 | # Dataset object version, comments. 8 | attr_accessor :version 9 | attr_reader :comments 10 | 11 | def initialize(*args, &block) # :nodoc: 12 | super 13 | @version = nil 14 | @comments = [] 15 | end 16 | 17 | ## 18 | # Outputs an LDAP Dataset as an array of strings representing LDIF 19 | # entries. 20 | def to_ldif 21 | ary = [] 22 | 23 | if version 24 | ary << "version: #{version}" 25 | ary << "" 26 | end 27 | 28 | ary += @comments unless @comments.empty? 29 | keys.sort.each do |dn| 30 | ary << "dn: #{dn}" 31 | 32 | attributes = self[dn].keys.map(&:to_s).sort 33 | attributes.each do |attr| 34 | self[dn][attr.to_sym].each do |value| 35 | if attr == "userpassword" or value_is_binary?(value) 36 | value = [value].pack("m").chomp.gsub(/\n/m, "\n ") 37 | ary << "#{attr}:: #{value}" 38 | else 39 | ary << "#{attr}: #{value}" 40 | end 41 | end 42 | end 43 | 44 | ary << "" 45 | end 46 | block_given? and ary.each { |line| yield line} 47 | 48 | ary 49 | end 50 | 51 | ## 52 | # Outputs an LDAP Dataset as an LDIF string. 53 | def to_ldif_string 54 | to_ldif.join("\n") 55 | end 56 | 57 | ## 58 | # Convert the parsed LDIF objects to Net::LDAP::Entry objects. 59 | def to_entries 60 | ary = [] 61 | keys.each do |dn| 62 | entry = Net::LDAP::Entry.new(dn) 63 | self[dn].each do |attr, value| 64 | entry[attr] = value 65 | end 66 | ary << entry 67 | end 68 | ary 69 | end 70 | 71 | ## 72 | # This is an internal convenience method to determine if a value requires 73 | # base64-encoding before conversion to LDIF output. The standard approach 74 | # in most LDAP tools is to check whether the value is a password, or if 75 | # the first or last bytes are non-printable. Microsoft Active Directory, 76 | # on the other hand, sometimes sends values that are binary in the middle. 77 | # 78 | # In the worst cases, this could be a nasty performance killer, which is 79 | # why we handle the simplest cases first. Ideally, we would also test the 80 | # first/last byte, but it's a bit harder to do this in a way that's 81 | # compatible with both 1.8.6 and 1.8.7. 82 | def value_is_binary?(value) # :nodoc: 83 | value = value.to_s 84 | return true if value[0] == ?: or value[0] == ?< 85 | value.each_byte { |byte| return true if (byte < 32) || (byte > 126) } 86 | false 87 | end 88 | private :value_is_binary? 89 | 90 | class << self 91 | class ChompedIO # :nodoc: 92 | def initialize(io) 93 | @io = io 94 | end 95 | def gets 96 | s = @io.gets 97 | s.chomp if s 98 | end 99 | end 100 | 101 | ## 102 | # Creates a Dataset object from an Entry object. Used mostly to assist 103 | # with the conversion of 104 | def from_entry(entry) 105 | dataset = Net::LDAP::Dataset.new 106 | hash = {} 107 | entry.each_attribute do |attribute, value| 108 | next if attribute == :dn 109 | hash[attribute] = value 110 | end 111 | dataset[entry.dn] = hash 112 | dataset 113 | end 114 | 115 | ## 116 | # Reads an object that returns data line-wise (using #gets) and parses 117 | # LDIF data into a Dataset object. 118 | def read_ldif(io) 119 | ds = Net::LDAP::Dataset.new 120 | io = ChompedIO.new(io) 121 | 122 | line = io.gets 123 | dn = nil 124 | 125 | while line 126 | new_line = io.gets 127 | 128 | if new_line =~ /^ / 129 | line << $' 130 | else 131 | nextline = new_line 132 | 133 | if line =~ /^#/ 134 | ds.comments << line 135 | yield :comment, line if block_given? 136 | elsif line =~ /^version:[\s]*([0-9]+)$/i 137 | ds.version = $1 138 | yield :version, line if block_given? 139 | elsif line =~ /^dn:([\:]?)[\s]*/i 140 | # $1 is a colon if the dn-value is base-64 encoded 141 | # $' is the dn-value 142 | # Avoid the Base64 class because not all Ruby versions have it. 143 | dn = ($1 == ":") ? $'.unpack('m').shift : $' 144 | ds[dn] = Hash.new { |k, v| k[v] = [] } 145 | yield :dn, dn if block_given? 146 | elsif line.empty? 147 | dn = nil 148 | yield :end, nil if block_given? 149 | elsif line =~ /^([^:]+):([\:]?)[\s]*/ 150 | # $1 is the attribute name 151 | # $2 is a colon iff the attr-value is base-64 encoded 152 | # $' is the attr-value 153 | # Avoid the Base64 class because not all Ruby versions have it. 154 | attrvalue = ($2 == ":") ? $'.unpack('m').shift : $' 155 | ds[dn][$1.downcase.to_sym] << attrvalue 156 | yield :attr, [$1.downcase.to_sym, attrvalue] if block_given? 157 | end 158 | 159 | line = nextline 160 | end 161 | end 162 | 163 | ds 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/net/ldap/dn.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | 3 | ## 4 | # Objects of this class represent an LDAP DN ("Distinguished Name"). A DN 5 | # ("Distinguished Name") is a unique identifier for an entry within an LDAP 6 | # directory. It is made up of a number of other attributes strung together, 7 | # to identify the entry in the tree. 8 | # 9 | # Each attribute that makes up a DN needs to have its value escaped so that 10 | # the DN is valid. This class helps take care of that. 11 | # 12 | # A fully escaped DN needs to be unescaped when analysing its contents. This 13 | # class also helps take care of that. 14 | class Net::LDAP::DN 15 | ## 16 | # Initialize a DN, escaping as required. Pass in attributes in name/value 17 | # pairs. If there is a left over argument, it will be appended to the dn 18 | # without escaping (useful for a base string). 19 | # 20 | # Most uses of this class will be to escape a DN, rather than to parse it, 21 | # so storing the dn as an escaped String and parsing parts as required 22 | # with a state machine seems sensible. 23 | def initialize(*args) 24 | buffer = StringIO.new 25 | 26 | args.each_index do |index| 27 | buffer << "=" if index % 2 == 1 28 | buffer << "," if index % 2 == 0 && index != 0 29 | 30 | if index < args.length - 1 || index % 2 == 1 31 | buffer << Net::LDAP::DN.escape(args[index]) 32 | else 33 | buffer << args[index] 34 | end 35 | end 36 | 37 | @dn = buffer.string 38 | end 39 | 40 | ## 41 | # Parse a DN into key value pairs using ASN from 42 | # http://tools.ietf.org/html/rfc2253 section 3. 43 | def each_pair 44 | state = :key 45 | key = StringIO.new 46 | value = StringIO.new 47 | hex_buffer = "" 48 | 49 | @dn.each_char do |char| 50 | case state 51 | when :key then 52 | case char 53 | when 'a'..'z', 'A'..'Z' then 54 | state = :key_normal 55 | key << char 56 | when '0'..'9' then 57 | state = :key_oid 58 | key << char 59 | when ' ' then state = :key 60 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 61 | end 62 | when :key_normal then 63 | case char 64 | when '=' then state = :value 65 | when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char 66 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 67 | end 68 | when :key_oid then 69 | case char 70 | when '=' then state = :value 71 | when '0'..'9', '.', ' ' then key << char 72 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 73 | end 74 | when :value then 75 | case char 76 | when '\\' then state = :value_normal_escape 77 | when '"' then state = :value_quoted 78 | when ' ' then state = :value 79 | when '#' then 80 | state = :value_hexstring 81 | value << char 82 | when ',' then 83 | state = :key 84 | yield key.string.strip, value.string 85 | key = StringIO.new 86 | value = StringIO.new; 87 | else 88 | state = :value_normal 89 | value << char 90 | end 91 | when :value_normal then 92 | case char 93 | when '\\' then state = :value_normal_escape 94 | when ',' then 95 | state = :key 96 | yield key.string.strip, value.string 97 | key = StringIO.new 98 | value = StringIO.new; 99 | else value << char 100 | end 101 | when :value_normal_escape then 102 | case char 103 | when '0'..'9', 'a'..'f', 'A'..'F' then 104 | state = :value_normal_escape_hex 105 | hex_buffer = char 106 | else state = :value_normal; value << char 107 | end 108 | when :value_normal_escape_hex then 109 | case char 110 | when '0'..'9', 'a'..'f', 'A'..'F' then 111 | state = :value_normal 112 | value << "#{hex_buffer}#{char}".to_i(16).chr 113 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 114 | end 115 | when :value_quoted then 116 | case char 117 | when '\\' then state = :value_quoted_escape 118 | when '"' then state = :value_end 119 | else value << char 120 | end 121 | when :value_quoted_escape then 122 | case char 123 | when '0'..'9', 'a'..'f', 'A'..'F' then 124 | state = :value_quoted_escape_hex 125 | hex_buffer = char 126 | else 127 | state = :value_quoted; 128 | value << char 129 | end 130 | when :value_quoted_escape_hex then 131 | case char 132 | when '0'..'9', 'a'..'f', 'A'..'F' then 133 | state = :value_quoted 134 | value << "#{hex_buffer}#{char}".to_i(16).chr 135 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 136 | end 137 | when :value_hexstring then 138 | case char 139 | when '0'..'9', 'a'..'f', 'A'..'F' then 140 | state = :value_hexstring_hex 141 | value << char 142 | when ' ' then state = :value_end 143 | when ',' then 144 | state = :key 145 | yield key.string.strip, value.string 146 | key = StringIO.new 147 | value = StringIO.new; 148 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 149 | end 150 | when :value_hexstring_hex then 151 | case char 152 | when '0'..'9', 'a'..'f', 'A'..'F' then 153 | state = :value_hexstring 154 | value << char 155 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 156 | end 157 | when :value_end then 158 | case char 159 | when ' ' then state = :value_end 160 | when ',' then 161 | state = :key 162 | yield key.string.strip, value.string 163 | key = StringIO.new 164 | value = StringIO.new; 165 | else raise Net::LDAP::InvalidDNError, "DN badly formed" 166 | end 167 | else raise Net::LDAP::InvalidDNError, "Fell out of state machine" 168 | end 169 | end 170 | 171 | # Last pair 172 | raise Net::LDAP::InvalidDNError, "DN badly formed" unless 173 | [:value, :value_normal, :value_hexstring, :value_end].include? state 174 | 175 | yield key.string.strip, value.string 176 | end 177 | 178 | ## 179 | # Returns the DN as an array in the form expected by the constructor. 180 | def to_a 181 | a = [] 182 | self.each_pair { |key, value| a << key << value } 183 | a 184 | end 185 | 186 | ## 187 | # Return the DN as an escaped string. 188 | def to_s 189 | @dn 190 | end 191 | 192 | # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions 193 | # for dn values. All of the following must be escaped in any normal string 194 | # using a single backslash ('\') as escape. 195 | ESCAPES = %w[, + " \\ < > ;] 196 | 197 | # Compiled character class regexp using the values from the above list, and 198 | # checking for a space or # at the start, or space at the end, of the 199 | # string. 200 | ESCAPE_RE = Regexp.new("(^ |^#| $|[" + 201 | ESCAPES.map { |e| Regexp.escape(e) }.join + 202 | "])") 203 | 204 | ## 205 | # Escape a string for use in a DN value 206 | def self.escape(string) 207 | string.gsub(ESCAPE_RE) { |char| "\\" + char } 208 | end 209 | 210 | ## 211 | # Proxy all other requests to the string object, because a DN is mainly 212 | # used within the library as a string 213 | def method_missing(method, *args, &block) 214 | @dn.send(method, *args, &block) 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /lib/net/ldap/entry.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | ## 3 | # Objects of this class represent individual entries in an LDAP directory. 4 | # User code generally does not instantiate this class. Net::LDAP#search 5 | # provides objects of this class to user code, either as block parameters or 6 | # as return values. 7 | # 8 | # In LDAP-land, an "entry" is a collection of attributes that are uniquely 9 | # and globally identified by a DN ("Distinguished Name"). Attributes are 10 | # identified by short, descriptive words or phrases. Although a directory is 11 | # free to implement any attribute name, most of them follow rigorous 12 | # standards so that the range of commonly-encountered attribute names is not 13 | # large. 14 | # 15 | # An attribute name is case-insensitive. Most directories also restrict the 16 | # range of characters allowed in attribute names. To simplify handling 17 | # attribute names, Net::LDAP::Entry internally converts them to a standard 18 | # format. Therefore, the methods which take attribute names can take Strings 19 | # or Symbols, and work correctly regardless of case or capitalization. 20 | # 21 | # An attribute consists of zero or more data items called values. An 22 | # entry is the combination of a unique DN, a set of attribute names, and a 23 | # (possibly-empty) array of values for each attribute. 24 | # 25 | # Class Net::LDAP::Entry provides convenience methods for dealing with LDAP 26 | # entries. In addition to the methods documented below, you may access 27 | # individual attributes of an entry simply by giving the attribute name as 28 | # the name of a method call. For example: 29 | # 30 | # ldap.search( ... ) do |entry| 31 | # puts "Common name: #{entry.cn}" 32 | # puts "Email addresses:" 33 | # entry.mail.each {|ma| puts ma} 34 | # end 35 | # 36 | # If you use this technique to access an attribute that is not present in a 37 | # particular Entry object, a NoMethodError exception will be raised. 38 | # 39 | #-- 40 | # Ugly problem to fix someday: We key off the internal hash with a canonical 41 | # form of the attribute name: convert to a string, downcase, then take the 42 | # symbol. Unfortunately we do this in at least three places. Should do it in 43 | # ONE place. 44 | class Net::LDAP::Entry 45 | ## 46 | # This constructor is not generally called by user code. 47 | def initialize(dn = nil) #:nodoc: 48 | @myhash = {} 49 | @myhash[:dn] = [dn] 50 | end 51 | 52 | ## 53 | # Use the LDIF format for Marshal serialization. 54 | def _dump(depth) #:nodoc: 55 | to_ldif 56 | end 57 | 58 | ## 59 | # Use the LDIF format for Marshal serialization. 60 | def self._load(entry) #:nodoc: 61 | from_single_ldif_string(entry) 62 | end 63 | 64 | class << self 65 | ## 66 | # Converts a single LDIF entry string into an Entry object. Useful for 67 | # Marshal serialization. If a string with multiple LDIF entries is 68 | # provided, an exception will be raised. 69 | def from_single_ldif_string(ldif) 70 | ds = Net::LDAP::Dataset.read_ldif(::StringIO.new(ldif)) 71 | 72 | return nil if ds.empty? 73 | 74 | raise Net::LDAP::EntryOverflowError, "Too many LDIF entries" unless ds.size == 1 75 | 76 | entry = ds.to_entries.first 77 | 78 | return nil if entry.dn.nil? 79 | entry 80 | end 81 | 82 | ## 83 | # Canonicalizes an LDAP attribute name as a \Symbol. The name is 84 | # lowercased and, if present, a trailing equals sign is removed. 85 | def attribute_name(name) 86 | name = name.to_s.downcase 87 | name = name[0..-2] if name[-1] == ?= 88 | name.to_sym 89 | end 90 | end 91 | 92 | ## 93 | # Sets or replaces the array of values for the provided attribute. The 94 | # attribute name is canonicalized prior to assignment. 95 | # 96 | # When an attribute is set using this, that attribute is now made 97 | # accessible through methods as well. 98 | # 99 | # entry = Net::LDAP::Entry.new("dc=com") 100 | # entry.foo # => NoMethodError 101 | # entry["foo"] = 12345 # => [12345] 102 | # entry.foo # => [12345] 103 | def []=(name, value) 104 | @myhash[self.class.attribute_name(name)] = Kernel::Array(value) 105 | end 106 | 107 | ## 108 | # Reads the array of values for the provided attribute. The attribute name 109 | # is canonicalized prior to reading. Returns an empty array if the 110 | # attribute does not exist. 111 | def [](name) 112 | name = self.class.attribute_name(name) 113 | @myhash[name] || [] 114 | end 115 | 116 | ## 117 | # Read the first value for the provided attribute. The attribute name 118 | # is canonicalized prior to reading. Returns nil if the attribute does 119 | # not exist. 120 | def first(name) 121 | self[name].first 122 | end 123 | 124 | ## 125 | # Returns the first distinguished name (dn) of the Entry as a \String. 126 | def dn 127 | self[:dn].first.to_s 128 | end 129 | 130 | ## 131 | # Returns an array of the attribute names present in the Entry. 132 | def attribute_names 133 | @myhash.keys 134 | end 135 | 136 | ## 137 | # Creates a duplicate of the internal Hash containing the attributes 138 | # of the entry. 139 | def to_h 140 | @myhash.dup 141 | end 142 | 143 | ## 144 | # Accesses each of the attributes present in the Entry. 145 | # 146 | # Calls a user-supplied block with each attribute in turn, passing two 147 | # arguments to the block: a Symbol giving the name of the attribute, and a 148 | # (possibly empty) \Array of data values. 149 | def each # :yields: attribute-name, data-values-array 150 | return unless block_given? 151 | attribute_names.each do|a| 152 | attr_name, values = a, self[a] 153 | yield attr_name, values 154 | end 155 | end 156 | alias_method :each_attribute, :each 157 | 158 | ## 159 | # Converts the Entry to an LDIF-formatted String 160 | def to_ldif 161 | Net::LDAP::Dataset.from_entry(self).to_ldif_string 162 | end 163 | 164 | def respond_to?(sym, include_all = false) #:nodoc: 165 | return true if valid_attribute?(self.class.attribute_name(sym)) 166 | return super 167 | end 168 | 169 | def method_missing(sym, *args, &block) #:nodoc: 170 | name = self.class.attribute_name(sym) 171 | 172 | if valid_attribute?(name ) 173 | if setter?(sym) && args.size == 1 174 | value = args.first 175 | value = Array(value) 176 | self[name]= value 177 | return value 178 | elsif args.empty? 179 | return self[name] 180 | end 181 | end 182 | 183 | super 184 | end 185 | 186 | # Given a valid attribute symbol, returns true. 187 | def valid_attribute?(attr_name) 188 | attribute_names.include?(attr_name) 189 | end 190 | private :valid_attribute? 191 | 192 | # Returns true if the symbol ends with an equal sign. 193 | def setter?(sym) 194 | sym.to_s[-1] == ?= 195 | end 196 | private :setter? 197 | 198 | def ==(other) 199 | other.instance_of?(self.class) && @myhash == other.to_h 200 | end 201 | end # class Entry 202 | -------------------------------------------------------------------------------- /lib/net/ldap/error.rb: -------------------------------------------------------------------------------- 1 | class Net::LDAP 2 | class Error < StandardError; end 3 | 4 | class AlreadyOpenedError < Error; end 5 | class SocketError < Error; end 6 | class ConnectionError < Error 7 | def self.new(errors) 8 | error = errors.first.first 9 | if errors.size == 1 10 | return error if error.is_a? Errno::ECONNREFUSED 11 | 12 | return Net::LDAP::Error.new(error.message) 13 | end 14 | 15 | super 16 | end 17 | 18 | def initialize(errors) 19 | message = "Unable to connect to any given server: \n #{errors.map { |e, h, p| "#{e.class}: #{e.message} (#{h}:#{p})" }.join("\n ")}" 20 | super(message) 21 | end 22 | end 23 | class NoOpenSSLError < Error; end 24 | class NoStartTLSResultError < Error; end 25 | class NoSearchBaseError < Error; end 26 | class StartTLSError < Error; end 27 | class EncryptionUnsupportedError < Error; end 28 | class EncMethodUnsupportedError < Error; end 29 | class AuthMethodUnsupportedError < Error; end 30 | class BindingInformationInvalidError < Error; end 31 | class NoBindResultError < Error; end 32 | class SASLChallengeOverflowError < Error; end 33 | class SearchSizeInvalidError < Error; end 34 | class SearchScopeInvalidError < Error; end 35 | class ResponseTypeInvalidError < Error; end 36 | class ResponseMissingOrInvalidError < Error; end 37 | class EmptyDNError < Error; end 38 | class InvalidDNError < Error; end 39 | class HashTypeUnsupportedError < Error; end 40 | class OperatorError < Error; end 41 | class SubstringFilterError < Error; end 42 | class SearchFilterError < Error; end 43 | class BERInvalidError < Error; end 44 | class SearchFilterTypeUnknownError < Error; end 45 | class BadAttributeError < Error; end 46 | class FilterTypeUnknownError < Error; end 47 | class FilterSyntaxInvalidError < Error; end 48 | class EntryOverflowError < Error; end 49 | end 50 | -------------------------------------------------------------------------------- /lib/net/ldap/instrumentation.rb: -------------------------------------------------------------------------------- 1 | module Net::LDAP::Instrumentation 2 | attr_reader :instrumentation_service 3 | private :instrumentation_service 4 | 5 | # Internal: Instrument a block with the defined instrumentation service. 6 | # 7 | # Yields the event payload if a block is given. 8 | # 9 | # Skips instrumentation if no service is set. 10 | # 11 | # Returns the return value of the block. 12 | def instrument(event, payload = {}) 13 | payload = (payload || {}).dup 14 | if instrumentation_service 15 | instrumentation_service.instrument(event, payload) do |instr_payload| 16 | instr_payload[:result] = yield(instr_payload) if block_given? 17 | end 18 | else 19 | yield(payload) if block_given? 20 | end 21 | end 22 | private :instrument 23 | end 24 | -------------------------------------------------------------------------------- /lib/net/ldap/password.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require 'digest/sha1' 3 | require 'digest/sha2' 4 | require 'digest/md5' 5 | require 'base64' 6 | require 'securerandom' 7 | 8 | class Net::LDAP::Password 9 | class << self 10 | # Generate a password-hash suitable for inclusion in an LDAP attribute. 11 | # Pass a hash type as a symbol (:md5, :sha, :ssha) and a plaintext 12 | # password. This function will return a hashed representation. 13 | # 14 | #-- 15 | # STUB: This is here to fulfill the requirements of an RFC, which 16 | # one? 17 | # 18 | # TODO: 19 | # * maybe salted-md5 20 | # * Should we provide sha1 as a synonym for sha1? I vote no because then 21 | # should you also provide ssha1 for symmetry? 22 | # 23 | def generate(type, str) 24 | case type 25 | when :md5 26 | '{MD5}' + Base64.strict_encode64(Digest::MD5.digest(str)) 27 | when :sha 28 | '{SHA}' + Base64.strict_encode64(Digest::SHA1.digest(str)) 29 | when :ssha 30 | salt = SecureRandom.random_bytes(16) 31 | '{SSHA}' + Base64.strict_encode64(Digest::SHA1.digest(str + salt) + salt) 32 | when :ssha256 33 | salt = SecureRandom.random_bytes(16) 34 | '{SSHA256}' + Base64.strict_encode64(Digest::SHA256.digest(str + salt) + salt) 35 | else 36 | raise Net::LDAP::HashTypeUnsupportedError, "Unsupported password-hash type (#{type})" 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/net/ldap/pdu.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require 'ostruct' 3 | 4 | ## 5 | # Defines the Protocol Data Unit (PDU) for LDAP. An LDAP PDU always looks 6 | # like a BER SEQUENCE with at least two elements: an INTEGER message ID 7 | # number and an application-specific SEQUENCE. Some LDAPv3 packets also 8 | # include an optional third element, a sequence of "controls" (see RFC 2251 9 | # section 4.1.12 for more information). 10 | # 11 | # The application-specific tag in the sequence tells us what kind of packet 12 | # it is, and each kind has its own format, defined in RFC-1777. 13 | # 14 | # Observe that many clients (such as ldapsearch) do not necessarily enforce 15 | # the expected application tags on received protocol packets. This 16 | # implementation does interpret the RFC strictly in this regard, and it 17 | # remains to be seen whether there are servers out there that will not work 18 | # well with our approach. 19 | # 20 | # Currently, we only support controls on SearchResult. 21 | # 22 | # http://tools.ietf.org/html/rfc4511#section-4.1.1 23 | # http://tools.ietf.org/html/rfc4511#section-4.1.9 24 | class Net::LDAP::PDU 25 | class Error < RuntimeError; end 26 | 27 | # http://tools.ietf.org/html/rfc4511#section-4.2 28 | BindRequest = 0 29 | # http://tools.ietf.org/html/rfc4511#section-4.2.2 30 | BindResult = 1 31 | # http://tools.ietf.org/html/rfc4511#section-4.3 32 | UnbindRequest = 2 33 | # http://tools.ietf.org/html/rfc4511#section-4.5.1 34 | SearchRequest = 3 35 | # http://tools.ietf.org/html/rfc4511#section-4.5.2 36 | SearchReturnedData = 4 37 | SearchResult = 5 38 | # see also SearchResultReferral (19) 39 | # http://tools.ietf.org/html/rfc4511#section-4.6 40 | ModifyRequest = 6 41 | ModifyResponse = 7 42 | # http://tools.ietf.org/html/rfc4511#section-4.7 43 | AddRequest = 8 44 | AddResponse = 9 45 | # http://tools.ietf.org/html/rfc4511#section-4.8 46 | DeleteRequest = 10 47 | DeleteResponse = 11 48 | # http://tools.ietf.org/html/rfc4511#section-4.9 49 | ModifyRDNRequest = 12 50 | ModifyRDNResponse = 13 51 | # http://tools.ietf.org/html/rfc4511#section-4.10 52 | CompareRequest = 14 53 | CompareResponse = 15 54 | # http://tools.ietf.org/html/rfc4511#section-4.11 55 | AbandonRequest = 16 56 | # http://tools.ietf.org/html/rfc4511#section-4.5.2 57 | SearchResultReferral = 19 58 | # http://tools.ietf.org/html/rfc4511#section-4.12 59 | ExtendedRequest = 23 60 | ExtendedResponse = 24 61 | # unused: http://tools.ietf.org/html/rfc4511#section-4.13 62 | IntermediateResponse = 25 63 | 64 | ## 65 | # The LDAP packet message ID. 66 | attr_reader :message_id 67 | alias_method :msg_id, :message_id 68 | 69 | ## 70 | # The application protocol format tag. 71 | attr_reader :app_tag 72 | 73 | attr_reader :search_entry 74 | attr_reader :search_referrals 75 | attr_reader :search_parameters 76 | attr_reader :bind_parameters 77 | attr_reader :extended_response 78 | 79 | ## 80 | # Returns RFC-2251 Controls if any. 81 | attr_reader :ldap_controls 82 | alias_method :result_controls, :ldap_controls 83 | # Messy. Does this functionality belong somewhere else? 84 | 85 | def initialize(ber_object) 86 | begin 87 | @message_id = ber_object[0].to_i 88 | # Grab the bottom five bits of the identifier so we know which type of 89 | # PDU this is. 90 | # 91 | # This is safe enough in LDAP-land, but it is recommended that other 92 | # approaches be taken for other protocols in the case that there's an 93 | # app-specific tag that has both primitive and constructed forms. 94 | @app_tag = ber_object[1].ber_identifier & 0x1f 95 | @ldap_controls = [] 96 | rescue Exception => ex 97 | raise Net::LDAP::PDU::Error, "LDAP PDU Format Error: #{ex.message}" 98 | end 99 | 100 | case @app_tag 101 | when BindResult 102 | parse_bind_response(ber_object[1]) 103 | when SearchReturnedData 104 | parse_search_return(ber_object[1]) 105 | when SearchResultReferral 106 | parse_search_referral(ber_object[1]) 107 | when SearchResult 108 | parse_ldap_result(ber_object[1]) 109 | when ModifyResponse 110 | parse_ldap_result(ber_object[1]) 111 | when AddResponse 112 | parse_ldap_result(ber_object[1]) 113 | when DeleteResponse 114 | parse_ldap_result(ber_object[1]) 115 | when ModifyRDNResponse 116 | parse_ldap_result(ber_object[1]) 117 | when SearchRequest 118 | parse_ldap_search_request(ber_object[1]) 119 | when BindRequest 120 | parse_bind_request(ber_object[1]) 121 | when UnbindRequest 122 | parse_unbind_request(ber_object[1]) 123 | when ExtendedResponse 124 | parse_extended_response(ber_object[1]) 125 | else 126 | raise Error.new("unknown pdu-type: #{@app_tag}") 127 | end 128 | 129 | parse_controls(ber_object[2]) if ber_object[2] 130 | end 131 | 132 | ## 133 | # Returns a hash which (usually) defines the members :resultCode, 134 | # :errorMessage, and :matchedDN. These values come directly from an LDAP 135 | # response packet returned by the remote peer. Also see #result_code. 136 | def result 137 | @ldap_result || {} 138 | end 139 | 140 | def error_message 141 | result[:errorMessage] || "" 142 | end 143 | 144 | ## 145 | # This returns an LDAP result code taken from the PDU, but it will be nil 146 | # if there wasn't a result code. That can easily happen depending on the 147 | # type of packet. 148 | def result_code(code = :resultCode) 149 | @ldap_result and @ldap_result[code] 150 | end 151 | 152 | def status 153 | Net::LDAP::ResultCodesNonError.include?(result_code) ? :success : :failure 154 | end 155 | 156 | def success? 157 | status == :success 158 | end 159 | 160 | def failure? 161 | !success? 162 | end 163 | 164 | ## 165 | # Return serverSaslCreds, which are only present in BindResponse packets. 166 | #-- 167 | # Messy. Does this functionality belong somewhere else? We ought to 168 | # refactor the accessors of this class before they get any kludgier. 169 | def result_server_sasl_creds 170 | @ldap_result && @ldap_result[:serverSaslCreds] 171 | end 172 | 173 | def parse_ldap_result(sequence) 174 | sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP result length." 175 | @ldap_result = { 176 | :resultCode => sequence[0], 177 | :matchedDN => sequence[1], 178 | :errorMessage => sequence[2], 179 | } 180 | parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == Net::LDAP::ResultCodeReferral 181 | end 182 | private :parse_ldap_result 183 | 184 | ## 185 | # Parse an extended response 186 | # 187 | # http://www.ietf.org/rfc/rfc2251.txt 188 | # 189 | # Each Extended operation consists of an Extended request and an 190 | # Extended response. 191 | # 192 | # ExtendedRequest ::= [APPLICATION 23] SEQUENCE { 193 | # requestName [0] LDAPOID, 194 | # requestValue [1] OCTET STRING OPTIONAL } 195 | 196 | def parse_extended_response(sequence) 197 | sequence.length.between?(3, 5) or raise Net::LDAP::PDU::Error, "Invalid LDAP result length." 198 | @ldap_result = { 199 | :resultCode => sequence[0], 200 | :matchedDN => sequence[1], 201 | :errorMessage => sequence[2], 202 | } 203 | @extended_response = sequence.length == 3 ? nil : sequence.last 204 | end 205 | private :parse_extended_response 206 | 207 | ## 208 | # A Bind Response may have an additional field, ID [7], serverSaslCreds, 209 | # per RFC 2251 pgh 4.2.3. 210 | def parse_bind_response(sequence) 211 | sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP Bind Response length." 212 | parse_ldap_result(sequence) 213 | @ldap_result[:serverSaslCreds] = sequence[3] if sequence.length >= 4 214 | @ldap_result 215 | end 216 | private :parse_bind_response 217 | 218 | # Definition from RFC 1777 (we're handling application-4 here). 219 | # 220 | # Search Response ::= 221 | # CHOICE { 222 | # entry [APPLICATION 4] SEQUENCE { 223 | # objectName LDAPDN, 224 | # attributes SEQUENCE OF SEQUENCE { 225 | # AttributeType, 226 | # SET OF AttributeValue 227 | # } 228 | # }, 229 | # resultCode [APPLICATION 5] LDAPResult 230 | # } 231 | # 232 | # We concoct a search response that is a hash of the returned attribute 233 | # values. 234 | # 235 | # NOW OBSERVE CAREFULLY: WE ARE DOWNCASING THE RETURNED ATTRIBUTE NAMES. 236 | # 237 | # This is to make them more predictable for user programs, but it may not 238 | # be a good idea. Maybe this should be configurable. 239 | def parse_search_return(sequence) 240 | sequence.length >= 2 or raise Net::LDAP::PDU::Error, "Invalid Search Response length." 241 | @search_entry = Net::LDAP::Entry.new(sequence[0]) 242 | sequence[1].each { |seq| @search_entry[seq[0]] = seq[1] } 243 | end 244 | private :parse_search_return 245 | 246 | ## 247 | # A search referral is a sequence of one or more LDAP URIs. Any number of 248 | # search-referral replies can be returned by the server, interspersed with 249 | # normal replies in any order. 250 | #-- 251 | # Until I can think of a better way to do this, we'll return the referrals 252 | # as an array. It'll be up to higher-level handlers to expose something 253 | # reasonable to the client. 254 | def parse_search_referral(uris) 255 | @search_referrals = uris 256 | end 257 | private :parse_search_referral 258 | 259 | ## 260 | # Per RFC 2251, an LDAP "control" is a sequence of tuples, each consisting 261 | # of an OID, a boolean criticality flag defaulting FALSE, and an OPTIONAL 262 | # Octet String. If only two fields are given, the second one may be either 263 | # criticality or data, since criticality has a default value. Someday we 264 | # may want to come back here and add support for some of more-widely used 265 | # controls. RFC-2696 is a good example. 266 | def parse_controls(sequence) 267 | @ldap_controls = sequence.map do |control| 268 | o = OpenStruct.new 269 | o.oid, o.criticality, o.value = control[0], control[1], control[2] 270 | if o.criticality and o.criticality.is_a?(String) 271 | o.value = o.criticality 272 | o.criticality = false 273 | end 274 | o 275 | end 276 | end 277 | private :parse_controls 278 | 279 | # (provisional, must document) 280 | def parse_ldap_search_request(sequence) 281 | s = OpenStruct.new 282 | s.base_object, s.scope, s.deref_aliases, s.size_limit, s.time_limit, 283 | s.types_only, s.filter, s.attributes = sequence 284 | @search_parameters = s 285 | end 286 | private :parse_ldap_search_request 287 | 288 | # (provisional, must document) 289 | def parse_bind_request sequence 290 | s = OpenStruct.new 291 | s.version, s.name, s.authentication = sequence 292 | @bind_parameters = s 293 | end 294 | private :parse_bind_request 295 | 296 | # (provisional, must document) 297 | # UnbindRequest has no content so this is a no-op. 298 | def parse_unbind_request(sequence) 299 | nil 300 | end 301 | private :parse_unbind_request 302 | end 303 | 304 | module Net 305 | ## 306 | # Handle renamed constants Net::LdapPdu (Net::LDAP::PDU) and 307 | # Net::LdapPduError (Net::LDAP::PDU::Error). 308 | def self.const_missing(name) #:nodoc: 309 | case name.to_s 310 | when "LdapPdu" 311 | warn "Net::#{name} has been deprecated. Use Net::LDAP::PDU instead." 312 | Net::LDAP::PDU 313 | when "LdapPduError" 314 | warn "Net::#{name} has been deprecated. Use Net::LDAP::PDU::Error instead." 315 | Net::LDAP::PDU::Error 316 | when 'LDAP' 317 | else 318 | super 319 | end 320 | end 321 | end # module Net 322 | -------------------------------------------------------------------------------- /lib/net/ldap/version.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class LDAP 3 | VERSION = "0.19.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/net/snmp.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require_relative 'ldap/version' 3 | 4 | # :stopdoc: 5 | module Net 6 | class SNMP 7 | VERSION = Net::LDAP::VERSION 8 | AsnSyntax = Net::BER.compile_syntax({ 9 | :application => { 10 | :primitive => { 11 | 1 => :integer, # Counter32, (RFC2578 sec 2) 12 | 2 => :integer, # Gauge32 or Unsigned32, (RFC2578 sec 2) 13 | 3 => :integer # TimeTicks32, (RFC2578 sec 2) 14 | }, 15 | :constructed => {}, 16 | }, 17 | :context_specific => { 18 | :primitive => {}, 19 | :constructed => { 20 | 0 => :array, # GetRequest PDU (RFC1157 pgh 4.1.2) 21 | 1 => :array, # GetNextRequest PDU (RFC1157 pgh 4.1.3) 22 | 2 => :array # GetResponse PDU (RFC1157 pgh 4.1.4) 23 | }, 24 | }, 25 | }) 26 | 27 | # SNMP 32-bit counter. 28 | # Defined in RFC1155 (Structure of Mangement Information), section 6. 29 | # A 32-bit counter is an ASN.1 application [1] implicit unsigned integer 30 | # with a range from 0 to 2^^32 - 1. 31 | class Counter32 32 | def initialize value 33 | @value = value 34 | end 35 | def to_ber 36 | @value.to_ber_application(1) 37 | end 38 | end 39 | 40 | # SNMP 32-bit gauge. 41 | # Defined in RFC1155 (Structure of Mangement Information), section 6. 42 | # A 32-bit counter is an ASN.1 application [2] implicit unsigned integer. 43 | # This is also indistinguishable from Unsigned32. (Need to alias them.) 44 | class Gauge32 45 | def initialize value 46 | @value = value 47 | end 48 | def to_ber 49 | @value.to_ber_application(2) 50 | end 51 | end 52 | 53 | # SNMP 32-bit timer-ticks. 54 | # Defined in RFC1155 (Structure of Mangement Information), section 6. 55 | # A 32-bit counter is an ASN.1 application [3] implicit unsigned integer. 56 | class TimeTicks32 57 | def initialize value 58 | @value = value 59 | end 60 | def to_ber 61 | @value.to_ber_application(3) 62 | end 63 | end 64 | end 65 | 66 | class SnmpPdu 67 | class Error < StandardError; end 68 | PduTypes = [ 69 | :get_request, 70 | :get_next_request, 71 | :get_response, 72 | :set_request, 73 | :trap, 74 | ] 75 | ErrorStatusCodes = { # Per RFC1157, pgh 4.1.1 76 | 0 => "noError", 77 | 1 => "tooBig", 78 | 2 => "noSuchName", 79 | 3 => "badValue", 80 | 4 => "readOnly", 81 | 5 => "genErr", 82 | } 83 | 84 | class << self 85 | def parse ber_object 86 | n = new 87 | n.send :parse, ber_object 88 | n 89 | end 90 | end 91 | 92 | attr_reader :version, :community, :pdu_type, :variables, :error_status 93 | attr_accessor :request_id, :error_index 94 | 95 | def initialize args={} 96 | @version = args[:version] || 0 97 | @community = args[:community] || "public" 98 | @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value. 99 | @error_status = args[:error_status] || 0 100 | @error_index = args[:error_index] || 0 101 | @variables = args[:variables] || [] 102 | end 103 | 104 | #-- 105 | def parse ber_object 106 | begin 107 | parse_ber_object ber_object 108 | rescue Error 109 | # Pass through any SnmpPdu::Error instances 110 | raise $! 111 | rescue 112 | # Wrap any basic parsing error so it becomes a PDU-format error 113 | raise Error.new( "snmp-pdu format error" ) 114 | end 115 | end 116 | private :parse 117 | 118 | def parse_ber_object ber_object 119 | send :version=, ber_object[0].to_i 120 | send :community=, ber_object[1].to_s 121 | 122 | data = ber_object[2] 123 | case (app_tag = data.ber_identifier & 31) 124 | when 0 125 | send :pdu_type=, :get_request 126 | parse_get_request data 127 | when 1 128 | send :pdu_type=, :get_next_request 129 | # This PDU is identical to get-request except for the type. 130 | parse_get_request data 131 | when 2 132 | send :pdu_type=, :get_response 133 | # This PDU is identical to get-request except for the type, 134 | # the error_status and error_index values are meaningful, 135 | # and the fact that the variable bindings will be non-null. 136 | parse_get_response data 137 | else 138 | raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) 139 | end 140 | end 141 | private :parse_ber_object 142 | 143 | #-- 144 | # Defined in RFC1157, pgh 4.1.2. 145 | def parse_get_request data 146 | send :request_id=, data[0].to_i 147 | # data[1] is error_status, always zero. 148 | # data[2] is error_index, always zero. 149 | send :error_status=, 0 150 | send :error_index=, 0 151 | data[3].each do |n, v| 152 | # A variable-binding, of which there may be several, 153 | # consists of an OID and a BER null. 154 | # We're ignoring the null, we might want to verify it instead. 155 | unless v.is_a?(Net::BER::BerIdentifiedNull) 156 | raise Error.new(" invalid variable-binding in get-request" ) 157 | end 158 | add_variable_binding n, nil 159 | end 160 | end 161 | private :parse_get_request 162 | 163 | #-- 164 | # Defined in RFC1157, pgh 4.1.4 165 | def parse_get_response data 166 | send :request_id=, data[0].to_i 167 | send :error_status=, data[1].to_i 168 | send :error_index=, data[2].to_i 169 | data[3].each do |n, v| 170 | # A variable-binding, of which there may be several, 171 | # consists of an OID and a BER null. 172 | # We're ignoring the null, we might want to verify it instead. 173 | add_variable_binding n, v 174 | end 175 | end 176 | private :parse_get_response 177 | 178 | 179 | def version= ver 180 | unless [0, 2].include?(ver) 181 | raise Error.new("unknown snmp-version: #{ver}") 182 | end 183 | @version = ver 184 | end 185 | 186 | def pdu_type= t 187 | unless PduTypes.include?(t) 188 | raise Error.new("unknown pdu-type: #{t}") 189 | end 190 | @pdu_type = t 191 | end 192 | 193 | def error_status= es 194 | unless ErrorStatusCodes.key?(es) 195 | raise Error.new("unknown error-status: #{es}") 196 | end 197 | @error_status = es 198 | end 199 | 200 | def community= c 201 | @community = c.to_s 202 | end 203 | 204 | #-- 205 | # Syntactic sugar 206 | def add_variable_binding name, value=nil 207 | @variables ||= [] 208 | @variables << [name, value] 209 | end 210 | 211 | def to_ber_string 212 | [ 213 | version.to_ber, 214 | community.to_ber, 215 | pdu_to_ber_string, 216 | ].to_ber_sequence 217 | end 218 | 219 | #-- 220 | # Helper method that returns a PDU payload in BER form, 221 | # depending on the PDU type. 222 | def pdu_to_ber_string 223 | case pdu_type 224 | when :get_request 225 | [ 226 | request_id.to_ber, 227 | error_status.to_ber, 228 | error_index.to_ber, 229 | [ 230 | @variables.map do|n, v| 231 | [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence 232 | end, 233 | ].to_ber_sequence, 234 | ].to_ber_contextspecific(0) 235 | when :get_next_request 236 | [ 237 | request_id.to_ber, 238 | error_status.to_ber, 239 | error_index.to_ber, 240 | [ 241 | @variables.map do|n, v| 242 | [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence 243 | end, 244 | ].to_ber_sequence, 245 | ].to_ber_contextspecific(1) 246 | when :get_response 247 | [ 248 | request_id.to_ber, 249 | error_status.to_ber, 250 | error_index.to_ber, 251 | [ 252 | @variables.map do|n, v| 253 | [n.to_ber_oid, v.to_ber].to_ber_sequence 254 | end, 255 | ].to_ber_sequence, 256 | ].to_ber_contextspecific(2) 257 | else 258 | raise Error.new( "unknown pdu-type: #{pdu_type}" ) 259 | end 260 | end 261 | private :pdu_to_ber_string 262 | end 263 | end 264 | # :startdoc: 265 | -------------------------------------------------------------------------------- /net-ldap.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require_relative 'lib/net/ldap/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{net-ldap} 8 | s.version = Net::LDAP::VERSION 9 | s.license = "MIT" 10 | s.authors = ["Francis Cianfrocca", "Emiel van de Laar", "Rory O'Connell", "Kaspar Schiess", "Austin Ziegler", "Michael Schaarschmidt"] 11 | s.description = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the 12 | Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for 13 | accessing distributed directory services. Net::LDAP is written completely in 14 | Ruby with no external dependencies. It supports most LDAP client features and a 15 | subset of server features as well. 16 | 17 | Net::LDAP has been tested against modern popular LDAP servers including 18 | OpenLDAP and Active Directory. The current release is mostly compliant with 19 | earlier versions of the IETF LDAP RFCs (2251-2256, 2829-2830, 3377, and 3771). 20 | Our roadmap for Net::LDAP 1.0 is to gain full client compliance with 21 | the most recent LDAP RFCs (4510-4519, plutions of 4520-4532).} 22 | s.email = ["blackhedd@rubyforge.org", "gemiel@gmail.com", "rory.ocon@gmail.com", "kaspar.schiess@absurd.li", "austin@rubyforge.org"] 23 | s.extra_rdoc_files = ["Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "README.rdoc"] 24 | s.files = Dir["*.rdoc", "lib/**/*"] 25 | s.test_files = s.files.grep(%r{^test}) 26 | s.homepage = %q{http://github.com/ruby-ldap/ruby-net-ldap} 27 | s.rdoc_options = ["--main", "README.rdoc"] 28 | s.require_paths = ["lib"] 29 | s.required_ruby_version = ">= 3.0.0" 30 | 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} 31 | 32 | s.add_dependency("base64") 33 | s.add_dependency("ostruct") 34 | end 35 | -------------------------------------------------------------------------------- /script/changelog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: script/changelog [-r ] [-b ] [-h ] 3 | # 4 | # repo: BASE string of GitHub REPOsitory url. e.g. "user_or_org/REPOsitory". Defaults to git remote url. 5 | # base: git ref to compare from. e.g. "v1.3.1". Defaults to latest git tag. 6 | # head: git ref to compare to. Defaults to "HEAD". 7 | # 8 | # Generate a changelog preview from pull requests merged between `base` and 9 | # `head`. 10 | # 11 | # https://github.com/jch/release-scripts/blob/master/changelog 12 | set -e 13 | 14 | [ $# -eq 0 ] && set -- --help 15 | while [[ $# > 1 ]] 16 | do 17 | key="$1" 18 | case $key in 19 | -r|--repo) 20 | repo="$2" 21 | shift 22 | ;; 23 | -b|--base) 24 | base="$2" 25 | shift 26 | ;; 27 | -h|--head) 28 | head="$2" 29 | shift 30 | ;; 31 | *) 32 | ;; 33 | esac 34 | shift 35 | done 36 | 37 | repo="${repo:-$(git remote -v | grep push | awk '{print $2}' | cut -d'/' -f4- | sed 's/\.git//')}" 38 | base="${base:-$(git tag -l | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -n 1)}" 39 | head="${head:-HEAD}" 40 | api_url="https://api.github.com" 41 | 42 | # get merged PR's. Better way is to query the API for these, but this is easier 43 | for pr in $(git log --oneline $base..$head | grep "Merge pull request" | awk '{gsub("#",""); print $5}') 44 | do 45 | # frustrated with trying to pull out the right values, fell back to ruby 46 | curl -s "$api_url/repos/$repo/pulls/$pr" | ruby -rjson -e 'pr=JSON.parse(STDIN.read); puts "* #{pr[%q(title)]} {##{pr[%q(number)]}}[#{pr[%q(html_url)]}]"' 47 | done 48 | -------------------------------------------------------------------------------- /script/ldap-docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: script/ldap-docker 3 | # 4 | # Starts a openldap docker container ready for integration tests 5 | 6 | docker run --rm -ti \ 7 | --hostname ldap.example.org \ 8 | --env LDAP_TLS_VERIFY_CLIENT=try \ 9 | -p 389:389 -p 636:636 \ 10 | -v "$(pwd)"/test/fixtures/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom \ 11 | --name my-openldap-container \ 12 | osixia/openldap:1.3.0 --copy-service --loglevel debug -------------------------------------------------------------------------------- /script/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: script/package 3 | # Updates the gemspec and builds a new gem in the pkg directory. 4 | 5 | mkdir -p pkg 6 | gem build *.gemspec 7 | mv *.gem pkg 8 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: script/release 3 | # Build the package, tag a commit, push it to origin, and then release the 4 | # package publicly. 5 | 6 | set -e 7 | 8 | version="$(script/package | grep Version: | awk '{print $2}')" 9 | [ -n "$version" ] || exit 1 10 | 11 | echo $version 12 | git tag "v$version" -m "Release $version" 13 | git push origin 14 | git push origin "v$version" 15 | gem push pkg/*-${version}.gem 16 | -------------------------------------------------------------------------------- /test/ber/core_ext/test_array.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | class TestBERArrayExtension < Test::Unit::TestCase 4 | def test_control_code_array 5 | control_codes = [] 6 | control_codes << ['1.2.3'.to_ber, true.to_ber].to_ber_sequence 7 | control_codes << ['1.7.9'.to_ber, false.to_ber].to_ber_sequence 8 | control_codes = control_codes.to_ber_sequence 9 | res = [['1.2.3', true], ['1.7.9', false]].to_ber_control 10 | assert_equal control_codes, res 11 | end 12 | 13 | def test_wrap_array_if_not_nested 14 | result1 = ['1.2.3', true].to_ber_control 15 | result2 = [['1.2.3', true]].to_ber_control 16 | assert_equal result2, result1 17 | end 18 | 19 | def test_empty_string_if_empty_array 20 | assert_equal "", [].to_ber_control 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/ber/core_ext/test_string.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | class TestBERStringExtension < Test::Unit::TestCase 4 | def setup 5 | @bind_request = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus UNCONSUMED".b 6 | @result = @bind_request.read_ber!(Net::LDAP::AsnSyntax) 7 | end 8 | 9 | def test_parse_ber 10 | assert_equal [1, [3, "Administrator", "ad_is_bogus"]], @result 11 | end 12 | 13 | def test_unconsumed_message 14 | assert_equal " UNCONSUMED", @bind_request 15 | end 16 | 17 | def test_exception_does_not_modify_string 18 | original = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus".b 19 | duplicate = original.dup 20 | flexmock(StringIO).new_instances.should_receive(:read_ber).and_raise(Net::BER::BerError) 21 | duplicate.read_ber!(Net::LDAP::AsnSyntax) rescue Net::BER::BerError 22 | 23 | assert_equal original, duplicate 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/ber/test_ber.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestBEREncoding < Test::Unit::TestCase 4 | def test_empty_array 5 | assert_equal [], [].to_ber.read_ber 6 | end 7 | 8 | def test_array 9 | ary = [1, 2, 3] 10 | encoded_ary = ary.map(&:to_ber).to_ber 11 | 12 | assert_equal ary, encoded_ary.read_ber 13 | end 14 | 15 | # http://tools.ietf.org/html/rfc4511#section-5.1 16 | def test_true 17 | assert_equal "\x01\x01\xFF".b, true.to_ber 18 | end 19 | 20 | def test_false 21 | assert_equal "\x01\x01\x00", false.to_ber 22 | end 23 | 24 | # Sample based 25 | { 26 | 0 => "\x02\x01\x00", 27 | 1 => "\x02\x01\x01", 28 | 127 => "\x02\x01\x7F", 29 | 128 => "\x02\x02\x00\x80", 30 | 255 => "\x02\x02\x00\xFF", 31 | 256 => "\x02\x02\x01\x00", 32 | 65535 => "\x02\x03\x00\xFF\xFF", 33 | 65536 => "\x02\x03\x01\x00\x00", 34 | 8388607 => "\x02\x03\x7F\xFF\xFF", 35 | 8388608 => "\x02\x04\x00\x80\x00\x00", 36 | 16_777_215 => "\x02\x04\x00\xFF\xFF\xFF", 37 | 0x01000000 => "\x02\x04\x01\x00\x00\x00", 38 | 0x3FFFFFFF => "\x02\x04\x3F\xFF\xFF\xFF", 39 | 0x4FFFFFFF => "\x02\x04\x4F\xFF\xFF\xFF", 40 | 41 | # Some odd samples... 42 | 5 => "\x02\x01\x05", 43 | 500 => "\x02\x02\x01\xf4", 44 | 50_000 => "\x02\x03\x00\xC3\x50", 45 | 5_000_000_000 => "\x02\x05\x01\x2a\x05\xF2\x00", 46 | 47 | # negatives 48 | -1 => "\x02\x01\xFF", 49 | -127 => "\x02\x01\x81", 50 | -128 => "\x02\x01\x80", 51 | -255 => "\x02\x02\xFF\x01", 52 | -256 => "\x02\x02\xFF\x00", 53 | -65535 => "\x02\x03\xFF\x00\x01", 54 | -65536 => "\x02\x03\xFF\x00\x00", 55 | -65537 => "\x02\x03\xFE\xFF\xFF", 56 | -8388607 => "\x02\x03\x80\x00\x01", 57 | -8388608 => "\x02\x03\x80\x00\x00", 58 | -16_777_215 => "\x02\x04\xFF\x00\x00\x01", 59 | }.each do |number, expected_encoding| 60 | define_method "test_encode_#{number}" do 61 | assert_equal expected_encoding.b, number.to_ber 62 | end 63 | 64 | define_method "test_decode_encoded_#{number}" do 65 | assert_equal number, expected_encoding.b.read_ber 66 | end 67 | end 68 | 69 | # Round-trip encoding: This is mostly to be sure to cover Bignums well. 70 | def test_powers_of_two 71 | 100.times do |p| 72 | n = 2 << p 73 | 74 | assert_equal n, n.to_ber.read_ber 75 | end 76 | end 77 | 78 | def test_powers_of_ten 79 | 100.times do |p| 80 | n = 5 * 10**p 81 | 82 | assert_equal n, n.to_ber.read_ber 83 | end 84 | end 85 | 86 | if "Ruby 1.9".respond_to?(:encoding) 87 | def test_encode_utf8_strings 88 | assert_equal "\x04\x02\xC3\xA5".b, "\u00e5".force_encoding("UTF-8").to_ber 89 | end 90 | 91 | def test_utf8_encodable_strings 92 | assert_equal "\x04\nteststring", "teststring".encode("US-ASCII").to_ber 93 | end 94 | 95 | def test_encode_binary_data 96 | # This is used for searching for GUIDs in Active Directory 97 | assert_equal "\x04\x10" + "j1\xB4\xA1*\xA2zA\xAC\xA9`?'\xDDQ\x16".b, 98 | ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").to_ber_bin 99 | end 100 | 101 | def test_non_utf8_encodable_strings 102 | assert_equal "\x04\x01\x81".b, "\x81".to_ber 103 | end 104 | end 105 | end 106 | 107 | class TestBERDecoding < Test::Unit::TestCase 108 | def test_decode_number 109 | assert_equal 6, "\002\001\006".read_ber(Net::LDAP::AsnSyntax) 110 | end 111 | 112 | def test_decode_string 113 | assert_equal "testing", "\004\007testing".read_ber(Net::LDAP::AsnSyntax) 114 | end 115 | 116 | def test_decode_ldap_bind_request 117 | assert_equal [1, [3, "Administrator", "ad_is_bogus"]], "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus".read_ber(Net::LDAP::AsnSyntax) 118 | end 119 | end 120 | 121 | class TestBERIdentifiedString < Test::Unit::TestCase 122 | def test_binary_data 123 | data = ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").force_encoding("ASCII-8BIT") 124 | bis = Net::BER::BerIdentifiedString.new(data) 125 | 126 | assert bis.valid_encoding?, "should be a valid encoding" 127 | assert_equal "ASCII-8BIT", bis.encoding.name 128 | end 129 | 130 | def test_ascii_data_in_utf8 131 | data = "some text".force_encoding("UTF-8") 132 | bis = Net::BER::BerIdentifiedString.new(data) 133 | 134 | assert bis.valid_encoding?, "should be a valid encoding" 135 | assert_equal "UTF-8", bis.encoding.name 136 | end 137 | 138 | def test_umlaut_data_in_utf8 139 | data = "Müller".force_encoding("UTF-8") 140 | bis = Net::BER::BerIdentifiedString.new(data) 141 | 142 | assert bis.valid_encoding?, "should be a valid encoding" 143 | assert_equal "UTF-8", bis.encoding.name 144 | end 145 | 146 | def test_utf8_data_in_utf8 147 | data = ["e4b8ad"].pack("H*").force_encoding("UTF-8") 148 | bis = Net::BER::BerIdentifiedString.new(data) 149 | 150 | assert bis.valid_encoding?, "should be a valid encoding" 151 | assert_equal "UTF-8", bis.encoding.name 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /test/fixtures/ca/docker-ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC0zCCAlmgAwIBAgIUCfQ+m0pgZ/BjYAJvxrn/bdGNZokwCgYIKoZIzj0EAwMw 3 | gZYxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxBMUEgQ2FyIFdhc2gxJDAiBgNVBAsT 4 | G0luZm9ybWF0aW9uIFRlY2hub2xvZ3kgRGVwLjEUMBIGA1UEBxMLQWxidXF1ZXJx 5 | dWUxEzARBgNVBAgTCk5ldyBNZXhpY28xHzAdBgNVBAMTFmRvY2tlci1saWdodC1i 6 | YXNlaW1hZ2UwHhcNMTUxMjIzMTM1MzAwWhcNMjAxMjIxMTM1MzAwWjCBljELMAkG 7 | A1UEBhMCVVMxFTATBgNVBAoTDEExQSBDYXIgV2FzaDEkMCIGA1UECxMbSW5mb3Jt 8 | YXRpb24gVGVjaG5vbG9neSBEZXAuMRQwEgYDVQQHEwtBbGJ1cXVlcnF1ZTETMBEG 9 | A1UECBMKTmV3IE1leGljbzEfMB0GA1UEAxMWZG9ja2VyLWxpZ2h0LWJhc2VpbWFn 10 | ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMZf/12pupAgl8Sm+j8GmjNeNbSFAZWW 11 | oTmIvf2Mu4LWPHy4bTldkQgHUbBpT3xWz8f0lB/ru7596CHsGoL2A28hxuclq5hb 12 | Ux1yrIt3bJIY3TuiX25HGTe6kGCJPB1aLaNmMGQwDgYDVR0PAQH/BAQDAgEGMBIG 13 | A1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFE+l6XolXDAYnGLTl4W6ULKHrm74 14 | MB8GA1UdIwQYMBaAFE+l6XolXDAYnGLTl4W6ULKHrm74MAoGCCqGSM49BAMDA2gA 15 | MGUCMQCXLZj8okyxW6UTL7hribUUbu63PbjuwIXnwi420DdNsvA9A7fcQEXScWFL 16 | XAGC8rkCMGcqwXZPSRfwuI9r+R11gTrP92hnaVxs9sjRikctpkQpOyNlIXFPopFK 17 | 8FdfWPypvA== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/fixtures/ldif/06-retcode.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=module{0},cn=config 2 | changetype: modify 3 | add: olcModuleLoad 4 | olcModuleLoad: retcode 5 | 6 | # source: http://www.opensource.apple.com/source/OpenLDAP/OpenLDAP-186/OpenLDAP/tests/data/retcode.conf?txt 7 | 8 | dn: olcOverlay={2}retcode,olcDatabase={1}{{ LDAP_BACKEND }},cn=config 9 | changetype: add 10 | objectClass: olcConfig 11 | objectClass: olcRetcodeConfig 12 | objectClass: olcOverlayConfig 13 | objectClass: top 14 | olcOverlay: retcode 15 | olcRetcodeParent: ou=Retcodes,dc=example,dc=org 16 | olcRetcodeInDir: TRUE 17 | olcRetcodeSleep: 0 18 | olcRetcodeItem: "cn=success" 0x00 19 | olcRetcodeItem: "cn=success w/ delay" 0x00 sleeptime=2 20 | olcRetcodeItem: "cn=operationsError" 0x01 21 | olcRetcodeItem: "cn=protocolError" 0x02 22 | olcRetcodeItem: "cn=timeLimitExceeded" 0x03 op=search 23 | olcRetcodeItem: "cn=sizeLimitExceeded" 0x04 op=search 24 | olcRetcodeItem: "cn=compareFalse" 0x05 op=compare 25 | olcRetcodeItem: "cn=compareTrue" 0x06 op=compare 26 | olcRetcodeItem: "cn=authMethodNotSupported" 0x07 27 | olcRetcodeItem: "cn=strongAuthNotSupported" 0x07 text="same as authMethodNotSupported" 28 | olcRetcodeItem: "cn=strongAuthRequired" 0x08 29 | olcRetcodeItem: "cn=strongerAuthRequired" 0x08 text="same as strongAuthRequired" 30 | olcRetcodeItem: "cn=referral" 0x0a text="LDAPv3" ref="ldap://:9019" 31 | olcRetcodeItem: "cn=adminLimitExceeded" 0x0b text="LDAPv3" 32 | olcRetcodeItem: "cn=unavailableCriticalExtension" 0x0c text="LDAPv3" 33 | olcRetcodeItem: "cn=confidentialityRequired" 0x0d text="LDAPv3" 34 | olcRetcodeItem: "cn=saslBindInProgress" 0x0e text="LDAPv3" 35 | olcRetcodeItem: "cn=noSuchAttribute" 0x10 36 | olcRetcodeItem: "cn=undefinedAttributeType" 0x11 37 | olcRetcodeItem: "cn=inappropriateMatching" 0x12 38 | olcRetcodeItem: "cn=constraintViolation" 0x13 39 | olcRetcodeItem: "cn=attributeOrValueExists" 0x14 40 | olcRetcodeItem: "cn=invalidAttributeSyntax" 0x15 41 | olcRetcodeItem: "cn=noSuchObject" 0x20 42 | olcRetcodeItem: "cn=aliasProblem" 0x21 43 | olcRetcodeItem: "cn=invalidDNSyntax" 0x22 44 | olcRetcodeItem: "cn=aliasDereferencingProblem" 0x24 45 | olcRetcodeItem: "cn=proxyAuthzFailure" 0x2F text="LDAPv3 proxy authorization" 46 | olcRetcodeItem: "cn=inappropriateAuthentication" 0x30 47 | olcRetcodeItem: "cn=invalidCredentials" 0x31 48 | olcRetcodeItem: "cn=insufficientAccessRights" 0x32 49 | olcRetcodeItem: "cn=busy" 0x33 50 | olcRetcodeItem: "cn=unavailable" 0x34 51 | olcRetcodeItem: "cn=unwillingToPerform" 0x35 52 | olcRetcodeItem: "cn=loopDetect" 0x36 53 | olcRetcodeItem: "cn=namingViolation" 0x40 54 | olcRetcodeItem: "cn=objectClassViolation" 0x41 55 | olcRetcodeItem: "cn=notAllowedOnNonleaf" 0x42 56 | olcRetcodeItem: "cn=notAllowedOnRDN" 0x43 57 | olcRetcodeItem: "cn=entryAlreadyExists" 0x44 58 | olcRetcodeItem: "cn=objectClassModsProhibited" 0x45 59 | olcRetcodeItem: "cn=resultsTooLarge" 0x46 text="CLDAP" 60 | olcRetcodeItem: "cn=affectsMultipleDSAs" 0x47 text="LDAPv3" 61 | olcRetcodeItem: "cn=other" 0x50 62 | olcRetcodeItem: "cn=cupResourcesExhausted" 0x71 63 | olcRetcodeItem: "cn=cupSecurityViolation" 0x72 64 | olcRetcodeItem: "cn=cupInvalidData" 0x73 65 | olcRetcodeItem: "cn=cupUnsupportedScheme" 0x74 66 | olcRetcodeItem: "cn=cupReloadRequired" 0x75 67 | olcRetcodeItem: "cn=cancelled" 0x76 68 | olcRetcodeItem: "cn=noSuchOperation" 0x77 69 | olcRetcodeItem: "cn=tooLate" 0x78 70 | olcRetcodeItem: "cn=cannotCancel" 0x79 71 | olcRetcodeItem: "cn=syncRefreshRequired" 0x4100 72 | olcRetcodeItem: "cn=noOperation" 0x410e 73 | olcRetcodeItem: "cn=assertionFailed" 0x410f 74 | olcRetcodeItem: "cn=noReferralsFound" 0x4110 75 | olcRetcodeItem: "cn=cannotChain" 0x4111 76 | -------------------------------------------------------------------------------- /test/fixtures/ldif/50-seed.ldif: -------------------------------------------------------------------------------- 1 | dn: ou=People,dc=example,dc=org 2 | objectClass: top 3 | objectClass: organizationalUnit 4 | ou: People 5 | 6 | dn: ou=Groups,dc=example,dc=org 7 | objectClass: top 8 | objectClass: organizationalUnit 9 | ou: Groups 10 | 11 | # Directory Superuser 12 | dn: uid=admin,dc=example,dc=org 13 | uid: admin 14 | cn: system administrator 15 | sn: administrator 16 | objectClass: top 17 | objectClass: person 18 | objectClass: organizationalPerson 19 | objectClass: inetOrgPerson 20 | displayName: Directory Superuser 21 | userPassword: passworD1 22 | 23 | # Users 1-10 24 | 25 | dn: uid=user1,ou=People,dc=example,dc=org 26 | uid: user1 27 | cn: user1 28 | sn: user1 29 | objectClass: top 30 | objectClass: person 31 | objectClass: organizationalPerson 32 | objectClass: inetOrgPerson 33 | userPassword: passworD1 34 | mail: user1@rubyldap.com 35 | 36 | dn: uid=user2,ou=People,dc=example,dc=org 37 | uid: user2 38 | cn: user2 39 | sn: user2 40 | objectClass: top 41 | objectClass: person 42 | objectClass: organizationalPerson 43 | objectClass: inetOrgPerson 44 | userPassword: passworD1 45 | mail: user2@rubyldap.com 46 | 47 | dn: uid=user3,ou=People,dc=example,dc=org 48 | uid: user3 49 | cn: user3 50 | sn: user3 51 | objectClass: top 52 | objectClass: person 53 | objectClass: organizationalPerson 54 | objectClass: inetOrgPerson 55 | userPassword: passworD1 56 | mail: user3@rubyldap.com 57 | 58 | dn: uid=user4,ou=People,dc=example,dc=org 59 | uid: user4 60 | cn: user4 61 | sn: user4 62 | objectClass: top 63 | objectClass: person 64 | objectClass: organizationalPerson 65 | objectClass: inetOrgPerson 66 | userPassword: passworD1 67 | mail: user4@rubyldap.com 68 | 69 | dn: uid=user5,ou=People,dc=example,dc=org 70 | uid: user5 71 | cn: user5 72 | sn: user5 73 | objectClass: top 74 | objectClass: person 75 | objectClass: organizationalPerson 76 | objectClass: inetOrgPerson 77 | userPassword: passworD1 78 | mail: user5@rubyldap.com 79 | 80 | dn: uid=user6,ou=People,dc=example,dc=org 81 | uid: user6 82 | cn: user6 83 | sn: user6 84 | objectClass: top 85 | objectClass: person 86 | objectClass: organizationalPerson 87 | objectClass: inetOrgPerson 88 | userPassword: passworD1 89 | mail: user6@rubyldap.com 90 | 91 | dn: uid=user7,ou=People,dc=example,dc=org 92 | uid: user7 93 | cn: user7 94 | sn: user7 95 | objectClass: top 96 | objectClass: person 97 | objectClass: organizationalPerson 98 | objectClass: inetOrgPerson 99 | userPassword: passworD1 100 | mail: user7@rubyldap.com 101 | 102 | dn: uid=user8,ou=People,dc=example,dc=org 103 | uid: user8 104 | cn: user8 105 | sn: user8 106 | objectClass: top 107 | objectClass: person 108 | objectClass: organizationalPerson 109 | objectClass: inetOrgPerson 110 | userPassword: passworD1 111 | mail: user8@rubyldap.com 112 | 113 | dn: uid=user9,ou=People,dc=example,dc=org 114 | uid: user9 115 | cn: user9 116 | sn: user9 117 | objectClass: top 118 | objectClass: person 119 | objectClass: organizationalPerson 120 | objectClass: inetOrgPerson 121 | userPassword: passworD1 122 | mail: user9@rubyldap.com 123 | 124 | dn: uid=user10,ou=People,dc=example,dc=org 125 | uid: user10 126 | cn: user10 127 | sn: user10 128 | objectClass: top 129 | objectClass: person 130 | objectClass: organizationalPerson 131 | objectClass: inetOrgPerson 132 | userPassword: passworD1 133 | mail: user10@rubyldap.com 134 | 135 | # Emailless User 136 | 137 | dn: uid=emailless-user1,ou=People,dc=example,dc=org 138 | uid: emailless-user1 139 | cn: emailless-user1 140 | sn: emailless-user1 141 | objectClass: top 142 | objectClass: person 143 | objectClass: organizationalPerson 144 | objectClass: inetOrgPerson 145 | userPassword: passworD1 146 | 147 | # Groupless User 148 | 149 | dn: uid=groupless-user1,ou=People,dc=example,dc=org 150 | uid: groupless-user1 151 | cn: groupless-user1 152 | sn: groupless-user1 153 | objectClass: top 154 | objectClass: person 155 | objectClass: organizationalPerson 156 | objectClass: inetOrgPerson 157 | userPassword: passworD1 158 | 159 | # Admin User 160 | 161 | dn: uid=admin1,ou=People,dc=example,dc=org 162 | uid: admin1 163 | cn: admin1 164 | sn: admin1 165 | objectClass: top 166 | objectClass: person 167 | objectClass: organizationalPerson 168 | objectClass: inetOrgPerson 169 | userPassword: passworD1 170 | mail: admin1@rubyldap.com 171 | 172 | # Groups 173 | 174 | dn: cn=ghe-users,ou=Groups,dc=example,dc=org 175 | cn: ghe-users 176 | objectClass: groupOfNames 177 | member: uid=user1,ou=People,dc=example,dc=org 178 | member: uid=emailless-user1,ou=People,dc=example,dc=org 179 | 180 | dn: cn=all-users,ou=Groups,dc=example,dc=org 181 | cn: all-users 182 | objectClass: groupOfNames 183 | member: cn=ghe-users,ou=Groups,dc=example,dc=org 184 | member: uid=user1,ou=People,dc=example,dc=org 185 | member: uid=user2,ou=People,dc=example,dc=org 186 | member: uid=user3,ou=People,dc=example,dc=org 187 | member: uid=user4,ou=People,dc=example,dc=org 188 | member: uid=user5,ou=People,dc=example,dc=org 189 | member: uid=user6,ou=People,dc=example,dc=org 190 | member: uid=user7,ou=People,dc=example,dc=org 191 | member: uid=user8,ou=People,dc=example,dc=org 192 | member: uid=user9,ou=People,dc=example,dc=org 193 | member: uid=user10,ou=People,dc=example,dc=org 194 | member: uid=emailless-user1,ou=People,dc=example,dc=org 195 | 196 | dn: cn=ghe-admins,ou=Groups,dc=example,dc=org 197 | cn: ghe-admins 198 | objectClass: groupOfNames 199 | member: uid=admin1,ou=People,dc=example,dc=org 200 | 201 | dn: cn=all-admins,ou=Groups,dc=example,dc=org 202 | cn: all-admins 203 | objectClass: groupOfNames 204 | member: cn=ghe-admins,ou=Groups,dc=example,dc=org 205 | member: uid=admin1,ou=People,dc=example,dc=org 206 | 207 | dn: cn=n-member-group10,ou=Groups,dc=example,dc=org 208 | cn: n-member-group10 209 | objectClass: groupOfNames 210 | member: uid=user1,ou=People,dc=example,dc=org 211 | member: uid=user2,ou=People,dc=example,dc=org 212 | member: uid=user3,ou=People,dc=example,dc=org 213 | member: uid=user4,ou=People,dc=example,dc=org 214 | member: uid=user5,ou=People,dc=example,dc=org 215 | member: uid=user6,ou=People,dc=example,dc=org 216 | member: uid=user7,ou=People,dc=example,dc=org 217 | member: uid=user8,ou=People,dc=example,dc=org 218 | member: uid=user9,ou=People,dc=example,dc=org 219 | member: uid=user10,ou=People,dc=example,dc=org 220 | 221 | dn: cn=nested-group1,ou=Groups,dc=example,dc=org 222 | cn: nested-group1 223 | objectClass: groupOfNames 224 | member: uid=user1,ou=People,dc=example,dc=org 225 | member: uid=user2,ou=People,dc=example,dc=org 226 | member: uid=user3,ou=People,dc=example,dc=org 227 | member: uid=user4,ou=People,dc=example,dc=org 228 | member: uid=user5,ou=People,dc=example,dc=org 229 | 230 | dn: cn=nested-group2,ou=Groups,dc=example,dc=org 231 | cn: nested-group2 232 | objectClass: groupOfNames 233 | member: uid=user6,ou=People,dc=example,dc=org 234 | member: uid=user7,ou=People,dc=example,dc=org 235 | member: uid=user8,ou=People,dc=example,dc=org 236 | member: uid=user9,ou=People,dc=example,dc=org 237 | member: uid=user10,ou=People,dc=example,dc=org 238 | 239 | dn: cn=nested-groups,ou=Groups,dc=example,dc=org 240 | cn: nested-groups 241 | objectClass: groupOfNames 242 | member: cn=nested-group1,ou=Groups,dc=example,dc=org 243 | member: cn=nested-group2,ou=Groups,dc=example,dc=org 244 | 245 | dn: cn=n-member-nested-group1,ou=Groups,dc=example,dc=org 246 | cn: n-member-nested-group1 247 | objectClass: groupOfNames 248 | member: cn=nested-group1,ou=Groups,dc=example,dc=org 249 | 250 | dn: cn=deeply-nested-group0.0.0,ou=Groups,dc=example,dc=org 251 | cn: deeply-nested-group0.0.0 252 | objectClass: groupOfNames 253 | member: uid=user1,ou=People,dc=example,dc=org 254 | member: uid=user2,ou=People,dc=example,dc=org 255 | member: uid=user3,ou=People,dc=example,dc=org 256 | member: uid=user4,ou=People,dc=example,dc=org 257 | member: uid=user5,ou=People,dc=example,dc=org 258 | 259 | dn: cn=deeply-nested-group0.0.1,ou=Groups,dc=example,dc=org 260 | cn: deeply-nested-group0.0.1 261 | objectClass: groupOfNames 262 | member: uid=user6,ou=People,dc=example,dc=org 263 | member: uid=user7,ou=People,dc=example,dc=org 264 | member: uid=user8,ou=People,dc=example,dc=org 265 | member: uid=user9,ou=People,dc=example,dc=org 266 | member: uid=user10,ou=People,dc=example,dc=org 267 | 268 | dn: cn=deeply-nested-group0.0,ou=Groups,dc=example,dc=org 269 | cn: deeply-nested-group0.0 270 | objectClass: groupOfNames 271 | member: cn=deeply-nested-group0.0.0,ou=Groups,dc=example,dc=org 272 | member: cn=deeply-nested-group0.0.1,ou=Groups,dc=example,dc=org 273 | 274 | dn: cn=deeply-nested-group0,ou=Groups,dc=example,dc=org 275 | cn: deeply-nested-group0 276 | objectClass: groupOfNames 277 | member: cn=deeply-nested-group0.0,ou=Groups,dc=example,dc=org 278 | 279 | dn: cn=deeply-nested-groups,ou=Groups,dc=example,dc=org 280 | cn: deeply-nested-groups 281 | objectClass: groupOfNames 282 | member: cn=deeply-nested-group0,ou=Groups,dc=example,dc=org 283 | 284 | dn: cn=n-depth-nested-group1,ou=Groups,dc=example,dc=org 285 | cn: n-depth-nested-group1 286 | objectClass: groupOfNames 287 | member: cn=nested-group1,ou=Groups,dc=example,dc=org 288 | 289 | dn: cn=n-depth-nested-group2,ou=Groups,dc=example,dc=org 290 | cn: n-depth-nested-group2 291 | objectClass: groupOfNames 292 | member: cn=n-depth-nested-group1,ou=Groups,dc=example,dc=org 293 | 294 | dn: cn=n-depth-nested-group3,ou=Groups,dc=example,dc=org 295 | cn: n-depth-nested-group3 296 | objectClass: groupOfNames 297 | member: cn=n-depth-nested-group2,ou=Groups,dc=example,dc=org 298 | 299 | dn: cn=n-depth-nested-group4,ou=Groups,dc=example,dc=org 300 | cn: n-depth-nested-group4 301 | objectClass: groupOfNames 302 | member: cn=n-depth-nested-group3,ou=Groups,dc=example,dc=org 303 | 304 | dn: cn=n-depth-nested-group5,ou=Groups,dc=example,dc=org 305 | cn: n-depth-nested-group5 306 | objectClass: groupOfNames 307 | member: cn=n-depth-nested-group4,ou=Groups,dc=example,dc=org 308 | 309 | dn: cn=n-depth-nested-group6,ou=Groups,dc=example,dc=org 310 | cn: n-depth-nested-group6 311 | objectClass: groupOfNames 312 | member: cn=n-depth-nested-group5,ou=Groups,dc=example,dc=org 313 | 314 | dn: cn=n-depth-nested-group7,ou=Groups,dc=example,dc=org 315 | cn: n-depth-nested-group7 316 | objectClass: groupOfNames 317 | member: cn=n-depth-nested-group6,ou=Groups,dc=example,dc=org 318 | 319 | dn: cn=n-depth-nested-group8,ou=Groups,dc=example,dc=org 320 | cn: n-depth-nested-group8 321 | objectClass: groupOfNames 322 | member: cn=n-depth-nested-group7,ou=Groups,dc=example,dc=org 323 | 324 | dn: cn=n-depth-nested-group9,ou=Groups,dc=example,dc=org 325 | cn: n-depth-nested-group9 326 | objectClass: groupOfNames 327 | member: cn=n-depth-nested-group8,ou=Groups,dc=example,dc=org 328 | 329 | dn: cn=head-group,ou=Groups,dc=example,dc=org 330 | cn: head-group 331 | objectClass: groupOfNames 332 | member: cn=tail-group,ou=Groups,dc=example,dc=org 333 | member: uid=user1,ou=People,dc=example,dc=org 334 | member: uid=user2,ou=People,dc=example,dc=org 335 | member: uid=user3,ou=People,dc=example,dc=org 336 | member: uid=user4,ou=People,dc=example,dc=org 337 | member: uid=user5,ou=People,dc=example,dc=org 338 | 339 | dn: cn=tail-group,ou=Groups,dc=example,dc=org 340 | cn: tail-group 341 | objectClass: groupOfNames 342 | member: cn=head-group,ou=Groups,dc=example,dc=org 343 | member: uid=user6,ou=People,dc=example,dc=org 344 | member: uid=user7,ou=People,dc=example,dc=org 345 | member: uid=user8,ou=People,dc=example,dc=org 346 | member: uid=user9,ou=People,dc=example,dc=org 347 | member: uid=user10,ou=People,dc=example,dc=org 348 | 349 | dn: cn=recursively-nested-groups,ou=Groups,dc=example,dc=org 350 | cn: recursively-nested-groups 351 | objectClass: groupOfNames 352 | member: cn=head-group,ou=Groups,dc=example,dc=org 353 | member: cn=tail-group,ou=Groups,dc=example,dc=org 354 | 355 | # posixGroup 356 | 357 | dn: cn=posix-group1,ou=Groups,dc=example,dc=org 358 | cn: posix-group1 359 | objectClass: posixGroup 360 | gidNumber: 1001 361 | memberUid: user1 362 | memberUid: user2 363 | memberUid: user3 364 | memberUid: user4 365 | memberUid: user5 366 | 367 | # missing members 368 | 369 | dn: cn=missing-users,ou=Groups,dc=example,dc=org 370 | cn: missing-users 371 | objectClass: groupOfNames 372 | member: uid=user1,ou=People,dc=example,dc=org 373 | member: uid=user2,ou=People,dc=example,dc=org 374 | member: uid=nonexistent-user,ou=People,dc=example,dc=org 375 | -------------------------------------------------------------------------------- /test/integration/test_add.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestAddIntegration < LDAPIntegrationTestCase 4 | def setup 5 | super 6 | @dn = "uid=added-user1,ou=People,dc=example,dc=org" 7 | end 8 | 9 | def test_add 10 | attrs = { 11 | objectclass: %w(top inetOrgPerson organizationalPerson person), 12 | uid: "added-user1", 13 | cn: "added-user1", 14 | sn: "added-user1", 15 | mail: "added-user1@rubyldap.com", 16 | } 17 | 18 | assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect 19 | 20 | assert result = @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject).first 21 | end 22 | 23 | def teardown 24 | @ldap.delete dn: @dn 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/integration/test_ber.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestBERIntegration < LDAPIntegrationTestCase 4 | # Test whether the TRUE boolean value is encoded correctly by performing a 5 | # search operation. 6 | def test_true_ber_encoding 7 | # request these attrs to simplify test; use symbols to match Entry#attribute_names 8 | attrs = [:dn, :uid, :cn, :mail] 9 | 10 | assert types_entry = @ldap.search( 11 | base: "dc=example,dc=org", 12 | filter: "(uid=user1)", 13 | size: 1, 14 | attributes: attrs, 15 | attributes_only: true, 16 | ).first 17 | 18 | # matches attributes we requested 19 | assert_equal attrs, types_entry.attribute_names 20 | 21 | # assert values are empty 22 | types_entry.each do |name, values| 23 | next if name == :dn 24 | assert values.empty? 25 | end 26 | 27 | assert_includes Net::LDAP::ResultCodesSearchSuccess, 28 | @ldap.get_operation_result.code, "should be a successful search operation" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/integration/test_bind.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestBindIntegration < LDAPIntegrationTestCase 4 | INTEGRATION_HOSTNAME = 'ldap.example.org'.freeze 5 | 6 | def test_bind_success 7 | assert @ldap.bind(BIND_CREDS), 8 | @ldap.get_operation_result.inspect 9 | end 10 | 11 | def test_bind_timeout 12 | @ldap.host = "10.255.255.1" # non-routable IP 13 | 14 | error = assert_raise Net::LDAP::Error do 15 | @ldap.bind BIND_CREDS 16 | end 17 | msgs = ['Operation timed out - user specified timeout', 18 | 'Connection timed out - user specified timeout'] 19 | assert_send([msgs, :include?, error.message]) 20 | end 21 | 22 | def test_bind_anonymous_fail 23 | refute @ldap.bind(BIND_CREDS.merge(password: '')), 24 | @ldap.get_operation_result.inspect 25 | 26 | result = @ldap.get_operation_result 27 | assert_equal Net::LDAP::ResultCodeUnwillingToPerform, result.code 28 | assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], result.message 29 | assert_equal "unauthenticated bind (DN with no password) disallowed", 30 | result.error_message 31 | assert_equal "", result.matched_dn 32 | end 33 | 34 | def test_bind_fail 35 | refute @ldap.bind(BIND_CREDS.merge(password: "not my password")), 36 | @ldap.get_operation_result.inspect 37 | end 38 | 39 | def test_bind_tls_with_cafile 40 | omit "We need to update our CA cert" 41 | @ldap.host = INTEGRATION_HOSTNAME 42 | @ldap.encryption( 43 | method: :start_tls, 44 | tls_options: TLS_OPTS.merge(ca_file: CA_FILE), 45 | ) 46 | assert @ldap.bind(BIND_CREDS), 47 | @ldap.get_operation_result.inspect 48 | end 49 | 50 | def test_bind_tls_with_bad_hostname_verify_none_no_ca_passes 51 | @ldap.host = INTEGRATION_HOSTNAME 52 | @ldap.encryption( 53 | method: :start_tls, 54 | tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE }, 55 | ) 56 | assert @ldap.bind(BIND_CREDS), 57 | @ldap.get_operation_result.inspect 58 | end 59 | 60 | def test_bind_tls_with_bad_hostname_verify_none_no_ca_opt_merge_passes 61 | @ldap.host = 'cert.mismatch.example.org' 62 | @ldap.encryption( 63 | method: :start_tls, 64 | tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_NONE), 65 | ) 66 | assert @ldap.bind(BIND_CREDS), 67 | @ldap.get_operation_result.inspect 68 | end 69 | 70 | def test_bind_tls_with_bad_hostname_verify_peer_ca_fails 71 | omit "We need to update our CA cert" 72 | @ldap.host = 'cert.mismatch.example.org' 73 | @ldap.encryption( 74 | method: :start_tls, 75 | tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER, 76 | ca_file: CA_FILE }, 77 | ) 78 | error = assert_raise Net::LDAP::Error, 79 | Errno::ECONNREFUSED do 80 | @ldap.bind BIND_CREDS 81 | end 82 | assert_equal( 83 | "hostname \"#{@ldap.host}\" does not match the server certificate", 84 | error.message, 85 | ) 86 | end 87 | 88 | def test_bind_tls_with_bad_hostname_ca_default_opt_merge_fails 89 | omit "We need to update our CA cert" 90 | @ldap.host = 'cert.mismatch.example.org' 91 | @ldap.encryption( 92 | method: :start_tls, 93 | tls_options: TLS_OPTS.merge(ca_file: CA_FILE), 94 | ) 95 | error = assert_raise Net::LDAP::Error, 96 | Errno::ECONNREFUSED do 97 | @ldap.bind BIND_CREDS 98 | end 99 | assert_equal( 100 | "hostname \"#{@ldap.host}\" does not match the server certificate", 101 | error.message, 102 | ) 103 | end 104 | 105 | def test_bind_tls_with_bad_hostname_ca_no_opt_merge_fails 106 | omit "We need to update our CA cert" 107 | @ldap.host = 'cert.mismatch.example.org' 108 | @ldap.encryption( 109 | method: :start_tls, 110 | tls_options: { ca_file: CA_FILE }, 111 | ) 112 | error = assert_raise Net::LDAP::Error, 113 | Errno::ECONNREFUSED do 114 | @ldap.bind BIND_CREDS 115 | end 116 | assert_equal( 117 | "hostname \"#{@ldap.host}\" does not match the server certificate", 118 | error.message, 119 | ) 120 | end 121 | 122 | def test_bind_tls_with_valid_hostname_default_opts_passes 123 | omit "We need to update our CA cert" 124 | @ldap.host = INTEGRATION_HOSTNAME 125 | @ldap.encryption( 126 | method: :start_tls, 127 | tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_PEER, 128 | ca_file: CA_FILE), 129 | ) 130 | assert @ldap.bind(BIND_CREDS), 131 | @ldap.get_operation_result.inspect 132 | end 133 | 134 | def test_bind_tls_with_valid_hostname_just_verify_peer_ca_passes 135 | omit "We need to update our CA cert" 136 | @ldap.host = INTEGRATION_HOSTNAME 137 | @ldap.encryption( 138 | method: :start_tls, 139 | tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER, 140 | ca_file: CA_FILE }, 141 | ) 142 | assert @ldap.bind(BIND_CREDS), 143 | @ldap.get_operation_result.inspect 144 | end 145 | 146 | def test_bind_tls_with_bogus_hostname_system_ca_fails 147 | @ldap.host = 'cert.mismatch.example.org' 148 | @ldap.encryption(method: :start_tls, tls_options: {}) 149 | error = assert_raise Net::LDAP::Error, 150 | Errno::ECONNREFUSED do 151 | @ldap.bind BIND_CREDS 152 | end 153 | assert_equal( 154 | "hostname \"#{@ldap.host}\" does not match the server certificate", 155 | error.message, 156 | ) 157 | end 158 | 159 | def test_bind_tls_with_multiple_hosts 160 | omit "We need to update our CA cert" 161 | @ldap.host = nil 162 | @ldap.hosts = [[INTEGRATION_HOSTNAME, 389], [INTEGRATION_HOSTNAME, 389]] 163 | @ldap.encryption( 164 | method: :start_tls, 165 | tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_PEER, 166 | ca_file: CA_FILE), 167 | ) 168 | assert @ldap.bind(BIND_CREDS), 169 | @ldap.get_operation_result.inspect 170 | end 171 | 172 | def test_bind_tls_with_multiple_bogus_hosts 173 | # omit "We need to update our CA cert" 174 | @ldap.host = nil 175 | @ldap.hosts = [['cert.mismatch.example.org', 389], ['bogus.example.com', 389]] 176 | @ldap.encryption( 177 | method: :start_tls, 178 | tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_PEER, 179 | ca_file: CA_FILE), 180 | ) 181 | error = assert_raise Net::LDAP::Error, 182 | Net::LDAP::ConnectionError do 183 | @ldap.bind BIND_CREDS 184 | end 185 | assert_equal("Unable to connect to any given server: ", 186 | error.message.split("\n").shift) 187 | end 188 | 189 | def test_bind_tls_with_multiple_bogus_hosts_no_verification 190 | omit "We need to update our CA cert" 191 | @ldap.host = nil 192 | @ldap.hosts = [['cert.mismatch.example.org', 389], ['bogus.example.com', 389]] 193 | @ldap.encryption( 194 | method: :start_tls, 195 | tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_NONE), 196 | ) 197 | assert @ldap.bind(BIND_CREDS), 198 | @ldap.get_operation_result.inspect 199 | end 200 | 201 | def test_bind_tls_with_multiple_bogus_hosts_ca_check_only_fails 202 | @ldap.host = nil 203 | @ldap.hosts = [['cert.mismatch.example.org', 389], ['bogus.example.com', 389]] 204 | @ldap.encryption( 205 | method: :start_tls, 206 | tls_options: { ca_file: CA_FILE }, 207 | ) 208 | error = assert_raise Net::LDAP::Error, 209 | Net::LDAP::ConnectionError do 210 | @ldap.bind BIND_CREDS 211 | end 212 | assert_equal("Unable to connect to any given server: ", 213 | error.message.split("\n").shift) 214 | end 215 | 216 | # This test is CI-only because we can't add the fixture CA 217 | # to the system CA store on people's dev boxes. 218 | def test_bind_tls_valid_hostname_system_ca_on_travis_passes 219 | omit "not sure how to install custom CA cert in travis" 220 | omit_unless ENV['TRAVIS'] == 'true' 221 | 222 | @ldap.host = INTEGRATION_HOSTNAME 223 | @ldap.encryption( 224 | method: :start_tls, 225 | tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER }, 226 | ) 227 | assert @ldap.bind(BIND_CREDS), 228 | @ldap.get_operation_result.inspect 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /test/integration/test_delete.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestDeleteIntegration < LDAPIntegrationTestCase 4 | def setup 5 | super 6 | @dn = "uid=delete-user1,ou=People,dc=example,dc=org" 7 | 8 | attrs = { 9 | objectclass: %w(top inetOrgPerson organizationalPerson person), 10 | uid: "delete-user1", 11 | cn: "delete-user1", 12 | sn: "delete-user1", 13 | mail: "delete-user1@rubyldap.com", 14 | } 15 | unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) 16 | assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect 17 | end 18 | assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) 19 | 20 | @parent_dn = "uid=parent,ou=People,dc=example,dc=org" 21 | parent_attrs = { 22 | objectclass: %w(top inetOrgPerson organizationalPerson person), 23 | uid: "parent", 24 | cn: "parent", 25 | sn: "parent", 26 | mail: "parent@rubyldap.com", 27 | } 28 | @child_dn = "uid=child,uid=parent,ou=People,dc=example,dc=org" 29 | child_attrs = { 30 | objectclass: %w(top inetOrgPerson organizationalPerson person), 31 | uid: "child", 32 | cn: "child", 33 | sn: "child", 34 | mail: "child@rubyldap.com", 35 | } 36 | unless @ldap.search(base: @parent_dn, scope: Net::LDAP::SearchScope_BaseObject) 37 | assert @ldap.add(dn: @parent_dn, attributes: parent_attrs), @ldap.get_operation_result.inspect 38 | assert @ldap.add(dn: @child_dn, attributes: child_attrs), @ldap.get_operation_result.inspect 39 | end 40 | assert @ldap.search(base: @parent_dn, scope: Net::LDAP::SearchScope_BaseObject) 41 | assert @ldap.search(base: @child_dn, scope: Net::LDAP::SearchScope_BaseObject) 42 | end 43 | 44 | def test_delete 45 | assert @ldap.delete(dn: @dn), @ldap.get_operation_result.inspect 46 | refute @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) 47 | 48 | result = @ldap.get_operation_result 49 | assert_equal Net::LDAP::ResultCodeNoSuchObject, result.code 50 | assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeNoSuchObject], result.message 51 | end 52 | 53 | def test_delete_tree 54 | assert @ldap.delete_tree(dn: @parent_dn), @ldap.get_operation_result.inspect 55 | refute @ldap.search(base: @parent_dn, scope: Net::LDAP::SearchScope_BaseObject) 56 | refute @ldap.search(base: @child_dn, scope: Net::LDAP::SearchScope_BaseObject) 57 | 58 | result = @ldap.get_operation_result 59 | assert_equal Net::LDAP::ResultCodeNoSuchObject, result.code 60 | assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeNoSuchObject], result.message 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/integration/test_open.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestBindIntegration < LDAPIntegrationTestCase 4 | def test_binds_without_open 5 | events = @service.subscribe "bind.net_ldap_connection" 6 | 7 | @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) 8 | @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) 9 | 10 | assert_equal 2, events.size 11 | end 12 | 13 | def test_binds_with_open 14 | events = @service.subscribe "bind.net_ldap_connection" 15 | 16 | @ldap.open do 17 | @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) 18 | @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) 19 | end 20 | 21 | assert_equal 1, events.size 22 | end 23 | 24 | # NOTE: query for two or more entries so that the socket must be read 25 | # multiple times. 26 | # See The Problem: https://github.com/ruby-ldap/ruby-net-ldap/issues/136 27 | 28 | def test_nested_search_without_open 29 | entries = [] 30 | nested_entry = nil 31 | 32 | @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=example,dc=org") do |entry| 33 | entries << entry.uid.first 34 | nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=example,dc=org").first 35 | end 36 | 37 | assert_equal "user3", nested_entry.uid.first 38 | assert_equal %w(user1 user2), entries 39 | end 40 | 41 | def test_nested_search_with_open 42 | entries = [] 43 | nested_entry = nil 44 | 45 | @ldap.open do 46 | @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=example,dc=org") do |entry| 47 | entries << entry.uid.first 48 | nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=example,dc=org").first 49 | end 50 | end 51 | 52 | assert_equal "user3", nested_entry.uid.first 53 | assert_equal %w(user1 user2), entries 54 | end 55 | 56 | def test_nested_add_with_open 57 | entries = [] 58 | nested_entry = nil 59 | 60 | dn = "uid=nested-open-added-user1,ou=People,dc=example,dc=org" 61 | attrs = { 62 | objectclass: %w(top inetOrgPerson organizationalPerson person), 63 | uid: "nested-open-added-user1", 64 | cn: "nested-open-added-user1", 65 | sn: "nested-open-added-user1", 66 | mail: "nested-open-added-user1@rubyldap.com", 67 | } 68 | 69 | @ldap.delete dn: dn 70 | 71 | @ldap.open do 72 | @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=example,dc=org") do |entry| 73 | entries << entry.uid.first 74 | 75 | nested_entry ||= begin 76 | assert @ldap.add(dn: dn, attributes: attrs), @ldap.get_operation_result.inspect 77 | @ldap.search(base: dn, scope: Net::LDAP::SearchScope_BaseObject).first 78 | end 79 | end 80 | end 81 | 82 | assert_equal %w(user1 user2), entries 83 | assert_equal "nested-open-added-user1", nested_entry.uid.first 84 | ensure 85 | @ldap.delete dn: dn 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/integration/test_password_modify.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestPasswordModifyIntegration < LDAPIntegrationTestCase 4 | # see: https://www.rfc-editor.org/rfc/rfc3062#section-2 5 | PASSWORD_MODIFY_SYNTAX = Net::BER.compile_syntax( 6 | application: {}, 7 | universal: {}, 8 | context_specific: { primitive: { 0 => :string } }, 9 | ) 10 | 11 | def setup 12 | super 13 | @admin_account = { dn: 'cn=admin,dc=example,dc=org', password: 'admin', method: :simple } 14 | @ldap.authenticate @admin_account[:dn], @admin_account[:password] 15 | 16 | @dn = 'uid=modify-password-user1,ou=People,dc=example,dc=org' 17 | 18 | attrs = { 19 | objectclass: %w(top inetOrgPerson organizationalPerson person), 20 | uid: 'modify-password-user1', 21 | cn: 'modify-password-user1', 22 | sn: 'modify-password-user1', 23 | mail: 'modify-password-user1@rubyldap.com', 24 | userPassword: 'admin', 25 | } 26 | unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) 27 | assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect 28 | end 29 | assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) 30 | 31 | @auth = { 32 | method: :simple, 33 | username: @dn, 34 | password: 'admin', 35 | } 36 | end 37 | 38 | def test_password_modify 39 | assert @ldap.password_modify(dn: @dn, 40 | auth: @auth, 41 | old_password: 'admin', 42 | new_password: 'passworD2') 43 | 44 | assert @ldap.get_operation_result.extended_response.nil?, 45 | 'Should not have generated a new password' 46 | 47 | refute @ldap.bind(username: @dn, password: 'admin', method: :simple), 48 | 'Old password should no longer be valid' 49 | 50 | assert @ldap.bind(username: @dn, password: 'passworD2', method: :simple), 51 | 'New password should be valid' 52 | end 53 | 54 | def test_password_modify_generate 55 | assert @ldap.password_modify(dn: @dn, 56 | auth: @auth, 57 | old_password: 'admin') 58 | 59 | passwd_modify_response_value = @ldap.get_operation_result.extended_response 60 | seq = Net::BER::BerIdentifiedArray.new 61 | sio = StringIO.new(passwd_modify_response_value) 62 | until (e = sio.read_ber(PASSWORD_MODIFY_SYNTAX)).nil? 63 | seq << e 64 | end 65 | generated_password = seq[0][0] 66 | 67 | assert generated_password, 'Should have generated a password' 68 | 69 | refute @ldap.bind(username: @dn, password: 'admin', method: :simple), 70 | 'Old password should no longer be valid' 71 | 72 | assert @ldap.bind(username: @dn, password: generated_password, method: :simple), 73 | 'New password should be valid' 74 | end 75 | 76 | def test_password_modify_generate_no_old_password 77 | assert @ldap.password_modify(dn: @dn, 78 | auth: @auth) 79 | 80 | passwd_modify_response_value = @ldap.get_operation_result.extended_response 81 | seq = Net::BER::BerIdentifiedArray.new 82 | sio = StringIO.new(passwd_modify_response_value) 83 | until (e = sio.read_ber(PASSWORD_MODIFY_SYNTAX)).nil? 84 | seq << e 85 | end 86 | generated_password = seq[0][0] 87 | assert generated_password, 'Should have generated a password' 88 | 89 | refute @ldap.bind(username: @dn, password: 'admin', method: :simple), 90 | 'Old password should no longer be valid' 91 | 92 | assert @ldap.bind(username: @dn, password: generated_password, method: :simple), 93 | 'New password should be valid' 94 | end 95 | 96 | def test_password_modify_overwrite_old_password 97 | assert @ldap.password_modify(dn: @dn, 98 | auth: @admin_account, 99 | new_password: 'passworD3') 100 | 101 | refute @ldap.bind(username: @dn, password: 'admin', method: :simple), 102 | 'Old password should no longer be valid' 103 | 104 | assert @ldap.bind(username: @dn, password: 'passworD3', method: :simple), 105 | 'New password should be valid' 106 | end 107 | 108 | def teardown 109 | @ldap.delete dn: @dn 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /test/integration/test_return_codes.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | # NOTE: These tests depend on the OpenLDAP retcode overlay. 4 | # See: section 12.12 http://www.openldap.org/doc/admin24/overlays.html 5 | 6 | class TestReturnCodeIntegration < LDAPIntegrationTestCase 7 | def test_open_error 8 | @ldap.authenticate "cn=fake", "creds" 9 | @ldap.open do 10 | result = @ldap.get_operation_result 11 | assert_equal Net::LDAP::ResultCodeInvalidCredentials, result.code 12 | end 13 | end 14 | 15 | def test_operations_error 16 | refute @ldap.search(filter: "cn=operationsError", base: "ou=Retcodes,dc=example,dc=org") 17 | assert result = @ldap.get_operation_result 18 | 19 | assert_equal Net::LDAP::ResultCodeOperationsError, result.code 20 | assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeOperationsError], result.message 21 | end 22 | 23 | def test_protocol_error 24 | refute @ldap.search(filter: "cn=protocolError", base: "ou=Retcodes,dc=example,dc=org") 25 | assert result = @ldap.get_operation_result 26 | 27 | assert_equal Net::LDAP::ResultCodeProtocolError, result.code 28 | assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeProtocolError], result.message 29 | end 30 | 31 | def test_time_limit_exceeded 32 | assert @ldap.search(filter: "cn=timeLimitExceeded", base: "ou=Retcodes,dc=example,dc=org") 33 | assert result = @ldap.get_operation_result 34 | 35 | assert_equal Net::LDAP::ResultCodeTimeLimitExceeded, result.code 36 | assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeTimeLimitExceeded], result.message 37 | end 38 | 39 | def test_size_limit_exceeded 40 | assert @ldap.search(filter: "cn=sizeLimitExceeded", base: "ou=Retcodes,dc=example,dc=org") 41 | assert result = @ldap.get_operation_result 42 | 43 | assert_equal Net::LDAP::ResultCodeSizeLimitExceeded, result.code 44 | assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeSizeLimitExceeded], result.message 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/integration/test_search.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class TestSearchIntegration < LDAPIntegrationTestCase 4 | def test_search 5 | entries = [] 6 | 7 | result = @ldap.search(base: "dc=example,dc=org") do |entry| 8 | assert_kind_of Net::LDAP::Entry, entry 9 | entries << entry 10 | end 11 | 12 | refute entries.empty? 13 | assert_equal entries, result 14 | end 15 | 16 | def test_search_without_result 17 | entries = [] 18 | 19 | result = @ldap.search(base: "dc=example,dc=org", return_result: false) do |entry| 20 | assert_kind_of Net::LDAP::Entry, entry 21 | entries << entry 22 | end 23 | 24 | assert result 25 | refute_equal entries, result 26 | end 27 | 28 | def test_search_filter_string 29 | entries = @ldap.search(base: "dc=example,dc=org", filter: "(uid=user1)") 30 | assert_equal 1, entries.size 31 | end 32 | 33 | def test_search_filter_object 34 | filter = Net::LDAP::Filter.eq("uid", "user1") | Net::LDAP::Filter.eq("uid", "user2") 35 | entries = @ldap.search(base: "dc=example,dc=org", filter: filter) 36 | assert_equal 2, entries.size 37 | end 38 | 39 | def test_search_constrained_attributes 40 | entry = @ldap.search(base: "uid=user1,ou=People,dc=example,dc=org", attributes: ["cn", "sn"]).first 41 | assert_equal [:cn, :dn, :sn], entry.attribute_names.sort # :dn is always included 42 | assert_empty entry[:mail] 43 | end 44 | 45 | def test_search_attributes_only 46 | entry = @ldap.search(base: "uid=user1,ou=People,dc=example,dc=org", attributes_only: true).first 47 | 48 | assert_empty entry[:cn], "unexpected attribute value: #{entry[:cn]}" 49 | end 50 | 51 | def test_search_timeout 52 | entries = [] 53 | events = @service.subscribe "search.net_ldap_connection" 54 | 55 | result = @ldap.search(base: "dc=example,dc=org", time: 5) do |entry| 56 | assert_kind_of Net::LDAP::Entry, entry 57 | entries << entry 58 | end 59 | 60 | payload, = events.pop 61 | assert_equal 5, payload[:time] 62 | assert_equal entries, result 63 | end 64 | 65 | # http://tools.ietf.org/html/rfc4511#section-4.5.1.4 66 | def test_search_with_size 67 | entries = [] 68 | 69 | result = @ldap.search(base: "dc=example,dc=org", size: 1) do |entry| 70 | assert_kind_of Net::LDAP::Entry, entry 71 | entries << entry 72 | end 73 | 74 | assert_equal 1, result.size 75 | assert_equal entries, result 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/support/vm/openldap/.gitignore: -------------------------------------------------------------------------------- 1 | /.vagrant 2 | -------------------------------------------------------------------------------- /test/test_auth_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestAuthAdapter < Test::Unit::TestCase 4 | class FakeSocket 5 | def initialize(*args) 6 | end 7 | end 8 | 9 | def test_undefined_auth_adapter 10 | conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379, :socket_class => FakeSocket) 11 | assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do 12 | conn.bind(method: :foo) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/test_dn.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | require_relative '../lib/net/ldap/dn' 3 | 4 | class TestDN < Test::Unit::TestCase 5 | def test_escape 6 | assert_equal '\\,\\+\\"\\\\\\<\\>\\;', Net::LDAP::DN.escape(',+"\\<>;') 7 | end 8 | 9 | def test_escape_pound_sign 10 | assert_equal '\\#test', Net::LDAP::DN.escape('#test') 11 | end 12 | 13 | def test_escape_space 14 | assert_equal '\\ before_after\\ ', Net::LDAP::DN.escape(' before_after ') 15 | end 16 | 17 | def test_retain_spaces 18 | dn = Net::LDAP::DN.new('CN=Foo.bar.baz, OU=Foo \ ,OU=\ Bar, O=Baz') 19 | assert_equal "CN=Foo.bar.baz, OU=Foo \\ ,OU=\\ Bar, O=Baz", dn.to_s 20 | assert_equal ["CN", "Foo.bar.baz", "OU", "Foo ", "OU", " Bar", "O", "Baz"], dn.to_a 21 | end 22 | 23 | def test_escape_on_initialize 24 | dn = Net::LDAP::DN.new('cn', ',+"\\<>;', 'ou=company') 25 | assert_equal 'cn=\\,\\+\\"\\\\\\<\\>\\;,ou=company', dn.to_s 26 | end 27 | 28 | def test_to_a 29 | dn = Net::LDAP::DN.new('cn=James, ou=Company\\,\\20LLC') 30 | assert_equal ['cn', 'James', 'ou', 'Company, LLC'], dn.to_a 31 | end 32 | 33 | def test_to_a_parenthesis 34 | dn = Net::LDAP::DN.new('cn = \ James , ou = "Comp\28ny" ') 35 | assert_equal ['cn', ' James ', 'ou', 'Comp(ny'], dn.to_a 36 | end 37 | 38 | def test_to_a_hash_symbol 39 | dn = Net::LDAP::DN.new('1.23.4= #A3B4D5 ,ou=Company') 40 | assert_equal ['1.23.4', '#A3B4D5', 'ou', 'Company'], dn.to_a 41 | end 42 | 43 | def test_bad_input_raises_error 44 | [ 45 | 'cn=James,', 46 | 'cn=#aa aa', 47 | 'cn="James', 48 | 'cn=J\ames', 49 | 'cn=\\', 50 | '1.2.d=Value', 51 | 'd1.2=Value', 52 | ].each do |input| 53 | dn = Net::LDAP::DN.new(input) 54 | assert_raises(Net::LDAP::InvalidDNError) { dn.to_a } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/test_entry.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class TestEntry < Test::Unit::TestCase 4 | def setup 5 | @entry = Net::LDAP::Entry.new 'cn=Barbara,o=corp' 6 | end 7 | 8 | def test_dn 9 | assert_equal 'cn=Barbara,o=corp', @entry.dn 10 | end 11 | 12 | def test_empty_array_when_accessing_nonexistent_attribute 13 | assert_equal [], @entry['sn'] 14 | end 15 | 16 | def test_attribute_assignment 17 | @entry['sn'] = 'Jensen' 18 | assert_equal ['Jensen'], @entry['sn'] 19 | assert_equal ['Jensen'], @entry.sn 20 | assert_equal ['Jensen'], @entry[:sn] 21 | 22 | @entry[:sn] = 'Jensen' 23 | assert_equal ['Jensen'], @entry['sn'] 24 | assert_equal ['Jensen'], @entry.sn 25 | assert_equal ['Jensen'], @entry[:sn] 26 | 27 | @entry.sn = 'Jensen' 28 | assert_equal ['Jensen'], @entry['sn'] 29 | assert_equal ['Jensen'], @entry.sn 30 | assert_equal ['Jensen'], @entry[:sn] 31 | end 32 | 33 | def test_case_insensitive_attribute_names 34 | @entry['sn'] = 'Jensen' 35 | assert_equal ['Jensen'], @entry.sn 36 | assert_equal ['Jensen'], @entry.Sn 37 | assert_equal ['Jensen'], @entry.SN 38 | assert_equal ['Jensen'], @entry['sn'] 39 | assert_equal ['Jensen'], @entry['Sn'] 40 | assert_equal ['Jensen'], @entry['SN'] 41 | end 42 | 43 | def test_to_h 44 | @entry['sn'] = 'Jensen' 45 | expected = { 46 | dn: ['cn=Barbara,o=corp'], 47 | sn: ['Jensen'], 48 | } 49 | duplicate = @entry.to_h 50 | assert_equal expected, duplicate 51 | 52 | # check that changing the duplicate 53 | # does not affect the internal state 54 | duplicate.delete(:sn) 55 | assert_not_equal duplicate, @entry.to_h 56 | end 57 | 58 | def test_equal_operator 59 | entry_two = Net::LDAP::Entry.new 'cn=Barbara,o=corp' 60 | assert_equal @entry, entry_two 61 | 62 | @entry['sn'] = 'Jensen' 63 | assert_not_equal @entry, entry_two 64 | 65 | entry_two['sn'] = 'Jensen' 66 | assert_equal @entry, entry_two 67 | end 68 | end 69 | 70 | class TestEntryLDIF < Test::Unit::TestCase 71 | def setup 72 | @entry = Net::LDAP::Entry.from_single_ldif_string( 73 | %Q{dn: something 74 | foo: foo 75 | barAttribute: bar 76 | }, 77 | ) 78 | end 79 | 80 | def test_attribute 81 | assert_equal ['foo'], @entry.foo 82 | assert_equal ['foo'], @entry.Foo 83 | end 84 | 85 | def test_modify_attribute 86 | @entry.foo = 'bar' 87 | assert_equal ['bar'], @entry.foo 88 | 89 | @entry.fOo = 'baz' 90 | assert_equal ['baz'], @entry.foo 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/test_filter.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class TestFilter < Test::Unit::TestCase 4 | Filter = Net::LDAP::Filter 5 | 6 | def test_bug_7534_rfc2254 7 | assert_equal("(cn=Tim Wizard)", 8 | Filter.from_rfc2254("(cn=Tim Wizard)").to_rfc2254) 9 | end 10 | 11 | def test_invalid_filter_string 12 | assert_raises(Net::LDAP::FilterSyntaxInvalidError) { Filter.from_rfc2254("") } 13 | end 14 | 15 | def test_invalid_filter 16 | assert_raises(Net::LDAP::OperatorError) do 17 | # This test exists to prove that our constructor blocks unknown filter 18 | # types. All filters must be constructed using helpers. 19 | Filter.__send__(:new, :xx, nil, nil) 20 | end 21 | end 22 | 23 | def test_to_s 24 | assert_equal("(uid=george *)", Filter.eq("uid", "george *").to_s) 25 | end 26 | 27 | def test_convenience_filters 28 | assert_equal("(uid=\\2A)", Filter.equals("uid", "*").to_s) 29 | assert_equal("(uid=\\28*)", Filter.begins("uid", "(").to_s) 30 | assert_equal("(uid=*\\29)", Filter.ends("uid", ")").to_s) 31 | assert_equal("(uid=*\\5C*)", Filter.contains("uid", "\\").to_s) 32 | end 33 | 34 | def test_c2 35 | assert_equal("(uid=george *)", 36 | Filter.from_rfc2254("uid=george *").to_rfc2254) 37 | assert_equal("(uid:=george *)", 38 | Filter.from_rfc2254("uid:=george *").to_rfc2254) 39 | assert_equal("(uid=george*)", 40 | Filter.from_rfc2254(" ( uid = george* ) ").to_rfc2254) 41 | assert_equal("(!(uid=george*))", 42 | Filter.from_rfc2254("uid!=george*").to_rfc2254) 43 | assert_equal("(uid<=george*)", 44 | Filter.from_rfc2254("uid <= george*").to_rfc2254) 45 | assert_equal("(uid>=george*)", 46 | Filter.from_rfc2254("uid>=george*").to_rfc2254) 47 | assert_equal("(&(uid=george*)(mail=*))", 48 | Filter.from_rfc2254("(& (uid=george* ) (mail=*))").to_rfc2254) 49 | assert_equal("(|(uid=george*)(mail=*))", 50 | Filter.from_rfc2254("(| (uid=george* ) (mail=*))").to_rfc2254) 51 | assert_equal("(!(mail=*))", 52 | Filter.from_rfc2254("(! (mail=*))").to_rfc2254) 53 | end 54 | 55 | def test_filter_with_single_clause 56 | assert_equal("(cn=name)", Net::LDAP::Filter.construct("(&(cn=name))").to_s) 57 | end 58 | 59 | def test_filters_from_ber 60 | [ 61 | Net::LDAP::Filter.eq("objectclass", "*"), 62 | Net::LDAP::Filter.pres("objectclass"), 63 | Net::LDAP::Filter.eq("objectclass", "ou"), 64 | Net::LDAP::Filter.ge("uid", "500"), 65 | Net::LDAP::Filter.le("uid", "500"), 66 | (~ Net::LDAP::Filter.pres("objectclass")), 67 | (Net::LDAP::Filter.pres("objectclass") & Net::LDAP::Filter.pres("ou")), 68 | (Net::LDAP::Filter.pres("objectclass") & Net::LDAP::Filter.pres("ou") & Net::LDAP::Filter.pres("sn")), 69 | (Net::LDAP::Filter.pres("objectclass") | Net::LDAP::Filter.pres("ou") | Net::LDAP::Filter.pres("sn")), 70 | 71 | Net::LDAP::Filter.eq("objectclass", "*aaa"), 72 | Net::LDAP::Filter.eq("objectclass", "*aaa*bbb"), 73 | Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*ccc"), 74 | Net::LDAP::Filter.eq("objectclass", "aaa*bbb"), 75 | Net::LDAP::Filter.eq("objectclass", "aaa*bbb*ccc"), 76 | Net::LDAP::Filter.eq("objectclass", "abc*def*1111*22*g"), 77 | Net::LDAP::Filter.eq("objectclass", "*aaa*"), 78 | Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*"), 79 | Net::LDAP::Filter.eq("objectclass", "*aaa*bbb*ccc*"), 80 | Net::LDAP::Filter.eq("objectclass", "aaa*"), 81 | Net::LDAP::Filter.eq("objectclass", "aaa*bbb*"), 82 | Net::LDAP::Filter.eq("objectclass", "aaa*bbb*ccc*"), 83 | ].each do |ber| 84 | f = Net::LDAP::Filter.parse_ber(ber.to_ber.read_ber(Net::LDAP::AsnSyntax)) 85 | assert(f == ber) 86 | assert_equal(f.to_ber, ber.to_ber) 87 | end 88 | end 89 | 90 | def test_ber_from_rfc2254_filter 91 | [ 92 | Net::LDAP::Filter.construct("objectclass=*"), 93 | Net::LDAP::Filter.construct("objectclass=ou"), 94 | Net::LDAP::Filter.construct("uid >= 500"), 95 | Net::LDAP::Filter.construct("uid <= 500"), 96 | Net::LDAP::Filter.construct("(!(uid=*))"), 97 | Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*))"), 98 | Net::LDAP::Filter.construct("(&(uid=*)(objectclass=*)(sn=*))"), 99 | Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*))"), 100 | Net::LDAP::Filter.construct("(|(uid=*)(objectclass=*)(sn=*))"), 101 | 102 | Net::LDAP::Filter.construct("objectclass=*aaa"), 103 | Net::LDAP::Filter.construct("objectclass=*aaa*bbb"), 104 | Net::LDAP::Filter.construct("objectclass=*aaa bbb"), 105 | Net::LDAP::Filter.construct("objectclass=*aaa bbb"), 106 | Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc"), 107 | Net::LDAP::Filter.construct("objectclass=aaa*bbb"), 108 | Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc"), 109 | Net::LDAP::Filter.construct("objectclass=abc*def*1111*22*g"), 110 | Net::LDAP::Filter.construct("objectclass=*aaa*"), 111 | Net::LDAP::Filter.construct("objectclass=*aaa*bbb*"), 112 | Net::LDAP::Filter.construct("objectclass=*aaa*bbb*ccc*"), 113 | Net::LDAP::Filter.construct("objectclass=aaa*"), 114 | Net::LDAP::Filter.construct("objectclass=aaa*bbb*"), 115 | Net::LDAP::Filter.construct("objectclass=aaa*bbb*ccc*"), 116 | ].each do |ber| 117 | f = Net::LDAP::Filter.parse_ber(ber.to_ber.read_ber(Net::LDAP::AsnSyntax)) 118 | assert(f == ber) 119 | assert_equal(f.to_ber, ber.to_ber) 120 | end 121 | end 122 | end 123 | 124 | # tests ported over from rspec. Not sure if these overlap with the above 125 | # https://github.com/ruby-ldap/ruby-net-ldap/pull/121 126 | class TestFilterRSpec < Test::Unit::TestCase 127 | def test_ex_convert 128 | assert_equal '(foo:=bar)', Net::LDAP::Filter.ex('foo', 'bar').to_s 129 | end 130 | 131 | def test_ex_rfc2254_roundtrip 132 | filter = Net::LDAP::Filter.ex('foo', 'bar') 133 | assert_equal filter, Net::LDAP::Filter.from_rfc2254(filter.to_s) 134 | end 135 | 136 | def test_ber_conversion 137 | filter = Net::LDAP::Filter.ex('foo', 'bar') 138 | ber = filter.to_ber 139 | assert_equal filter, Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) 140 | end 141 | 142 | [ 143 | '(o:dn:=Ace Industry)', 144 | '(:dn:2.4.8.10:=Dino)', 145 | '(cn:dn:1.2.3.4.5:=John Smith)', 146 | '(sn:dn:2.4.6.8.10:=Barbara Jones)', 147 | '(&(sn:dn:2.4.6.8.10:=Barbara Jones))', 148 | ].each_with_index do |filter_str, index| 149 | define_method "test_decode_filter_#{index}" do 150 | filter = Net::LDAP::Filter.from_rfc2254(filter_str) 151 | assert_kind_of Net::LDAP::Filter, filter 152 | end 153 | 154 | define_method "test_ber_conversion_#{index}" do 155 | filter = Net::LDAP::Filter.from_rfc2254(filter_str) 156 | ber = Net::LDAP::Filter.from_rfc2254(filter_str).to_ber 157 | assert_equal filter, Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) 158 | end 159 | end 160 | 161 | def test_apostrophes 162 | assert_equal "(uid=O'Keefe)", Net::LDAP::Filter.construct("uid=O'Keefe").to_rfc2254 163 | end 164 | 165 | def test_equals 166 | assert_equal Net::LDAP::Filter.eq('dn', 'f\2Aoo'), Net::LDAP::Filter.equals('dn', 'f*oo') 167 | end 168 | 169 | def test_begins 170 | assert_equal Net::LDAP::Filter.eq('dn', 'f\2Aoo*'), Net::LDAP::Filter.begins('dn', 'f*oo') 171 | end 172 | 173 | def test_ends 174 | assert_equal Net::LDAP::Filter.eq('dn', '*f\2Aoo'), Net::LDAP::Filter.ends('dn', 'f*oo') 175 | end 176 | 177 | def test_contains 178 | assert_equal Net::LDAP::Filter.eq('dn', '*f\2Aoo*'), Net::LDAP::Filter.contains('dn', 'f*oo') 179 | end 180 | 181 | def test_escape 182 | # escapes nul, *, (, ) and \\ 183 | assert_equal "\\00\\2A\\28\\29\\5C", Net::LDAP::Filter.escape("\0*()\\") 184 | end 185 | 186 | def test_well_known_ber_string 187 | ber = "\xa4\x2d" \ 188 | "\x04\x0b" "objectclass" \ 189 | "\x30\x1e" \ 190 | "\x80\x08" "foo" "*\\" "bar" \ 191 | "\x81\x08" "foo" "*\\" "bar" \ 192 | "\x82\x08" "foo" "*\\" "bar".b 193 | 194 | [ 195 | "foo" "\\2A\\5C" "bar", 196 | "foo" "\\2a\\5c" "bar", 197 | "foo" "\\2A\\5c" "bar", 198 | "foo" "\\2a\\5C" "bar", 199 | ].each do |escaped| 200 | # unescapes escaped characters 201 | filter = Net::LDAP::Filter.eq("objectclass", "#{escaped}*#{escaped}*#{escaped}") 202 | assert_equal ber, filter.to_ber 203 | end 204 | end 205 | 206 | def test_parse_ber_escapes_characters 207 | ber = "\xa4\x2d" \ 208 | "\x04\x0b" "objectclass" \ 209 | "\x30\x1e" \ 210 | "\x80\x08" "foo" "*\\" "bar" \ 211 | "\x81\x08" "foo" "*\\" "bar" \ 212 | "\x82\x08" "foo" "*\\" "bar".b 213 | 214 | escaped = Net::LDAP::Filter.escape("foo" "*\\" "bar") 215 | filter = Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) 216 | assert_equal "(objectclass=#{escaped}*#{escaped}*#{escaped})", filter.to_s 217 | end 218 | 219 | def test_unescape_fixnums 220 | filter = Net::LDAP::Filter.eq("objectclass", 3) 221 | assert_equal "\xA3\x10\x04\vobjectclass\x04\x013".b, filter.to_ber 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /test/test_filter_parser.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative 'test_helper' 4 | 5 | class TestFilterParser < Test::Unit::TestCase 6 | def test_ascii 7 | assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=name)") 8 | end 9 | 10 | def test_multibyte_characters 11 | assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=名前)") 12 | end 13 | 14 | def test_brackets 15 | assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=[{something}])") 16 | end 17 | 18 | def test_slash 19 | assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(departmentNumber=FOO//BAR/FOO)") 20 | end 21 | 22 | def test_colons 23 | assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(ismemberof=cn=edu:berkeley:app:calmessages:deans,ou=campus groups,dc=berkeley,dc=edu)") 24 | end 25 | 26 | def test_attr_tag 27 | assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(mail;primary=jane@example.org)") 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Add 'lib' to load path. 2 | require 'test/unit' 3 | require_relative '../lib/net/ldap' 4 | require 'flexmock/test_unit' 5 | 6 | # Whether integration tests should be run. 7 | INTEGRATION = ENV.fetch("INTEGRATION", "skip") != "skip" 8 | 9 | # The CA file to verify certs against for tests. 10 | # Override with CA_FILE env variable; otherwise checks for the VM-specific path 11 | # and falls back to the test/fixtures/cacert.pem for local testing. 12 | CA_FILE = 13 | ENV.fetch("CA_FILE") do 14 | if File.exist?("/etc/ssl/certs/cacert.pem") 15 | "/etc/ssl/certs/cacert.pem" 16 | else 17 | File.expand_path("fixtures/ca/docker-ca.pem", File.dirname(__FILE__)) 18 | end 19 | end 20 | 21 | BIND_CREDS = { 22 | method: :simple, 23 | username: "cn=admin,dc=example,dc=org", 24 | password: "admin", 25 | }.freeze 26 | 27 | TLS_OPTS = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.merge({}).freeze 28 | 29 | if RUBY_VERSION < "2.0" 30 | class String 31 | def b 32 | self 33 | end 34 | end 35 | end 36 | 37 | class MockInstrumentationService 38 | def initialize 39 | @events = {} 40 | end 41 | 42 | def instrument(event, payload) 43 | result = yield(payload) 44 | @events[event] ||= [] 45 | @events[event] << [payload, result] 46 | result 47 | end 48 | 49 | def subscribe(event) 50 | @events[event] ||= [] 51 | @events[event] 52 | end 53 | end 54 | 55 | class LDAPIntegrationTestCase < Test::Unit::TestCase 56 | # If integration tests aren't enabled, noop these tests. 57 | if !INTEGRATION 58 | def run(*) 59 | self 60 | end 61 | end 62 | 63 | def setup 64 | @service = MockInstrumentationService.new 65 | @ldap = Net::LDAP.new \ 66 | host: ENV.fetch('INTEGRATION_HOST', 'localhost'), 67 | port: ENV.fetch('INTEGRATION_PORT', 389), 68 | search_domains: %w(dc=example,dc=org), 69 | uid: 'uid', 70 | instrumentation_service: @service 71 | @ldap.authenticate "cn=admin,dc=example,dc=org", "admin" 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/test_ldap.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class TestLDAPInstrumentation < Test::Unit::TestCase 4 | # Fake Net::LDAP::Connection for testing 5 | class FakeConnection 6 | # It's difficult to instantiate Net::LDAP::PDU objects. Faking out what we 7 | # need here until that object is brought under test and has it's constructor 8 | # cleaned up. 9 | class Result < Struct.new(:success?, :result_code); end 10 | 11 | def initialize 12 | @bind_success = Result.new(true, Net::LDAP::ResultCodeSuccess) 13 | @search_success = Result.new(true, Net::LDAP::ResultCodeSizeLimitExceeded) 14 | end 15 | 16 | def bind(args = {}) 17 | @bind_success 18 | end 19 | 20 | def search(*args) 21 | yield @search_success if block_given? 22 | @search_success 23 | end 24 | end 25 | 26 | def setup 27 | @connection = flexmock(:connection, :close => true) 28 | flexmock(Net::LDAP::Connection).should_receive(:new).and_return(@connection) 29 | 30 | @service = MockInstrumentationService.new 31 | @subject = Net::LDAP.new \ 32 | :host => "test.mocked.com", :port => 636, 33 | :force_no_page => true, # so server capabilities are not queried 34 | :instrumentation_service => @service 35 | end 36 | 37 | def test_instrument_bind 38 | events = @service.subscribe "bind.net_ldap" 39 | 40 | fake_connection = FakeConnection.new 41 | @subject.connection = fake_connection 42 | bind_result = fake_connection.bind 43 | 44 | assert @subject.bind 45 | 46 | payload, result = events.pop 47 | assert result 48 | assert_equal bind_result, payload[:bind] 49 | end 50 | 51 | def test_instrument_search 52 | events = @service.subscribe "search.net_ldap" 53 | 54 | fake_connection = FakeConnection.new 55 | @subject.connection = fake_connection 56 | entry = fake_connection.search 57 | 58 | refute_nil @subject.search(:filter => "(uid=user1)") 59 | 60 | payload, result = events.pop 61 | assert_equal [entry], result 62 | assert_equal [entry], payload[:result] 63 | assert_equal "(uid=user1)", payload[:filter] 64 | end 65 | 66 | def test_instrument_search_with_size 67 | events = @service.subscribe "search.net_ldap" 68 | 69 | fake_connection = FakeConnection.new 70 | @subject.connection = fake_connection 71 | entry = fake_connection.search 72 | 73 | refute_nil @subject.search(:filter => "(uid=user1)", :size => 1) 74 | 75 | payload, result = events.pop 76 | assert_equal [entry], result 77 | assert_equal [entry], payload[:result] 78 | assert_equal "(uid=user1)", payload[:filter] 79 | assert_equal result.size, payload[:size] 80 | end 81 | 82 | def test_obscure_auth 83 | password = "opensesame" 84 | assert_include(@subject.inspect, "anonymous") 85 | @subject.auth "joe_user", password 86 | assert_not_include(@subject.inspect, password) 87 | end 88 | 89 | def test_encryption 90 | enc = @subject.encryption('start_tls') 91 | 92 | assert_equal enc[:method], :start_tls 93 | end 94 | 95 | def test_normalize_encryption_symbol 96 | enc = @subject.send(:normalize_encryption, :start_tls) 97 | assert_equal enc, :method => :start_tls, :tls_options => {} 98 | end 99 | 100 | def test_normalize_encryption_nil 101 | enc = @subject.send(:normalize_encryption, nil) 102 | assert_equal enc, nil 103 | end 104 | 105 | def test_normalize_encryption_string 106 | enc = @subject.send(:normalize_encryption, 'start_tls') 107 | assert_equal enc, :method => :start_tls, :tls_options => {} 108 | end 109 | 110 | def test_normalize_encryption_hash 111 | enc = @subject.send(:normalize_encryption, :method => :start_tls, :tls_options => { :foo => :bar }) 112 | assert_equal enc, :method => :start_tls, :tls_options => { :foo => :bar } 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/test_ldif.rb: -------------------------------------------------------------------------------- 1 | # $Id: testldif.rb 61 2006-04-18 20:55:55Z blackhedd $ 2 | 3 | require_relative 'test_helper' 4 | 5 | require 'digest/sha1' 6 | require 'base64' 7 | 8 | class TestLdif < Test::Unit::TestCase 9 | TestLdifFilename = "#{File.dirname(__FILE__)}/testdata.ldif" 10 | 11 | def test_empty_ldif 12 | ds = Net::LDAP::Dataset.read_ldif(StringIO.new) 13 | assert_equal(true, ds.empty?) 14 | end 15 | 16 | def test_ldif_with_version 17 | io = StringIO.new("version: 1") 18 | ds = Net::LDAP::Dataset.read_ldif(io) 19 | assert_equal "1", ds.version 20 | end 21 | 22 | def test_ldif_with_comments 23 | str = ["# Hello from LDIF-land", "# This is an unterminated comment"] 24 | io = StringIO.new(str[0] + "\r\n" + str[1]) 25 | ds = Net::LDAP::Dataset.read_ldif(io) 26 | assert_equal(str, ds.comments) 27 | end 28 | 29 | def test_ldif_with_password 30 | psw = "goldbricks" 31 | hashed_psw = "{SHA}" + Base64.encode64(Digest::SHA1.digest(psw)).chomp 32 | 33 | ldif_encoded = Base64.encode64(hashed_psw).chomp 34 | ds = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n")) 35 | recovered_psw = ds["Goldbrick"][:userpassword].shift 36 | assert_equal(hashed_psw, recovered_psw) 37 | end 38 | 39 | def test_ldif_with_continuation_lines 40 | ds = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n")) 41 | assert_equal(true, ds.key?("abcdefghijklmn")) 42 | end 43 | 44 | def test_ldif_with_continuation_lines_and_extra_whitespace 45 | ds1 = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n")) 46 | assert_equal(true, ds1.key?("abcdefg hijklmn")) 47 | ds2 = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: abcdefg\r\n hij klmn\r\n\r\n")) 48 | assert_equal(true, ds2.key?("abcdefghij klmn")) 49 | end 50 | 51 | def test_ldif_tab_is_not_continuation 52 | ds = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: key\r\n\tnotcontinued\r\n\r\n")) 53 | assert_equal(true, ds.key?("key")) 54 | end 55 | 56 | def test_ldif_with_base64_dn 57 | str = "dn:: Q049QmFzZTY0IGRuIHRlc3QsT1U9VGVzdCxPVT1Vbml0cyxEQz1leGFtcGxlLERDPWNvbQ==\r\n\r\n" 58 | ds = Net::LDAP::Dataset.read_ldif(StringIO.new(str)) 59 | assert_equal(true, ds.key?("CN=Base64 dn test,OU=Test,OU=Units,DC=example,DC=com")) 60 | end 61 | 62 | def test_ldif_with_base64_dn_and_continuation_lines 63 | str = "dn:: Q049QmFzZTY0IGRuIHRlc3Qgd2l0aCBjb250aW51YXRpb24gbGluZSxPVT1UZXN0LE9VPVVua\r\n XRzLERDPWV4YW1wbGUsREM9Y29t\r\n\r\n" 64 | ds = Net::LDAP::Dataset.read_ldif(StringIO.new(str)) 65 | assert_equal(true, ds.key?("CN=Base64 dn test with continuation line,OU=Test,OU=Units,DC=example,DC=com")) 66 | end 67 | 68 | # TODO, INADEQUATE. We need some more tests 69 | # to verify the content. 70 | def test_ldif 71 | File.open(TestLdifFilename, "r") do |f| 72 | ds = Net::LDAP::Dataset.read_ldif(f) 73 | assert_equal(13, ds.length) 74 | end 75 | end 76 | 77 | # Must test folded lines and base64-encoded lines as well as normal ones. 78 | def test_to_ldif 79 | data = File.open(TestLdifFilename, "rb", &:read) 80 | io = StringIO.new(data) 81 | 82 | # added .lines to turn to array because 1.9 doesn't have 83 | # .grep on basic strings 84 | entries = data.lines.grep(/^dn:\s*/) { $'.chomp } 85 | dn_entries = entries.dup 86 | 87 | ds = Net::LDAP::Dataset.read_ldif(io) do |type, value| 88 | case type 89 | when :dn 90 | assert_equal(dn_entries.first, value) 91 | dn_entries.shift 92 | end 93 | end 94 | assert_equal(entries.size, ds.size) 95 | assert_equal(entries.sort, ds.to_ldif.grep(/^dn:\s*/) { $'.chomp }) 96 | end 97 | 98 | def test_to_ldif_with_version 99 | ds = Net::LDAP::Dataset.new 100 | ds.version = "1" 101 | 102 | assert_equal "version: 1", ds.to_ldif_string.chomp 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/test_password.rb: -------------------------------------------------------------------------------- 1 | # $Id: testpsw.rb 72 2006-04-24 21:58:14Z blackhedd $ 2 | 3 | require_relative 'test_helper' 4 | 5 | class TestPassword < Test::Unit::TestCase 6 | def test_psw 7 | assert_equal("{MD5}xq8jwrcfibi0sZdZYNkSng==", Net::LDAP::Password.generate(:md5, "cashflow")) 8 | assert_equal("{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", Net::LDAP::Password.generate(:sha, "cashflow")) 9 | end 10 | 11 | def test_psw_with_ssha256_should_not_contain_linefeed 12 | flexmock(SecureRandom).should_receive(:random_bytes).and_return('\xE5\x8A\x99\xF8\xCB\x15GW\xE8\xEA\xAD\x0F\xBF\x95\xB0\xDC') 13 | assert_equal("{SSHA256}Cc7MXboTyUP5PnPAeJeCrgMy8+7Gus0sw7kBJuTrmf1ceEU1XHg4QVx4OTlceEY4XHhDQlx4MTVHV1x4RThceEVBXHhBRFx4MEZceEJGXHg5NVx4QjBceERD", Net::LDAP::Password.generate(:ssha256, "cashflow")) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/test_rename.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | # Commented out since it assumes you have a live LDAP server somewhere. This 4 | # will be migrated to the integration specs, as soon as they are ready. 5 | =begin 6 | class TestRename < Test::Unit::TestCase 7 | HOST= '10.10.10.71' 8 | PORT = 389 9 | BASE = "o=test" 10 | AUTH = { :method => :simple, :username => "cn=testadmin,#{BASE}", :password => 'password' } 11 | BASIC_USER = "cn=jsmith,ou=sales,#{BASE}" 12 | RENAMED_USER = "cn=jbrown,ou=sales,#{BASE}" 13 | MOVED_USER = "cn=jsmith,ou=marketing,#{BASE}" 14 | RENAMED_MOVED_USER = "cn=jjones,ou=marketing,#{BASE}" 15 | 16 | def setup 17 | # create the entries we're going to manipulate 18 | Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| 19 | if ldap.add(:dn => "ou=sales,#{BASE}", :attributes => { :ou => "sales", :objectclass => "organizationalUnit" }) 20 | puts "Add failed: #{ldap.get_operation_result.message} - code: #{ldap.get_operation_result.code}" 21 | end 22 | ldap.add(:dn => "ou=marketing,#{BASE}", :attributes => { :ou => "marketing", :objectclass => "organizationalUnit" }) 23 | ldap.add(:dn => BASIC_USER, :attributes => { :cn => "jsmith", :objectclass => "inetOrgPerson", :sn => "Smith" }) 24 | end 25 | end 26 | 27 | def test_rename_entry 28 | dn = nil 29 | Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| 30 | ldap.rename(:olddn => BASIC_USER, :newrdn => "cn=jbrown") 31 | 32 | ldap.search(:base => RENAMED_USER) do |entry| 33 | dn = entry.dn 34 | end 35 | end 36 | assert_equal(RENAMED_USER, dn) 37 | end 38 | 39 | def test_move_entry 40 | dn = nil 41 | Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| 42 | ldap.rename(:olddn => BASIC_USER, :newrdn => "cn=jsmith", :new_superior => "ou=marketing,#{BASE}") 43 | 44 | ldap.search(:base => MOVED_USER) do |entry| 45 | dn = entry.dn 46 | end 47 | end 48 | assert_equal(MOVED_USER, dn) 49 | end 50 | 51 | def test_move_and_rename_entry 52 | dn = nil 53 | Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| 54 | ldap.rename(:olddn => BASIC_USER, :newrdn => "cn=jjones", :new_superior => "ou=marketing,#{BASE}") 55 | 56 | ldap.search(:base => RENAMED_MOVED_USER) do |entry| 57 | dn = entry.dn 58 | end 59 | end 60 | assert_equal(RENAMED_MOVED_USER, dn) 61 | end 62 | 63 | def teardown 64 | # delete the entries 65 | # note: this doesn't always completely clear up on eDirectory as objects get locked while 66 | # the rename/move is being completed on the server and this prevents the delete from happening 67 | Net::LDAP::open(:host => HOST, :port => PORT, :auth => AUTH) do |ldap| 68 | ldap.delete(:dn => BASIC_USER) 69 | ldap.delete(:dn => RENAMED_USER) 70 | ldap.delete(:dn => MOVED_USER) 71 | ldap.delete(:dn => RENAMED_MOVED_USER) 72 | ldap.delete(:dn => "ou=sales,#{BASE}") 73 | ldap.delete(:dn => "ou=marketing,#{BASE}") 74 | end 75 | end 76 | end 77 | =end 78 | -------------------------------------------------------------------------------- /test/test_search.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require_relative 'test_helper' 3 | 4 | class TestSearch < Test::Unit::TestCase 5 | class FakeConnection 6 | def search(args) 7 | OpenStruct.new(:result_code => Net::LDAP::ResultCodeOperationsError, :message => "error", :success? => false) 8 | end 9 | end 10 | 11 | def setup 12 | @service = MockInstrumentationService.new 13 | @connection = Net::LDAP.new :instrumentation_service => @service 14 | @connection.instance_variable_set(:@open_connection, FakeConnection.new) 15 | end 16 | 17 | def test_true_result 18 | assert_nil @connection.search(:return_result => true) 19 | end 20 | 21 | def test_false_result 22 | refute @connection.search(:return_result => false) 23 | end 24 | 25 | def test_no_result 26 | assert_nil @connection.search 27 | end 28 | 29 | def test_instrumentation_publishes_event 30 | events = @service.subscribe "search.net_ldap" 31 | 32 | @connection.search(:filter => "test") 33 | 34 | payload, result = events.pop 35 | assert payload.key?(:result) 36 | assert payload.key?(:filter) 37 | assert_equal "test", payload[:filter] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/test_snmp.rb: -------------------------------------------------------------------------------- 1 | # $Id: testsnmp.rb 231 2006-12-21 15:09:29Z blackhedd $ 2 | 3 | require_relative 'test_helper' 4 | require_relative '../lib/net/snmp' 5 | 6 | class TestSnmp < Test::Unit::TestCase 7 | def self.raw_string(s) 8 | # Conveniently, String#b only needs to be called when it exists 9 | s.respond_to?(:b) ? s.b : s 10 | end 11 | 12 | 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") 13 | 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") 14 | 15 | 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") 16 | 17 | def test_invalid_packet 18 | data = "xxxx" 19 | assert_raise(Net::BER::BerError) do 20 | data.read_ber(Net::SNMP::AsnSyntax) 21 | end 22 | end 23 | 24 | # The method String#read_ber! added by Net::BER consumes a well-formed BER 25 | # object from the head of a string. If it doesn't find a complete, 26 | # well-formed BER object, it returns nil and leaves the string unchanged. 27 | # If it finds an object, it returns the object and removes it from the 28 | # head of the string. This is good for handling partially-received data 29 | # streams, such as from network connections. 30 | def _test_consume_string 31 | data = "xxx" 32 | assert_equal(nil, data.read_ber!) 33 | assert_equal("xxx", data) 34 | 35 | data = SnmpGetRequest + "!!!" 36 | ary = data.read_ber!(Net::SNMP::AsnSyntax) 37 | assert_equal("!!!", data) 38 | assert ary.is_a?(Array) 39 | assert ary.is_a?(Net::BER::BerIdentifiedArray) 40 | end 41 | 42 | def test_weird_packet 43 | assert_raise(Net::SnmpPdu::Error) do 44 | Net::SnmpPdu.parse("aaaaaaaaaaaaaa") 45 | end 46 | end 47 | 48 | def test_get_request 49 | data = SnmpGetRequest.dup 50 | pkt = data.read_ber(Net::SNMP::AsnSyntax) 51 | assert pkt.is_a?(Net::BER::BerIdentifiedArray) 52 | assert_equal(48, pkt.ber_identifier) # Constructed [0], signifies GetRequest 53 | 54 | pdu = Net::SnmpPdu.parse(pkt) 55 | assert_equal(:get_request, pdu.pdu_type) 56 | assert_equal(16170, pdu.request_id) # whatever was in the test data. 16170 is not magic. 57 | assert_equal([[[1, 3, 6, 1, 2, 1, 1, 1, 0], nil]], pdu.variables) 58 | 59 | assert_equal(pdu.to_ber_string, SnmpGetRequest) 60 | end 61 | 62 | def test_empty_pdu 63 | pdu = Net::SnmpPdu.new 64 | assert_raise(Net::SnmpPdu::Error) { pdu.to_ber_string } 65 | end 66 | 67 | def test_malformations 68 | pdu = Net::SnmpPdu.new 69 | pdu.version = 0 70 | pdu.version = 2 71 | assert_raise(Net::SnmpPdu::Error) { pdu.version = 100 } 72 | 73 | pdu.pdu_type = :get_request 74 | pdu.pdu_type = :get_next_request 75 | pdu.pdu_type = :get_response 76 | pdu.pdu_type = :set_request 77 | pdu.pdu_type = :trap 78 | assert_raise(Net::SnmpPdu::Error) { pdu.pdu_type = :something_else } 79 | end 80 | 81 | def test_make_response 82 | pdu = Net::SnmpPdu.new 83 | pdu.version = 0 84 | pdu.community = "public" 85 | pdu.pdu_type = :get_response 86 | pdu.request_id = 9999 87 | pdu.error_status = 0 88 | pdu.error_index = 0 89 | pdu.add_variable_binding [1, 3, 6, 1, 2, 1, 1, 1, 0], "test" 90 | 91 | assert_equal(SnmpGetResponse, pdu.to_ber_string) 92 | end 93 | 94 | def test_make_bad_response 95 | pdu = Net::SnmpPdu.new 96 | assert_raise(Net::SnmpPdu::Error) { pdu.to_ber_string } 97 | pdu.pdu_type = :get_response 98 | pdu.request_id = 999 99 | pdu.to_ber_string 100 | # Not specifying variables doesn't create an error. (Maybe it should?) 101 | end 102 | 103 | def test_snmp_integers 104 | c32 = Net::SNMP::Counter32.new(100) 105 | assert_equal("A\001d", c32.to_ber) 106 | g32 = Net::SNMP::Gauge32.new(100) 107 | assert_equal("B\001d", g32.to_ber) 108 | t32 = Net::SNMP::TimeTicks32.new(100) 109 | assert_equal("C\001d", t32.to_ber) 110 | end 111 | 112 | def test_community 113 | data = SnmpGetRequestXXX.dup 114 | ary = data.read_ber(Net::SNMP::AsnSyntax) 115 | pdu = Net::SnmpPdu.parse(ary) 116 | assert_equal("xxxxxx", pdu.community) 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /test/test_ssl_ber.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | require 'timeout' 3 | 4 | class TestSSLBER < Test::Unit::TestCase 5 | # Transmits str to @to and reads it back from @from. 6 | # 7 | def transmit(str) 8 | Timeout.timeout(1) do 9 | @to.write(str) 10 | @to.close 11 | 12 | @from.read 13 | end 14 | end 15 | 16 | def setup 17 | @from, @to = IO.pipe 18 | 19 | # The production code operates on sockets, which do need #connect called 20 | # on them to work. Pipes are more robust for this test, so we'll skip 21 | # the #connect call since it fails. 22 | # 23 | # TODO: Replace test with real socket 24 | # https://github.com/ruby-ldap/ruby-net-ldap/pull/121#discussion_r18746386 25 | flexmock(OpenSSL::SSL::SSLSocket) 26 | .new_instances.should_receive(:connect => nil) 27 | 28 | @to = Net::LDAP::Connection.wrap_with_ssl(@to) 29 | @from = Net::LDAP::Connection.wrap_with_ssl(@from) 30 | end 31 | 32 | def test_transmit_strings 33 | omit_if RUBY_PLATFORM == "java", "JRuby throws an error without a real socket" 34 | omit_if (RUBY_VERSION >= "3.1" || RUBY_ENGINE == "truffleruby"), "Ruby complains about connection not being open" 35 | 36 | assert_equal "foo", transmit("foo") 37 | end 38 | 39 | def test_transmit_ber_encoded_numbers 40 | omit_if RUBY_PLATFORM == "java", "JRuby throws an error without a real socket" 41 | omit_if (RUBY_VERSION >= "3.1" || RUBY_ENGINE == "truffleruby"), "Ruby complains about connection not being open" 42 | 43 | @to.write 1234.to_ber 44 | assert_equal 1234, @from.read_ber 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/testdata.ldif: -------------------------------------------------------------------------------- 1 | # $Id: testdata.ldif 50 2006-04-17 17:57:33Z blackhedd $ 2 | # 3 | # This is test-data for an LDAP server in LDIF format. 4 | # 5 | dn: dc=bayshorenetworks,dc=com 6 | objectClass: dcObject 7 | objectClass: organization 8 | o: Bayshore Networks LLC 9 | dc: bayshorenetworks 10 | 11 | dn: cn=Manager,dc=bayshorenetworks,dc=com 12 | objectClass: organizationalrole 13 | cn: Manager 14 | 15 | dn: ou=people,dc=bayshorenetworks,dc=com 16 | objectClass: organizationalunit 17 | ou: people 18 | 19 | dn: ou=privileges,dc=bayshorenetworks,dc=com 20 | objectClass: organizationalunit 21 | ou: privileges 22 | 23 | dn: ou=roles,dc=bayshorenetworks,dc=com 24 | objectClass: organizationalunit 25 | ou: roles 26 | 27 | dn: ou=office,dc=bayshorenetworks,dc=com 28 | objectClass: organizationalunit 29 | ou: office 30 | 31 | dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com 32 | cn: Bob Fosse 33 | mail: nogoodnik@steamheat.net 34 | sn: Fosse 35 | ou: people 36 | objectClass: top 37 | objectClass: inetorgperson 38 | objectClass: authorizedperson 39 | hasAccessRole: uniqueIdentifier=engineer,ou=roles 40 | hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles 41 | hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles 42 | hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles 43 | hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles 44 | hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles 45 | hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles 46 | hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles 47 | hasAccessRole: uniqueIdentifier=workorder_user,ou=roles 48 | hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles 49 | hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles 50 | hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles 51 | 52 | dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com 53 | cn: Gwen Verdon 54 | mail: elephant@steamheat.net 55 | sn: Verdon 56 | ou: people 57 | objectClass: top 58 | objectClass: inetorgperson 59 | objectClass: authorizedperson 60 | hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles 61 | hasAccessRole: uniqueIdentifier=engineer,ou=roles 62 | hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles 63 | hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles 64 | hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles 65 | 66 | dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com 67 | uniqueIdentifier: engineering 68 | ou: privileges 69 | objectClass: accessPrivilege 70 | 71 | dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com 72 | uniqueIdentifier: engineer 73 | ou: roles 74 | objectClass: accessRole 75 | hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges 76 | 77 | dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com 78 | uniqueIdentifier: ldapadmin 79 | ou: roles 80 | objectClass: accessRole 81 | 82 | dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com 83 | uniqueIdentifier: ldapsuperadmin 84 | ou: roles 85 | objectClass: accessRole 86 | 87 | dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com 88 | cn: Sid Sorokin 89 | mail: catperson@steamheat.net 90 | sn: Sorokin 91 | ou: people 92 | objectClass: top 93 | objectClass: inetorgperson 94 | objectClass: authorizedperson 95 | hasAccessRole: uniqueIdentifier=engineer,ou=roles 96 | hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles 97 | hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles 98 | hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles 99 | hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles 100 | hasAccessRole: uniqueIdentifier=workorder_user,ou=roles 101 | 102 | -------------------------------------------------------------------------------- /testserver/ldapserver.rb: -------------------------------------------------------------------------------- 1 | # $Id$ 2 | # 3 | # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. 4 | # Gmail account: garbagecat10. 5 | # 6 | # This is an LDAP server intended for unit testing of Net::LDAP. 7 | # It implements as much of the protocol as we have the stomach 8 | # to implement but serves static data. Use ldapsearch to test 9 | # this server! 10 | # 11 | # To make this easier to write, we use the Ruby/EventMachine 12 | # reactor library. 13 | # 14 | 15 | #------------------------------------------------ 16 | 17 | module LdapServer 18 | LdapServerAsnSyntaxTemplate = { 19 | :application => { 20 | :constructed => { 21 | 0 => :array, # LDAP BindRequest 22 | 3 => :array # LDAP SearchRequest 23 | }, 24 | :primitive => { 25 | 2 => :string, # ldapsearch sends this to unbind 26 | }, 27 | }, 28 | :context_specific => { 29 | :primitive => { 30 | 0 => :string, # simple auth (password) 31 | 7 => :string # present filter 32 | }, 33 | :constructed => { 34 | 3 => :array # equality filter 35 | }, 36 | }, 37 | } 38 | 39 | def post_init 40 | $logger.info "Accepted LDAP connection" 41 | @authenticated = false 42 | end 43 | 44 | def receive_data data 45 | @data ||= ""; @data << data 46 | while pdu = @data.read_ber!(LdapServerAsnSyntax) 47 | begin 48 | handle_ldap_pdu pdu 49 | rescue 50 | $logger.error "closing connection due to error #{$!}" 51 | close_connection 52 | end 53 | end 54 | end 55 | 56 | def handle_ldap_pdu pdu 57 | tag_id = pdu[1].ber_identifier 58 | case tag_id 59 | when 0x60 60 | handle_bind_request pdu 61 | when 0x63 62 | handle_search_request pdu 63 | when 0x42 64 | # bizarre thing, it's a null object (primitive application-2) 65 | # sent by ldapsearch to request an unbind (or a kiss-off, not sure which) 66 | close_connection_after_writing 67 | else 68 | $logger.error "received unknown packet-type #{tag_id}" 69 | close_connection_after_writing 70 | end 71 | end 72 | 73 | def handle_bind_request pdu 74 | # TODO, return a proper LDAP error instead of blowing up on version error 75 | if pdu[1][0] != 3 76 | send_ldap_response 1, pdu[0].to_i, 2, "", "We only support version 3" 77 | elsif pdu[1][1] != "cn=bigshot,dc=bayshorenetworks,dc=com" 78 | send_ldap_response 1, pdu[0].to_i, 48, "", "Who are you?" 79 | elsif pdu[1][2].ber_identifier != 0x80 80 | send_ldap_response 1, pdu[0].to_i, 7, "", "Keep it simple, man" 81 | elsif pdu[1][2] != "opensesame" 82 | send_ldap_response 1, pdu[0].to_i, 49, "", "Make my day" 83 | else 84 | @authenticated = true 85 | send_ldap_response 1, pdu[0].to_i, 0, pdu[1][1], "I'll take it" 86 | end 87 | end 88 | 89 | # -- 90 | # Search Response ::= 91 | # CHOICE { 92 | # entry [APPLICATION 4] SEQUENCE { 93 | # objectName LDAPDN, 94 | # attributes SEQUENCE OF SEQUENCE { 95 | # AttributeType, 96 | # SET OF AttributeValue 97 | # } 98 | # }, 99 | # resultCode [APPLICATION 5] LDAPResult 100 | # } 101 | def handle_search_request pdu 102 | unless @authenticated 103 | # NOTE, early exit. 104 | send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?" 105 | return 106 | end 107 | 108 | treebase = pdu[1][0] 109 | if treebase != "dc=bayshorenetworks,dc=com" 110 | send_ldap_response 5, pdu[0].to_i, 32, "", "unknown treebase" 111 | return 112 | end 113 | 114 | msgid = pdu[0].to_i.to_ber 115 | 116 | # pdu[1][7] is the list of requested attributes. 117 | # If it's an empty array, that means that *all* attributes were requested. 118 | requested_attrs = if pdu[1][7].length > 0 119 | pdu[1][7].map(&:downcase) 120 | else 121 | :all 122 | end 123 | 124 | filters = pdu[1][6] 125 | if filters.length == 0 126 | # NOTE, early exit. 127 | send_ldap_response 5, pdu[0].to_i, 53, "", "No filter specified" 128 | end 129 | 130 | # TODO, what if this returns nil? 131 | filter = Net::LDAP::Filter.parse_ldap_filter(filters) 132 | 133 | $ldif.each do |dn, entry| 134 | if filter.match(entry) 135 | attrs = [] 136 | entry.each do |k, v| 137 | if requested_attrs == :all || requested_attrs.include?(k.downcase) 138 | attrvals = v.map(&:to_ber).to_ber_set 139 | attrs << [k.to_ber, attrvals].to_ber_sequence 140 | end 141 | end 142 | 143 | appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) 144 | pkt = [msgid.to_ber, appseq].to_ber_sequence 145 | send_data pkt 146 | end 147 | end 148 | 149 | send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?" 150 | end 151 | 152 | def send_ldap_response pkt_tag, msgid, code, dn, text 153 | send_data([msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag)].to_ber) 154 | end 155 | end 156 | 157 | #------------------------------------------------ 158 | 159 | # Rather bogus, a global method, which reads a HARDCODED filename 160 | # parses out LDIF data. It will be used to serve LDAP queries out of this server. 161 | # 162 | def load_test_data 163 | ary = File.readlines("./testdata.ldif") 164 | hash = {} 165 | while (line = ary.shift) && line.chomp! 166 | if line =~ /^dn:[\s]*/i 167 | dn = $' 168 | hash[dn] = {} 169 | while (attr = ary.shift) && attr.chomp! && attr =~ /^([\w]+)[\s]*:[\s]*/ 170 | hash[dn][$1.downcase] ||= [] 171 | hash[dn][$1.downcase] << $' 172 | end 173 | end 174 | end 175 | hash 176 | end 177 | 178 | #------------------------------------------------ 179 | 180 | if __FILE__ == $0 181 | 182 | require 'rubygems' 183 | require 'eventmachine' 184 | 185 | require 'logger' 186 | $logger = Logger.new $stderr 187 | 188 | $logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP." 189 | $:.unshift "../lib" 190 | 191 | $ldif = load_test_data 192 | 193 | require 'net/ldap' 194 | LdapServerAsnSyntax = Net::BER.compile_syntax(LdapServerAsnSyntaxTemplate) 195 | EventMachine.run do 196 | $logger.info "starting LDAP server on 127.0.0.1 port 3890" 197 | EventMachine.start_server "127.0.0.1", 3890, LdapServer 198 | EventMachine.add_periodic_timer 60, proc { $logger.info "heartbeat" } 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /testserver/testdata.ldif: -------------------------------------------------------------------------------- 1 | # $Id$ 2 | # 3 | # This is test-data for an LDAP server in LDIF format. 4 | # 5 | dn: dc=bayshorenetworks,dc=com 6 | objectClass: dcObject 7 | objectClass: organization 8 | o: Bayshore Networks LLC 9 | dc: bayshorenetworks 10 | 11 | dn: cn=Manager,dc=bayshorenetworks,dc=com 12 | objectClass: organizationalrole 13 | cn: Manager 14 | 15 | dn: ou=people,dc=bayshorenetworks,dc=com 16 | objectClass: organizationalunit 17 | ou: people 18 | 19 | dn: ou=privileges,dc=bayshorenetworks,dc=com 20 | objectClass: organizationalunit 21 | ou: privileges 22 | 23 | dn: ou=roles,dc=bayshorenetworks,dc=com 24 | objectClass: organizationalunit 25 | ou: roles 26 | 27 | dn: ou=office,dc=bayshorenetworks,dc=com 28 | objectClass: organizationalunit 29 | ou: office 30 | 31 | dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com 32 | cn: Bob Fosse 33 | mail: nogoodnik@steamheat.net 34 | sn: Fosse 35 | ou: people 36 | objectClass: top 37 | objectClass: inetorgperson 38 | objectClass: authorizedperson 39 | hasAccessRole: uniqueIdentifier=engineer,ou=roles 40 | hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles 41 | hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles 42 | hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles 43 | hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles 44 | hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles 45 | hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles 46 | hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles 47 | hasAccessRole: uniqueIdentifier=workorder_user,ou=roles 48 | hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles 49 | hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles 50 | hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles 51 | 52 | dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com 53 | cn: Gwen Verdon 54 | mail: elephant@steamheat.net 55 | sn: Verdon 56 | ou: people 57 | objectClass: top 58 | objectClass: inetorgperson 59 | objectClass: authorizedperson 60 | hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles 61 | hasAccessRole: uniqueIdentifier=engineer,ou=roles 62 | hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles 63 | hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles 64 | hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles 65 | 66 | dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com 67 | uniqueIdentifier: engineering 68 | ou: privileges 69 | objectClass: accessPrivilege 70 | 71 | dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com 72 | uniqueIdentifier: engineer 73 | ou: roles 74 | objectClass: accessRole 75 | hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges 76 | 77 | dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com 78 | uniqueIdentifier: ldapadmin 79 | ou: roles 80 | objectClass: accessRole 81 | 82 | dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com 83 | uniqueIdentifier: ldapsuperadmin 84 | ou: roles 85 | objectClass: accessRole 86 | 87 | dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com 88 | cn: Sid Sorokin 89 | mail: catperson@steamheat.net 90 | sn: Sorokin 91 | ou: people 92 | objectClass: top 93 | objectClass: inetorgperson 94 | objectClass: authorizedperson 95 | hasAccessRole: uniqueIdentifier=engineer,ou=roles 96 | hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles 97 | hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles 98 | hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles 99 | hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles 100 | hasAccessRole: uniqueIdentifier=workorder_user,ou=roles 101 | 102 | --------------------------------------------------------------------------------