lib/whimsy/asf/ldap.rb in whimsy-asf-0.0.76 vs lib/whimsy/asf/ldap.rb in whimsy-asf-0.0.77
- old
+ new
@@ -37,18 +37,19 @@
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)
+ # Updated 2016-04-11
HOSTS = %w(
- ldaps://ldap1-us-west.apache.org:636
- ldaps://ldap1-lw-us.apache.org:636
- ldaps://ldap2-us-west.apache.org:636
+ ldaps://devops.apache.org:636
ldaps://ldap1-lw-eu.apache.org:636
- ldaps://snappy5.apache.org:636
- ldaps://ldap2-lw-us.apache.org:636
+ ldaps://ldap1-lw-us.apache.org:636
ldaps://ldap2-lw-eu.apache.org:636
+ ldaps://ldap2-lw-us.apache.org:636
+ ldaps://snappy5.apache.org:636
+ ldaps://themis.apache.org:636
)
CONNECT_LOCK = Mutex.new
HOST_QUEUE = Queue.new
@@ -56,10 +57,12 @@
def self.puppet_config
return @puppet if @puppet
file = '/apache/infrastructure-puppet/deployment/data/common.yaml'
http = Net::HTTP.new('raw.githubusercontent.com', 443)
http.use_ssl = true
+ # the enclosing method is optional, so we only require the gem here
+ require 'yaml'
@puppet = YAML.load(http.request(Net::HTTP::Get.new(file)).body)
end
# extract the ldapcert from the puppet configuration
def self.puppet_cert
@@ -80,11 +83,11 @@
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}"
+ Wunderbar.info "[#{host}] - Connecting to LDAP server"
begin
# request connection
uri = URI.parse(host)
if uri.scheme == 'ldaps'
@@ -99,19 +102,137 @@
# save the host
@host = host
return ldap
rescue ::LDAP::ResultError => re
- Wunderbar.warn "Error connecting to LDAP server #{host}: " +
+ 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
+
+ def self.bind(user, password, &block)
+ dn = ASF::Person.new(user).dn
+ raise ::LDAP::ResultError.new('Unknown user') unless dn
+
+ ASF.ldap.unbind if ASF.ldap.bound? rescue nil
+ ldap = ASF.init_ldap(true)
+ if block
+ ldap.bind(dn, password, &block)
+ ASF.init_ldap(true)
+ else
+ ldap.bind(dn, password)
+ end
+ end
+
+ # validate HTTP authorization, and optionally invoke a block bound to
+ # that user.
+ 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
+
+ # Return the last chosen host (if any)
+ def self.host
+ @host
+ 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.shuffle!
+ #Wunderbar.debug "Hosts:\n#{hosts.join(' ')}"
+ @hosts = hosts
+ end
+
+ # query and extract cert from openssl output
+ def self.extract_cert
+ 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
+
+ # update /etc/ldap.conf. Usage:
+ #
+ # sudo ruby -r whimsy/asf -e "ASF::LDAP.configure"
+ #
+ def self.configure
+ cert = Dir["#{ETCLDAP}/asf*-ldap-client.pem"].first
+
+ # verify/obtain/write the cert
+ if not cert
+ cert = "#{ETCLDAP}/asf-ldap-client.pem"
+ File.write cert, ASF::LDAP.puppet_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.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 (Mac OS/X 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
end
# public entry point for establishing a connection safely
def self.init_ldap(reset = false)
ASF::LDAP::CONNECT_LOCK.synchronize do
@@ -124,10 +245,12 @@
if Dir.exist? '/etc/openldap'
ETCLDAP = '/etc/openldap'
else
ETCLDAP = '/etc/ldap'
end
+ # Note: FreeBSD seems to use
+ # /usr/local/etc/openldap/ldap.conf
def self.ldap
@ldap || self.init_ldap
end
@@ -199,10 +322,15 @@
end
class Base
attr_reader :name
+ # define default sort key (make Base objects sortable)
+ def <=>(other)
+ @name <=> other.name
+ end
+
def self.base
@base
end
def base
@@ -247,10 +375,23 @@
unless Object.respond_to? :id
def id
@name
end
end
+
+ def self.mod_add(attr, vals)
+ ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, attr.to_s, Array(vals))
+ end
+
+ def self.mod_replace(attr, vals)
+ vals = Array(vals) unless Hash === vals
+ ::LDAP::Mod.new(::LDAP::LDAP_MOD_REPLACE, attr.to_s, vals)
+ end
+
+ def self.mod_delete(attr, vals)
+ ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, attr.to_s, Array(vals))
+ end
end
class LazyHash < Hash
def initialize(&initializer)
@initializer = initializer
@@ -309,10 +450,15 @@
def attrs
@attrs ||= LazyHash.new {ASF.search_one(base, "uid=#{name}").first}
end
+ def reload!
+ @attrs = nil
+ attrs
+ end
+
def public_name
return icla.name if icla
cn = [attrs['cn']].flatten.first
cn.force_encoding('utf-8') if cn.respond_to? :force_encoding
return cn if cn
@@ -345,25 +491,38 @@
def pgp_key_fingerprints
attrs['asf-pgpKeyFingerprint'] || []
end
+ def ssh_public_keys
+ attrs['sshPublicKey'] || []
+ end
+
def urls
attrs['asf-personalURL'] || []
end
def committees
- Committee.list("member=uid=#{name},#{base}")
+ weakref(:committees) do
+ Committee.list("member=uid=#{name},#{base}")
+ end
end
def groups
- Group.list("memberUid=#{name}")
+ weakref(:groups) do
+ Group.list("memberUid=#{name}")
+ end
end
+ def services
+ weakref(:services) do
+ Service.list("member=#{dn}")
+ end
+ end
+
def dn
- value = attrs['dn']
- value.first if Array === value
+ "uid=#{name},#{ASF::Person.base}"
end
def method_missing(name, *args)
if name.to_s.end_with? '=' and args.length == 1
return modify(name.to_s[0..-2], args)
@@ -388,13 +547,11 @@
end
end
end
def modify(attr, value)
- value = Array(value) unless Hash === value
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_REPLACE, attr.to_s, value)
- ASF.ldap.modify(self.dn, [mod])
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_replace(attr.to_s, value)])
attrs[attr.to_s] = value
end
end
class Group < Base
@@ -441,23 +598,50 @@
def dn
@dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
end
+ # remove people from an existing group
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
+ people = (Array(people) & members).map(&:id)
+ return if people.empty?
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_delete('memberUid', people)])
+ ensure
+ @members = nil
end
+ # add people to an existing group
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
+ people = (Array(people) - members).map(&:id)
+ return if people.empty?
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_add('memberUid', people)])
+ ensure
+ @members = nil
end
+
+ # add a new group
+ def self.add(name, people)
+ nextgid = ASF::search_one(ASF::Group.base, 'cn=*', 'gidNumber').
+ flatten.map(&:to_i).max + 1
+
+ entry = [
+ mod_add('objectClass', ['posixGroup', 'top']),
+ mod_add('cn', name),
+ mod_add('userPassword', '{crypt}*'),
+ mod_add('gidNumber', nextgid.to_s),
+ mod_add('memberUid', people.map(&:id))
+ ]
+
+ ASF.ldap.add("cn=#{name},#{base}", entry)
+ end
+
+ # remove a group
+ def self.remove(name)
+ ASF.ldap.delete("cn=#{name},#{base}")
+ end
end
class Committee < Base
@base = 'ou=pmc,ou=committees,ou=groups,dc=apache,dc=org'
@@ -493,23 +677,43 @@
def dn
@dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
end
+ # remove people from a committee
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
+ people = (Array(people) & members).map(&:dn)
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_delete('member', people)])
+ ensure
+ @members = nil
end
+ # add people to a committee
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
+ people = (Array(people) - members).map(&:dn)
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_add('member', people)])
+ ensure
+ @members = nil
end
+
+ # add a new committee
+ def self.add(name, people)
+ entry = [
+ mod_add('objectClass', ['groupOfNames', 'top']),
+ mod_add('cn', name),
+ mod_add('member', Array(people).map(&:dn))
+ ]
+
+ ASF.ldap.add("cn=#{name},#{base}", entry)
+ end
+
+ # remove a committee
+ def self.remove(name)
+ ASF.ldap.delete("cn=#{name},#{base}")
+ end
end
class Service < Base
@base = 'ou=groups,ou=services,dc=apache,dc=org'
@@ -546,134 +750,49 @@
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
+ people = Array(people - members).map(&:dn)
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_delete('member', people)])
+ ensure
+ @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
+ people = (Array(people) & members).map(&:dn)
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_add('member', people)])
+ ensure
+ @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
+end
- ASF.ldap.unbind if ASF.ldap.bound? rescue nil
- ldap = ASF.init_ldap(true)
- if block
- ldap.bind(dn, password, &block)
- ASF.init_ldap(true)
- else
- ldap.bind(dn, password)
+if __FILE__ == $0
+ module ASF
+ module LDAP
+ def self.getHOSTS
+ HOSTS
end
end
-
- # validate HTTP authorization, and optionally invoke a block bound to
- # that user.
- 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
+ hosts=ASF::LDAP.getHOSTS().sort!
+ puppet=ASF::LDAP.puppet_ldapservers().sort!
+ if hosts == puppet
+ puts("LDAP HOSTS array is up to date with the puppet list")
+ else
+ puts("LDAP HOSTS array does not agree with the puppet list")
+ hostsonly=hosts-puppet
+ if hostsonly.length > 0
+ print("In HOSTS but not in puppet:")
+ puts(hostsonly)
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.shuffle!
- #Wunderbar.debug "Hosts:\n#{hosts.join(' ')}"
- @hosts = hosts
- end
-
- # query and extract cert from openssl output
- def self.extract_cert
- 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
-
- # update /etc/ldap.conf. Usage:
- #
- # sudo ruby -r whimsy/asf -e "ASF::LDAP.configure"
- #
- def self.configure
- cert = Dir["#{ETCLDAP}/asf*-ldap-client.pem"].first
-
- # verify/obtain/write the cert
- if not cert
- cert = "#{ETCLDAP}/asf-ldap-client.pem"
- File.write cert, ASF::LDAP.puppet_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.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 (Mac OS/X 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)
+ puppetonly=puppet-hosts
+ if puppetonly.length > 0
+ print("In puppet but not in HOSTS: ")
+ puts(puppetonly)
end
end
end