pgls 1.0.3
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 +7 -0
- data/.autotest +23 -0
- data/.github/workflows/ci.yml +81 -0
- data/.gitignore +10 -0
- data/.travis.yml +20 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/Manifest.txt +16 -0
- data/README.md +93 -0
- data/Rakefile +13 -0
- data/appveyor.yml +27 -0
- data/config/krb5-cfg.yml +35 -0
- data/config/sample-config.yaml +68 -0
- data/config/sample-config2.yaml +76 -0
- data/config/schema.yaml +83 -0
- data/exe/pgls +9 -0
- data/lib/pgls/application.rb +497 -0
- data/lib/pgls/compat.rb +10 -0
- data/lib/pgls/logger.rb +28 -0
- data/lib/pgls/version.rb +3 -0
- data/lib/pgls.rb +23 -0
- data/pgls.gemspec +33 -0
- metadata +208 -0
@@ -0,0 +1,497 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'net/ldap'
|
4
|
+
require 'optparse'
|
5
|
+
require 'yaml'
|
6
|
+
require 'kwalify'
|
7
|
+
require 'pg'
|
8
|
+
require "pgls/logger"
|
9
|
+
|
10
|
+
module PgLdapSync
|
11
|
+
class Application
|
12
|
+
attr_accessor :config_fname
|
13
|
+
attr_accessor :groups_fname
|
14
|
+
attr_accessor :log
|
15
|
+
attr_accessor :test
|
16
|
+
|
17
|
+
def string_to_symbol(hash)
|
18
|
+
if hash.kind_of?(Hash)
|
19
|
+
return hash.inject({}) do |h, v|
|
20
|
+
raise "expected String instead of #{h.inspect}" unless v[0].kind_of?(String)
|
21
|
+
h[v[0].intern] = string_to_symbol(v[1])
|
22
|
+
h
|
23
|
+
end
|
24
|
+
else
|
25
|
+
return hash
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def validate_config(config, schema, fname)
|
31
|
+
schema = YAML.load_file(schema)
|
32
|
+
validator = Kwalify::Validator.new(schema)
|
33
|
+
errors = validator.validate(config)
|
34
|
+
if errors && !errors.empty?
|
35
|
+
errors.each do |err|
|
36
|
+
log.fatal "error in #{fname}: [#{err.path}] #{err.message}"
|
37
|
+
end
|
38
|
+
raise InvalidConfig, 78 # EX_CONFIG
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_config_file(fname)
|
43
|
+
raise "Config file #{fname.inspect} does not exist" unless File.exist?(fname)
|
44
|
+
config = YAML.load(File.read(fname))
|
45
|
+
|
46
|
+
schema_fname = File.join(File.dirname(__FILE__), '../../config/schema.yaml')
|
47
|
+
validate_config(config, schema_fname, fname)
|
48
|
+
|
49
|
+
@config = string_to_symbol(config)
|
50
|
+
end
|
51
|
+
|
52
|
+
LdapRole = Struct.new :name, :dn, :member_dns
|
53
|
+
|
54
|
+
def read_groups_from_file(fname)
|
55
|
+
groups = []
|
56
|
+
File.foreach(fname) do |line|
|
57
|
+
# Удалите пробелы и символы новой строки с помощью strip
|
58
|
+
group = line.strip
|
59
|
+
groups << group unless group.empty?
|
60
|
+
end
|
61
|
+
groups
|
62
|
+
end
|
63
|
+
|
64
|
+
def format_groups_for_ldap_users(groups, filter)
|
65
|
+
# Используйте метод map для преобразования каждой группы в соответствующий формат
|
66
|
+
formatted_groups = groups.map { |group| "(memberOf=CN=#{group},#{filter})" }
|
67
|
+
# Используйте метод join для объединения форматированных групп в одну строку с оператором "или" (|)
|
68
|
+
"(&(objectClass=organizationalPerson)(|" + formatted_groups.join + "))"
|
69
|
+
end
|
70
|
+
|
71
|
+
def format_groups_for_ldap_groups(groups)
|
72
|
+
# Используйте метод map для преобразования каждой группы в соответствующий формат
|
73
|
+
formatted_groups = groups.map { |group| "(cn=#{group})" }
|
74
|
+
# Используйте метод join для объединения форматированных групп в одну строку с оператором "или" (|)
|
75
|
+
"(|" + formatted_groups.join + ")"
|
76
|
+
end
|
77
|
+
|
78
|
+
def search_ldap_users
|
79
|
+
ldap_user_conf = @config[:ldap_users]
|
80
|
+
name_attribute = ldap_user_conf[:name_attribute]
|
81
|
+
|
82
|
+
users = []
|
83
|
+
|
84
|
+
if @groups_fname != ''
|
85
|
+
@groups = read_groups_from_file(@groups_fname)
|
86
|
+
|
87
|
+
res = @ldap.search(
|
88
|
+
base: ldap_user_conf[:base],
|
89
|
+
filter: format_groups_for_ldap_users(@groups, ldap_user_conf[:filter]),
|
90
|
+
attributes: [name_attribute, :dn]
|
91
|
+
)
|
92
|
+
else
|
93
|
+
res = @ldap.search(
|
94
|
+
base: ldap_user_conf[:base],
|
95
|
+
filter: ldap_user_conf[:filter],
|
96
|
+
attributes: [name_attribute, :dn]
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
res.each do |entry|
|
101
|
+
name = entry[name_attribute].first
|
102
|
+
|
103
|
+
unless name
|
104
|
+
log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}"
|
105
|
+
next
|
106
|
+
end
|
107
|
+
log.info "found user-dn: #{entry.dn}"
|
108
|
+
|
109
|
+
names = if ldap_user_conf[:bothcase_name]
|
110
|
+
[name, name.downcase].uniq
|
111
|
+
elsif ldap_user_conf[:lowercase_name]
|
112
|
+
[name.downcase]
|
113
|
+
elsif ldap_user_conf[:uppercase_name]
|
114
|
+
[name.upcase]
|
115
|
+
else
|
116
|
+
[name]
|
117
|
+
end
|
118
|
+
|
119
|
+
names.each do |n|
|
120
|
+
users << LdapRole.new(n, entry.dn)
|
121
|
+
end
|
122
|
+
entry.each do |attribute, values|
|
123
|
+
log.debug " #{attribute}:"
|
124
|
+
values.each do |value|
|
125
|
+
log.debug " --->#{value.inspect}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
130
|
+
return users
|
131
|
+
end
|
132
|
+
|
133
|
+
def retrieve_array_attribute(entry, attribute_name)
|
134
|
+
array = entry[attribute_name]
|
135
|
+
if array.empty?
|
136
|
+
# Possibly an attribute, which must be retrieved in several ranges
|
137
|
+
|
138
|
+
ranged_attr = entry.attribute_names.find { |n| n =~ /\A#{Regexp.escape(attribute_name)};range=/ }
|
139
|
+
if ranged_attr
|
140
|
+
entry_dn = entry.dn
|
141
|
+
|
142
|
+
loop do
|
143
|
+
array += entry[ranged_attr]
|
144
|
+
log.debug "retrieved attribute range #{ranged_attr.inspect} of dn #{entry_dn}"
|
145
|
+
|
146
|
+
if ranged_attr =~ /;range=\d\-\*\z/
|
147
|
+
break
|
148
|
+
end
|
149
|
+
|
150
|
+
attribute_with_range = ranged_attr.to_s.gsub(/;range=.*/, ";range=#{array.size}-*")
|
151
|
+
entry = @ldap.search(
|
152
|
+
base: entry_dn,
|
153
|
+
scope: Net::LDAP::SearchScope_BaseObject,
|
154
|
+
attributes: attribute_with_range).first
|
155
|
+
|
156
|
+
ranged_attr = entry.attribute_names.find { |n| n =~ /\A#{Regexp.escape(attribute_name)};range=/ }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
else
|
160
|
+
# Values already received -> No ranged attribute
|
161
|
+
end
|
162
|
+
return array
|
163
|
+
end
|
164
|
+
|
165
|
+
def search_ldap_groups
|
166
|
+
ldap_group_conf = @config[:ldap_groups]
|
167
|
+
name_attribute = ldap_group_conf[:name_attribute]
|
168
|
+
member_attribute = ldap_group_conf[:member_attribute]
|
169
|
+
|
170
|
+
groups = []
|
171
|
+
|
172
|
+
if @groups_fname != ''
|
173
|
+
res = @ldap.search(
|
174
|
+
base: ldap_group_conf[:base],
|
175
|
+
filter: format_groups_for_ldap_groups(@groups),
|
176
|
+
attributes: [name_attribute, member_attribute, :dn]
|
177
|
+
)
|
178
|
+
else
|
179
|
+
res = @ldap.search(
|
180
|
+
base: ldap_group_conf[:base],
|
181
|
+
filter: ldap_group_conf[:filter],
|
182
|
+
attributes: [name_attribute, member_attribute, :dn]
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
res.each do |entry|
|
187
|
+
name = entry[name_attribute].first
|
188
|
+
|
189
|
+
unless name
|
190
|
+
log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}"
|
191
|
+
next
|
192
|
+
end
|
193
|
+
|
194
|
+
log.info "found group-dn: #{entry.dn}"
|
195
|
+
|
196
|
+
names = if ldap_group_conf[:bothcase_name]
|
197
|
+
[name, name.downcase].uniq
|
198
|
+
elsif ldap_group_conf[:lowercase_name]
|
199
|
+
[name.downcase]
|
200
|
+
elsif ldap_group_conf[:uppercase_name]
|
201
|
+
[name.upcase]
|
202
|
+
else
|
203
|
+
[name]
|
204
|
+
end
|
205
|
+
|
206
|
+
names.each do |n|
|
207
|
+
group_members = retrieve_array_attribute(entry, member_attribute)
|
208
|
+
groups << LdapRole.new(n, entry.dn, group_members)
|
209
|
+
end
|
210
|
+
entry.each do |attribute, values|
|
211
|
+
log.debug " #{attribute}:"
|
212
|
+
values.each do |value|
|
213
|
+
log.debug " --->#{value.inspect}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
218
|
+
return groups
|
219
|
+
end
|
220
|
+
|
221
|
+
PgRole = Struct.new :name, :member_names
|
222
|
+
|
223
|
+
# List of default roles taken from https://www.postgresql.org/docs/current/predefined-roles.html
|
224
|
+
PG_BUILTIN_ROLES = %w[ pg_read_all_data pg_write_all_data pg_read_all_settings pg_read_all_stats pg_stat_scan_tables pg_monitor pg_database_owner pg_signal_backend pg_read_server_files pg_write_server_files pg_execute_server_program pg_checkpoint]
|
225
|
+
|
226
|
+
def search_pg_users
|
227
|
+
pg_users_conf = @config[:pg_users]
|
228
|
+
|
229
|
+
users = []
|
230
|
+
res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}"
|
231
|
+
res.each do |tuple|
|
232
|
+
user = PgRole.new tuple[0]
|
233
|
+
next if PG_BUILTIN_ROLES.include?(user.name)
|
234
|
+
log.info{ "found pg-user: #{user.name.inspect}"}
|
235
|
+
users << user
|
236
|
+
end
|
237
|
+
return users
|
238
|
+
end
|
239
|
+
|
240
|
+
def search_pg_groups
|
241
|
+
pg_groups_conf = @config[:pg_groups]
|
242
|
+
|
243
|
+
groups = []
|
244
|
+
res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}"
|
245
|
+
res.each do |tuple|
|
246
|
+
res2 = pg_exec "SELECT pr.rolname FROM pg_auth_members pam JOIN pg_roles pr ON pr.oid=pam.member WHERE pam.roleid=#{@pgconn.escape_string(tuple[1])}"
|
247
|
+
member_names = res2.map{|row| row[0] }
|
248
|
+
group = PgRole.new tuple[0], member_names
|
249
|
+
next if PG_BUILTIN_ROLES.include?(group.name)
|
250
|
+
log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"}
|
251
|
+
groups << group
|
252
|
+
end
|
253
|
+
return groups
|
254
|
+
end
|
255
|
+
|
256
|
+
def uniq_names(list)
|
257
|
+
names = {}
|
258
|
+
new_list = list.select do |entry|
|
259
|
+
name = entry.name
|
260
|
+
if names[name]
|
261
|
+
log.warn{ "duplicated group/user #{name.inspect} (#{entry.inspect})" }
|
262
|
+
next false
|
263
|
+
else
|
264
|
+
names[name] = true
|
265
|
+
next true
|
266
|
+
end
|
267
|
+
end
|
268
|
+
return new_list
|
269
|
+
end
|
270
|
+
|
271
|
+
MatchedRole = Struct.new :ldap, :pg, :name, :state, :type
|
272
|
+
|
273
|
+
def match_roles(ldaps, pgs, type)
|
274
|
+
ldap_by_name = ldaps.inject({}){|h,u| h[u.name] = u; h }
|
275
|
+
pg_by_name = pgs.inject({}){|h,u| h[u.name] = u; h }
|
276
|
+
|
277
|
+
roles = []
|
278
|
+
ldaps.each do |ld|
|
279
|
+
pg = pg_by_name[ld.name]
|
280
|
+
role = MatchedRole.new ld, pg, ld.name
|
281
|
+
roles << role
|
282
|
+
end
|
283
|
+
pgs.each do |pg|
|
284
|
+
ld = ldap_by_name[pg.name]
|
285
|
+
next if ld
|
286
|
+
role = MatchedRole.new ld, pg, pg.name
|
287
|
+
roles << role
|
288
|
+
end
|
289
|
+
|
290
|
+
roles.each do |r|
|
291
|
+
r.state = case
|
292
|
+
when r.ldap && !r.pg then :create
|
293
|
+
when !r.ldap && r.pg then :drop
|
294
|
+
when r.pg && r.ldap then :keep
|
295
|
+
else raise "invalid user #{r.inspect}"
|
296
|
+
end
|
297
|
+
r.type = type
|
298
|
+
end
|
299
|
+
|
300
|
+
log.info do
|
301
|
+
roles.each do |role|
|
302
|
+
log.debug{ "#{role.state} #{role.type}: #{role.name}" }
|
303
|
+
end
|
304
|
+
"#{type} stat: create: #{roles.count{|r| r.state==:create }} drop: #{roles.count{|r| r.state==:drop }} keep: #{roles.count{|r| r.state==:keep }}"
|
305
|
+
end
|
306
|
+
return roles
|
307
|
+
end
|
308
|
+
|
309
|
+
def try_sql(text)
|
310
|
+
begin
|
311
|
+
@pgconn.exec "SAVEPOINT try_sql;"
|
312
|
+
@pgconn.exec text
|
313
|
+
rescue PG::Error => err
|
314
|
+
@pgconn.exec "ROLLBACK TO try_sql;"
|
315
|
+
|
316
|
+
log.error{ "#{err} (#{err.class})" }
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def pg_exec_modify(sql)
|
321
|
+
log.info{ "SQL: #{sql}" }
|
322
|
+
unless self.test
|
323
|
+
try_sql sql
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def pg_exec(sql)
|
328
|
+
res = @pgconn.exec sql
|
329
|
+
(0...res.num_tuples).map{|t| (0...res.num_fields).map{|i| res.getvalue(t, i) } }
|
330
|
+
end
|
331
|
+
|
332
|
+
def create_pg_role(role)
|
333
|
+
pg_conf = @config[role.type==:user ? :pg_users : :pg_groups]
|
334
|
+
pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}"
|
335
|
+
end
|
336
|
+
|
337
|
+
def drop_pg_role(role)
|
338
|
+
pg_exec_modify "DROP ROLE \"#{role.name}\""
|
339
|
+
end
|
340
|
+
|
341
|
+
def sync_roles_to_pg(roles, for_state)
|
342
|
+
roles.sort{|a,b| a.name<=>b.name }.each do |role|
|
343
|
+
create_pg_role(role) if role.state==:create && for_state==:create
|
344
|
+
drop_pg_role(role) if role.state==:drop && for_state==:drop
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
MatchedMembership = Struct.new :role_name, :has_member, :state
|
349
|
+
|
350
|
+
def match_memberships(ldap_roles, pg_roles)
|
351
|
+
ldap_group_conf = @config[:ldap_groups]
|
352
|
+
hash_of_arrays = Hash.new { |h, k| h[k] = [] }
|
353
|
+
if ldap_group_conf[:ald_domain]
|
354
|
+
ldap_by_dn = ldap_roles.inject(hash_of_arrays){|h,r| h[r.name] << r; h }
|
355
|
+
else
|
356
|
+
ldap_by_dn = ldap_roles.inject(hash_of_arrays){|h,r| h[r.dn] << r; h }
|
357
|
+
end
|
358
|
+
ldap_by_m2m = ldap_roles.inject([]) do |a,r|
|
359
|
+
next a unless r.member_dns
|
360
|
+
a + r.member_dns.flat_map do |dn|
|
361
|
+
has_members = ldap_by_dn[dn]
|
362
|
+
log.warn{"ldap member with dn #{dn} is unknown"} if has_members.empty?
|
363
|
+
has_members.map do |has_member|
|
364
|
+
[r.name, has_member.name]
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
hash_of_arrays = Hash.new { |h, k| h[k] = [] }
|
370
|
+
pg_by_name = pg_roles.inject(hash_of_arrays){|h,r| h[r.name] << r; h }
|
371
|
+
pg_by_m2m = pg_roles.inject([]) do |a,r|
|
372
|
+
next a unless r.member_names
|
373
|
+
a + r.member_names.flat_map do |name|
|
374
|
+
has_members = pg_by_name[name]
|
375
|
+
log.warn{"pg member with name #{name} is unknown"} if has_members.empty?
|
376
|
+
has_members.map do |has_member|
|
377
|
+
[r.name, has_member.name]
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
memberships = (ldap_by_m2m & pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :keep }
|
383
|
+
memberships += (ldap_by_m2m - pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :grant }
|
384
|
+
memberships += (pg_by_m2m - ldap_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :revoke }
|
385
|
+
|
386
|
+
log.info do
|
387
|
+
memberships.each do |membership|
|
388
|
+
log.debug{ "#{membership.state} #{membership.role_name} to #{membership.has_member}" }
|
389
|
+
end
|
390
|
+
"membership stat: grant: #{memberships.count{|u| u.state==:grant }} revoke: #{memberships.count{|u| u.state==:revoke }} keep: #{memberships.count{|u| u.state==:keep }}"
|
391
|
+
end
|
392
|
+
return memberships
|
393
|
+
end
|
394
|
+
|
395
|
+
def grant_membership(role_name, add_members)
|
396
|
+
pg_conf = @config[:pg_groups]
|
397
|
+
add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",")
|
398
|
+
pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}"
|
399
|
+
end
|
400
|
+
|
401
|
+
def revoke_membership(role_name, rm_members)
|
402
|
+
rm_members_escaped = rm_members.map{|m| "\"#{m}\"" }.join(",")
|
403
|
+
pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}"
|
404
|
+
end
|
405
|
+
|
406
|
+
def sync_membership_to_pg(memberships, for_state)
|
407
|
+
grants = {}
|
408
|
+
memberships.select{|ms| ms.state==for_state }.each do |ms|
|
409
|
+
grants[ms.role_name] ||= []
|
410
|
+
grants[ms.role_name] << ms.has_member
|
411
|
+
end
|
412
|
+
|
413
|
+
grants.each do |role_name, members|
|
414
|
+
grant_membership(role_name, members) if for_state==:grant
|
415
|
+
revoke_membership(role_name, members) if for_state==:revoke
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def start!
|
420
|
+
read_config_file(@config_fname)
|
421
|
+
|
422
|
+
ldap_conf = @config[:ldap_connection]
|
423
|
+
auth_meth = ldap_conf.dig(:auth, :method).to_s
|
424
|
+
if auth_meth == "gssapi"
|
425
|
+
begin
|
426
|
+
require 'net/ldap/auth_adapter/gssapi'
|
427
|
+
rescue LoadError => err
|
428
|
+
raise "#{err}\nTo use GSSAPI authentication please run:\n gem install net-ldap-auth_adapter-gssapi"
|
429
|
+
end
|
430
|
+
elsif auth_meth == "gss_spnego"
|
431
|
+
begin
|
432
|
+
require 'net-ldap-gss-spnego'
|
433
|
+
# This doesn't work since this file is defined in net-ldap as a placeholder:
|
434
|
+
# require 'net/ldap/auth_adapter/gss_spnego'
|
435
|
+
rescue LoadError => err
|
436
|
+
raise "#{err}\nTo use GSSAPI authentication please run:\n gem install net-ldap-gss-spnego"
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# gather LDAP users and groups
|
441
|
+
@ldap = Net::LDAP.new ldap_conf
|
442
|
+
ldap_users = uniq_names search_ldap_users
|
443
|
+
ldap_groups = uniq_names search_ldap_groups
|
444
|
+
|
445
|
+
# gather PGs users and groups
|
446
|
+
@pgconn = PG.connect @config[:pg_connection]
|
447
|
+
begin
|
448
|
+
@pgconn.transaction do
|
449
|
+
pg_users = uniq_names search_pg_users
|
450
|
+
pg_groups = uniq_names search_pg_groups
|
451
|
+
|
452
|
+
# compare LDAP to PG users and groups
|
453
|
+
mroles = match_roles(ldap_users, pg_users, :user)
|
454
|
+
mroles += match_roles(ldap_groups, pg_groups, :group)
|
455
|
+
|
456
|
+
# compare LDAP to PG memberships
|
457
|
+
mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups)
|
458
|
+
|
459
|
+
# drop/revoke roles/memberships first
|
460
|
+
sync_membership_to_pg(mmemberships, :revoke)
|
461
|
+
sync_roles_to_pg(mroles, :drop)
|
462
|
+
# create/grant roles/memberships
|
463
|
+
sync_roles_to_pg(mroles, :create)
|
464
|
+
sync_membership_to_pg(mmemberships, :grant)
|
465
|
+
end
|
466
|
+
ensure
|
467
|
+
@pgconn.close
|
468
|
+
end
|
469
|
+
|
470
|
+
# Determine exitcode
|
471
|
+
if log.had_errors?
|
472
|
+
raise ErrorExit.new(1, log.first_error)
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
def self.run(argv)
|
477
|
+
s = self.new
|
478
|
+
s.config_fname = '/etc/pg_ldap_sync.yaml'
|
479
|
+
s.groups_fname = ''
|
480
|
+
s.log = Logger.new($stdout)
|
481
|
+
s.log.level = Logger::ERROR
|
482
|
+
|
483
|
+
OptionParser.new do |opts|
|
484
|
+
opts.version = VERSION
|
485
|
+
opts.banner = "Usage: #{$0} [options]"
|
486
|
+
opts.on("-v", "--[no-]verbose", "Increase verbose level"){|v| s.log.level += v ? -1 : 1 }
|
487
|
+
opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=))
|
488
|
+
opts.on("-g", "--groups FILE", "Groups file [#{s.groups_fname}]", &s.method(:groups_fname=))
|
489
|
+
opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=))
|
490
|
+
|
491
|
+
opts.parse!(argv)
|
492
|
+
end
|
493
|
+
|
494
|
+
s.start!
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
data/lib/pgls/compat.rb
ADDED
data/lib/pgls/logger.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module PgLdapSync
|
4
|
+
class Logger < ::Logger
|
5
|
+
def initialize(io)
|
6
|
+
super(io)
|
7
|
+
@counters = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(severity, *args, &block)
|
11
|
+
super
|
12
|
+
return unless [Logger::FATAL, Logger::ERROR].include?(severity)
|
13
|
+
@counters[severity] ||= block ? block.call : args.first
|
14
|
+
end
|
15
|
+
|
16
|
+
def had_logged?(severity)
|
17
|
+
!!@counters[severity]
|
18
|
+
end
|
19
|
+
|
20
|
+
def had_errors?
|
21
|
+
had_logged?(Logger::FATAL) || had_logged?(Logger::ERROR)
|
22
|
+
end
|
23
|
+
|
24
|
+
def first_error
|
25
|
+
@counters[Logger::FATAL] || @counters[Logger::ERROR]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/pgls/version.rb
ADDED
data/lib/pgls.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "pgls/application"
|
2
|
+
require "pgls/compat"
|
3
|
+
require "pgls/version"
|
4
|
+
|
5
|
+
module PgLdapSync
|
6
|
+
class LdapError < RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
class ApplicationExit < RuntimeError
|
10
|
+
attr_reader :exitcode
|
11
|
+
|
12
|
+
def initialize(exitcode, error=nil)
|
13
|
+
super(error)
|
14
|
+
@exitcode = exitcode
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidConfig < ApplicationExit
|
19
|
+
end
|
20
|
+
|
21
|
+
class ErrorExit < ApplicationExit
|
22
|
+
end
|
23
|
+
end
|
data/pgls.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "pgls/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "pgls"
|
7
|
+
spec.version = PgLdapSync::VERSION
|
8
|
+
spec.authors = ["fruworg"]
|
9
|
+
spec.email = ["im@fruw.org"]
|
10
|
+
|
11
|
+
spec.summary = %q{Use LDAP permissions in PostgreSQL}
|
12
|
+
spec.homepage = "https://github.com/fruworg/pgls"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
spec.rdoc_options = %w[--main README.md --charset=UTF-8]
|
22
|
+
spec.required_ruby_version = ">= 2.3"
|
23
|
+
|
24
|
+
spec.add_runtime_dependency "net-ldap", "~> 0.16"
|
25
|
+
spec.add_runtime_dependency "kwalify", "~> 0.7"
|
26
|
+
spec.add_runtime_dependency "pg", ">= 0.14", "<= 1.0.0"
|
27
|
+
spec.add_runtime_dependency "net-ldap-auth_adapter-gssapi", "~> 0.2.0"
|
28
|
+
spec.add_development_dependency "ruby-ldapserver", "~> 0.3"
|
29
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
30
|
+
spec.add_development_dependency "bundler", ">= 1.16", "< 3.0"
|
31
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
32
|
+
spec.add_development_dependency "minitest-hooks", "~> 1.4"
|
33
|
+
end
|