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 +4 -4
- data/asf.version +1 -1
- data/lib/whimsy/asf.rb +16 -12
- data/lib/whimsy/asf/agenda.rb +9 -0
- data/lib/whimsy/asf/agenda/special.rb +22 -7
- data/lib/whimsy/asf/auth.rb +16 -1
- data/lib/whimsy/asf/committee.rb +12 -6
- data/lib/whimsy/asf/git.rb +48 -0
- data/lib/whimsy/asf/icla.rb +9 -80
- data/lib/whimsy/asf/ldap.rb +260 -141
- data/lib/whimsy/asf/mail.rb +25 -1
- data/lib/whimsy/asf/member.rb +74 -8
- data/lib/whimsy/asf/nominees.rb +1 -1
- data/lib/whimsy/asf/person.rb +81 -0
- data/lib/whimsy/asf/podlings.rb +111 -28
- data/lib/whimsy/asf/rack.rb +4 -4
- data/lib/whimsy/asf/site.rb +1 -1
- data/lib/whimsy/asf/svn.rb +131 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9de8ca22abe552126b86e1490237de5bd1c7f601
|
4
|
+
data.tar.gz: 94c07c220d6efacbbb4667f76ab06bcf6d08e17c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a52d9845077b7f361e70870c6f71cd16c5564ef4669f8ceff4302f6edba39af239e4276190d0c9f2668a4d405946846fe6f5883a947b43e631c538aa57f67139
|
7
|
+
data.tar.gz: a0e00b2a2dbc544e9e7a5763ccda6f4d8e631976e90c0371add54188bce0ad48c8f19fa29c9bf836f8e0c494e2e2edd60e62a3ac55a2b7f0cde275ba4f56ca18
|
data/asf.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.77
|
data/lib/whimsy/asf.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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`.
|
26
|
+
@info = `git show --format="%h %ci" -s HEAD`.strip
|
23
27
|
end
|
24
28
|
end
|
data/lib/whimsy/asf/agenda.rb
CHANGED
@@ -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|$)
|
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*$/
|
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*$/
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
attrs['
|
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']
|
data/lib/whimsy/asf/auth.rb
CHANGED
@@ -12,12 +12,27 @@ module ASF
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def each
|
15
|
-
|
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
|
data/lib/whimsy/asf/committee.rb
CHANGED
@@ -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 =
|
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.*?)\
|
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(
|
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(/^ \
|
99
|
-
committee, comment = committee.first.split(
|
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
|
data/lib/whimsy/asf/icla.rb
CHANGED
@@ -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
|
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
|
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(
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
data/lib/whimsy/asf/ldap.rb
CHANGED
@@ -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://
|
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://
|
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
|
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
|
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
|
-
|
505
|
+
weakref(:committees) do
|
506
|
+
Committee.list("member=uid=#{name},#{base}")
|
507
|
+
end
|
356
508
|
end
|
357
509
|
|
358
510
|
def groups
|
359
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
448
|
-
|
449
|
-
|
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
|
-
|
455
|
-
|
456
|
-
|
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
|
-
|
500
|
-
|
501
|
-
ASF.ldap.modify(self.dn, [
|
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
|
-
|
552
|
-
|
553
|
-
ASF.ldap.modify(self.dn, [
|
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
|
-
|
559
|
-
|
560
|
-
ASF.ldap.modify(self.dn, [
|
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
|
-
|
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
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
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
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
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
|