Class: Entitlements::Service::LDAP

Inherits:
Object
  • Object
show all
Includes:
Contracts::Core
Defined in:
lib/entitlements.rb,
lib/entitlements/service/ldap.rb

Defined Under Namespace

Classes: ConnectionError, DuplicateEntryError, EntryError, WTFError

Constant Summary collapse

C =
::Contracts

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Contracts::Core

common, extended, included

Constructor Details

#initialize(addr:, binddn:, bindpw:, ca_file: , disable_ssl_verification: false, person_dn_format:) ⇒ LDAP

Returns a new instance of LDAP.



75
76
77
78
79
80
81
82
83
# File 'lib/entitlements/service/ldap.rb', line 75

def initialize(addr:, binddn:, bindpw:, ca_file: ENV["LDAP_CACERT"], disable_ssl_verification: false, person_dn_format:)
  # Save some parameters for the LDAP connection but don't actually bind yet.
  @addr = addr
  @binddn = binddn
  @bindpw = bindpw
  @ca_file = ca_file
  @disable_ssl_verification = disable_ssl_verification
  @person_dn_format = person_dn_format
end

Instance Attribute Details

#binddnObject (readonly)

We use the binddn as the owner of the group, for lack of anything better. This keeps the schema happy.



18
19
20
# File 'lib/entitlements/service/ldap.rb', line 18

def binddn
  @binddn
end

#person_dn_formatObject (readonly)

We use the binddn as the owner of the group, for lack of anything better. This keeps the schema happy.



18
19
20
# File 'lib/entitlements/service/ldap.rb', line 18

def person_dn_format
  @person_dn_format
end

Class Method Details

.new_with_cache(addr:, binddn:, bindpw:, ca_file: ENV["LDAP_CACERT"], disable_ssl_verification: false, person_dn_format:) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/entitlements/service/ldap.rb', line 39

def self.new_with_cache(addr:, binddn:, bindpw:, ca_file: ENV["LDAP_CACERT"], disable_ssl_verification: false, person_dn_format:)
  # only look at LDAP_DISABLE_SSL_VERIFICATION in the environment if we didn't pass true to the method already
  if disable_ssl_verification == false
    # otherwise if it's set to anything at all in env, disable ssl verification
    disable_ssl_verification = !!ENV["LDAP_DISABLE_SSL_VERIFICATION"]
  end
  fingerprint = [addr, binddn, bindpw, ca_file, disable_ssl_verification, person_dn_format].map(&:inspect).join("|")
  Entitlements.cache[:ldap_connections] ||= {}
  Entitlements.cache[:ldap_connections][fingerprint] ||= new(
    addr: addr,
    binddn: binddn,
    bindpw: bindpw,
    ca_file: ca_file,
    disable_ssl_verification: disable_ssl_verification,
    person_dn_format: person_dn_format
  )
end

Instance Method Details

#delete(dn) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/entitlements/service/ldap.rb', line 173

def delete(dn)
  # See if the object exists by searching for it. If it exists we'll get its data back as a hash. If not
  # we'll get an empty hash. We don't need to delete something that doesn't already exist.
  unless exists?(dn)
    Entitlements.logger.debug "Not deleting #{dn} because it does not exist"
    return true
  end

  ldap.delete(dn: dn)
  operation_result = ldap.get_operation_result
  return true if operation_result["code"] == 0
  Entitlements.logger.error "Error deleting #{dn}: #{operation_result['message']}"
  false
end

#exists?(dn) ⇒ Boolean

Returns:

  • (Boolean)


147
148
149
# File 'lib/entitlements/service/ldap.rb', line 147

def exists?(dn)
  read(dn).is_a?(Net::LDAP::Entry)
end

#modify(dn, updates) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/entitlements/service/ldap.rb', line 195

def modify(dn, updates)
  return false unless updates.any?
  updates.each do |attr_name, val|
    operation = ""
    if val.nil?
      next if ldap.delete_attribute(dn, attr_name)
      operation = "deleting"
    else
      next if ldap.replace_attribute(dn, attr_name, val)
      operation = "modifying"
    end
    operation_result = ldap.get_operation_result
    Entitlements.logger.error "Error #{operation} attribute #{attr_name} in #{dn}: #{operation_result['message']}"
    Entitlements.logger.error "LDAP code=#{operation_result.code}: #{operation_result.error_message}"
    return false
  end
  true
end

#read(dn) ⇒ Object



92
93
94
95
96
# File 'lib/entitlements/service/ldap.rb', line 92

def read(dn)
  @dn_cache ||= {}
  @dn_cache[dn] ||= search(base: dn, attrs: "*", scope: Net::LDAP::SearchScope_BaseObject)[dn] || :none
  @dn_cache[dn] == :none ? nil : @dn_cache[dn]
end

#search(base:, filter: nil, attrs: "*", index: :dn, scope: Net::LDAP::SearchScope_WholeSubtree) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/entitlements/service/ldap.rb', line 114

def search(base:, filter: nil, attrs: "*", index: :dn, scope: Net::LDAP::SearchScope_WholeSubtree)
  Entitlements.logger.debug "LDAP Search: filter=#{filter.inspect} base=#{base.inspect}"

  # Ruby downcases these in the results anyway, so just downcase everything here so it'll
  # be consistent no matter what. LDAP is case insensitive after all!
  downcased_attrs = attrs == "*" ? "*" : attrs.map { |a| a.downcase }

  result = {}
  ldap.search(base: base, filter: filter, attributes: downcased_attrs, scope: scope, return_result: false) do |entry|
    result_key = index == :dn ? entry.dn : entry[index]
    unless result_key
      raise EntryError, "#{entry.dn} has no value for #{index.inspect}"
    end

    if result.key?(result_key)
      other_entry_dn = result[result_key].dn
      raise DuplicateEntryError, "#{entry.dn} and #{other_entry_dn} have the same value of #{index} = #{result_key.inspect}"
    end

    result[result_key] = entry
  end

  Entitlements.logger.debug "Completed search: #{result.keys.size} result(s)"

  result
end

#upsert(dn:, attributes:) ⇒ Object



161
162
163
164
165
# File 'lib/entitlements/service/ldap.rb', line 161

def upsert(dn:, attributes:)
  # See if the object exists by searching for it. If it exists we'll get its data back as a hash. If not
  # we'll get an empty hash. Dispatch this to the create or update methods.
  read(dn) ? update(dn: dn, existing: read(dn), attributes: attributes) : create(dn: dn, attributes: attributes)
end