ldap_query 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ldap_query"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'ldap_query/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'ldap_query'
9
+ spec.version = LdapQuery::VERSION
10
+ spec.authors = ['Brandon Hicks']
11
+ spec.email = ['tarellel@gmail.com']
12
+
13
+ spec.summary = 'To easily connect and query ldap'
14
+ spec.description = 'Easily generate LDAP connections and queries without having to learn how to build an LDAP connection with ruby.'
15
+ spec.homepage = "https://github.com/tarellel/ldap_query"
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
22
+
23
+ # spec.files = Dir.glob('lib/**/*')
24
+ spec.files = `git ls-files | grep -Ev '^(test|myapp|examples)'`.split("\n")
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency('activesupport', '>= 5.0')
30
+ spec.add_dependency('net-ldap', '~> 0.16')
31
+ spec.add_development_dependency('bundler', '>= 1.17', '< 3.0')
32
+ spec.add_development_dependency('rake', '~> 13')
33
+ spec.add_development_dependency('rspec', '~> 3.9')
34
+ end
data/lib/ldap_query.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ldap_query/version'
4
+ require 'active_support'
5
+ require 'active_support/core_ext/object/blank' # Used so methods like .blank? will be available
6
+ require_relative 'ldap_query/ldap_query'
7
+ require_relative 'ldap_query/authenticate'
8
+ require_relative 'ldap_query/config'
9
+ require_relative 'ldap_query/connection'
10
+ require_relative 'ldap_query/error'
11
+ require_relative 'ldap_query/filter'
12
+ require_relative 'ldap_query/ldap_helper'
13
+ require_relative 'ldap_query/rails_credentials'
14
+ require_relative 'ldap_query/query'
15
+
16
+ require_relative 'ldap_query/railtie' if defined?(::Rails) || defined?(Rails)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LdapQuery
4
+ # Used to authenticate a users LDAP credentials to a user in LDAP
5
+ class Authenticate
6
+ attr_accessor :config, :connection
7
+
8
+ REQUIRED_CONNECTION_KEYS = %i[host username password base].freeze
9
+
10
+ # Initialzile an ldap connection for authenticating a user
11
+ #
12
+ # @params credentials [Hash]
13
+ def initialize(credentials = {})
14
+ establish_connection(credentials)
15
+ end
16
+
17
+ # Authenticate the user again ldap with the supplied username/password
18
+ #
19
+ # @param username [String]
20
+ # @param password [String]
21
+ # @return [Boolean, Hash, Net::Ldap]
22
+ def auth_user(username, password)
23
+ return false if username.nil? || password.nil?
24
+
25
+ response = @connection.link.bind_as(base: @config.base,
26
+ size: 1,
27
+ filter: LdapQuery::Filter.auth(username),
28
+ password: password)
29
+ # if no user was found return false, otherwise return the user
30
+ (response && response[0]) ? response : false
31
+ end
32
+
33
+ private
34
+
35
+ # Establish an ldap connection without fulling binding a connection yet
36
+ #
37
+ # @params credentials [Hash]
38
+ def establish_connection(credentials = {})
39
+ raise_keys_error if credentials.blank? || !required_credentials_supplied?(credentials)
40
+
41
+ @config = LdapQuery::Config.new(credentials)
42
+ @connection = LdapQuery::Connection.new(@config.auth_hash, type: :auth)
43
+ @connection.link.auth(@config.username, @config.password)
44
+ rescue
45
+ raise(ConnectionError, 'Failure connecting to LDAP host')
46
+ end
47
+
48
+ # Verify if the required encryption keys have been supplied
49
+ #
50
+ # @return [Boolean]
51
+ def required_credentials_supplied?(credentials = {})
52
+ # Verify all reqiured credentail values have been supplied for the LDAP connection
53
+ REQUIRED_CONNECTION_KEYS.all? { |req_key| credentials.key?(req_key) }
54
+ end
55
+
56
+ # Raise an exception if any of the credentials are missing any key/value for authenticating a user
57
+ def raise_keys_error
58
+ raise(CredentialsError, "The following credentials attributes are required: #{REQUIRED_CONNECTION_KEYS}")
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash'
4
+
5
+ module LdapQuery
6
+ # Used to validate and filter credentials for establishing an LDAP connection
7
+ class Config
8
+ DEFAULT_CONFIG = { port: 389, # Usually 389 && 636
9
+ encryption: nil,
10
+ base: nil, # 'dc=company,dc=tld'
11
+ username: nil,
12
+ password: nil,
13
+ method: :simple }.freeze
14
+ ALLOWED_KEYS = %i[base encryption host method port username password].freeze
15
+ ALLOWED_KEYS.each { |attr| attr_accessor attr }
16
+
17
+ # Required to be assigned and not have nil valuess
18
+ REQUIRED_KEYS = %i[base username password host port].freeze
19
+
20
+ # Attributes whos values are required to by symbols for the LDAP gem
21
+ VALS_REQUIRED_TO_BE_SYMBOLS = %i[encryption method].freeze
22
+
23
+ # Build and validate the configuration hash supplied for creating an LDAP connection
24
+ #
25
+ # @param config [Hash]
26
+ # @return [Hash]
27
+ def initialize(config = {})
28
+ raise(ArgumentError, "the following attributes are required for an ldap connection #{REQUIRED_KEYS}") unless config.is_a?(Hash) && !config.blank?
29
+
30
+ map_variables(validate_keys(cleanup_hash(defaults(config))))
31
+ end
32
+
33
+ # Build a hash out of the provided config to establish an LDAP connection
34
+ #
35
+ # @return [Hash]
36
+ def hash
37
+ @hash ||= { host: @host, port: @port, base: @base, encryption: @encryption }.merge(credentials_hash).freeze
38
+ end
39
+
40
+ # Build a hash of required config for authenticating a user against ldap
41
+ #
42
+ # @return [Hash]
43
+ def auth_hash
44
+ @auth_hash ||= { host: @host, port: @port, encryption: @encryption }.freeze
45
+ end
46
+
47
+ private
48
+
49
+ # Build a hash with the connections username, password, and method for the LDAP connection
50
+ #
51
+ # @return [Hash]
52
+ def credentials_hash
53
+ raise(ConfigError, 'config username or password are nil') unless @username && @password
54
+
55
+ credentials_hash = { username: @username, password: @password }.freeze
56
+ credentials_hash = credentials_hash.merge({ method: @method }) if @method
57
+ { auth: credentials_hash }.freeze
58
+ end
59
+
60
+ # Associate all the config hash key/values as instance variables for easy reference
61
+ #
62
+ # @param config_hash [Hash]
63
+ def map_variables(config_hash = {})
64
+ ALLOWED_KEYS.each do |attr|
65
+ instance_variable_set("@#{attr}", config_hash[attr])
66
+ end
67
+ end
68
+
69
+ # Set default keys/values if not supplied in the config hash
70
+ #
71
+ # @param config [Hash]
72
+ # @return [Hash]
73
+ def defaults(config = {})
74
+ config.reverse_merge(DEFAULT_CONFIG).freeze
75
+ end
76
+
77
+ # Loop through the supplied config hash, make all keys unique and symbolzied, and symbolize the specified values
78
+ #
79
+ # @param config_hash [Hash]
80
+ # @return [Hash]
81
+ def cleanup_hash(config_hash = {})
82
+ # ensure keys like :key and 'key' are considered the same
83
+ # this is similar to running .uniq on an array
84
+ config_hash.with_indifferent_access.symbolize_keys.tap do |config|
85
+ VALS_REQUIRED_TO_BE_SYMBOLS.each do |key|
86
+ config[key] = config[key].to_sym if config[key]
87
+ end
88
+ end
89
+ end
90
+
91
+ # Validate all required keys have been included
92
+ #
93
+ # @param config_hash [Hash]
94
+ # @return [Boolean]
95
+ def required_keys_included?(config_hash = {})
96
+ REQUIRED_KEYS.all? do |key|
97
+ raise(ConfigError, "config key #{key} is required to be set.") unless config_hash.key?(key)
98
+ end
99
+ end
100
+
101
+ # Ensure that a handful of parameters have actual values and not just nil or ''
102
+ #
103
+ # @param config_hash [Hash]
104
+ # @return [Boolean]
105
+ def required_vals_given?(config_hash)
106
+ raise(ConfigError, "required config values (#{REQUIRED_KEYS}) can not be nil") unless REQUIRED_KEYS.all? { |k| !config_hash[k].nil? }
107
+ end
108
+
109
+ # Filter out and remove unneeded configuration hash keys
110
+ #
111
+ # @param config_hash [Hash]
112
+ # @return [Hash]
113
+ def remove_unknown_keys(config_hash)
114
+ config_hash.select { |key, _v| ALLOWED_KEYS.include?(key) }.freeze
115
+ end
116
+
117
+ # Validate and cleanup all supplied configuration key/values
118
+ #
119
+ # @param config_hash [Hash]
120
+ def validate_keys(config_hash = {})
121
+ required_keys_included?(config_hash)
122
+ required_vals_given?(config_hash)
123
+ remove_unknown_keys(config_hash)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/ldap'
4
+
5
+ module LdapQuery
6
+ # For establishing an LDAP connection (binding LDAP connection)
7
+ class Connection
8
+ attr_accessor :link
9
+
10
+ REQUIRED_KEYS = %i[host port base auth].freeze
11
+
12
+ # Used for creating the initial Ldap connection for querying with supplied parameters
13
+ #
14
+ # @param credentials [Hash]
15
+ # @return [Interface <Net::Ldap>]
16
+ def initialize(credentials, type: :basic)
17
+ if type == :auth
18
+ credentials = filter_auth_credentials(credentials)
19
+ else
20
+ valid_credentials?(credentials)
21
+ end
22
+ @link = bind_connection(credentials)
23
+ end
24
+
25
+ private
26
+
27
+ # Filter out all parameter keyus/values and only keep the required auth keys
28
+ #
29
+ # @param credentials [Hash]
30
+ # @return [Hash]
31
+ def filter_auth_credentials(credentials)
32
+ auth_keys = %i[host port encryption].freeze
33
+ credentials.select { |key, _v| auth_keys.include?(key) }.freeze
34
+ end
35
+
36
+ # Validate all required keys have been included
37
+ #
38
+ # @param credentials [Hash]
39
+ # @return [Hash]
40
+ def valid_credentials?(credentials)
41
+ credentials_error if !credentials.is_a?(Hash) || credentials.empty?
42
+ required_credentials?(credentials)
43
+ end
44
+
45
+ # Validate all required auth credentials have been supplied
46
+ #
47
+ # @param credentials [Hash]
48
+ # @return [Hash]
49
+ def required_credentials?(credentials = {})
50
+ credentials_error unless REQUIRED_KEYS.all? { |k| credentials[k] }
51
+ end
52
+
53
+ # Raise an exception error if not all LDAP credentials have been included
54
+ def credentials_error
55
+ raise(CredentialsError, 'valid ldap credentials must be passed in order to establish a connection')
56
+ end
57
+
58
+ # Create LDAP connection string
59
+ #
60
+ # @params credentials [Hash]
61
+ # @return [Interface <Net::Ldap>]
62
+ def bind_connection(credentials)
63
+ Net::LDAP.new(credentials)
64
+ rescue
65
+ raise(ConnectionError, 'Failure connecting to LDAP host')
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Used for creating exceptions with the Ldap connections and configuration
4
+ module LdapQuery
5
+ class Error < StandardError; end
6
+
7
+ class AttributeError < Error; end
8
+ class ConnectionError < Error; end
9
+ class CredentialsError < Error; end
10
+ class ConfigError < Error; end
11
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net-ldap'
4
+
5
+ module LdapQuery
6
+ # Used to create return LDAP query stings depending on what attribute you want to filter
7
+ class Filter
8
+ # Used to create an LDAP filter for the specified LDAP attribute
9
+ #
10
+ # @param str [String]
11
+ # @attr wildcard [Boolean] used to determine if the filter should be wildcard match
12
+ # @example:
13
+ # LdapQuery::Filter.cn('mscott')
14
+ # @example:
15
+ # LdapQuery::Filter.memberof('CN=somegroup,OU=OrganizationalUnit,DC=company.tld)
16
+ %w[cn displayname mail memberof samaccountname].each do |attr|
17
+ define_singleton_method(attr) do |str, wildcard: false|
18
+ Net::LDAP::Filter.eq(attr, clean_str(str, wildcard: wildcard))
19
+ end
20
+ end
21
+
22
+ # Generally most ldap queries are again person, sometimes other types will be used for service accounts
23
+ #
24
+ # @param str [String]
25
+ # @attr wildcard [Boolean] used to determine if the filter should be wildcard match
26
+ def self.object_class(str = 'person', wildcard: false)
27
+ Net::LDAP::Filter.eq('objectClass', clean_str(str, wildcard: wildcard))
28
+ end
29
+
30
+ # Used to filter LDAP accounts against a custom attribute and valuess
31
+ #
32
+ # @param attr [String] a custom attribute to query again
33
+ # @param val [String] a user specified value to filter against
34
+ # @attr wildcard [Boolean] used to determine if the filter should be wildcard match
35
+ def self.other(attr, val, wildcard: false)
36
+ Net::LDAP::Filter.eq(attr, clean_str(val, wildcard: wildcard))
37
+ end
38
+
39
+ # Filter user based on CN attribute (CN is a required attribute)
40
+ #
41
+ # @param username [String]
42
+ # @attr wildcard [Boolean] used to determine if the filter should be wildcard match
43
+ def self.person(username, wildcard: false)
44
+ cn_filter = LdapQuery::Filter.cn(username, wildcard: wildcard)
45
+ user_filter = LdapQuery::Filter.object_class
46
+ Net::LDAP::Filter.join(cn_filter, user_filter)
47
+ end
48
+
49
+ # Filter used to authenticate a validate CN & Person entry
50
+ #
51
+ # @param username [String]
52
+ def self.auth(username)
53
+ Net::LDAP::Filter.join(cn(username), object_class)
54
+ end
55
+
56
+ # If you want to wildcard it this turns all spaces into '*' and adds '*' at the beginning and end of the str as well
57
+ #
58
+ # @param str [String] the query str
59
+ # @attr wildcard [Boolean] used to determine if the filter should be wildcard match
60
+ # @return [String] either the original str, or a value prepared to wildcard when hitting ldap
61
+ def self.clean_str(str, wildcard: false)
62
+ str = str&.strip
63
+ return str unless wildcard && str.is_a?(String)
64
+
65
+ str = str.split(/\s/).compact.join('*').squeeze('*')
66
+ "*#{str}*"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LdapQuery
4
+ # Methods to be included as helpers in the rails application
5
+ module LdapHelper
6
+ def self.included(base); end
7
+ def self.extended(base); end
8
+
9
+ # Search ldap based on users samaccountname attribute
10
+ #
11
+ # @param str [String] the attributes string value you want to query against
12
+ # @param credentials [Hash]
13
+ def search_ldap_by_username(str, credentials = {}, wildcard: false, limit: 20)
14
+ credentials = determine_credentials(credentials)
15
+ LdapQuery::Query.perform(credentials, attr: :cn, val: str, wildcard: wildcard, limit: limit)
16
+ end
17
+
18
+ # Query LDAP based on all users displayname attribute
19
+ #
20
+ # @param str [String] the attributes string value you want to query against
21
+ # @params wildcard [Boolean] used to determine if the filter should be wildcard match
22
+ def search_ldap_by_name(str, credentials = {}, wildcard: false, limit: 20)
23
+ credentials = determine_credentials(credentials)
24
+ LdapQuery::Query.perform(credentials, attr: :displayname, val: str, wildcard: wildcard, limit: limit)
25
+ end
26
+
27
+ # Query ldap based on memberof attribute
28
+ #
29
+ # @param str [String] the attributes string value you want to query against
30
+ # @param credentials [Hash]
31
+ # @params wildcard [Boolean] used to determine if the filter should be wildcard match
32
+ def search_ldap_by_group(str, credentials = {}, wildcard: false, limit: 20)
33
+ credentials = determine_credentials(credentials)
34
+ LdapQuery::Query.perform(credentials, attr: :memberof, val: str, wildcard: wildcard, limit: limit)
35
+ end
36
+
37
+ # Query LDAP against a custome attribute and value
38
+ #
39
+ # @param credentials [Hash]
40
+ # @params attr [String, Symbol] to attribute you want to query against
41
+ # @params val [String, Symbol] the attribute vale you want to query with when filtering against ldap results
42
+ # @params wildcard [Boolean] used to determine if the filter should be wildcard match
43
+ def search_ldap_by_other(credentials = {}, attr: nil, val: nil, wildcard: false, limit: 20)
44
+ raise(AttributeError, 'a valid attribute name and value must be supplied to query against') if attr.nil? || val.nil?
45
+
46
+ credentials = determine_credentials(credentials)
47
+ LdapQuery::Query.perform(credentials, attr: attr, val: val, wildcard: wildcard, limit: limit)
48
+ end
49
+
50
+ # Query ldap based on mail attribute
51
+ #
52
+ # @param str [String] the attributes string value you want to query against
53
+ # @param credentials [Hash]
54
+ # @params wildcard [Boolean] used to determine if the filter should be wildcard match
55
+ def search_ldap_by_mail(str, credentials = {}, wildcard: false, limit: 20)
56
+ credentials = determine_credentials(credentials)
57
+ LdapQuery::Query.perform(credentials, attr: :mail, val: str, wildcard: wildcard, limit: limit)
58
+ end
59
+
60
+ # Used to authenticate a user against ldap
61
+ #
62
+ # @param credentials [Hash]
63
+ # @param username [String]
64
+ # @params password [String]
65
+ def authenticate_user(credentials, username: nil, password: '')
66
+ username, password = username.to_s, password.to_s
67
+ credentials = determine_credentials(credentials)
68
+ raise(AttributeError, 'an ldap username is required in order to authenticate.') if username.nil?
69
+
70
+ LdapQuery::Authenticate.new(credentials).auth_user(username, password)
71
+ end
72
+
73
+ private
74
+
75
+ # Determine if the helpers have had ldap credentials passed to it
76
+ # If not, it will attempt to grab ldap credentials from the applications encrypted credentials
77
+ #
78
+ # @param credentials [Hash]
79
+ # @return [Hash]
80
+ def determine_credentials(credentials = {})
81
+ return credentials if credentials.is_a?(Hash) && !credentials.empty?
82
+
83
+ rails_credentials
84
+ end
85
+
86
+ # Grab the applications Rails.application.credentials[:ldap]
87
+ #
88
+ # @return [Hash]
89
+ def rails_credentials
90
+ @_rails_credentials ||= LdapQuery::RailsCredentials.credentials
91
+ end
92
+ end
93
+ end