lib/whimsy/asf/ldap.rb in whimsy-asf-0.0.75 vs lib/whimsy/asf/ldap.rb in whimsy-asf-0.0.76
- old
+ new
@@ -31,10 +31,11 @@
require 'wunderbar'
require 'ldap'
require 'weakref'
require 'net/http'
require 'base64'
+require 'thread'
module ASF
module LDAP
# https://www.pingmybox.com/dashboard?location=304
# https://github.com/apache/infrastructure-puppet/blob/deployment/data/common.yaml (ldapserver::slapd_peers)
@@ -46,10 +47,13 @@
ldaps://snappy5.apache.org:636
ldaps://ldap2-lw-us.apache.org:636
ldaps://ldap2-lw-eu.apache.org:636
)
+ CONNECT_LOCK = Mutex.new
+ HOST_QUEUE = Queue.new
+
# fetch configuration from apache/infrastructure-puppet
def self.puppet_config
return @puppet if @puppet
file = '/apache/infrastructure-puppet/deployment/data/common.yaml'
http = Net::HTTP.new('raw.githubusercontent.com', 443)
@@ -69,12 +73,17 @@
rescue
nil
end
# connect to LDAP
- def self.connect
- hosts.shuffle.each do |host|
+ def self.connect(test = true)
+ # Try each host at most once
+ hosts.length.times do
+ # Ensure we use each host in turn
+ hosts.each {|host| HOST_QUEUE.push host} if HOST_QUEUE.empty?
+ host = HOST_QUEUE.shift
+
Wunderbar.info "Connecting to LDAP server: #{host}"
begin
# request connection
uri = URI.parse(host)
@@ -83,11 +92,11 @@
else
ldap = ::LDAP::Conn.new(uri.host, uri.port)
end
# test the connection
- ldap.bind
+ ldap.bind if test
# save the host
@host = host
return ldap
@@ -95,18 +104,22 @@
Wunderbar.warn "Error connecting to LDAP server #{host}: " +
re.message + " (continuing)"
end
end
+
Wunderbar.error "Failed to connect to any LDAP host"
return nil
end
end
- # backwards compatibility for tools that called this interface
- def self.init_ldap
- @ldap ||= ASF::LDAP.connect
+ # public entry point for establishing a connection safely
+ def self.init_ldap(reset = false)
+ ASF::LDAP::CONNECT_LOCK.synchronize do
+ @ldap = nil if reset
+ @ldap ||= ASF::LDAP.connect(!reset)
+ end
end
# determine where ldap.conf resides
if Dir.exist? '/etc/openldap'
ETCLDAP = '/etc/openldap'
@@ -116,27 +129,43 @@
def self.ldap
@ldap || self.init_ldap
end
- # search with a scope of one
+ # search with a scope of one, with automatic retry/failover
def self.search_one(base, filter, attrs=nil)
- init_ldap unless defined? @ldap
- return [] unless @ldap
- Wunderbar.info "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
+ cmd = "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
"#{[attrs].flatten.join(' ')}"
-
+
+ # try once per host, with a minimum of two tries
+ attempts_left = [ASF::LDAP.hosts.length, 2].max
begin
+ attempts_left -= 1
+ init_ldap unless @ldap
+ return [] unless @ldap
+
+ target = @ldap.get_option(::LDAP::LDAP_OPT_HOST_NAME) rescue '?'
+ Wunderbar.info "[#{target}] #{cmd}"
+
result = @ldap.search2(base, ::LDAP::LDAP_SCOPE_ONELEVEL, filter, attrs)
- rescue
- result = []
+ rescue Exception => re
+ if attempts_left <= 0
+ Wunderbar.error "[#{target}] => #{re.inspect} for #{cmd}"
+ raise
+ else
+ Wunderbar.warn "[#{target}] => #{re.inspect} for #{cmd}, retrying ..."
+ @ldap.unbind if @ldap.bound? rescue nil
+ @ldap = nil # force new connection
+ sleep 1
+ retry
+ end
end
result.map! {|hash| hash[attrs]} if String === attrs
- result
+ result.compact
end
# safely dereference a weakref array attribute. Block provided is
# used when reference is not set or has been reclaimed.
def self.dereference_weakref(object, attr, &block)
@@ -144,11 +173,13 @@
value = object.instance_variable_get(attr) || block.call
value[0..-1]
rescue WeakRef::RefError
value = block.call
ensure
- if value and not value.instance_of? WeakRef
+ if not value or RUBY_VERSION.start_with? '1'
+ object.instance_variable_set(attr, value)
+ elsif value and not value.instance_of? WeakRef
object.instance_variable_set(attr, WeakRef.new(value))
end
end
def self.weakref(attr, &block)
@@ -311,11 +342,11 @@
def alt_email
attrs['asf-altEmail'] || []
end
def pgp_key_fingerprints
- attrs['asf-pgpKeyFingerprint']
+ attrs['asf-pgpKeyFingerprint'] || []
end
def urls
attrs['asf-personalURL'] || []
end
@@ -381,21 +412,22 @@
return true
end
end
def self.preload
- Hash[ASF.search_one(base, "cn=*", %w(dn memberUid modifyTimestamp)).map do |results|
+ Hash[ASF.search_one(base, "cn=*", %w(dn memberUid modifyTimestamp createTimestamp)).map do |results|
cn = results['dn'].first[/^cn=(.*?),/, 1]
group = ASF::Group.find(cn)
group.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
- members = results['memberUid']
- group.members = members || []
+ group.createTimestamp = results['createTimestamp'].first # it is returned as an array of 1 entry
+ members = results['memberUid'] || []
+ group.members = members
[group, members]
end]
end
- attr_accessor :modifyTimestamp
+ attr_accessor :modifyTimestamp, :createTimestamp
def members=(members)
@members = WeakRef.new(members)
end
@@ -404,31 +436,50 @@
ASF.search_one(base, "cn=#{name}", 'memberUid').flatten
end
members.map {|uid| Person.find(uid)}
end
+
+ def dn
+ @dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
+ end
+
+ def remove(people)
+ people = Array(people).map(&:id)
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'memberUid', people)
+ ASF.ldap.modify(self.dn, [mod])
+ @members = nil
+ end
+
+ def add(people)
+ people = Array(people).map(&:dn)
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'memberUid', people)
+ ASF.ldap.modify(self.dn, [mod])
+ @members = nil
+ end
end
class Committee < Base
@base = 'ou=pmc,ou=committees,ou=groups,dc=apache,dc=org'
def self.list(filter='cn=*')
ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
end
def self.preload
- Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp)).map do |results|
+ Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp createTimestamp)).map do |results|
cn = results['dn'].first[/^cn=(.*?),/, 1]
committee = ASF::Committee.find(cn)
committee.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
- members = results['member']
+ committee.createTimestamp = results['createTimestamp'].first # it is returned as an array of 1 entry
+ members = results['member'] || []
committee.members = members
[committee, members]
end]
end
- attr_accessor :modifyTimestamp
+ attr_accessor :modifyTimestamp, :createTimestamp
def members=(members)
@members = WeakRef.new(members)
end
@@ -441,10 +492,24 @@
end
def dn
@dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
end
+
+ def remove(people)
+ people = Array(people).map(&:dn)
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'member', people)
+ ASF.ldap.modify(self.dn, [mod])
+ @members = nil
+ end
+
+ def add(people)
+ people = Array(people).map(&:dn)
+ mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'member', people)
+ ASF.ldap.modify(self.dn, [mod])
+ @members = nil
+ end
end
class Service < Base
@base = 'ou=groups,ou=services,dc=apache,dc=org'
@@ -454,39 +519,63 @@
def dn
"cn=#{id},#{self.class.base}"
end
+ def self.preload
+ Hash[ASF.search_one(base, "cn=*", %w(dn member modifyTimestamp createTimestamp)).map do |results|
+ cn = results['dn'].first[/^cn=(.*?),/, 1]
+ service = ASF::Service.find(cn)
+ service.modifyTimestamp = results['modifyTimestamp'].first # it is returned as an array of 1 entry
+ service.createTimestamp = results['createTimestamp'].first # it is returned as an array of 1 entry
+ members = results['member'] || []
+ service.members = members
+ [service, members]
+ end]
+ end
+
+ attr_accessor :modifyTimestamp, :createTimestamp
+
+ def members=(members)
+ @members = WeakRef.new(members)
+ end
+
def members
- ASF.search_one(base, "cn=#{name}", 'member').flatten.
- map {|uid| Person.find uid[/uid=(.*?),/,1]}
+ members = weakref(:members) do
+ ASF.search_one(base, "cn=#{name}", 'member').flatten
+ end
+
+ members.map {|uid| Person.find uid[/uid=(.*?),/,1]}
end
def remove(people)
people = Array(people).map(&:dn)
mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'member', people)
ASF.ldap.modify(self.dn, [mod])
+ @members = nil
end
def add(people)
people = Array(people).map(&:dn)
mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'member', people)
ASF.ldap.modify(self.dn, [mod])
+ @members = nil
end
end
module LDAP
def self.bind(user, password, &block)
dn = ASF::Person.new(user).dn
raise ::LDAP::ResultError.new('Unknown user') unless dn
- ASF.ldap.unbind rescue nil
+ ASF.ldap.unbind if ASF.ldap.bound? rescue nil
+ ldap = ASF.init_ldap(true)
if block
- ASF.ldap.bind(dn, password, &block)
- ASF.init_ldap
+ ldap.bind(dn, password, &block)
+ ASF.init_ldap(true)
else
- ASF.ldap.bind(dn, password)
+ ldap.bind(dn, password)
end
end
# validate HTTP authorization, and optionally invoke a block bound to
# that user.
@@ -507,35 +596,37 @@
end
end
# determine what LDAP hosts are available
def self.hosts
+ return @hosts if @hosts # cache the hosts list
# try whimsy config
hosts = Array(ASF::Config.get(:ldap))
# check system configuration
if hosts.empty?
conf = "#{ETCLDAP}/ldap.conf"
if File.exist? conf
uris = File.read(conf)[/^uri\s+(.*)/i, 1].to_s
hosts = uris.scan(/ldaps?:\/\/\S+?:\d+/)
+ Wunderbar.debug "Using hosts from LDAP config"
end
+ else
+ Wunderbar.debug "Using hosts from Whimsy config"
end
# if all else fails, use default list
+ Wunderbar.debug "Using default host list" if hosts.empty?
hosts = ASF::LDAP::HOSTS if hosts.empty?
- hosts
+ hosts.shuffle!
+ #Wunderbar.debug "Hosts:\n#{hosts.join(' ')}"
+ @hosts = hosts
end
- # select LDAP host
- def self.host
- @host ||= hosts.sample
- end
-
# query and extract cert from openssl output
def self.extract_cert
- host = LDAP.host[%r{//(.*?)(/|$)}, 1]
+ host = hosts.sample[%r{//(.*?)(/|$)}, 1]
puts ['openssl', 's_client', '-connect', host, '-showcerts'].join(' ')
out, err, rc = Open3.capture3 'openssl', 's_client',
'-connect', host, '-showcerts'
out[/^-+BEGIN.*?\n-+END[^\n]+\n/m]
end