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 +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
|