whimsy-asf 0.0.76 → 0.0.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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