From 88f8d535f165572621afea92f7d73237f46ba420 Mon Sep 17 00:00:00 2001 From: jcredding Date: Tue, 16 Aug 2011 12:59:25 -0500 Subject: [PATCH] first commit, still mostly just code pulled from ad-schema --- .gitignore | 4 +++ Gemfile | 7 ++++ README.textile | 45 ++++++++++++++++++++++++ Rakefile | 7 ++++ ad-ldap.gemspec | 25 ++++++++++++++ lib/ad-ldap.rb | 32 +++++++++++++++++ lib/ad-ldap/adapter.rb | 69 +++++++++++++++++++++++++++++++++++++ lib/ad-ldap/exceptions.rb | 0 lib/ad-ldap/logger.rb | 70 ++++++++++++++++++++++++++++++++++++++ lib/ad-ldap/response.rb | 28 +++++++++++++++ lib/ad-ldap/search_args.rb | 43 +++++++++++++++++++++++ lib/ad-ldap/version.rb | 5 +++ test/helper.rb | 13 +++++++ test/unit/ad-ldap_test.rb | 61 +++++++++++++++++++++++++++++++++ 14 files changed, 409 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 README.textile create mode 100644 Rakefile create mode 100644 ad-ldap.gemspec create mode 100644 lib/ad-ldap.rb create mode 100644 lib/ad-ldap/adapter.rb create mode 100644 lib/ad-ldap/exceptions.rb create mode 100644 lib/ad-ldap/logger.rb create mode 100644 lib/ad-ldap/response.rb create mode 100644 lib/ad-ldap/search_args.rb create mode 100644 lib/ad-ldap/version.rb create mode 100644 test/helper.rb create mode 100644 test/unit/ad-ldap_test.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4040c6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.gem +.bundle +Gemfile.lock +pkg/* diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..0da4189 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +source "http://rubygems.org" + +# Specify your gem's dependencies in ad-ldap.gemspec +gemspec + +gem 'rake', '~>0.9.2' +gem "assert", :path => "~/Projects/work/gems/assert" diff --git a/README.textile b/README.textile new file mode 100644 index 0000000..fda7061 --- /dev/null +++ b/README.textile @@ -0,0 +1,45 @@ +h1. AD::LDAP + +A small wrapper to Net::LDAP to provide some extended functionality and utility. + +h2. Description + +AD::LDAP is a small wrapper to the Net::LDAP library. Net::LDAP provides a nice low-level interface +for interacting with an LDAP server. AD::LDAP simply wraps that interface and provides some +extended functionality through: + +* Built-in logging of any communication with the LDAP server +* Easier use of net-ldap's search + +h2. Installation + + gem install ad-ldap + +h2. Usage + +TODO + +h2. License + +Copyright (c) 2011 Collin Redding, and Team Insight + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..52f1039 --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +include Rake::DSL + +require 'bundler' +Bundler::GemHelper.install_tasks + +require 'assert/rake_tasks' +Assert::RakeTasks.for :test diff --git a/ad-ldap.gemspec b/ad-ldap.gemspec new file mode 100644 index 0000000..498ab87 --- /dev/null +++ b/ad-ldap.gemspec @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "ad-ldap/version" + +Gem::Specification.new do |s| + s.name = "ad-ldap" + s.version = AD::LDAP::VERSION + s.authors = ["jcredding"] + s.email = ["TempestTTU@gmail.com"] + s.homepage = "" + s.summary = %q{TODO: Write a gem summary} + s.description = %q{TODO: Write a gem description} + + s.rubyforge_project = "ad-ldap" + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.require_paths = ["lib"] + + # specify any dependencies here; for example: + s.add_runtime_dependency "net-ldap" + + s.add_development_dependency "mocha" +end diff --git a/lib/ad-ldap.rb b/lib/ad-ldap.rb new file mode 100644 index 0000000..5670efc --- /dev/null +++ b/lib/ad-ldap.rb @@ -0,0 +1,32 @@ +require 'ostruct' + +require 'ad-ldap/adapter' +require 'ad-ldap/version' + +module AD + module LDAP + class << self + + def configure + yield self.config + end + + def method_missing(method, *args, &block) + if self.adapter.respond_to?(method) + self.adapter.send(method, *args, &block) + else + super + end + end + + def config + @config ||= OpenStruct.new + end + + def adapter + @adapter ||= AD::LDAP::Adapter.new(self.config) + end + + end + end +end diff --git a/lib/ad-ldap/adapter.rb b/lib/ad-ldap/adapter.rb new file mode 100644 index 0000000..f26ce64 --- /dev/null +++ b/lib/ad-ldap/adapter.rb @@ -0,0 +1,69 @@ +require 'net/ldap' + +require 'ad-ldap/response' + +module AD + module LDAP + + class Adapter < ::Net::LDAP + attr_accessor :config + + def initialize(config) + self.config = config + super({ + :host => self.config.host, + :port => self.config.port, + :base => self.config.base, + :encryption => self.config.encryption + }) + if self.config.auth + self.auth(self.config.auth.username, self.config.auth.password) + end + end + + # don't raise when an open connection is already open, just yield it + def open + if @open_connection + yield @open_connection + else + super + end + end + + [ :add, :delete, :modify ].each do |method| + define_method(method) do |args| + result = super + self.check_operation + result + end + end + [ :add_attribute, :replace_attribute ].each do |method| + define_method(method) do |dn, attribute, value| + result = super + self.check_operation + result + end + end + def delete_attribute(dn, attribute) + result = super + self.check_operation + result + end + + def search(args = {}) + results = super(args.dup) + self.check_operation + results + end + + protected + + def check_operation + check = self.get_operation_result + ActiveDirectory::LDAP::Response.new(check.code, check.message).handle! + end + + end + + end +end diff --git a/lib/ad-ldap/exceptions.rb b/lib/ad-ldap/exceptions.rb new file mode 100644 index 0000000..e69de29 diff --git a/lib/ad-ldap/logger.rb b/lib/ad-ldap/logger.rb new file mode 100644 index 0000000..1b41825 --- /dev/null +++ b/lib/ad-ldap/logger.rb @@ -0,0 +1,70 @@ +module ActiveDirectory + module LDAP + + # Inspired by https://github.com/tpett/perry logger + class Logger + attr_accessor :logger + + def initialize(logger) + self.logger = logger + end + + def out(method, args, result, time) + color = "4;32;1" + name = "%s (%.1fms)" % [ "LDAP", time ] + message = self.generate_message(method, args) + output = " \e[#{color}]#{name} #{message}\e[0m" + if self.logger + self.logger.debug(output) + else + puts output + end + end + + protected + + def delete(dn) + self.run(:delete, { :dn => dn }) + end + + def replace_attribute(dn, field, value) + self.run(:replace_attribute, dn, field, value) + end + + def delete_attribute(dn, field) + self.run(:delete_attribute, dn, field) + end + + def generate_message(method, args) + case(method.to_sym) + when :replace_attribute + dn, field, value = args + "#{method}(#{dn.inspect}, #{field.inspect}, #{self.filter_value(value, field)})" + when :delete_attribute + dn, field = args + "#{method}(#{dn.inspect}, #{field.inspect})" + else + "#{method}(#{self.filter_args(args.first)})" + end + end + + FILTERED = [ /password/, /unicodePwd/ ] + def filter_args(args = {}) + (args.inject({}) do |filtered, (key, value)| + filtered[key] = self.filter_value(value, key) + filtered + end).inspect + end + def filter_value(value, key) + case(key) + when *FILTERED + "[FILTERED]" + else + value.to_s + end + end + + end + + end +end diff --git a/lib/ad-ldap/response.rb b/lib/ad-ldap/response.rb new file mode 100644 index 0000000..1a6ede4 --- /dev/null +++ b/lib/ad-ldap/response.rb @@ -0,0 +1,28 @@ +require 'ad-ldap/exceptions' + +module ActiveDirectory + module LDAP + + # descriptions: http://wiki.service-now.com/index.php?title=LDAP_Error_Codes + class Response + attr_accessor :code, :message + + CODES = { + :success => 0 + }.freeze + + def initialize(code, message) + self.code = code.to_i + self.message = message + end + + def handle! + if self.code != CODES[:success] + raise(AD::LDAP::Error, "#{self.code}: #{self.message}") + end + end + + end + + end +end diff --git a/lib/ad-ldap/search_args.rb b/lib/ad-ldap/search_args.rb new file mode 100644 index 0000000..8357bce --- /dev/null +++ b/lib/ad-ldap/search_args.rb @@ -0,0 +1,43 @@ +require 'net/ldap' + +module ActiveDirectory + module LDAP + + class SearchArgs < Hash + + LDAP_KEYS = [ :base, :filter, :attributes, :return_result, :attributes_only, + :scope, :size ] + + def initialize(args) + super() + conditions = {} + args.each do |key, value| + if LDAP_KEYS.include?(key.to_sym) + self[key.to_sym] = value + else + conditions[key.to_sym] = value + end + end + if !self[:filter] && (filters = self.build_filters(conditions)) + self[:filter] = filters + end + end + + protected + + def build_filters(conditions = {}) + conditions.inject(nil) do |filters, (key, value)| + field, operator = key.to_s.split("__") + operator ||= "eq" + if attribute = ActiveDirectory.config.attributes[field.to_sym] + field = (attribute.ldap_name || field) + end + filter = ::Net::LDAP::Filter.send(operator, field, value) + filters ? ::Net::LDAP::Filter.join(filters, filter) : filter + end + end + + end + + end +end diff --git a/lib/ad-ldap/version.rb b/lib/ad-ldap/version.rb new file mode 100644 index 0000000..0f10ab3 --- /dev/null +++ b/lib/ad-ldap/version.rb @@ -0,0 +1,5 @@ +module AD + module LDAP + VERSION = "0.0.1" + end +end diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 0000000..853520d --- /dev/null +++ b/test/helper.rb @@ -0,0 +1,13 @@ +require 'mocha' + +# add the current gem root path to the LOAD_PATH +root_path = File.expand_path("../..", __FILE__) +if !$LOAD_PATH.include?(root_path) + $LOAD_PATH.unshift(root_path) +end + +require 'ad-ldap' + +class Assert::Context + include Mocha::API +end diff --git a/test/unit/ad-ldap_test.rb b/test/unit/ad-ldap_test.rb new file mode 100644 index 0000000..88d1511 --- /dev/null +++ b/test/unit/ad-ldap_test.rb @@ -0,0 +1,61 @@ +require 'assert' + +module AD + + class LDAPTest < Assert::Context + desc "the module AD::LDAP" + subject{ AD::LDAP } + + should "respond to #configure" do + assert_respond_to subject, :configure + end + should "respond to #config" do + assert_respond_to subject, :config + end + should "respond to #adapter" do + assert_respond_to subject, :adapter + end + + should "return an instance of AD::LDAP::Adapter with #adapter" do + assert_kind_of AD::LDAP::Adapter, subject.adapter + end + end + + class ConfigureTest < AD::LDAPTest + desc "configure method" + setup do + AD::LDAP.configure do |config| + @config = config + end + end + subject{ @config } + + should "yield it's config" do + assert_kind_of OpenStruct, subject + assert_equal AD::LDAP.config, subject + end + end + + ADAPTER_METHODS = [ + :add, :delete, :modify, :add_attribute, :replace_attribute, + :delete_attribute, :search, :bind, :bind_as + ] + class MethodMissingTest < AD::LDAPTest + desc "method_missing method" + setup do + + mock_adapter = mock() + ADAPTER_METHODS.each do |name| + mock_adapter.expects(name.to_sym).returns("#{name} method called") + end + AD::LDAP.stubs(:adapter).returns(mock_adapter) + end + + ADAPTER_METHODS.each do |name| + should "proxy the missing method ##{name} to it's adapter" do + assert_equal "#{name} method called", subject.send(name) + end + end + end + +end