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