whimsy-asf 0.0.76 → 0.0.77

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 42ee442311d0fc054e4dd8fb6f2bdb4635d03ce1
4
- data.tar.gz: d556f03175acf95b7882d59b1abfe0b3332c81b5
3
+ metadata.gz: 9de8ca22abe552126b86e1490237de5bd1c7f601
4
+ data.tar.gz: 94c07c220d6efacbbb4667f76ab06bcf6d08e17c
5
5
  SHA512:
6
- metadata.gz: 5a00c4fa7f49262e06d04cdffb11929b1955e27afb55b3bc5f2224703a1ccd3d6d609a3656e6a6a3d54677bfcd8e7191641e944888aeebc8359028ab33d7c57c
7
- data.tar.gz: ed851c6a5edc6fef8325345c96d75d9abfaafa2b1e6753b2de2a8ff3d65ccdb30f35f5bbc74fc0912eb2cdd87227c27d08c4564610b753a06e84721638b2525c
6
+ metadata.gz: a52d9845077b7f361e70870c6f71cd16c5564ef4669f8ceff4302f6edba39af239e4276190d0c9f2668a4d405946846fe6f5883a947b43e631c538aa57f67139
7
+ data.tar.gz: a0e00b2a2dbc544e9e7a5763ccda6f4d8e631976e90c0371add54188bce0ad48c8f19fa29c9bf836f8e0c494e2e2edd60e62a3ac55a2b7f0cde275ba4f56ca18
@@ -1 +1 @@
1
- 0.0.76
1
+ 0.0.77
@@ -1,14 +1,17 @@
1
- require File.expand_path('../asf/config', __FILE__)
2
- require File.expand_path('../asf/committee', __FILE__)
3
- require File.expand_path('../asf/ldap', __FILE__)
4
- require File.expand_path('../asf/mail', __FILE__)
5
- require File.expand_path('../asf/svn', __FILE__)
6
- require File.expand_path('../asf/watch', __FILE__)
7
- require File.expand_path('../asf/nominees', __FILE__)
8
- require File.expand_path('../asf/icla', __FILE__)
9
- require File.expand_path('../asf/auth', __FILE__)
10
- require File.expand_path('../asf/member', __FILE__)
11
- require File.expand_path('../asf/site', __FILE__)
1
+ require_relative 'asf/config'
2
+ require_relative 'asf/committee'
3
+ require_relative 'asf/ldap'
4
+ require_relative 'asf/mail'
5
+ require_relative 'asf/svn'
6
+ require_relative 'asf/git'
7
+ require_relative 'asf/watch'
8
+ require_relative 'asf/nominees'
9
+ require_relative 'asf/icla'
10
+ require_relative 'asf/auth'
11
+ require_relative 'asf/member'
12
+ require_relative 'asf/site'
13
+ require_relative 'asf/podlings'
14
+ require_relative 'asf/person'
12
15
 
13
16
  module ASF
14
17
  def self.library_mtime
@@ -17,8 +20,9 @@ module ASF
17
20
  times = sources.map {|source| File.mtime(source)}
18
21
  times.max.gmtime
19
22
  end
23
+
20
24
  def self.library_gitinfo
21
25
  return @info if @info
22
- @info = `git show --format="%h %ci" -s HEAD`.chomp
26
+ @info = `git show --format="%h %ci" -s HEAD`.strip
23
27
  end
24
28
  end
@@ -124,6 +124,15 @@ class ASF::Board::Agenda
124
124
  hash[:attach] = section
125
125
  end
126
126
 
127
+ # look for missing titles
128
+ @sections.each do |section, hash|
129
+ hash['title'] ||= "UNKNOWN"
130
+
131
+ if hash['title'] == "UNKNOWN"
132
+ hash['warnings'] = ['unable to find attachment']
133
+ end
134
+ end
135
+
127
136
  @sections.values
128
137
  end
129
138
 
@@ -23,7 +23,7 @@ class ASF::Board::Agenda
23
23
  title.sub! /\sthe\s/, ' '
24
24
  title.sub! /\sApache\s/, ' '
25
25
  title.sub! /\sCommittee\s/, ' '
26
- title.sub! /\sProject(\s|$)/, '\1'
26
+ title.sub! /\sProject(\s|$)/i, '\1'
27
27
  title.sub! /\sPMC(\s|$)/, '\1'
28
28
  title.sub! /\s\(.*\)$/, ''
29
29
 
@@ -49,9 +49,9 @@ class ASF::Board::Agenda
49
49
 
50
50
  people = text.scan(/#{list_item}\((#{asfid})\)\s*$/)
51
51
  people += text.scan(/#{list_item}\((#{asfid})(?:@|\s*at\s*)
52
- (?:\.\.\.|apache\.org)\)\s*$/x)
52
+ (?:\.\.\.|apache\.org|apache\sdot\sorg)\)\s*$/xi)
53
53
  people += text.scan(/#{list_item}<(#{asfid})(?:@|\s*at\s*)
54
- (?:\.\.\.|apache\.org|apache\sdot\sorg)>\s*$/x)
54
+ (?:\.\.\.|apache\.org|apache\sdot\sorg)>\s*$/xi)
55
55
 
56
56
  whimsy = 'https://whimsy.apache.org'
57
57
  if title =~ /Change (.*?) Chair/ or title =~ /Terminate (\w+)$/
@@ -87,10 +87,25 @@ class ASF::Board::Agenda
87
87
  "#{whimsy}/board/minutes/#{name.gsub(/\W/,'_')}"
88
88
  if text =~ /FURTHER RESOLVED, that\s+([^,]*?),?\s+be\b/
89
89
  chairname = $1.gsub(/\s+/, ' ').strip
90
- chair = people.find {|person| person.first == chairname}
91
- attrs['chair'] = (chair ? chair.last : nil)
92
- unless chair and chair.last
93
- attrs['warnings'] ||= ['Chair not found in resolution']
90
+
91
+ if chairname =~ /\s\(([-.\w]+)\)$/
92
+ # if chair's id is present in parens, use that value
93
+ attrs['chair'] = $1 unless $1.empty?
94
+ chairname.sub! /\s+\(.*\)$/, ''
95
+ else
96
+ # match chair's name against people in the committee
97
+ chair = people.find {|person| person.first == chairname}
98
+ attrs['chair'] = (chair ? chair.last : nil)
99
+ end
100
+
101
+ unless people.include? [chairname, attrs['chair']]
102
+ if people.empty?
103
+ attrs['warnings'] ||= ['Unable to locate PMC email addresses']
104
+ elsif attrs['chair']
105
+ attrs['warnings'] ||= ['Chair not member of PMC']
106
+ else
107
+ attrs['warnings'] ||= ['Chair not found in resolution']
108
+ end
94
109
  end
95
110
  else
96
111
  attrs['warnings'] ||= ['Chair not found in resolution']
@@ -12,12 +12,27 @@ module ASF
12
12
  end
13
13
 
14
14
  def each
15
- auth = ASF::SVN['infra/infrastructure/trunk/subversion/authorization']
15
+ # TODO - should this read the Git repo directly?
16
+ auth = ASF::Git.find('infrastructure-puppet')
17
+ if auth
18
+ auth += '/modules/subversion_server/files/authorization'
19
+ else
20
+ # SVN copy is no longer in use - see INFRA-11452
21
+ raise Exception.new("Cannot find Git: infrastructure-puppet")
22
+ end
23
+
16
24
  File.read("#{auth}/#{@file}-authorization-template").
17
25
  scan(/^([-\w]+)=(\w.*)$/).each do |pmc, ids|
18
26
  yield pmc, ids.split(',')
19
27
  end
20
28
  end
29
+
30
+ unless Enumerable.instance_methods.include? :to_h
31
+ # backwards compatibility for Ruby versions <= 2.0
32
+ def to_h
33
+ Hash[self.to_a]
34
+ end
35
+ end
21
36
  end
22
37
 
23
38
  class Person
@@ -52,14 +52,19 @@ module ASF
52
52
  return @committee_info
53
53
  end
54
54
 
55
- list = Hash.new {|hash, name| hash[name] = find(name)}
56
55
 
57
56
  @committee_mtime = File.mtime(file)
58
57
  @@svn_change = Time.parse(
59
58
  `svn info #{file}`[/Last Changed Date: (.*) \(/, 1]).gmtime
60
59
 
60
+ parse_committee_info File.read(file)
61
+ end
62
+
63
+ def self.parse_committee_info(contents)
64
+ list = Hash.new {|hash, name| hash[name] = find(name)}
65
+
61
66
  # Split the file on lines starting "* ", i.e. the start of each group in section 3
62
- info = File.read(file).split(/^\* /)
67
+ info = contents.split(/^\* /)
63
68
  # Extract the text before first entry in section 3 and split on section headers,
64
69
  # keeping sections 1 (COMMITTEES) and 2 (REPORTING).
65
70
  head, report = info.shift.split(/^\d\./)[1..2]
@@ -82,21 +87,22 @@ module ASF
82
87
  # for each committee in section 3
83
88
  info.each do |roster|
84
89
  # extract the committee name and canonicalise
85
- committee = list[@@namemap.call(roster[/(\w.*?)\s+\(/,1])]
90
+ committee = list[@@namemap.call(roster[/(\w.*?)[ \t]+\(/,1])]
86
91
  # get the start date
87
92
  committee.established = roster[/\(est\. (.*?)\)/, 1]
88
93
  # extract the availids (is this used?)
89
94
  committee.info = roster.scan(/<(.*?)@apache\.org>/).flatten
90
95
  # drop (chair) markers and extract 0: name, 1: availid, 2: [date], 3: date
96
+ # the date is optional (e.g. infrastructure)
91
97
  committee.roster = Hash[roster.gsub(/\(\w+\)/, '').
92
- scan(/^\s*(.*?)\s*<(.*?)@apache\.org>\s+(\[(.*?)\])?/).
98
+ scan(/^[ \t]*(.*?)[ \t]*<(.*?)@apache\.org>(?:[ \t]+(\[(.*?)\]))?/).
93
99
  map {|list| [list[1], {name: list[0], date: list[3]}]}]
94
100
  end
95
101
 
96
102
  # process report section
97
103
  report.scan(/^([^\n]+)\n---+\n(.*?)\n\n/m).each do |period, committees|
98
- committees.scan(/^ \s*(.*)/).each do |committee|
99
- committee, comment = committee.first.split(/\s+#\s+/,2)
104
+ committees.scan(/^ [ \t]*(.*)/).each do |committee|
105
+ committee, comment = committee.first.split(/[ \t]+#[ \t]+/,2)
100
106
  committee = list[committee]
101
107
  if comment
102
108
  committee.report = "#{period}: #{comment}"
@@ -0,0 +1,48 @@
1
+ require 'thread'
2
+ require 'open3'
3
+
4
+ module ASF
5
+
6
+ class Git
7
+ @semaphore = Mutex.new
8
+
9
+ def self.repos
10
+ @semaphore.synchronize do
11
+ git = Array(ASF::Config.get(:git)).map {|dir| dir.untaint}
12
+ @repos ||= Hash[Dir[*git].map { |name|
13
+ next unless Dir.exist? name.untaint
14
+ Dir.chdir name.untaint do
15
+ out, err, status =
16
+ Open3.capture3(*%(git config --get remote.origin.url))
17
+ if status.success?
18
+ [File.basename(out.chomp, '.git'), Dir.pwd.untaint]
19
+ end
20
+ end
21
+ }.compact]
22
+ end
23
+ end
24
+
25
+ def self.[]=(name, path)
26
+ @testdata[name] = File.expand_path(path).untaint
27
+ end
28
+
29
+ def self.[](name)
30
+ self.find!(name)
31
+ end
32
+
33
+ def self.find(name)
34
+ repos[name]
35
+ end
36
+
37
+ def self.find!(name)
38
+ result = self.find(name)
39
+
40
+ if not result
41
+ raise Exception.new("Unable to find git clone for #{name}")
42
+ end
43
+
44
+ result
45
+ end
46
+ end
47
+
48
+ end
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module ASF
2
4
 
3
5
  class ICLA
@@ -8,7 +10,7 @@ module ASF
8
10
 
9
11
  @@mtime = nil
10
12
 
11
- OFFICERS = ASF::SVN['private/foundation/officers']
13
+ OFFICERS = ASF::SVN.find('private/foundation/officers')
12
14
  SOURCE = OFFICERS ? "#{OFFICERS}/iclas.txt" : nil
13
15
 
14
16
  # flush caches if source file changed
@@ -120,60 +122,6 @@ module ASF
120
122
  end
121
123
  end
122
124
 
123
- # sort support
124
-
125
- def self.asciize(name)
126
- if name.match /[^\x00-\x7F]/
127
- # digraphs. May be culturally sensitive
128
- name.gsub! /\u00df/, 'ss'
129
- name.gsub! /\u00e4|a\u0308/, 'ae'
130
- name.gsub! /\u00e5|a\u030a/, 'aa'
131
- name.gsub! /\u00e6/, 'ae'
132
- name.gsub! /\u00f1|n\u0303/, 'ny'
133
- name.gsub! /\u00f6|o\u0308/, 'oe'
134
- name.gsub! /\u00fc|u\u0308/, 'ue'
135
-
136
- # latin 1
137
- name.gsub! /[\u00e0-\u00e5]/, 'a'
138
- name.gsub! /\u00e7/, 'c'
139
- name.gsub! /[\u00e8-\u00eb]/, 'e'
140
- name.gsub! /[\u00ec-\u00ef]/, 'i'
141
- name.gsub! /[\u00f2-\u00f6]|\u00f8/, 'o'
142
- name.gsub! /[\u00f9-\u00fc]/, 'u'
143
- name.gsub! /[\u00fd\u00ff]/, 'y'
144
-
145
- # Latin Extended-A
146
- name.gsub! /[\u0100-\u0105]/, 'a'
147
- name.gsub! /[\u0106-\u010d]/, 'c'
148
- name.gsub! /[\u010e-\u0111]/, 'd'
149
- name.gsub! /[\u0112-\u011b]/, 'e'
150
- name.gsub! /[\u011c-\u0123]/, 'g'
151
- name.gsub! /[\u0124-\u0127]/, 'h'
152
- name.gsub! /[\u0128-\u0131]/, 'i'
153
- name.gsub! /[\u0132-\u0133]/, 'ij'
154
- name.gsub! /[\u0134-\u0135]/, 'j'
155
- name.gsub! /[\u0136-\u0138]/, 'k'
156
- name.gsub! /[\u0139-\u0142]/, 'l'
157
- name.gsub! /[\u0143-\u014b]/, 'n'
158
- name.gsub! /[\u014C-\u0151]/, 'o'
159
- name.gsub! /[\u0152-\u0153]/, 'oe'
160
- name.gsub! /[\u0154-\u0159]/, 'r'
161
- name.gsub! /[\u015a-\u0162]/, 's'
162
- name.gsub! /[\u0162-\u0167]/, 't'
163
- name.gsub! /[\u0168-\u0173]/, 'u'
164
- name.gsub! /[\u0174-\u0175]/, 'w'
165
- name.gsub! /[\u0176-\u0178]/, 'y'
166
- name.gsub! /[\u0179-\u017e]/, 'z'
167
-
168
- # denormalized diacritics
169
- name.gsub! /[\u0300-\u036f]/, ''
170
- end
171
-
172
- name.strip.gsub /[^\w]+/, '-'
173
- end
174
-
175
- SUFFIXES = /^([Jj][Rr]\.?|I{2,3}|I?V|VI{1,3}|[A-Z]\.)$/
176
-
177
125
  # rearrange line in an order suitable for sorting
178
126
  def self.lname(line)
179
127
  return '' if line.start_with? '#'
@@ -185,18 +133,7 @@ module ASF
185
133
  name.sub! /\/\*.+\*\/$/,''
186
134
  return '' if name.strip.empty?
187
135
 
188
- name = name.split.reverse
189
- suffix = (name.shift if name.first =~ SUFFIXES)
190
- suffix += ' ' + name.shift if name.first =~ SUFFIXES
191
- name << name.shift
192
- name << name.shift if name.first=='Lewis' and name.last=='Ship'
193
- name << name.shift if name.first=='Gallardo' and name.last=='Rivera'
194
- name << name.shift if name.first=="S\u00e1nchez" and name.last=='Vega'
195
- # name << name.shift if name.first=='van'
196
- name.last.sub! /^IJ/, 'Ij'
197
- name.unshift(suffix) if suffix
198
- name.map! {|word| asciize(word)}
199
- name = name.reverse.join(' ')
136
+ name = ASF::Person.sortable_name(name)
200
137
 
201
138
  "#{name}:#{rest}"
202
139
  end
@@ -228,18 +165,10 @@ module ASF
228
165
  # Search archive for historical records of people who were committers
229
166
  # but never submitted an ICLA (some of which are still ASF members or
230
167
  # members of a PMC).
231
- def self.search_archive_by_id(value)
232
- require 'net/http'
233
- require 'nokogiri'
234
- historical_committers = 'http://people.apache.org/~rubys/committers.html'
235
- doc = Nokogiri::HTML(Net::HTTP.get(URI.parse(historical_committers)))
236
- doc.search('tr').each do |tr|
237
- tds = tr.search('td')
238
- next unless tds.length == 3
239
- return tds[1].text if tds[0].text == value
240
- end
241
- nil
242
- rescue
243
- nil
168
+ def self.search_archive_by_id(id)
169
+ archive = ASF::SVN['private/foundation/officers/historic']
170
+ name = JSON.parse(File.read("#{archive}/committers.json"))[id]
171
+ name = id if name and name.empty?
172
+ name
244
173
  end
245
174
  end
@@ -39,14 +39,15 @@ module ASF
39
39
  module LDAP
40
40
  # https://www.pingmybox.com/dashboard?location=304
41
41
  # https://github.com/apache/infrastructure-puppet/blob/deployment/data/common.yaml (ldapserver::slapd_peers)
42
+ # Updated 2016-04-11
42
43
  HOSTS = %w(
43
- ldaps://ldap1-us-west.apache.org:636
44
- ldaps://ldap1-lw-us.apache.org:636
45
- ldaps://ldap2-us-west.apache.org:636
44
+ ldaps://devops.apache.org:636
46
45
  ldaps://ldap1-lw-eu.apache.org:636
47
- ldaps://snappy5.apache.org:636
48
- ldaps://ldap2-lw-us.apache.org:636
46
+ ldaps://ldap1-lw-us.apache.org:636
49
47
  ldaps://ldap2-lw-eu.apache.org:636
48
+ ldaps://ldap2-lw-us.apache.org:636
49
+ ldaps://snappy5.apache.org:636
50
+ ldaps://themis.apache.org:636
50
51
  )
51
52
 
52
53
  CONNECT_LOCK = Mutex.new
@@ -58,6 +59,8 @@ module ASF
58
59
  file = '/apache/infrastructure-puppet/deployment/data/common.yaml'
59
60
  http = Net::HTTP.new('raw.githubusercontent.com', 443)
60
61
  http.use_ssl = true
62
+ # the enclosing method is optional, so we only require the gem here
63
+ require 'yaml'
61
64
  @puppet = YAML.load(http.request(Net::HTTP::Get.new(file)).body)
62
65
  end
63
66
 
@@ -82,7 +85,7 @@ module ASF
82
85
  hosts.each {|host| HOST_QUEUE.push host} if HOST_QUEUE.empty?
83
86
  host = HOST_QUEUE.shift
84
87
 
85
- Wunderbar.info "Connecting to LDAP server: #{host}"
88
+ Wunderbar.info "[#{host}] - Connecting to LDAP server"
86
89
 
87
90
  begin
88
91
  # request connection
@@ -101,7 +104,7 @@ module ASF
101
104
 
102
105
  return ldap
103
106
  rescue ::LDAP::ResultError => re
104
- Wunderbar.warn "Error connecting to LDAP server #{host}: " +
107
+ Wunderbar.warn "[#{host}] - Error connecting to LDAP server: " +
105
108
  re.message + " (continuing)"
106
109
  end
107
110
 
@@ -110,6 +113,124 @@ module ASF
110
113
  Wunderbar.error "Failed to connect to any LDAP host"
111
114
  return nil
112
115
  end
116
+
117
+ def self.bind(user, password, &block)
118
+ dn = ASF::Person.new(user).dn
119
+ raise ::LDAP::ResultError.new('Unknown user') unless dn
120
+
121
+ ASF.ldap.unbind if ASF.ldap.bound? rescue nil
122
+ ldap = ASF.init_ldap(true)
123
+ if block
124
+ ldap.bind(dn, password, &block)
125
+ ASF.init_ldap(true)
126
+ else
127
+ ldap.bind(dn, password)
128
+ end
129
+ end
130
+
131
+ # validate HTTP authorization, and optionally invoke a block bound to
132
+ # that user.
133
+ def self.http_auth(string, &block)
134
+ auth = Base64.decode64(string.to_s[/Basic (.*)/, 1] || '')
135
+ user, password = auth.split(':', 2)
136
+ return unless password
137
+
138
+ if block
139
+ self.bind(user, password, &block)
140
+ else
141
+ begin
142
+ ASF::LDAP.bind(user, password) {}
143
+ return ASF::Person.new(user)
144
+ rescue ::LDAP::ResultError
145
+ return nil
146
+ end
147
+ end
148
+ end
149
+
150
+ # Return the last chosen host (if any)
151
+ def self.host
152
+ @host
153
+ end
154
+
155
+ # determine what LDAP hosts are available
156
+ def self.hosts
157
+ return @hosts if @hosts # cache the hosts list
158
+ # try whimsy config
159
+ hosts = Array(ASF::Config.get(:ldap))
160
+
161
+ # check system configuration
162
+ if hosts.empty?
163
+ conf = "#{ETCLDAP}/ldap.conf"
164
+ if File.exist? conf
165
+ uris = File.read(conf)[/^uri\s+(.*)/i, 1].to_s
166
+ hosts = uris.scan(/ldaps?:\/\/\S+?:\d+/)
167
+ Wunderbar.debug "Using hosts from LDAP config"
168
+ end
169
+ else
170
+ Wunderbar.debug "Using hosts from Whimsy config"
171
+ end
172
+
173
+ # if all else fails, use default list
174
+ Wunderbar.debug "Using default host list" if hosts.empty?
175
+ hosts = ASF::LDAP::HOSTS if hosts.empty?
176
+
177
+ hosts.shuffle!
178
+ #Wunderbar.debug "Hosts:\n#{hosts.join(' ')}"
179
+ @hosts = hosts
180
+ end
181
+
182
+ # query and extract cert from openssl output
183
+ def self.extract_cert
184
+ host = hosts.sample[%r{//(.*?)(/|$)}, 1]
185
+ puts ['openssl', 's_client', '-connect', host, '-showcerts'].join(' ')
186
+ out, err, rc = Open3.capture3 'openssl', 's_client',
187
+ '-connect', host, '-showcerts'
188
+ out[/^-+BEGIN.*?\n-+END[^\n]+\n/m]
189
+ end
190
+
191
+ # update /etc/ldap.conf. Usage:
192
+ #
193
+ # sudo ruby -r whimsy/asf -e "ASF::LDAP.configure"
194
+ #
195
+ def self.configure
196
+ cert = Dir["#{ETCLDAP}/asf*-ldap-client.pem"].first
197
+
198
+ # verify/obtain/write the cert
199
+ if not cert
200
+ cert = "#{ETCLDAP}/asf-ldap-client.pem"
201
+ File.write cert, ASF::LDAP.puppet_cert || self.extract_cert
202
+ end
203
+
204
+ # read the current configuration file
205
+ ldap_conf = "#{ETCLDAP}/ldap.conf"
206
+ content = File.read(ldap_conf)
207
+
208
+ # ensure that the right cert is used
209
+ unless content =~ /asf.*-ldap-client\.pem/
210
+ content.gsub!(/^TLS_CACERT/i, '# TLS_CACERT')
211
+ content += "TLS_CACERT #{ETCLDAP}/asf-ldap-client.pem\n"
212
+ end
213
+
214
+ # provide the URIs of the ldap hosts
215
+ content.gsub!(/^URI/, '# URI')
216
+ content += "uri \n" unless content =~ /^uri /
217
+ content[/uri (.*)\n/, 1] = hosts.join(' ')
218
+
219
+ # verify/set the base
220
+ unless content.include? 'base dc=apache'
221
+ content.gsub!(/^BASE/i, '# BASE')
222
+ content += "base dc=apache,dc=org\n"
223
+ end
224
+
225
+ # ensure TLS_REQCERT is allow (Mac OS/X only)
226
+ if ETCLDAP.include? 'openldap' and not content.include? 'REQCERT allow'
227
+ content.gsub!(/^TLS_REQCERT/i, '# TLS_REQCERT')
228
+ content += "TLS_REQCERT allow\n"
229
+ end
230
+
231
+ # write the configuration if there were any changes
232
+ File.write(ldap_conf, content) unless content == File.read(ldap_conf)
233
+ end
113
234
  end
114
235
 
115
236
  # public entry point for establishing a connection safely
@@ -126,6 +247,8 @@ module ASF
126
247
  else
127
248
  ETCLDAP = '/etc/ldap'
128
249
  end
250
+ # Note: FreeBSD seems to use
251
+ # /usr/local/etc/openldap/ldap.conf
129
252
 
130
253
  def self.ldap
131
254
  @ldap || self.init_ldap
@@ -201,6 +324,11 @@ module ASF
201
324
  class Base
202
325
  attr_reader :name
203
326
 
327
+ # define default sort key (make Base objects sortable)
328
+ def <=>(other)
329
+ @name <=> other.name
330
+ end
331
+
204
332
  def self.base
205
333
  @base
206
334
  end
@@ -249,6 +377,19 @@ module ASF
249
377
  @name
250
378
  end
251
379
  end
380
+
381
+ def self.mod_add(attr, vals)
382
+ ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, attr.to_s, Array(vals))
383
+ end
384
+
385
+ def self.mod_replace(attr, vals)
386
+ vals = Array(vals) unless Hash === vals
387
+ ::LDAP::Mod.new(::LDAP::LDAP_MOD_REPLACE, attr.to_s, vals)
388
+ end
389
+
390
+ def self.mod_delete(attr, vals)
391
+ ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, attr.to_s, Array(vals))
392
+ end
252
393
  end
253
394
 
254
395
  class LazyHash < Hash
@@ -311,6 +452,11 @@ module ASF
311
452
  @attrs ||= LazyHash.new {ASF.search_one(base, "uid=#{name}").first}
312
453
  end
313
454
 
455
+ def reload!
456
+ @attrs = nil
457
+ attrs
458
+ end
459
+
314
460
  def public_name
315
461
  return icla.name if icla
316
462
  cn = [attrs['cn']].flatten.first
@@ -347,21 +493,34 @@ module ASF
347
493
  attrs['asf-pgpKeyFingerprint'] || []
348
494
  end
349
495
 
496
+ def ssh_public_keys
497
+ attrs['sshPublicKey'] || []
498
+ end
499
+
350
500
  def urls
351
501
  attrs['asf-personalURL'] || []
352
502
  end
353
503
 
354
504
  def committees
355
- Committee.list("member=uid=#{name},#{base}")
505
+ weakref(:committees) do
506
+ Committee.list("member=uid=#{name},#{base}")
507
+ end
356
508
  end
357
509
 
358
510
  def groups
359
- Group.list("memberUid=#{name}")
511
+ weakref(:groups) do
512
+ Group.list("memberUid=#{name}")
513
+ end
514
+ end
515
+
516
+ def services
517
+ weakref(:services) do
518
+ Service.list("member=#{dn}")
519
+ end
360
520
  end
361
521
 
362
522
  def dn
363
- value = attrs['dn']
364
- value.first if Array === value
523
+ "uid=#{name},#{ASF::Person.base}"
365
524
  end
366
525
 
367
526
  def method_missing(name, *args)
@@ -390,9 +549,7 @@ module ASF
390
549
  end
391
550
 
392
551
  def modify(attr, value)
393
- value = Array(value) unless Hash === value
394
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_REPLACE, attr.to_s, value)
395
- ASF.ldap.modify(self.dn, [mod])
552
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_replace(attr.to_s, value)])
396
553
  attrs[attr.to_s] = value
397
554
  end
398
555
  end
@@ -443,19 +600,46 @@ module ASF
443
600
  @dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
444
601
  end
445
602
 
603
+ # remove people from an existing group
446
604
  def remove(people)
447
- people = Array(people).map(&:id)
448
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'memberUid', people)
449
- ASF.ldap.modify(self.dn, [mod])
605
+ @members = nil
606
+ people = (Array(people) & members).map(&:id)
607
+ return if people.empty?
608
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_delete('memberUid', people)])
609
+ ensure
450
610
  @members = nil
451
611
  end
452
612
 
613
+ # add people to an existing group
453
614
  def add(people)
454
- people = Array(people).map(&:dn)
455
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'memberUid', people)
456
- ASF.ldap.modify(self.dn, [mod])
615
+ @members = nil
616
+ people = (Array(people) - members).map(&:id)
617
+ return if people.empty?
618
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_add('memberUid', people)])
619
+ ensure
457
620
  @members = nil
458
621
  end
622
+
623
+ # add a new group
624
+ def self.add(name, people)
625
+ nextgid = ASF::search_one(ASF::Group.base, 'cn=*', 'gidNumber').
626
+ flatten.map(&:to_i).max + 1
627
+
628
+ entry = [
629
+ mod_add('objectClass', ['posixGroup', 'top']),
630
+ mod_add('cn', name),
631
+ mod_add('userPassword', '{crypt}*'),
632
+ mod_add('gidNumber', nextgid.to_s),
633
+ mod_add('memberUid', people.map(&:id))
634
+ ]
635
+
636
+ ASF.ldap.add("cn=#{name},#{base}", entry)
637
+ end
638
+
639
+ # remove a group
640
+ def self.remove(name)
641
+ ASF.ldap.delete("cn=#{name},#{base}")
642
+ end
459
643
  end
460
644
 
461
645
  class Committee < Base
@@ -495,18 +679,38 @@ module ASF
495
679
  @dn ||= ASF.search_one(base, "cn=#{name}", 'dn').first.first
496
680
  end
497
681
 
682
+ # remove people from a committee
498
683
  def remove(people)
499
- people = Array(people).map(&:dn)
500
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'member', people)
501
- ASF.ldap.modify(self.dn, [mod])
684
+ @members = nil
685
+ people = (Array(people) & members).map(&:dn)
686
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_delete('member', people)])
687
+ ensure
502
688
  @members = nil
503
689
  end
504
690
 
691
+ # add people to a committee
505
692
  def add(people)
506
- people = Array(people).map(&:dn)
507
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'member', people)
508
- ASF.ldap.modify(self.dn, [mod])
509
693
  @members = nil
694
+ people = (Array(people) - members).map(&:dn)
695
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_add('member', people)])
696
+ ensure
697
+ @members = nil
698
+ end
699
+
700
+ # add a new committee
701
+ def self.add(name, people)
702
+ entry = [
703
+ mod_add('objectClass', ['groupOfNames', 'top']),
704
+ mod_add('cn', name),
705
+ mod_add('member', Array(people).map(&:dn))
706
+ ]
707
+
708
+ ASF.ldap.add("cn=#{name},#{base}", entry)
709
+ end
710
+
711
+ # remove a committee
712
+ def self.remove(name)
713
+ ASF.ldap.delete("cn=#{name},#{base}")
510
714
  end
511
715
  end
512
716
 
@@ -548,132 +752,47 @@ module ASF
548
752
  end
549
753
 
550
754
  def remove(people)
551
- people = Array(people).map(&:dn)
552
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_DELETE, 'member', people)
553
- ASF.ldap.modify(self.dn, [mod])
755
+ @members = nil
756
+ people = Array(people - members).map(&:dn)
757
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_delete('member', people)])
758
+ ensure
554
759
  @members = nil
555
760
  end
556
761
 
557
762
  def add(people)
558
- people = Array(people).map(&:dn)
559
- mod = ::LDAP::Mod.new(::LDAP::LDAP_MOD_ADD, 'member', people)
560
- ASF.ldap.modify(self.dn, [mod])
763
+ @members = nil
764
+ people = (Array(people) & members).map(&:dn)
765
+ ASF.ldap.modify(self.dn, [ASF::Base.mod_add('member', people)])
766
+ ensure
561
767
  @members = nil
562
768
  end
563
769
  end
564
770
 
565
- module LDAP
566
- def self.bind(user, password, &block)
567
- dn = ASF::Person.new(user).dn
568
- raise ::LDAP::ResultError.new('Unknown user') unless dn
569
-
570
- ASF.ldap.unbind if ASF.ldap.bound? rescue nil
571
- ldap = ASF.init_ldap(true)
572
- if block
573
- ldap.bind(dn, password, &block)
574
- ASF.init_ldap(true)
575
- else
576
- ldap.bind(dn, password)
577
- end
578
- end
579
-
580
- # validate HTTP authorization, and optionally invoke a block bound to
581
- # that user.
582
- def self.http_auth(string, &block)
583
- auth = Base64.decode64(string.to_s[/Basic (.*)/, 1] || '')
584
- user, password = auth.split(':', 2)
585
- return unless password
771
+ end
586
772
 
587
- if block
588
- self.bind(user, password, &block)
589
- else
590
- begin
591
- ASF::LDAP.bind(user, password) {}
592
- return ASF::Person.new(user)
593
- rescue ::LDAP::ResultError
594
- return nil
595
- end
773
+ if __FILE__ == $0
774
+ module ASF
775
+ module LDAP
776
+ def self.getHOSTS
777
+ HOSTS
596
778
  end
597
779
  end
598
-
599
- # determine what LDAP hosts are available
600
- def self.hosts
601
- return @hosts if @hosts # cache the hosts list
602
- # try whimsy config
603
- hosts = Array(ASF::Config.get(:ldap))
604
-
605
- # check system configuration
606
- if hosts.empty?
607
- conf = "#{ETCLDAP}/ldap.conf"
608
- if File.exist? conf
609
- uris = File.read(conf)[/^uri\s+(.*)/i, 1].to_s
610
- hosts = uris.scan(/ldaps?:\/\/\S+?:\d+/)
611
- Wunderbar.debug "Using hosts from LDAP config"
612
- end
613
- else
614
- Wunderbar.debug "Using hosts from Whimsy config"
615
- end
616
-
617
- # if all else fails, use default list
618
- Wunderbar.debug "Using default host list" if hosts.empty?
619
- hosts = ASF::LDAP::HOSTS if hosts.empty?
620
-
621
- hosts.shuffle!
622
- #Wunderbar.debug "Hosts:\n#{hosts.join(' ')}"
623
- @hosts = hosts
624
- end
625
-
626
- # query and extract cert from openssl output
627
- def self.extract_cert
628
- host = hosts.sample[%r{//(.*?)(/|$)}, 1]
629
- puts ['openssl', 's_client', '-connect', host, '-showcerts'].join(' ')
630
- out, err, rc = Open3.capture3 'openssl', 's_client',
631
- '-connect', host, '-showcerts'
632
- out[/^-+BEGIN.*?\n-+END[^\n]+\n/m]
633
- end
634
-
635
- # update /etc/ldap.conf. Usage:
636
- #
637
- # sudo ruby -r whimsy/asf -e "ASF::LDAP.configure"
638
- #
639
- def self.configure
640
- cert = Dir["#{ETCLDAP}/asf*-ldap-client.pem"].first
641
-
642
- # verify/obtain/write the cert
643
- if not cert
644
- cert = "#{ETCLDAP}/asf-ldap-client.pem"
645
- File.write cert, ASF::LDAP.puppet_cert || self.extract_cert
646
- end
647
-
648
- # read the current configuration file
649
- ldap_conf = "#{ETCLDAP}/ldap.conf"
650
- content = File.read(ldap_conf)
651
-
652
- # ensure that the right cert is used
653
- unless content =~ /asf.*-ldap-client\.pem/
654
- content.gsub!(/^TLS_CACERT/i, '# TLS_CACERT')
655
- content += "TLS_CACERT #{ETCLDAP}/asf-ldap-client.pem\n"
656
- end
657
-
658
- # provide the URIs of the ldap hosts
659
- content.gsub!(/^URI/, '# URI')
660
- content += "uri \n" unless content =~ /^uri /
661
- content[/uri (.*)\n/, 1] = hosts.join(' ')
662
-
663
- # verify/set the base
664
- unless content.include? 'base dc=apache'
665
- content.gsub!(/^BASE/i, '# BASE')
666
- content += "base dc=apache,dc=org\n"
667
- end
668
-
669
- # ensure TLS_REQCERT is allow (Mac OS/X only)
670
- if ETCLDAP.include? 'openldap' and not content.include? 'REQCERT allow'
671
- content.gsub!(/^TLS_REQCERT/i, '# TLS_REQCERT')
672
- content += "TLS_REQCERT allow\n"
673
- end
674
-
675
- # write the configuration if there were any changes
676
- File.write(ldap_conf, content) unless content == File.read(ldap_conf)
780
+ end
781
+ hosts=ASF::LDAP.getHOSTS().sort!
782
+ puppet=ASF::LDAP.puppet_ldapservers().sort!
783
+ if hosts == puppet
784
+ puts("LDAP HOSTS array is up to date with the puppet list")
785
+ else
786
+ puts("LDAP HOSTS array does not agree with the puppet list")
787
+ hostsonly=hosts-puppet
788
+ if hostsonly.length > 0
789
+ print("In HOSTS but not in puppet:")
790
+ puts(hostsonly)
791
+ end
792
+ puppetonly=puppet-hosts
793
+ if puppetonly.length > 0
794
+ print("In puppet but not in HOSTS: ")
795
+ puts(puppetonly)
677
796
  end
678
797
  end
679
798
  end