pg-ldap-sync 0.5.1 → 0.5.2
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
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +2 -2
- data/.standard.yml +1 -0
- data/CHANGELOG.md +8 -0
- data/README.md +24 -16
- data/Rakefile +1 -1
- data/config/sample-config.yaml +2 -2
- data/config/sample-config2.yaml +2 -2
- data/config/schema.yaml +73 -69
- data/exe/pg_ldap_sync +1 -1
- data/lib/pg_ldap_sync/application.rb +346 -340
- data/lib/pg_ldap_sync/compat.rb +7 -5
- data/lib/pg_ldap_sync/logger.rb +20 -20
- data/lib/pg_ldap_sync/version.rb +1 -1
- data/lib/pg_ldap_sync.rb +1 -1
- data/pg-ldap-sync.gemspec +12 -13
- data.tar.gz.sig +0 -0
- metadata +24 -37
- metadata.gz.sig +1 -3
@@ -1,437 +1,443 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
3
|
+
require "net/ldap"
|
4
|
+
require "optparse"
|
5
|
+
require "erb"
|
6
|
+
require "yaml"
|
7
|
+
require "json-schema"
|
8
|
+
require "pg"
|
8
9
|
require "pg_ldap_sync/logger"
|
9
10
|
|
10
11
|
module PgLdapSync
|
11
|
-
class Application
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
12
|
+
class Application
|
13
|
+
attr_accessor :config_fname
|
14
|
+
attr_accessor :log
|
15
|
+
attr_accessor :test
|
16
|
+
|
17
|
+
def string_to_symbol(hash)
|
18
|
+
if hash.is_a?(Hash)
|
19
|
+
hash.each_with_object({}) do |v, h|
|
20
|
+
raise "expected String instead of #{h.inspect}" unless v[0].is_a?(String)
|
21
|
+
h[v[0].intern] = string_to_symbol(v[1])
|
22
|
+
end
|
23
|
+
else
|
24
|
+
hash
|
22
25
|
end
|
23
|
-
else
|
24
|
-
return hash
|
25
26
|
end
|
26
|
-
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
28
|
+
def validate_config(config, schema, fname)
|
29
|
+
schema = YAML.load_file(schema)
|
30
|
+
errors = JSON::Validator.fully_validate(schema, config, validate_schema: true, insert_defaults: true)
|
31
|
+
if errors && !errors.empty?
|
32
|
+
errors.each do |err|
|
33
|
+
log.fatal "error in #{fname}: #{err}"
|
34
|
+
end
|
35
|
+
raise InvalidConfig, 78 # EX_CONFIG
|
36
36
|
end
|
37
|
-
raise InvalidConfig, 78 # EX_CONFIG
|
38
37
|
end
|
39
|
-
end
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
def read_config_file(fname)
|
40
|
+
raise "Config file #{fname.inspect} does not exist" unless File.exist?(fname)
|
41
|
+
config = YAML.load(ERB.new(File.read(fname)).result)
|
44
42
|
|
45
|
-
|
46
|
-
|
43
|
+
schema_fname = File.join(File.dirname(__FILE__), "../../config/schema.yaml")
|
44
|
+
validate_config(config, schema_fname, fname)
|
47
45
|
|
48
|
-
|
49
|
-
|
46
|
+
@config = string_to_symbol(config)
|
47
|
+
end
|
50
48
|
|
51
|
-
|
49
|
+
LdapRole = Struct.new :name, :dn, :member_dns
|
52
50
|
|
53
|
-
|
54
|
-
|
55
|
-
|
51
|
+
def search_ldap_users
|
52
|
+
ldap_user_conf = @config[:ldap_users]
|
53
|
+
name_attribute = ldap_user_conf[:name_attribute]
|
56
54
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
55
|
+
users = []
|
56
|
+
res = @ldap.search(
|
57
|
+
base: ldap_user_conf[:base],
|
58
|
+
filter: ldap_user_conf[:filter],
|
59
|
+
attributes: [name_attribute, :dn]
|
60
|
+
) do |entry|
|
61
|
+
name = entry[name_attribute].first
|
64
62
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
63
|
+
unless name
|
64
|
+
log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}"
|
65
|
+
next
|
66
|
+
end
|
67
|
+
log.info "found user-dn: #{entry.dn}"
|
68
|
+
|
69
|
+
names = if ldap_user_conf[:bothcase_name]
|
70
|
+
[name, name.downcase].uniq
|
71
|
+
elsif ldap_user_conf[:lowercase_name]
|
72
|
+
[name.downcase]
|
73
|
+
else
|
74
|
+
[name]
|
75
|
+
end
|
78
76
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
77
|
+
names.each do |n|
|
78
|
+
users << LdapRole.new(n, entry.dn)
|
79
|
+
end
|
80
|
+
entry.each do |attribute, values|
|
81
|
+
log.debug " #{attribute}:"
|
82
|
+
values.each do |value|
|
83
|
+
log.debug " --->#{value.inspect}"
|
84
|
+
end
|
86
85
|
end
|
87
86
|
end
|
87
|
+
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
88
|
+
users
|
88
89
|
end
|
89
|
-
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
90
|
-
return users
|
91
|
-
end
|
92
90
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
91
|
+
def retrieve_array_attribute(entry, attribute_name)
|
92
|
+
array = entry[attribute_name]
|
93
|
+
if array.empty?
|
94
|
+
# Possibly an attribute, which must be retrieved in several ranges
|
97
95
|
|
98
|
-
|
99
|
-
|
100
|
-
|
96
|
+
ranged_attr = entry.attribute_names.find { |n| n =~ /\A#{Regexp.escape(attribute_name)};range=/ }
|
97
|
+
if ranged_attr
|
98
|
+
entry_dn = entry.dn
|
101
99
|
|
102
|
-
|
103
|
-
|
104
|
-
|
100
|
+
loop do
|
101
|
+
array += entry[ranged_attr]
|
102
|
+
log.debug "retrieved attribute range #{ranged_attr.inspect} of dn #{entry_dn}"
|
105
103
|
|
106
|
-
|
107
|
-
|
108
|
-
|
104
|
+
if ranged_attr =~ /;range=\d+-\*\z/
|
105
|
+
break
|
106
|
+
end
|
109
107
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
108
|
+
attribute_with_range = ranged_attr.to_s.gsub(/;range=.*/, ";range=#{array.size}-*")
|
109
|
+
entry = @ldap.search(
|
110
|
+
base: entry_dn,
|
111
|
+
scope: Net::LDAP::SearchScope_BaseObject,
|
112
|
+
attributes: attribute_with_range
|
113
|
+
).first
|
115
114
|
|
116
|
-
|
115
|
+
ranged_attr = entry.attribute_names.find { |n| n =~ /\A#{Regexp.escape(attribute_name)};range=/ }
|
116
|
+
end
|
117
117
|
end
|
118
|
+
else
|
119
|
+
# Values already received -> No ranged attribute
|
118
120
|
end
|
119
|
-
|
120
|
-
# Values already received -> No ranged attribute
|
121
|
+
array
|
121
122
|
end
|
122
|
-
return array
|
123
|
-
end
|
124
123
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
124
|
+
def search_ldap_groups
|
125
|
+
ldap_group_conf = @config[:ldap_groups]
|
126
|
+
name_attribute = ldap_group_conf[:name_attribute]
|
127
|
+
member_attribute = ldap_group_conf[:member_attribute]
|
128
|
+
|
129
|
+
groups = []
|
130
|
+
res = @ldap.search(
|
131
|
+
base: ldap_group_conf[:base],
|
132
|
+
filter: ldap_group_conf[:filter],
|
133
|
+
attributes: [name_attribute, member_attribute, :dn]
|
134
|
+
) do |entry|
|
135
|
+
name = entry[name_attribute].first
|
136
|
+
|
137
|
+
unless name
|
138
|
+
log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}"
|
139
|
+
next
|
140
|
+
end
|
142
141
|
|
143
|
-
|
142
|
+
log.info "found group-dn: #{entry.dn}"
|
144
143
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
144
|
+
names = if ldap_group_conf[:bothcase_name]
|
145
|
+
[name, name.downcase].uniq
|
146
|
+
elsif ldap_group_conf[:lowercase_name]
|
147
|
+
[name.downcase]
|
148
|
+
else
|
149
|
+
[name]
|
150
|
+
end
|
152
151
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
152
|
+
names.each do |n|
|
153
|
+
group_members = retrieve_array_attribute(entry, member_attribute)
|
154
|
+
groups << LdapRole.new(n, entry.dn, group_members)
|
155
|
+
end
|
156
|
+
entry.each do |attribute, values|
|
157
|
+
log.debug " #{attribute}:"
|
158
|
+
values.each do |value|
|
159
|
+
log.debug " --->#{value.inspect}"
|
160
|
+
end
|
161
161
|
end
|
162
162
|
end
|
163
|
+
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
164
|
+
groups
|
163
165
|
end
|
164
|
-
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
165
|
-
return groups
|
166
|
-
end
|
167
166
|
|
168
|
-
|
167
|
+
PgRole = Struct.new :name, :member_names
|
169
168
|
|
170
|
-
|
171
|
-
|
169
|
+
# List of default roles taken from https://www.postgresql.org/docs/current/predefined-roles.html
|
170
|
+
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 pg_create_subscription pg_maintain pg_use_reserved_connections]
|
172
171
|
|
173
|
-
|
174
|
-
|
172
|
+
def search_pg_users
|
173
|
+
pg_users_conf = @config[:pg_users]
|
175
174
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
175
|
+
users = []
|
176
|
+
res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}"
|
177
|
+
res.each do |tuple|
|
178
|
+
user = PgRole.new tuple[0]
|
179
|
+
next if PG_BUILTIN_ROLES.include?(user.name)
|
180
|
+
log.info { "found pg-user: #{user.name.inspect}" }
|
181
|
+
users << user
|
182
|
+
end
|
183
|
+
users
|
183
184
|
end
|
184
|
-
return users
|
185
|
-
end
|
186
185
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
186
|
+
def search_pg_groups
|
187
|
+
pg_groups_conf = @config[:pg_groups]
|
188
|
+
|
189
|
+
groups = []
|
190
|
+
res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}"
|
191
|
+
res.each do |tuple|
|
192
|
+
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])}"
|
193
|
+
member_names = res2.map { |row| row[0] }
|
194
|
+
group = PgRole.new tuple[0], member_names
|
195
|
+
next if PG_BUILTIN_ROLES.include?(group.name)
|
196
|
+
log.info { "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}" }
|
197
|
+
groups << group
|
198
|
+
end
|
199
|
+
groups
|
199
200
|
end
|
200
|
-
return groups
|
201
|
-
end
|
202
201
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
202
|
+
def uniq_names(list)
|
203
|
+
names = {}
|
204
|
+
list.select do |entry|
|
205
|
+
name = entry.name
|
206
|
+
if names[name]
|
207
|
+
log.warn { "duplicated group/user #{name.inspect} (#{entry.inspect})" }
|
208
|
+
next false
|
209
|
+
else
|
210
|
+
names[name] = true
|
211
|
+
next true
|
212
|
+
end
|
213
213
|
end
|
214
214
|
end
|
215
|
-
return new_list
|
216
|
-
end
|
217
|
-
|
218
|
-
MatchedRole = Struct.new :ldap, :pg, :name, :state, :type
|
219
215
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
216
|
+
MatchedRole = Struct.new :ldap, :pg, :name, :state, :type
|
217
|
+
|
218
|
+
def match_roles(ldaps, pgs, type)
|
219
|
+
ldap_by_name = ldaps.each_with_object({}) { |u, h|
|
220
|
+
h[u.name] = u
|
221
|
+
}
|
222
|
+
pg_by_name = pgs.each_with_object({}) { |u, h|
|
223
|
+
h[u.name] = u
|
224
|
+
}
|
225
|
+
|
226
|
+
roles = []
|
227
|
+
ldaps.each do |ld|
|
228
|
+
pg = pg_by_name[ld.name]
|
229
|
+
role = MatchedRole.new ld, pg, ld.name
|
230
|
+
roles << role
|
231
|
+
end
|
232
|
+
pgs.each do |pg|
|
233
|
+
ld = ldap_by_name[pg.name]
|
234
|
+
next if ld
|
235
|
+
role = MatchedRole.new ld, pg, pg.name
|
236
|
+
roles << role
|
237
|
+
end
|
236
238
|
|
237
|
-
|
238
|
-
|
239
|
+
roles.each do |r|
|
240
|
+
r.state = case
|
239
241
|
when r.ldap && !r.pg then :create
|
240
242
|
when !r.ldap && r.pg then :drop
|
241
243
|
when r.pg && r.ldap then :keep
|
242
244
|
else raise "invalid user #{r.inspect}"
|
245
|
+
end
|
246
|
+
r.type = type
|
243
247
|
end
|
244
|
-
r.type = type
|
245
|
-
end
|
246
248
|
|
247
|
-
|
248
|
-
|
249
|
-
|
249
|
+
log.info do
|
250
|
+
roles.each do |role|
|
251
|
+
log.debug { "#{role.state} #{role.type}: #{role.name}" }
|
252
|
+
end
|
253
|
+
"#{type} stat: create: #{roles.count { |r| r.state == :create }} drop: #{roles.count { |r| r.state == :drop }} keep: #{roles.count { |r| r.state == :keep }}"
|
250
254
|
end
|
251
|
-
|
255
|
+
roles
|
252
256
|
end
|
253
|
-
return roles
|
254
|
-
end
|
255
257
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
258
|
+
def try_sql(text)
|
259
|
+
begin
|
260
|
+
@pgconn.exec "SAVEPOINT try_sql;"
|
261
|
+
@pgconn.exec text
|
262
|
+
rescue PG::Error => err
|
263
|
+
@pgconn.exec "ROLLBACK TO try_sql;"
|
262
264
|
|
263
|
-
|
265
|
+
log.error { "#{err} (#{err.class})" }
|
266
|
+
end
|
264
267
|
end
|
265
|
-
end
|
266
268
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
269
|
+
def pg_exec_modify(sql)
|
270
|
+
log.info { "SQL: #{sql}" }
|
271
|
+
unless self.test
|
272
|
+
try_sql sql
|
273
|
+
end
|
271
274
|
end
|
272
|
-
end
|
273
275
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
276
|
+
def pg_exec(sql)
|
277
|
+
res = @pgconn.exec sql
|
278
|
+
(0...res.num_tuples).map { |t| (0...res.num_fields).map { |i| res.getvalue(t, i) } }
|
279
|
+
end
|
278
280
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
281
|
+
def create_pg_role(role)
|
282
|
+
pg_conf = @config[(role.type == :user) ? :pg_users : :pg_groups]
|
283
|
+
pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}"
|
284
|
+
end
|
283
285
|
|
284
|
-
|
285
|
-
|
286
|
-
|
286
|
+
def drop_pg_role(role)
|
287
|
+
pg_exec_modify "DROP ROLE \"#{role.name}\""
|
288
|
+
end
|
287
289
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
290
|
+
def sync_roles_to_pg(roles, for_state)
|
291
|
+
roles.sort_by(&:name).each do |role|
|
292
|
+
create_pg_role(role) if role.state == :create && for_state == :create
|
293
|
+
drop_pg_role(role) if role.state == :drop && for_state == :drop
|
294
|
+
end
|
292
295
|
end
|
293
|
-
end
|
294
296
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
297
|
+
MatchedMembership = Struct.new :role_name, :has_member, :state
|
298
|
+
|
299
|
+
def match_memberships(ldap_roles, pg_roles)
|
300
|
+
hash_of_arrays = Hash.new { |h, k| h[k] = [] }
|
301
|
+
ldap_by_dn = ldap_roles.each_with_object(hash_of_arrays) { |r, h|
|
302
|
+
h[r.dn] << r
|
303
|
+
}
|
304
|
+
ldap_by_m2m = ldap_roles.inject([]) do |a, r|
|
305
|
+
next a unless r.member_dns
|
306
|
+
a + r.member_dns.flat_map do |dn|
|
307
|
+
has_members = ldap_by_dn[dn]
|
308
|
+
log.warn { "ldap member with dn #{dn} is unknown" } if has_members.empty?
|
309
|
+
has_members.map do |has_member|
|
310
|
+
[r.name, has_member.name]
|
311
|
+
end
|
307
312
|
end
|
308
313
|
end
|
309
|
-
end
|
310
314
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
315
|
+
hash_of_arrays = Hash.new { |h, k| h[k] = [] }
|
316
|
+
pg_by_name = pg_roles.each_with_object(hash_of_arrays) { |r, h|
|
317
|
+
h[r.name] << r
|
318
|
+
}
|
319
|
+
pg_by_m2m = pg_roles.inject([]) do |a, r|
|
320
|
+
next a unless r.member_names
|
321
|
+
a + r.member_names.flat_map do |name|
|
322
|
+
has_members = pg_by_name[name]
|
323
|
+
log.warn { "pg member with name #{name} is unknown" } if has_members.empty?
|
324
|
+
has_members.map do |has_member|
|
325
|
+
[r.name, has_member.name]
|
326
|
+
end
|
320
327
|
end
|
321
328
|
end
|
322
|
-
end
|
323
329
|
|
324
|
-
|
325
|
-
|
326
|
-
|
330
|
+
memberships = (ldap_by_m2m & pg_by_m2m).map { |r, mo| MatchedMembership.new r, mo, :keep }
|
331
|
+
memberships += (ldap_by_m2m - pg_by_m2m).map { |r, mo| MatchedMembership.new r, mo, :grant }
|
332
|
+
memberships += (pg_by_m2m - ldap_by_m2m).map { |r, mo| MatchedMembership.new r, mo, :revoke }
|
327
333
|
|
328
|
-
|
329
|
-
|
330
|
-
|
334
|
+
log.info do
|
335
|
+
memberships.each do |membership|
|
336
|
+
log.debug { "#{membership.state} #{membership.role_name} to #{membership.has_member}" }
|
337
|
+
end
|
338
|
+
"membership stat: grant: #{memberships.count { |u| u.state == :grant }} revoke: #{memberships.count { |u| u.state == :revoke }} keep: #{memberships.count { |u| u.state == :keep }}"
|
331
339
|
end
|
332
|
-
|
340
|
+
memberships
|
333
341
|
end
|
334
|
-
return memberships
|
335
|
-
end
|
336
|
-
|
337
|
-
def grant_membership(role_name, add_members)
|
338
|
-
pg_conf = @config[:pg_groups]
|
339
|
-
add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",")
|
340
|
-
pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}"
|
341
|
-
end
|
342
342
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
343
|
+
def grant_membership(role_name, add_members)
|
344
|
+
pg_conf = @config[:pg_groups]
|
345
|
+
add_members_escaped = add_members.map { |m| "\"#{m}\"" }.join(",")
|
346
|
+
pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}"
|
347
|
+
end
|
347
348
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
grants[ms.role_name] ||= []
|
352
|
-
grants[ms.role_name] << ms.has_member
|
349
|
+
def revoke_membership(role_name, rm_members)
|
350
|
+
rm_members_escaped = rm_members.map { |m| "\"#{m}\"" }.join(",")
|
351
|
+
pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}"
|
353
352
|
end
|
354
353
|
|
355
|
-
|
356
|
-
|
357
|
-
|
354
|
+
def sync_membership_to_pg(memberships, for_state)
|
355
|
+
grants = {}
|
356
|
+
memberships.select { |ms| ms.state == for_state }.each do |ms|
|
357
|
+
grants[ms.role_name] ||= []
|
358
|
+
grants[ms.role_name] << ms.has_member
|
359
|
+
end
|
360
|
+
|
361
|
+
grants.each do |role_name, members|
|
362
|
+
grant_membership(role_name, members) if for_state == :grant
|
363
|
+
revoke_membership(role_name, members) if for_state == :revoke
|
364
|
+
end
|
358
365
|
end
|
359
|
-
end
|
360
366
|
|
361
|
-
|
362
|
-
|
367
|
+
def start!
|
368
|
+
read_config_file(@config_fname)
|
363
369
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
370
|
+
ldap_conf = @config[:ldap_connection]
|
371
|
+
auth_meth = ldap_conf.dig(:auth, :method).to_s
|
372
|
+
if auth_meth == "gssapi"
|
373
|
+
begin
|
374
|
+
require "net/ldap/auth_adapter/gssapi"
|
375
|
+
rescue LoadError => err
|
376
|
+
raise "#{err}\nTo use GSSAPI authentication please run:\n gem install net-ldap-auth_adapter-gssapi"
|
377
|
+
end
|
378
|
+
elsif auth_meth == "gss_spnego"
|
379
|
+
begin
|
380
|
+
require "net-ldap-gss-spnego"
|
381
|
+
# This doesn't work since this file is defined in net-ldap as a placeholder:
|
382
|
+
# require 'net/ldap/auth_adapter/gss_spnego'
|
383
|
+
rescue LoadError => err
|
384
|
+
raise "#{err}\nTo use GSSAPI authentication please run:\n gem install net-ldap-gss-spnego"
|
385
|
+
end
|
371
386
|
end
|
372
|
-
|
387
|
+
|
388
|
+
# gather LDAP users and groups
|
389
|
+
@ldap = Net::LDAP.new ldap_conf
|
390
|
+
ldap_users = uniq_names search_ldap_users
|
391
|
+
ldap_groups = uniq_names search_ldap_groups
|
392
|
+
|
393
|
+
# gather PGs users and groups
|
394
|
+
@pgconn = PG.connect @config[:pg_connection]
|
373
395
|
begin
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
396
|
+
@pgconn.transaction do
|
397
|
+
pg_users = uniq_names search_pg_users
|
398
|
+
pg_groups = uniq_names search_pg_groups
|
399
|
+
|
400
|
+
# compare LDAP to PG users and groups
|
401
|
+
mroles = match_roles(ldap_users, pg_users, :user)
|
402
|
+
mroles += match_roles(ldap_groups, pg_groups, :group)
|
403
|
+
|
404
|
+
# compare LDAP to PG memberships
|
405
|
+
mmemberships = match_memberships(ldap_users + ldap_groups, pg_users + pg_groups)
|
406
|
+
|
407
|
+
# drop/revoke roles/memberships first
|
408
|
+
sync_membership_to_pg(mmemberships, :revoke)
|
409
|
+
sync_roles_to_pg(mroles, :drop)
|
410
|
+
# create/grant roles/memberships
|
411
|
+
sync_roles_to_pg(mroles, :create)
|
412
|
+
sync_membership_to_pg(mmemberships, :grant)
|
413
|
+
end
|
414
|
+
ensure
|
415
|
+
@pgconn.close
|
379
416
|
end
|
380
|
-
end
|
381
417
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
ldap_groups = uniq_names search_ldap_groups
|
386
|
-
|
387
|
-
# gather PGs users and groups
|
388
|
-
@pgconn = PG.connect @config[:pg_connection]
|
389
|
-
begin
|
390
|
-
@pgconn.transaction do
|
391
|
-
pg_users = uniq_names search_pg_users
|
392
|
-
pg_groups = uniq_names search_pg_groups
|
393
|
-
|
394
|
-
# compare LDAP to PG users and groups
|
395
|
-
mroles = match_roles(ldap_users, pg_users, :user)
|
396
|
-
mroles += match_roles(ldap_groups, pg_groups, :group)
|
397
|
-
|
398
|
-
# compare LDAP to PG memberships
|
399
|
-
mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups)
|
400
|
-
|
401
|
-
# drop/revoke roles/memberships first
|
402
|
-
sync_membership_to_pg(mmemberships, :revoke)
|
403
|
-
sync_roles_to_pg(mroles, :drop)
|
404
|
-
# create/grant roles/memberships
|
405
|
-
sync_roles_to_pg(mroles, :create)
|
406
|
-
sync_membership_to_pg(mmemberships, :grant)
|
418
|
+
# Determine exitcode
|
419
|
+
if log.had_errors?
|
420
|
+
raise ErrorExit.new(1, log.first_error)
|
407
421
|
end
|
408
|
-
ensure
|
409
|
-
@pgconn.close
|
410
422
|
end
|
411
423
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
424
|
+
def self.run(argv)
|
425
|
+
s = new
|
426
|
+
s.config_fname = "/etc/pg_ldap_sync.yaml"
|
427
|
+
s.log = Logger.new($stdout)
|
428
|
+
s.log.level = Logger::ERROR
|
417
429
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
430
|
+
OptionParser.new do |opts|
|
431
|
+
opts.version = VERSION
|
432
|
+
opts.banner = "Usage: #{$0} [options]"
|
433
|
+
opts.on("-v", "--[no-]verbose", "Increase verbose level") { |v| s.log.level += v ? -1 : 1 }
|
434
|
+
opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=))
|
435
|
+
opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=))
|
423
436
|
|
424
|
-
|
425
|
-
|
426
|
-
opts.banner = "Usage: #{$0} [options]"
|
427
|
-
opts.on("-v", "--[no-]verbose", "Increase verbose level"){|v| s.log.level += v ? -1 : 1 }
|
428
|
-
opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=))
|
429
|
-
opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=))
|
437
|
+
opts.parse!(argv)
|
438
|
+
end
|
430
439
|
|
431
|
-
|
440
|
+
s.start!
|
432
441
|
end
|
433
|
-
|
434
|
-
s.start!
|
435
442
|
end
|
436
443
|
end
|
437
|
-
end
|