diff --git a/.autotest b/.autotest deleted file mode 100644 index f5f85be9..00000000 --- a/.autotest +++ /dev/null @@ -1,11 +0,0 @@ -require 'rubygems' -#require 'redgreen/autotest' -require 'autotest/timestamp' - -Autotest.add_hook :initialize do |autotest| - %w{.git .hg .DS_Store ._* tmp log doc}.each do |exception| - autotest.add_exception(exception) - end -end - -# vim: syntax=ruby diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..a1ce7996 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Test + +on: + pull_request: + push: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: + - "3.0" + - "3.1" + - "3.2" + - "3.3" + - "3.4" + - "jruby-9.4" + - "truffleruby" + steps: + - uses: actions/checkout@v4 + - name: Run tests with Ruby ${{ matrix.ruby }} + run: docker compose run ci-${{ matrix.ruby }} diff --git a/.gitignore b/.gitignore index 1959fc00..e7d58b8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ -spec/ldap.yml +*~ +*.swp .rvmrc -*.gemspec pkg/ -*.swp -html/ doc/ publish/ -coverage/ -coverage.info -.rake_tasks~ Gemfile.lock +.bundle +bin/ +.idea +*.gem diff --git a/.rspec b/.rspec deleted file mode 100644 index 7438fbe5..00000000 --- a/.rspec +++ /dev/null @@ -1,2 +0,0 @@ ---colour ---format documentation diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..b2f78bb0 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,20 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + Exclude: + - 'pkg/**/*' + +Layout/ExtraSpacing: + Enabled: false + +Lint/AssignmentInCondition: + Enabled: false + +Style/ParallelAssignment: + Enabled: false + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..50901661 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,960 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2025-05-31 20:03:27 UTC using RuboCop version 1.75.8. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_element, with_fixed_indentation +Layout/ArrayAlignment: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentOneStep, IndentationWidth. +# SupportedStyles: case, end +Layout/CaseIndentation: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 24 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLineAfterGuardClause: + Exclude: + - 'lib/net/ber.rb' + - 'lib/net/ber/core_ext/array.rb' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/auth_adapter.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/entry.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/snmp.rb' + - 'test/integration/test_ber.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLineAfterMagicComment: + Exclude: + - 'net-ldap.gemspec' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. +Layout/EmptyLineBetweenDefs: + Exclude: + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/error.rb' + - 'lib/net/snmp.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLines: + Exclude: + - 'lib/net/snmp.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowAliasSyntax, AllowedMethods. +# AllowedMethods: alias_method, public, protected, private +Layout/EmptyLinesAroundAttributeAccessor: + Exclude: + - 'lib/net/ber.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only +Layout/EmptyLinesAroundClassBody: + Exclude: + - 'lib/net/ldap.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLinesAroundExceptionHandlingKeywords: + Exclude: + - 'lib/net/ldap/connection.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleAlignWith, Severity. +# SupportedStylesAlignWith: keyword, variable, start_of_line +Layout/EndAlignment: + Exclude: + - 'testserver/ldapserver.rb' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_brackets +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_braces +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +# Offense count: 124 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'lib/net/ber.rb' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/auth_adapter/gss_spnego.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/filter.rb' + - 'test/ber/test_ber.rb' + - 'test/integration/test_add.rb' + - 'test/integration/test_bind.rb' + - 'test/integration/test_delete.rb' + - 'test/integration/test_open.rb' + - 'test/test_helper.rb' + - 'test/test_ldap_connection.rb' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Width, AllowedPatterns. +Layout/IndentationWidth: + Exclude: + - 'lib/net/ber.rb' + - 'lib/net/ldap/password.rb' + - 'lib/net/snmp.rb' + +# Offense count: 14 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment, AllowRBSInlineAnnotation, AllowSteepAnnotation. +Layout/LeadingCommentSpace: + Exclude: + - 'lib/net/ber/core_ext/array.rb' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/entry.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/ldap/pdu.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineMethodCallBraceLayout: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: space, no_space +Layout/SpaceAroundEqualsInParameterDefault: + Exclude: + - 'lib/net/ldap/connection.rb' + - 'lib/net/snmp.rb' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Layout/SpaceAroundKeyword: + Exclude: + - 'lib/net/ldap/entry.rb' + - 'lib/net/snmp.rb' + +# Offense count: 7 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals. +# SupportedStylesForExponentOperator: space, no_space +# SupportedStylesForRationalLiterals: space, no_space +Layout/SpaceAroundOperators: + Exclude: + - 'lib/net/ber/ber_parser.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/entry.rb' + - 'lib/net/ldap/filter.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. +# SupportedStyles: space, no_space +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideBlockBraces: + Exclude: + - 'lib/net/ldap/dataset.rb' + +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: space, compact, no_space +Layout/SpaceInsideParens: + Exclude: + - 'lib/net/ldap/entry.rb' + - 'lib/net/snmp.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect, AllowComments. +Lint/EmptyConditionalBody: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 1 +# Configuration parameters: AllowComments. +Lint/EmptyWhen: + Exclude: + - 'lib/net/ldap/pdu.rb' + +# Offense count: 30 +# This cop supports safe autocorrection (--autocorrect). +Lint/ImplicitStringConcatenation: + Exclude: + - 'test/test_filter.rb' + +# Offense count: 1 +Lint/NonLocalExitFromIterator: + Exclude: + - 'lib/net/ldap/connection.rb' + +# Offense count: 1 +Lint/RescueException: + Exclude: + - 'lib/net/ldap/pdu.rb' + +# Offense count: 10 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/snmp.rb' + +# Offense count: 7 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# NotImplementedExceptions: NotImplementedError +Lint/UnusedMethodArgument: + Exclude: + - 'lib/net/ldap/entry.rb' + - 'lib/net/ldap/pdu.rb' + - 'test/test_ldap.rb' + - 'test/test_ldap_connection.rb' + - 'test/test_search.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods. +Lint/UselessAccessModifier: + Exclude: + - 'lib/net/ldap/connection.rb' + +# Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect. +Lint/UselessAssignment: + Exclude: + - 'test/integration/test_add.rb' + - 'test/test_ldap_connection.rb' + - 'test/test_search.rb' + +# Offense count: 42 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 124 + +# Offense count: 3 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +# AllowedMethods: refine +Metrics/BlockLength: + Max: 119 + +# Offense count: 6 +# Configuration parameters: CountBlocks, CountModifierForms. +Metrics/BlockNesting: + Max: 4 + +# Offense count: 12 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 451 + +# Offense count: 21 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/CyclomaticComplexity: + Max: 45 + +# Offense count: 79 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 130 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ModuleLength: + Max: 103 + +# Offense count: 12 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/PerceivedComplexity: + Max: 46 + +# Offense count: 1 +Naming/AccessorMethodName: + Exclude: + - 'lib/net/ldap.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Naming/BinaryOperatorParameterName: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 1 +# Configuration parameters: AllowedNames. +# AllowedNames: module_parent +Naming/ClassAndModuleCamelCase: + Exclude: + - 'lib/net/ldap/auth_adapter/gss_spnego.rb' + +# Offense count: 88 +Naming/ConstantName: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/ldap/pdu.rb' + - 'lib/net/snmp.rb' + - 'test/test_ldif.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 1 +# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. +# CheckDefinitionPathHierarchyRoots: lib, spec, test, src +# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS +Naming/FileName: + Exclude: + - 'Rakefile.rb' + - 'lib/net-ldap.rb' + +# Offense count: 11 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to +Naming/MethodParameterName: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/entry.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/snmp.rb' + - 'test/test_snmp.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredName. +Naming/RescuedExceptionsVariableName: + Exclude: + - 'lib/net/ldap/pdu.rb' + +# Offense count: 9 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: separated, grouped +Style/AccessorGrouping: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/pdu.rb' + +# Offense count: 11 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: prefer_alias, prefer_alias_method +Style/Alias: + Exclude: + - 'lib/net/ber/core_ext/array.rb' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/entry.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/ldap/pdu.rb' + +# Offense count: 12 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, conditionals +Style/AndOr: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/ldap/pdu.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: percent_q, bare_percent +Style/BarePercentLiterals: + Exclude: + - 'test/test_entry.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/BlockComments: + Exclude: + - 'test/test_rename.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: MinBranchesCount. +Style/CaseLikeIf: + Exclude: + - 'lib/net/ber/ber_parser.rb' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Style/CharacterLiteral: + Exclude: + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/entry.rb' + +# Offense count: 23 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. +# SupportedStyles: nested, compact +# SupportedStylesForClasses: ~, nested, compact +# SupportedStylesForModules: ~, nested, compact +Style/ClassAndModuleChildren: + Enabled: false + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: is_a?, kind_of? +Style/ClassCheck: + Exclude: + - 'lib/net/ber/core_ext/array.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Keywords, RequireColon. +# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW, NOTE +Style/CommentAnnotation: + Exclude: + - 'lib/net/ber.rb' + +# Offense count: 8 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CommentedKeyword: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/entry.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/ldap/pdu.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. +# SupportedStyles: assign_to_condition, assign_inside_condition +Style/ConditionalAssignment: + Exclude: + - 'lib/net/ldap/dn.rb' + +# Offense count: 12 +# Configuration parameters: AllowedConstants. +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/auth_adapter.rb' + - 'lib/net/ldap/auth_adapter/sasl.rb' + - 'lib/net/ldap/auth_adapter/simple.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/error.rb' + - 'lib/net/ldap/instrumentation.rb' + - 'lib/net/ldap/password.rb' + - 'lib/net/ldap/pdu.rb' + - 'lib/net/snmp.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect, EnforcedStyle. +# SupportedStyles: compact, expanded +Style/EmptyMethod: + Exclude: + - 'test/test_auth_adapter.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Style/Encoding: + Exclude: + - 'net-ldap.gemspec' + - 'test/test_filter_parser.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Style/EvenOdd: + Exclude: + - 'lib/net/ldap/dn.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/ExpandPathArguments: + Exclude: + - 'net-ldap.gemspec' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Style/ExplicitBlockArgument: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/dataset.rb' + +# Offense count: 57 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: + Enabled: false + +# Offense count: 9 +# Configuration parameters: AllowedVariables. +Style/GlobalVars: + Exclude: + - 'testserver/ldapserver.rb' + +# Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. +Style/GuardClause: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 164 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. +# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys +# SupportedShorthandSyntax: always, never, either, consistent, either_consistent +Style/HashSyntax: + Exclude: + - 'lib/net/ber.rb' + - 'lib/net/ber/ber_parser.rb' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/auth_adapter/gss_spnego.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/pdu.rb' + - 'lib/net/snmp.rb' + - 'test/test_auth_adapter.rb' + - 'test/test_ldap.rb' + - 'test/test_ldap_connection.rb' + - 'test/test_search.rb' + - 'test/test_ssl_ber.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowIfModifier. +Style/IfInsideElse: + Exclude: + - 'lib/net/ldap/instrumentation.rb' + +# Offense count: 28 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'lib/net/ber.rb' + - 'lib/net/ber/core_ext/integer.rb' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/auth_adapter.rb' + - 'lib/net/ldap/auth_adapter/sasl.rb' + - 'lib/net/ldap/auth_adapter/simple.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/snmp.rb' + - 'test/integration/test_delete.rb' + - 'test/integration/test_password_modify.rb' + +# Offense count: 21 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline +Style/MethodDefParentheses: + Exclude: + - 'lib/net/ber.rb' + - 'lib/net/ldap/pdu.rb' + - 'lib/net/snmp.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 2 +Style/MissingRespondToMissing: + Exclude: + - 'lib/net/ldap/dn.rb' + - 'lib/net/ldap/entry.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Style/MultilineIfModifier: + Exclude: + - 'lib/net/ldap/connection.rb' + +# Offense count: 26 +# This cop supports safe autocorrection (--autocorrect). +Style/MultilineWhenThen: + Exclude: + - 'lib/net/ldap/dn.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMethodComparison, ComparisonsThreshold. +Style/MultipleComparison: + Exclude: + - 'lib/net/ldap/dataset.rb' + +# Offense count: 26 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'lib/net/ber.rb' + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/dn.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/ldap/version.rb' + - 'lib/net/snmp.rb' + - 'test/test_ldif.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: both, prefix, postfix +Style/NegatedIf: + Exclude: + - 'test/test_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/NegatedWhile: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals. +# SupportedStyles: skip_modifier_ifs, always +Style/Next: + Exclude: + - 'lib/net/ldap/connection.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: predicate, comparison +Style/NilComparison: + Exclude: + - 'lib/net/ldap/connection.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IncludeSemanticChanges. +Style/NonNilCheck: + Exclude: + - 'lib/net/ber/ber_parser.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/Not: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 13 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Strict, AllowedNumbers, AllowedPatterns. +Style/NumericLiterals: + MinDigits: 8 + +# Offense count: 14 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. +# SupportedStyles: predicate, comparison +Style/NumericPredicate: + Exclude: + - 'spec/**/*' + - 'lib/net/ber/core_ext/integer.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/dn.rb' + - 'lib/net/ldap/filter.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 1 +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'lib/net/ldap/entry.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. +Style/ParenthesesAroundCondition: + Exclude: + - 'lib/net/ldap/auth_adapter/sasl.rb' + +# Offense count: 13 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'net-ldap.gemspec' + - 'test/integration/test_add.rb' + - 'test/integration/test_delete.rb' + - 'test/integration/test_open.rb' + - 'test/integration/test_password_modify.rb' + - 'test/test_entry.rb' + - 'test/test_helper.rb' + +# Offense count: 20 +# This cop supports safe autocorrection (--autocorrect). +Style/PerlBackrefs: + Exclude: + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/filter.rb' + - 'test/test_ldif.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 10 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle, AllowedCompactTypes. +# SupportedStyles: compact, exploded +Style/RaiseArgs: + Exclude: + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/pdu.rb' + - 'lib/net/snmp.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantBegin: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/snmp.rb' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantParentheses: + Exclude: + - 'lib/net/ldap/filter.rb' + - 'test/test_filter.rb' + +# Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantPercentQ: + Exclude: + - 'net-ldap.gemspec' + - 'test/test_entry.rb' + +# Offense count: 11 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantRegexpCharacterClass: + Exclude: + - 'lib/net/ber/core_ext/integer.rb' + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/filter.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantRegexpEscape: + Exclude: + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/filter.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleReturnValues. +Style/RedundantReturn: + Exclude: + - 'lib/net/ber/core_ext/string.rb' + - 'lib/net/ldap/auth_adapter.rb' + - 'lib/net/ldap/entry.rb' + +# Offense count: 8 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantSelf: + Exclude: + - 'lib/net/ber/core_ext/array.rb' + - 'lib/net/ber/core_ext/string.rb' + - 'lib/net/ldap/dn.rb' + - 'lib/net/ldap/filter.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowInnerSlashes. +# SupportedStyles: slashes, percent_r, mixed +Style/RegexpLiteral: + Exclude: + - 'lib/net/ldap/filter.rb' + - 'net-ldap.gemspec' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/RescueModifier: + Exclude: + - 'test/ber/core_ext/test_string.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: implicit, explicit +Style/RescueStandardError: + Exclude: + - 'lib/net/snmp.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 13 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/dataset.rb' + - 'lib/net/ldap/pdu.rb' + +# Offense count: 7 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowAsExpressionSeparator. +Style/Semicolon: + Exclude: + - 'lib/net/ldap/dn.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowModifier. +Style/SoleNestedConditional: + Exclude: + - 'lib/net/ldap.rb' + - 'lib/net/ldap/connection.rb' + +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: RequireEnglish, EnforcedStyle. +# SupportedStyles: use_perl_names, use_english_names, use_builtin_english_names +Style/SpecialGlobalVars: + Exclude: + - 'lib/net/snmp.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 15 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'lib/net/ldap/dn.rb' + - 'lib/net/ldap/filter.rb' + - 'lib/net/ldap/password.rb' + - 'test/ber/test_ber.rb' + - 'test/test_ldif.rb' + - 'test/test_snmp.rb' + +# Offense count: 728 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Enabled: false + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/StructInheritance: + Exclude: + - 'test/test_ldap.rb' + +# Offense count: 11 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MinSize. +# SupportedStyles: percent, brackets +Style/SymbolArray: + EnforcedStyle: brackets + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex +Style/TernaryParentheses: + Exclude: + - 'lib/net/ber/core_ext/integer.rb' + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/dataset.rb' + +# Offense count: 38 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma +Style/TrailingCommaInHashLiteral: + Enabled: false + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. +# AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym +Style/TrivialAccessors: + Exclude: + - 'lib/net/ldap/connection.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/UnpackFirst: + Exclude: + - 'lib/net/ber/ber_parser.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/WhileUntilModifier: + Exclude: + - 'lib/net/ldap/filter.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + EnforcedStyle: percent + MinSize: 3 + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: forbid_for_all_comparison_operators, forbid_for_equality_operators_only, require_for_all_comparison_operators, require_for_equality_operators_only +Style/YodaCondition: + Exclude: + - 'lib/net/ber/ber_parser.rb' + +# Offense count: 6 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/ZeroLengthPredicate: + Exclude: + - 'lib/net/ldap/connection.rb' + - 'lib/net/ldap/filter.rb' + - 'testserver/ldapserver.rb' + +# Offense count: 27 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# URISchemes: http, https +Layout/LineLength: + Max: 360 diff --git a/.travis.yml b/.travis.yml index 9e097a28..8956efb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,60 @@ language: ruby rvm: - - 1.9.3 - 2.0.0 + - 2.1 + - 2.2 + - 2.3 + - 2.4 + - 2.5 + - 2.6 + - 2.7 + - jruby-9.2 + # optional + - ruby-head - jruby-19mode - - rbx-19mode -script: bundle exec rake spec + - jruby-9.2 + - jruby-head + +addons: + hosts: + - ldap.example.org # needed for TLS verification + - cert.mismatch.example.org + +services: + - docker + +env: + - INTEGRATION=openldap + +cache: bundler + +before_install: + - gem update bundler + +install: + - > + docker run \ + --hostname ldap.example.org \ + --env LDAP_TLS_VERIFY_CLIENT=try \ + -p 389:389 \ + -p 636:636 \ + -v "$(pwd)"/test/fixtures/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom \ + --name openldap \ + --detach \ + osixia/openldap:1.3.0 \ + --copy-service \ + --loglevel debug \ + - bundle install + +script: bundle exec rake ci + +matrix: + allow_failures: + - rvm: ruby-head + - rvm: jruby-19mode + - rvm: jruby-9.2 + - rvm: jruby-head + fast_finish: true + +notifications: + email: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ee5335b7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contribution guide + +Thank you for using net-ldap. If you'd like to help, keep these guidelines in +mind. + +## Submitting a New Issue + +If you find a bug, or would like to propose an idea, file a [new issue][issues]. +Include as many details as possible: + +- Version of net-ldap gem +- LDAP server version +- Queries, connection information, any other input +- output or error messages + +## Sending a Pull Request + +[Pull requests][pr] are always welcome! + +Check out [the project's issues list][issues] for ideas on what could be improved. + +Before sending, please add tests and ensure the test suite passes. + +To run the full suite: + + `bundle exec rake` + +To run a specific test file: + + `bundle exec ruby test/test_ldap.rb` + +To run a specific test: + + `bundle exec ruby test/test_ldap.rb -n test_instrument_bind` + +Pull requests will trigger automatic continuous integration builds on +[TravisCI][travis]. To run integration tests locally, see the `test/support` +folder. + +## Styleguide + +```ruby +# 1.9+ style hashes +{key: "value"} + +# Multi-line arguments with `\` +MyClass.new \ + foo: 'bar', + baz: 'garply' +``` + +[issues]: https://github.com/ruby-ldap/ruby-net-ldap/issues +[pr]: https://help.github.com/articles/using-pull-requests +[travis]: https://travis-ci.org/ruby-ldap/ruby-net-ldap diff --git a/Contributors.rdoc b/Contributors.rdoc index bef012a9..137394f8 100644 --- a/Contributors.rdoc +++ b/Contributors.rdoc @@ -20,3 +20,6 @@ Contributions since: * Erik Hetzner (egh) * nowhereman * David J. Lee (DavidJLee) +* Cody Cutrer (ccutrer) +* WoodsBagotAndreMarquesLee +* Rufus Post (mynameisrufus) diff --git a/Gemfile b/Gemfile index 851fabc2..10d2031f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,8 @@ source 'https://rubygems.org/' gemspec + +gem "debug", platform: :mri +gem "flexmock", "~> 1.3" +gem "rake", "~> 12.3.3" +gem "rubocop", "~> 1.48" +gem "test-unit" diff --git a/Hacking.rdoc b/Hacking.rdoc index 09452514..4bbf4ec5 100644 --- a/Hacking.rdoc +++ b/Hacking.rdoc @@ -25,7 +25,7 @@ patches being accepted, we recommend that you follow the guidelines below: == Documentation -* Documentation: {net-ldap}[http://net-ldap.rubyforge.org/] +* Documentation: {net-ldap}[http://rubydoc.info/gems/net-ldap] It is very important that, if you add new methods or objects, your code is well-documented. The purpose of the changes should be clearly described so that @@ -40,8 +40,8 @@ modification to +Contributors.rdoc+ to add yourself. == Tests -The Net::LDAP team uses RSpec for unit testing; all changes must have rspec -tests for any new or changed features. +The Net::LDAP team uses [Minitest](http://docs.seattlerb.org/minitest/) for unit +testing; all changes must have tests for any new or changed features. Your changes should have been tested against at least one real LDAP server; the current tests are not sufficient to find all possible bugs. It's unlikely that @@ -55,14 +55,9 @@ us with a sample LDIF data file for importing into LDAP servers for testing. Net::LDAP uses several libraries during development, all of which can be installed using RubyGems. -* *hoe* -* *hoe-git* -* *metaid* -* *rspec* * *flexmock* == Participation -* RubyForge: {net-ldap}[http://rubyforge.org/projects/net-ldap] * GitHub: {ruby-ldap/ruby-net-ldap}[https://github.com/ruby-ldap/ruby-net-ldap/] * Group: {ruby-ldap}[http://groups.google.com/group/ruby-ldap] diff --git a/History.rdoc b/History.rdoc index e0ee3e0a..919eaf67 100644 --- a/History.rdoc +++ b/History.rdoc @@ -1,3 +1,206 @@ +=== Net::LDAP 0.20.0 +* Update test.yml by @HarlemSquirrel in #433 +* Add `ostruct` as a dependency to the gemspec by @Ivanov-Anton in #432 +* Require Ruby >= 3.0 by @HarlemSquirrel in #435 +* Link to usage examples by @sebbASF in #428 +* Add controls for modify and add operations by @zeroSteiner in #426 +* Add support for ldapwhoami (RFC4532) (now with tests) by @zeroSteiner in #425 +* Update for ruby 3.4 by @HarlemSquirrel in #439 +* Add ruby 3.4 to CI by @hakeem0114 in #438 +* Add support for UTF-8 encoded passwords by @frankwalentowski in #430 + +=== Net::LDAP 0.19.0 +* Net::LDAP::DN - Retain trailing spaces in RDN values in DNs #412 +* Add in ability for users to specify LDAP controls when conducting searches #411 +* Document connect_timeout in Constructor Details #415 +* Fix openssl error when using multiple hosts #417 + +=== Net::LDAP 0.18.0 +* Fix escaping of # and space in attrs #408 +* Add support to use SNI #406 +* Drop Ruby 2.5 and JRuby 9.2 from CI tests +* Bump rubocop to 1.48.1 +* Update CI for TruffleRuby 22 + +=== Net::LDAP 0.17.1 +* Fixed shebang of bash #385 +* Omit some tests for now until we update our CA cert #386 +* Add Ruby 3.0 support #388 +* Add TruffleRuby 21.0.0 to CI #389 +* Correct a typo in an error message #391 +* Enable bundler caching for travis #390 +* Fix circular require while loading lib/net/ldap/entry.rb and lib/net/ldap/dataset.rb #392 +* Handle nil value in GetbyteForSSLSocket::getbyte #306 + +=== Net::LDAP 0.17.0 +* Added private recursive_delete as alternative to DELETE_TREE #268 +* Test suite updates #373 #376 #377 +* Use Base64.strict_encode64 and SSHA256 #303 +* Remove deprecated ConnectionRefusedError #366 +* Added method to get a duplicate of the internal Hash #286 +* remove a circular require #380 +* fix LdapServerAsnSyntax compile #379 +* Implement '==' operator for entries #381 +* fix for undefined method for write exception #383 + +=== Net::LDAP 0.16.3 + +* Add Net::LDAP::InvalidDNError #371 +* Use require_relative instead of require #360 +* Address some warnings and fix JRuby test omissions #365 +* Bump rake dev dependency to 12.3 #359 +* Enable rubocop in ci #251 +* Enhance rubocop configuration and test syntax #344 +* CI: Drop rbx-2, uninstallable #364 +* Fix RuboCop warnings #312 +* Fix wrong error class #305 +* CONTRIBUTING.md: Repair link to Issues #309 +* Make the generate() method more idiomatic... #326 +* Make encode_sort_controls() more idiomatic... #327 +* Make the instrument() method more idiomatic... #328 +* Fix uninitialised Net::LDAP::LdapPduError #338 +* README.rdoc: Use SVG build badge #310 +* Update TravisCI config to inclue Ruby 2.7 #346 +* add explicit ** to silence Ruby 2.7 warning #342 +* Support parsing filters with attribute tags #345 +* Bump rubocop development dependency version #336 +* Add link to generated and hosted documentation on rubydoc #319 +* Fix 'uninitialized constant Net::LDAP::PDU::LdapPduError' error #317 +* simplify encoding logic: no more chomping required #362 + +=== Net::LDAP 0.16.2 + +* Net::LDAP#open does not cache bind result {#334}[https://github.com/ruby-ldap/ruby-net-ldap/pull/334] +* Fix CI build {#333}[https://github.com/ruby-ldap/ruby-net-ldap/pull/333] +* Fix to "undefined method 'result_code'" {#308}[https://github.com/ruby-ldap/ruby-net-ldap/pull/308] +* Fixed Exception: incompatible character encodings: ASCII-8BIT and UTF-8 in filter.rb {#285}[https://github.com/ruby-ldap/ruby-net-ldap/pull/285] + +=== Net::LDAP 0.16.1 + +* Send DN and newPassword with password_modify request {#271}[https://github.com/ruby-ldap/ruby-net-ldap/pull/271] + +=== Net::LDAP 0.16.0 + +* Sasl fix {#281}[https://github.com/ruby-ldap/ruby-net-ldap/pull/281] +* enable TLS hostname validation {#279}[https://github.com/ruby-ldap/ruby-net-ldap/pull/279] +* update rubocop to 0.42.0 {#278}[https://github.com/ruby-ldap/ruby-net-ldap/pull/278] + +=== Net::LDAP 0.15.0 + +* Respect connect_timeout when establishing SSL connections {#273}[https://github.com/ruby-ldap/ruby-net-ldap/pull/273] + +=== Net::LDAP 0.14.0 + +* Normalize the encryption parameter passed to the LDAP constructor {#264}[https://github.com/ruby-ldap/ruby-net-ldap/pull/264] +* Update Docs: Net::LDAP now requires ruby >= 2 {#261}[https://github.com/ruby-ldap/ruby-net-ldap/pull/261] +* fix symbol proc {#255}[https://github.com/ruby-ldap/ruby-net-ldap/pull/255] +* fix trailing commas {#256}[https://github.com/ruby-ldap/ruby-net-ldap/pull/256] +* fix deprecated hash methods {#254}[https://github.com/ruby-ldap/ruby-net-ldap/pull/254] +* fix space after comma {#253}[https://github.com/ruby-ldap/ruby-net-ldap/pull/253] +* fix space inside brackets {#252}[https://github.com/ruby-ldap/ruby-net-ldap/pull/252] +* Rubocop style fixes {#249}[https://github.com/ruby-ldap/ruby-net-ldap/pull/249] +* Lazy initialize Net::LDAP::Connection's internal socket {#235}[https://github.com/ruby-ldap/ruby-net-ldap/pull/235] +* Support for rfc3062 Password Modify, closes #163 {#178}[https://github.com/ruby-ldap/ruby-net-ldap/pull/178] + +=== Net::LDAP 0.13.0 + +Avoid this release for because of an backwards incompatibility in how encryption +is initialized https://github.com/ruby-ldap/ruby-net-ldap/pull/264. We did not +yank it because people have already worked around it. + +* Set a connect_timeout for the creation of a socket {#243}[https://github.com/ruby-ldap/ruby-net-ldap/pull/243] +* Update bundler before installing gems with bundler {#245}[https://github.com/ruby-ldap/ruby-net-ldap/pull/245] +* Net::LDAP#encryption accepts string {#239}[https://github.com/ruby-ldap/ruby-net-ldap/pull/239] +* Adds correct UTF-8 encoding to Net::BER::BerIdentifiedString {#242}[https://github.com/ruby-ldap/ruby-net-ldap/pull/242] +* Remove 2.3.0-preview since ruby-head already is included {#241}[https://github.com/ruby-ldap/ruby-net-ldap/pull/241] +* Drop support for ruby 1.9.3 {#240}[https://github.com/ruby-ldap/ruby-net-ldap/pull/240] +* Fixed capitalization of StartTLSError {#234}[https://github.com/ruby-ldap/ruby-net-ldap/pull/234] + +=== Net::LDAP 0.12.1 + +* Whitespace formatting cleanup {#236}[https://github.com/ruby-ldap/ruby-net-ldap/pull/236] +* Set operation result if LDAP server is not accessible {#232}[https://github.com/ruby-ldap/ruby-net-ldap/pull/232] + +=== Net::LDAP 0.12.0 + +* DRY up connection handling logic {#224}[https://github.com/ruby-ldap/ruby-net-ldap/pull/224] +* Define auth adapters {#226}[https://github.com/ruby-ldap/ruby-net-ldap/pull/226] +* add slash to attribute value filter {#225}[https://github.com/ruby-ldap/ruby-net-ldap/pull/225] +* Add the ability to provide a list of hosts for a connection {#223}[https://github.com/ruby-ldap/ruby-net-ldap/pull/223] +* Specify the port of LDAP server by giving INTEGRATION_PORT {#221}[https://github.com/ruby-ldap/ruby-net-ldap/pull/221] +* Correctly set BerIdentifiedString values to UTF-8 {#212}[https://github.com/ruby-ldap/ruby-net-ldap/pull/212] +* Raise Net::LDAP::ConnectionRefusedError when new connection is refused. {#213}[https://github.com/ruby-ldap/ruby-net-ldap/pull/213] +* obscure auth password upon #inspect, added test, closes #216 {#217}[https://github.com/ruby-ldap/ruby-net-ldap/pull/217] +* Fixing incorrect error class name {#207}[https://github.com/ruby-ldap/ruby-net-ldap/pull/207] +* Travis update {#205}[https://github.com/ruby-ldap/ruby-net-ldap/pull/205] +* Remove obsolete rbx-19mode from Travis {#204}[https://github.com/ruby-ldap/ruby-net-ldap/pull/204] +* mv "sudo" from script/install-openldap to .travis.yml {#199}[https://github.com/ruby-ldap/ruby-net-ldap/pull/199] +* Remove meaningless shebang {#200}[https://github.com/ruby-ldap/ruby-net-ldap/pull/200] +* Fix Travis CI build {#202}[https://github.com/ruby-ldap/ruby-net-ldap/pull/202] +* README.rdoc: fix travis link {#195}[https://github.com/ruby-ldap/ruby-net-ldap/pull/195] + +=== Net::LDAP 0.11 +* Major enhancements: + * #183 Specific errors subclassing Net::LDAP::Error +* Bug fixes: + * #176 Fix nil tls options + * #184 Search guards against nil queued reads. Connection#unescape handles numerics +* Code clean-up: + * #180 Refactor connection establishment + +=== Net::LDAP 0.10.1 +* Bug fixes: + * Fix Integer BER encoding of signed values + +=== Net::LDAP 0.10.0 +* Major enhancements: + * Accept SimpleTLS/StartTLS encryption options (compatible with `OpenSSL::SSL::SSLContext#set_params`) +* Bug fixes: + * Parse filter strings with square and curly braces (`[]` and `{}`) + * Handle connection timeout errors (`Errno::ETIMEDOUT` raised as `Net::LDAP::LdapError`) +* Testing changes: + * Add integration tests for StartTLS connections to OpenLDAP +* Meta changes: + * Update Gem release tooling (remove Hoe, use Rake) + * Fix Gem release date + +=== Net::LDAP 0.9.0 +* Major changes: + * Dropped support for ruby 1.8.7, ruby >= 1.9.3 now required +* Major enhancements: + * Add support for search time limit parameter + * Instrument received messages, PDU parsing +* Minor enhancments: + * Add support for querying ActiveDirectory capabilities from root dse +* Bug fixes: + * Fix reads for multiple concurrent requests with shared, open connections mixing up the results + * Fix search size option + * Fix BER encoding bug +* Code clean-up: + * Added integration test suite + * Switch to minitest + +* Details + * #150 Support querying ActiveDirectory capabilities when searching root dse + * #142 Encode true as xFF + * #124, #145, #146, #152 Cleanup gemspec + * #138, #144 Track response messages by message id + * #141 Magic number/constant cleanup + * #119, #129, #130, #132, #133, #137 Integration tests + * #115 Search timeout support + * #140 Fix search size option + * #139 Cleanup and inline documentation for Net::LDAP::Connection#search + * #131 Instrumentation + * #116 Refactor Connection#write + * #126 Update gitignore + * #128 Fix whitespace + * #113, #121 Switch to minitest + * #123 Base64 encoded dn + * #114 Separate file for Net::LDAP::Connection + * #104 Parse version spec in LDIF datasets + * #106 ldap.modify doc fixes + * #111 Fix test deprecations + === Net::LDAP 0.5.0 / 2013-07-22 * Major changes: * Required Ruby version is >=1.9.3 diff --git a/Manifest.txt b/Manifest.txt deleted file mode 100644 index 86cbea5f..00000000 --- a/Manifest.txt +++ /dev/null @@ -1,55 +0,0 @@ -.autotest -.rspec -.travis.yml -Contributors.rdoc -Gemfile -Hacking.rdoc -History.rdoc -License.rdoc -Manifest.txt -README.rdoc -Rakefile -autotest/discover.rb -lib/net-ldap.rb -lib/net/ber.rb -lib/net/ber/ber_parser.rb -lib/net/ber/core_ext.rb -lib/net/ber/core_ext/array.rb -lib/net/ber/core_ext/bignum.rb -lib/net/ber/core_ext/false_class.rb -lib/net/ber/core_ext/fixnum.rb -lib/net/ber/core_ext/string.rb -lib/net/ber/core_ext/true_class.rb -lib/net/ldap.rb -lib/net/ldap/dataset.rb -lib/net/ldap/dn.rb -lib/net/ldap/entry.rb -lib/net/ldap/filter.rb -lib/net/ldap/password.rb -lib/net/ldap/pdu.rb -lib/net/ldap/version.rb -lib/net/snmp.rb -net-ldap.gemspec -spec/integration/ssl_ber_spec.rb -spec/spec.opts -spec/spec_helper.rb -spec/unit/ber/ber_spec.rb -spec/unit/ber/core_ext/array_spec.rb -spec/unit/ber/core_ext/string_spec.rb -spec/unit/ldap/dn_spec.rb -spec/unit/ldap/entry_spec.rb -spec/unit/ldap/filter_parser_spec.rb -spec/unit/ldap/filter_spec.rb -spec/unit/ldap/search_spec.rb -spec/unit/ldap_spec.rb -test/common.rb -test/test_entry.rb -test/test_filter.rb -test/test_ldap_connection.rb -test/test_ldif.rb -test/test_password.rb -test/test_rename.rb -test/test_snmp.rb -test/testdata.ldif -testserver/ldapserver.rb -testserver/testdata.ldif diff --git a/README.rdoc b/README.rdoc index 32c9f6e9..88bdba61 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,4 +1,6 @@ -= Net::LDAP for Ruby {}[https://travis-ci.org/ruby-ldap/ruby-net-ldap] += Net::LDAP for Ruby +{Gem Version}[https://badge.fury.io/rb/net-ldap] +{}[https://travis-ci.org/ruby-ldap/ruby-net-ldap] == Description @@ -16,21 +18,16 @@ the most recent LDAP RFCs (4510–4519, plus portions of 4520–4532). == Where -* {RubyForge}[http://rubyforge.org/projects/net-ldap] * {GitHub}[https://github.com/ruby-ldap/ruby-net-ldap] * {ruby-ldap@googlegroups.com}[http://groups.google.com/group/ruby-ldap] -* {Documentation}[http://net-ldap.rubyforge.org/] - -The Net::LDAP for Ruby documentation, project description, and main downloads -can currently be found on {RubyForge}[http://rubyforge.org/projects/net-ldap]. == Synopsis -See Net::LDAP for documentation and usage samples. +See {Net::LDAP on rubydoc.info}[https://www.rubydoc.info/github/ruby-ldap/ruby-net-ldap/Net/LDAP] for documentation and usage samples. == Requirements -Net::LDAP requires a Ruby 1.9.3 compatible interpreter or better. +Net::LDAP requires a Ruby 2.0.0 compatible interpreter or better. == Install @@ -42,6 +39,46 @@ sources. Simply require either 'net-ldap' or 'net/ldap'. +== Extensions + +This library focuses on the core LDAP RFCs referenced in the description. +However, we recognize there are commonly used extensions to the spec that are +useful. If there is another library which handles it, we list it here. + +* {resolv-srv}[https://rubygems.org/gems/resolv-srv]: Support RFC2782 SRV record lookup and failover + +== Develop + +This task will run the test suite and the +{RuboCop}[https://github.com/bbatsov/rubocop] static code analyzer. + + rake rubotest + +CI takes too long? If your local box supports +{Docker}[https://www.docker.com/], you can also run integration tests locally. +Simply run: + + script/ldap-docker + INTEGRATION=openldap rake test + +Or, use {Docker Compose}[https://docs.docker.com/compose/]. See docker-compose.yml for available Ruby versions. + + docker-compose run ci-2.7 + +CAVEAT: you need to add the following line to /etc/hosts + 127.0.0.1 ldap.example.org + 127.0.0.1 cert.mismatch.example.org + +== Release + +This section is for gem maintainers to cut a new version of the gem. + +* Check out a new branch `release-VERSION` +* Update lib/net/ldap/version.rb to next version number X.X.X following {semver}[http://semver.org/]. +* Update `History.rdoc`. Get latest changes with `script/changelog` +* Open a pull request with these changes for review +* After merging, on the master branch, run `script/release` + :include: Contributors.rdoc :include: License.rdoc diff --git a/Rakefile b/Rakefile index 159a8a09..da4cf8e7 100644 --- a/Rakefile +++ b/Rakefile @@ -1,76 +1,23 @@ # -*- ruby encoding: utf-8 -*- +# vim: syntax=ruby -require "rubygems" -require 'hoe' - -Hoe.plugin :doofus -Hoe.plugin :git -Hoe.plugin :gemspec - -Hoe.spec 'net-ldap' do |spec| - # spec.rubyforge_name = spec.name - - spec.developer("Francis Cianfrocca", "blackhedd@rubyforge.org") - spec.developer("Emiel van de Laar", "gemiel@gmail.com") - spec.developer("Rory O'Connell", "rory.ocon@gmail.com") - spec.developer("Kaspar Schiess", "kaspar.schiess@absurd.li") - spec.developer("Austin Ziegler", "austin@rubyforge.org") - spec.developer("Michael Schaarschmidt", "michael@schaaryworks.com") - - spec.remote_rdoc_dir = '' - spec.rsync_args << ' --exclude=statsvn/' - - spec.urls = %w(http://rubyldap.com/' 'https://github.com/ruby-ldap/ruby-net-ldap)-%20%20spec.licenses%20=%20['MIT'] - - spec.history_file = 'History.rdoc' - spec.readme_file = 'README.rdoc' - - spec.extra_rdoc_files = FileList["*.rdoc"].to_a - - spec.extra_dev_deps << [ "hoe-git", "~> 1" ] - spec.extra_dev_deps << [ "hoe-gemspec", "~> 1" ] - spec.extra_dev_deps << [ "metaid", "~> 1" ] - spec.extra_dev_deps << [ "flexmock", ">= 1.3.0" ] - spec.extra_dev_deps << [ "rspec", "~> 2.0" ] - - spec.clean_globs << "coverage" +require 'rake/testtask' +require 'rubocop/rake_task' +require 'bundler' - spec.spec_extras[:required_ruby_version] = ">= 1.8.7" - spec.multiruby_skip << "1.8.6" - spec.multiruby_skip << "1_8_6" +RuboCop::RakeTask.new - spec.need_tar = true +Rake::TestTask.new do |t| + t.libs << 'test' + t.test_files = FileList['test/**/test_*.rb'] + t.verbose = true + t.description = 'Run tests, set INTEGRATION=openldap to run integration tests, INTEGRATION_HOST and INTEGRATION_PORT are also supported' end -# I'm not quite ready to get rid of this, but I think "rake git:manifest" is -# sufficient. -namespace :old do - desc "Build the manifest file from the current set of files." - task :build_manifest do |t| - require 'find' +desc 'Run tests and RuboCop (RuboCop runs on mri only)' +task ci: Bundler.current_ruby.mri? ? [:test, :rubocop] : [:test] - paths = [] - Find.find(".") do |path| - next if File.directory?(path) - next if path =~ /\.svn/ - next if path =~ /\.git/ - next if path =~ /\.hoerc/ - next if path =~ /\.swp$/ - next if path =~ %r{coverage/} - next if path =~ /~$/ - paths << path.sub(%r{^\./}, '') - end +desc 'Run tests and RuboCop' +task rubotest: [:test, :rubocop] - File.open("Manifest.txt", "w") do |f| - f.puts paths.sort.join("\n") - end - - puts paths.sort.join("\n") - end -end - -desc "Run a full set of integration and unit tests" -task :cruise => [:test, :spec] - -# vim: syntax=ruby +task default: Bundler.current_ruby.mri? ? [:test, :rubocop] : [:test] diff --git a/autotest/discover.rb b/autotest/discover.rb deleted file mode 100644 index cd6892cc..00000000 --- a/autotest/discover.rb +++ /dev/null @@ -1 +0,0 @@ -Autotest.add_discovery { "rspec2" } diff --git a/ci-run.sh b/ci-run.sh new file mode 100755 index 00000000..cef309c0 --- /dev/null +++ b/ci-run.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +gem install bundler +ruby -v | grep jruby && apt update && apt install -y gcc +bundle check || bundle install +bundle exec rake ci diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4fbfbec8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,136 @@ +networks: + integration_test_network: + +services: + openldap: + image: osixia/openldap:1.4.0 + networks: + integration_test_network: + aliases: + - ldap.example.org + - cert.mismatch.example.org + environment: + LDAP_TLS_VERIFY_CLIENT: "try" + LDAP_SEED_INTERNAL_LDIF_PATH: "/ldif" + healthcheck: + test: ["CMD", "ldapsearch", "-x", "-s", "base"] + interval: 60s + start_period: 30s + timeout: 5s + retries: 1 + hostname: "ldap.example.org" + volumes: + - ./test/fixtures/ldif:/ldif:ro + + ci-3.0: + image: ruby:3.0 + command: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code + + ci-3.1: + image: ruby:3.1 + command: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code + + ci-3.2: + image: ruby:3.2 + command: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code + + ci-3.3: + image: ruby:3.3 + command: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code + + ci-3.4: + image: ruby:3.4 + entrypoint: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code + + # https://github.com/flavorjones/truffleruby/pkgs/container/truffleruby + ci-truffleruby: + image: ghcr.io/flavorjones/truffleruby:stable + command: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code + + ci-jruby-9.3: + image: jruby:9.3 + command: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code + + ci-jruby-9.4: + image: jruby:9.4 + command: /code/ci-run.sh + environment: + INTEGRATION: openldap + INTEGRATION_HOST: ldap.example.org + depends_on: + - openldap + networks: + integration_test_network: + volumes: + - .:/code + working_dir: /code diff --git a/lib/net-ldap.rb b/lib/net-ldap.rb index 879851eb..717878ca 100644 --- a/lib/net-ldap.rb +++ b/lib/net-ldap.rb @@ -1,2 +1,2 @@ # -*- ruby encoding: utf-8 -*- -require 'net/ldap' +require_relative 'net/ldap' diff --git a/lib/net/ber.rb b/lib/net/ber.rb index b8992a92..34696cc3 100644 --- a/lib/net/ber.rb +++ b/lib/net/ber.rb @@ -1,5 +1,5 @@ # -*- ruby encoding: utf-8 -*- -require 'net/ldap/version' +require_relative 'ldap/version' module Net # :nodoc: ## @@ -106,6 +106,7 @@ module Net # :nodoc: # CHARACTER STRINGC29: 61 (0x3d, 0b00111101) # BMPStringP30: 30 (0x1e, 0b00011110) # BMPStringC30: 62 (0x3e, 0b00111110) + # ExtendedResponseC107: 139 (0x8b, 0b010001011) # module BER VERSION = Net::LDAP::VERSION @@ -234,7 +235,7 @@ def self.compile_syntax(syntax) # TODO 20100327 AZ: Should we be allocating an array of 256 values # that will either be +nil+ or an object type symbol, or should we # allocate an empty Hash since unknown values return +nil+ anyway? - out = [ nil ] * 256 + out = [nil] * 256 syntax.each do |tag_class_id, encodings| tag_class = TAG_CLASS[tag_class_id] encodings.each do |encoding_id, classes| @@ -269,7 +270,7 @@ class Net::BER::BerIdentifiedOid def initialize(oid) if oid.is_a?(String) - oid = oid.split(/\./).map {|s| s.to_i } + oid = oid.split(/\./).map(&:to_i) end @value = oid end @@ -293,12 +294,43 @@ def to_arr ## # A String object with a BER identifier attached. +# class Net::BER::BerIdentifiedString < String attr_accessor :ber_identifier + + # The binary data provided when parsing the result of the LDAP search + # has the encoding 'ASCII-8BIT' (which is basically 'BINARY', or 'unknown'). + # + # This is the kind of a backtrace showing how the binary `data` comes to + # BerIdentifiedString.new(data): + # + # @conn.read_ber(syntax) + # -> StringIO.new(self).read_ber(syntax), i.e. included from module + # -> Net::BER::BERParser.read_ber(syntax) + # -> (private)Net::BER::BERParser.parse_ber_object(syntax, id, data) + # + # In the `#parse_ber_object` method `data`, according to its OID, is being + # 'casted' to one of the Net::BER:BerIdentifiedXXX classes. + # + # As we are using LDAP v3 we can safely assume that the data is encoded + # in UTF-8 and therefore the only thing to be done when instantiating is to + # switch the encoding from 'ASCII-8BIT' to 'UTF-8'. + # + # Unfortunately, there are some ActiveDirectory specific attributes + # (like `objectguid`) that should remain binary (do they really?). + # Using the `#valid_encoding?` we can trap this cases. Special cases like + # Japanese, Korean, etc. encodings might also profit from this. However + # I have no clue how this encodings function. def initialize args - super args - # LDAP uses UTF-8 encoded strings - self.encode('UTF-8') if self.respond_to?(:encoding) rescue self + super + # + # Check the encoding of the newly created String and set the encoding + # to 'UTF-8' (NOTE: we do NOT change the bytes, but only set the + # encoding to 'UTF-8'). + return unless encoding == Encoding::BINARY + current_encoding = encoding + force_encoding('UTF-8') + force_encoding(current_encoding) unless valid_encoding? end end @@ -317,4 +349,4 @@ def to_ber Null = Net::BER::BerIdentifiedNull.new end -require 'net/ber/core_ext' +require_relative 'ber/core_ext' diff --git a/lib/net/ber/ber_parser.rb b/lib/net/ber/ber_parser.rb index 682a5999..39d3737e 100644 --- a/lib/net/ber/ber_parser.rb +++ b/lib/net/ber/ber_parser.rb @@ -14,7 +14,7 @@ module Net::BER::BERParser } constructed = { 16 => :array, - 17 => :array + 17 => :array, } universal = { :primitive => primitive, :constructed => constructed } @@ -41,9 +41,18 @@ def parse_ber_object(syntax, id, data) s.ber_identifier = id s elsif object_type == :integer - j = 0 - data.each_byte { |b| j = (j << 8) + b } - j + neg = !(data.unpack("C").first & 0x80).zero? + int = 0 + + data.each_byte do |b| + int = (int << 8) + (neg ? 255 - b : b) + end + + if neg + (int + 1) * -1 + else + int + end elsif object_type == :oid # See X.690 pgh 8.19 for an explanation of this algorithm. # This is potentially not good enough. We may need a @@ -148,6 +157,9 @@ def read_ber_length # implemented on the including object and that it returns a Fixnum value. # Also requires #read(bytes) to work. # + # Yields the object type `id` and the data `content_length` if a block is + # given. This is namely to support instrumentation. + # # This does not work with non-blocking I/O. def read_ber(syntax = nil) # TODO: clean this up so it works properly with partial packets coming @@ -157,11 +169,13 @@ def read_ber(syntax = nil) id = getbyte or return nil # don't trash this value, we'll use it later content_length = read_ber_length + yield id, content_length if block_given? + if -1 == content_length - raise Net::BER::BerError, "Indeterminite BER content length not implemented." - else - data = read(content_length) + raise Net::BER::BerError, + "Indeterminite BER content length not implemented." end + data = read(content_length) parse_ber_object(syntax, id, data) end diff --git a/lib/net/ber/core_ext.rb b/lib/net/ber/core_ext.rb index b176df7f..37e0993b 100644 --- a/lib/net/ber/core_ext.rb +++ b/lib/net/ber/core_ext.rb @@ -1,5 +1,5 @@ # -*- ruby encoding: utf-8 -*- -require 'net/ber/ber_parser' +require_relative 'ber_parser' # :stopdoc: class IO include Net::BER::BERParser @@ -19,44 +19,37 @@ class OpenSSL::SSL::SSLSocket module Net::BER::Extensions # :nodoc: end -require 'net/ber/core_ext/string' +require_relative 'core_ext/string' # :stopdoc: class String include Net::BER::BERParser include Net::BER::Extensions::String end -require 'net/ber/core_ext/array' +require_relative 'core_ext/array' # :stopdoc: -class Array +class Array include Net::BER::Extensions::Array end # :startdoc: -require 'net/ber/core_ext/bignum' +require_relative 'core_ext/integer' # :stopdoc: -class Bignum - include Net::BER::Extensions::Bignum +class Integer + include Net::BER::Extensions::Integer end # :startdoc: -require 'net/ber/core_ext/fixnum' +require_relative 'core_ext/true_class' # :stopdoc: -class Fixnum - include Net::BER::Extensions::Fixnum -end -# :startdoc: - -require 'net/ber/core_ext/true_class' -# :stopdoc: -class TrueClass +class TrueClass include Net::BER::Extensions::TrueClass end # :startdoc: -require 'net/ber/core_ext/false_class' +require_relative 'core_ext/false_class' # :stopdoc: -class FalseClass +class FalseClass include Net::BER::Extensions::FalseClass end # :startdoc: diff --git a/lib/net/ber/core_ext/array.rb b/lib/net/ber/core_ext/array.rb index 250fa243..9deb4a1e 100644 --- a/lib/net/ber/core_ext/array.rb +++ b/lib/net/ber/core_ext/array.rb @@ -89,7 +89,7 @@ def to_ber_control #if our array does not contain at least one array then wrap it in an array before going forward ary = self[0].kind_of?(Array) ? self : [self] ary = ary.collect do |control_sequence| - control_sequence.collect{|element| element.to_ber}.to_ber_sequence.reject_empty_ber_arrays + control_sequence.collect(&:to_ber).to_ber_sequence.reject_empty_ber_arrays end ary.to_ber_sequence.reject_empty_ber_arrays end diff --git a/lib/net/ber/core_ext/bignum.rb b/lib/net/ber/core_ext/bignum.rb deleted file mode 100644 index dc62fb8b..00000000 --- a/lib/net/ber/core_ext/bignum.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -*- ruby encoding: utf-8 -*- -## -# BER extensions to the Bignum class. -module Net::BER::Extensions::Bignum - ## - # Converts a Bignum to an uncompressed BER integer. - def to_ber - result = [] - - # NOTE: Array#pack's 'w' is a BER _compressed_ integer. We need - # uncompressed BER integers, so we're not using that. See also: - # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/228864 - n = self - while n > 0 - b = n & 0xff - result << b - n = n >> 8 - end - - "\002" + ([result.size] + result.reverse).pack('C*') - end -end diff --git a/lib/net/ber/core_ext/fixnum.rb b/lib/net/ber/core_ext/fixnum.rb deleted file mode 100644 index 221baddf..00000000 --- a/lib/net/ber/core_ext/fixnum.rb +++ /dev/null @@ -1,66 +0,0 @@ -# -*- ruby encoding: utf-8 -*- -## -# Ber extensions to the Fixnum class. -module Net::BER::Extensions::Fixnum - ## - # Converts the fixnum to BER format. - def to_ber - "\002#{to_ber_internal}" - end - - ## - # Converts the fixnum to BER enumerated format. - def to_ber_enumerated - "\012#{to_ber_internal}" - end - - ## - # Converts the fixnum to BER length encodining format. - def to_ber_length_encoding - if self <= 127 - [self].pack('C') - else - i = [self].pack('N').sub(/^[\0]+/,"") - [0x80 + i.length].pack('C') + i - end - end - - ## - # Generate a BER-encoding for an application-defined INTEGER. Examples of - # such integers are SNMP's Counter, Gauge, and TimeTick types. - def to_ber_application(tag) - [0x40 + tag].pack("C") + to_ber_internal - end - - ## - # Used to BER-encode the length and content bytes of a Fixnum. Callers - # must prepend the tag byte for the contained value. - def to_ber_internal - # CAUTION: Bit twiddling ahead. You might want to shield your eyes or - # something. - - # Looks for the first byte in the fixnum that is not all zeroes. It does - # this by masking one byte after another, checking the result for bits - # that are left on. - size = Net::BER::MAX_FIXNUM_SIZE - while size > 1 - break if (self & (0xff << (size - 1) * 8)) > 0 - size -= 1 - end - - # Store the size of the fixnum in the result - result = [size] - - # Appends bytes to result, starting with higher orders first. Extraction - # of bytes is done by right shifting the original fixnum by an amount - # and then masking that with 0xff. - while size > 0 - # right shift size - 1 bytes, mask with 0xff - result << ((self >> ((size - 1) * 8)) & 0xff) - size -= 1 - end - - result.pack('C*') - end - private :to_ber_internal -end diff --git a/lib/net/ber/core_ext/integer.rb b/lib/net/ber/core_ext/integer.rb new file mode 100644 index 00000000..78313045 --- /dev/null +++ b/lib/net/ber/core_ext/integer.rb @@ -0,0 +1,74 @@ +# -*- ruby encoding: utf-8 -*- +## +# BER extensions to the Integer class, affecting Fixnum and Bignum objects. +module Net::BER::Extensions::Integer + ## + # Converts the Integer to BER format. + def to_ber + "\002#{to_ber_internal}" + end + + ## + # Converts the Integer to BER enumerated format. + def to_ber_enumerated + "\012#{to_ber_internal}" + end + + ## + # Converts the Integer to BER length encoding format. + def to_ber_length_encoding + if self <= 127 + [self].pack('C') + else + i = [self].pack('N').sub(/^[\0]+/, "") + [0x80 + i.length].pack('C') + i + end + end + + ## + # Generate a BER-encoding for an application-defined INTEGER. Examples of + # such integers are SNMP's Counter, Gauge, and TimeTick types. + def to_ber_application(tag) + [0x40 + tag].pack("C") + to_ber_internal + end + + ## + # Used to BER-encode the length and content bytes of an Integer. Callers + # must prepend the tag byte for the contained value. + def to_ber_internal + # Compute the byte length, accounting for negative values requiring two's + # complement. + size = 1 + size += 1 until (((self < 0) ? ~self : self) >> (size * 8)).zero? + + # Padding for positive, negative values. See section 8.5 of ITU-T X.690: + # http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf + + # For positive integers, if most significant bit in an octet is set to one, + # pad the result (otherwise it's decoded as a negative value). + if self > 0 && (self & (0x80 << (size - 1) * 8)) > 0 + size += 1 + end + + # And for negative integers, pad if the most significant bit in the octet + # is not set to one (othwerise, it's decoded as positive value). + if self < 0 && (self & (0x80 << (size - 1) * 8)) == 0 + size += 1 + end + + # Store the size of the Integer in the result + result = [size] + + # Appends bytes to result, starting with higher orders first. Extraction + # of bytes is done by right shifting the original Integer by an amount + # and then masking that with 0xff. + while size > 0 + # right shift size - 1 bytes, mask with 0xff + result << ((self >> ((size - 1) * 8)) & 0xff) + size -= 1 + end + + result.pack('C*') + end + private :to_ber_internal +end diff --git a/lib/net/ber/core_ext/string.rb b/lib/net/ber/core_ext/string.rb index b4ad8039..995d26d4 100644 --- a/lib/net/ber/core_ext/string.rb +++ b/lib/net/ber/core_ext/string.rb @@ -16,13 +16,13 @@ def to_ber(code = 0x04) [code].pack('C') + raw_string.length.to_ber_length_encoding + raw_string end - ## - # Converts a string to a BER string but does *not* encode to UTF-8 first. - # This is required for proper representation of binary data for Microsoft - # Active Directory - def to_ber_bin(code = 0x04) - [code].pack('C') + length.to_ber_length_encoding + self - end + ## + # Converts a string to a BER string but does *not* encode to UTF-8 first. + # This is required for proper representation of binary data for Microsoft + # Active Directory + def to_ber_bin(code = 0x04) + [code].pack('C') + length.to_ber_length_encoding + self + end def raw_utf8_encoded if self.respond_to?(:encode) @@ -75,6 +75,6 @@ def read_ber!(syntax = nil) end def reject_empty_ber_arrays - self.gsub(/0\000/n,'') + self.gsub(/0\000/n, '') end end diff --git a/lib/net/ber/core_ext/true_class.rb b/lib/net/ber/core_ext/true_class.rb index ac66c926..bf199d1e 100644 --- a/lib/net/ber/core_ext/true_class.rb +++ b/lib/net/ber/core_ext/true_class.rb @@ -5,8 +5,7 @@ module Net::BER::Extensions::TrueClass ## # Converts +true+ to the BER wireline representation of +true+. def to_ber - # 20100319 AZ: Note that this may not be the completely correct value, - # per some test documentation. We need to determine the truth of this. - "\001\001\001" + # http://tools.ietf.org/html/rfc4511#section-5.1 + "\001\001\xFF".force_encoding("ASCII-8BIT") end end diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index be21c1ef..8dca73c0 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -17,13 +17,22 @@ class LDAP end require 'socket' -require 'net/ber' -require 'net/ldap/pdu' -require 'net/ldap/filter' -require 'net/ldap/dataset' -require 'net/ldap/password' -require 'net/ldap/entry' -require 'net/ldap/version' +require_relative 'ber' +require_relative 'ldap/pdu' +require_relative 'ldap/filter' +require_relative 'ldap/dataset' +require_relative 'ldap/password' +require_relative 'ldap/entry' +require_relative 'ldap/instrumentation' +require_relative 'ldap/connection' +require_relative 'ldap/version' +require_relative 'ldap/error' +require_relative 'ldap/auth_adapter' +require_relative 'ldap/auth_adapter/simple' +require_relative 'ldap/auth_adapter/sasl' + +Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapter::Simple) +Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl) # == Quick-start for the Impatient # === Quick Example of a user-authentication against an LDAP directory: @@ -70,6 +79,14 @@ class LDAP # # p ldap.get_operation_result # +# === Setting connect timeout +# +# By default, Net::LDAP uses TCP sockets with a connection timeout of 5 seconds. +# +# This value can be tweaked passing the :connect_timeout parameter. +# i.e. +# ldap = Net::LDAP.new ..., +# :connect_timeout => 3 # # == A Brief Introduction to LDAP # @@ -242,21 +259,20 @@ class LDAP # and then keeps it open while it executes a user-supplied block. # Net::LDAP#open closes the connection on completion of the block. class Net::LDAP - - class LdapError < StandardError; end + include Net::LDAP::Instrumentation SearchScope_BaseObject = 0 SearchScope_SingleLevel = 1 SearchScope_WholeSubtree = 2 - SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel, - SearchScope_WholeSubtree ] + SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel, + SearchScope_WholeSubtree] DerefAliases_Never = 0 DerefAliases_Search = 1 DerefAliases_Find = 2 DerefAliases_Always = 3 - DerefAliasesArray = [ DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always ] - + DerefAliasesArray = [DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always] + primitive = { 2 => :null } # UnbindRequest body constructed = { 0 => :array, # BindRequest @@ -295,7 +311,7 @@ class LdapError < StandardError; end 0 => :array, # RFC-2251 Control and Filter-AND 1 => :array, # SearchFilter-OR 2 => :array, # SearchFilter-NOT - 3 => :array, # Seach referral + 3 => :array, # Search referral 4 => :array, # unknown use in Microsoft Outlook 5 => :array, # SearchFilter-GE 6 => :array, # SearchFilter-LE @@ -307,40 +323,123 @@ class LdapError < StandardError; end :constructed => constructed, } + universal = { + constructed: { + 107 => :string, # ExtendedResponse + }, + } + AsnSyntax = Net::BER.compile_syntax(:application => application, + :universal => universal, :context_specific => context_specific) DefaultHost = "127.0.0.1" DefaultPort = 389 DefaultAuth = { :method => :anonymous } DefaultTreebase = "dc=com" - DefaultForceNoPage = false - - StartTlsOid = "1.3.6.1.4.1.1466.20037" - + DefaultForceNoPage = false + + StartTlsOid = '1.3.6.1.4.1.1466.20037' + PasswdModifyOid = '1.3.6.1.4.1.4203.1.11.1' + WhoamiOid = '1.3.6.1.4.1.4203.1.11.3' + + # https://tools.ietf.org/html/rfc4511#section-4.1.9 + # https://tools.ietf.org/html/rfc4511#appendix-A + ResultCodeSuccess = 0 + ResultCodeOperationsError = 1 + ResultCodeProtocolError = 2 + ResultCodeTimeLimitExceeded = 3 + ResultCodeSizeLimitExceeded = 4 + ResultCodeCompareFalse = 5 + ResultCodeCompareTrue = 6 + ResultCodeAuthMethodNotSupported = 7 + ResultCodeStrongerAuthRequired = 8 + ResultCodeReferral = 10 + ResultCodeAdminLimitExceeded = 11 + ResultCodeUnavailableCriticalExtension = 12 + ResultCodeConfidentialityRequired = 13 + ResultCodeSaslBindInProgress = 14 + ResultCodeNoSuchAttribute = 16 + ResultCodeUndefinedAttributeType = 17 + ResultCodeInappropriateMatching = 18 + ResultCodeConstraintViolation = 19 + ResultCodeAttributeOrValueExists = 20 + ResultCodeInvalidAttributeSyntax = 21 + ResultCodeNoSuchObject = 32 + ResultCodeAliasProblem = 33 + ResultCodeInvalidDNSyntax = 34 + ResultCodeAliasDereferencingProblem = 36 + ResultCodeInappropriateAuthentication = 48 + ResultCodeInvalidCredentials = 49 + ResultCodeInsufficientAccessRights = 50 + ResultCodeBusy = 51 + ResultCodeUnavailable = 52 + ResultCodeUnwillingToPerform = 53 + ResultCodeNamingViolation = 64 + ResultCodeObjectClassViolation = 65 + ResultCodeNotAllowedOnNonLeaf = 66 + ResultCodeNotAllowedOnRDN = 67 + ResultCodeEntryAlreadyExists = 68 + ResultCodeObjectClassModsProhibited = 69 + ResultCodeAffectsMultipleDSAs = 71 + ResultCodeOther = 80 + + # https://tools.ietf.org/html/rfc4511#appendix-A.1 + ResultCodesNonError = [ + ResultCodeSuccess, + ResultCodeCompareFalse, + ResultCodeCompareTrue, + ResultCodeReferral, + ResultCodeSaslBindInProgress, + ] + + # nonstandard list of "successful" result codes for searches + ResultCodesSearchSuccess = [ + ResultCodeSuccess, + ResultCodeTimeLimitExceeded, + ResultCodeSizeLimitExceeded, + ] + + # map of result code to human message ResultStrings = { - 0 => "Success", - 1 => "Operations Error", - 2 => "Protocol Error", - 3 => "Time Limit Exceeded", - 4 => "Size Limit Exceeded", - 10 => "Referral", - 12 => "Unavailable crtical extension", - 14 => "saslBindInProgress", - 16 => "No Such Attribute", - 17 => "Undefined Attribute Type", - 19 => "Constraint Violation", - 20 => "Attribute or Value Exists", - 32 => "No Such Object", - 34 => "Invalid DN Syntax", - 48 => "Inappropriate Authentication", - 49 => "Invalid Credentials", - 50 => "Insufficient Access Rights", - 51 => "Busy", - 52 => "Unavailable", - 53 => "Unwilling to perform", - 65 => "Object Class Violation", - 68 => "Entry Already Exists" + ResultCodeSuccess => "Success", + ResultCodeOperationsError => "Operations Error", + ResultCodeProtocolError => "Protocol Error", + ResultCodeTimeLimitExceeded => "Time Limit Exceeded", + ResultCodeSizeLimitExceeded => "Size Limit Exceeded", + ResultCodeCompareFalse => "False Comparison", + ResultCodeCompareTrue => "True Comparison", + ResultCodeAuthMethodNotSupported => "Auth Method Not Supported", + ResultCodeStrongerAuthRequired => "Stronger Auth Needed", + ResultCodeReferral => "Referral", + ResultCodeAdminLimitExceeded => "Admin Limit Exceeded", + ResultCodeUnavailableCriticalExtension => "Unavailable critical extension", + ResultCodeConfidentialityRequired => "Confidentiality Required", + ResultCodeSaslBindInProgress => "saslBindInProgress", + ResultCodeNoSuchAttribute => "No Such Attribute", + ResultCodeUndefinedAttributeType => "Undefined Attribute Type", + ResultCodeInappropriateMatching => "Inappropriate Matching", + ResultCodeConstraintViolation => "Constraint Violation", + ResultCodeAttributeOrValueExists => "Attribute or Value Exists", + ResultCodeInvalidAttributeSyntax => "Invalide Attribute Syntax", + ResultCodeNoSuchObject => "No Such Object", + ResultCodeAliasProblem => "Alias Problem", + ResultCodeInvalidDNSyntax => "Invalid DN Syntax", + ResultCodeAliasDereferencingProblem => "Alias Dereferencing Problem", + ResultCodeInappropriateAuthentication => "Inappropriate Authentication", + ResultCodeInvalidCredentials => "Invalid Credentials", + ResultCodeInsufficientAccessRights => "Insufficient Access Rights", + ResultCodeBusy => "Busy", + ResultCodeUnavailable => "Unavailable", + ResultCodeUnwillingToPerform => "Unwilling to perform", + ResultCodeNamingViolation => "Naming Violation", + ResultCodeObjectClassViolation => "Object Class Violation", + ResultCodeNotAllowedOnNonLeaf => "Not Allowed On Non-Leaf", + ResultCodeNotAllowedOnRDN => "Not Allowed On RDN", + ResultCodeEntryAlreadyExists => "Entry Already Exists", + ResultCodeObjectClassModsProhibited => "ObjectClass Modifications Prohibited", + ResultCodeAffectsMultipleDSAs => "Affects Multiple DSAs", + ResultCodeOther => "Other", } module LDAPControls @@ -356,6 +455,7 @@ def self.result2string(code) #:nodoc: attr_accessor :host attr_accessor :port + attr_accessor :hosts attr_accessor :base # Instantiate an object of type Net::LDAP to perform directory operations. @@ -364,6 +464,8 @@ def self.result2string(code) #:nodoc: # described below. The following arguments are supported: # * :host => the LDAP server's IP-address (default 127.0.0.1) # * :port => the LDAP server's TCP port (default 389) + # * :hosts => an enumerable of pairs of hosts and corresponding ports with + # which to attempt opening connections (default [[host, port]]) # * :auth => a Hash containing authorization parameters. Currently # supported values include: {:method => :anonymous} and {:method => # :simple, :username => your_user_name, :password => your_password } @@ -375,31 +477,92 @@ def self.result2string(code) #:nodoc: # specify a treebase. If you give a treebase value in any particular # call to #search, that value will override any treebase value you give # here. - # * :encryption => specifies the encryption to be used in communicating - # with the LDAP server. The value is either a Hash containing additional - # parameters, or the Symbol :simple_tls, which is equivalent to - # specifying the Hash {:method => :simple_tls}. There is a fairly large - # range of potential values that may be given for this parameter. See - # #encryption for details. # * :force_no_page => Set to true to prevent paged results even if your # server says it supports them. This is a fix for MS Active Directory + # * :instrumentation_service => An object responsible for instrumenting + # operations, compatible with ActiveSupport::Notifications' public API. + # * :connect_timeout => The TCP socket timeout (in seconds) to use when + # connecting to the LDAP server (default 5 seconds). + # * :encryption => specifies the encryption to be used in communicating + # with the LDAP server. The value must be a Hash containing additional + # parameters, which consists of two keys: + # method: - :simple_tls or :start_tls + # tls_options: - Hash of options for that method + # The :simple_tls encryption method encrypts all communications + # with the LDAP server. It completely establishes SSL/TLS encryption with + # the LDAP server before any LDAP-protocol data is exchanged. There is no + # plaintext negotiation and no special encryption-request controls are + # sent to the server. The :simple_tls option is the simplest, easiest + # way to encrypt communications between Net::LDAP and LDAP servers. + # If you get communications or protocol errors when using this option, + # check with your LDAP server administrator. Pay particular attention + # to the TCP port you are connecting to. It's impossible for an LDAP + # server to support plaintext LDAP communications and simple TLS + # connections on the same port. The standard TCP port for unencrypted + # LDAP connections is 389, but the standard port for simple-TLS + # encrypted connections is 636. Be sure you are using the correct port. + # The :start_tls like the :simple_tls encryption method also encrypts all + # communcations with the LDAP server. With the exception that it operates + # over the standard TCP port. + # + # To validate the LDAP server's certificate (a security must if you're + # talking over the public internet), you need to set :tls_options + # something like this... + # + # Net::LDAP.new( + # # ... set host, bind dn, etc ... + # encryption: { + # method: :simple_tls, + # tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, + # } + # ) + # + # The above will use the operating system-provided store of CA + # certificates to validate your LDAP server's cert. + # If cert validation fails, it'll happen during the #bind + # whenever you first try to open a connection to the server. + # Those methods will throw Net::LDAP::ConnectionError with + # a message about certificate verify failing. If your + # LDAP server's certificate is signed by DigiCert, Comodo, etc., + # you're probably good. If you've got a self-signed cert but it's + # been added to the host's OS-maintained CA store (e.g. on Debian + # add foobar.crt to /usr/local/share/ca-certificates/ and run + # `update-ca-certificates`), then the cert should pass validation. + # To ignore the OS's CA store, put your CA in a PEM-encoded file and... + # + # encryption: { + # method: :simple_tls, + # tls_options: { ca_file: '/path/to/my-little-ca.pem', + # ssl_version: 'TLSv1_1' }, + # } + # + # As you might guess, the above example also fails the connection + # if the client can't negotiate TLS v1.1. + # tls_options is ultimately passed to OpenSSL::SSL::SSLContext#set_params + # For more details, see + # http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html # # Instantiating a Net::LDAP object does not result in network # traffic to the LDAP server. It simply stores the connection and binding - # parameters in the object. + # parameters in the object. That's why Net::LDAP.new doesn't throw + # cert validation errors itself; #bind does instead. def initialize(args = {}) @host = args[:host] || DefaultHost @port = args[:port] || DefaultPort + @hosts = args[:hosts] @verbose = false # Make this configurable with a switch on the class. @auth = args[:auth] || DefaultAuth @base = args[:base] || DefaultTreebase - @force_no_page = args[:force_no_page] || DefaultForceNoPage - encryption args[:encryption] # may be nil + @force_no_page = args[:force_no_page] || DefaultForceNoPage + @encryption = normalize_encryption(args[:encryption]) # may be nil + @connect_timeout = args[:connect_timeout] if pr = @auth[:password] and pr.respond_to?(:call) @auth[:password] = pr.call end + @instrumentation_service = args[:instrumentation_service] + # This variable is only set when we are created with LDAP::open. All of # our internal methods will connect using it, or else they will create # their own. @@ -443,7 +606,7 @@ def authenticate(username, password) @auth = { :method => :simple, :username => username, - :password => password + :password => password, } end alias_method :auth, :authenticate @@ -456,38 +619,12 @@ def authenticate(username, password) # additional capabilities are added, more configuration values will be # added here. # - # Currently, the only supported argument is { :method => :simple_tls }. - # (Equivalently, you may pass the symbol :simple_tls all by itself, - # without enclosing it in a Hash.) - # - # The :simple_tls encryption method encrypts all communications - # with the LDAP server. It completely establishes SSL/TLS encryption with - # the LDAP server before any LDAP-protocol data is exchanged. There is no - # plaintext negotiation and no special encryption-request controls are - # sent to the server. The :simple_tls option is the simplest, easiest - # way to encrypt communications between Net::LDAP and LDAP servers. - # It's intended for cases where you have an implicit level of trust in the - # authenticity of the LDAP server. No validation of the LDAP server's SSL - # certificate is performed. This means that :simple_tls will not produce - # errors if the LDAP server's encryption certificate is not signed by a - # well-known Certification Authority. If you get communications or - # protocol errors when using this option, check with your LDAP server - # administrator. Pay particular attention to the TCP port you are - # connecting to. It's impossible for an LDAP server to support plaintext - # LDAP communications and simple TLS connections on the same port. - # The standard TCP port for unencrypted LDAP connections is 389, but the - # standard port for simple-TLS encrypted connections is 636. Be sure you - # are using the correct port. - # - # [Note: a future version of Net::LDAP will support the STARTTLS LDAP - # control, which will enable encrypted communications on the same TCP port - # used for unencrypted connections.] + # This method is deprecated. + # def encryption(args) - case args - when :simple_tls, :start_tls - args = { :method => args } - end - @encryption = args + warn "Deprecation warning: please give :encryption option as a Hash to Net::LDAP.new" + return if args.nil? + @encryption = normalize_encryption(args) end # #open takes the same parameters as #new. #open makes a network @@ -531,8 +668,11 @@ def self.open(args) #++ def get_operation_result result = @result - result = result.result if result.is_a?(Net::LDAP::PDU) os = OpenStruct.new + if result.is_a?(Net::LDAP::PDU) + os.extended_response = result.extended_response + result = result.result + end if result.is_a?(Hash) # We might get a hash of LDAP response codes instead of a simple # numeric code. @@ -542,7 +682,7 @@ def get_operation_result elsif result os.code = result else - os.code = 0 + os.code = Net::LDAP::ResultCodeSuccess end os.message = Net::LDAP.result2string(os.code) os @@ -569,18 +709,18 @@ def open # anything with the bind results. We then pass self to the caller's # block, where he will execute his LDAP operations. Of course they will # all generate auth failures if the bind was unsuccessful. - raise Net::LDAP::LdapError, "Open already in progress" if @open_connection + raise Net::LDAP::AlreadyOpenedError, "Open already in progress" if @open_connection - begin - @open_connection = Net::LDAP::Connection.new(:host => @host, - :port => @port, - :encryption => - @encryption) - @open_connection.bind(@auth) - yield self - ensure - @open_connection.close if @open_connection - @open_connection = nil + instrument "open.net_ldap" do |payload| + begin + @open_connection = new_connection + payload[:connection] = @open_connection + payload[:bind] = @result = @open_connection.bind(@auth) + yield self + ensure + @open_connection.close if @open_connection + @open_connection = nil + end end end @@ -598,6 +738,7 @@ def open # Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.) # * :size (an integer indicating the maximum number of search entries to # return. Default is zero, which signifies no limit.) + # * :time (an integer restricting the maximum time in seconds allowed for a search. Default is zero, no time limit RFC 4511 4.5.1.5) # * :deref (one of: Net::LDAP::DerefAliases_Never, Net::LDAP::DerefAliases_Search, # Net::LDAP::DerefAliases_Find, Net::LDAP::DerefAliases_Always. Default is Never.) # @@ -641,30 +782,23 @@ def search(args = {}) return_result_set = args[:return_result] != false result_set = return_result_set ? [] : nil - if @open_connection - @result = @open_connection.search(args) { |entry| - result_set << entry if result_set - yield entry if block_given? - } - else - begin - conn = Net::LDAP::Connection.new(:host => @host, :port => @port, - :encryption => @encryption) - if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 - @result = conn.search(args) { |entry| - result_set << entry if result_set - yield entry if block_given? - } + instrument "search.net_ldap", args do |payload| + @result = use_connection(args) do |conn| + conn.search(args) do |entry| + result_set << entry if result_set + yield entry if block_given? end - ensure - conn.close if conn end - end - if return_result_set - (!@result.nil? && @result.result_code == 0) ? result_set : nil - else - @result.success? + if return_result_set + unless @result.nil? + if ResultCodesSearchSuccess.include?(@result.result_code) + result_set + end + end + else + @result.success? + end end end @@ -726,19 +860,22 @@ def search(args = {}) # the documentation for #auth, the password parameter can be a Ruby Proc # instead of a String. def bind(auth = @auth) - if @open_connection - @result = @open_connection.bind(auth) - else - begin - conn = Connection.new(:host => @host, :port => @port, - :encryption => @encryption) - @result = conn.bind(auth) - ensure - conn.close if conn + instrument "bind.net_ldap" do |payload| + if @open_connection + payload[:connection] = @open_connection + payload[:bind] = @result = @open_connection.bind(auth) + else + begin + conn = new_connection + payload[:connection] = conn + payload[:bind] = @result = conn.bind(auth) + ensure + conn.close if conn + end end - end - @result.success? + @result.success? + end end # #bind_as is for testing authentication credentials. @@ -789,7 +926,7 @@ def bind(auth = @auth) # end def bind_as(args = {}) result = false - open { |me| + open do |me| rs = search args if rs and rs.first and dn = rs.first.dn password = args[:password] @@ -797,7 +934,7 @@ def bind_as(args = {}) result = rs if bind(:method => :simple, :username => dn, :password => password) end - } + end result end @@ -826,21 +963,12 @@ def bind_as(args = {}) # ldap.add(:dn => dn, :attributes => attr) # end def add(args) - if @open_connection - @result = @open_connection.add(args) - else - @result = 0 - begin - conn = Connection.new(:host => @host, :port => @port, - :encryption => @encryption) - if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 - @result = conn.add(args) - end - ensure - conn.close if conn + instrument "add.net_ldap", args do |payload| + @result = use_connection(args) do |conn| + conn.add(args) end + @result.success? end - @result.success? end # Modifies the attribute values of a particular entry on the LDAP @@ -858,7 +986,7 @@ def add(args) # The LDAP protocol provides a full and well thought-out set of operations # for changing the values of attributes, but they are necessarily somewhat # complex and not always intuitive. If these instructions are confusing or - # incomplete, please send us email or create a bug report on rubyforge. + # incomplete, please send us email or create an issue on GitHub. # # The :operations parameter to #modify takes an array of # operation-descriptors. Each individual operation is specified in one @@ -866,9 +994,10 @@ def add(args) # operations in order. # # Each of the operations appearing in the Array must itself be an Array - # with exactly three elements: an operator:: must be :add, :replace, or - # :delete an attribute name:: the attribute name (string or symbol) to - # modify a value:: either a string or an array of strings. + # with exactly three elements: + # an operator :: must be :add, :replace, or :delete + # an attribute name :: the attribute name (string or symbol) to modify + # a value :: either a string or an array of strings. # # The :add operator will, unsurprisingly, add the specified values to the # specified attribute. If the attribute does not already exist, :add will @@ -911,35 +1040,63 @@ def add(args) # may not get extended information that will tell you which one failed. # #modify has no notion of an atomic transaction. If you specify a chain # of modifications in one call to #modify, and one of them fails, the - # preceding ones will usually not be "rolled back, " resulting in a + # preceding ones will usually not be "rolled back", resulting in a # partial update. This is a limitation of the LDAP protocol, not of # Net::LDAP. # # The lack of transactional atomicity in LDAP means that you're usually # better off using the convenience methods #add_attribute, - # #replace_attribute, and #delete_attribute, which are are wrappers over + # #replace_attribute, and #delete_attribute, which are wrappers over # #modify. However, certain LDAP servers may provide concurrency # semantics, in which the several operations contained in a single #modify # call are not interleaved with other modification-requests received # simultaneously by the server. It bears repeating that this concurrency # does _not_ imply transactional atomicity, which LDAP does not provide. def modify(args) - if @open_connection - @result = @open_connection.modify(args) - else - @result = 0 - begin - conn = Connection.new(:host => @host, :port => @port, - :encryption => @encryption) - if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 - @result = conn.modify(args) - end - ensure - conn.close if conn + instrument "modify.net_ldap", args do |payload| + @result = use_connection(args) do |conn| + conn.modify(args) end + @result.success? end + end - @result.success? + # Password Modify + # + # Change existing password: + # + # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com' + # auth = { + # method: :simple, + # username: dn, + # password: 'passworD1' + # } + # ldap.password_modify(dn: dn, + # auth: auth, + # old_password: 'passworD1', + # new_password: 'passworD2') + # + # Or get the LDAP server to generate a password for you: + # + # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com' + # auth = { + # method: :simple, + # username: dn, + # password: 'passworD1' + # } + # ldap.password_modify(dn: dn, + # auth: auth, + # old_password: 'passworD1') + # + # ldap.get_operation_result.extended_response[0][0] #=> 'VtcgGf/G' + # + def password_modify(args) + instrument "modify_password.net_ldap", args do |payload| + @result = use_connection(args) do |conn| + conn.password_modify(args) + end + @result.success? + end end # Add a value to an attribute. Takes the full DN of the entry to modify, @@ -996,21 +1153,12 @@ def delete_attribute(dn, attribute) # # _Documentation_ _stub_ def rename(args) - if @open_connection - @result = @open_connection.rename(args) - else - @result = 0 - begin - conn = Connection.new(:host => @host, :port => @port, - :encryption => @encryption) - if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 - @result = conn.rename(args) - end - ensure - conn.close if conn + instrument "rename.net_ldap", args do |payload| + @result = use_connection(args) do |conn| + conn.rename(args) end + @result.success? end - @result.success? end alias_method :modify_rdn, :rename @@ -1024,21 +1172,12 @@ def rename(args) # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com" # ldap.delete :dn => dn def delete(args) - if @open_connection - @result = @open_connection.delete(args) - else - @result = 0 - begin - conn = Connection.new(:host => @host, :port => @port, - :encryption => @encryption) - if (@result = conn.bind(args[:auth] || @auth)).result_code == 0 - @result = conn.delete(args) - end - ensure - conn.close + instrument "delete.net_ldap", args do |payload| + @result = use_connection(args) do |conn| + conn.delete(args) end + @result.success? end - @result.success? end # Delete an entry from the LDAP directory along with all subordinate entries. @@ -1046,14 +1185,39 @@ def delete(args) # entries. This method sends an extra control code to tell the LDAP server # to do a tree delete. ('1.2.840.113556.1.4.805') # + # If the LDAP server does not support the DELETE_TREE control code, subordinate + # entries are deleted recursively instead. + # # Returns True or False to indicate whether the delete succeeded. Extended # status information is available by calling #get_operation_result. # # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com" # ldap.delete_tree :dn => dn def delete_tree(args) - delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]])) + if search_root_dse[:supportedcontrol].include? Net::LDAP::LDAPControls::DELETE_TREE + delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]])) + else + recursive_delete(args) + end end + + # Return the authorization identity of the client that issues the + # ldapwhoami request. The method does not support any arguments. + # + # Returns True or False to indicate whether the request was successfull. + # The result is available in the extended status information when calling + # #get_operation_result. + # + # ldap.ldapwhoami + # puts ldap.get_operation_result.extended_response + def ldapwhoami(args = {}) + instrument "ldapwhoami.net_ldap", args do |payload| + @result = use_connection(args, &:ldapwhoami) + @result.success? ? @result.extended_response : nil + end + end + alias_method :whoami, :ldapwhoami + # This method is experimental and subject to change. Return the rootDSE # record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if # the server doesn't return the record. @@ -1070,9 +1234,16 @@ def delete_tree(args) def search_root_dse rs = search(:ignore_server_caps => true, :base => "", :scope => SearchScope_BaseObject, - :attributes => [ :namingContexts, :supportedLdapVersion, - :altServer, :supportedControl, :supportedExtension, - :supportedFeatures, :supportedSASLMechanisms]) + :attributes => [ + :altServer, + :namingContexts, + :supportedCapabilities, + :supportedControl, + :supportedExtension, + :supportedFeatures, + :supportedLdapVersion, + :supportedSASLMechanisms, + ]) (rs and rs.first) or Net::LDAP::Entry.new end @@ -1104,10 +1275,10 @@ def search_subschema_entry rs = search(:ignore_server_caps => true, :base => "", :scope => SearchScope_BaseObject, :attributes => [:subschemaSubentry]) - return Net::LDAP::Entry.new unless (rs and rs.first) + return Net::LDAP::Entry.new unless rs and rs.first subschema_name = rs.first.subschemasubentry - return Net::LDAP::Entry.new unless (subschema_name and subschema_name.first) + return Net::LDAP::Entry.new unless subschema_name and subschema_name.first rs = search(:ignore_server_caps => true, :base => subschema_name.first, :scope => SearchScope_BaseObject, @@ -1123,524 +1294,93 @@ def search_subschema_entry # MUST refactor the root_dse call out. #++ def paged_searches_supported? - # active directory returns that it supports paged results. However - # it returns binary data in the rfc2696_cookie which throws an - # encoding exception breaking searching. - return false if @force_no_page + # active directory returns that it supports paged results. However + # it returns binary data in the rfc2696_cookie which throws an + # encoding exception breaking searching. + return false if @force_no_page @server_caps ||= search_root_dse @server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS) end -end # class LDAP - -# This is a private class used internally by the library. It should not -# be called by user code. -class Net::LDAP::Connection #:nodoc: - LdapVersion = 3 - MaxSaslChallenges = 10 - - def initialize(server) - begin - @conn = TCPSocket.new(server[:host], server[:port]) - rescue SocketError - raise Net::LDAP::LdapError, "No such address or other socket error." - rescue Errno::ECONNREFUSED - raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}." - end - - if server[:encryption] - setup_encryption server[:encryption] - end - yield self if block_given? + # Mask auth password + def inspect + inspected = super + inspected.gsub! @auth[:password], "*******" if @auth[:password] + inspected end - module GetbyteForSSLSocket - def getbyte - getc.ord - end + # Internal: Set @open_connection for testing + def connection=(connection) + @open_connection = connection end - def self.wrap_with_ssl(io) - raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL - ctx = OpenSSL::SSL::SSLContext.new - conn = OpenSSL::SSL::SSLSocket.new(io, ctx) - conn.connect - conn.sync_close = true - - conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte) + private - conn - end - - #-- - # Helper method called only from new, and only after we have a - # successfully-opened @conn instance variable, which is a TCP connection. - # Depending on the received arguments, we establish SSL, potentially - # replacing the value of @conn accordingly. Don't generate any errors here - # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption - # is requested and we have trouble setting it up. That includes if OpenSSL - # is not set up on the machine. (Question: how does the Ruby OpenSSL - # wrapper react in that case?) DO NOT filter exceptions raised by the - # OpenSSL library. Let them pass back to the user. That should make it - # easier for us to debug the problem reports. Presumably (hopefully?) that - # will also produce recognizable errors if someone tries to use this on a - # machine without OpenSSL. - # - # The simple_tls method is intended as the simplest, stupidest, easiest - # solution for people who want nothing more than encrypted comms with the - # LDAP server. It doesn't do any server-cert validation and requires - # nothing in the way of key files and root-cert files, etc etc. OBSERVE: - # WE REPLACE the value of @conn, which is presumed to be a connected - # TCPSocket object. - # - # The start_tls method is supported by many servers over the standard LDAP - # port. It does not require an alternative port for encrypted - # communications, as with simple_tls. Thanks for Kouhei Sutou for - # generously contributing the :start_tls path. - #++ - def setup_encryption(args) - case args[:method] - when :simple_tls - @conn = self.class.wrap_with_ssl(@conn) - # additional branches requiring server validation and peer certs, etc. - # go here. - when :start_tls - msgid = next_msgid.to_ber - request = [Net::LDAP::StartTlsOid.to_ber].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) - request_pkt = [msgid, request].to_ber_sequence - @conn.write request_pkt - be = @conn.read_ber(Net::LDAP::AsnSyntax) - raise Net::LDAP::LdapError, "no start_tls result" if be.nil? - pdu = Net::LDAP::PDU.new(be) - raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil? - if pdu.result_code.zero? - @conn = self.class.wrap_with_ssl(@conn) - else - raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}" - end - else - raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}" - end - end - - #-- - # This is provided as a convenience method to make sure a connection - # object gets closed without waiting for a GC to happen. Clients shouldn't - # have to call it, but perhaps it will come in handy someday. - #++ - def close - @conn.close - @conn = nil - end - - def next_msgid - @msgid ||= 0 - @msgid += 1 - end - - def bind(auth) - meth = auth[:method] - if [:simple, :anonymous, :anon].include?(meth) - bind_simple auth - elsif meth == :sasl - bind_sasl(auth) - elsif meth == :gss_spnego - bind_gss_spnego(auth) + # Yields an open connection if there is one, otherwise establishes a new + # connection, binds, and yields it. If binding fails, it will return the + # result from that, and :use_connection: will not yield at all. If not + # the return value is whatever is returned from the block. + def use_connection(args) + if @open_connection + yield @open_connection else - raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})" + begin + conn = new_connection + result = conn.bind(args[:auth] || @auth) + return result unless result.result_code == Net::LDAP::ResultCodeSuccess + yield conn + ensure + conn.close if conn + end end end - #-- - # Implements a simple user/psw authentication. Accessed by calling #bind - # with a method of :simple or :anonymous. - #++ - def bind_simple(auth) - user, psw = if auth[:method] == :simple - [auth[:username] || auth[:dn], auth[:password]] - else - ["", ""] - end - - raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) - - msgid = next_msgid.to_ber - request = [LdapVersion.to_ber, user.to_ber, - psw.to_ber_contextspecific(0)].to_ber_appsequence(0) - request_pkt = [msgid, request].to_ber_sequence - @conn.write request_pkt - - (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" - - pdu - end - - #-- - # Required parameters: :mechanism, :initial_credential and - # :challenge_response - # - # Mechanism is a string value that will be passed in the SASL-packet's - # "mechanism" field. - # - # Initial credential is most likely a string. It's passed in the initial - # BindRequest that goes to the server. In some protocols, it may be empty. - # - # Challenge-response is a Ruby proc that takes a single parameter and - # returns an object that will typically be a string. The - # challenge-response block is called when the server returns a - # BindResponse with a result code of 14 (saslBindInProgress). The - # challenge-response block receives a parameter containing the data - # returned by the server in the saslServerCreds field of the LDAP - # BindResponse packet. The challenge-response block may be called multiple - # times during the course of a SASL authentication, and each time it must - # return a value that will be passed back to the server as the credential - # data in the next BindRequest packet. - #++ - def bind_sasl(auth) - mech, cred, chall = auth[:mechanism], auth[:initial_credential], - auth[:challenge_response] - raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall) - - n = 0 - loop { - msgid = next_msgid.to_ber - sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) - request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) - request_pkt = [msgid, request].to_ber_sequence - @conn.write request_pkt - - (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" - return pdu unless pdu.result_code == 14 # saslBindInProgress - raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges) - - cred = chall.call(pdu.result_server_sasl_creds) + # Establish a new connection to the LDAP server + def new_connection + connection = Net::LDAP::Connection.new \ + :host => @host, + :port => @port, + :hosts => @hosts, + :encryption => @encryption, + :instrumentation_service => @instrumentation_service, + :connect_timeout => @connect_timeout + + # Force connect to see if there's a connection error + connection.socket + connection + rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e + @result = { + :resultCode => 52, + :errorMessage => ResultStrings[ResultCodeUnavailable], } - - raise Net::LDAP::LdapError, "why are we here?" + raise e end - private :bind_sasl - - #-- - # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. - # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to - # integrate it without introducing an external dependency. - # - # This authentication method is accessed by calling #bind with a :method - # parameter of :gss_spnego. It requires :username and :password - # attributes, just like the :simple authentication method. It performs a - # GSS-SPNEGO authentication with the server, which is presumed to be a - # Microsoft Active Directory. - #++ - def bind_gss_spnego(auth) - require 'ntlm' - - user, psw = [auth[:username] || auth[:dn], auth[:password]] - raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) - - nego = proc { |challenge| - t2_msg = NTLM::Message.parse(challenge) - t3_msg = t2_msg.response({ :user => user, :password => psw }, - { :ntlmv2 => true }) - t3_msg.serialize - } - bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO", - :initial_credential => NTLM::Message::Type1.new.serialize, - :challenge_response => nego) - end - private :bind_gss_spnego - - - #-- - # Allow the caller to specify a sort control - # - # The format of the sort control needs to be: - # - # :sort_control => ["cn"] # just a string - # or - # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false) - # or - # :sort_control => ["givenname","sn"] #multiple strings or arrays - # - def encode_sort_controls(sort_definitions) - return sort_definitions unless sort_definitions + # Normalize encryption parameter the constructor accepts, expands a few + # convenience symbols into recognizable hashes + def normalize_encryption(args) + return if args.nil? + return args if args.is_a? Hash - sort_control_values = sort_definitions.map do |control| - control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder - control[0] = String(control[0]).to_ber, - control[1] = String(control[1]).to_ber, - control[2] = (control[2] == true).to_ber - control.to_ber_sequence + case method = args.to_sym + when :simple_tls, :start_tls + { :method => method, :tls_options => {} } end - sort_control = [ - Net::LDAP::LDAPControls::SORT_REQUEST.to_ber, - false.to_ber, - sort_control_values.to_ber_sequence.to_s.to_ber - ].to_ber_sequence end - #-- - # Alternate implementation, this yields each search entry to the caller as - # it are received. - # - # TODO: certain search parameters are hardcoded. - # TODO: if we mis-parse the server results or the results are wrong, we - # can block forever. That's because we keep reading results until we get a - # type-5 packet, which might never come. We need to support the time-limit - # in the protocol. - #++ - def search(args = {}) - search_filter = (args && args[:filter]) || - Net::LDAP::Filter.eq("objectclass", "*") - search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String) - search_base = (args && args[:base]) || "dc=example, dc=com" - search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber} - return_referrals = args && args[:return_referrals] == true - sizelimit = (args && args[:size].to_i) || 0 - raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0 - paged_searches_supported = (args && args[:paged_searches_supported]) - - attributes_only = (args and args[:attributes_only] == true) - scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree - raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope) - - sort_control = encode_sort_controls(args.fetch(:sort_controls){ false }) - - deref = args[:deref] || Net::LDAP::DerefAliases_Never - raise Net::LDAP::LdapError.new( "invalid alias dereferencing value" ) unless Net::LDAP::DerefAliasesArray.include?(deref) - - - # An interesting value for the size limit would be close to A/D's - # built-in page limit of 1000 records, but openLDAP newer than version - # 2.2.0 chokes on anything bigger than 126. You get a silent error that - # is easily visible by running slapd in debug mode. Go figure. - # - # Changed this around 06Sep06 to support a caller-specified search-size - # limit. Because we ALWAYS do paged searches, we have to work around the - # problem that it's not legal to specify a "normal" sizelimit (in the - # body of the search request) that is larger than the page size we're - # requesting. Unfortunately, I have the feeling that this will break - # with LDAP servers that don't support paged searches!!! - # - # (Because we pass zero as the sizelimit on search rounds when the - # remaining limit is larger than our max page size of 126. In these - # cases, I think the caller's search limit will be ignored!) - # - # CONFIRMED: This code doesn't work on LDAPs that don't support paged - # searches when the size limit is larger than 126. We're going to have - # to do a root-DSE record search and not do a paged search if the LDAP - # doesn't support it. Yuck. - rfc2696_cookie = [126, ""] - result_pdu = nil - n_results = 0 - - loop { - # should collect this into a private helper to clarify the structure - query_limit = 0 - if sizelimit > 0 - if paged_searches_supported - query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - - n_results) : 0) - else - query_limit = sizelimit - end - end - - request = [ - search_base.to_ber, - scope.to_ber_enumerated, - deref.to_ber_enumerated, - query_limit.to_ber, # size limit - 0.to_ber, - attributes_only.to_ber, - search_filter.to_ber, - search_attributes.to_ber_sequence - ].to_ber_appsequence(3) - - # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory - # this breaks when calling to_ber. (Can't force binary data to UTF-8) - # we have to disable paging (even though server supports it) to get around this... - - controls = [] - controls << - [ - Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber, - # Criticality MUST be false to interoperate with normal LDAPs. - false.to_ber, - rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber - ].to_ber_sequence if paged_searches_supported - controls << sort_control if sort_control - controls = controls.empty? ? nil : controls.to_ber_contextspecific(0) - - pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence - @conn.write pkt - - result_pdu = nil - controls = [] - - while (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) - case pdu.app_tag - when 4 # search-data - n_results += 1 - yield pdu.search_entry if block_given? - when 19 # search-referral - if return_referrals - if block_given? - se = Net::LDAP::Entry.new - se[:search_referrals] = (pdu.search_referrals || []) - yield se - end - end - when 5 # search-result - result_pdu = pdu - controls = pdu.result_controls - if return_referrals && pdu.result_code == 10 - if block_given? - se = Net::LDAP::Entry.new - se[:search_referrals] = (pdu.search_referrals || []) - yield se - end - end - break - else - raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}" - end - end - - # When we get here, we have seen a type-5 response. If there is no - # error AND there is an RFC-2696 cookie, then query again for the next - # page of results. If not, we're done. Don't screw this up or we'll - # break every search we do. - # - # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't - # that have a parameter of AsnSyntax? Does this just accidentally - # work? According to RFC-2696, the value expected in this position is - # of type OCTET STRING, covered in the default syntax supported by - # read_ber, so I guess we're ok. - more_pages = false - if result_pdu.result_code == 0 and controls - controls.each do |c| - if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS - # just in case some bogus server sends us more than 1 of these. - more_pages = false - if c.value and c.value.length > 0 - cookie = c.value.read_ber[1] - if cookie and cookie.length > 0 - rfc2696_cookie[1] = cookie - more_pages = true - end - end - end - end - end - - break unless more_pages - } # loop - - result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search") - end - - MODIFY_OPERATIONS = { #:nodoc: - :add => 0, - :delete => 1, - :replace => 2 - } - - def self.modify_ops(operations) - ops = [] - if operations - operations.each { |op, attrib, values| - # TODO, fix the following line, which gives a bogus error if the - # opcode is invalid. - op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated - values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set - values = [ attrib.to_s.to_ber, values ].to_ber_sequence - ops << [ op_ber, values ].to_ber - } + # Recursively delete a dn and it's subordinate children. + # This is useful when a server does not support the DELETE_TREE control code. + def recursive_delete(args) + raise EmptyDNError unless args.is_a?(Hash) && args.key?(:dn) + # Delete Children + search(base: args[:dn], scope: Net::LDAP::SearchScope_SingleLevel) do |entry| + recursive_delete(dn: entry.dn) end - ops - end - - #-- - # TODO: need to support a time limit, in case the server fails to respond. - # TODO: We're throwing an exception here on empty DN. Should return a - # proper error instead, probaby from farther up the chain. - # TODO: If the user specifies a bogus opcode, we'll throw a confusing - # error here ("to_ber_enumerated is not defined on nil"). - #++ - def modify(args) - modify_dn = args[:dn] or raise "Unable to modify empty DN" - ops = self.class.modify_ops args[:operations] - request = [ modify_dn.to_ber, - ops.to_ber_sequence ].to_ber_appsequence(6) - pkt = [ next_msgid.to_ber, request ].to_ber_sequence - @conn.write pkt - - (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 7) or raise Net::LDAP::LdapError, "response missing or invalid" - - pdu - end - - #-- - # TODO: need to support a time limit, in case the server fails to respond. - # Unlike other operation-methods in this class, we return a result hash - # rather than a simple result number. This is experimental, and eventually - # we'll want to do this with all the others. The point is to have access - # to the error message and the matched-DN returned by the server. - #++ - def add(args) - add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN" - add_attrs = [] - a = args[:attributes] and a.each { |k, v| - add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence - } - - request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) - pkt = [next_msgid.to_ber, request].to_ber_sequence - @conn.write pkt - - (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && - (pdu = Net::LDAP::PDU.new(be)) && - (pdu.app_tag == 9) or - raise Net::LDAP::LdapError, "response missing or invalid" - - pdu - end - - #-- - # TODO: need to support a time limit, in case the server fails to respond. - #++ - def rename(args) - old_dn = args[:olddn] or raise "Unable to rename empty DN" - new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" - delete_attrs = args[:delete_attributes] ? true : false - new_superior = args[:new_superior] - - request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber] - request << new_superior.to_ber_contextspecific(0) unless new_superior == nil - - pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence - @conn.write pkt - - (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && - (pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == 13) or - raise Net::LDAP::LdapError.new( "response missing or invalid" ) - - pdu + # Delete Self + unless delete(dn: args[:dn]) + raise Net::LDAP::Error, get_operation_result[:error_message].to_s + end + true end - #-- - # TODO, need to support a time limit, in case the server fails to respond. - #++ - def delete(args) - dn = args[:dn] or raise "Unable to delete empty DN" - controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later - request = dn.to_s.to_ber_application_string(10) - pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence - @conn.write pkt - - (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 11) or raise Net::LDAP::LdapError, "response missing or invalid" - - pdu - end -end # class Connection +end # class LDAP diff --git a/lib/net/ldap/auth_adapter.rb b/lib/net/ldap/auth_adapter.rb new file mode 100644 index 00000000..f74232d1 --- /dev/null +++ b/lib/net/ldap/auth_adapter.rb @@ -0,0 +1,29 @@ +module Net + class LDAP + class AuthAdapter + def self.register(names, adapter) + names = Array(names) + @adapters ||= {} + names.each do |name| + @adapters[name] = adapter + end + end + + def self.[](name) + a = @adapters[name] + if a.nil? + raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{name})" + end + return a + end + + def initialize(conn) + @connection = conn + end + + def bind + raise "bind method must be overwritten" + end + end + end +end diff --git a/lib/net/ldap/auth_adapter/gss_spnego.rb b/lib/net/ldap/auth_adapter/gss_spnego.rb new file mode 100644 index 00000000..b4c3e519 --- /dev/null +++ b/lib/net/ldap/auth_adapter/gss_spnego.rb @@ -0,0 +1,41 @@ +require_relative '../auth_adapter' +require_relative 'sasl' + +module Net + class LDAP + module AuthAdapers + #-- + # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. + # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to + # integrate it without introducing an external dependency. + # + # This authentication method is accessed by calling #bind with a :method + # parameter of :gss_spnego. It requires :username and :password + # attributes, just like the :simple authentication method. It performs a + # GSS-SPNEGO authentication with the server, which is presumed to be a + # Microsoft Active Directory. + #++ + class GSS_SPNEGO < Net::LDAP::AuthAdapter + def bind(auth) + require 'ntlm' + + user, psw = [auth[:username] || auth[:dn], auth[:password]] + raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless user && psw + + nego = proc do |challenge| + t2_msg = NTLM::Message.parse(challenge) + t3_msg = t2_msg.response({ :user => user, :password => psw }, + { :ntlmv2 => true }) + t3_msg.serialize + end + + Net::LDAP::AuthAdapter::Sasl.new(@connection).bind \ + :method => :sasl, + :mechanism => "GSS-SPNEGO", + :initial_credential => NTLM::Message::Type1.new.serialize, + :challenge_response => nego + end + end + end + end +end diff --git a/lib/net/ldap/auth_adapter/sasl.rb b/lib/net/ldap/auth_adapter/sasl.rb new file mode 100644 index 00000000..bfebfc94 --- /dev/null +++ b/lib/net/ldap/auth_adapter/sasl.rb @@ -0,0 +1,62 @@ +require_relative '../auth_adapter' + +module Net + class LDAP + class AuthAdapter + class Sasl < Net::LDAP::AuthAdapter + MAX_SASL_CHALLENGES = 10 + + #-- + # Required parameters: :mechanism, :initial_credential and + # :challenge_response + # + # Mechanism is a string value that will be passed in the SASL-packet's + # "mechanism" field. + # + # Initial credential is most likely a string. It's passed in the initial + # BindRequest that goes to the server. In some protocols, it may be empty. + # + # Challenge-response is a Ruby proc that takes a single parameter and + # returns an object that will typically be a string. The + # challenge-response block is called when the server returns a + # BindResponse with a result code of 14 (saslBindInProgress). The + # challenge-response block receives a parameter containing the data + # returned by the server in the saslServerCreds field of the LDAP + # BindResponse packet. The challenge-response block may be called multiple + # times during the course of a SASL authentication, and each time it must + # return a value that will be passed back to the server as the credential + # data in the next BindRequest packet. + #++ + def bind(auth) + mech, cred, chall = auth[:mechanism], auth[:initial_credential], + auth[:challenge_response] + raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless mech && cred && chall + + message_id = @connection.next_msgid + + n = 0 + loop do + sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) + request = [ + Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) + + @connection.send(:write, request, nil, message_id) + pdu = @connection.queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult + raise Net::LDAP::NoBindResultError, "no bind result" + end + + return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress + raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MAX_SASL_CHALLENGES) + + cred = chall.call(pdu.result_server_sasl_creds) + end + + raise Net::LDAP::SASLChallengeOverflowError, "why are we here?" + end + end + end + end +end diff --git a/lib/net/ldap/auth_adapter/simple.rb b/lib/net/ldap/auth_adapter/simple.rb new file mode 100644 index 00000000..8a753ea6 --- /dev/null +++ b/lib/net/ldap/auth_adapter/simple.rb @@ -0,0 +1,34 @@ +require_relative '../auth_adapter' + +module Net + class LDAP + class AuthAdapter + class Simple < AuthAdapter + def bind(auth) + user, psw = if auth[:method] == :simple + [auth[:username] || auth[:dn], auth[:password]] + else + ["", ""] + end + + raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless user && psw + + message_id = @connection.next_msgid + request = [ + Net::LDAP::Connection::LdapVersion.to_ber, user.to_ber, + psw.to_ber_contextspecific(0) + ].to_ber_appsequence(Net::LDAP::PDU::BindRequest) + + @connection.send(:write, request, nil, message_id) + pdu = @connection.queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult + raise Net::LDAP::NoBindResultError, "no bind result" + end + + pdu + end + end + end + end +end diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb new file mode 100644 index 00000000..f1a70b18 --- /dev/null +++ b/lib/net/ldap/connection.rb @@ -0,0 +1,750 @@ +# This is a private class used internally by the library. It should not +# be called by user code. +class Net::LDAP::Connection #:nodoc: + include Net::LDAP::Instrumentation + + # Seconds before failing for socket connect timeout + DefaultConnectTimeout = 5 + + LdapVersion = 3 + + # Initialize a connection to an LDAP server + # + # :server + # :hosts Array of tuples specifying host, port + # :host host + # :port port + # :socket prepared socket + # + def initialize(server = {}) + @server = server + @instrumentation_service = server[:instrumentation_service] + + # Allows tests to parameterize what socket class to use + @socket_class = server.fetch(:socket_class, DefaultSocket) + + yield self if block_given? + end + + def socket_class=(socket_class) + @socket_class = socket_class + end + + def prepare_socket(server, timeout=nil, hostname='127.0.0.1') + socket = server[:socket] + encryption = server[:encryption] + + @conn = socket + setup_encryption(encryption, timeout, hostname) if encryption + end + + def open_connection(server) + hosts = server[:hosts] + encryption = server[:encryption] + + timeout = server[:connect_timeout] || DefaultConnectTimeout + socket_opts = { + connect_timeout: timeout, + } + + errors = [] + hosts.each do |host, port| + begin + prepare_socket(server.merge(socket: @socket_class.new(host, port, socket_opts)), timeout, host) + if encryption + if encryption[:tls_options] && + encryption[:tls_options][:verify_mode] && + encryption[:tls_options][:verify_mode] == OpenSSL::SSL::VERIFY_NONE + warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'" + else + @conn.post_connection_check(host) + end + end + return + rescue Net::LDAP::Error, SocketError, SystemCallError, + OpenSSL::SSL::SSLError => e + # Ensure the connection is closed in the event a setup failure. + close + errors << [e, host, port] + end + end + + raise Net::LDAP::ConnectionError.new(errors) + end + + module GetbyteForSSLSocket + def getbyte + c = getc + c && c.ord + end + end + + module FixSSLSocketSyncClose + def close + super + io.close + end + end + + def self.wrap_with_ssl(io, tls_options = {}, timeout=nil, hostname=nil) + raise Net::LDAP::NoOpenSSLError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL + + ctx = OpenSSL::SSL::SSLContext.new + + # By default, we do not verify certificates. For a 1.0 release, this should probably be changed at some point. + # See discussion in https://github.com/ruby-ldap/ruby-net-ldap/pull/161 + ctx.set_params(tls_options) unless tls_options.empty? + + conn = OpenSSL::SSL::SSLSocket.new(io, ctx) + conn.hostname = hostname + + begin + if timeout + conn.connect_nonblock + else + conn.connect + end + rescue IO::WaitReadable + raise Errno::ETIMEDOUT, "OpenSSL connection read timeout" unless + IO.select([conn], nil, nil, timeout) + retry + rescue IO::WaitWritable + raise Errno::ETIMEDOUT, "OpenSSL connection write timeout" unless + IO.select(nil, [conn], nil, timeout) + retry + end + + # Doesn't work: + # conn.sync_close = true + + conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte) + conn.extend(FixSSLSocketSyncClose) + + conn + end + + #-- + # Helper method called only from prepare_socket or open_connection, and only + # after we have a successfully-opened @conn instance variable, which is a TCP + # connection. Depending on the received arguments, we establish SSL, + # potentially replacing the value of @conn accordingly. Don't generate any + # errors here if no encryption is requested. DO raise Net::LDAP::Error objects + # if encryption is requested and we have trouble setting it up. That includes + # if OpenSSL is not set up on the machine. (Question: how does the Ruby + # OpenSSL wrapper react in that case?) DO NOT filter exceptions raised by the + # OpenSSL library. Let them pass back to the user. That should make it easier + # for us to debug the problem reports. Presumably (hopefully?) that will also + # produce recognizable errors if someone tries to use this on a machine + # without OpenSSL. + # + # The simple_tls method is intended as the simplest, stupidest, easiest + # solution for people who want nothing more than encrypted comms with the + # LDAP server. It doesn't do any server-cert validation and requires + # nothing in the way of key files and root-cert files, etc etc. OBSERVE: + # WE REPLACE the value of @conn, which is presumed to be a connected + # TCPSocket object. + # + # The start_tls method is supported by many servers over the standard LDAP + # port. It does not require an alternative port for encrypted + # communications, as with simple_tls. Thanks for Kouhei Sutou for + # generously contributing the :start_tls path. + #++ + def setup_encryption(args, timeout=nil, hostname=nil) + args[:tls_options] ||= {} + case args[:method] + when :simple_tls + @conn = self.class.wrap_with_ssl(@conn, args[:tls_options], timeout, hostname) + # additional branches requiring server validation and peer certs, etc. + # go here. + when :start_tls + message_id = next_msgid + request = [ + Net::LDAP::StartTlsOid.to_ber_contextspecific(0), + ].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) + + write(request, nil, message_id) + pdu = queued_read(message_id) + + if pdu.nil? || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse + raise Net::LDAP::NoStartTLSResultError, "no start_tls result" + end + + raise Net::LDAP::StartTLSError, + "start_tls failed: #{pdu.result_code}" unless pdu.result_code.zero? + @conn = self.class.wrap_with_ssl(@conn, args[:tls_options], timeout, hostname) + else + raise Net::LDAP::EncMethodUnsupportedError, "unsupported encryption method #{args[:method]}" + end + end + + #-- + # This is provided as a convenience method to make sure a connection + # object gets closed without waiting for a GC to happen. Clients shouldn't + # have to call it, but perhaps it will come in handy someday. + #++ + def close + return if !defined?(@conn) || @conn.nil? + @conn.close + @conn = nil + end + + # Internal: Reads messages by ID from a queue, falling back to reading from + # the connected socket until a message matching the ID is read. Any messages + # with mismatched IDs gets queued for subsequent reads by the origin of that + # message ID. + # + # Returns a Net::LDAP::PDU object or nil. + def queued_read(message_id) + if pdu = message_queue[message_id].shift + return pdu + end + + # read messages until we have a match for the given message_id + while pdu = read + return pdu if pdu.message_id == message_id + + message_queue[pdu.message_id].push pdu + next + end + + pdu + end + + # Internal: The internal queue of messages, read from the socket, grouped by + # message ID. + # + # Used by `queued_read` to return messages sent by the server with the given + # ID. If no messages are queued for that ID, `queued_read` will `read` from + # the socket and queue messages that don't match the given ID for other + # readers. + # + # Returns the message queue Hash. + def message_queue + @message_queue ||= Hash.new do |hash, key| + hash[key] = [] + end + end + + # Internal: Reads and parses data from the configured connection. + # + # - syntax: the BER syntax to use to parse the read data with + # + # Returns parsed Net::LDAP::PDU object. + def read(syntax = Net::LDAP::AsnSyntax) + ber_object = + instrument "read.net_ldap_connection", :syntax => syntax do |payload| + socket.read_ber(syntax) do |id, content_length| + payload[:object_type_id] = id + payload[:content_length] = content_length + end + end + + return unless ber_object + + instrument "parse_pdu.net_ldap_connection" do |payload| + pdu = payload[:pdu] = Net::LDAP::PDU.new(ber_object) + + payload[:message_id] = pdu.message_id + payload[:app_tag] = pdu.app_tag + + pdu + end + end + private :read + + # Internal: Write a BER formatted packet with the next message id to the + # configured connection. + # + # - request: required BER formatted request + # - controls: optional BER formatted controls + # + # Returns the return value from writing to the connection, which in some + # cases is the Integer number of bytes written to the socket. + def write(request, controls = nil, message_id = next_msgid) + instrument "write.net_ldap_connection" do |payload| + packet = [message_id.to_ber, request, controls].compact.to_ber_sequence + payload[:content_length] = socket.write(packet) + end + end + private :write + + def next_msgid + @msgid ||= 0 + @msgid += 1 + end + + def bind(auth) + instrument "bind.net_ldap_connection" do |payload| + payload[:method] = meth = auth[:method] + adapter = Net::LDAP::AuthAdapter[meth] + adapter.new(self).bind(auth) + end + end + + #-- + # Allow the caller to specify a sort control + # + # The format of the sort control needs to be: + # + # :sort_control => ["cn"] # just a string + # or + # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false) + # or + # :sort_control => ["givenname","sn"] #multiple strings or arrays + # + def encode_sort_controls(sort_definitions) + return sort_definitions unless sort_definitions + + sort_control_values = sort_definitions.map do |control| + control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder + control[0] = String(control[0]).to_ber, + control[1] = String(control[1]).to_ber, + control[2] = (control[2] == true).to_ber + control.to_ber_sequence + end + [ + Net::LDAP::LDAPControls::SORT_REQUEST.to_ber, + false.to_ber, + sort_control_values.to_ber_sequence.to_s.to_ber, + ].to_ber_sequence + end + + #-- + # Alternate implementation, this yields each search entry to the caller as + # it are received. + # + # TODO: certain search parameters are hardcoded. + # TODO: if we mis-parse the server results or the results are wrong, we + # can block forever. That's because we keep reading results until we get a + # type-5 packet, which might never come. We need to support the time-limit + # in the protocol. + #++ + def search(args = nil) + args ||= {} + + # filtering, scoping, search base + # filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7 + # base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1 + # scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2 + filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*") + base = args[:base] + scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree + + # attr handling + # attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8 + # attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6 + attrs = Array(args[:attributes]) + attrs_only = args[:attributes_only] == true + + # references + # refs: https://tools.ietf.org/html/rfc4511#section-4.5.3 + # deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3 + refs = args[:return_referrals] == true + deref = args[:deref] || Net::LDAP::DerefAliases_Never + + # limiting, paging, sorting + # size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4 + # time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5 + size = args[:size].to_i + time = args[:time].to_i + paged = args[:paged_searches_supported] + sort = args.fetch(:sort_controls, false) + + # arg validation + raise ArgumentError, "search base is required" unless base + raise ArgumentError, "invalid search-size" unless size >= 0 + raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope) + raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref) + + # arg transforms + filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String) + ber_attrs = attrs.map { |attr| attr.to_s.to_ber } + ber_sort = encode_sort_controls(sort) + + # An interesting value for the size limit would be close to A/D's + # built-in page limit of 1000 records, but openLDAP newer than version + # 2.2.0 chokes on anything bigger than 126. You get a silent error that + # is easily visible by running slapd in debug mode. Go figure. + # + # Changed this around 06Sep06 to support a caller-specified search-size + # limit. Because we ALWAYS do paged searches, we have to work around the + # problem that it's not legal to specify a "normal" sizelimit (in the + # body of the search request) that is larger than the page size we're + # requesting. Unfortunately, I have the feeling that this will break + # with LDAP servers that don't support paged searches!!! + # + # (Because we pass zero as the sizelimit on search rounds when the + # remaining limit is larger than our max page size of 126. In these + # cases, I think the caller's search limit will be ignored!) + # + # CONFIRMED: This code doesn't work on LDAPs that don't support paged + # searches when the size limit is larger than 126. We're going to have + # to do a root-DSE record search and not do a paged search if the LDAP + # doesn't support it. Yuck. + rfc2696_cookie = [126, ""] + result_pdu = nil + n_results = 0 + + message_id = next_msgid + + instrument "search.net_ldap_connection", + message_id: message_id, + filter: filter, + base: base, + scope: scope, + size: size, + time: time, + sort: sort, + referrals: refs, + deref: deref, + attributes: attrs do |payload| + loop do + # should collect this into a private helper to clarify the structure + query_limit = 0 + if size > 0 + query_limit = if paged + (((size - n_results) < 126) ? (size - n_results) : 0) + else + size + end + end + + request = [ + base.to_ber, + scope.to_ber_enumerated, + deref.to_ber_enumerated, + query_limit.to_ber, # size limit + time.to_ber, + attrs_only.to_ber, + filter.to_ber, + ber_attrs.to_ber_sequence, + ].to_ber_appsequence(Net::LDAP::PDU::SearchRequest) + + # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory + # this breaks when calling to_ber. (Can't force binary data to UTF-8) + # we have to disable paging (even though server supports it) to get around this... + + user_controls = args.fetch(:controls, []) + controls = [] + controls << + [ + Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber, + # Criticality MUST be false to interoperate with normal LDAPs. + false.to_ber, + rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber, + ].to_ber_sequence if paged + controls << ber_sort if ber_sort + if controls.empty? && user_controls.empty? + controls = nil + else + controls += user_controls + controls = controls.to_ber_contextspecific(0) + end + + write(request, controls, message_id) + + result_pdu = nil + controls = [] + + while pdu = queued_read(message_id) + case pdu.app_tag + when Net::LDAP::PDU::SearchReturnedData + n_results += 1 + yield pdu.search_entry if block_given? + when Net::LDAP::PDU::SearchResultReferral + if refs + if block_given? + se = Net::LDAP::Entry.new + se[:search_referrals] = (pdu.search_referrals || []) + yield se + end + end + when Net::LDAP::PDU::SearchResult + result_pdu = pdu + controls = pdu.result_controls + if refs && pdu.result_code == Net::LDAP::ResultCodeReferral + if block_given? + se = Net::LDAP::Entry.new + se[:search_referrals] = (pdu.search_referrals || []) + yield se + end + end + break + else + raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}" + end + end + + if result_pdu.nil? + raise Net::LDAP::ResponseMissingOrInvalidError, "response missing" + end + + # count number of pages of results + payload[:page_count] ||= 0 + payload[:page_count] += 1 + + # When we get here, we have seen a type-5 response. If there is no + # error AND there is an RFC-2696 cookie, then query again for the next + # page of results. If not, we're done. Don't screw this up or we'll + # break every search we do. + # + # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't + # that have a parameter of AsnSyntax? Does this just accidentally + # work? According to RFC-2696, the value expected in this position is + # of type OCTET STRING, covered in the default syntax supported by + # read_ber, so I guess we're ok. + more_pages = false + if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls + controls.each do |c| + if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS + # just in case some bogus server sends us more than 1 of these. + more_pages = false + if c.value and c.value.length > 0 + cookie = c.value.read_ber[1] + if cookie and cookie.length > 0 + rfc2696_cookie[1] = cookie + more_pages = true + end + end + end + end + end + + break unless more_pages + end # loop + + # track total result count + payload[:result_count] = n_results + + result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search") + end # instrument + ensure + + # clean up message queue for this search + messages = message_queue.delete(message_id) + + # in the exceptional case some messages were *not* consumed from the queue, + # instrument the event but do not fail. + if !messages.nil? && !messages.empty? + instrument "search_messages_unread.net_ldap_connection", + message_id: message_id, messages: messages + end + end + + MODIFY_OPERATIONS = { #:nodoc: + :add => 0, + :delete => 1, + :replace => 2, + } + + def self.modify_ops(operations) + ops = [] + if operations + operations.each do |op, attrib, values| + # TODO, fix the following line, which gives a bogus error if the + # opcode is invalid. + op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated + values = [values].flatten.map { |v| v.to_ber if v }.to_ber_set + values = [attrib.to_s.to_ber, values].to_ber_sequence + ops << [op_ber, values].to_ber + end + end + ops + end + + #-- + # TODO: need to support a time limit, in case the server fails to respond. + # TODO: We're throwing an exception here on empty DN. Should return a + # proper error instead, probaby from farther up the chain. + # TODO: If the user specifies a bogus opcode, we'll throw a confusing + # error here ("to_ber_enumerated is not defined on nil"). + #++ + def modify(args) + modify_dn = args[:dn] or raise "Unable to modify empty DN" + ops = self.class.modify_ops args[:operations] + + message_id = next_msgid + request = [ + modify_dn.to_ber, + ops.to_ber_sequence, + ].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest) + + controls = args.fetch(:controls, nil) + unless controls.nil? + controls = controls.to_ber_contextspecific(0) + end + + write(request, controls, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse + raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid" + end + + pdu + end + + ## + # Password Modify + # + # http://tools.ietf.org/html/rfc3062 + # + # passwdModifyOID OBJECT IDENTIFIER ::= 1.3.6.1.4.1.4203.1.11.1 + # + # PasswdModifyRequestValue ::= SEQUENCE { + # userIdentity [0] OCTET STRING OPTIONAL + # oldPasswd [1] OCTET STRING OPTIONAL + # newPasswd [2] OCTET STRING OPTIONAL } + # + # PasswdModifyResponseValue ::= SEQUENCE { + # genPasswd [0] OCTET STRING OPTIONAL } + # + # Encoded request: + # + # 00\x02\x01\x02w+\x80\x171.3.6.1.4.1.4203.1.11.1\x81\x100\x0E\x81\x05old\x82\x05new + # + def password_modify(args) + dn = args[:dn] + raise ArgumentError, 'DN is required' if !dn || dn.empty? + + ext_seq = [Net::LDAP::PasswdModifyOid.to_ber_contextspecific(0)] + + pwd_seq = [] + pwd_seq << dn.to_ber(0x80) + pwd_seq << args[:old_password].to_ber(0x81) unless args[:old_password].nil? + pwd_seq << args[:new_password].to_ber(0x82) unless args[:new_password].nil? + ext_seq << pwd_seq.to_ber_sequence.to_ber(0x81) + + request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) + + message_id = next_msgid + + write(request, nil, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse + raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid" + end + + pdu + end + + #-- + # TODO: need to support a time limit, in case the server fails to respond. + # Unlike other operation-methods in this class, we return a result hash + # rather than a simple result number. This is experimental, and eventually + # we'll want to do this with all the others. The point is to have access + # to the error message and the matched-DN returned by the server. + #++ + def add(args) + add_dn = args[:dn] or raise Net::LDAP::EmptyDNError, "Unable to add empty DN" + add_attrs = [] + a = args[:attributes] and a.each do |k, v| + add_attrs << [k.to_s.to_ber, Array(v).map(&:to_ber).to_ber_set].to_ber_sequence + end + + message_id = next_msgid + request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest) + + controls = args.fetch(:controls, nil) + unless controls.nil? + controls = controls.to_ber_contextspecific(0) + end + + write(request, controls, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse + raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid" + end + + pdu + end + + #-- + # TODO: need to support a time limit, in case the server fails to respond. + #++ + def rename(args) + old_dn = args[:olddn] or raise "Unable to rename empty DN" + new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" + delete_attrs = args[:delete_attributes] ? true : false + new_superior = args[:new_superior] + + message_id = next_msgid + request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber] + request << new_superior.to_ber_contextspecific(0) unless new_superior == nil + + write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest), nil, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyRDNResponse + raise Net::LDAP::ResponseMissingOrInvalidError.new "response missing or invalid" + end + + pdu + end + + #-- + # TODO, need to support a time limit, in case the server fails to respond. + #++ + def delete(args) + dn = args[:dn] or raise "Unable to delete empty DN" + controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later + message_id = next_msgid + request = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest) + + write(request, controls, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::DeleteResponse + raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid" + end + + pdu + end + + def ldapwhoami + ext_seq = [Net::LDAP::WhoamiOid.to_ber_contextspecific(0)] + request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) + + message_id = next_msgid + + write(request, nil, message_id) + pdu = queued_read(message_id) + + if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse + raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid" + end + + pdu + end + + # Internal: Returns a Socket like object used internally to communicate with + # LDAP server. + # + # Typically a TCPSocket, but can be a OpenSSL::SSL::SSLSocket + def socket + return @conn if defined?(@conn) && !@conn.nil? + + # First refactoring uses the existing methods open_connection and + # prepare_socket to set @conn. Next cleanup would centralize connection + # handling here. + if @server[:socket] + prepare_socket(@server) + else + @server[:hosts] = [[@server[:host], @server[:port]]] if @server[:hosts].nil? + open_connection(@server) + end + + @conn + end + + private + + # Wrap around Socket.tcp to normalize with other Socket initializers + class DefaultSocket + def self.new(host, port, socket_opts = {}) + Socket.tcp(host, port, **socket_opts) + end + end +end # class Connection diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb index ffdee11f..bc225e89 100644 --- a/lib/net/ldap/dataset.rb +++ b/lib/net/ldap/dataset.rb @@ -4,11 +4,13 @@ # to and from LDIF strings and Net::LDAP::Entry objects. class Net::LDAP::Dataset < Hash ## - # Dataset object comments. - attr_reader :comments + # Dataset object version, comments. + attr_accessor :version + attr_reader :comments def initialize(*args, &block) # :nodoc: super + @version = nil @comments = [] end @@ -17,11 +19,17 @@ def initialize(*args, &block) # :nodoc: # entries. def to_ldif ary = [] + + if version + ary << "version: #{version}" + ary << "" + end + ary += @comments unless @comments.empty? keys.sort.each do |dn| ary << "dn: #{dn}" - attributes = self[dn].keys.map { |attr| attr.to_s }.sort + attributes = self[dn].keys.map(&:to_s).sort attributes.each do |attr| self[dn][attr.to_sym].each do |value| if attr == "userpassword" or value_is_binary?(value) @@ -95,7 +103,7 @@ def gets # with the conversion of def from_entry(entry) dataset = Net::LDAP::Dataset.new - hash = { } + hash = {} entry.each_attribute do |attribute, value| next if attribute == :dn hash[attribute] = value @@ -125,9 +133,15 @@ def read_ldif(io) if line =~ /^#/ ds.comments << line yield :comment, line if block_given? - elsif line =~ /^dn:[\s]*/i - dn = $' - ds[dn] = Hash.new { |k,v| k[v] = [] } + elsif line =~ /^version:[\s]*([0-9]+)$/i + ds.version = $1 + yield :version, line if block_given? + elsif line =~ /^dn:([\:]?)[\s]*/i + # $1 is a colon if the dn-value is base-64 encoded + # $' is the dn-value + # Avoid the Base64 class because not all Ruby versions have it. + dn = ($1 == ":") ? $'.unpack('m').shift : $' + ds[dn] = Hash.new { |k, v| k[v] = [] } yield :dn, dn if block_given? elsif line.empty? dn = nil @@ -150,5 +164,3 @@ def read_ldif(io) end end end - -require 'net/ldap/entry' unless defined? Net::LDAP::Entry diff --git a/lib/net/ldap/dn.rb b/lib/net/ldap/dn.rb index 3037eefd..9098cdb9 100644 --- a/lib/net/ldap/dn.rb +++ b/lib/net/ldap/dn.rb @@ -57,19 +57,19 @@ def each_pair state = :key_oid key << char when ' ' then state = :key - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end when :key_normal then case char when '=' then state = :value when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end when :key_oid then case char when '=' then state = :value when '0'..'9', '.', ' ' then key << char - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end when :value then case char @@ -81,7 +81,7 @@ def each_pair value << char when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, value.string key = StringIO.new value = StringIO.new; else @@ -93,7 +93,7 @@ def each_pair when '\\' then state = :value_normal_escape when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, value.string key = StringIO.new value = StringIO.new; else value << char @@ -110,7 +110,7 @@ def each_pair when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal value << "#{hex_buffer}#{char}".to_i(16).chr - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end when :value_quoted then case char @@ -132,7 +132,7 @@ def each_pair when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_quoted value << "#{hex_buffer}#{char}".to_i(16).chr - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end when :value_hexstring then case char @@ -142,38 +142,37 @@ def each_pair when ' ' then state = :value_end when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, value.string key = StringIO.new value = StringIO.new; - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end when :value_hexstring_hex then case char when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_hexstring value << char - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end when :value_end then case char when ' ' then state = :value_end when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, value.string key = StringIO.new value = StringIO.new; - else raise "DN badly formed" + else raise Net::LDAP::InvalidDNError, "DN badly formed" end - else raise "Fell out of state machine" + else raise Net::LDAP::InvalidDNError, "Fell out of state machine" end end # Last pair - if [:value, :value_normal, :value_hexstring, :value_end].include? state - yield key.string.strip, value.string.rstrip - else - raise "DN badly formed" - end + raise Net::LDAP::InvalidDNError, "DN badly formed" unless + [:value, :value_normal, :value_hexstring, :value_end].include? state + + yield key.string.strip, value.string end ## @@ -193,27 +192,19 @@ def to_s # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions # for dn values. All of the following must be escaped in any normal string # using a single backslash ('\') as escape. - ESCAPES = { - ',' => ',', - '+' => '+', - '"' => '"', - '\\' => '\\', - '<' => '<', - '>' => '>', - ';' => ';', - } + ESCAPES = %w[, + " \\ < > ;] - # Compiled character class regexp using the keys from the above hash, and + # Compiled character class regexp using the values from the above list, and # checking for a space or # at the start, or space at the end, of the # string. ESCAPE_RE = Regexp.new("(^ |^#| $|[" + - ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + ESCAPES.map { |e| Regexp.escape(e) }.join + "])") ## # Escape a string for use in a DN value def self.escape(string) - string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] } + string.gsub(ESCAPE_RE) { |char| "\\" + char } end ## diff --git a/lib/net/ldap/entry.rb b/lib/net/ldap/entry.rb index 616ffe7f..18668892 100644 --- a/lib/net/ldap/entry.rb +++ b/lib/net/ldap/entry.rb @@ -71,7 +71,7 @@ def from_single_ldif_string(ldif) return nil if ds.empty? - raise Net::LDAP::LdapError, "Too many LDIF entries" unless ds.size == 1 + raise Net::LDAP::EntryOverflowError, "Too many LDIF entries" unless ds.size == 1 entry = ds.to_entries.first @@ -113,6 +113,14 @@ def [](name) @myhash[name] || [] end + ## + # Read the first value for the provided attribute. The attribute name + # is canonicalized prior to reading. Returns nil if the attribute does + # not exist. + def first(name) + self[name].first + end + ## # Returns the first distinguished name (dn) of the Entry as a \String. def dn @@ -125,6 +133,13 @@ def attribute_names @myhash.keys end + ## + # Creates a duplicate of the internal Hash containing the attributes + # of the entry. + def to_h + @myhash.dup + end + ## # Accesses each of the attributes present in the Entry. # @@ -132,11 +147,10 @@ def attribute_names # arguments to the block: a Symbol giving the name of the attribute, and a # (possibly empty) \Array of data values. def each # :yields: attribute-name, data-values-array - if block_given? - attribute_names.each {|a| - attr_name,values = a,self[a] - yield attr_name, values - } + return unless block_given? + attribute_names.each do|a| + attr_name, values = a, self[a] + yield attr_name, values end end alias_method :each_attribute, :each @@ -147,7 +161,7 @@ def to_ldif Net::LDAP::Dataset.from_entry(self).to_ldif_string end - def respond_to?(sym) #:nodoc: + def respond_to?(sym, include_all = false) #:nodoc: return true if valid_attribute?(self.class.attribute_name(sym)) return super end @@ -180,6 +194,8 @@ def setter?(sym) sym.to_s[-1] == ?= end private :setter? -end # class Entry -require 'net/ldap/dataset' unless defined? Net::LDAP::Dataset + def ==(other) + other.instance_of?(self.class) && @myhash == other.to_h + end +end # class Entry diff --git a/lib/net/ldap/error.rb b/lib/net/ldap/error.rb new file mode 100644 index 00000000..49a338d6 --- /dev/null +++ b/lib/net/ldap/error.rb @@ -0,0 +1,49 @@ +class Net::LDAP + class Error < StandardError; end + + class AlreadyOpenedError < Error; end + class SocketError < Error; end + class ConnectionError < Error + def self.new(errors) + error = errors.first.first + if errors.size == 1 + return error if error.is_a? Errno::ECONNREFUSED + + return Net::LDAP::Error.new(error.message) + end + + super + end + + def initialize(errors) + message = "Unable to connect to any given server: \n #{errors.map { |e, h, p| "#{e.class}: #{e.message} (#{h}:#{p})" }.join("\n ")}" + super(message) + end + end + class NoOpenSSLError < Error; end + class NoStartTLSResultError < Error; end + class NoSearchBaseError < Error; end + class StartTLSError < Error; end + class EncryptionUnsupportedError < Error; end + class EncMethodUnsupportedError < Error; end + class AuthMethodUnsupportedError < Error; end + class BindingInformationInvalidError < Error; end + class NoBindResultError < Error; end + class SASLChallengeOverflowError < Error; end + class SearchSizeInvalidError < Error; end + class SearchScopeInvalidError < Error; end + class ResponseTypeInvalidError < Error; end + class ResponseMissingOrInvalidError < Error; end + class EmptyDNError < Error; end + class InvalidDNError < Error; end + class HashTypeUnsupportedError < Error; end + class OperatorError < Error; end + class SubstringFilterError < Error; end + class SearchFilterError < Error; end + class BERInvalidError < Error; end + class SearchFilterTypeUnknownError < Error; end + class BadAttributeError < Error; end + class FilterTypeUnknownError < Error; end + class FilterSyntaxInvalidError < Error; end + class EntryOverflowError < Error; end +end diff --git a/lib/net/ldap/filter.rb b/lib/net/ldap/filter.rb index 66610010..dc0d0ab3 100644 --- a/lib/net/ldap/filter.rb +++ b/lib/net/ldap/filter.rb @@ -23,11 +23,11 @@ class Net::LDAP::Filter ## # Known filter types. - FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex, :bineq ] + FilterTypes = [:ne, :eq, :ge, :le, :and, :or, :not, :ex, :bineq] def initialize(op, left, right) #:nodoc: unless FilterTypes.include?(op) - raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter." + raise Net::LDAP::OperatorError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter." end @op = op @left = left @@ -65,22 +65,22 @@ def eq(attribute, value) new(:eq, attribute, value) end - ## - # Creates a Filter object indicating a binary comparison. - # this prevents the search data from being forced into a UTF-8 string. - # - # This is primarily used for Microsoft Active Directory to compare - # GUID values. - # - # # for guid represented as hex charecters - # guid = "6a31b4a12aa27a41aca9603f27dd5116" - # guid_bin = [guid].pack("H*") - # f = Net::LDAP::Filter.bineq("objectGUID", guid_bin) - # - # This filter does not perform any escaping. - def bineq(attribute, value) - new(:bineq, attribute, value) - end + ## + # Creates a Filter object indicating a binary comparison. + # this prevents the search data from being forced into a UTF-8 string. + # + # This is primarily used for Microsoft Active Directory to compare + # GUID values. + # + # # for guid represented as hex charecters + # guid = "6a31b4a12aa27a41aca9603f27dd5116" + # guid_bin = [guid].pack("H*") + # f = Net::LDAP::Filter.bineq("objectGUID", guid_bin) + # + # This filter does not perform any escaping. + def bineq(attribute, value) + new(:bineq, attribute, value) + end ## # Creates a Filter object indicating extensible comparison. This Filter @@ -242,7 +242,7 @@ def present?(attribute) # http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1 # charset for filters. All of the following must be escaped in any normal - # string using a single backslash ('\') as escape. + # string using a single backslash ('\') as escape. # ESCAPES = { "\0" => '00', # NUL = %x00 ; null character @@ -251,10 +251,10 @@ def present?(attribute) ')' => '29', # RPARENS = %x29 ; right parenthesis (")") '\\' => '5C', # ESC = %x5C ; esc (or backslash) ("\") } - # Compiled character class regexp using the keys from the above hash. + # Compiled character class regexp using the keys from the above hash. ESCAPE_RE = Regexp.new( - "[" + - ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + "[" + + ESCAPES.keys.map { |e| Regexp.escape(e) }.join + "]") ## @@ -287,10 +287,10 @@ def parse_ber(ber) when 0xa4 # context-specific constructed 4, "substring" str = "" final = false - ber.last.each { |b| + ber.last.each do |b| case b.ber_identifier when 0x80 # context-specific primitive 0, SubstringFilter "initial" - raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0 + raise Net::LDAP::SubstringFilterError, "Unrecognized substring filter; bad initial value." if str.length > 0 str += escape(b) when 0x81 # context-specific primitive 0, SubstringFilter "any" str += "*#{escape(b)}" @@ -298,7 +298,7 @@ def parse_ber(ber) str += "*#{escape(b)}" final = true end - } + end str += "*" unless final eq(ber.first.to_s, str) when 0xa5 # context-specific constructed 5, "greaterOrEqual" @@ -309,9 +309,9 @@ def parse_ber(ber) # call to_s to get rid of the BER-identifiedness of the incoming string. present?(ber.to_s) when 0xa9 # context-specific constructed 9, "extensible comparison" - raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2 - - # Reassembles the extensible filter parts + raise Net::LDAP::SearchFilterError, "Invalid extensible search filter, should be at least two elements" if ber.size < 2 + + # Reassembles the extensible filter parts # (["sn", "2.4.6.8.10", "Barbara Jones", '1']) type = value = dn = rule = nil ber.each do |element| @@ -327,10 +327,10 @@ def parse_ber(ber) attribute << type if type attribute << ":#{dn}" if dn attribute << ":#{rule}" if rule - + ex(attribute, value) else - raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter." + raise Net::LDAP::BERInvalidError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter." end end @@ -357,7 +357,7 @@ def parse_ldap_filter(obj) when 0xa3 # equalityMatch. context-specific constructed 3. eq(obj[0], obj[1]) else - raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}" + raise Net::LDAP::SearchFilterTypeUnknownError, "Unknown LDAP search-filter type: #{obj.ber_identifier}" end end end @@ -414,10 +414,8 @@ def to_raw_rfc2254 case @op when :ne "!(#{@left}=#{@right})" - when :eq + when :eq, :bineq "#{@left}=#{@right}" - when :bineq - "#{@left}=#{@right}" when :ex "#{@left}:=#{@right}" when :ge @@ -492,7 +490,7 @@ def to_ber when :eq if @right == "*" # presence test @left.to_s.to_ber_contextspecific(7) - elsif @right =~ /[*]/ # substring + elsif @right.to_s =~ /[*]/ # substring # Parsing substrings is a little tricky. We use String#split to # break a string into substrings delimited by the * (star) # character. But we also need to know whether there is a star at the @@ -527,14 +525,14 @@ def to_ber else # equality [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3) end - when :bineq - # make sure data is not forced to UTF-8 - [@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3) + when :bineq + # make sure data is not forced to UTF-8 + [@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3) when :ex seq = [] unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/ - raise Net::LDAP::LdapError, "Bad attribute #{@left}" + raise Net::LDAP::BadAttributeError, "Bad attribute #{@left}" end type, dn, rule = $1, $2, $4 @@ -552,10 +550,10 @@ def to_ber [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2) when :and ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten - ary.map {|a| a.to_ber}.to_ber_contextspecific(0) + ary.map(&:to_ber).to_ber_contextspecific(0) when :or ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten - ary.map {|a| a.to_ber}.to_ber_contextspecific(1) + ary.map(&:to_ber).to_ber_contextspecific(1) when :not [@left.to_ber].to_ber_contextspecific(2) end @@ -641,15 +639,21 @@ def match(entry) l = entry[@left] and l = Array(l) and l.index(@right) end else - raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}" + raise Net::LDAP::FilterTypeUnknownError, "Unknown filter type in match: #{@op}" end end ## # Converts escaped characters (e.g., "\\28") to unescaped characters - # ("("). + # @note slawson20170317: Don't attempt to unescape 16 byte binary data which we assume are objectGUIDs + # The binary form of 5936AE79-664F-44EA-BCCB-5C39399514C6 triggers a BINARY -> UTF-8 conversion error def unescape(right) - right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") } + right = right.to_s + if right.length == 16 && right.encoding == Encoding::BINARY + right + else + right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") } + end end private :unescape @@ -674,7 +678,7 @@ def parse(ldap_filter_string) def initialize(str) require 'strscan' # Don't load strscan until we need it. @filter = parse(StringScanner.new(str)) - raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter + raise Net::LDAP::FilterSyntaxInvalidError, "Invalid filter syntax." unless @filter end ## @@ -751,11 +755,11 @@ def parse_paren_expression(scanner) # This parses a given expression inside of parentheses. def parse_filter_branch(scanner) scanner.scan(/\s*/) - if token = scanner.scan(/[-\w:.]*[\w]/) + if token = scanner.scan(/[-\w:.;]*[\w]/) scanner.scan(/\s*/) if op = scanner.scan(/<=|>=|!=|:=|=/) scanner.scan(/\s*/) - if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!'\s\xC3\x80-\xCA\xAF]|[^\x00-\x7F]|\\[a-fA-F\d]{2})+/u) + if value = scanner.scan(/(?:[-\[\]{}\w*.+\/:@=,#\$%&!'^~\s\xC3\x80-\xCA\xAF]|[^\x00-\x7F]|\\[a-fA-F\d]{2})+/u) # 20100313 AZ: Assumes that "(uid=george*)" is the same as # "(uid=george* )". The standard doesn't specify, but I can find # no examples that suggest otherwise. diff --git a/lib/net/ldap/instrumentation.rb b/lib/net/ldap/instrumentation.rb new file mode 100644 index 00000000..d5cc6bf7 --- /dev/null +++ b/lib/net/ldap/instrumentation.rb @@ -0,0 +1,23 @@ +module Net::LDAP::Instrumentation + attr_reader :instrumentation_service + private :instrumentation_service + + # Internal: Instrument a block with the defined instrumentation service. + # + # Yields the event payload if a block is given. + # + # Skips instrumentation if no service is set. + # + # Returns the return value of the block. + def instrument(event, payload = {}) + payload = (payload || {}).dup + if instrumentation_service + instrumentation_service.instrument(event, payload) do |instr_payload| + instr_payload[:result] = yield(instr_payload) if block_given? + end + else + yield(payload) if block_given? + end + end + private :instrument +end diff --git a/lib/net/ldap/password.rb b/lib/net/ldap/password.rb index 9c873029..4a6a1ae7 100644 --- a/lib/net/ldap/password.rb +++ b/lib/net/ldap/password.rb @@ -1,7 +1,9 @@ # -*- ruby encoding: utf-8 -*- require 'digest/sha1' +require 'digest/sha2' require 'digest/md5' require 'base64' +require 'securerandom' class Net::LDAP::Password class << self @@ -18,20 +20,25 @@ class << self # * Should we provide sha1 as a synonym for sha1? I vote no because then # should you also provide ssha1 for symmetry? # - attribute_value = "" def generate(type, str) - case type - when :md5 - attribute_value = '{MD5}' + Base64.encode64(Digest::MD5.digest(str)).chomp! - when :sha - attribute_value = '{SHA}' + Base64.encode64(Digest::SHA1.digest(str)).chomp! - when :ssha - srand; salt = (rand * 1000).to_i.to_s - attribute_value = '{SSHA}' + Base64.encode64(Digest::SHA1.digest(str + salt) + salt).chomp! - else - raise Net::LDAP::LdapError, "Unsupported password-hash type (#{type})" - end - return attribute_value + case type + when :md5 + '{MD5}' + Base64.strict_encode64(Digest::MD5.digest(str)) + when :sha + '{SHA}' + Base64.strict_encode64(Digest::SHA1.digest(str)) + when :ssha + salt = SecureRandom.random_bytes(16) + digest = Digest::SHA1.new + digest << str << salt + '{SSHA}' + Base64.strict_encode64(digest.digest + salt) + when :ssha256 + salt = SecureRandom.random_bytes(16) + digest = Digest::SHA256.new + digest << str << salt + '{SSHA256}' + Base64.strict_encode64(digest.digest + salt) + else + raise Net::LDAP::HashTypeUnsupportedError, "Unsupported password-hash type (#{type})" + end end end end diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 26d4f8b8..83a609b7 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -18,24 +18,48 @@ # well with our approach. # # Currently, we only support controls on SearchResult. +# +# http://tools.ietf.org/html/rfc4511#section-4.1.1 +# http://tools.ietf.org/html/rfc4511#section-4.1.9 class Net::LDAP::PDU class Error < RuntimeError; end - ## - # This message packet is a bind request. + # http://tools.ietf.org/html/rfc4511#section-4.2 BindRequest = 0 + # http://tools.ietf.org/html/rfc4511#section-4.2.2 BindResult = 1 + # http://tools.ietf.org/html/rfc4511#section-4.3 UnbindRequest = 2 + # http://tools.ietf.org/html/rfc4511#section-4.5.1 SearchRequest = 3 + # http://tools.ietf.org/html/rfc4511#section-4.5.2 SearchReturnedData = 4 SearchResult = 5 + # see also SearchResultReferral (19) + # http://tools.ietf.org/html/rfc4511#section-4.6 + ModifyRequest = 6 ModifyResponse = 7 + # http://tools.ietf.org/html/rfc4511#section-4.7 + AddRequest = 8 AddResponse = 9 + # http://tools.ietf.org/html/rfc4511#section-4.8 + DeleteRequest = 10 DeleteResponse = 11 + # http://tools.ietf.org/html/rfc4511#section-4.9 + ModifyRDNRequest = 12 ModifyRDNResponse = 13 + # http://tools.ietf.org/html/rfc4511#section-4.10 + CompareRequest = 14 + CompareResponse = 15 + # http://tools.ietf.org/html/rfc4511#section-4.11 + AbandonRequest = 16 + # http://tools.ietf.org/html/rfc4511#section-4.5.2 SearchResultReferral = 19 + # http://tools.ietf.org/html/rfc4511#section-4.12 ExtendedRequest = 23 ExtendedResponse = 24 + # unused: http://tools.ietf.org/html/rfc4511#section-4.13 + IntermediateResponse = 25 ## # The LDAP packet message ID. @@ -50,6 +74,7 @@ class Error < RuntimeError; end attr_reader :search_referrals attr_reader :search_parameters attr_reader :bind_parameters + attr_reader :extended_response ## # Returns RFC-2251 Controls if any. @@ -96,9 +121,9 @@ def initialize(ber_object) when UnbindRequest parse_unbind_request(ber_object[1]) when ExtendedResponse - parse_ldap_result(ber_object[1]) + parse_extended_response(ber_object[1]) else - raise LdapPduError.new("unknown pdu-type: #{@app_tag}") + raise Error.new("unknown pdu-type: #{@app_tag}") end parse_controls(ber_object[2]) if ber_object[2] @@ -125,7 +150,7 @@ def result_code(code = :resultCode) end def status - result_code == 0 ? :success : :failure + Net::LDAP::ResultCodesNonError.include?(result_code) ? :success : :failure end def success? @@ -150,12 +175,35 @@ def parse_ldap_result(sequence) @ldap_result = { :resultCode => sequence[0], :matchedDN => sequence[1], - :errorMessage => sequence[2] + :errorMessage => sequence[2], } - parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == 10 + parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == Net::LDAP::ResultCodeReferral end private :parse_ldap_result + ## + # Parse an extended response + # + # http://www.ietf.org/rfc/rfc2251.txt + # + # Each Extended operation consists of an Extended request and an + # Extended response. + # + # ExtendedRequest ::= [APPLICATION 23] SEQUENCE { + # requestName [0] LDAPOID, + # requestValue [1] OCTET STRING OPTIONAL } + + def parse_extended_response(sequence) + sequence.length.between?(3, 5) or raise Net::LDAP::PDU::Error, "Invalid LDAP result length." + @ldap_result = { + :resultCode => sequence[0], + :matchedDN => sequence[1], + :errorMessage => sequence[2], + } + @extended_response = sequence.length == 3 ? nil : sequence.last + end + private :parse_extended_response + ## # A Bind Response may have an additional field, ID [7], serverSaslCreds, # per RFC 2251 pgh 4.2.3. diff --git a/lib/net/ldap/version.rb b/lib/net/ldap/version.rb index 1c5566e6..2caeaa5f 100644 --- a/lib/net/ldap/version.rb +++ b/lib/net/ldap/version.rb @@ -1,5 +1,5 @@ module Net class LDAP - VERSION = "0.5.1" + VERSION = "0.20.0" end end diff --git a/lib/net/snmp.rb b/lib/net/snmp.rb index 3f9e5fb1..f89fe267 100644 --- a/lib/net/snmp.rb +++ b/lib/net/snmp.rb @@ -1,270 +1,264 @@ # -*- ruby encoding: utf-8 -*- -require 'net/ldap/version' +require_relative 'ldap/version' # :stopdoc: module Net - class SNMP - VERSION = Net::LDAP::VERSION + class SNMP + VERSION = Net::LDAP::VERSION + AsnSyntax = Net::BER.compile_syntax({ + :application => { + :primitive => { + 1 => :integer, # Counter32, (RFC2578 sec 2) + 2 => :integer, # Gauge32 or Unsigned32, (RFC2578 sec 2) + 3 => :integer # TimeTicks32, (RFC2578 sec 2) + }, + :constructed => {}, + }, + :context_specific => { + :primitive => {}, + :constructed => { + 0 => :array, # GetRequest PDU (RFC1157 pgh 4.1.2) + 1 => :array, # GetNextRequest PDU (RFC1157 pgh 4.1.3) + 2 => :array # GetResponse PDU (RFC1157 pgh 4.1.4) + }, + }, + }) - AsnSyntax = Net::BER.compile_syntax({ - :application => { - :primitive => { - 1 => :integer, # Counter32, (RFC2578 sec 2) - 2 => :integer, # Gauge32 or Unsigned32, (RFC2578 sec 2) - 3 => :integer # TimeTicks32, (RFC2578 sec 2) - }, - :constructed => { - } - }, - :context_specific => { - :primitive => { - }, - :constructed => { - 0 => :array, # GetRequest PDU (RFC1157 pgh 4.1.2) - 1 => :array, # GetNextRequest PDU (RFC1157 pgh 4.1.3) - 2 => :array # GetResponse PDU (RFC1157 pgh 4.1.4) - } - } - }) - - # SNMP 32-bit counter. - # Defined in RFC1155 (Structure of Mangement Information), section 6. - # A 32-bit counter is an ASN.1 application [1] implicit unsigned integer - # with a range from 0 to 2^^32 - 1. - class Counter32 - def initialize value - @value = value - end - def to_ber - @value.to_ber_application(1) - end - end - - # SNMP 32-bit gauge. - # Defined in RFC1155 (Structure of Mangement Information), section 6. - # A 32-bit counter is an ASN.1 application [2] implicit unsigned integer. - # This is also indistinguishable from Unsigned32. (Need to alias them.) - class Gauge32 - def initialize value - @value = value - end - def to_ber - @value.to_ber_application(2) - end - end - - # SNMP 32-bit timer-ticks. - # Defined in RFC1155 (Structure of Mangement Information), section 6. - # A 32-bit counter is an ASN.1 application [3] implicit unsigned integer. - class TimeTicks32 - def initialize value - @value = value - end - def to_ber - @value.to_ber_application(3) - end - end + # SNMP 32-bit counter. + # Defined in RFC1155 (Structure of Mangement Information), section 6. + # A 32-bit counter is an ASN.1 application [1] implicit unsigned integer + # with a range from 0 to 2^^32 - 1. + class Counter32 + def initialize value + @value = value + end + def to_ber + @value.to_ber_application(1) + end end - class SnmpPdu - class Error < StandardError; end - - PduTypes = [ - :get_request, - :get_next_request, - :get_response, - :set_request, - :trap - ] - ErrorStatusCodes = { # Per RFC1157, pgh 4.1.1 - 0 => "noError", - 1 => "tooBig", - 2 => "noSuchName", - 3 => "badValue", - 4 => "readOnly", - 5 => "genErr" - } + # SNMP 32-bit gauge. + # Defined in RFC1155 (Structure of Mangement Information), section 6. + # A 32-bit counter is an ASN.1 application [2] implicit unsigned integer. + # This is also indistinguishable from Unsigned32. (Need to alias them.) + class Gauge32 + def initialize value + @value = value + end + def to_ber + @value.to_ber_application(2) + end + end - class << self - def parse ber_object - n = new - n.send :parse, ber_object - n - end - end + # SNMP 32-bit timer-ticks. + # Defined in RFC1155 (Structure of Mangement Information), section 6. + # A 32-bit counter is an ASN.1 application [3] implicit unsigned integer. + class TimeTicks32 + def initialize value + @value = value + end + def to_ber + @value.to_ber_application(3) + end + end + end - attr_reader :version, :community, :pdu_type, :variables, :error_status - attr_accessor :request_id, :error_index + class SnmpPdu + class Error < StandardError; end + PduTypes = [ + :get_request, + :get_next_request, + :get_response, + :set_request, + :trap, + ] + ErrorStatusCodes = { # Per RFC1157, pgh 4.1.1 + 0 => "noError", + 1 => "tooBig", + 2 => "noSuchName", + 3 => "badValue", + 4 => "readOnly", + 5 => "genErr", + } + class << self + def parse ber_object + n = new + n.send :parse, ber_object + n + end + end - def initialize args={} - @version = args[:version] || 0 - @community = args[:community] || "public" - @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value. - @error_status = args[:error_status] || 0 - @error_index = args[:error_index] || 0 - @variables = args[:variables] || [] - end + attr_reader :version, :community, :pdu_type, :variables, :error_status + attr_accessor :request_id, :error_index - #-- - def parse ber_object - begin - parse_ber_object ber_object - rescue Error - # Pass through any SnmpPdu::Error instances - raise $! - rescue - # Wrap any basic parsing error so it becomes a PDU-format error - raise Error.new( "snmp-pdu format error" ) - end - end - private :parse + def initialize args={} + @version = args[:version] || 0 + @community = args[:community] || "public" + @pdu_type = args[:pdu_type] # leave nil unless specified; there's no reasonable default value. + @error_status = args[:error_status] || 0 + @error_index = args[:error_index] || 0 + @variables = args[:variables] || [] + end - def parse_ber_object ber_object - send :version=, ber_object[0].to_i - send :community=, ber_object[1].to_s + #-- + def parse ber_object + begin + parse_ber_object ber_object + rescue Error + # Pass through any SnmpPdu::Error instances + raise $! + rescue + # Wrap any basic parsing error so it becomes a PDU-format error + raise Error.new( "snmp-pdu format error" ) + end + end + private :parse - data = ber_object[2] - case (app_tag = data.ber_identifier & 31) - when 0 - send :pdu_type=, :get_request - parse_get_request data - when 1 - send :pdu_type=, :get_next_request - # This PDU is identical to get-request except for the type. - parse_get_request data - when 2 - send :pdu_type=, :get_response - # This PDU is identical to get-request except for the type, - # the error_status and error_index values are meaningful, - # and the fact that the variable bindings will be non-null. - parse_get_response data - else - raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) - end - end - private :parse_ber_object + def parse_ber_object ber_object + send :version=, ber_object[0].to_i + send :community=, ber_object[1].to_s - #-- - # Defined in RFC1157, pgh 4.1.2. - def parse_get_request data - send :request_id=, data[0].to_i - # data[1] is error_status, always zero. - # data[2] is error_index, always zero. - send :error_status=, 0 - send :error_index=, 0 - data[3].each {|n,v| - # A variable-binding, of which there may be several, - # consists of an OID and a BER null. - # We're ignoring the null, we might want to verify it instead. - unless v.is_a?(Net::BER::BerIdentifiedNull) - raise Error.new(" invalid variable-binding in get-request" ) - end - add_variable_binding n, nil - } - end - private :parse_get_request + data = ber_object[2] + case (app_tag = data.ber_identifier & 31) + when 0 + send :pdu_type=, :get_request + parse_get_request data + when 1 + send :pdu_type=, :get_next_request + # This PDU is identical to get-request except for the type. + parse_get_request data + when 2 + send :pdu_type=, :get_response + # This PDU is identical to get-request except for the type, + # the error_status and error_index values are meaningful, + # and the fact that the variable bindings will be non-null. + parse_get_response data + else + raise Error.new( "unknown snmp-pdu type: #{app_tag}" ) + end + end + private :parse_ber_object - #-- - # Defined in RFC1157, pgh 4.1.4 - def parse_get_response data - send :request_id=, data[0].to_i - send :error_status=, data[1].to_i - send :error_index=, data[2].to_i - data[3].each {|n,v| - # A variable-binding, of which there may be several, - # consists of an OID and a BER null. - # We're ignoring the null, we might want to verify it instead. - add_variable_binding n, v - } - end - private :parse_get_response + #-- + # Defined in RFC1157, pgh 4.1.2. + def parse_get_request data + send :request_id=, data[0].to_i + # data[1] is error_status, always zero. + # data[2] is error_index, always zero. + send :error_status=, 0 + send :error_index=, 0 + data[3].each do |n, v| + # A variable-binding, of which there may be several, + # consists of an OID and a BER null. + # We're ignoring the null, we might want to verify it instead. + unless v.is_a?(Net::BER::BerIdentifiedNull) + raise Error.new(" invalid variable-binding in get-request" ) + end + add_variable_binding n, nil + end + end + private :parse_get_request + #-- + # Defined in RFC1157, pgh 4.1.4 + def parse_get_response data + send :request_id=, data[0].to_i + send :error_status=, data[1].to_i + send :error_index=, data[2].to_i + data[3].each do |n, v| + # A variable-binding, of which there may be several, + # consists of an OID and a BER null. + # We're ignoring the null, we might want to verify it instead. + add_variable_binding n, v + end + end + private :parse_get_response - def version= ver - unless [0,2].include?(ver) - raise Error.new("unknown snmp-version: #{ver}") - end - @version = ver - end - def pdu_type= t - unless PduTypes.include?(t) - raise Error.new("unknown pdu-type: #{t}") - end - @pdu_type = t - end + def version= ver + unless [0, 2].include?(ver) + raise Error.new("unknown snmp-version: #{ver}") + end + @version = ver + end - def error_status= es - unless ErrorStatusCodes.has_key?(es) - raise Error.new("unknown error-status: #{es}") - end - @error_status = es - end + def pdu_type= t + unless PduTypes.include?(t) + raise Error.new("unknown pdu-type: #{t}") + end + @pdu_type = t + end - def community= c - @community = c.to_s - end + def error_status= es + unless ErrorStatusCodes.key?(es) + raise Error.new("unknown error-status: #{es}") + end + @error_status = es + end - #-- - # Syntactic sugar - def add_variable_binding name, value=nil - @variables ||= [] - @variables << [name, value] - end + def community= c + @community = c.to_s + end - def to_ber_string - [ - version.to_ber, - community.to_ber, - pdu_to_ber_string - ].to_ber_sequence - end + #-- + # Syntactic sugar + def add_variable_binding name, value=nil + @variables ||= [] + @variables << [name, value] + end - #-- - # Helper method that returns a PDU payload in BER form, - # depending on the PDU type. - def pdu_to_ber_string - case pdu_type - when :get_request - [ - request_id.to_ber, - error_status.to_ber, - error_index.to_ber, - [ - @variables.map {|n,v| - [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence - } - ].to_ber_sequence - ].to_ber_contextspecific(0) - when :get_next_request - [ - request_id.to_ber, - error_status.to_ber, - error_index.to_ber, - [ - @variables.map {|n,v| - [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence - } - ].to_ber_sequence - ].to_ber_contextspecific(1) - when :get_response - [ - request_id.to_ber, - error_status.to_ber, - error_index.to_ber, - [ - @variables.map {|n,v| - [n.to_ber_oid, v.to_ber].to_ber_sequence - } - ].to_ber_sequence - ].to_ber_contextspecific(2) - else - raise Error.new( "unknown pdu-type: #{pdu_type}" ) - end - end - private :pdu_to_ber_string + def to_ber_string + [ + version.to_ber, + community.to_ber, + pdu_to_ber_string, + ].to_ber_sequence + end + #-- + # Helper method that returns a PDU payload in BER form, + # depending on the PDU type. + def pdu_to_ber_string + case pdu_type + when :get_request + [ + request_id.to_ber, + error_status.to_ber, + error_index.to_ber, + [ + @variables.map do|n, v| + [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence + end, + ].to_ber_sequence, + ].to_ber_contextspecific(0) + when :get_next_request + [ + request_id.to_ber, + error_status.to_ber, + error_index.to_ber, + [ + @variables.map do|n, v| + [n.to_ber_oid, Net::BER::BerIdentifiedNull.new.to_ber].to_ber_sequence + end, + ].to_ber_sequence, + ].to_ber_contextspecific(1) + when :get_response + [ + request_id.to_ber, + error_status.to_ber, + error_index.to_ber, + [ + @variables.map do|n, v| + [n.to_ber_oid, v.to_ber].to_ber_sequence + end, + ].to_ber_sequence, + ].to_ber_contextspecific(2) + else + raise Error.new( "unknown pdu-type: #{pdu_type}" ) + end end + private :pdu_to_ber_string + end end # :startdoc: diff --git a/net-ldap.gemspec b/net-ldap.gemspec index 01545adf..077077f2 100644 --- a/net-ldap.gemspec +++ b/net-ldap.gemspec @@ -1,16 +1,13 @@ # -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'net/ldap/version' +require_relative 'lib/net/ldap/version' Gem::Specification.new do |s| s.name = %q{net-ldap} s.version = Net::LDAP::VERSION s.license = "MIT" - - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Francis Cianfrocca", "Emiel van de Laar", "Rory O'Connell", "Kaspar Schiess", "Austin Ziegler", "Michael Schaarschmidt"] - s.date = %q{2012-02-28} s.description = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services. Net::LDAP is written completely in @@ -23,41 +20,15 @@ earlier versions of the IETF LDAP RFCs (2251-2256, 2829-2830, 3377, and 3771). Our roadmap for Net::LDAP 1.0 is to gain full client compliance with the most recent LDAP RFCs (4510-4519, plutions of 4520-4532).} s.email = ["blackhedd@rubyforge.org", "gemiel@gmail.com", "rory.ocon@gmail.com", "kaspar.schiess@absurd.li", "austin@rubyforge.org"] - s.extra_rdoc_files = ["Manifest.txt", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "README.rdoc"] - s.files = [".autotest", ".rspec", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "Manifest.txt", "README.rdoc", "Rakefile", "autotest/discover.rb", "lib/net-ldap.rb", "lib/net/ber.rb", "lib/net/ber/ber_parser.rb", "lib/net/ber/core_ext.rb", "lib/net/ber/core_ext/array.rb", "lib/net/ber/core_ext/bignum.rb", "lib/net/ber/core_ext/false_class.rb", "lib/net/ber/core_ext/fixnum.rb", "lib/net/ber/core_ext/string.rb", "lib/net/ber/core_ext/true_class.rb", "lib/net/ldap.rb", "lib/net/ldap/dataset.rb", "lib/net/ldap/dn.rb", "lib/net/ldap/entry.rb", "lib/net/ldap/filter.rb", "lib/net/ldap/password.rb", "lib/net/ldap/pdu.rb", "lib/net/snmp.rb", "net-ldap.gemspec", "spec/integration/ssl_ber_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/unit/ber/ber_spec.rb", "spec/unit/ber/core_ext/string_spec.rb", "spec/unit/ldap/dn_spec.rb", "spec/unit/ldap/entry_spec.rb", "spec/unit/ldap/filter_spec.rb", "spec/unit/ldap_spec.rb", "test/common.rb", "test/test_entry.rb", "test/test_filter.rb", "test/test_ldap_connection.rb", "test/test_ldif.rb", "test/test_password.rb", "test/test_rename.rb", "test/test_snmp.rb", "test/testdata.ldif", "testserver/ldapserver.rb", "testserver/testdata.ldif", "lib/net/ldap/version.rb"] - s.homepage = %q{http://github.com.org/ruby-ldap/ruby-net-ldap} + s.extra_rdoc_files = ["Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "README.rdoc"] + s.files = Dir["*.rdoc", "lib/**/*"] + s.test_files = s.files.grep(%r{^test}) + s.homepage = %q{http://github.com/ruby-ldap/ruby-net-ldap} s.rdoc_options = ["--main", "README.rdoc"] s.require_paths = ["lib"] - s.required_ruby_version = Gem::Requirement.new(">= 1.8.7") - s.rubyforge_project = %q{net-ldap} - s.rubygems_version = %q{1.5.2} + s.required_ruby_version = ">= 3.0.0" s.summary = %q{Net::LDAP for Ruby (also called net-ldap) implements client access for the Lightweight Directory Access Protocol (LDAP), an IETF standard protocol for accessing distributed directory services} - s.test_files = ["test/test_entry.rb", "test/test_filter.rb", "test/test_ldap_connection.rb", "test/test_ldif.rb", "test/test_password.rb", "test/test_rename.rb", "test/test_snmp.rb"] - - if s.respond_to? :specification_version then - s.specification_version = 3 - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_development_dependency(%q, ["~> 1"]) - s.add_development_dependency(%q, ["~> 1"]) - s.add_development_dependency(%q, ["~> 1"]) - s.add_development_dependency(%q, [">= 1.3.0"]) - s.add_development_dependency(%q, ["~> 2.0"]) - s.add_development_dependency(%q, [">= 2.9.1"]) - else - s.add_dependency(%q, ["~> 1"]) - s.add_dependency(%q, ["~> 1"]) - s.add_dependency(%q, ["~> 1"]) - s.add_dependency(%q, [">= 1.3.0"]) - s.add_dependency(%q, ["~> 2.0"]) - s.add_dependency(%q, [">= 2.9.1"]) - end - else - s.add_dependency(%q, ["~> 1"]) - s.add_dependency(%q, ["~> 1"]) - s.add_dependency(%q, ["~> 1"]) - s.add_dependency(%q, [">= 1.3.0"]) - s.add_dependency(%q, ["~> 2.0"]) - s.add_dependency(%q, [">= 2.9.1"]) - end + s.add_dependency("base64") + s.add_dependency("ostruct") end diff --git a/script/changelog b/script/changelog new file mode 100755 index 00000000..f42a0bd4 --- /dev/null +++ b/script/changelog @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Usage: script/changelog [-r ] [-b ] [-h ] +# +# repo: BASE string of GitHub REPOsitory url. e.g. "user_or_org/REPOsitory". Defaults to git remote url. +# base: git ref to compare from. e.g. "v1.3.1". Defaults to latest git tag. +# head: git ref to compare to. Defaults to "HEAD". +# +# Generate a changelog preview from pull requests merged between `base` and +# `head`. +# +# https://github.com/jch/release-scripts/blob/master/changelog +set -e + +[ $# -eq 0 ] && set -- --help +while [[ $# > 1 ]] +do + key="$1" + case $key in + -r|--repo) + repo="$2" + shift + ;; + -b|--base) + base="$2" + shift + ;; + -h|--head) + head="$2" + shift + ;; + *) + ;; + esac + shift +done + +repo="${repo:-$(git remote -v | grep push | awk '{print $2}' | cut -d'/' -f4- | sed 's/\.git//')}" +base="${base:-$(git tag -l | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -n 1)}" +head="${head:-HEAD}" +api_url="https://api.github.com/" + +# get merged PR's. Better way is to query the API for these, but this is easier +for pr in $(git log --oneline $base..$head | grep "Merge pull request" | awk '{gsub("#",""); print $5}') +do + # frustrated with trying to pull out the right values, fell back to ruby + 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)]}]"' +done diff --git a/script/ldap-docker b/script/ldap-docker new file mode 100755 index 00000000..c677eec8 --- /dev/null +++ b/script/ldap-docker @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Usage: script/ldap-docker +# +# Starts a openldap docker container ready for integration tests + +docker run --rm -ti \ + --hostname ldap.example.org \ + --env LDAP_TLS_VERIFY_CLIENT=try \ + -p 389:389 -p 636:636 \ + -v "$(pwd)"/test/fixtures/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom \ + --name my-openldap-container \ + osixia/openldap:1.3.0 --copy-service --loglevel debug \ No newline at end of file diff --git a/script/package b/script/package new file mode 100755 index 00000000..5851400e --- /dev/null +++ b/script/package @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Usage: script/package +# Updates the gemspec and builds a new gem in the pkg directory. + +mkdir -p pkg +gem build *.gemspec +mv *.gem pkg diff --git a/script/release b/script/release new file mode 100755 index 00000000..595a00dc --- /dev/null +++ b/script/release @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Usage: script/release +# Build the package, tag a commit, push it to origin, and then release the +# package publicly. + +set -e + +version="$(script/package | grep Version: | awk '{print $2}')" +[ -n "$version" ] || exit 1 + +echo $version +git tag "v$version" -m "Release $version" +git push origin +git push origin "v$version" +gem push pkg/*-${version}.gem diff --git a/spec/integration/ssl_ber_spec.rb b/spec/integration/ssl_ber_spec.rb deleted file mode 100644 index 4f40a204..00000000 --- a/spec/integration/ssl_ber_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -require 'net/ldap' - -describe "BER serialisation (SSL)" do - # Transmits str to #to and reads it back from #from. - # - def transmit(str) - to.write(str) - to.close - - from.read - end - - attr_reader :to, :from - before(:each) do - @from, @to = IO.pipe - - # The production code operates on sockets, which do need #connect called - # on them to work. Pipes are more robust for this test, so we'll skip - # the #connect call since it fails. - flexmock(OpenSSL::SSL::SSLSocket). - new_instances.should_receive(:connect => nil) - - @to = Net::LDAP::Connection.wrap_with_ssl(to) - @from = Net::LDAP::Connection.wrap_with_ssl(from) - end - - it "should transmit strings" do - transmit('foo').should == 'foo' - end - it "should correctly transmit numbers" do - to.write 1234.to_ber - from.read_ber.should == 1234 - end -end \ No newline at end of file diff --git a/spec/spec.opts b/spec/spec.opts deleted file mode 100644 index d019bfbf..00000000 --- a/spec/spec.opts +++ /dev/null @@ -1,2 +0,0 @@ ---format specdoc ---colour \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 50795370..00000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'net/ldap' - -RSpec.configure do |config| - config.mock_with :flexmock -end diff --git a/spec/unit/ber/ber_spec.rb b/spec/unit/ber/ber_spec.rb deleted file mode 100644 index 48e161ad..00000000 --- a/spec/unit/ber/ber_spec.rb +++ /dev/null @@ -1,141 +0,0 @@ -require 'spec_helper' - -require 'net/ber' -require 'net/ldap' - -describe "BER encoding of" do - - RSpec::Matchers.define :properly_encode_and_decode do - match do |given| - given.to_ber.read_ber.should == given - end - end - - context "arrays" do - it "should properly encode/decode []" do - [].should properly_encode_and_decode - end - it "should properly encode/decode [1,2,3]" do - ary = [1,2,3] - encoded_ary = ary.map { |el| el.to_ber }.to_ber - - encoded_ary.read_ber.should == ary - end - end - context "booleans" do - it "should encode true" do - true.to_ber.should == "\x01\x01\x01" - end - it "should encode false" do - false.to_ber.should == "\x01\x01\x00" - end - end - context "numbers" do - # Sample based - { - 0 => "\x02\x01\x00", - 1 => "\x02\x01\x01", - 127 => "\x02\x01\x7F", - 128 => "\x02\x01\x80", - 255 => "\x02\x01\xFF", - 256 => "\x02\x02\x01\x00", - 65535 => "\x02\x02\xFF\xFF", - 65536 => "\x02\x03\x01\x00\x00", - 16_777_215 => "\x02\x03\xFF\xFF\xFF", - 0x01000000 => "\x02\x04\x01\x00\x00\x00", - 0x3FFFFFFF => "\x02\x04\x3F\xFF\xFF\xFF", - 0x4FFFFFFF => "\x02\x04\x4F\xFF\xFF\xFF", - - # Some odd samples... - 5 => "\002\001\005", - 500 => "\002\002\001\364", - 50_000 => "\x02\x02\xC3P", - 5_000_000_000 => "\002\005\001*\005\362\000" - }.each do |number, expected_encoding| - it "should encode #{number} as #{expected_encoding.inspect}" do - number.to_ber.should == expected_encoding - end - end - - # Round-trip encoding: This is mostly to be sure to cover Bignums well. - context "when decoding with #read_ber" do - it "should correctly handle powers of two" do - 100.times do |p| - n = 2 << p - - n.should properly_encode_and_decode - end - end - it "should correctly handle powers of ten" do - 100.times do |p| - n = 5 * 10**p - - n.should properly_encode_and_decode - end - end - end - end - if "Ruby 1.9".respond_to?(:encoding) - context "strings" do - it "should properly encode UTF-8 strings" do - "\u00e5".force_encoding("UTF-8").to_ber.should == - "\x04\x02\xC3\xA5" - end - it "should properly encode strings encodable as UTF-8" do - "teststring".encode("US-ASCII").to_ber.should == "\x04\nteststring" - end - it "should properly encode binary data strings using to_ber_bin" do - # This is used for searching for GUIDs in Active Directory - ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").to_ber_bin.should == - "\x04\x10" + "j1\xB4\xA1*\xA2zA\xAC\xA9`?'\xDDQ\x16" - end - it "should not fail on strings that can not be converted to UTF-8" do - error = Encoding::UndefinedConversionError - lambda {"\x81".to_ber }.should_not raise_exception(error) - end - end - end -end - -describe "BER decoding of" do - context "numbers" do - it "should decode #{"\002\001\006".inspect} (6)" do - "\002\001\006".read_ber(Net::LDAP::AsnSyntax).should == 6 - end - it "should decode #{"\004\007testing".inspect} ('testing')" do - "\004\007testing".read_ber(Net::LDAP::AsnSyntax).should == 'testing' - end - it "should decode an ldap bind request" do - "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus". - read_ber(Net::LDAP::AsnSyntax).should == - [1, [3, "Administrator", "ad_is_bogus"]] - end - end -end - -describe Net::BER::BerIdentifiedString do - describe "initialize" do - subject { Net::BER::BerIdentifiedString.new(data) } - - context "binary data" do - let(:data) { ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").force_encoding("ASCII-8BIT") } - - its(:valid_encoding?) { should be_true } - specify { subject.encoding.name.should == "ASCII-8BIT" } - end - - context "ascii data in UTF-8" do - let(:data) { "some text".force_encoding("UTF-8") } - - its(:valid_encoding?) { should be_true } - specify { subject.encoding.name.should == "UTF-8" } - end - - context "UTF-8 data in UTF-8" do - let(:data) { ["e4b8ad"].pack("H*").force_encoding("UTF-8") } - - its(:valid_encoding?) { should be_true } - specify { subject.encoding.name.should == "UTF-8" } - end - end -end diff --git a/spec/unit/ber/core_ext/array_spec.rb b/spec/unit/ber/core_ext/array_spec.rb deleted file mode 100644 index c8a6b4eb..00000000 --- a/spec/unit/ber/core_ext/array_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' -require 'metaid' - -describe Array, "when extended with BER core extensions" do - - it "should correctly convert a control code array" do - control_codes = [] - control_codes << ['1.2.3'.to_ber, true.to_ber].to_ber_sequence - control_codes << ['1.7.9'.to_ber, false.to_ber].to_ber_sequence - control_codes = control_codes.to_ber_sequence - res = [['1.2.3', true],['1.7.9',false]].to_ber_control - res.should eq(control_codes) - end - - it "should wrap the array in another array if a nested array is not passed" do - result1 = ['1.2.3', true].to_ber_control - result2 = [['1.2.3', true]].to_ber_control - result1.should eq(result2) - end - - it "should return an empty string if an empty array is passed" do - [].to_ber_control.should be_empty - end -end diff --git a/spec/unit/ber/core_ext/string_spec.rb b/spec/unit/ber/core_ext/string_spec.rb deleted file mode 100644 index 6eebe05c..00000000 --- a/spec/unit/ber/core_ext/string_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'spec_helper' -require 'metaid' - -describe String, "when extended with BER core extensions" do - describe "<- #read_ber! (consuming read_ber method)" do - context "when passed an ldap bind request and some extra data" do - attr_reader :str, :result - before(:each) do - @str = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus UNCONSUMED" - @result = str.read_ber!(Net::LDAP::AsnSyntax) - end - - it "should correctly parse the ber message" do - result.should == [1, [3, "Administrator", "ad_is_bogus"]] - end - it "should leave unconsumed part of message in place" do - str.should == " UNCONSUMED" - end - - context "if an exception occurs during #read_ber" do - attr_reader :initial_value - before(:each) do - stub_exception_class = Class.new(StandardError) - - @initial_value = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus" - @str = initial_value.dup - - # Defines a string - io = StringIO.new(initial_value) - io.meta_def :read_ber do |syntax| - read - raise stub_exception_class - end - flexmock(StringIO).should_receive(:new).and_return(io) - - begin - str.read_ber!(Net::LDAP::AsnSyntax) - rescue stub_exception_class - # EMPTY ON PURPOSE - else - raise "The stub code should raise an exception!" - end - end - - it "should not modify string" do - str.should == initial_value - end - end - end - end -end diff --git a/spec/unit/ldap/dn_spec.rb b/spec/unit/ldap/dn_spec.rb deleted file mode 100644 index 8d1b5852..00000000 --- a/spec/unit/ldap/dn_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -require 'spec_helper' -require 'net/ldap/dn' - -describe Net::LDAP::DN do - describe "<- .construct" do - attr_reader :dn - - before(:each) do - @dn = Net::LDAP::DN.new('cn', ',+"\\<>;', 'ou=company') - end - - it "should construct a Net::LDAP::DN" do - dn.should be_an_instance_of(Net::LDAP::DN) - end - - it "should escape all the required characters" do - dn.to_s.should == 'cn=\\,\\+\\"\\\\\\<\\>\\;,ou=company' - end - end - - describe "<- .to_a" do - context "parsing" do - { - 'cn=James, ou=Company\\,\\20LLC' => ['cn','James','ou','Company, LLC'], - 'cn = \ James , ou = "Comp\28ny" ' => ['cn',' James','ou','Comp(ny'], - '1.23.4= #A3B4D5 ,ou=Company' => ['1.23.4','#A3B4D5','ou','Company'], - }.each do |key, value| - context "(#{key})" do - attr_reader :dn - - before(:each) do - @dn = Net::LDAP::DN.new(key) - end - - it "should decode into a Net::LDAP::DN" do - dn.should be_an_instance_of(Net::LDAP::DN) - end - - it "should return the correct array" do - dn.to_a.should == value - end - end - end - end - - context "parsing bad input" do - [ - 'cn=James,', - 'cn=#aa aa', - 'cn="James', - 'cn=J\ames', - 'cn=\\', - '1.2.d=Value', - 'd1.2=Value', - ].each do |value| - context "(#{value})" do - attr_reader :dn - - before(:each) do - @dn = Net::LDAP::DN.new(value) - end - - it "should decode into a Net::LDAP::DN" do - dn.should be_an_instance_of(Net::LDAP::DN) - end - - it "should raise an error on parsing" do - lambda { dn.to_a }.should raise_error - end - end - end - end - end - - describe "<- .escape(str)" do - it "should escape ,, +, \", \\, <, >, and ;" do - Net::LDAP::DN.escape(',+"\\<>;').should == '\\,\\+\\"\\\\\\<\\>\\;' - end - end -end diff --git a/spec/unit/ldap/entry_spec.rb b/spec/unit/ldap/entry_spec.rb deleted file mode 100644 index e0270cbd..00000000 --- a/spec/unit/ldap/entry_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'spec_helper' - -describe Net::LDAP::Entry do - attr_reader :entry - before(:each) do - @entry = Net::LDAP::Entry.from_single_ldif_string( - %Q{dn: something -foo: foo -barAttribute: bar - } - ) - end - - describe "entry access" do - it "should always respond to #dn" do - entry.should respond_to(:dn) - end - - context "<- #foo" do - it "should respond_to?" do - entry.should respond_to(:foo) - end - it "should return 'foo'" do - entry.foo.should == ['foo'] - end - end - context "<- #Foo" do - it "should respond_to?" do - entry.should respond_to(:Foo) - end - it "should return 'foo'" do - entry.foo.should == ['foo'] - end - end - context "<- #foo=" do - it "should respond_to?" do - entry.should respond_to(:foo=) - end - it "should set 'foo'" do - entry.foo= 'bar' - entry.foo.should == ['bar'] - end - end - context "<- #fOo=" do - it "should return 'foo'" do - entry.fOo= 'bar' - entry.fOo.should == ['bar'] - end - end - end -end \ No newline at end of file diff --git a/spec/unit/ldap/filter_parser_spec.rb b/spec/unit/ldap/filter_parser_spec.rb deleted file mode 100644 index e34828eb..00000000 --- a/spec/unit/ldap/filter_parser_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# encoding: utf-8 -require 'spec_helper' - -describe Net::LDAP::Filter::FilterParser do - - describe "#parse" do - context "Given ASCIIs as filter string" do - let(:filter_string) { "(cn=name)" } - specify "should generate filter object" do - expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter - end - end - context "Given string including multibyte chars as filter string" do - let(:filter_string) { "(cn=名前)" } - specify "should generate filter object" do - expect(Net::LDAP::Filter::FilterParser.parse(filter_string)).to be_a Net::LDAP::Filter - end - end - end -end diff --git a/spec/unit/ldap/filter_spec.rb b/spec/unit/ldap/filter_spec.rb deleted file mode 100644 index 5e4cb8a8..00000000 --- a/spec/unit/ldap/filter_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -require 'spec_helper' - -describe Net::LDAP::Filter do - describe "<- .ex(attr, value)" do - context "('foo', 'bar')" do - attr_reader :filter - before(:each) do - @filter = Net::LDAP::Filter.ex('foo', 'bar') - end - it "should convert to 'foo:=bar'" do - filter.to_s.should == '(foo:=bar)' - end - it "should survive roundtrip via to_s/from_rfc2254" do - Net::LDAP::Filter.from_rfc2254(filter.to_s).should == filter - end - it "should survive roundtrip conversion to/from ber" do - ber = filter.to_ber - Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)).should == - filter - end - end - context "various legal inputs" do - [ - '(o:dn:=Ace Industry)', - '(:dn:2.4.8.10:=Dino)', - '(cn:dn:1.2.3.4.5:=John Smith)', - '(sn:dn:2.4.6.8.10:=Barbara Jones)', - '(&(sn:dn:2.4.6.8.10:=Barbara Jones))' - ].each do |filter_str| - context "from_rfc2254(#{filter_str.inspect})" do - attr_reader :filter - before(:each) do - @filter = Net::LDAP::Filter.from_rfc2254(filter_str) - end - - it "should decode into a Net::LDAP::Filter" do - filter.should be_an_instance_of(Net::LDAP::Filter) - end - it "should survive roundtrip conversion to/from ber" do - ber = filter.to_ber - Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)).should == - filter - end - end - end - end - end - describe "<- .construct" do - it "should accept apostrophes in filters (regression)" do - Net::LDAP::Filter.construct("uid=O'Keefe").to_rfc2254.should == "(uid=O'Keefe)" - end - end - - describe "convenience filter constructors" do - def eq(attribute, value) - described_class.eq(attribute, value) - end - describe "<- .equals(attr, val)" do - it "should delegate to .eq with escaping" do - described_class.equals('dn', 'f*oo').should == eq('dn', 'f\2Aoo') - end - end - describe "<- .begins(attr, val)" do - it "should delegate to .eq with escaping" do - described_class.begins('dn', 'f*oo').should == eq('dn', 'f\2Aoo*') - end - end - describe "<- .ends(attr, val)" do - it "should delegate to .eq with escaping" do - described_class.ends('dn', 'f*oo').should == eq('dn', '*f\2Aoo') - end - end - describe "<- .contains(attr, val)" do - it "should delegate to .eq with escaping" do - described_class.contains('dn', 'f*oo').should == eq('dn', '*f\2Aoo*') - end - end - end - describe "<- .escape(str)" do - it "should escape nul, *, (, ) and \\" do - Net::LDAP::Filter.escape("\0*()\\").should == "\\00\\2A\\28\\29\\5C" - end - end - - context 'with a well-known BER string' do - ber = "\xa4\x2d" \ - "\x04\x0b" "objectclass" \ - "\x30\x1e" \ - "\x80\x08" "foo" "*\\" "bar" \ - "\x81\x08" "foo" "*\\" "bar" \ - "\x82\x08" "foo" "*\\" "bar" - - describe "<- .to_ber" do - [ - "foo" "\\2A\\5C" "bar", - "foo" "\\2a\\5c" "bar", - "foo" "\\2A\\5c" "bar", - "foo" "\\2a\\5C" "bar" - ].each do |escaped| - it 'unescapes escaped characters' do - filter = Net::LDAP::Filter.eq("objectclass", "#{escaped}*#{escaped}*#{escaped}") - filter.to_ber.should == ber - end - end - end - - describe '<- .parse_ber' do - it 'escapes characters' do - escaped = Net::LDAP::Filter.escape("foo" "*\\" "bar") - filter = Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) - filter.to_s.should == "(objectclass=#{escaped}*#{escaped}*#{escaped})" - end - end - end -end diff --git a/spec/unit/ldap/search_spec.rb b/spec/unit/ldap/search_spec.rb deleted file mode 100644 index 5d06a88e..00000000 --- a/spec/unit/ldap/search_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# -*- ruby encoding: utf-8 -*- - -describe Net::LDAP, "search method" do - class FakeConnection - def search(args) - OpenStruct.new(:result_code => 1, :message => "error", :success? => false) - end - end - - before(:each) do - @connection = Net::LDAP.new - @connection.instance_variable_set(:@open_connection, FakeConnection.new) - end - - context "when :return_result => true" do - it "should return nil upon error" do - result_set = @connection.search(:return_result => true) - result_set.should be_nil - end - end - - context "when :return_result => false" do - it "should return false upon error" do - result = @connection.search(:return_result => false) - result.should be_false - end - end - - context "When :return_result is not given" do - it "should return nil upon error" do - result_set = @connection.search - result_set.should be_nil - end - end -end diff --git a/spec/unit/ldap_spec.rb b/spec/unit/ldap_spec.rb deleted file mode 100644 index 272d4eee..00000000 --- a/spec/unit/ldap_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' - -describe Net::LDAP::Connection do - describe "initialize" do - context "when host is not responding" do - before(:each) do - flexmock(TCPSocket). - should_receive(:new).and_raise(Errno::ECONNREFUSED) - end - - it "should raise LdapError" do - lambda { - Net::LDAP::Connection.new( - :server => 'test.mocked.com', - :port => 636) - }.should raise_error(Net::LDAP::LdapError) - end - end - context "when host is blocking the port" do - before(:each) do - flexmock(TCPSocket). - should_receive(:new).and_raise(SocketError) - end - - it "should raise LdapError" do - lambda { - Net::LDAP::Connection.new( - :server => 'test.mocked.com', - :port => 636) - }.should raise_error(Net::LDAP::LdapError) - end - end - context "on other exceptions" do - before(:each) do - flexmock(TCPSocket). - should_receive(:new).and_raise(NameError) - end - - it "should rethrow the exception" do - lambda { - Net::LDAP::Connection.new( - :server => 'test.mocked.com', - :port => 636) - }.should raise_error(NameError) - end - end - end - - context "populate error messages" do - before do - @tcp_socket = flexmock(:connection) - @tcp_socket.should_receive(:write) - flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket) - end - - subject { Net::LDAP::Connection.new(:server => 'test.mocked.com', :port => 636) } - - it "should get back error messages if operation fails" do - ber = Net::BER::BerIdentifiedArray.new([53, "", "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1"]) - ber.ber_identifier = 7 - @tcp_socket.should_receive(:read_ber).and_return([2, ber]) - - result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) - result.should be_failure - result.error_message.should == "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1" - end - - it "shouldn't get back error messages if operation succeeds" do - ber = Net::BER::BerIdentifiedArray.new([0, "", ""]) - ber.ber_identifier = 7 - @tcp_socket.should_receive(:read_ber).and_return([2, ber]) - - result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) - result.should be_success - result.error_message.should == "" - end - end -end diff --git a/test/ber/core_ext/test_array.rb b/test/ber/core_ext/test_array.rb new file mode 100644 index 00000000..2d1e957a --- /dev/null +++ b/test/ber/core_ext/test_array.rb @@ -0,0 +1,22 @@ +require_relative '../../test_helper' + +class TestBERArrayExtension < Test::Unit::TestCase + def test_control_code_array + control_codes = [] + control_codes << ['1.2.3'.to_ber, true.to_ber].to_ber_sequence + control_codes << ['1.7.9'.to_ber, false.to_ber].to_ber_sequence + control_codes = control_codes.to_ber_sequence + res = [['1.2.3', true], ['1.7.9', false]].to_ber_control + assert_equal control_codes, res + end + + def test_wrap_array_if_not_nested + result1 = ['1.2.3', true].to_ber_control + result2 = [['1.2.3', true]].to_ber_control + assert_equal result2, result1 + end + + def test_empty_string_if_empty_array + assert_equal "", [].to_ber_control + end +end diff --git a/test/ber/core_ext/test_string.rb b/test/ber/core_ext/test_string.rb new file mode 100644 index 00000000..692770e4 --- /dev/null +++ b/test/ber/core_ext/test_string.rb @@ -0,0 +1,25 @@ +require_relative '../../test_helper' + +class TestBERStringExtension < Test::Unit::TestCase + def setup + @bind_request = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus UNCONSUMED".b + @result = @bind_request.read_ber!(Net::LDAP::AsnSyntax) + end + + def test_parse_ber + assert_equal [1, [3, "Administrator", "ad_is_bogus"]], @result + end + + def test_unconsumed_message + assert_equal " UNCONSUMED", @bind_request + end + + def test_exception_does_not_modify_string + original = "0$\002\001\001`\037\002\001\003\004\rAdministrator\200\vad_is_bogus".b + duplicate = original.dup + flexmock(StringIO).new_instances.should_receive(:read_ber).and_raise(Net::BER::BerError) + duplicate.read_ber!(Net::LDAP::AsnSyntax) rescue Net::BER::BerError + + assert_equal original, duplicate + end +end diff --git a/test/ber/test_ber.rb b/test/ber/test_ber.rb new file mode 100644 index 00000000..b700972e --- /dev/null +++ b/test/ber/test_ber.rb @@ -0,0 +1,153 @@ +require_relative '../test_helper' + +class TestBEREncoding < Test::Unit::TestCase + def test_empty_array + assert_equal [], [].to_ber.read_ber + end + + def test_array + ary = [1, 2, 3] + encoded_ary = ary.map(&:to_ber).to_ber + + assert_equal ary, encoded_ary.read_ber + end + + # http://tools.ietf.org/html/rfc4511#section-5.1 + def test_true + assert_equal "\x01\x01\xFF".b, true.to_ber + end + + def test_false + assert_equal "\x01\x01\x00", false.to_ber + end + + # Sample based + { + 0 => "\x02\x01\x00", + 1 => "\x02\x01\x01", + 127 => "\x02\x01\x7F", + 128 => "\x02\x02\x00\x80", + 255 => "\x02\x02\x00\xFF", + 256 => "\x02\x02\x01\x00", + 65535 => "\x02\x03\x00\xFF\xFF", + 65536 => "\x02\x03\x01\x00\x00", + 8388607 => "\x02\x03\x7F\xFF\xFF", + 8388608 => "\x02\x04\x00\x80\x00\x00", + 16_777_215 => "\x02\x04\x00\xFF\xFF\xFF", + 0x01000000 => "\x02\x04\x01\x00\x00\x00", + 0x3FFFFFFF => "\x02\x04\x3F\xFF\xFF\xFF", + 0x4FFFFFFF => "\x02\x04\x4F\xFF\xFF\xFF", + + # Some odd samples... + 5 => "\x02\x01\x05", + 500 => "\x02\x02\x01\xf4", + 50_000 => "\x02\x03\x00\xC3\x50", + 5_000_000_000 => "\x02\x05\x01\x2a\x05\xF2\x00", + + # negatives + -1 => "\x02\x01\xFF", + -127 => "\x02\x01\x81", + -128 => "\x02\x01\x80", + -255 => "\x02\x02\xFF\x01", + -256 => "\x02\x02\xFF\x00", + -65535 => "\x02\x03\xFF\x00\x01", + -65536 => "\x02\x03\xFF\x00\x00", + -65537 => "\x02\x03\xFE\xFF\xFF", + -8388607 => "\x02\x03\x80\x00\x01", + -8388608 => "\x02\x03\x80\x00\x00", + -16_777_215 => "\x02\x04\xFF\x00\x00\x01", + }.each do |number, expected_encoding| + define_method "test_encode_#{number}" do + assert_equal expected_encoding.b, number.to_ber + end + + define_method "test_decode_encoded_#{number}" do + assert_equal number, expected_encoding.b.read_ber + end + end + + # Round-trip encoding: This is mostly to be sure to cover Bignums well. + def test_powers_of_two + 100.times do |p| + n = 2 << p + + assert_equal n, n.to_ber.read_ber + end + end + + def test_powers_of_ten + 100.times do |p| + n = 5 * 10**p + + assert_equal n, n.to_ber.read_ber + end + end + + if "Ruby 1.9".respond_to?(:encoding) + def test_encode_utf8_strings + assert_equal "\x04\x02\xC3\xA5".b, "\u00e5".force_encoding("UTF-8").to_ber + end + + def test_utf8_encodable_strings + assert_equal "\x04\nteststring", "teststring".encode("US-ASCII").to_ber + end + + def test_encode_binary_data + # This is used for searching for GUIDs in Active Directory + assert_equal "\x04\x10" + "j1\xB4\xA1*\xA2zA\xAC\xA9`?'\xDDQ\x16".b, + ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").to_ber_bin + end + + def test_non_utf8_encodable_strings + assert_equal "\x04\x01\x81".b, "\x81".to_ber + end + end +end + +class TestBERDecoding < Test::Unit::TestCase + def test_decode_number + assert_equal 6, "\002\001\006".read_ber(Net::LDAP::AsnSyntax) + end + + def test_decode_string + assert_equal "testing", "\004\007testing".read_ber(Net::LDAP::AsnSyntax) + end + + def test_decode_ldap_bind_request + 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) + end +end + +class TestBERIdentifiedString < Test::Unit::TestCase + def test_binary_data + data = ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").force_encoding("ASCII-8BIT") + bis = Net::BER::BerIdentifiedString.new(data) + + assert bis.valid_encoding?, "should be a valid encoding" + assert_equal "ASCII-8BIT", bis.encoding.name + end + + def test_ascii_data_in_utf8 + data = "some text".force_encoding("UTF-8") + bis = Net::BER::BerIdentifiedString.new(data) + + assert bis.valid_encoding?, "should be a valid encoding" + assert_equal "UTF-8", bis.encoding.name + end + + def test_umlaut_data_in_utf8 + data = "Müller".force_encoding("UTF-8") + bis = Net::BER::BerIdentifiedString.new(data) + + assert bis.valid_encoding?, "should be a valid encoding" + assert_equal "UTF-8", bis.encoding.name + end + + def test_utf8_data_in_utf8 + data = ["e4b8ad"].pack("H*").force_encoding("UTF-8") + bis = Net::BER::BerIdentifiedString.new(data) + + assert bis.valid_encoding?, "should be a valid encoding" + assert_equal "UTF-8", bis.encoding.name + end +end diff --git a/test/common.rb b/test/common.rb deleted file mode 100644 index baa06884..00000000 --- a/test/common.rb +++ /dev/null @@ -1,3 +0,0 @@ -# Add 'lib' to load path. -require 'test/unit' -require 'net/ldap' diff --git a/test/fixtures/ca/docker-ca.pem b/test/fixtures/ca/docker-ca.pem new file mode 100644 index 00000000..ab543a31 --- /dev/null +++ b/test/fixtures/ca/docker-ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0zCCAlmgAwIBAgIUCfQ+m0pgZ/BjYAJvxrn/bdGNZokwCgYIKoZIzj0EAwMw +gZYxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxBMUEgQ2FyIFdhc2gxJDAiBgNVBAsT +G0luZm9ybWF0aW9uIFRlY2hub2xvZ3kgRGVwLjEUMBIGA1UEBxMLQWxidXF1ZXJx +dWUxEzARBgNVBAgTCk5ldyBNZXhpY28xHzAdBgNVBAMTFmRvY2tlci1saWdodC1i +YXNlaW1hZ2UwHhcNMTUxMjIzMTM1MzAwWhcNMjAxMjIxMTM1MzAwWjCBljELMAkG +A1UEBhMCVVMxFTATBgNVBAoTDEExQSBDYXIgV2FzaDEkMCIGA1UECxMbSW5mb3Jt +YXRpb24gVGVjaG5vbG9neSBEZXAuMRQwEgYDVQQHEwtBbGJ1cXVlcnF1ZTETMBEG +A1UECBMKTmV3IE1leGljbzEfMB0GA1UEAxMWZG9ja2VyLWxpZ2h0LWJhc2VpbWFn +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMZf/12pupAgl8Sm+j8GmjNeNbSFAZWW +oTmIvf2Mu4LWPHy4bTldkQgHUbBpT3xWz8f0lB/ru7596CHsGoL2A28hxuclq5hb +Ux1yrIt3bJIY3TuiX25HGTe6kGCJPB1aLaNmMGQwDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFE+l6XolXDAYnGLTl4W6ULKHrm74 +MB8GA1UdIwQYMBaAFE+l6XolXDAYnGLTl4W6ULKHrm74MAoGCCqGSM49BAMDA2gA +MGUCMQCXLZj8okyxW6UTL7hribUUbu63PbjuwIXnwi420DdNsvA9A7fcQEXScWFL +XAGC8rkCMGcqwXZPSRfwuI9r+R11gTrP92hnaVxs9sjRikctpkQpOyNlIXFPopFK +8FdfWPypvA== +-----END CERTIFICATE----- diff --git a/test/fixtures/ldif/06-retcode.ldif b/test/fixtures/ldif/06-retcode.ldif new file mode 100644 index 00000000..dfd12d06 --- /dev/null +++ b/test/fixtures/ldif/06-retcode.ldif @@ -0,0 +1,75 @@ +dn: cn=module{0},cn=config +changetype: modify +add: olcModuleLoad +olcModuleLoad: retcode + +# source: http://www.opensource.apple.com/source/OpenLDAP/OpenLDAP-186/OpenLDAP/tests/data/retcode.conf?txt + +dn: olcOverlay={2}retcode,olcDatabase={1}{{ LDAP_BACKEND }},cn=config +changetype: add +objectClass: olcConfig +objectClass: olcRetcodeConfig +objectClass: olcOverlayConfig +objectClass: top +olcOverlay: retcode +olcRetcodeParent: ou=Retcodes,dc=example,dc=org +olcRetcodeInDir: TRUE +olcRetcodeSleep: 0 +olcRetcodeItem: "cn=success" 0x00 +olcRetcodeItem: "cn=success w/ delay" 0x00 sleeptime=2 +olcRetcodeItem: "cn=operationsError" 0x01 +olcRetcodeItem: "cn=protocolError" 0x02 +olcRetcodeItem: "cn=timeLimitExceeded" 0x03 op=search +olcRetcodeItem: "cn=sizeLimitExceeded" 0x04 op=search +olcRetcodeItem: "cn=compareFalse" 0x05 op=compare +olcRetcodeItem: "cn=compareTrue" 0x06 op=compare +olcRetcodeItem: "cn=authMethodNotSupported" 0x07 +olcRetcodeItem: "cn=strongAuthNotSupported" 0x07 text="same as authMethodNotSupported" +olcRetcodeItem: "cn=strongAuthRequired" 0x08 +olcRetcodeItem: "cn=strongerAuthRequired" 0x08 text="same as strongAuthRequired" +olcRetcodeItem: "cn=referral" 0x0a text="LDAPv3" ref="ldap://:9019" +olcRetcodeItem: "cn=adminLimitExceeded" 0x0b text="LDAPv3" +olcRetcodeItem: "cn=unavailableCriticalExtension" 0x0c text="LDAPv3" +olcRetcodeItem: "cn=confidentialityRequired" 0x0d text="LDAPv3" +olcRetcodeItem: "cn=saslBindInProgress" 0x0e text="LDAPv3" +olcRetcodeItem: "cn=noSuchAttribute" 0x10 +olcRetcodeItem: "cn=undefinedAttributeType" 0x11 +olcRetcodeItem: "cn=inappropriateMatching" 0x12 +olcRetcodeItem: "cn=constraintViolation" 0x13 +olcRetcodeItem: "cn=attributeOrValueExists" 0x14 +olcRetcodeItem: "cn=invalidAttributeSyntax" 0x15 +olcRetcodeItem: "cn=noSuchObject" 0x20 +olcRetcodeItem: "cn=aliasProblem" 0x21 +olcRetcodeItem: "cn=invalidDNSyntax" 0x22 +olcRetcodeItem: "cn=aliasDereferencingProblem" 0x24 +olcRetcodeItem: "cn=proxyAuthzFailure" 0x2F text="LDAPv3 proxy authorization" +olcRetcodeItem: "cn=inappropriateAuthentication" 0x30 +olcRetcodeItem: "cn=invalidCredentials" 0x31 +olcRetcodeItem: "cn=insufficientAccessRights" 0x32 +olcRetcodeItem: "cn=busy" 0x33 +olcRetcodeItem: "cn=unavailable" 0x34 +olcRetcodeItem: "cn=unwillingToPerform" 0x35 +olcRetcodeItem: "cn=loopDetect" 0x36 +olcRetcodeItem: "cn=namingViolation" 0x40 +olcRetcodeItem: "cn=objectClassViolation" 0x41 +olcRetcodeItem: "cn=notAllowedOnNonleaf" 0x42 +olcRetcodeItem: "cn=notAllowedOnRDN" 0x43 +olcRetcodeItem: "cn=entryAlreadyExists" 0x44 +olcRetcodeItem: "cn=objectClassModsProhibited" 0x45 +olcRetcodeItem: "cn=resultsTooLarge" 0x46 text="CLDAP" +olcRetcodeItem: "cn=affectsMultipleDSAs" 0x47 text="LDAPv3" +olcRetcodeItem: "cn=other" 0x50 +olcRetcodeItem: "cn=cupResourcesExhausted" 0x71 +olcRetcodeItem: "cn=cupSecurityViolation" 0x72 +olcRetcodeItem: "cn=cupInvalidData" 0x73 +olcRetcodeItem: "cn=cupUnsupportedScheme" 0x74 +olcRetcodeItem: "cn=cupReloadRequired" 0x75 +olcRetcodeItem: "cn=cancelled" 0x76 +olcRetcodeItem: "cn=noSuchOperation" 0x77 +olcRetcodeItem: "cn=tooLate" 0x78 +olcRetcodeItem: "cn=cannotCancel" 0x79 +olcRetcodeItem: "cn=syncRefreshRequired" 0x4100 +olcRetcodeItem: "cn=noOperation" 0x410e +olcRetcodeItem: "cn=assertionFailed" 0x410f +olcRetcodeItem: "cn=noReferralsFound" 0x4110 +olcRetcodeItem: "cn=cannotChain" 0x4111 diff --git a/test/fixtures/ldif/50-seed.ldif b/test/fixtures/ldif/50-seed.ldif new file mode 100644 index 00000000..addedf5a --- /dev/null +++ b/test/fixtures/ldif/50-seed.ldif @@ -0,0 +1,374 @@ +dn: ou=People,dc=example,dc=org +objectClass: top +objectClass: organizationalUnit +ou: People + +dn: ou=Groups,dc=example,dc=org +objectClass: top +objectClass: organizationalUnit +ou: Groups + +# Directory Superuser +dn: uid=admin,dc=example,dc=org +uid: admin +cn: system administrator +sn: administrator +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +displayName: Directory Superuser +userPassword: passworD1 + +# Users 1-10 + +dn: uid=user1,ou=People,dc=example,dc=org +uid: user1 +cn: user1 +sn: user1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user1@rubyldap.com + +dn: uid=user2,ou=People,dc=example,dc=org +uid: user2 +cn: user2 +sn: user2 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user2@rubyldap.com + +dn: uid=user3,ou=People,dc=example,dc=org +uid: user3 +cn: user3 +sn: user3 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user3@rubyldap.com + +dn: uid=user4,ou=People,dc=example,dc=org +uid: user4 +cn: user4 +sn: user4 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user4@rubyldap.com + +dn: uid=user5,ou=People,dc=example,dc=org +uid: user5 +cn: user5 +sn: user5 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user5@rubyldap.com + +dn: uid=user6,ou=People,dc=example,dc=org +uid: user6 +cn: user6 +sn: user6 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user6@rubyldap.com + +dn: uid=user7,ou=People,dc=example,dc=org +uid: user7 +cn: user7 +sn: user7 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user7@rubyldap.com + +dn: uid=user8,ou=People,dc=example,dc=org +uid: user8 +cn: user8 +sn: user8 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user8@rubyldap.com + +dn: uid=user9,ou=People,dc=example,dc=org +uid: user9 +cn: user9 +sn: user9 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user9@rubyldap.com + +dn: uid=user10,ou=People,dc=example,dc=org +uid: user10 +cn: user10 +sn: user10 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: user10@rubyldap.com + +# Emailless User + +dn: uid=emailless-user1,ou=People,dc=example,dc=org +uid: emailless-user1 +cn: emailless-user1 +sn: emailless-user1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 + +# Groupless User + +dn: uid=groupless-user1,ou=People,dc=example,dc=org +uid: groupless-user1 +cn: groupless-user1 +sn: groupless-user1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 + +# Admin User + +dn: uid=admin1,ou=People,dc=example,dc=org +uid: admin1 +cn: admin1 +sn: admin1 +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +userPassword: passworD1 +mail: admin1@rubyldap.com + +# Groups + +dn: cn=ghe-users,ou=Groups,dc=example,dc=org +cn: ghe-users +objectClass: groupOfNames +member: uid=user1,ou=People,dc=example,dc=org +member: uid=emailless-user1,ou=People,dc=example,dc=org + +dn: cn=all-users,ou=Groups,dc=example,dc=org +cn: all-users +objectClass: groupOfNames +member: cn=ghe-users,ou=Groups,dc=example,dc=org +member: uid=user1,ou=People,dc=example,dc=org +member: uid=user2,ou=People,dc=example,dc=org +member: uid=user3,ou=People,dc=example,dc=org +member: uid=user4,ou=People,dc=example,dc=org +member: uid=user5,ou=People,dc=example,dc=org +member: uid=user6,ou=People,dc=example,dc=org +member: uid=user7,ou=People,dc=example,dc=org +member: uid=user8,ou=People,dc=example,dc=org +member: uid=user9,ou=People,dc=example,dc=org +member: uid=user10,ou=People,dc=example,dc=org +member: uid=emailless-user1,ou=People,dc=example,dc=org + +dn: cn=ghe-admins,ou=Groups,dc=example,dc=org +cn: ghe-admins +objectClass: groupOfNames +member: uid=admin1,ou=People,dc=example,dc=org + +dn: cn=all-admins,ou=Groups,dc=example,dc=org +cn: all-admins +objectClass: groupOfNames +member: cn=ghe-admins,ou=Groups,dc=example,dc=org +member: uid=admin1,ou=People,dc=example,dc=org + +dn: cn=n-member-group10,ou=Groups,dc=example,dc=org +cn: n-member-group10 +objectClass: groupOfNames +member: uid=user1,ou=People,dc=example,dc=org +member: uid=user2,ou=People,dc=example,dc=org +member: uid=user3,ou=People,dc=example,dc=org +member: uid=user4,ou=People,dc=example,dc=org +member: uid=user5,ou=People,dc=example,dc=org +member: uid=user6,ou=People,dc=example,dc=org +member: uid=user7,ou=People,dc=example,dc=org +member: uid=user8,ou=People,dc=example,dc=org +member: uid=user9,ou=People,dc=example,dc=org +member: uid=user10,ou=People,dc=example,dc=org + +dn: cn=nested-group1,ou=Groups,dc=example,dc=org +cn: nested-group1 +objectClass: groupOfNames +member: uid=user1,ou=People,dc=example,dc=org +member: uid=user2,ou=People,dc=example,dc=org +member: uid=user3,ou=People,dc=example,dc=org +member: uid=user4,ou=People,dc=example,dc=org +member: uid=user5,ou=People,dc=example,dc=org + +dn: cn=nested-group2,ou=Groups,dc=example,dc=org +cn: nested-group2 +objectClass: groupOfNames +member: uid=user6,ou=People,dc=example,dc=org +member: uid=user7,ou=People,dc=example,dc=org +member: uid=user8,ou=People,dc=example,dc=org +member: uid=user9,ou=People,dc=example,dc=org +member: uid=user10,ou=People,dc=example,dc=org + +dn: cn=nested-groups,ou=Groups,dc=example,dc=org +cn: nested-groups +objectClass: groupOfNames +member: cn=nested-group1,ou=Groups,dc=example,dc=org +member: cn=nested-group2,ou=Groups,dc=example,dc=org + +dn: cn=n-member-nested-group1,ou=Groups,dc=example,dc=org +cn: n-member-nested-group1 +objectClass: groupOfNames +member: cn=nested-group1,ou=Groups,dc=example,dc=org + +dn: cn=deeply-nested-group0.0.0,ou=Groups,dc=example,dc=org +cn: deeply-nested-group0.0.0 +objectClass: groupOfNames +member: uid=user1,ou=People,dc=example,dc=org +member: uid=user2,ou=People,dc=example,dc=org +member: uid=user3,ou=People,dc=example,dc=org +member: uid=user4,ou=People,dc=example,dc=org +member: uid=user5,ou=People,dc=example,dc=org + +dn: cn=deeply-nested-group0.0.1,ou=Groups,dc=example,dc=org +cn: deeply-nested-group0.0.1 +objectClass: groupOfNames +member: uid=user6,ou=People,dc=example,dc=org +member: uid=user7,ou=People,dc=example,dc=org +member: uid=user8,ou=People,dc=example,dc=org +member: uid=user9,ou=People,dc=example,dc=org +member: uid=user10,ou=People,dc=example,dc=org + +dn: cn=deeply-nested-group0.0,ou=Groups,dc=example,dc=org +cn: deeply-nested-group0.0 +objectClass: groupOfNames +member: cn=deeply-nested-group0.0.0,ou=Groups,dc=example,dc=org +member: cn=deeply-nested-group0.0.1,ou=Groups,dc=example,dc=org + +dn: cn=deeply-nested-group0,ou=Groups,dc=example,dc=org +cn: deeply-nested-group0 +objectClass: groupOfNames +member: cn=deeply-nested-group0.0,ou=Groups,dc=example,dc=org + +dn: cn=deeply-nested-groups,ou=Groups,dc=example,dc=org +cn: deeply-nested-groups +objectClass: groupOfNames +member: cn=deeply-nested-group0,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group1,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group1 +objectClass: groupOfNames +member: cn=nested-group1,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group2,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group2 +objectClass: groupOfNames +member: cn=n-depth-nested-group1,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group3,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group3 +objectClass: groupOfNames +member: cn=n-depth-nested-group2,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group4,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group4 +objectClass: groupOfNames +member: cn=n-depth-nested-group3,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group5,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group5 +objectClass: groupOfNames +member: cn=n-depth-nested-group4,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group6,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group6 +objectClass: groupOfNames +member: cn=n-depth-nested-group5,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group7,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group7 +objectClass: groupOfNames +member: cn=n-depth-nested-group6,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group8,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group8 +objectClass: groupOfNames +member: cn=n-depth-nested-group7,ou=Groups,dc=example,dc=org + +dn: cn=n-depth-nested-group9,ou=Groups,dc=example,dc=org +cn: n-depth-nested-group9 +objectClass: groupOfNames +member: cn=n-depth-nested-group8,ou=Groups,dc=example,dc=org + +dn: cn=head-group,ou=Groups,dc=example,dc=org +cn: head-group +objectClass: groupOfNames +member: cn=tail-group,ou=Groups,dc=example,dc=org +member: uid=user1,ou=People,dc=example,dc=org +member: uid=user2,ou=People,dc=example,dc=org +member: uid=user3,ou=People,dc=example,dc=org +member: uid=user4,ou=People,dc=example,dc=org +member: uid=user5,ou=People,dc=example,dc=org + +dn: cn=tail-group,ou=Groups,dc=example,dc=org +cn: tail-group +objectClass: groupOfNames +member: cn=head-group,ou=Groups,dc=example,dc=org +member: uid=user6,ou=People,dc=example,dc=org +member: uid=user7,ou=People,dc=example,dc=org +member: uid=user8,ou=People,dc=example,dc=org +member: uid=user9,ou=People,dc=example,dc=org +member: uid=user10,ou=People,dc=example,dc=org + +dn: cn=recursively-nested-groups,ou=Groups,dc=example,dc=org +cn: recursively-nested-groups +objectClass: groupOfNames +member: cn=head-group,ou=Groups,dc=example,dc=org +member: cn=tail-group,ou=Groups,dc=example,dc=org + +# posixGroup + +dn: cn=posix-group1,ou=Groups,dc=example,dc=org +cn: posix-group1 +objectClass: posixGroup +gidNumber: 1001 +memberUid: user1 +memberUid: user2 +memberUid: user3 +memberUid: user4 +memberUid: user5 + +# missing members + +dn: cn=missing-users,ou=Groups,dc=example,dc=org +cn: missing-users +objectClass: groupOfNames +member: uid=user1,ou=People,dc=example,dc=org +member: uid=user2,ou=People,dc=example,dc=org +member: uid=nonexistent-user,ou=People,dc=example,dc=org diff --git a/test/integration/test_add.rb b/test/integration/test_add.rb new file mode 100644 index 00000000..108fd93b --- /dev/null +++ b/test/integration/test_add.rb @@ -0,0 +1,26 @@ +require_relative '../test_helper' + +class TestAddIntegration < LDAPIntegrationTestCase + def setup + super + @dn = "uid=added-user1,ou=People,dc=example,dc=org" + end + + def test_add + attrs = { + objectclass: %w(top inetOrgPerson organizationalPerson person), + uid: "added-user1", + cn: "added-user1", + sn: "added-user1", + mail: "added-user1@rubyldap.com", + } + + assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect + + assert result = @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject).first + end + + def teardown + @ldap.delete dn: @dn + end +end diff --git a/test/integration/test_ber.rb b/test/integration/test_ber.rb new file mode 100644 index 00000000..4464bf78 --- /dev/null +++ b/test/integration/test_ber.rb @@ -0,0 +1,30 @@ +require_relative '../test_helper' + +class TestBERIntegration < LDAPIntegrationTestCase + # Test whether the TRUE boolean value is encoded correctly by performing a + # search operation. + def test_true_ber_encoding + # request these attrs to simplify test; use symbols to match Entry#attribute_names + attrs = [:dn, :uid, :cn, :mail] + + assert types_entry = @ldap.search( + base: "dc=example,dc=org", + filter: "(uid=user1)", + size: 1, + attributes: attrs, + attributes_only: true, + ).first + + # matches attributes we requested + assert_equal attrs, types_entry.attribute_names + + # assert values are empty + types_entry.each do |name, values| + next if name == :dn + assert values.empty? + end + + assert_includes Net::LDAP::ResultCodesSearchSuccess, + @ldap.get_operation_result.code, "should be a successful search operation" + end +end diff --git a/test/integration/test_bind.rb b/test/integration/test_bind.rb new file mode 100644 index 00000000..4a1a0194 --- /dev/null +++ b/test/integration/test_bind.rb @@ -0,0 +1,230 @@ +require_relative '../test_helper' + +class TestBindIntegration < LDAPIntegrationTestCase + INTEGRATION_HOSTNAME = 'ldap.example.org'.freeze + + def test_bind_success + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_timeout + @ldap.host = "10.255.255.1" # non-routable IP + + error = assert_raise Net::LDAP::Error do + @ldap.bind BIND_CREDS + end + msgs = ['Operation timed out - user specified timeout', + 'Connection timed out - user specified timeout'] + assert_send([msgs, :include?, error.message]) + end + + def test_bind_anonymous_fail + refute @ldap.bind(BIND_CREDS.merge(password: '')), + @ldap.get_operation_result.inspect + + result = @ldap.get_operation_result + assert_equal Net::LDAP::ResultCodeUnwillingToPerform, result.code + assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform], result.message + assert_equal "unauthenticated bind (DN with no password) disallowed", + result.error_message + assert_equal "", result.matched_dn + end + + def test_bind_fail + refute @ldap.bind(BIND_CREDS.merge(password: "not my password")), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_cafile + omit "We need to update our CA cert" + @ldap.host = INTEGRATION_HOSTNAME + @ldap.encryption( + method: :start_tls, + tls_options: TLS_OPTS.merge(ca_file: CA_FILE), + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_bad_hostname_verify_none_no_ca_passes + @ldap.host = INTEGRATION_HOSTNAME + @ldap.encryption( + method: :start_tls, + tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE }, + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_bad_hostname_verify_none_no_ca_opt_merge_passes + @ldap.host = 'cert.mismatch.example.org' + @ldap.encryption( + method: :start_tls, + tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_NONE), + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_bad_hostname_verify_peer_ca_fails + omit "We need to update our CA cert" + @ldap.host = 'cert.mismatch.example.org' + @ldap.encryption( + method: :start_tls, + tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER, + ca_file: CA_FILE }, + ) + error = assert_raise Net::LDAP::Error, + Errno::ECONNREFUSED do + @ldap.bind BIND_CREDS + end + assert_equal( + "hostname \"#{@ldap.host}\" does not match the server certificate", + error.message, + ) + end + + def test_bind_tls_with_bad_hostname_ca_default_opt_merge_fails + omit "We need to update our CA cert" + @ldap.host = 'cert.mismatch.example.org' + @ldap.encryption( + method: :start_tls, + tls_options: TLS_OPTS.merge(ca_file: CA_FILE), + ) + error = assert_raise Net::LDAP::Error, + Errno::ECONNREFUSED do + @ldap.bind BIND_CREDS + end + assert_equal( + "hostname \"#{@ldap.host}\" does not match the server certificate", + error.message, + ) + end + + def test_bind_tls_with_bad_hostname_ca_no_opt_merge_fails + omit "We need to update our CA cert" + @ldap.host = 'cert.mismatch.example.org' + @ldap.encryption( + method: :start_tls, + tls_options: { ca_file: CA_FILE }, + ) + error = assert_raise Net::LDAP::Error, + Errno::ECONNREFUSED do + @ldap.bind BIND_CREDS + end + assert_equal( + "hostname \"#{@ldap.host}\" does not match the server certificate", + error.message, + ) + end + + def test_bind_tls_with_valid_hostname_default_opts_passes + omit "We need to update our CA cert" + @ldap.host = INTEGRATION_HOSTNAME + @ldap.encryption( + method: :start_tls, + tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_PEER, + ca_file: CA_FILE), + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_valid_hostname_just_verify_peer_ca_passes + omit "We need to update our CA cert" + @ldap.host = INTEGRATION_HOSTNAME + @ldap.encryption( + method: :start_tls, + tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER, + ca_file: CA_FILE }, + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_bogus_hostname_system_ca_fails + @ldap.host = 'cert.mismatch.example.org' + @ldap.encryption(method: :start_tls, tls_options: {}) + error = assert_raise Net::LDAP::Error, + Errno::ECONNREFUSED do + @ldap.bind BIND_CREDS + end + assert_equal( + "hostname \"#{@ldap.host}\" does not match the server certificate", + error.message, + ) + end + + def test_bind_tls_with_multiple_hosts + omit "We need to update our CA cert" + @ldap.host = nil + @ldap.hosts = [[INTEGRATION_HOSTNAME, 389], [INTEGRATION_HOSTNAME, 389]] + @ldap.encryption( + method: :start_tls, + tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_PEER, + ca_file: CA_FILE), + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_multiple_bogus_hosts + # omit "We need to update our CA cert" + @ldap.host = nil + @ldap.hosts = [['cert.mismatch.example.org', 389], ['bogus.example.com', 389]] + @ldap.encryption( + method: :start_tls, + tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_PEER, + ca_file: CA_FILE), + ) + error = assert_raise Net::LDAP::Error, + Net::LDAP::ConnectionError do + @ldap.bind BIND_CREDS + end + assert_equal("Unable to connect to any given server: ", + error.message.split("\n").shift) + end + + def test_bind_tls_with_multiple_bogus_hosts_no_verification + omit "We need to update our CA cert" + @ldap.host = nil + @ldap.hosts = [['cert.mismatch.example.org', 389], ['bogus.example.com', 389]] + @ldap.encryption( + method: :start_tls, + tls_options: TLS_OPTS.merge(verify_mode: OpenSSL::SSL::VERIFY_NONE), + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end + + def test_bind_tls_with_multiple_bogus_hosts_ca_check_only_fails + @ldap.host = nil + @ldap.hosts = [['cert.mismatch.example.org', 389], ['bogus.example.com', 389]] + @ldap.encryption( + method: :start_tls, + tls_options: { ca_file: CA_FILE }, + ) + error = assert_raise Net::LDAP::Error, + Net::LDAP::ConnectionError do + @ldap.bind BIND_CREDS + end + assert_equal("Unable to connect to any given server: ", + error.message.split("\n").shift) + end + + # This test is CI-only because we can't add the fixture CA + # to the system CA store on people's dev boxes. + def test_bind_tls_valid_hostname_system_ca_on_travis_passes + omit "not sure how to install custom CA cert in travis" + omit_unless ENV['TRAVIS'] == 'true' + + @ldap.host = INTEGRATION_HOSTNAME + @ldap.encryption( + method: :start_tls, + tls_options: { verify_mode: OpenSSL::SSL::VERIFY_PEER }, + ) + assert @ldap.bind(BIND_CREDS), + @ldap.get_operation_result.inspect + end +end diff --git a/test/integration/test_delete.rb b/test/integration/test_delete.rb new file mode 100644 index 00000000..20e3414c --- /dev/null +++ b/test/integration/test_delete.rb @@ -0,0 +1,62 @@ +require_relative '../test_helper' + +class TestDeleteIntegration < LDAPIntegrationTestCase + def setup + super + @dn = "uid=delete-user1,ou=People,dc=example,dc=org" + + attrs = { + objectclass: %w(top inetOrgPerson organizationalPerson person), + uid: "delete-user1", + cn: "delete-user1", + sn: "delete-user1", + mail: "delete-user1@rubyldap.com", + } + unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) + assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect + end + assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) + + @parent_dn = "uid=parent,ou=People,dc=example,dc=org" + parent_attrs = { + objectclass: %w(top inetOrgPerson organizationalPerson person), + uid: "parent", + cn: "parent", + sn: "parent", + mail: "parent@rubyldap.com", + } + @child_dn = "uid=child,uid=parent,ou=People,dc=example,dc=org" + child_attrs = { + objectclass: %w(top inetOrgPerson organizationalPerson person), + uid: "child", + cn: "child", + sn: "child", + mail: "child@rubyldap.com", + } + unless @ldap.search(base: @parent_dn, scope: Net::LDAP::SearchScope_BaseObject) + assert @ldap.add(dn: @parent_dn, attributes: parent_attrs), @ldap.get_operation_result.inspect + assert @ldap.add(dn: @child_dn, attributes: child_attrs), @ldap.get_operation_result.inspect + end + assert @ldap.search(base: @parent_dn, scope: Net::LDAP::SearchScope_BaseObject) + assert @ldap.search(base: @child_dn, scope: Net::LDAP::SearchScope_BaseObject) + end + + def test_delete + assert @ldap.delete(dn: @dn), @ldap.get_operation_result.inspect + refute @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) + + result = @ldap.get_operation_result + assert_equal Net::LDAP::ResultCodeNoSuchObject, result.code + assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeNoSuchObject], result.message + end + + def test_delete_tree + assert @ldap.delete_tree(dn: @parent_dn), @ldap.get_operation_result.inspect + refute @ldap.search(base: @parent_dn, scope: Net::LDAP::SearchScope_BaseObject) + refute @ldap.search(base: @child_dn, scope: Net::LDAP::SearchScope_BaseObject) + + result = @ldap.get_operation_result + assert_equal Net::LDAP::ResultCodeNoSuchObject, result.code + assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeNoSuchObject], result.message + end +end diff --git a/test/integration/test_open.rb b/test/integration/test_open.rb new file mode 100644 index 00000000..9ce36d72 --- /dev/null +++ b/test/integration/test_open.rb @@ -0,0 +1,87 @@ +require_relative '../test_helper' + +class TestBindIntegration < LDAPIntegrationTestCase + def test_binds_without_open + events = @service.subscribe "bind.net_ldap_connection" + + @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) + @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) + + assert_equal 2, events.size + end + + def test_binds_with_open + events = @service.subscribe "bind.net_ldap_connection" + + @ldap.open do + @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) + @ldap.search(filter: "uid=user1", base: "ou=People,dc=example,dc=org", ignore_server_caps: true) + end + + assert_equal 1, events.size + end + + # NOTE: query for two or more entries so that the socket must be read + # multiple times. + # See The Problem: https://github.com/ruby-ldap/ruby-net-ldap/issues/136 + + def test_nested_search_without_open + entries = [] + nested_entry = nil + + @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=example,dc=org") do |entry| + entries << entry.uid.first + nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=example,dc=org").first + end + + assert_equal "user3", nested_entry.uid.first + assert_equal %w(user1 user2), entries + end + + def test_nested_search_with_open + entries = [] + nested_entry = nil + + @ldap.open do + @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=example,dc=org") do |entry| + entries << entry.uid.first + nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=example,dc=org").first + end + end + + assert_equal "user3", nested_entry.uid.first + assert_equal %w(user1 user2), entries + end + + def test_nested_add_with_open + entries = [] + nested_entry = nil + + dn = "uid=nested-open-added-user1,ou=People,dc=example,dc=org" + attrs = { + objectclass: %w(top inetOrgPerson organizationalPerson person), + uid: "nested-open-added-user1", + cn: "nested-open-added-user1", + sn: "nested-open-added-user1", + mail: "nested-open-added-user1@rubyldap.com", + } + + @ldap.delete dn: dn + + @ldap.open do + @ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=example,dc=org") do |entry| + entries << entry.uid.first + + nested_entry ||= begin + assert @ldap.add(dn: dn, attributes: attrs), @ldap.get_operation_result.inspect + @ldap.search(base: dn, scope: Net::LDAP::SearchScope_BaseObject).first + end + end + end + + assert_equal %w(user1 user2), entries + assert_equal "nested-open-added-user1", nested_entry.uid.first + ensure + @ldap.delete dn: dn + end +end diff --git a/test/integration/test_password_modify.rb b/test/integration/test_password_modify.rb new file mode 100644 index 00000000..e7d8d670 --- /dev/null +++ b/test/integration/test_password_modify.rb @@ -0,0 +1,111 @@ +require_relative '../test_helper' + +class TestPasswordModifyIntegration < LDAPIntegrationTestCase + # see: https://www.rfc-editor.org/rfc/rfc3062#section-2 + PASSWORD_MODIFY_SYNTAX = Net::BER.compile_syntax( + application: {}, + universal: {}, + context_specific: { primitive: { 0 => :string } }, + ) + + def setup + super + @admin_account = { dn: 'cn=admin,dc=example,dc=org', password: 'admin', method: :simple } + @ldap.authenticate @admin_account[:dn], @admin_account[:password] + + @dn = 'uid=modify-password-user1,ou=People,dc=example,dc=org' + + attrs = { + objectclass: %w(top inetOrgPerson organizationalPerson person), + uid: 'modify-password-user1', + cn: 'modify-password-user1', + sn: 'modify-password-user1', + mail: 'modify-password-user1@rubyldap.com', + userPassword: 'admin', + } + unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) + assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect + end + assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject) + + @auth = { + method: :simple, + username: @dn, + password: 'admin', + } + end + + def test_password_modify + assert @ldap.password_modify(dn: @dn, + auth: @auth, + old_password: 'admin', + new_password: 'passworD2') + + assert @ldap.get_operation_result.extended_response.nil?, + 'Should not have generated a new password' + + refute @ldap.bind(username: @dn, password: 'admin', method: :simple), + 'Old password should no longer be valid' + + assert @ldap.bind(username: @dn, password: 'passworD2', method: :simple), + 'New password should be valid' + end + + def test_password_modify_generate + assert @ldap.password_modify(dn: @dn, + auth: @auth, + old_password: 'admin') + + passwd_modify_response_value = @ldap.get_operation_result.extended_response + seq = Net::BER::BerIdentifiedArray.new + sio = StringIO.new(passwd_modify_response_value) + until (e = sio.read_ber(PASSWORD_MODIFY_SYNTAX)).nil? + seq << e + end + generated_password = seq[0][0] + + assert generated_password, 'Should have generated a password' + + refute @ldap.bind(username: @dn, password: 'admin', method: :simple), + 'Old password should no longer be valid' + + assert @ldap.bind(username: @dn, password: generated_password, method: :simple), + 'New password should be valid' + end + + def test_password_modify_generate_no_old_password + assert @ldap.password_modify(dn: @dn, + auth: @auth) + + passwd_modify_response_value = @ldap.get_operation_result.extended_response + seq = Net::BER::BerIdentifiedArray.new + sio = StringIO.new(passwd_modify_response_value) + until (e = sio.read_ber(PASSWORD_MODIFY_SYNTAX)).nil? + seq << e + end + generated_password = seq[0][0] + assert generated_password, 'Should have generated a password' + + refute @ldap.bind(username: @dn, password: 'admin', method: :simple), + 'Old password should no longer be valid' + + assert @ldap.bind(username: @dn, password: generated_password, method: :simple), + 'New password should be valid' + end + + def test_password_modify_overwrite_old_password + assert @ldap.password_modify(dn: @dn, + auth: @admin_account, + new_password: 'passworD3') + + refute @ldap.bind(username: @dn, password: 'admin', method: :simple), + 'Old password should no longer be valid' + + assert @ldap.bind(username: @dn, password: 'passworD3', method: :simple), + 'New password should be valid' + end + + def teardown + @ldap.delete dn: @dn + end +end diff --git a/test/integration/test_return_codes.rb b/test/integration/test_return_codes.rb new file mode 100644 index 00000000..30057a2a --- /dev/null +++ b/test/integration/test_return_codes.rb @@ -0,0 +1,46 @@ +require_relative '../test_helper' + +# NOTE: These tests depend on the OpenLDAP retcode overlay. +# See: section 12.12 http://www.openldap.org/doc/admin24/overlays.html + +class TestReturnCodeIntegration < LDAPIntegrationTestCase + def test_open_error + @ldap.authenticate "cn=fake", "creds" + @ldap.open do + result = @ldap.get_operation_result + assert_equal Net::LDAP::ResultCodeInvalidCredentials, result.code + end + end + + def test_operations_error + refute @ldap.search(filter: "cn=operationsError", base: "ou=Retcodes,dc=example,dc=org") + assert result = @ldap.get_operation_result + + assert_equal Net::LDAP::ResultCodeOperationsError, result.code + assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeOperationsError], result.message + end + + def test_protocol_error + refute @ldap.search(filter: "cn=protocolError", base: "ou=Retcodes,dc=example,dc=org") + assert result = @ldap.get_operation_result + + assert_equal Net::LDAP::ResultCodeProtocolError, result.code + assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeProtocolError], result.message + end + + def test_time_limit_exceeded + assert @ldap.search(filter: "cn=timeLimitExceeded", base: "ou=Retcodes,dc=example,dc=org") + assert result = @ldap.get_operation_result + + assert_equal Net::LDAP::ResultCodeTimeLimitExceeded, result.code + assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeTimeLimitExceeded], result.message + end + + def test_size_limit_exceeded + assert @ldap.search(filter: "cn=sizeLimitExceeded", base: "ou=Retcodes,dc=example,dc=org") + assert result = @ldap.get_operation_result + + assert_equal Net::LDAP::ResultCodeSizeLimitExceeded, result.code + assert_equal Net::LDAP::ResultStrings[Net::LDAP::ResultCodeSizeLimitExceeded], result.message + end +end diff --git a/test/integration/test_search.rb b/test/integration/test_search.rb new file mode 100644 index 00000000..1f562c22 --- /dev/null +++ b/test/integration/test_search.rb @@ -0,0 +1,77 @@ +require_relative '../test_helper' + +class TestSearchIntegration < LDAPIntegrationTestCase + def test_search + entries = [] + + result = @ldap.search(base: "dc=example,dc=org") do |entry| + assert_kind_of Net::LDAP::Entry, entry + entries << entry + end + + refute entries.empty? + assert_equal entries, result + end + + def test_search_without_result + entries = [] + + result = @ldap.search(base: "dc=example,dc=org", return_result: false) do |entry| + assert_kind_of Net::LDAP::Entry, entry + entries << entry + end + + assert result + refute_equal entries, result + end + + def test_search_filter_string + entries = @ldap.search(base: "dc=example,dc=org", filter: "(uid=user1)") + assert_equal 1, entries.size + end + + def test_search_filter_object + filter = Net::LDAP::Filter.eq("uid", "user1") | Net::LDAP::Filter.eq("uid", "user2") + entries = @ldap.search(base: "dc=example,dc=org", filter: filter) + assert_equal 2, entries.size + end + + def test_search_constrained_attributes + entry = @ldap.search(base: "uid=user1,ou=People,dc=example,dc=org", attributes: ["cn", "sn"]).first + assert_equal [:cn, :dn, :sn], entry.attribute_names.sort # :dn is always included + assert_empty entry[:mail] + end + + def test_search_attributes_only + entry = @ldap.search(base: "uid=user1,ou=People,dc=example,dc=org", attributes_only: true).first + + assert_empty entry[:cn], "unexpected attribute value: #{entry[:cn]}" + end + + def test_search_timeout + entries = [] + events = @service.subscribe "search.net_ldap_connection" + + result = @ldap.search(base: "dc=example,dc=org", time: 5) do |entry| + assert_kind_of Net::LDAP::Entry, entry + entries << entry + end + + payload, = events.pop + assert_equal 5, payload[:time] + assert_equal entries, result + end + + # http://tools.ietf.org/html/rfc4511#section-4.5.1.4 + def test_search_with_size + entries = [] + + result = @ldap.search(base: "dc=example,dc=org", size: 1) do |entry| + assert_kind_of Net::LDAP::Entry, entry + entries << entry + end + + assert_equal 1, result.size + assert_equal entries, result + end +end diff --git a/test/support/vm/openldap/.gitignore b/test/support/vm/openldap/.gitignore new file mode 100644 index 00000000..dace7081 --- /dev/null +++ b/test/support/vm/openldap/.gitignore @@ -0,0 +1 @@ +/.vagrant diff --git a/test/test_auth_adapter.rb b/test/test_auth_adapter.rb new file mode 100644 index 00000000..9e4c6002 --- /dev/null +++ b/test/test_auth_adapter.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class TestAuthAdapter < Test::Unit::TestCase + class FakeSocket + def initialize(*args) + end + end + + def test_undefined_auth_adapter + conn = Net::LDAP::Connection.new(host: 'ldap.example.com', port: 379, :socket_class => FakeSocket) + assert_raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (foo)" do + conn.bind(method: :foo) + end + end +end diff --git a/test/test_dn.rb b/test/test_dn.rb new file mode 100644 index 00000000..52e87bd7 --- /dev/null +++ b/test/test_dn.rb @@ -0,0 +1,57 @@ +require_relative 'test_helper' +require_relative '../lib/net/ldap/dn' + +class TestDN < Test::Unit::TestCase + def test_escape + assert_equal '\\,\\+\\"\\\\\\<\\>\\;', Net::LDAP::DN.escape(',+"\\<>;') + end + + def test_escape_pound_sign + assert_equal '\\#test', Net::LDAP::DN.escape('#test') + end + + def test_escape_space + assert_equal '\\ before_after\\ ', Net::LDAP::DN.escape(' before_after ') + end + + def test_retain_spaces + dn = Net::LDAP::DN.new('CN=Foo.bar.baz, OU=Foo \ ,OU=\ Bar, O=Baz') + assert_equal "CN=Foo.bar.baz, OU=Foo \\ ,OU=\\ Bar, O=Baz", dn.to_s + assert_equal ["CN", "Foo.bar.baz", "OU", "Foo ", "OU", " Bar", "O", "Baz"], dn.to_a + end + + def test_escape_on_initialize + dn = Net::LDAP::DN.new('cn', ',+"\\<>;', 'ou=company') + assert_equal 'cn=\\,\\+\\"\\\\\\<\\>\\;,ou=company', dn.to_s + end + + def test_to_a + dn = Net::LDAP::DN.new('cn=James, ou=Company\\,\\20LLC') + assert_equal ['cn', 'James', 'ou', 'Company, LLC'], dn.to_a + end + + def test_to_a_parenthesis + dn = Net::LDAP::DN.new('cn = \ James , ou = "Comp\28ny" ') + assert_equal ['cn', ' James ', 'ou', 'Comp(ny'], dn.to_a + end + + def test_to_a_hash_symbol + dn = Net::LDAP::DN.new('1.23.4= #A3B4D5 ,ou=Company') + assert_equal ['1.23.4', '#A3B4D5', 'ou', 'Company'], dn.to_a + end + + def test_bad_input_raises_error + [ + 'cn=James,', + 'cn=#aa aa', + 'cn="James', + 'cn=J\ames', + 'cn=\\', + '1.2.d=Value', + 'd1.2=Value', + ].each do |input| + dn = Net::LDAP::DN.new(input) + assert_raises(Net::LDAP::InvalidDNError) { dn.to_a } + end + end +end diff --git a/test/test_entry.rb b/test/test_entry.rb index 73898d67..60c89ba6 100644 --- a/test/test_entry.rb +++ b/test/test_entry.rb @@ -1,59 +1,92 @@ -require 'common' +require_relative 'test_helper' -=begin class TestEntry < Test::Unit::TestCase -Commented out until I can make it a spec. - context "An instance of Entry" do - setup do - @entry = Net::LDAP::Entry.new 'cn=Barbara,o=corp' - end - - should "be initialized with the DN" do - assert_equal 'cn=Barbara,o=corp', @entry.dn - end - - should 'return an empty array when accessing a nonexistent attribute (index lookup)' do - assert_equal [], @entry['sn'] - end - - should 'return an empty array when accessing a nonexistent attribute (method call)' do - assert_equal [], @entry.sn - end - - should 'create an attribute on assignment (index lookup)' do - @entry['sn'] = 'Jensen' - assert_equal ['Jensen'], @entry['sn'] - end - - should 'create an attribute on assignment (method call)' do - @entry.sn = 'Jensen' - assert_equal ['Jensen'], @entry.sn - end - - should 'have attributes accessible by index lookup' do - @entry['sn'] = 'Jensen' - assert_equal ['Jensen'], @entry['sn'] - end - - should 'have attributes accessible using a Symbol as the index' do - @entry[:sn] = 'Jensen' - assert_equal ['Jensen'], @entry[:sn] - end - - should 'have attributes accessible by method call' do - @entry['sn'] = 'Jensen' - assert_equal ['Jensen'], @entry.sn - end - - should 'ignore case of attribute names' do - @entry['sn'] = 'Jensen' - assert_equal ['Jensen'], @entry.sn - assert_equal ['Jensen'], @entry.Sn - assert_equal ['Jensen'], @entry.SN - assert_equal ['Jensen'], @entry['sn'] - assert_equal ['Jensen'], @entry['Sn'] - assert_equal ['Jensen'], @entry['SN'] - end - end + def setup + @entry = Net::LDAP::Entry.new 'cn=Barbara,o=corp' + end + + def test_dn + assert_equal 'cn=Barbara,o=corp', @entry.dn + end + + def test_empty_array_when_accessing_nonexistent_attribute + assert_equal [], @entry['sn'] + end + + def test_attribute_assignment + @entry['sn'] = 'Jensen' + assert_equal ['Jensen'], @entry['sn'] + assert_equal ['Jensen'], @entry.sn + assert_equal ['Jensen'], @entry[:sn] + + @entry[:sn] = 'Jensen' + assert_equal ['Jensen'], @entry['sn'] + assert_equal ['Jensen'], @entry.sn + assert_equal ['Jensen'], @entry[:sn] + + @entry.sn = 'Jensen' + assert_equal ['Jensen'], @entry['sn'] + assert_equal ['Jensen'], @entry.sn + assert_equal ['Jensen'], @entry[:sn] + end + + def test_case_insensitive_attribute_names + @entry['sn'] = 'Jensen' + assert_equal ['Jensen'], @entry.sn + assert_equal ['Jensen'], @entry.Sn + assert_equal ['Jensen'], @entry.SN + assert_equal ['Jensen'], @entry['sn'] + assert_equal ['Jensen'], @entry['Sn'] + assert_equal ['Jensen'], @entry['SN'] + end + + def test_to_h + @entry['sn'] = 'Jensen' + expected = { + dn: ['cn=Barbara,o=corp'], + sn: ['Jensen'], + } + duplicate = @entry.to_h + assert_equal expected, duplicate + + # check that changing the duplicate + # does not affect the internal state + duplicate.delete(:sn) + assert_not_equal duplicate, @entry.to_h + end + + def test_equal_operator + entry_two = Net::LDAP::Entry.new 'cn=Barbara,o=corp' + assert_equal @entry, entry_two + + @entry['sn'] = 'Jensen' + assert_not_equal @entry, entry_two + + entry_two['sn'] = 'Jensen' + assert_equal @entry, entry_two + end +end + +class TestEntryLDIF < Test::Unit::TestCase + def setup + @entry = Net::LDAP::Entry.from_single_ldif_string( + %Q{dn: something +foo: foo +barAttribute: bar + }, + ) + end + + def test_attribute + assert_equal ['foo'], @entry.foo + assert_equal ['foo'], @entry.Foo + end + + def test_modify_attribute + @entry.foo = 'bar' + assert_equal ['bar'], @entry.foo + + @entry.fOo = 'baz' + assert_equal ['baz'], @entry.foo + end end -=end diff --git a/test/test_filter.rb b/test/test_filter.rb index 03436e03..807c86dd 100644 --- a/test/test_filter.rb +++ b/test/test_filter.rb @@ -1,4 +1,4 @@ -require 'common' +require_relative 'test_helper' class TestFilter < Test::Unit::TestCase Filter = Net::LDAP::Filter @@ -9,15 +9,15 @@ def test_bug_7534_rfc2254 end def test_invalid_filter_string - assert_raises(Net::LDAP::LdapError) { Filter.from_rfc2254("") } + assert_raises(Net::LDAP::FilterSyntaxInvalidError) { Filter.from_rfc2254("") } end def test_invalid_filter - assert_raises(Net::LDAP::LdapError) { + assert_raises(Net::LDAP::OperatorError) do # This test exists to prove that our constructor blocks unknown filter # types. All filters must be constructed using helpers. Filter.__send__(:new, :xx, nil, nil) - } + end end def test_to_s @@ -28,7 +28,7 @@ def test_convenience_filters assert_equal("(uid=\\2A)", Filter.equals("uid", "*").to_s) assert_equal("(uid=\\28*)", Filter.begins("uid", "(").to_s) assert_equal("(uid=*\\29)", Filter.ends("uid", ")").to_s) - assert_equal("(uid=*\\5C*)", Filter.contains("uid", "\\").to_s) + assert_equal("(uid=*\\5C*)", Filter.contains("uid", "\\").to_s) end def test_c2 @@ -120,3 +120,104 @@ def test_ber_from_rfc2254_filter end end end + +# tests ported over from rspec. Not sure if these overlap with the above +# https://github.com/ruby-ldap/ruby-net-ldap/pull/121 +class TestFilterRSpec < Test::Unit::TestCase + def test_ex_convert + assert_equal '(foo:=bar)', Net::LDAP::Filter.ex('foo', 'bar').to_s + end + + def test_ex_rfc2254_roundtrip + filter = Net::LDAP::Filter.ex('foo', 'bar') + assert_equal filter, Net::LDAP::Filter.from_rfc2254(filter.to_s) + end + + def test_ber_conversion + filter = Net::LDAP::Filter.ex('foo', 'bar') + ber = filter.to_ber + assert_equal filter, Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) + end + + [ + '(o:dn:=Ace Industry)', + '(:dn:2.4.8.10:=Dino)', + '(cn:dn:1.2.3.4.5:=John Smith)', + '(sn:dn:2.4.6.8.10:=Barbara Jones)', + '(&(sn:dn:2.4.6.8.10:=Barbara Jones))', + ].each_with_index do |filter_str, index| + define_method "test_decode_filter_#{index}" do + filter = Net::LDAP::Filter.from_rfc2254(filter_str) + assert_kind_of Net::LDAP::Filter, filter + end + + define_method "test_ber_conversion_#{index}" do + filter = Net::LDAP::Filter.from_rfc2254(filter_str) + ber = Net::LDAP::Filter.from_rfc2254(filter_str).to_ber + assert_equal filter, Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) + end + end + + def test_apostrophes + assert_equal "(uid=O'Keefe)", Net::LDAP::Filter.construct("uid=O'Keefe").to_rfc2254 + end + + def test_equals + assert_equal Net::LDAP::Filter.eq('dn', 'f\2Aoo'), Net::LDAP::Filter.equals('dn', 'f*oo') + end + + def test_begins + assert_equal Net::LDAP::Filter.eq('dn', 'f\2Aoo*'), Net::LDAP::Filter.begins('dn', 'f*oo') + end + + def test_ends + assert_equal Net::LDAP::Filter.eq('dn', '*f\2Aoo'), Net::LDAP::Filter.ends('dn', 'f*oo') + end + + def test_contains + assert_equal Net::LDAP::Filter.eq('dn', '*f\2Aoo*'), Net::LDAP::Filter.contains('dn', 'f*oo') + end + + def test_escape + # escapes nul, *, (, ) and \\ + assert_equal "\\00\\2A\\28\\29\\5C", Net::LDAP::Filter.escape("\0*()\\") + end + + def test_well_known_ber_string + ber = "\xa4\x2d" \ + "\x04\x0b" "objectclass" \ + "\x30\x1e" \ + "\x80\x08" "foo" "*\\" "bar" \ + "\x81\x08" "foo" "*\\" "bar" \ + "\x82\x08" "foo" "*\\" "bar".b + + [ + "foo" "\\2A\\5C" "bar", + "foo" "\\2a\\5c" "bar", + "foo" "\\2A\\5c" "bar", + "foo" "\\2a\\5C" "bar", + ].each do |escaped| + # unescapes escaped characters + filter = Net::LDAP::Filter.eq("objectclass", "#{escaped}*#{escaped}*#{escaped}") + assert_equal ber, filter.to_ber + end + end + + def test_parse_ber_escapes_characters + ber = "\xa4\x2d" \ + "\x04\x0b" "objectclass" \ + "\x30\x1e" \ + "\x80\x08" "foo" "*\\" "bar" \ + "\x81\x08" "foo" "*\\" "bar" \ + "\x82\x08" "foo" "*\\" "bar".b + + escaped = Net::LDAP::Filter.escape("foo" "*\\" "bar") + filter = Net::LDAP::Filter.parse_ber(ber.read_ber(Net::LDAP::AsnSyntax)) + assert_equal "(objectclass=#{escaped}*#{escaped}*#{escaped})", filter.to_s + end + + def test_unescape_fixnums + filter = Net::LDAP::Filter.eq("objectclass", 3) + assert_equal "\xA3\x10\x04\vobjectclass\x04\x013".b, filter.to_ber + end +end diff --git a/test/test_filter_parser.rb b/test/test_filter_parser.rb new file mode 100644 index 00000000..960ff1ad --- /dev/null +++ b/test/test_filter_parser.rb @@ -0,0 +1,29 @@ +# encoding: utf-8 + +require_relative 'test_helper' + +class TestFilterParser < Test::Unit::TestCase + def test_ascii + assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=name)") + end + + def test_multibyte_characters + assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=名前)") + end + + def test_brackets + assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(cn=[{something}])") + end + + def test_slash + assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(departmentNumber=FOO//BAR/FOO)") + end + + def test_colons + 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)") + end + + def test_attr_tag + assert_kind_of Net::LDAP::Filter, Net::LDAP::Filter::FilterParser.parse("(mail;primary=jane@example.org)") + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..4a7600bd --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,73 @@ +# Add 'lib' to load path. +require 'test/unit' +require_relative '../lib/net/ldap' +require 'flexmock/test_unit' + +# Whether integration tests should be run. +INTEGRATION = ENV.fetch("https://github.com/INTEGRATION", "skip") != "skip" + +# The CA file to verify certs against for tests. +# Override with CA_FILE env variable; otherwise checks for the VM-specific path +# and falls back to the test/fixtures/cacert.pem for local testing. +CA_FILE = + ENV.fetch("https://github.com/CA_FILE") do + if File.exist?("/etc/ssl/certs/cacert.pem") + "/etc/ssl/certs/cacert.pem" + else + File.expand_path("fixtures/ca/docker-ca.pem", File.dirname(__FILE__)) + end + end + +BIND_CREDS = { + method: :simple, + username: "cn=admin,dc=example,dc=org", + password: "admin", +}.freeze + +TLS_OPTS = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.merge({}).freeze + +if RUBY_VERSION < "2.0" + class String + def b + self + end + end +end + +class MockInstrumentationService + def initialize + @events = {} + end + + def instrument(event, payload) + result = yield(payload) + @events[event] ||= [] + @events[event] << [payload, result] + result + end + + def subscribe(event) + @events[event] ||= [] + @events[event] + end +end + +class LDAPIntegrationTestCase < Test::Unit::TestCase + # If integration tests aren't enabled, noop these tests. + if !INTEGRATION + def run(*) + self + end + end + + def setup + @service = MockInstrumentationService.new + @ldap = Net::LDAP.new \ + host: ENV.fetch('https://github.com/INTEGRATION_HOST', 'localhost'), + port: ENV.fetch('https://github.com/INTEGRATION_PORT', 389), + search_domains: %w(dc=example,dc=org), + uid: 'uid', + instrumentation_service: @service + @ldap.authenticate "cn=admin,dc=example,dc=org", "admin" + end +end diff --git a/test/test_ldap.rb b/test/test_ldap.rb new file mode 100644 index 00000000..6c061475 --- /dev/null +++ b/test/test_ldap.rb @@ -0,0 +1,114 @@ +require_relative 'test_helper' + +class TestLDAPInstrumentation < Test::Unit::TestCase + # Fake Net::LDAP::Connection for testing + class FakeConnection + # It's difficult to instantiate Net::LDAP::PDU objects. Faking out what we + # need here until that object is brought under test and has it's constructor + # cleaned up. + class Result < Struct.new(:success?, :result_code); end + + def initialize + @bind_success = Result.new(true, Net::LDAP::ResultCodeSuccess) + @search_success = Result.new(true, Net::LDAP::ResultCodeSizeLimitExceeded) + end + + def bind(args = {}) + @bind_success + end + + def search(*args) + yield @search_success if block_given? + @search_success + end + end + + def setup + @connection = flexmock(:connection, :close => true) + flexmock(Net::LDAP::Connection).should_receive(:new).and_return(@connection) + + @service = MockInstrumentationService.new + @subject = Net::LDAP.new \ + :host => "test.mocked.com", :port => 636, + :force_no_page => true, # so server capabilities are not queried + :instrumentation_service => @service + end + + def test_instrument_bind + events = @service.subscribe "bind.net_ldap" + + fake_connection = FakeConnection.new + @subject.connection = fake_connection + bind_result = fake_connection.bind + + assert @subject.bind + + payload, result = events.pop + assert result + assert_equal bind_result, payload[:bind] + end + + def test_instrument_search + events = @service.subscribe "search.net_ldap" + + fake_connection = FakeConnection.new + @subject.connection = fake_connection + entry = fake_connection.search + + refute_nil @subject.search(:filter => "(uid=user1)") + + payload, result = events.pop + assert_equal [entry], result + assert_equal [entry], payload[:result] + assert_equal "(uid=user1)", payload[:filter] + end + + def test_instrument_search_with_size + events = @service.subscribe "search.net_ldap" + + fake_connection = FakeConnection.new + @subject.connection = fake_connection + entry = fake_connection.search + + refute_nil @subject.search(:filter => "(uid=user1)", :size => 1) + + payload, result = events.pop + assert_equal [entry], result + assert_equal [entry], payload[:result] + assert_equal "(uid=user1)", payload[:filter] + assert_equal result.size, payload[:size] + end + + def test_obscure_auth + password = "opensesame" + assert_include(@subject.inspect, "anonymous") + @subject.auth "joe_user", password + assert_not_include(@subject.inspect, password) + end + + def test_encryption + enc = @subject.encryption('start_tls') + + assert_equal enc[:method], :start_tls + end + + def test_normalize_encryption_symbol + enc = @subject.send(:normalize_encryption, :start_tls) + assert_equal enc, :method => :start_tls, :tls_options => {} + end + + def test_normalize_encryption_nil + enc = @subject.send(:normalize_encryption, nil) + assert_equal enc, nil + end + + def test_normalize_encryption_string + enc = @subject.send(:normalize_encryption, 'start_tls') + assert_equal enc, :method => :start_tls, :tls_options => {} + end + + def test_normalize_encryption_hash + enc = @subject.send(:normalize_encryption, :method => :start_tls, :tls_options => { :foo => :bar }) + assert_equal enc, :method => :start_tls, :tls_options => { :foo => :bar } + end +end diff --git a/test/test_ldap_connection.rb b/test/test_ldap_connection.rb index a3643016..fdfa418c 100644 --- a/test/test_ldap_connection.rb +++ b/test/test_ldap_connection.rb @@ -1,24 +1,588 @@ -require 'common' +require_relative 'test_helper' + +class TestLDAPConnection < Test::Unit::TestCase + def capture_stderr + stderr, $stderr = $stderr, StringIO.new + yield + $stderr.string + ensure + $stderr = stderr + end + + # Fake socket for testing + # + # FakeTCPSocket.new("success", 636) + # FakeTCPSocket.new("fail.SocketError", 636) # raises SocketError + class FakeTCPSocket + def initialize(host, port, socket_opts = {}) + status, error = host.split(".") + raise Object.const_get(error) if status == "fail" + end + end + + def test_list_of_hosts_with_first_host_successful + hosts = [ + ["success.host", 636], + ["fail.SocketError", 636], + ["fail.SocketError", 636], + ] + + connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket) + connection.socket + end + + def test_list_of_hosts_with_first_host_failure + hosts = [ + ["fail.SocketError", 636], + ["success.host", 636], + ["fail.SocketError", 636], + ] + + connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket) + connection.socket + end + + def test_list_of_hosts_with_all_hosts_failure + hosts = [ + ["fail.SocketError", 636], + ["fail.SocketError", 636], + ["fail.SocketError", 636], + ] + + connection = Net::LDAP::Connection.new(:hosts => hosts, :socket_class => FakeTCPSocket) + assert_raise Net::LDAP::ConnectionError do + connection.socket + end + end + + # This belongs in test_ldap, not test_ldap_connection + def test_result_for_connection_failed_is_set + flexmock(Socket).should_receive(:tcp).and_raise(Errno::ECONNREFUSED) + + ldap_client = Net::LDAP.new(host: '127.0.0.1', port: 12345) + + assert_raise Errno::ECONNREFUSED do + ldap_client.bind(method: :simple, username: 'asdf', password: 'asdf') + end + + assert_equal(ldap_client.get_operation_result.code, 52) + assert_equal(ldap_client.get_operation_result.message, 'Unavailable') + end + + def test_unresponsive_host + connection = Net::LDAP::Connection.new(:host => "fail.Errno::ETIMEDOUT", :port => 636, :socket_class => FakeTCPSocket) + assert_raise Net::LDAP::Error do + connection.socket + end + end + + def test_blocked_port + connection = Net::LDAP::Connection.new(:host => "fail.SocketError", :port => 636, :socket_class => FakeTCPSocket) + assert_raise Net::LDAP::Error do + connection.socket + end + end + + def test_connection_refused + connection = Net::LDAP::Connection.new(:host => "fail.Errno::ECONNREFUSED", :port => 636, :socket_class => FakeTCPSocket) + stderr = capture_stderr do + assert_raise Errno::ECONNREFUSED do + connection.socket + end + end + end + + def test_connection_timeout + connection = Net::LDAP::Connection.new(:host => "fail.Errno::ETIMEDOUT", :port => 636, :socket_class => FakeTCPSocket) + capture_stderr do + assert_raise Net::LDAP::Error do + connection.socket + end + end + end + + def test_raises_unknown_exceptions + connection = Net::LDAP::Connection.new(:host => "fail.StandardError", :port => 636, :socket_class => FakeTCPSocket) + assert_raise StandardError do + connection.socket + end + end -class TestLDAP < Test::Unit::TestCase def test_modify_ops_delete - args = { :operations => [ [ :delete, "mail" ] ] } + args = { :operations => [[:delete, "mail"]] } result = Net::LDAP::Connection.modify_ops(args[:operations]) - expected = [ "0\r\n\x01\x010\b\x04\x04mail1\x00" ] + expected = ["0\r\n\x01\x010\b\x04\x04mail1\x00"] assert_equal(expected, result) end def test_modify_ops_add - args = { :operations => [ [ :add, "mail", "testuser@example.com" ] ] } + args = { :operations => [[:add, "mail", "testuser@example.com"]] } result = Net::LDAP::Connection.modify_ops(args[:operations]) - expected = [ "0#\n\x01\x000\x1E\x04\x04mail1\x16\x04\x14testuser@example.com" ] + expected = ["0#\n\x01\x000\x1E\x04\x04mail1\x16\x04\x14testuser@example.com"] assert_equal(expected, result) end def test_modify_ops_replace - args = { :operations =>[ [ :replace, "mail", "testuser@example.com" ] ] } + args = { :operations => [[:replace, "mail", "testuser@example.com"]] } result = Net::LDAP::Connection.modify_ops(args[:operations]) - expected = [ "0#\n\x01\x020\x1E\x04\x04mail1\x16\x04\x14testuser@example.com" ] + expected = ["0#\n\x01\x020\x1E\x04\x04mail1\x16\x04\x14testuser@example.com"] assert_equal(expected, result) end + + def test_write + mock = flexmock("socket") + mock.should_receive(:write).with([1.to_ber, "request"].to_ber_sequence).and_return(true) + conn = Net::LDAP::Connection.new(:socket => mock) + conn.send(:write, "request") + end + + def test_write_with_controls + mock = flexmock("socket") + mock.should_receive(:write).with([1.to_ber, "request", "controls"].to_ber_sequence).and_return(true) + conn = Net::LDAP::Connection.new(:socket => mock) + conn.send(:write, "request", "controls") + end + + def test_write_increments_msgid + mock = flexmock("socket") + mock.should_receive(:write).with([1.to_ber, "request1"].to_ber_sequence).and_return(true) + mock.should_receive(:write).with([2.to_ber, "request2"].to_ber_sequence).and_return(true) + conn = Net::LDAP::Connection.new(:socket => mock) + conn.send(:write, "request1") + conn.send(:write, "request2") + end +end + +class TestLDAPConnectionSocketReads < Test::Unit::TestCase + def make_message(message_id, options = {}) + options = { + app_tag: Net::LDAP::PDU::SearchResult, + code: Net::LDAP::ResultCodeSuccess, + matched_dn: "", + error_message: "", + }.merge(options) + result = Net::BER::BerIdentifiedArray.new([options[:code], options[:matched_dn], options[:error_message]]) + result.ber_identifier = options[:app_tag] + [message_id, result] + end + + def test_queued_read_drains_queue_before_read + result1a = make_message(1, error_message: "one") + result1b = make_message(1, error_message: "two") + + mock = flexmock("socket") + mock.should_receive(:read_ber).and_return(result1b) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.message_queue[1].push Net::LDAP::PDU.new(result1a) + + assert msg1 = conn.queued_read(1) + assert msg2 = conn.queued_read(1) + + assert_equal 1, msg1.message_id + assert_equal "one", msg1.error_message + assert_equal 1, msg2.message_id + assert_equal "two", msg2.error_message + end + + def test_queued_read_reads_until_message_id_match + result1 = make_message(1) + result2 = make_message(2) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + conn = Net::LDAP::Connection.new(:socket => mock) + + assert result = conn.queued_read(2) + assert_equal 2, result.message_id + assert_equal 1, conn.queued_read(1).message_id + end + + def test_queued_read_modify + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::ModifyResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + conn.instance_variable_get("@msgid") + + assert result = conn.modify(dn: "uid=modified-user1,ou=People,dc=rubyldap,dc=com", + operations: [[:add, :mail, "modified-user1@example.com"]]) + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_add + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::AddResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.add(dn: "uid=added-user1,ou=People,dc=rubyldap,dc=com") + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_rename + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::ModifyRDNResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.rename( + olddn: "uid=renamable-user1,ou=People,dc=rubyldap,dc=com", + newrdn: "uid=renamed-user1", + ) + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_delete + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::DeleteResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.delete(dn: "uid=deletable-user1,ou=People,dc=rubyldap,dc=com") + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_setup_encryption_with_start_tls + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::ExtendedResponse) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + flexmock(Net::LDAP::Connection).should_receive(:wrap_with_ssl).with(mock, {}, nil, nil) + .and_return(mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.setup_encryption(method: :start_tls) + assert_equal mock, result + end + + def test_queued_read_bind_simple + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.bind( + method: :simple, + username: "uid=user1,ou=People,dc=rubyldap,dc=com", + password: "passworD1", + ) + assert result.success? + assert_equal 2, result.message_id + end + + def test_queued_read_bind_sasl + result1 = make_message(1, app_tag: Net::LDAP::PDU::SearchResult) + result2 = make_message(2, app_tag: Net::LDAP::PDU::BindResult) + + mock = flexmock("socket") + mock.should_receive(:read_ber) + .and_return(result1) + .and_return(result2) + mock.should_receive(:write) + conn = Net::LDAP::Connection.new(:socket => mock) + + conn.next_msgid # simulates ongoing query + + assert result = conn.bind( + method: :sasl, + mechanism: "fake", + initial_credential: "passworD1", + challenge_response: flexmock("challenge proc"), + ) + assert result.success? + assert_equal 2, result.message_id + end + + def test_invalid_pdu_type + options = { + code: Net::LDAP::ResultCodeSuccess, + matched_dn: "", + error_message: "", + } + ber = Net::BER::BerIdentifiedArray.new([options[:code], options[:matched_dn], options[:error_message]]) + assert_raise Net::LDAP::PDU::Error do + Net::LDAP::PDU.new([0, ber]) + end + end +end + +class TestLDAPConnectionErrors < Test::Unit::TestCase + def setup + @tcp_socket = flexmock(:connection) + @tcp_socket.should_receive(:write) + flexmock(Socket).should_receive(:tcp).and_return(@tcp_socket) + @connection = Net::LDAP::Connection.new(:host => 'test.mocked.com', :port => 636) + end + + def test_error_failed_operation + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeUnwillingToPerform, "", "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1"]) + ber.ber_identifier = Net::LDAP::PDU::ModifyResponse + @tcp_socket.should_receive(:read_ber).and_return([1, ber]) + + result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) + assert result.failure?, "should be failure" + assert_equal "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1", result.error_message + end + + def test_no_error_on_success + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::ModifyResponse + @tcp_socket.should_receive(:read_ber).and_return([1, ber]) + + result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]]) + assert result.success?, "should be success" + assert_equal "", result.error_message + end +end + +class TestLDAPConnectionInstrumentation < Test::Unit::TestCase + def setup + @tcp_socket = flexmock(:connection) + @tcp_socket.should_receive(:write) + flexmock(Socket).should_receive(:tcp).and_return(@tcp_socket) + + @service = MockInstrumentationService.new + @connection = Net::LDAP::Connection.new \ + :host => 'test.mocked.com', + :port => 636, + :instrumentation_service => @service + end + + def test_write_net_ldap_connection_event + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::BindResult + read_result = [1, ber] + @tcp_socket.should_receive(:read_ber).and_return(read_result) + + events = @service.subscribe "write.net_ldap_connection" + + result = @connection.bind(method: :anon) + assert result.success?, "should be success" + + # a write event + payload, result = events.pop + assert payload.key?(:result) + assert payload.key?(:content_length) + end + + def test_read_net_ldap_connection_event + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::BindResult + read_result = [1, ber] + @tcp_socket.should_receive(:read_ber).and_return(read_result) + + events = @service.subscribe "read.net_ldap_connection" + + result = @connection.bind(method: :anon) + assert result.success?, "should be success" + + # a read event + payload, result = events.pop + assert payload.key?(:result) + assert_equal read_result, result + end + + def test_parse_pdu_net_ldap_connection_event + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::BindResult + read_result = [1, ber] + @tcp_socket.should_receive(:read_ber).and_return(read_result) + + events = @service.subscribe "parse_pdu.net_ldap_connection" + + result = @connection.bind(method: :anon) + assert result.success?, "should be success" + + # a parse_pdu event + payload, result = events.pop + assert payload.key?(:pdu) + assert payload.key?(:app_tag) + assert payload.key?(:message_id) + assert_equal Net::LDAP::PDU::BindResult, payload[:app_tag] + assert_equal 1, payload[:message_id] + pdu = payload[:pdu] + assert_equal Net::LDAP::ResultCodeSuccess, pdu.result_code + end + + def test_bind_net_ldap_connection_event + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::BindResult + bind_result = [1, ber] + @tcp_socket.should_receive(:read_ber).and_return(bind_result) + + events = @service.subscribe "bind.net_ldap_connection" + + result = @connection.bind(method: :anon) + assert result.success?, "should be success" + + # a read event + payload, result = events.pop + assert payload.key?(:result) + assert result.success?, "should be success" + end + + def test_search_net_ldap_connection_event + # search data + search_data_ber = Net::BER::BerIdentifiedArray.new([1, [ + "uid=user1,ou=People,dc=rubyldap,dc=com", + [["uid", ["user1"]]], + ]]) + search_data_ber.ber_identifier = Net::LDAP::PDU::SearchReturnedData + search_data = [1, search_data_ber] + # search result (end of results) + search_result_ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + search_result_ber.ber_identifier = Net::LDAP::PDU::SearchResult + search_result = [1, search_result_ber] + @tcp_socket.should_receive(:read_ber).and_return(search_data) + .and_return(search_result) + + events = @service.subscribe "search.net_ldap_connection" + unread = @service.subscribe "search_messages_unread.net_ldap_connection" + + result = @connection.search(filter: "(uid=user1)", base: "ou=People,dc=rubyldap,dc=com") + assert result.success?, "should be success" + + # a search event + payload, result = events.pop + assert payload.key?(:result) + assert payload.key?(:filter) + assert_equal "(uid=user1)", payload[:filter].to_s + assert result + + # ensure no unread + assert unread.empty?, "should not have any leftover unread messages" + end + + def test_add_with_controls + dacl_flag = 0x4 # DACL_SECURITY_INFORMATION + control_values = [dacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber + controls = [] + # LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID + ldap_server_sd_flags = '1.2.840.113556.1.4.801'.freeze + controls << [ldap_server_sd_flags.to_ber, true.to_ber, control_values].to_ber_sequence + + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::AddResponse + @tcp_socket.should_receive(:read_ber).and_return([1, ber]) + + result = @connection.add(:dn => "uid=added-user1,ou=People,dc=rubyldap,dc=com", :controls => controls) + assert result.success?, "should be success" + assert_equal "", result.error_message + end + + def test_modify_with_controls + dacl_flag = 0x4 # DACL_SECURITY_INFORMATION + control_values = [dacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber + controls = [] + # LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID + ldap_server_sd_flags = '1.2.840.113556.1.4.801'.freeze + controls << [ldap_server_sd_flags.to_ber, true.to_ber, control_values].to_ber_sequence + + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::ModifyResponse + @tcp_socket.should_receive(:read_ber).and_return([1, ber]) + + result = @connection.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]], :controls => controls) + assert result.success?, "should be success" + assert_equal "", result.error_message + end + + def test_search_with_controls + # search data + search_data_ber = Net::BER::BerIdentifiedArray.new([1, [ + "uid=user1,ou=People,dc=rubyldap,dc=com", + [["uid", ["user1"]]], + ]]) + search_data_ber.ber_identifier = Net::LDAP::PDU::SearchReturnedData + search_data = [1, search_data_ber] + # search result (end of results) + search_result_ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + search_result_ber.ber_identifier = Net::LDAP::PDU::SearchResult + search_result = [1, search_result_ber] + @tcp_socket.should_receive(:read_ber).and_return(search_data) + .and_return(search_result) + + events = @service.subscribe "search.net_ldap_connection" + unread = @service.subscribe "search_messages_unread.net_ldap_connection" + + all_but_sacl_flag = 0x1 | 0x2 | 0x4 # OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION + control_values = [all_but_sacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber + controls = [] + # LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID + ldap_server_sd_flags = '1.2.840.113556.1.4.801'.freeze + controls << [ldap_server_sd_flags.to_ber, true.to_ber, control_values].to_ber_sequence + + result = @connection.search(filter: "(uid=user1)", base: "ou=People,dc=rubyldap,dc=com", controls: controls) + assert result.success?, "should be success" + + # a search event + payload, result = events.pop + assert payload.key?(:result) + assert payload.key?(:filter) + assert_equal "(uid=user1)", payload[:filter].to_s + assert result + + # ensure no unread + assert unread.empty?, "should not have any leftover unread messages" + end + + def test_ldapwhoami + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, '', '', 0, 'dn:uid=zerosteiner,ou=users,dc=example,dc=org']) + ber.ber_identifier = Net::LDAP::PDU::ExtendedResponse + response = [1, ber] + + @tcp_socket.should_receive(:read_ber).and_return(response) + + result = @connection.ldapwhoami + assert result.extended_response == 'dn:uid=zerosteiner,ou=users,dc=example,dc=org' + end end diff --git a/test/test_ldif.rb b/test/test_ldif.rb index fb4d5ee9..c74ea6e7 100644 --- a/test/test_ldif.rb +++ b/test/test_ldif.rb @@ -1,6 +1,6 @@ # $Id: testldif.rb 61 2006-04-18 20:55:55Z blackhedd $ -require 'common' +require_relative 'test_helper' require 'digest/sha1' require 'base64' @@ -13,52 +13,70 @@ def test_empty_ldif assert_equal(true, ds.empty?) end + def test_ldif_with_version + io = StringIO.new("version: 1") + ds = Net::LDAP::Dataset.read_ldif(io) + assert_equal "1", ds.version + end + def test_ldif_with_comments str = ["# Hello from LDIF-land", "# This is an unterminated comment"] io = StringIO.new(str[0] + "\r\n" + str[1]) - ds = Net::LDAP::Dataset::read_ldif(io) + ds = Net::LDAP::Dataset.read_ldif(io) assert_equal(str, ds.comments) end def test_ldif_with_password psw = "goldbricks" - hashed_psw = "{SHA}" + Base64::encode64(Digest::SHA1.digest(psw)).chomp + hashed_psw = "{SHA}" + Base64.encode64(Digest::SHA1.digest(psw)).chomp - ldif_encoded = Base64::encode64(hashed_psw).chomp - ds = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n")) + ldif_encoded = Base64.encode64(hashed_psw).chomp + ds = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: Goldbrick\r\nuserPassword:: #{ldif_encoded}\r\n\r\n")) recovered_psw = ds["Goldbrick"][:userpassword].shift assert_equal(hashed_psw, recovered_psw) end def test_ldif_with_continuation_lines - ds = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n")) - assert_equal(true, ds.has_key?("abcdefghijklmn")) + ds = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n")) + assert_equal(true, ds.key?("abcdefghijklmn")) end def test_ldif_with_continuation_lines_and_extra_whitespace - ds1 = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n")) - assert_equal(true, ds1.has_key?("abcdefg hijklmn")) - ds2 = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: abcdefg\r\n hij klmn\r\n\r\n")) - assert_equal(true, ds2.has_key?("abcdefghij klmn")) + ds1 = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: abcdefg\r\n hijklmn\r\n\r\n")) + assert_equal(true, ds1.key?("abcdefg hijklmn")) + ds2 = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: abcdefg\r\n hij klmn\r\n\r\n")) + assert_equal(true, ds2.key?("abcdefghij klmn")) end def test_ldif_tab_is_not_continuation - ds = Net::LDAP::Dataset::read_ldif(StringIO.new("dn: key\r\n\tnotcontinued\r\n\r\n")) - assert_equal(true, ds.has_key?("key")) + ds = Net::LDAP::Dataset.read_ldif(StringIO.new("dn: key\r\n\tnotcontinued\r\n\r\n")) + assert_equal(true, ds.key?("key")) + end + + def test_ldif_with_base64_dn + str = "dn:: Q049QmFzZTY0IGRuIHRlc3QsT1U9VGVzdCxPVT1Vbml0cyxEQz1leGFtcGxlLERDPWNvbQ==\r\n\r\n" + ds = Net::LDAP::Dataset.read_ldif(StringIO.new(str)) + assert_equal(true, ds.key?("CN=Base64 dn test,OU=Test,OU=Units,DC=example,DC=com")) + end + + def test_ldif_with_base64_dn_and_continuation_lines + str = "dn:: Q049QmFzZTY0IGRuIHRlc3Qgd2l0aCBjb250aW51YXRpb24gbGluZSxPVT1UZXN0LE9VPVVua\r\n XRzLERDPWV4YW1wbGUsREM9Y29t\r\n\r\n" + ds = Net::LDAP::Dataset.read_ldif(StringIO.new(str)) + assert_equal(true, ds.key?("CN=Base64 dn test with continuation line,OU=Test,OU=Units,DC=example,DC=com")) end # TODO, INADEQUATE. We need some more tests # to verify the content. def test_ldif - File.open(TestLdifFilename, "r") {|f| - ds = Net::LDAP::Dataset::read_ldif(f) + File.open(TestLdifFilename, "r") do |f| + ds = Net::LDAP::Dataset.read_ldif(f) assert_equal(13, ds.length) - } + end end # Must test folded lines and base64-encoded lines as well as normal ones. def test_to_ldif - data = File.open(TestLdifFilename, "rb") { |f| f.read } + data = File.open(TestLdifFilename, "rb", &:read) io = StringIO.new(data) # added .lines to turn to array because 1.9 doesn't have @@ -66,14 +84,21 @@ def test_to_ldif entries = data.lines.grep(/^dn:\s*/) { $'.chomp } dn_entries = entries.dup - ds = Net::LDAP::Dataset::read_ldif(io) { |type, value| + ds = Net::LDAP::Dataset.read_ldif(io) do |type, value| case type when :dn assert_equal(dn_entries.first, value) dn_entries.shift end - } + end assert_equal(entries.size, ds.size) assert_equal(entries.sort, ds.to_ldif.grep(/^dn:\s*/) { $'.chomp }) end + + def test_to_ldif_with_version + ds = Net::LDAP::Dataset.new + ds.version = "1" + + assert_equal "version: 1", ds.to_ldif_string.chomp + end end diff --git a/test/test_password.rb b/test/test_password.rb index abc8c22b..407cde94 100644 --- a/test/test_password.rb +++ b/test/test_password.rb @@ -1,17 +1,22 @@ # $Id: testpsw.rb 72 2006-04-24 21:58:14Z blackhedd $ -require 'common' +require_relative 'test_helper' class TestPassword < Test::Unit::TestCase - def test_psw - assert_equal( - "{MD5}xq8jwrcfibi0sZdZYNkSng==", - Net::LDAP::Password.generate( :md5, "cashflow" )) + assert_equal("{MD5}xq8jwrcfibi0sZdZYNkSng==", Net::LDAP::Password.generate(:md5, "cashflow")) + assert_equal("{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", Net::LDAP::Password.generate(:sha, "cashflow")) + end - assert_equal( - "{SHA}YE4eGkN4BvwNN1f5R7CZz0kFn14=", - Net::LDAP::Password.generate( :sha, "cashflow" )) + def test_psw_with_ssha256_should_not_contain_linefeed + flexmock(SecureRandom).should_receive(:random_bytes).and_return('\xE5\x8A\x99\xF8\xCB\x15GW\xE8\xEA\xAD\x0F\xBF\x95\xB0\xDC') + assert_equal("{SSHA256}Cc7MXboTyUP5PnPAeJeCrgMy8+7Gus0sw7kBJuTrmf1ceEU1XHg4QVx4OTlceEY4XHhDQlx4MTVHV1x4RThceEVBXHhBRFx4MEZceEJGXHg5NVx4QjBceERD", Net::LDAP::Password.generate(:ssha256, "cashflow")) end + def test_utf8_psw + flexmock(SecureRandom).should_receive(:random_bytes).and_return('\xE5\x8A\x99\xF8\xCB\x15GW\xE8\xEA\xAD\x0F\xBF\x95\xB0\xDC') + utf8_psw = "iHVh©NjrLR§h!cru" + assert_equal("{SSHA}shzNiWgSPr3DoDm+Re7QPCcu1g1ceEU1XHg4QVx4OTlceEY4XHhDQlx4MTVHV1x4RThceEVBXHhBRFx4MEZceEJGXHg5NVx4QjBceERD", Net::LDAP::Password.generate(:ssha, utf8_psw)) + assert_equal("{SSHA256}/aS06GodUyRYx+z436t+WZsH2aQCSac9FY4ewaXzhSNceEU1XHg4QVx4OTlceEY4XHhDQlx4MTVHV1x4RThceEVBXHhBRFx4MEZceEJGXHg5NVx4QjBceERD", Net::LDAP::Password.generate(:ssha256, utf8_psw)) + end end diff --git a/test/test_rename.rb b/test/test_rename.rb index db82340a..6e6ee65a 100644 --- a/test/test_rename.rb +++ b/test/test_rename.rb @@ -1,7 +1,7 @@ -require 'common' +require_relative 'test_helper' # Commented out since it assumes you have a live LDAP server somewhere. This -# will be migrated to the integration specs, as soon as they are ready. +# will be migrated to the integration specs, as soon as they are ready. =begin class TestRename < Test::Unit::TestCase HOST= '10.10.10.71' diff --git a/test/test_search.rb b/test/test_search.rb new file mode 100644 index 00000000..c577a6a2 --- /dev/null +++ b/test/test_search.rb @@ -0,0 +1,39 @@ +# -*- ruby encoding: utf-8 -*- +require_relative 'test_helper' + +class TestSearch < Test::Unit::TestCase + class FakeConnection + def search(args) + OpenStruct.new(:result_code => Net::LDAP::ResultCodeOperationsError, :message => "error", :success? => false) + end + end + + def setup + @service = MockInstrumentationService.new + @connection = Net::LDAP.new :instrumentation_service => @service + @connection.instance_variable_set(:@open_connection, FakeConnection.new) + end + + def test_true_result + assert_nil @connection.search(:return_result => true) + end + + def test_false_result + refute @connection.search(:return_result => false) + end + + def test_no_result + assert_nil @connection.search + end + + def test_instrumentation_publishes_event + events = @service.subscribe "search.net_ldap" + + @connection.search(:filter => "test") + + payload, result = events.pop + assert payload.key?(:result) + assert payload.key?(:filter) + assert_equal "test", payload[:filter] + end +end diff --git a/test/test_snmp.rb b/test/test_snmp.rb index 88a619dc..fa064d41 100644 --- a/test/test_snmp.rb +++ b/test/test_snmp.rb @@ -1,19 +1,24 @@ # $Id: testsnmp.rb 231 2006-12-21 15:09:29Z blackhedd $ -require 'common' -require 'net/snmp' +require_relative 'test_helper' +require_relative '../lib/net/snmp' class TestSnmp < Test::Unit::TestCase - SnmpGetRequest = "0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000" - SnmpGetResponse = "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" + def self.raw_string(s) + # Conveniently, String#b only needs to be called when it exists + s.respond_to?(:b) ? s.b : s + end + + SnmpGetRequest = raw_string("0'\002\001\000\004\006public\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000") + SnmpGetResponse = raw_string("0+\002\001\000\004\006public\242\036\002\002'\017\002\001\000\002\001\0000\0220\020\006\b+\006\001\002\001\001\001\000\004\004test") - SnmpGetRequestXXX = "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" + SnmpGetRequestXXX = raw_string("0'\002\001\000\004\006xxxxxx\240\032\002\002?*\002\001\000\002\001\0000\0160\f\006\b+\006\001\002\001\001\001\000\005\000") def test_invalid_packet data = "xxxx" - assert_raise(Net::BER::BerError) { -ary = data.read_ber(Net::SNMP::AsnSyntax) - } + assert_raise(Net::BER::BerError) do + data.read_ber(Net::SNMP::AsnSyntax) + end end # The method String#read_ber! added by Net::BER consumes a well-formed BER @@ -35,9 +40,9 @@ def _test_consume_string end def test_weird_packet - assert_raise(Net::SnmpPdu::Error) { -Net::SnmpPdu.parse("aaaaaaaaaaaaaa") - } + assert_raise(Net::SnmpPdu::Error) do + Net::SnmpPdu.parse("aaaaaaaaaaaaaa") + end end def test_get_request @@ -88,7 +93,7 @@ def test_make_response def test_make_bad_response pdu = Net::SnmpPdu.new - assert_raise(Net::SnmpPdu::Error) {pdu.to_ber_string} + assert_raise(Net::SnmpPdu::Error) { pdu.to_ber_string } pdu.pdu_type = :get_response pdu.request_id = 999 pdu.to_ber_string @@ -110,5 +115,4 @@ def test_community pdu = Net::SnmpPdu.parse(ary) assert_equal("xxxxxx", pdu.community) end - end diff --git a/test/test_ssl_ber.rb b/test/test_ssl_ber.rb new file mode 100644 index 00000000..766c8b84 --- /dev/null +++ b/test/test_ssl_ber.rb @@ -0,0 +1,46 @@ +require_relative 'test_helper' +require 'timeout' + +class TestSSLBER < Test::Unit::TestCase + # Transmits str to @to and reads it back from @from. + # + def transmit(str) + Timeout.timeout(1) do + @to.write(str) + @to.close + + @from.read + end + end + + def setup + @from, @to = IO.pipe + + # The production code operates on sockets, which do need #connect called + # on them to work. Pipes are more robust for this test, so we'll skip + # the #connect call since it fails. + # + # TODO: Replace test with real socket + # https://github.com/ruby-ldap/ruby-net-ldap/pull/121#discussion_r18746386 + flexmock(OpenSSL::SSL::SSLSocket) + .new_instances.should_receive(:connect => nil) + + @to = Net::LDAP::Connection.wrap_with_ssl(@to) + @from = Net::LDAP::Connection.wrap_with_ssl(@from) + end + + def test_transmit_strings + omit_if RUBY_PLATFORM == "java", "JRuby throws an error without a real socket" + omit_if (RUBY_VERSION >= "3.1" || RUBY_ENGINE == "truffleruby"), "Ruby complains about connection not being open" + + assert_equal "foo", transmit("foo") + end + + def test_transmit_ber_encoded_numbers + omit_if RUBY_PLATFORM == "java", "JRuby throws an error without a real socket" + omit_if (RUBY_VERSION >= "3.1" || RUBY_ENGINE == "truffleruby"), "Ruby complains about connection not being open" + + @to.write 1234.to_ber + assert_equal 1234, @from.read_ber + end +end diff --git a/testserver/ldapserver.rb b/testserver/ldapserver.rb index eba130ce..9adeacb0 100644 --- a/testserver/ldapserver.rb +++ b/testserver/ldapserver.rb @@ -15,8 +15,7 @@ #------------------------------------------------ module LdapServer - - LdapServerAsnSyntax = { + LdapServerAsnSyntaxTemplate = { :application => { :constructed => { 0 => :array, # LDAP BindRequest @@ -24,7 +23,7 @@ module LdapServer }, :primitive => { 2 => :string, # ldapsearch sends this to unbind - } + }, }, :context_specific => { :primitive => { @@ -34,7 +33,7 @@ module LdapServer :constructed => { 3 => :array # equality filter }, - } + }, } def post_init @@ -46,7 +45,7 @@ def receive_data data @data ||= ""; @data << data while pdu = @data.read_ber!(LdapServerAsnSyntax) begin - handle_ldap_pdu pdu + handle_ldap_pdu pdu rescue $logger.error "closing connection due to error #{$!}" close_connection @@ -87,9 +86,7 @@ def handle_bind_request pdu end end - - - #-- + # -- # Search Response ::= # CHOICE { # entry [APPLICATION 4] SEQUENCE { @@ -119,9 +116,9 @@ def handle_search_request pdu # pdu[1][7] is the list of requested attributes. # If it's an empty array, that means that *all* attributes were requested. requested_attrs = if pdu[1][7].length > 0 - pdu[1][7].map {|a| a.downcase} - else - :all + pdu[1][7].map(&:downcase) + else + :all end filters = pdu[1][6] @@ -131,50 +128,45 @@ def handle_search_request pdu end # TODO, what if this returns nil? - filter = Net::LDAP::Filter.parse_ldap_filter( filters ) + filter = Net::LDAP::Filter.parse_ldap_filter(filters) - $ldif.each {|dn, entry| - if filter.match( entry ) + $ldif.each do |dn, entry| + if filter.match(entry) attrs = [] - entry.each {|k, v| - if requested_attrs == :all or requested_attrs.include?(k.downcase) - attrvals = v.map {|v1| v1.to_ber}.to_ber_set + entry.each do |k, v| + if requested_attrs == :all || requested_attrs.include?(k.downcase) + attrvals = v.map(&:to_ber).to_ber_set attrs << [k.to_ber, attrvals].to_ber_sequence end - } + end appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) pkt = [msgid.to_ber, appseq].to_ber_sequence send_data pkt end - } - + end send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?" end - - def send_ldap_response pkt_tag, msgid, code, dn, text - send_data( [msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag) ].to_ber ) + send_data([msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag)].to_ber) end - end - #------------------------------------------------ # Rather bogus, a global method, which reads a HARDCODED filename # parses out LDIF data. It will be used to serve LDAP queries out of this server. # def load_test_data - ary = File.readlines( "./testdata.ldif" ) + ary = File.readlines("./testdata.ldif") hash = {} - while line = ary.shift and line.chomp! + while (line = ary.shift) && line.chomp! if line =~ /^dn:[\s]*/i dn = $' hash[dn] = {} - while attr = ary.shift and attr.chomp! and attr =~ /^([\w]+)[\s]*:[\s]*/ + while (attr = ary.shift) && attr.chomp! && attr =~ /^([\w]+)[\s]*:[\s]*/ hash[dn][$1.downcase] ||= [] hash[dn][$1.downcase] << $' end @@ -183,7 +175,6 @@ def load_test_data hash end - #------------------------------------------------ if __FILE__ == $0 @@ -200,11 +191,10 @@ def load_test_data $ldif = load_test_data require 'net/ldap' - - EventMachine.run { + LdapServerAsnSyntax = Net::BER.compile_syntax(LdapServerAsnSyntaxTemplate) + EventMachine.run do $logger.info "starting LDAP server on 127.0.0.1 port 3890" EventMachine.start_server "127.0.0.1", 3890, LdapServer - EventMachine.add_periodic_timer 60, proc {$logger.info "heartbeat"} - } + EventMachine.add_periodic_timer 60, proc { $logger.info "heartbeat" } + end end -