module ASF::LDAP

Constants

CONNECT_LOCK

Mutex preventing simultaneous connections to LDAP from a single process

LDAP_CREDS
LDAP_MISC

Public Class Methods

add(dn, list) click to toggle source

add an entry to LDAP; dump information on LDAP errors

# File lib/whimsy/asf/ldap.rb, line 287
def self.add(dn, list)
  ASF.ldap.add(dn, list)
rescue ::LDAP::ResultError
  Wunderbar.warn(list.inspect)
  Wunderbar.warn(dn.to_s)
  raise
end
bind(user=nil, password=nil, &block) click to toggle source

connect to LDAP with a user and password; generally required for update operations. If a block is passed, the connection will be closed after the block executes.

when run interactively, will default user and prompt for password

# File lib/whimsy/asf/ldap.rb, line 97
def self.bind(user=nil, password=nil, &block)
  if not user or not password
    raise ArgumentError.new('Need user name and password') unless STDIN.isatty

    require 'etc'
    require 'io/console'
    user ||= Etc.getlogin
    password = STDIN.getpass("Password for #{user}:")
  end

  dn = ASF::Person.new(user).dn

  begin
    @ldap.unbind if @ldap&.bound?
  rescue StandardError
    # ignore
  end
  self.rwhosts.each do |rwhost|
    begin
      ldap = ASF._init_ldap(true, [rwhost])
      Wunderbar.debug("#{ldap.object_id}: bind as #{dn} as #{ldap}")
      if block
        ASF.flush_weakrefs
        ldap.bind(dn, password, &block)
        ASF._init_ldap(true)
      else
        ldap.bind(dn, password)
      end
      break
    rescue ::LDAP::ResultError => e
      if e.message == "Can't contact LDAP server" # Any others worth a retry?
        Wunderbar.warn "#{rwhost}: #{e.inspect}, continuing"
      else
        raise
      end
    end
  end
ensure
  ASF.flush_weakrefs
end
configure() click to toggle source

update /etc/ldap.conf. Usage:

sudo ruby -I /srv/whimsy/lib -r whimsy/asf -e "ASF::LDAP.configure"
# File lib/whimsy/asf/ldap.rb, line 232
def self.configure
  cert = Dir["#{ETCLDAP}/asf*-ldap-client.pem"].first

  # verify/obtain/write the cert
  unless cert
    cert = "#{ETCLDAP}/asf-ldap-client.pem"
    File.write cert, self.extract_cert
  end

  # read the current configuration file
  ldap_conf = "#{ETCLDAP}/ldap.conf"
  content = File.read(ldap_conf)

  # ensure that the right cert is used
  unless content =~ /asf.*-ldap-client\.pem/
    content.gsub!(/^TLS_CACERT/i, '# TLS_CACERT')
    content += "TLS_CACERT #{ETCLDAP}/asf-ldap-client.pem\n"
  end

  # provide the URIs of the ldap hosts
  content.gsub!(/^URI/, '# URI')
  content += "uri \n" unless content =~ /^uri /
  content[/uri (.*)\n/, 1] = hosts(false).join(' ')

  # verify/set the base
  unless content.include? 'base dc=apache'
    content.gsub!(/^BASE/i, '# BASE')
    content += "base dc=apache,dc=org\n"
  end

  # ensure TLS_REQCERT is allow (macOS only)
  if ETCLDAP.include? 'openldap' and not content.include? 'REQCERT allow'
    content.gsub!(/^TLS_REQCERT/i, '# TLS_REQCERT')
    content += "TLS_REQCERT allow\n"
  end

  # write the configuration if there were any changes
  File.write(ldap_conf, content) unless content == File.read(ldap_conf)
end
configured?() click to toggle source

determine if ldap has been configured at least once

# File lib/whimsy/asf/ldap.rb, line 273
def self.configured?
  return File.read("#{ETCLDAP}/ldap.conf").include? 'asf-ldap-client.pem'
end
connect(hosts = nil) click to toggle source

connect to LDAP

# File lib/whimsy/asf/ldap.rb, line 58
def self.connect(hosts = nil)
  # If the host list is specified, use that as is
  # otherwise ensure we start with the next in the default list
  hosts ||= self.hosts.rotate!

  # Try each host at most once
  hosts.each do |host|

    Wunderbar.info "[#{host}] - Connecting to LDAP server"

    begin
      # request connection
      uri = URI.parse(host)
      if uri.scheme == 'ldaps'
        ldap = ::LDAP::SSLConn.new(uri.host, uri.port)
      else
        ldap = ::LDAP::Conn.new(uri.host, uri.port)
      end

      # save the host
      @host = host

      return ldap
    rescue ::LDAP::ResultError => re
      Wunderbar.warn "[#{host}] - Error connecting to LDAP server: " +
        re.message + ' (continuing)'
    end

  end

  Wunderbar.error 'Failed to connect to any LDAP host'
  return nil
end
delete(dn) click to toggle source

delete an entry from LDAP; dump information on LDAP errors

# File lib/whimsy/asf/ldap.rb, line 296
def self.delete(dn)
  ASF.ldap.delete(dn)
rescue ::LDAP::ResultError
  Wunderbar.warn(dn.to_s)
  raise
end
extract_cert(host=nil) click to toggle source

query and extract cert from openssl output returns the last certificate found (WHIMSY-368)

# File lib/whimsy/asf/ldap.rb, line 219
def self.extract_cert(host=nil)
  host ||= hosts.sample[%r{//(.*?)(/|$)}, 1]
  host += ':636' unless host =~ %r{:\d+\z}
  cmd = ['openssl', 's_client', '-connect', host, '-showcerts']
  puts cmd.join(' ')
  out, _, _ = Open3.capture3(*cmd)
  out.scan(/^-+BEGIN.*?\n-+END[^\n]+\n/m).last
end
host() click to toggle source

Return the last chosen host (if any)

# File lib/whimsy/asf/ldap.rb, line 158
def self.host
  @host
end
hosts(use_config = true) click to toggle source

determine what LDAP hosts are available use_config=false is needed for the configure method only

# File lib/whimsy/asf/ldap.rb, line 192
def self.hosts(use_config = true)
  return @hosts if @hosts # cache the hosts list
  # try whimsy config (overrides ldap.conf)
  hosts = Array(ASF::Config.get(:ldap))

  # check system configuration
  if use_config and hosts.empty?
    conf = "#{ETCLDAP}/ldap.conf"
    if File.exist? conf
      uris = File.read(conf)[/^uri\s+(.*)/i, 1].to_s
      hosts = uris.scan(%r{ldaps?://\S+}) # May not have a port
      Wunderbar.debug 'Using hosts from LDAP config'
    end
  else
    Wunderbar.debug 'Using hosts from Whimsy config'
  end

  # There is no default
  raise 'Cannot determine LDAP URI from ldap.conf or local config!' if hosts.empty?

  hosts.shuffle!
  # Wunderbar.debug "Hosts:\n#{hosts.join(' ')}"
  @hosts = hosts
end
http_auth(string, &block) click to toggle source

validate HTTP authorization, and optionally invoke a block bound to that user.

# File lib/whimsy/asf/ldap.rb, line 140
def self.http_auth(string, &block)
  auth = Base64.decode64(string.to_s[/Basic (.*)/, 1] || '')
  user, password = auth.split(':', 2)
  return unless password

  if block
    self.bind(user, password, &block)
  else
    begin
      ASF::LDAP.bind(user, password) {}
      return ASF::Person.new(user)
    rescue ::LDAP::ResultError
      return nil
    end
  end
end
modify(dn, list) click to toggle source

modify an entry in LDAP; dump information on LDAP errors

# File lib/whimsy/asf/ldap.rb, line 278
def self.modify(dn, list)
  ASF.ldap.modify(dn, list)
rescue ::LDAP::ResultError
  Wunderbar.warn(list.inspect)
  Wunderbar.warn(dn.to_s)
  raise
end
rwhosts() click to toggle source

allow override of writable host by :ldaprw

# File lib/whimsy/asf/ldap.rb, line 163
def self.rwhosts
  return @rwhosts if @rwhosts # cache the rwhosts list
  rwhosts = Array(ASF::Config.get(:ldaprw)) # allow separate override for RW LDAP
  if rwhosts.empty?
    if File.exist? LDAP_MISC
      begin
        ldap_misc = YAML.safe_load(File.read(LDAP_MISC))
        rwhosts = Array(ldap_misc['ldapclient_asf']['write_uri'])
      rescue StandardError => e
        Wunderbar.warn "Could not parse write_uri: #{e.inspect}"
      end
    else
      Wunderbar.warn "Could not find #{LDAP_MISC}"
    end
    if rwhosts.empty? # default to RO hosts
      rwhosts = hosts
      Wunderbar.debug 'Using rwhosts from hosts'
    else
      Wunderbar.debug 'Using rwhosts from LDAP_MISC'
    end
  else
    Wunderbar.debug 'Using rwhosts from Whimsy config'
  end
  raise 'Cannot determine writable LDAP URI from ldap.conf or local config!' if rwhosts.empty?
  @rwhosts = rwhosts
end