├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── Rakefile ├── Readme.md ├── adauth.gemspec ├── lib ├── adauth.rb ├── adauth │ ├── ad_object.rb │ ├── ad_objects │ │ ├── computer.rb │ │ ├── folder.rb │ │ ├── group.rb │ │ ├── ou.rb │ │ └── user.rb │ ├── authenticate.rb │ ├── config.rb │ ├── connection.rb │ ├── net-ldap │ │ └── string.rb │ ├── rails.rb │ ├── rails │ │ ├── helpers.rb │ │ └── model_bridge.rb │ ├── search_results.rb │ └── version.rb └── generators │ └── adauth │ ├── config │ ├── USAGE │ ├── config_generator.rb │ └── templates │ │ └── config.rb.erb │ └── sessions │ ├── USAGE │ ├── sessions_generator.rb │ └── templates │ ├── new.html.erb │ └── sessions_controller.rb.erb ├── rspec_results.txt └── spec ├── adauth_ad_object_computer_spec.rb ├── adauth_ad_object_folder_spec.rb ├── adauth_ad_object_group_spec.rb ├── adauth_ad_object_ou_spec.rb ├── adauth_ad_object_spec.rb ├── adauth_ad_object_user_spec.rb ├── adauth_authenticate_spec.rb ├── adauth_config_spec.rb ├── adauth_connection_spec.rb ├── adauth_issue_spec.rb ├── adauth_rails_model_bridge_spec.rb ├── adauth_search_results_spec.rb ├── adauth_spec.rb ├── spec_helper.rb └── test_data.example.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | pkg/* 3 | tmp/* 4 | *~ 5 | spec/test_data.yml 6 | doc/* 7 | .yardoc/* 8 | .idea/ 9 | .rvmrc 10 | log/* 11 | coverage/* 12 | 13 | spec/test.sqlite3 14 | 15 | spec/db/db.sqlite3 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | - 2.0.0 6 | - rbx-18mode 7 | - rbx-19mode 8 | - ruby-head 9 | env: 10 | - "rake=0.9" 11 | script: "bundle exec rspec -t no_ad" 12 | before_script: "mkdir log" 13 | notifications: 14 | email: false 15 | matrix: 16 | allow_failures: 17 | - rvm: 1.8.7 18 | - rvm: rbx-18mode 19 | - rvm: ruby-head 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | adauth (2.0.2) 5 | expects (~> 0.0.2) 6 | net-ldap 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | diff-lcs (1.1.3) 12 | expects (0.0.2) 13 | multi_json (1.7.7) 14 | net-ldap (0.3.1) 15 | rake (0.9.2.2) 16 | rspec (2.11.0) 17 | rspec-core (~> 2.11.0) 18 | rspec-expectations (~> 2.11.0) 19 | rspec-mocks (~> 2.11.0) 20 | rspec-core (2.11.1) 21 | rspec-expectations (2.11.2) 22 | diff-lcs (~> 1.1.3) 23 | rspec-mocks (2.11.2) 24 | simplecov (0.7.1) 25 | multi_json (~> 1.0) 26 | simplecov-html (~> 0.7.1) 27 | simplecov-html (0.7.1) 28 | yard (0.8.6.2) 29 | 30 | PLATFORMS 31 | ruby 32 | 33 | DEPENDENCIES 34 | adauth! 35 | rake 36 | rspec 37 | simplecov 38 | yard 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Adam Laycock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | 4 | Bundler::GemHelper.install_tasks 5 | 6 | desc "Save test results to a file" 7 | task :generate_test_results do 8 | puts "Running Tests" 9 | system("rspec -c > rspec_results.txt") 10 | puts "Saved!" 11 | puts "Results:" 12 | system("cat rspec_results.txt") 13 | end -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Adauth 2 | [RDoc](http://rubydoc.info/github/Arcath/Adauth/master/frames) | [www](http://adauth.arcath.net) | [Gempage](http://rubygems.org/gems/adauth) | [![Status](https://secure.travis-ci.org/Arcath/Adauth.png?branch=master)](http://travis-ci.org/Arcath/Adauth) | [![Code Climate](https://codeclimate.com/github/Arcath/Adauth.png)](https://codeclimate.com/github/Arcath/Adauth) | [![Dependency Status](https://gemnasium.com/Arcath/Adauth.png)](https://gemnasium.com/Arcath/Adauth) 3 | 4 | 5 | Easy to use Active Directory Authentication for Rails. 6 | 7 | ## Install 8 | 9 | Add the Adauth gem to your Gemfile: 10 | 11 | gem 'adauth' 12 | 13 | and run a bundle install 14 | 15 | ## Usage 16 | 17 | ### In Rails 18 | 19 | First off create a new config file by running the config generator 20 | 21 | rails g adauth:config 22 | 23 | Fill out the config values in _config/initializers/adauth.rb_ 24 | 25 | #### Joining a model to Adauth 26 | 27 | If you want to link your user model to Adauth you can use this simple code: 28 | 29 | class User < ActiveRecord::Base 30 | include Adauth::Rails::ModelBridge 31 | 32 | AdauthMappings = { 33 | :login => :login 34 | :group_strings => :cn_groups 35 | } 36 | 37 | AdauthSearchField = [:login, :login] 38 | end 39 | 40 | This gives you a bridge between Adauth and your model. When you call `User.create_from_adauth(adauth_model)` it does: 41 | 42 | u = User.new 43 | u.login = adauth_model.login 44 | u.group_strings = adauth_model.cn_groups 45 | u.save 46 | 47 | This can be used for any model and anything that you pull over through adauth. 48 | 49 | #### SessionsController 50 | 51 | You can use a premade sessions controller by running 52 | 53 | rails g adauth:sessions 54 | 55 | Which adds a couple of routes, a sessions controller and a login form. To login go to _/sessions/new_ and fill out the form, you will then POST to _/adauth_ and if succesful you will be sent back to _root_path_ 56 | 57 | ### In Scripts 58 | 59 | To use Adauth in a script or other program just call `Adauth.configure` somewhere at the begining of the script, once configured Adauth can be used anywhere in your program the same as rails. 60 | 61 | ## Configuring 62 | 63 | Adauth has a few configuration options which are described in detail on the [wiki](https://github.com/Arcath/Adauth/wiki/Configuring). 64 | 65 | ## Logs 66 | 67 | Adauth logs to weekly logs in logs/adauth.log(.DATE) 68 | 69 | You can interact with the logger through `Adauth.logger` and set a new one using `Adauth.logger=` 70 | 71 | ## Developing 72 | 73 | Before you can run the tests you will need to write a yml file with your domain settings in and place it at _spec/test_data.yml_, there is an example of this file in the spec folder. 74 | 75 | When you fork Adauth please: 76 | 77 | 1. Create your feature branch (`git checkout -b my-new-feature`) 78 | 2. Commit your changes (`git commit -am 'Add some feature'`) 79 | 3. Push to the branch (`git push origin my-new-feature`) 80 | 4. Create new Pull Request 81 | -------------------------------------------------------------------------------- /adauth.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require 'adauth/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "adauth" 7 | s.version = Adauth::Version 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Adam \"Arcath\" Laycock"] 10 | s.email = ["gems@arcath.net"] 11 | s.homepage = "http://adauth.arcath.net" 12 | s.summary = "Provides Active Directory authentication for Rails" 13 | s.description = "A full featured library for working with Microsofts Active Directory in Ruby." 14 | s.license = 'MIT' 15 | 16 | s.add_development_dependency "rake" 17 | s.add_development_dependency "rspec" 18 | s.add_development_dependency "simplecov" 19 | s.add_development_dependency "yard" 20 | s.add_dependency "net-ldap" 21 | s.add_dependency "expects", "~> 0.0.2" 22 | s.add_dependency "activesupport" 23 | 24 | s.files = `git ls-files`.split("\n") 25 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 26 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 27 | s.require_paths = ["lib"] 28 | end 29 | -------------------------------------------------------------------------------- /lib/adauth.rb: -------------------------------------------------------------------------------- 1 | # Requires 2 | require 'expects' 3 | require 'logger' 4 | require 'net/ldap' 5 | require 'timeout' 6 | # Version 7 | require 'adauth/version' 8 | # Classes 9 | require 'adauth/ad_object' 10 | require 'adauth/authenticate' 11 | require 'adauth/config' 12 | require 'adauth/connection' 13 | require 'adauth/search_results' 14 | # AdObjects 15 | require 'adauth/ad_objects/computer' 16 | require 'adauth/ad_objects/folder' 17 | require 'adauth/ad_objects/group' 18 | require 'adauth/ad_objects/ou' 19 | require 'adauth/ad_objects/user' 20 | # Rails 21 | require 'adauth/rails' 22 | require 'adauth/rails/helpers' 23 | require 'adauth/rails/model_bridge' 24 | 25 | require 'adauth/net-ldap/string.rb' # Hot fix for issue 26 | 27 | # Adauth Container Module 28 | module Adauth 29 | # Yields a new config object and then sets it as the Adauth Config 30 | def self.configure 31 | @logger ||= Logger.new('log/adauth.log', 'weekly') 32 | @logger.info('load') { "Loading new config" } 33 | @connection = nil 34 | @config = Config.new 35 | yield(@config) 36 | end 37 | 38 | # Returns Adauths current connection to ActiveDirectory 39 | def self.connection 40 | @logger.fatal('connection') { "Attempted to create connection without configuring" } if @config == nil 41 | raise 'Adauth needs configuring before use' if @config == nil # Still raise an error here even after logging it so that adauth stops dead and doesn't error on the next line 42 | connect unless @connection 43 | @connection 44 | end 45 | 46 | # Connects to ActiveDirectory using the query user details 47 | def self.connect 48 | @logger.info('connection') { "Connecting to AD as \"#{@config.query_user}\"" } 49 | @connection = Adauth::Connection.new(connection_hash(@config.query_user, @config.query_password)).bind 50 | end 51 | 52 | # Generates a hash for the connection class, takes a username and password 53 | def self.connection_hash(user, password) 54 | { 55 | :domain => @config.domain, 56 | :server => @config.server, 57 | :port => @config.port, 58 | :base => @config.base, 59 | :encryption => @config.encryption, 60 | :allow_fallback => @config.allow_fallback, 61 | :username => user, 62 | :password => password, 63 | :anonymous_bind => @config.anonymous_bind 64 | } 65 | end 66 | 67 | # Returns the logger object 68 | def self.logger 69 | @logger 70 | end 71 | 72 | # Lets you set a new logger 73 | def self.logger=(inputs) 74 | @logger = inputs 75 | end 76 | end -------------------------------------------------------------------------------- /lib/adauth/ad_object.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/object/try" 2 | 3 | module Adauth 4 | # Container for Objects which inherit from Adauth::AdObject 5 | module AdObjects 6 | end 7 | 8 | # Add a field to the specified model 9 | def self.add_field(object, adauth_method, ldap_method) 10 | Adauth.logger.info(object.inspect) { "Adding field \"#{ldap_method}\"" } 11 | object::Fields[adauth_method] = ldap_method 12 | end 13 | 14 | # Active Directory Interface Object 15 | # 16 | # Objects inherit from this class. 17 | # 18 | # Provides all the common functions for Active Directory. 19 | class AdObject 20 | include Expects 21 | 22 | def self.method_missing(method, *args) 23 | return super unless method =~ /^find_by_/ 24 | method_field = method.to_s.split("_").last 25 | field = self::Fields[method_field.to_sym] 26 | return super unless field 27 | self.where(field, args.first) 28 | end 29 | 30 | def method_missing(method, *args) 31 | field = self.class::Fields[method] 32 | return handle_field(field) if field 33 | return super 34 | end 35 | 36 | def self.reverse_field(search) 37 | hash = {} 38 | self::Fields.each do |k, v| 39 | hash[v] = k 40 | end 41 | return hash[search] 42 | end 43 | 44 | # Returns all objects which have the ObjectClass of the inherited class 45 | def self.all 46 | Adauth.logger.info(self.class.inspect) { "Searching for all objects matching filter \"#{self::ObjectFilter}\"" } 47 | Adauth::SearchResults.new(self.filter(self::ObjectFilter)) 48 | end 49 | 50 | # Returns all the objects which match the supplied query 51 | # 52 | # Uses ObjectFilter to restrict to the current object 53 | def self.where(field, value) 54 | search_filter = Net::LDAP::Filter.eq(field, value) 55 | Adauth.logger.info(self.class.inspect) { "Searching for all \"#{self::ObjectFilter}\" where #{field} = #{value}" } 56 | Adauth::SearchResults.new(filter(add_object_filter(search_filter))) 57 | end 58 | 59 | # Returns all LDAP objects that match the given filter 60 | # 61 | # Use with add_object_filter to make sure that you only get objects that match the object you are querying though 62 | def self.filter(filter) 63 | results = [] 64 | 65 | result = Adauth.connection.search(:filter => filter) 66 | 67 | raise 'Search returned NIL' if result == nil 68 | 69 | result.each do |entry| 70 | results << self.new(entry) 71 | end 72 | 73 | results 74 | end 75 | 76 | # Adds the object filter to the passed filter 77 | def self.add_object_filter(filter) 78 | filter & self::ObjectFilter 79 | end 80 | 81 | # Creates a new instance of the object and sets @ldap_object to the passed Net::LDAP entity 82 | def initialize(ldap_object) 83 | expects ldap_object, Net::LDAP::Entry 84 | @ldap_object = ldap_object 85 | end 86 | 87 | # Allows direct access to @ldap_object 88 | def ldap_object 89 | @ldap_object 90 | end 91 | 92 | # Handle the output for the given field 93 | def handle_field(field) 94 | case field 95 | when Symbol then return return_symbol_value(field) 96 | when Array then return @ldap_object.try(field.first).try(:collect, &field.last) 97 | end 98 | end 99 | 100 | # Returns all the groups the object is a member of 101 | def groups 102 | unless @groups 103 | @groups = convert_to_objects(cn_groups) 104 | end 105 | @groups 106 | end 107 | 108 | # The same as cn_groups, but with the parent groups included 109 | def cn_groups_nested 110 | @cn_groups_nested = cn_groups 111 | cn_groups.each do |group| 112 | ado = Adauth::AdObjects::Group.where('name', group).first 113 | if ado 114 | groups = convert_to_objects ado.cn_groups 115 | groups.each do |g| 116 | @cn_groups_nested.push g if !(@cn_groups_nested.include?(g)) 117 | end 118 | end 119 | end 120 | return @cn_groups_nested 121 | end 122 | 123 | # Returns all the ous the object is in 124 | def ous 125 | unless @ous 126 | @ous = [] 127 | @ldap_object.dn.split(/,/).each do |entry| 128 | @ous.push Adauth::AdObjects::OU.where('name', entry.gsub(/OU=/, '')).first if entry =~ /OU=/ 129 | end 130 | end 131 | @ous 132 | end 133 | 134 | # CSV Version of the ous list (can't be pulled over from AD) 135 | def dn_ous 136 | unless @dn_ous 137 | @dn_ous = [] 138 | @ldap_object.dn.split(/,/).each do |entry| 139 | @dn_ous.push entry.gsub(/OU=/, '').gsub(/CN=/,'') if entry =~ /OU=/ or entry == "CN=Users" 140 | end 141 | end 142 | @dn_ous 143 | end 144 | 145 | # Runs a modify action on the current object, takes an aray of operations 146 | def modify(operations) 147 | Adauth.logger.info(self.class.inspect) { "Attempting modify operation" } 148 | unless Adauth.connection.modify :dn => @ldap_object.dn, :operations => operations 149 | Adauth.logger.fatal(self.class.inspect) { "Modify Operation Failed! Code: #{Adauth.connection.get_operation_result.code} Message: #{Adauth.connection.get_operation_result.message}" } 150 | raise 'Modify Operation Failed (see log for details)' 151 | end 152 | end 153 | 154 | # Returns an array of member objects for this object 155 | def members 156 | unless @members 157 | @members = [] 158 | [Adauth::AdObjects::Computer, Adauth::AdObjects::OU, Adauth::AdObjects::User, Adauth::AdObjects::Group].each do |object| 159 | object.all.each do |entity| 160 | @members.push entity if entity.is_a_member?(self) 161 | end 162 | end 163 | end 164 | @members 165 | end 166 | 167 | # Checks to see if the object is a member of a given parent (though DN) 168 | def is_a_member?(parent) 169 | my_split_dn = @ldap_object.dn.split(",") 170 | parent_split_dn = parent.ldap_object.dn.split(",") 171 | if (my_split_dn.count - 1) == parent_split_dn.count 172 | return true if my_split_dn[1] == parent_split_dn[0] 173 | end 174 | return false 175 | end 176 | 177 | # Delete the object 178 | def delete 179 | Adauth.connection.delete(dn: @ldap_object.dn) 180 | end 181 | 182 | private 183 | 184 | def convert_to_objects(array) 185 | out = [] 186 | array.each do |entity| 187 | out.push convert_to_object(entity) 188 | end 189 | out 190 | end 191 | 192 | def convert_to_object(entity) 193 | user = Adauth::AdObjects::User.where('sAMAccountName', entity).first 194 | group = Adauth::AdObjects::Group.where('sAMAccountName', entity).first 195 | (user || group) 196 | end 197 | 198 | def return_symbol_value(field) 199 | value = @ldap_object.try(field) 200 | 201 | case value 202 | when Net::BER::BerIdentifiedArray then return value.first 203 | else return value 204 | end 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/adauth/ad_objects/computer.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module AdObjects 3 | # Active Directory Computer Object 4 | # 5 | # Inherits from Adauth::AdObject 6 | class Computer < Adauth::AdObject 7 | # Field mapping 8 | # 9 | # Maps methods to LDAP fields e.g. 10 | # 11 | # :foo => :bar 12 | # 13 | # Becomes 14 | # 15 | # Computer.name 16 | # 17 | # Which calls .name on the LDAP object 18 | Fields = { 19 | :name => :name 20 | } 21 | 22 | # Object Net::LDAP filter 23 | # 24 | # Used to restrict searches to just this object 25 | ObjectFilter = Net::LDAP::Filter.eq("objectClass", "computer") 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/adauth/ad_objects/folder.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module AdObjects 3 | # Active Directory OU Object 4 | # 5 | # Inherits from Adauth::AdObject 6 | class Folder < Adauth::AdObject 7 | # Field mapping 8 | # 9 | # Maps methods to LDAP fields e.g. 10 | # 11 | # :foo => :bar 12 | # 13 | # Becomes 14 | # 15 | # Computer.name 16 | # 17 | # Which calls .name on the LDAP object 18 | Fields = { 19 | :name => :name 20 | } 21 | 22 | # Object Net::LDAP filter 23 | # 24 | # Used to restrict searches to just this object 25 | ObjectFilter = Net::LDAP::Filter.eq("objectClass", "top") 26 | 27 | # Returns the Domain Object which is useful for building domain maps. 28 | def self.root 29 | self.new(Adauth.connection.search(:filter => Net::LDAP::Filter.eq("objectClass", "Domain")).first) 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/adauth/ad_objects/group.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module AdObjects 3 | # Active Directory Group Object 4 | # 5 | # Inherits from Adauth::AdObject 6 | class Group < Adauth::AdObject 7 | # Field mapping 8 | # 9 | # Maps methods to LDAP fields e.g. 10 | # 11 | # :foo => :bar 12 | # 13 | # Becomes 14 | # 15 | # Computer.name 16 | # 17 | # Which calls .name on the LDAP object 18 | Fields = { 19 | :name => :samaccountname, 20 | :cn_members => [ :member, 21 | Proc.new {|g| g.sub(/.*?CN=(.*?),.*/, '\1').to_s} ], 22 | :memberof => :member 23 | } 24 | 25 | # Object Net::LDAP filter 26 | # 27 | # Used to restrict searches' to just this object 28 | ObjectFilter = Net::LDAP::Filter.eq("objectClass", "group") 29 | 30 | # Create a new Group 31 | def self.new_group(name, parent) 32 | expects parent, [Adauth::AdObjects::OU, Adauth::AdObjects::Folder] 33 | attributes = { 34 | cn: name, 35 | objectclass: ["top", "group"] 36 | } 37 | Adauth.connection.add(dn: "CN=#{name},#{parent.ldap_object.dn}", attributes: attributes ) 38 | return Adauth::AdObjects::Group.where('name', name).first 39 | end 40 | 41 | # Returns all the objects which are members of this group 42 | def members 43 | Adauth.logger.info(self.class.inspect) { "Getting group members for #{self.name}" } 44 | unless @members 45 | @members = convert_to_objects(cn_members) 46 | end 47 | @members 48 | end 49 | 50 | def cn_groups 51 | if memberof.nil? 52 | [] 53 | else 54 | memberof.split(/.*?CN=(.*?),.*/) 55 | end 56 | end 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /lib/adauth/ad_objects/ou.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module AdObjects 3 | # Active Directory OU Object 4 | # 5 | # Inherits from Adauth::AdObject 6 | class OU < Adauth::AdObject 7 | # Field mapping 8 | # 9 | # Maps methods to LDAP fields e.g. 10 | # 11 | # :foo => :bar 12 | # 13 | # Becomes 14 | # 15 | # Computer.name 16 | # 17 | # Which calls .name on the LDAP object 18 | Fields = { 19 | :name => :name 20 | } 21 | 22 | # Object Net::LDAP filter 23 | # 24 | # Used to restrict searches to just this object 25 | ObjectFilter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit") 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/adauth/ad_objects/user.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module AdObjects 3 | # Active Directory User Object 4 | # 5 | # Inherits from Adauth::AdObject 6 | class User < Adauth::AdObject 7 | # Field mapping 8 | # 9 | # Maps methods to LDAP fields e.g. 10 | # 11 | # :foo => :bar 12 | # 13 | # Becomes 14 | # 15 | # Computer.name 16 | # 17 | # Which calls .name on the LDAP object 18 | Fields = { :login => :samaccountname, 19 | :first_name => :givenname, 20 | :last_name => :sn, 21 | :email => :mail, 22 | :name => :name, 23 | :cn_groups => [ :memberof, 24 | Proc.new {|g| g.sub(/.*?CN=(.*?),.*/, '\1').to_s} ] 25 | } 26 | 27 | # Object Net::LDAP filter 28 | # 29 | # Used to restrict searches to just this object 30 | ObjectFilter = Net::LDAP::Filter.eq('objectClass', 'user') 31 | 32 | # Returns a connection to AD within the users context, used to check a user credentails 33 | # 34 | # Using this would by pass the group and OU Filtering provided by Adauth#authenticate 35 | def self.authenticate(user, password) 36 | user_connection = Adauth::Connection.new(Adauth.connection_hash(user, password)).bind 37 | end 38 | 39 | # Returns True/False if the user is member of the supplied group 40 | def member_of?(group) 41 | cn_groups.include?(group) 42 | end 43 | 44 | # Changes the password to the supplied value 45 | def set_password(new_password) 46 | Adauth.logger.info("password management") { "Attempting password reset for #{self.login}" } 47 | password = microsoft_encode_password(new_password) 48 | modify([[:replace, :unicodePwd, password]]) 49 | end 50 | 51 | # Add the user to the supplied group 52 | def add_to_group(group) 53 | expects group, Adauth::AdObjects::Group 54 | group.modify([[:add, :member, @ldap_object.dn]]) 55 | end 56 | 57 | # Remove the user from the supplied group 58 | def remove_from_group(group) 59 | expects group, Adauth::AdObjects::Group 60 | group.modify([[:delete, :member, @ldap_object.dn]]) 61 | end 62 | 63 | private 64 | 65 | def microsoft_encode_password(password) 66 | out = "" 67 | password = "\"" + password + "\"" 68 | password.length.times{|i| out+= "#{password[i..i]}\000" } 69 | return out 70 | end 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /lib/adauth/authenticate.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | # Authenticates the specifed user agains the domain 3 | # 4 | # Checks the groups & ous are in the allow/deny lists 5 | def self.authenticate(username, password) 6 | begin 7 | Adauth.logger.info("authentication") { "Attempting to authenticate as #{username}" } 8 | if Adauth::AdObjects::User.authenticate(username, password) 9 | user = Adauth::AdObjects::User.where('sAMAccountName', username).first 10 | if allowed_to_login(user) 11 | Adauth.logger.info("authentication") { "Authentication succesful" } 12 | return user 13 | else 14 | Adauth.logger.info("authentication") { "Authentication failed (not in allowed group or ou)" } 15 | return false 16 | end 17 | end 18 | rescue RuntimeError 19 | Adauth.logger.info("authentication") { "Authentication failed (RuntimeError)" } 20 | return false 21 | end 22 | end 23 | 24 | # Authenticates the user against the domain without using the query user 25 | # 26 | # Checks the groups & ous are in the allow/deny lists 27 | def self.simple_authenticate(username, password) 28 | begin 29 | Adauth.logger.info("authentication") { "Attempting to simple authenticate as #{username}" } 30 | user_connection = Adauth::AdObjects::User.authenticate(username, password) 31 | if user_connection 32 | search_filter = Net::LDAP::Filter.eq('SAMAccountName', username) & Net::LDAP::Filter.eq('objectClass', 'user') 33 | results = user_connection.search(filter: search_filter) 34 | user = Adauth::AdObjects::User.new(results[0]) 35 | if allowed_to_login(user) 36 | Adauth.logger.info("authentication") { "Authentication succesful" } 37 | return user 38 | else 39 | Adauth.logger.info("authentication") { "Authentication failed (not in allowed group or ou)" } 40 | return false 41 | end 42 | end 43 | rescue RuntimeError 44 | Adauth.logger.info("authentication") { "Authentication failed (RuntimeError)" } 45 | return false 46 | end 47 | end 48 | 49 | # Check if the user is allowed to login 50 | def self.allowed_to_login(user) 51 | if (@config.allowed_groups.empty? && @config.allowed_ous.empty?) && (@config.denied_groups.empty? && @config.denied_ous.empty?) 52 | return true 53 | else 54 | return (allowed_from_arrays(@config.allowed_groups, @config.denied_groups, user.cn_groups_nested) && allowed_from_arrays(@config.allowed_ous, @config.denied_ous, user.dn_ous)) 55 | end 56 | end 57 | 58 | private 59 | 60 | def self.allowed_from_arrays(allowed, denied, test) 61 | return true if allowed.empty? && denied.empty? 62 | return true if !((allowed & test).empty?) 63 | return false if !((denied & test).empty?) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/adauth/config.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | # Holds all of Adauths Config values. 3 | # 4 | # Sets the defaults an create and generates guess values. 5 | class Config 6 | attr_accessor :domain, :port, :base, :server, :encryption, :query_user, :query_password, :allow_fallback, 7 | :allowed_groups, :denied_groups, :allowed_ous, :denied_ous, :contains_nested_groups, 8 | :anonymous_bind 9 | 10 | def initialize 11 | @port = 389 12 | @allowed_groups = [] 13 | @allowed_ous = [] 14 | @denied_groups =[] 15 | @denied_ous = [] 16 | @allow_fallback = false 17 | @contains_nested_groups = false 18 | @anonymous_bind = false 19 | end 20 | 21 | # Guesses the Server and Base string 22 | def domain=(s) 23 | @domain = s 24 | @server ||= s 25 | @base ||= s.gsub(/\./,', dc=').insert(0, 'dc=') 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/adauth/connection.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | # Active Directory Connection wrapper 3 | # 4 | # Handles errors and configures the connection. 5 | class Connection 6 | include Expects 7 | 8 | def initialize(config) 9 | expects config, Hash 10 | @config = config 11 | end 12 | 13 | # Attempts to bind to Active Directory 14 | # 15 | # If it works it returns the connection 16 | # 17 | # If it fails it raises and exception 18 | def bind 19 | conn = Net::LDAP.new :host => @config[:server], 20 | :port => @config[:port], 21 | :base => @config[:base] 22 | if @config[:encryption] 23 | conn.encryption @config[:encryption] 24 | end 25 | 26 | raise "Anonymous Bind is disabled" if @config[:password] == "" && !(@config[:anonymous_bind]) 27 | 28 | conn.auth "#{@config[:username]}@#{@config[:domain]}", @config[:password] 29 | 30 | begin 31 | Timeout::timeout(10){ 32 | if conn.bind 33 | return conn 34 | else 35 | raise 'Query User Rejected' 36 | end 37 | } 38 | rescue Timeout::Error 39 | raise 'Unable to connect to LDAP Server' 40 | rescue Errno::ECONNRESET 41 | if @config[:allow_fallback] 42 | @config[:port] = @config[:allow_fallback] 43 | @config[:encryption] = false 44 | return Adauth::Connection.new(@config).bind 45 | end 46 | end 47 | end 48 | end 49 | end -------------------------------------------------------------------------------- /lib/adauth/net-ldap/string.rb: -------------------------------------------------------------------------------- 1 | # -*- ruby encoding: utf-8 -*- 2 | require 'stringio' 3 | 4 | # THIS FILE OVERRIDES SOME OF THE CONFIG IN NET::LDAP 5 | # 6 | # It exists because adauth needs this pull request 7 | 8 | ## 9 | # BER extensions to the String class. 10 | module Net::BER::Extensions::String 11 | ## 12 | # Converts a string to a BER string. Universal octet-strings are tagged 13 | # with 0x04, but other values are possible depending on the context, so we 14 | # let the caller give us one. 15 | # 16 | # User code should call either #to_ber_application_string or 17 | # #to_ber_contextspecific. 18 | def to_ber(code = 0x04) 19 | raw_string = raw_utf8_encoded 20 | [code].pack('C') + raw_string.length.to_ber_length_encoding + raw_string 21 | end 22 | 23 | # The patched method we need 24 | def raw_utf8_encoded 25 | if self.respond_to?(:encode) 26 | # Strings should be UTF-8 encoded according to LDAP. 27 | # However, the BER code is not necessarily valid UTF-8 28 | #self.encode('UTF-8').force_encoding('ASCII-8BIT') 29 | self.encode('UTF-8', invalid: :replace, undef: :replace, replace: '' ).force_encoding('ASCII-8BIT') 30 | else 31 | self 32 | end 33 | end 34 | private :raw_utf8_encoded 35 | 36 | ## 37 | # Creates an application-specific BER string encoded value with the 38 | # provided syntax code value. 39 | def to_ber_application_string(code) 40 | to_ber(0x40 + code) 41 | end 42 | 43 | ## 44 | # Creates a context-specific BER string encoded value with the provided 45 | # syntax code value. 46 | def to_ber_contextspecific(code) 47 | to_ber(0x80 + code) 48 | end 49 | 50 | ## 51 | # Nondestructively reads a BER object from this string. 52 | def read_ber(syntax = nil) 53 | StringIO.new(self).read_ber(syntax) 54 | end 55 | 56 | ## 57 | # Destructively reads a BER object from the string. 58 | def read_ber!(syntax = nil) 59 | io = StringIO.new(self) 60 | 61 | result = io.read_ber(syntax) 62 | self.slice!(0...io.pos) 63 | 64 | return result 65 | end 66 | 67 | # Removes empty blocks from arrays 68 | def reject_empty_ber_arrays 69 | self.gsub(/0\000/n,'') 70 | end 71 | end -------------------------------------------------------------------------------- /lib/adauth/rails.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | # Container for Rails te-ins 3 | module Rails 4 | end 5 | 6 | # Rails generators 7 | module Generators 8 | end 9 | end -------------------------------------------------------------------------------- /lib/adauth/rails/helpers.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module Rails 3 | # Helper methods for rails 4 | module Helpers 5 | 6 | # Creates a form_tag for the adauth form 7 | # 8 | # Sets the html id to "adauth_login" and the form destination to "/adauth" 9 | def adauth_form 10 | form_tag '/adauth', :id => "adauth_login" do 11 | yield.html_safe 12 | end 13 | end 14 | 15 | # Create the default form by calling `adauth_form` and passing a username and password input 16 | def default_adauth_form 17 | adauth_form do 18 | "

#{label_tag :username}: 19 | #{text_field_tag :username}

20 |

#{label_tag :password}: 21 | #{password_field_tag :password}

22 |

#{submit_tag "Login!"}

" 23 | end 24 | end 25 | end 26 | end 27 | end 28 | 29 | ActionView::Base.send :include, Adauth::Rails::Helpers if defined? ActionView -------------------------------------------------------------------------------- /lib/adauth/rails/model_bridge.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module Rails 3 | # Included into Models in Rails 4 | # 5 | # Requires you to set 2 Constants 6 | # 7 | # AdauthMappings 8 | # 9 | # A hash which controls how Adauth maps its values to rails e.g. 10 | # 11 | # AdauthMappings = { 12 | # :name => :login 13 | # } 14 | # 15 | # This will store Adauths 'login' value in the 'name' field. 16 | # 17 | # AdauthSearchField 18 | # 19 | # This is an array which contains 2 values and is used to find the objects record e.g. 20 | # 21 | # AdauthSearchField = [:login, :name] 22 | # 23 | # This will cause RailsModel.where(:name => AdauthObject.login).first_or_initialize 24 | # 25 | # The Order is [adauth_field, rails_field] 26 | module ModelBridge 27 | # Registers the class methods when ModelBridge is included 28 | def self.included(base) 29 | base.extend ClassMethods 30 | end 31 | 32 | # Uses AdauthMappings to update the values on the model using the ones from Adauth 33 | def update_from_adauth(adauth_model) 34 | self.class::AdauthMappings.each do |k, v| 35 | setter = "#{k.to_s}=".to_sym 36 | value = v.is_a?(Array) ? v.join(", ") : v 37 | 38 | # ensure the attribute exists in the collected data and only assign if it does 39 | if adauth_model.respond_to?(value) 40 | self.send(setter, adauth_model.send(value)) 41 | end 42 | end 43 | self.save 44 | self 45 | end 46 | 47 | # Class Methods for ModelBridge 48 | module ClassMethods 49 | # Creates a new RailsModel from the adauth_model 50 | def create_from_adauth(adauth_model) 51 | rails_model = self.new 52 | rails_model.update_from_adauth(adauth_model) 53 | end 54 | 55 | # Used to create the RailsModel if it doesn't exist and update it if it does 56 | def return_and_create_from_adauth(adauth_model) 57 | adauth_field = self::AdauthSearchField.first 58 | adauth_search_value = adauth_model.send(adauth_field) 59 | rails_search_field = self::AdauthSearchField.second 60 | # Model#where({}).first_or_initialize is also compatible with Mongoid (3.1.0+) 61 | rails_model = self.send(:where, { rails_search_field => adauth_search_value }).first_or_initialize 62 | rails_model.update_from_adauth(adauth_model) 63 | rails_model 64 | end 65 | end 66 | end 67 | end 68 | end -------------------------------------------------------------------------------- /lib/adauth/search_results.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | class SearchResults < Array 3 | def limit(x) 4 | return self[0..(x-1)] 5 | end 6 | 7 | def order(field, direction = :asc) 8 | case direction 9 | when :asc 10 | return sort! { |x, y| x.send(field) <=> y.send(field) } 11 | when :desc 12 | return order(field, :asc).reverse! 13 | else 14 | raise "Invalid Order Provided, please use :asc or :desc" 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/adauth/version.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | # Adauths Version Number 3 | Version = '2.0.6' 4 | end 5 | -------------------------------------------------------------------------------- /lib/generators/adauth/config/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a config file for Adauth with sample data and details comments 3 | 4 | Example: 5 | rails g adauth:config -------------------------------------------------------------------------------- /lib/generators/adauth/config/config_generator.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module Generators 3 | 4 | # Generates a sample config file 5 | class ConfigGenerator < ::Rails::Generators::Base 6 | source_root File.expand_path('../templates', __FILE__) 7 | 8 | # Generates a sample config file 9 | # 10 | # Called by running: 11 | # rails g adauth:config 12 | def generate_config 13 | template "config.rb.erb", "config/initializers/adauth.rb" 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/generators/adauth/config/templates/config.rb.erb: -------------------------------------------------------------------------------- 1 | Adauth.configure do |c| 2 | # The Domain name of your Domain 3 | # 4 | # This is usually my_company.com or my_company.local 5 | # 6 | # If you don't know your domain contact your IT support, 7 | # it will be the DNS suffix applied to your machines 8 | c.domain = "example.com" 9 | 10 | # Adauth needs a query user to interact with the domain. 11 | # This user can be anything with domain access 12 | # 13 | # If Adauth doesn't work contact your IT support and make sure this account has full query access 14 | c.query_user = "username" 15 | c.query_password = "password" 16 | 17 | # The IP address or Hostname of a DC (Domain Controller) on your network 18 | # 19 | # This could be anything and probably wont be 127.0.0.1 20 | # 21 | # Again contact your IT Support if you can't work this out 22 | c.server = "127.0.0.1" 23 | 24 | # The LDAP base of your domain/intended users 25 | # 26 | # For all users in your domain the base would be: 27 | # dc=example, dc=com 28 | # OUs can be prepeneded to restrict access to your app 29 | c.base = "dc=example, dc=com" 30 | 31 | # The port isn't always needed as Adauth defaults to 389 the LDAP Port 32 | # 33 | # If your DC is on the other side of a firewall you may need to change the port 34 | # If your DC is using SSL, the port may be 636. 35 | #c.port = 389 36 | 37 | # If your DC is using SSL, set encryption to :simple_tls 38 | #c.encryption = :simple_tls 39 | 40 | # Windows Security groups to allow 41 | # 42 | # Only allow members of set windows security groups to login 43 | # 44 | # Takes an array for group names 45 | #c.allowed_groups = ["Group1", "Group2"] 46 | 47 | # Windows Security groups to deny 48 | # 49 | # Only allow users who aren't in these groups to login 50 | # 51 | # Takes an array for group names 52 | #c.denied_groups = ["Group1", "Group2"] 53 | end -------------------------------------------------------------------------------- /lib/generators/adauth/sessions/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a controller for sessions. 3 | 4 | Example: 5 | rails g adauth:sessions user 6 | 7 | rails g adauth:sessions employee -------------------------------------------------------------------------------- /lib/generators/adauth/sessions/sessions_generator.rb: -------------------------------------------------------------------------------- 1 | module Adauth 2 | module Generators 3 | # Generates the sessions controller 4 | class SessionsGenerator < ::Rails::Generators::Base 5 | source_root File.expand_path('../templates', __FILE__) 6 | argument :model_name, :type => :string, :default => "user" 7 | 8 | # Generates the sessions controller 9 | # 10 | # Called as: 11 | # rails g adauth:sessions 12 | # 13 | # Has 1 optional input of "model_name", which needs to be set the the model that include Adauth::UserModel 14 | def generate_sessions 15 | template "sessions_controller.rb.erb", "app/controllers/sessions_controller.rb" 16 | template "new.html.erb", "app/views/sessions/new.html.erb" 17 | route "resources :sessions" 18 | route "match \"/adauth\" => \"sessions#create\"" 19 | route "match \"/signout\" => \"sessions#destroy\"" 20 | puts " extra Add this code to your ApplicationController" 21 | puts "" 22 | puts " helper_method :current_user" 23 | puts "" 24 | puts " def current_user" 25 | puts " @current_user ||= User.find(session[:user_id]) if session[:user_id]" 26 | puts " end" 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /lib/generators/adauth/sessions/templates/new.html.erb: -------------------------------------------------------------------------------- 1 |

Login

2 | 3 | <%%= default_adauth_form %> -------------------------------------------------------------------------------- /lib/generators/adauth/sessions/templates/sessions_controller.rb.erb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | def new 3 | redirect_to root_path if current_user 4 | end 5 | 6 | def create 7 | ldap_user = Adauth.authenticate(params[:username], params[:password]) 8 | if ldap_user 9 | user = <%= model_name.camelize %>.return_and_create_from_adauth(ldap_user) 10 | session[:user_id] = user.id 11 | redirect_to root_path 12 | else 13 | redirect_to root_path, :error => "Invalid Login" 14 | end 15 | end 16 | 17 | def destroy 18 | session[:user_id] = nil 19 | redirect_to root_path 20 | end 21 | end -------------------------------------------------------------------------------- /rspec_results.txt: -------------------------------------------------------------------------------- 1 | ....................*.................. 2 | 3 | Pending: 4 | Adauth::AdObjects::User should allow you to reset the password 5 | # Insecure connection, unable to test change password 6 | # ./spec/adauth_ad_object_user_spec.rb:54 7 | 8 | Finished in 28.72 seconds 9 | 39 examples, 0 failures, 1 pending 10 | Coverage report generated for RSpec to /Users/arcath/Code/Gems/Adauth/coverage. 534 / 551 LOC (96.91%) covered. 11 | -------------------------------------------------------------------------------- /spec/adauth_ad_object_computer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::AdObjects::Computer do 4 | let(:computer) do 5 | ou = Adauth::AdObjects::OU.where('name', 'Domain Controllers').first 6 | ou.members.first 7 | end 8 | 9 | it "Should find a computer" do 10 | default_config 11 | computer.should be_a Adauth::AdObjects::Computer 12 | end 13 | 14 | it "should only find computers" do 15 | default_config 16 | Adauth::AdObjects::Computer.all.each do |computer| 17 | computer.should be_a Adauth::AdObjects::Computer 18 | end 19 | end 20 | 21 | it "should be in an ou" do 22 | default_config 23 | computer.ous.should be_a Array 24 | computer.ous.first.should be_a Adauth::AdObjects::OU 25 | computer.ous.first.name.should eq "Domain Controllers" 26 | end 27 | end -------------------------------------------------------------------------------- /spec/adauth_ad_object_folder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::AdObjects::Folder do 4 | let(:root_folder) do 5 | Adauth::AdObjects::Folder.root 6 | end 7 | 8 | it "should find the root of the domain" do 9 | default_config 10 | root_folder.should be_a Adauth::AdObjects::Folder 11 | end 12 | 13 | it "should have members" do 14 | default_config 15 | root_folder.members.should be_a Array 16 | end 17 | end -------------------------------------------------------------------------------- /spec/adauth_ad_object_group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::AdObjects::Group do 4 | let(:domain_admins) do 5 | Adauth::AdObjects::Group.where('name', 'Domain Admins').first 6 | end 7 | 8 | let(:test_ou) do 9 | Adauth::AdObjects::OU.where('name', test_data("domain", "testable_ou")).first 10 | end 11 | 12 | it "should have a name" do 13 | default_config 14 | domain_admins.name.should eq "Domain Admins" 15 | end 16 | 17 | it "should have a members list" do 18 | default_config 19 | domain_admins.members.should be_a Array 20 | domain_admins.members.last.name.should be_a String 21 | end 22 | 23 | it "should be a member of" do 24 | default_config 25 | domain_admins.groups.should be_a Array 26 | end 27 | 28 | it "should let you create and destroy a group" do 29 | default_config 30 | new_group = Adauth::AdObjects::Group.new_group("Adauth Test Group", test_ou) 31 | new_group.should be_a Adauth::AdObjects::Group 32 | new_group.delete 33 | Adauth::AdObjects::Group.where('name', "Adauth Test Group").count.should eq 0 34 | end 35 | end -------------------------------------------------------------------------------- /spec/adauth_ad_object_ou_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::AdObjects::OU do 4 | let(:domain_controllers) do 5 | Adauth::AdObjects::OU.where('name', 'Domain Controllers').first 6 | end 7 | 8 | it "should find Domain Controllers" do 9 | default_config 10 | domain_controllers.should be_a Adauth::AdObjects::OU 11 | end 12 | 13 | it "should have members" do 14 | default_config 15 | domain_controllers.members.should be_a Array 16 | end 17 | 18 | it "should have a computer as a member" do 19 | default_config 20 | domain_controllers.members.first.should be_a Adauth::AdObjects::Computer 21 | end 22 | end -------------------------------------------------------------------------------- /spec/adauth_ad_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::AdObject do 4 | let(:computer) do 5 | ou = Adauth::AdObjects::OU.where('name', 'Domain Controllers').first 6 | ou.members.first 7 | end 8 | 9 | let(:user) do 10 | Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 11 | end 12 | 13 | it "should still have method missing" do 14 | default_config 15 | computer.should be_a Adauth::AdObjects::Computer 16 | lambda { computer.foo_bar }.should raise_exception NoMethodError 17 | end 18 | 19 | it "should generate a nested group list" do 20 | user.cn_groups.should_not eq user.cn_groups_nested 21 | end 22 | end -------------------------------------------------------------------------------- /spec/adauth_ad_object_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::AdObjects::User do 4 | let(:user) do 5 | Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 6 | end 7 | 8 | let(:test_ou) do 9 | Adauth::AdObjects::OU.where('name', test_data("domain", "testable_ou")).first 10 | end 11 | 12 | it "should find administrator" do 13 | default_config 14 | user.login.should eq test_data("domain", "breakable_user") 15 | end 16 | 17 | it "should authenticate a user" do 18 | default_config 19 | Adauth::AdObjects::User.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_true 20 | lambda { Adauth::AdObjects::User.authenticate(test_data("domain", "query_user"), "does not work") }.should raise_exception 21 | end 22 | 23 | it "should find groups" do 24 | default_config 25 | user.groups.should be_a Array 26 | user.groups.first.should be_a Adauth::AdObjects::Group 27 | end 28 | 29 | it "should return boolean for member_of" do 30 | default_config 31 | user.member_of?("A Group").should be_false 32 | end 33 | 34 | it "should allow for modification" do 35 | default_config 36 | Adauth.add_field(Adauth::AdObjects::User, :phone, :homePhone) 37 | number = user.phone 38 | user.modify([[:replace, :homephone, "8765"]]) 39 | new_user = Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 40 | new_user.phone.should eq "8765" 41 | new_user.modify([[:replace, :homephone, number]]) 42 | new2_user = Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 43 | new2_user.phone.should eq number 44 | end 45 | 46 | it "should allow for additional methods" do 47 | default_config 48 | Adauth.add_field(Adauth::AdObjects::User, :description, :description) 49 | user.description.should be_a String 50 | Adauth.add_field(Adauth::AdObjects::User, :objectguid, :objectguid) 51 | user.objectguid.should be_a String 52 | end 53 | 54 | it "should allow you to reset the password" do 55 | default_config 56 | begin 57 | Adauth::AdObjects::User.authenticate(test_data("domain", "breakable_user"), test_data("domain", "breakable_password")).should be_true 58 | user = Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 59 | user.login.should eq test_data("domain", "breakable_user") 60 | user.set_password("adauth_test") 61 | Adauth::AdObjects::User.authenticate(test_data("domain", "breakable_user"), "adauth_test").should be_true 62 | user.set_password(test_data("domain", "breakable_password")) 63 | Adauth::AdObjects::User.authenticate(test_data("domain", "breakable_user"), test_data("domain", "breakable_password")).should be_true 64 | rescue RuntimeError 65 | pending("Insecure connection, unable to test change password") 66 | end 67 | end 68 | 69 | it "should be able to add a user to a group" do 70 | default_config 71 | new_group = Adauth::AdObjects::Group.new_group("Adauth Test Group", test_ou) 72 | user.add_to_group new_group 73 | rq_user = Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 74 | rq_user.member_of?("Adauth Test Group").should be_true 75 | new_group.delete 76 | end 77 | 78 | it "should be able to remove a user from a group" do 79 | default_config 80 | new_group = Adauth::AdObjects::Group.new_group("Adauth Test Group", test_ou) 81 | user.add_to_group new_group 82 | rq_user = Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 83 | rq_user.member_of?("Adauth Test Group").should be_true 84 | rq_user.remove_from_group new_group 85 | rq_user = Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 86 | rq_user.member_of?("Adauth Test Group").should be_false 87 | new_group.delete 88 | end 89 | 90 | it "should have find_by methods (and not break method_missing)" do 91 | default_config 92 | lambda { Adauth::AdObjects::User.fooooooooo }.should raise_exception 93 | Adauth::AdObjects::User.find_by_login(test_data("domain", "breakable_user")).should be_a Adauth::SearchResults 94 | end 95 | end -------------------------------------------------------------------------------- /spec/adauth_authenticate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth, "#authenticate" do 4 | it "should return a user for authentication" do 5 | default_config 6 | Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_a Adauth::AdObjects::User 7 | end 8 | 9 | it "should return false for failed authentication" do 10 | default_config 11 | Adauth.authenticate(test_data("domain", "query_user"), "foo").should be_false 12 | end 13 | 14 | it "should return false for a user that does not exist" do 15 | default_config 16 | Adauth.authenticate("foo", "bar").should be_false 17 | end 18 | 19 | it "should allow the user if allowed groups are used" do 20 | Adauth.configure do |c| 21 | c.domain = test_data("domain", "domain") 22 | c.port = test_data("domain", "port") 23 | c.base = test_data("domain", "base") 24 | c.server = test_data("domain", "server") 25 | c.query_user = test_data("domain", "query_user") 26 | c.query_password = test_data("domain", "query_password") 27 | c.allowed_groups = ["Administrators"] 28 | end 29 | Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_a Adauth::AdObjects::User 30 | end 31 | 32 | it "should allow the user if allowed ous are used" do 33 | Adauth.configure do |c| 34 | c.domain = test_data("domain", "domain") 35 | c.port = test_data("domain", "port") 36 | c.base = test_data("domain", "base") 37 | c.server = test_data("domain", "server") 38 | c.query_user = test_data("domain", "query_user") 39 | c.query_password = test_data("domain", "query_password") 40 | c.allowed_ous = ["Users"] 41 | end 42 | Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_a Adauth::AdObjects::User 43 | end 44 | 45 | it "should reject a user not in an allowed ou" do 46 | Adauth.configure do |c| 47 | c.domain = test_data("domain", "domain") 48 | c.port = test_data("domain", "port") 49 | c.base = test_data("domain", "base") 50 | c.server = test_data("domain", "server") 51 | c.query_user = test_data("domain", "query_user") 52 | c.query_password = test_data("domain", "query_password") 53 | c.allowed_ous = ["Users2"] 54 | end 55 | Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_false 56 | end 57 | 58 | it "should reject a user if denied group is used" do 59 | Adauth.configure do |c| 60 | c.domain = test_data("domain", "domain") 61 | c.port = test_data("domain", "port") 62 | c.base = test_data("domain", "base") 63 | c.server = test_data("domain", "server") 64 | c.query_user = test_data("domain", "query_user") 65 | c.query_password = test_data("domain", "query_password") 66 | c.denied_groups = ["Administrators"] 67 | end 68 | Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_false 69 | end 70 | 71 | it "should reject a user if denied ous is used" do 72 | Adauth.configure do |c| 73 | c.domain = test_data("domain", "domain") 74 | c.port = test_data("domain", "port") 75 | c.base = test_data("domain", "base") 76 | c.server = test_data("domain", "server") 77 | c.query_user = test_data("domain", "query_user") 78 | c.query_password = test_data("domain", "query_password") 79 | c.denied_ous = ["Users"] 80 | end 81 | Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_false 82 | end 83 | 84 | it "should allow for simple authentication" do 85 | Adauth.configure do |c| 86 | c.domain = test_data("domain", "domain") 87 | c.port = test_data("domain", "port") 88 | c.base = test_data("domain", "base") 89 | c.server = test_data("domain", "server") 90 | end 91 | 92 | Adauth.simple_authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")).should be_true 93 | end 94 | 95 | it "should allow for simple authentication to return false" do 96 | Adauth.configure do |c| 97 | c.domain = test_data("domain", "domain") 98 | c.port = test_data("domain", "port") 99 | c.base = test_data("domain", "base") 100 | c.server = test_data("domain", "server") 101 | end 102 | 103 | Adauth.simple_authenticate(test_data("domain", "query_user"), 'not the password').should be_false 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/adauth_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::Config, :no_ad => true do 4 | it "should default port to 389" do 5 | config = Adauth::Config.new 6 | config.port.should eq 389 7 | end 8 | 9 | it "should calculate the default settings" do 10 | config = Adauth::Config.new 11 | config.domain = "example.com" 12 | config.base.should eq "dc=example, dc=com" 13 | config.server.should eq "example.com" 14 | end 15 | end -------------------------------------------------------------------------------- /spec/adauth_connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::Connection do 4 | it "should support encryption" do 5 | Adauth.configure do |c| 6 | c.domain = test_data("domain", "domain") 7 | c.port = test_data("domain", "port") 8 | c.base = test_data("domain", "base") 9 | c.server = test_data("domain", "server") 10 | c.encryption = :simple_tls 11 | c.query_user = test_data("domain", "query_user") 12 | c.query_password = test_data("domain", "query_password") 13 | end 14 | begin 15 | Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")) 16 | rescue 17 | # Failed to authenticate due to encryption (not what we are testing here) 18 | end 19 | end 20 | 21 | it "should timeout if asked to connect to a server that doesn't exist" do 22 | Adauth.configure do |c| 23 | c.domain = test_data("domain", "domain") 24 | c.port = test_data("domain", "port") 25 | c.base = test_data("domain", "base") 26 | c.server = "127.0.0.2" 27 | c.query_user = test_data("domain", "query_user") 28 | c.query_password = test_data("domain", "query_password") 29 | end 30 | 31 | lambda { Adauth::AdObjects::User.all }.should raise_exception 32 | end 33 | end -------------------------------------------------------------------------------- /spec/adauth_issue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "issue #37" do 4 | it "should not happen" do 5 | default_config 6 | ldap_user = Adauth.authenticate("administrator", "foo") 7 | ldap_user.should be_false 8 | ldap_user = Adauth.authenticate(test_data("domain", "breakable_user"), "") 9 | ldap_user.should be_false 10 | ldap_user = Adauth.authenticate(test_data("domain", "query_user"), test_data("domain", "query_password")) 11 | ldap_user.should be_a Adauth::AdObjects::User 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/adauth_rails_model_bridge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class TestUserModel 4 | include Adauth::Rails::ModelBridge 5 | 6 | attr_accessor :name 7 | 8 | AdauthMappings = { 9 | :name => :name 10 | } 11 | 12 | AdauthSearchField = [:name] 13 | 14 | def self.find_by_name(name) 15 | TestUserModel.new 16 | end 17 | 18 | def save 19 | return true 20 | end 21 | end 22 | 23 | describe Adauth::Rails::ModelBridge do 24 | let(:user) do 25 | Adauth::AdObjects::User.where('sAMAccountName', test_data("domain", "breakable_user")).first 26 | end 27 | 28 | it "should extend", :no_ad => true do 29 | TestUserModel.should respond_to :create_from_adauth 30 | end 31 | 32 | it "should create the model" do 33 | default_config 34 | TestUserModel.create_from_adauth(user) 35 | end 36 | 37 | it "should return and create the model" do 38 | default_config 39 | TestUserModel.return_and_create_from_adauth(user) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/adauth_search_results_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth::SearchResults, :no_ad => true do 4 | let(:test_array) do 5 | [OpenStruct.new({name: "foo"}), OpenStruct.new({name: "bar"}), OpenStruct.new({name: "widget"})] 6 | end 7 | 8 | let(:sorted_array) do 9 | [OpenStruct.new({name: "bar"}), OpenStruct.new({name: "foo"}), OpenStruct.new({name: "widget"})] 10 | end 11 | 12 | let(:search_results) do 13 | Adauth::SearchResults.new(test_array) 14 | end 15 | 16 | it "should create self from_array" do 17 | Adauth::SearchResults.new(test_array).should be_a Adauth::SearchResults 18 | end 19 | 20 | it "should have the limit function" do 21 | search_results.limit(2).length.should eq 2 22 | search_results.limit(2).last.should_not eq test_array.last 23 | search_results.limit(2).should be_a Adauth::SearchResults 24 | end 25 | 26 | it "should have the order function" do 27 | search_results.order(:name, :asc).should eq sorted_array 28 | search_results.order(:name, :asc).should be_a Adauth::SearchResults 29 | search_results.order(:name, :desc).should eq sorted_array.reverse 30 | search_results.order(:name, :desc).should be_a Adauth::SearchResults 31 | end 32 | 33 | it "should handle having a wrong direction passed to it" do 34 | lambda { search_results.order(:name, :foo) }.should raise_exception 35 | end 36 | 37 | it "should default to :asc for order" do 38 | search_results.order(:name).should eq sorted_array 39 | search_results.order(:name).should be_a Adauth::SearchResults 40 | end 41 | end -------------------------------------------------------------------------------- /spec/adauth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Adauth, :no_ad => true do 4 | it "should accept a block" do 5 | Adauth.configure do |c| 6 | end 7 | end 8 | 9 | it "should be able to have a new logged defined" do 10 | Adauth.logger= Logger.new('log/newlogger.log', 'daily') 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Test coverage 2 | require 'simplecov' 3 | SimpleCov.start 4 | 5 | # Requires 6 | require 'adauth' 7 | require 'yaml' 8 | 9 | def default_config 10 | Adauth.configure do |c| 11 | c.domain = test_data("domain", "domain") 12 | c.port = test_data("domain", "port") 13 | c.base = test_data("domain", "base") 14 | c.server = test_data("domain", "server") 15 | c.encryption = test_data("domain", "encryption").to_sym if test_data("domain", "encryption") 16 | c.allow_fallback = test_data("domain", "allow_fallback") if test_data("domain", "allow_fallback") 17 | c.query_user = test_data("domain", "query_user") 18 | c.query_password = test_data("domain", "query_password") 19 | end 20 | 21 | Adauth.logger = Logger.new('/dev/null') 22 | end 23 | 24 | def test_data(set, key) 25 | @yaml ||= YAML::load(File.open('spec/test_data.yml')) 26 | @yaml[set][key] 27 | end 28 | -------------------------------------------------------------------------------- /spec/test_data.example.yml: -------------------------------------------------------------------------------- 1 | domain: 2 | domain: example.com 3 | port: 389 # Change to 636 for ssl encryption 4 | #encryption: simple_tls 5 | #allow_fallback: 389 # Used to fallback to insecure 6 | base: "dc=example, dc=com" 7 | server: dc1.example.com 8 | query_user: User.Name 9 | query_password: Password 10 | breakable_user: User.Name 11 | breakable_password: Password 12 | testable_ou: Your OU --------------------------------------------------------------------------------