pg-ldap-sync 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
@@ -0,0 +1,4 @@
1
+ === 0.1.0 / 2011-07-13
2
+
3
+ * Birthday!
4
+
@@ -0,0 +1,15 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/pg_ldap_sync
7
+ config/sample-config.yaml
8
+ config/sample-config2.yaml
9
+ config/schema.yaml
10
+ lib/pg_ldap_sync.rb
11
+ lib/pg_ldap_sync/application.rb
12
+ test/fixtures/config-ldapdb.yaml
13
+ test/fixtures/ldapdb.yaml
14
+ test/ldap_server.rb
15
+ test/test_pg_ldap_sync.rb
@@ -0,0 +1,102 @@
1
+ = Use LDAP permissions in PostgreSQL
2
+
3
+ * http://github.com/larskanis/pg-ldap-sync
4
+
5
+ == DESCRIPTION:
6
+
7
+ LDAP is often used to do a centralized user and role management
8
+ in an enterprise environment. PostgreSQL offers different
9
+ authentication methods, like LDAP, SSPI, GSSAPI or SSL.
10
+ However, for any method the user must already exist in the database,
11
+ before the authentication can be used. There is currently
12
+ no authorization of database users directly based on LDAP.
13
+
14
+ This program helps to solve the issue by synchronizing users,
15
+ groups and their memberships from LDAP to PostgreSQL.
16
+ Access to LDAP is read-only. <tt>pg_ldap_sync</tt> issues proper
17
+ CREATE ROLE, DROP ROLE, GRANT and REVOKE commands to synchronize
18
+ users and groups.
19
+
20
+ It is meant to be started as a cron job.
21
+
22
+ == FEATURES:
23
+
24
+ * Configurable per YAML config file
25
+ * Can use Active Directory as LDAP-Server
26
+ * Nested groups/roles supported
27
+ * Runs with pg.gem (C-library) or postgres-pr.gem (pure Ruby)
28
+ * Test mode which doesn't do any changes to the DBMS
29
+
30
+ == REQUIREMENTS:
31
+
32
+ * Installed Ruby and rubygems
33
+ * LDAP- and PostgreSQL-Server
34
+
35
+ == INSTALL:
36
+
37
+ Install Ruby and rubygems:
38
+ * on Windows: http://rubyinstaller.org
39
+ * on Debian/Ubuntu: <tt>apt-get install ruby rubygems</tt>
40
+
41
+ Install pg-ldap-sync and a database connector for PostgreSQL:
42
+ gem install pg-ldap-sync pg
43
+ You may also use the pure ruby postgres-connector which is less mature,
44
+ but doesn't need compilation:
45
+ gem install pg-ldap-sync postgres-pr
46
+
47
+ === Install from Git:
48
+ git clone https://github.com/larskanis/pg-ldap-sync.git
49
+ cd pg-ldap-sync
50
+ gem install hoe
51
+ rake install_gem
52
+
53
+ == USAGE:
54
+
55
+ Create a config file based on <tt>config/sample-config.yaml</tt> .
56
+ Run in test-mode:
57
+
58
+ pg_ldap_sync -c my_config.yaml -vv -t
59
+
60
+ Run in modify-mode:
61
+
62
+ pg_ldap_sync -c my_config.yaml -vv
63
+
64
+
65
+ == TEST:
66
+ There is a small test suite in the <tt>test</tt> directory that runs
67
+ against an internal ruby-ldapserver and PostgreSQL server. Ensure gem
68
+ <tt>ruby-ldapserver</tt> is installed and <tt>pg_ctl</tt>, <tt>initdb</tt> and <tt>psql</tt>
69
+ commands are in the <tt>PATH</tt>. Then:
70
+
71
+ cd pg-ldap-sync
72
+ rake test
73
+
74
+ == ISSUES:
75
+ * There is currently no way to set certain user attributes in PG
76
+ based on individual attributes in LDAP (expiration date etc.)
77
+
78
+
79
+ == LICENSE:
80
+
81
+ (The MIT License)
82
+
83
+ Copyright (c) 2011 FIX
84
+
85
+ Permission is hereby granted, free of charge, to any person obtaining
86
+ a copy of this software and associated documentation files (the
87
+ 'Software'), to deal in the Software without restriction, including
88
+ without limitation the rights to use, copy, modify, merge, publish,
89
+ distribute, sublicense, and/or sell copies of the Software, and to
90
+ permit persons to whom the Software is furnished to do so, subject to
91
+ the following conditions:
92
+
93
+ The above copyright notice and this permission notice shall be
94
+ included in all copies or substantial portions of the Software.
95
+
96
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
97
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
98
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
99
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
100
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
101
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
102
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,18 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'pg-ldap-sync' do
7
+ developer('Lars Kanis', 'kanis@comcard.de')
8
+
9
+ extra_deps << ['net-ldap', '>= 0.2']
10
+ extra_deps << ['kwalify', '>= 0.7']
11
+ extra_dev_deps << ['ruby-ldapserver', '>= 0.3']
12
+
13
+ self.readme_file = 'README.rdoc'
14
+ spec_extras[:rdoc_options] = ['--main', readme_file, "--charset=UTF-8"]
15
+ self.extra_rdoc_files << self.readme_file
16
+ end
17
+
18
+ # vim: syntax=ruby
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'pg_ldap_sync/application'
5
+
6
+ PgLdapSync::Application.run(ARGV)
@@ -0,0 +1,54 @@
1
+ # With this sample config the distinction between PG groups and users is
2
+ # done by the LOGIN/NOLOGIN attribute. Any non-superuser account
3
+ # is considered as LDAP-synchronized.
4
+
5
+ # Connection parameters to LDAP server
6
+ # see also: http://net-ldap.rubyforge.org/Net/LDAP.html#method-c-new
7
+ ldap_connection:
8
+ host: localhost
9
+ port: 389
10
+ auth:
11
+ method: :simple
12
+ username: CN=username,OU=!Serviceaccounts,OU=company,DC=company,DC=de
13
+ password: secret
14
+
15
+ # Search parameters for LDAP users which should be synchronized
16
+ ldap_users:
17
+ base: OU=company,OU=company,DC=company,DC=de
18
+ # LDAP filter (according to RFC 2254)
19
+ # defines to users in LDAP to be synchronized
20
+ filter: (&(objectClass=person)(objectClass=organizationalPerson)(givenName=*)(sn=*))
21
+ # this attribute is used as PG role name
22
+ name_attribute: sAMAccountName
23
+
24
+ # Search parameters for LDAP groups which should be synchronized
25
+ ldap_groups:
26
+ base: OU=company,OU=company,DC=company,DC=de
27
+ filter: (|(cn=group1)(cn=group2)(cn=group3))
28
+ # this attribute is used as PG role name
29
+ name_attribute: cn
30
+ # this attribute must reference to all member DN's of the given group
31
+ member_attribute: member
32
+
33
+ # Connection parameters to PostgreSQL server
34
+ # see also: http://rubydoc.info/gems/pg/0.11.0/PGconn#initialize-instance_method
35
+ pg_connection:
36
+ host:
37
+ dbname: postgres
38
+ user: db-username
39
+ password:
40
+
41
+ pg_users:
42
+ # Filter for identifying LDAP generated users in the database.
43
+ # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles"
44
+ filter: rolcanlogin AND NOT rolsuper
45
+ # Options for CREATE RULE statements
46
+ create_options: LOGIN
47
+
48
+ pg_groups:
49
+ # Filter for identifying LDAP generated groups in the database.
50
+ # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles"
51
+ filter: NOT rolcanlogin AND NOT rolsuper
52
+ # Options for CREATE RULE statements
53
+ create_options: NOLOGIN
54
+ grant_options:
@@ -0,0 +1,54 @@
1
+ # With this sample config the distinction between LDAP-synchronized
2
+ # groups/users from is done by the membership to ldap_user and
3
+ # ldap_group. These two roles has to be defined manally.
4
+
5
+ # Connection parameters to LDAP server
6
+ # see also: http://net-ldap.rubyforge.org/Net/LDAP.html#method-c-new
7
+ ldap_connection:
8
+ host: ldapserver
9
+ port: 389
10
+ auth:
11
+ method: :simple
12
+ username: CN=username,OU=!Serviceaccounts,OU=company,DC=company,DC=de
13
+ password: secret
14
+
15
+ # Search parameters for LDAP users which should be synchronized
16
+ ldap_users:
17
+ base: OU=company,DC=company,DC=prod
18
+ # LDAP filter (according to RFC 2254)
19
+ # defines to users in LDAP to be synchronized
20
+ filter: (&(objectClass=person)(objectClass=organizationalPerson)(givenName=*)(sn=*)(sAMAccountName=*))
21
+ # this attribute is used as PG role name
22
+ name_attribute: sAMAccountName
23
+
24
+ # Search parameters for LDAP groups which should be synchronized
25
+ ldap_groups:
26
+ base: OU=company,DC=company,DC=prod
27
+ filter: (cn=company.*)
28
+ # this attribute is used as PG role name
29
+ name_attribute: cn
30
+ # this attribute must reference to all member DN's of the given group
31
+ member_attribute: member
32
+
33
+ # Connection parameters to PostgreSQL server
34
+ # see also: http://rubydoc.info/gems/pg/0.11.0/PGconn#initialize-instance_method
35
+ pg_connection:
36
+ host:
37
+ dbname: postgres
38
+ user:
39
+ password:
40
+
41
+ pg_users:
42
+ # Filter for identifying LDAP generated users in the database.
43
+ # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles"
44
+ filter: oid IN (SELECT pam.member FROM pg_auth_members pam JOIN pg_roles pr ON pr.oid=pam.roleid WHERE pr.rolname='ldap_users')
45
+ # Options for CREATE RULE statements
46
+ create_options: LOGIN IN ROLE ldap_users
47
+
48
+ pg_groups:
49
+ # Filter for identifying LDAP generated groups in the database.
50
+ # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles"
51
+ filter: oid IN (SELECT pam.member FROM pg_auth_members pam JOIN pg_roles pr ON pr.oid=pam.roleid WHERE pr.rolname='ldap_groups')
52
+ # Options for CREATE RULE statements
53
+ create_options: NOLOGIN IN ROLE ldap_groups
54
+ grant_options:
@@ -0,0 +1,62 @@
1
+ type: map
2
+ mapping:
3
+ "ldap_connection":
4
+ type: any
5
+ required: yes
6
+
7
+ "ldap_users":
8
+ type: map
9
+ required: yes
10
+ mapping:
11
+ "base":
12
+ type: str
13
+ required: yes
14
+ "filter":
15
+ type: str
16
+ required: yes
17
+ "name_attribute":
18
+ type: str
19
+ required: yes
20
+
21
+ "ldap_groups":
22
+ type: map
23
+ required: yes
24
+ mapping:
25
+ "base":
26
+ type: str
27
+ required: yes
28
+ "filter":
29
+ type: str
30
+ required: yes
31
+ "name_attribute":
32
+ type: str
33
+ required: yes
34
+ "member_attribute":
35
+ type: str
36
+ required: yes
37
+
38
+ "pg_connection":
39
+ type: any
40
+ required: yes
41
+
42
+ "pg_users":
43
+ type: map
44
+ required: yes
45
+ mapping:
46
+ "filter":
47
+ type: str
48
+ required: yes
49
+ "create_options":
50
+ type: str
51
+
52
+ "pg_groups":
53
+ type: map
54
+ required: yes
55
+ mapping:
56
+ "filter":
57
+ type: str
58
+ required: yes
59
+ "create_options":
60
+ type: str
61
+ "grant_options":
62
+ type: str
@@ -0,0 +1,3 @@
1
+ module PgLdapSync
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'net/ldap'
5
+ require 'optparse'
6
+ require 'yaml'
7
+ require 'logger'
8
+ require 'kwalify'
9
+
10
+ begin
11
+ require 'pg'
12
+ rescue LoadError => e
13
+ begin
14
+ require 'postgres'
15
+ class PGconn
16
+ alias initialize_before_hash_change initialize
17
+ def initialize(*args)
18
+ arg = args.first
19
+ if args.length==1 && arg.kind_of?(Hash)
20
+ initialize_before_hash_change(arg[:host], arg[:port], nil, nil, arg[:dbname], arg[:user], arg[:password])
21
+ else
22
+ initialize_before_hash_change(*args)
23
+ end
24
+ end
25
+ end
26
+ rescue LoadError
27
+ raise e
28
+ end
29
+ end
30
+
31
+ require 'pg_ldap_sync'
32
+
33
+ module PgLdapSync
34
+ class Application
35
+ class LdapError < RuntimeError; end
36
+ attr_accessor :config_fname
37
+ attr_accessor :log
38
+ attr_accessor :test
39
+
40
+ def string_to_symbol(hash)
41
+ if hash.kind_of?(Hash)
42
+ return hash.inject({}){|h, v|
43
+ raise "expected String instead of #{h.inspect}" unless v[0].kind_of?(String)
44
+ h[v[0].intern] = string_to_symbol(v[1])
45
+ h
46
+ }
47
+ else
48
+ return hash
49
+ end
50
+ end
51
+
52
+
53
+ def validate_config(config, schema, fname)
54
+ schema = YAML.load_file(schema)
55
+ validator = Kwalify::Validator.new(schema)
56
+ errors = validator.validate(config)
57
+ if errors && !errors.empty?
58
+ errors.each do |err|
59
+ log.fatal "error in #{fname}: [#{err.path}] #{err.message}"
60
+ end
61
+ exit(-1)
62
+ end
63
+ end
64
+
65
+ def read_config_file(fname)
66
+ raise "Config file #{fname.inspect} does not exist" unless File.exist?(fname)
67
+ config = YAML.load(File.read(fname))
68
+
69
+ schema_fname = File.join(File.dirname(__FILE__), '../../config/schema.yaml')
70
+ validate_config(config, schema_fname, fname)
71
+
72
+ @config = string_to_symbol(config)
73
+ end
74
+
75
+ LdapRole = Struct.new :name, :dn, :member_dns
76
+
77
+ def search_ldap_users
78
+ ldap_user_conf = @config[:ldap_users]
79
+
80
+ users = []
81
+ res = @ldap.search(:base => ldap_user_conf[:base], :filter => ldap_user_conf[:filter]) do |entry|
82
+ name = entry[ldap_user_conf[:name_attribute]].first
83
+
84
+ unless name
85
+ log.warn "user attribute #{ldap_user_conf[:name_attribute].inspect} not defined for #{entry.dn}"
86
+ next
87
+ end
88
+
89
+ log.info "found user-dn: #{entry.dn}"
90
+ user = LdapRole.new name, entry.dn
91
+ users << user
92
+ entry.each do |attribute, values|
93
+ log.debug " #{attribute}:"
94
+ values.each do |value|
95
+ log.debug " --->#{value.inspect}"
96
+ end
97
+ end
98
+ end
99
+ raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
100
+ return users
101
+ end
102
+
103
+ def search_ldap_groups
104
+ ldap_group_conf = @config[:ldap_groups]
105
+
106
+ groups = []
107
+ res = @ldap.search(:base => ldap_group_conf[:base], :filter => ldap_group_conf[:filter]) do |entry|
108
+ name = entry[ldap_group_conf[:name_attribute]].first
109
+
110
+ unless name
111
+ log.warn "user attribute #{ldap_group_conf[:name_attribute].inspect} not defined for #{entry.dn}"
112
+ next
113
+ end
114
+
115
+ log.info "found group-dn: #{entry.dn}"
116
+ group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]]
117
+ groups << group
118
+ entry.each do |attribute, values|
119
+ log.debug " #{attribute}:"
120
+ values.each do |value|
121
+ log.debug " --->#{value.inspect}"
122
+ end
123
+ end
124
+ end
125
+ raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
126
+ return groups
127
+ end
128
+
129
+ PgRole = Struct.new :name, :member_names
130
+
131
+ def search_pg_users
132
+ pg_users_conf = @config[:pg_users]
133
+
134
+ users = []
135
+ res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}"
136
+ res.each do |tuple|
137
+ user = PgRole.new tuple[0]
138
+ log.info{ "found pg-user: #{user.name.inspect}"}
139
+ users << user
140
+ end
141
+ return users
142
+ end
143
+
144
+ def search_pg_groups
145
+ pg_groups_conf = @config[:pg_groups]
146
+
147
+ groups = []
148
+ res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}"
149
+ res.each do |tuple|
150
+ 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(tuple[1])}"
151
+ member_names = res2.map{|row| row[0] }
152
+ group = PgRole.new tuple[0], member_names
153
+ log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"}
154
+ groups << group
155
+ end
156
+ return groups
157
+ end
158
+
159
+ def uniq_names(list)
160
+ names = {}
161
+ new_list = list.select do |entry|
162
+ name = entry.name
163
+ if names[name]
164
+ log.warn{ "duplicated group/user #{name.inspect} (#{entry.inspect})" }
165
+ next false
166
+ else
167
+ names[name] = true
168
+ next true
169
+ end
170
+ end
171
+ return new_list
172
+ end
173
+
174
+ MatchedRole = Struct.new :ldap, :pg, :name, :state, :type
175
+
176
+ def match_roles(ldaps, pgs, type)
177
+ ldap_by_name = ldaps.inject({}){|h,u| h[u.name] = u; h }
178
+ pg_by_name = pgs.inject({}){|h,u| h[u.name] = u; h }
179
+
180
+ roles = []
181
+ ldaps.each do |ld|
182
+ pg = pg_by_name[ld.name]
183
+ role = MatchedRole.new ld, pg, ld.name
184
+ roles << role
185
+ end
186
+ pgs.each do |pg|
187
+ ld = ldap_by_name[pg.name]
188
+ next if ld
189
+ role = MatchedRole.new ld, pg, pg.name
190
+ roles << role
191
+ end
192
+
193
+ roles.each do |r|
194
+ r.state = case
195
+ when r.ldap && !r.pg then :create
196
+ when !r.ldap && r.pg then :drop
197
+ when r.pg && r.ldap then :keep
198
+ else raise "invalid user #{r.inspect}"
199
+ end
200
+ r.type = type
201
+ end
202
+
203
+ log.info{
204
+ roles.each do |role|
205
+ log.debug{ "#{role.state} #{role.type}: #{role.name}" }
206
+ end
207
+ "#{type} stat: create: #{roles.count{|r| r.state==:create }} drop: #{roles.count{|r| r.state==:drop }} keep: #{roles.count{|r| r.state==:keep }}"
208
+ }
209
+ return roles
210
+ end
211
+
212
+ def pg_exec_modify(sql)
213
+ log.info{ "SQL: #{sql}" }
214
+ unless self.test
215
+ res = @pgconn.exec sql
216
+ end
217
+ end
218
+
219
+ def pg_exec(sql)
220
+ res = @pgconn.exec sql
221
+ (0...res.num_tuples).map{|t| (0...res.num_fields).map{|i| res.getvalue(t, i) } }
222
+ end
223
+
224
+ def create_pg_role(role)
225
+ pg_conf = @config[role.type==:user ? :pg_users : :pg_groups]
226
+ pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}"
227
+ end
228
+
229
+ def drop_pg_role(role)
230
+ pg_exec_modify "DROP ROLE \"#{role.name}\""
231
+ end
232
+
233
+ def sync_roles_to_pg(roles, for_state)
234
+ roles.sort{|a,b| a.name<=>b.name }.each do |role|
235
+ create_pg_role(role) if role.state==:create && for_state==:create
236
+ drop_pg_role(role) if role.state==:drop && for_state==:drop
237
+ end
238
+ end
239
+
240
+ MatchedMembership = Struct.new :role_name, :has_member, :state
241
+
242
+ def match_memberships(ldap_roles, pg_roles)
243
+ ldap_by_dn = ldap_roles.inject({}){|h,r| h[r.dn] = r; h }
244
+ ldap_by_m2m = ldap_roles.inject([]){|a,r|
245
+ next a unless r.member_dns
246
+ a + r.member_dns.map{|dn|
247
+ if has_member=ldap_by_dn[dn]
248
+ [r.name, has_member.name]
249
+ else
250
+ log.warn{"ldap member with dn #{dn} is unknown"}
251
+ nil
252
+ end
253
+ }.compact
254
+ }
255
+
256
+ pg_by_name = pg_roles.inject({}){|h,r| h[r.name] = r; h }
257
+ pg_by_m2m = pg_roles.inject([]){|a,r|
258
+ next a unless r.member_names
259
+ a + r.member_names.map{|name|
260
+ if has_member=pg_by_name[name]
261
+ [r.name, has_member.name]
262
+ else
263
+ log.warn{"pg member with name #{name} is unknown"}
264
+ nil
265
+ end
266
+ }.compact
267
+ }
268
+
269
+ memberships = (ldap_by_m2m & pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :keep }
270
+ memberships += (ldap_by_m2m - pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :grant }
271
+ memberships += (pg_by_m2m - ldap_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :revoke }
272
+
273
+ log.info{
274
+ memberships.each do |membership|
275
+ log.debug{ "#{membership.state} #{membership.role_name} to #{membership.has_member}" }
276
+ end
277
+ "membership stat: grant: #{memberships.count{|u| u.state==:grant }} revoke: #{memberships.count{|u| u.state==:revoke }} keep: #{memberships.count{|u| u.state==:keep }}"
278
+ }
279
+ return memberships
280
+ end
281
+
282
+ def grant_membership(role_name, add_members)
283
+ pg_conf = @config[:pg_groups]
284
+ add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",")
285
+ pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}"
286
+ end
287
+
288
+ def revoke_membership(role_name, rm_members)
289
+ rm_members_escaped = rm_members.map{|m| "\"#{m}\"" }.join(",")
290
+ pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}"
291
+ end
292
+
293
+ def sync_membership_to_pg(memberships, for_state)
294
+ grants = {}
295
+ memberships.select{|ms| ms.state==for_state }.each do |ms|
296
+ grants[ms.role_name] ||= []
297
+ grants[ms.role_name] << ms.has_member
298
+ end
299
+
300
+ grants.each do |role_name, members|
301
+ grant_membership(role_name, members) if for_state==:grant
302
+ revoke_membership(role_name, members) if for_state==:revoke
303
+ end
304
+ end
305
+
306
+ def start!
307
+ read_config_file(@config_fname)
308
+
309
+ # gather LDAP users and groups
310
+ @ldap = Net::LDAP.new @config[:ldap_connection]
311
+ ldap_users = uniq_names search_ldap_users
312
+ ldap_groups = uniq_names search_ldap_groups
313
+
314
+ # gather PGs users and groups
315
+ @pgconn = PGconn.connect @config[:pg_connection]
316
+ pg_users = uniq_names search_pg_users
317
+ pg_groups = uniq_names search_pg_groups
318
+
319
+ # compare LDAP to PG users and groups
320
+ mroles = match_roles(ldap_users, pg_users, :user)
321
+ mroles += match_roles(ldap_groups, pg_groups, :group)
322
+
323
+ # compare LDAP to PG memberships
324
+ mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups)
325
+
326
+ # drop/revoke roles/memberships first
327
+ sync_membership_to_pg(mmemberships, :revoke)
328
+ sync_roles_to_pg(mroles, :drop)
329
+ # create/grant roles/memberships
330
+ sync_roles_to_pg(mroles, :create)
331
+ sync_membership_to_pg(mmemberships, :grant)
332
+
333
+ @pgconn.close
334
+ end
335
+
336
+ def self.run(argv)
337
+ s = self.new
338
+ s.config_fname = '/etc/pg_ldap_sync.yaml'
339
+ s.log = Logger.new(STDOUT)
340
+ s.log.level = Logger::ERROR
341
+
342
+ OptionParser.new do |opts|
343
+ opts.banner = "Usage: #{$0} [options]"
344
+ opts.on("-v", "--[no-]verbose", "Increase verbose level"){ s.log.level-=1 }
345
+ opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=))
346
+ opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=))
347
+
348
+ opts.parse!(argv)
349
+ end
350
+
351
+ s.start!
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,32 @@
1
+ ---
2
+ ldap_connection:
3
+ host: localhost
4
+ port: 1389
5
+
6
+ ldap_users:
7
+ base: dc=example,dc=com
8
+ filter: (&(cn=*)(sAMAccountName=*))
9
+ name_attribute: sAMAccountName
10
+
11
+ ldap_groups:
12
+ base: dc=example,dc=com
13
+ filter: (member=*)
14
+ name_attribute: cn
15
+ member_attribute: member
16
+
17
+ pg_connection:
18
+ dbname: postgres
19
+ # needed for postgres-pr:
20
+ # host: localhost
21
+ # port: 54321
22
+ # user: insert_your_username_here
23
+ # password:
24
+
25
+ pg_users:
26
+ filter: rolcanlogin AND NOT rolsuper
27
+ create_options: LOGIN
28
+
29
+ pg_groups:
30
+ filter: NOT rolcanlogin
31
+ create_options: NOLOGIN
32
+ grant_options:
@@ -0,0 +1,38 @@
1
+ ---
2
+ dc=example,dc=com:
3
+ cn:
4
+ - Top object
5
+ cn=Fred Flintstone,dc=example,dc=com:
6
+ cn:
7
+ - Fred Flintstone
8
+ mail:
9
+ - fred@bedrock.org
10
+ - fred.flintstone@bedrock.org
11
+ sn:
12
+ - Flintstone
13
+ sAMAccountName:
14
+ - fred
15
+ cn=Wilma Flintstone,dc=example,dc=com:
16
+ cn:
17
+ - Wilma Flintstone
18
+ mail:
19
+ - wilma@bedrock.org
20
+ sAMAccountName:
21
+ - wilma
22
+ cn=Flintstones,dc=example,dc=com:
23
+ cn:
24
+ - Flintstones
25
+ member:
26
+ - cn=Fred Flintstone,dc=example,dc=com
27
+ - cn=Wilma Flintstone,dc=example,dc=com
28
+ cn=Wilmas,dc=example,dc=com:
29
+ cn:
30
+ - Wilmas
31
+ member:
32
+ - cn=Wilma Flintstone,dc=example,dc=com
33
+ cn=All Users,dc=example,dc=com:
34
+ cn:
35
+ - All Users
36
+ member:
37
+ - cn=Wilmas,dc=example,dc=com
38
+ - cn=Fred Flintstone,dc=example,dc=com
@@ -0,0 +1,41 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # This is a trivial LDAP server which just stores directory entries in RAM.
4
+ # It does no validation or authentication. This is intended just to
5
+ # demonstrate the API, it's not for real-world use!!
6
+
7
+ require 'rubygems'
8
+ require 'ldap/server'
9
+
10
+ # We subclass the Operation class, overriding the methods to do what we need
11
+
12
+ class HashOperation < LDAP::Server::Operation
13
+ def initialize(connection, messageID, hash)
14
+ super(connection, messageID)
15
+ @hash = hash # an object reference to our directory data
16
+ end
17
+
18
+ def search(basedn, scope, deref, filter)
19
+ basedn.downcase!
20
+
21
+ case scope
22
+ when LDAP::Server::BaseObject
23
+ # client asked for single object by DN
24
+ obj = @hash[basedn]
25
+ raise LDAP::ResultError::NoSuchObject unless obj
26
+ send_SearchResultEntry(basedn, obj) if LDAP::Server::Filter.run(filter, obj)
27
+
28
+ when LDAP::Server::WholeSubtree
29
+ @hash.each do |dn, av|
30
+ next unless dn.index(basedn, -basedn.length) # under basedn?
31
+ next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
32
+ send_SearchResultEntry(dn, av)
33
+ end
34
+
35
+ else
36
+ raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
37
+
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "test/unit"
4
+ require "pg_ldap_sync/application"
5
+ require 'yaml'
6
+ require 'test/ldap_server'
7
+ require 'fileutils'
8
+
9
+ class TestPgLdapSync < Test::Unit::TestCase
10
+ def log_and_run( *cmd )
11
+ puts cmd.join(' ')
12
+ system( *cmd )
13
+ raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
14
+ end
15
+
16
+ def start_ldap_server
17
+ yaml_fname = File.join(File.dirname(__FILE__), "fixtures/ldapdb.yaml")
18
+ @directory = File.open(yaml_fname){|f| YAML::load(f.read) }
19
+
20
+ # Listen for incoming LDAP connections. For each one, create a Connection
21
+ # object, which will invoke a HashOperation object for each request.
22
+
23
+ @ldap_server = LDAP::Server.new(
24
+ :port => 1389,
25
+ :nodelay => true,
26
+ :listen => 10,
27
+ # :ssl_key_file => "key.pem",
28
+ # :ssl_cert_file => "cert.pem",
29
+ # :ssl_on_connect => true,
30
+ :operation_class => HashOperation,
31
+ :operation_args => [@directory]
32
+ )
33
+ @ldap_server.run_tcpserver
34
+ end
35
+
36
+ def stop_ldap_server
37
+ @ldap_server.stop
38
+ end
39
+
40
+ def start_pg_server
41
+ @port = 54321
42
+ ENV['PGPORT'] = @port.to_s
43
+ ENV['PGHOST'] = 'localhost'
44
+ unless File.exist?('temp/pg_data')
45
+ FileUtils.mkdir_p 'temp/pg_data'
46
+ log_and_run 'initdb', '-D', 'temp/pg_data'
47
+ end
48
+ log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start'
49
+ log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, wilma, \"Flintstones\", \"Wilmas\", \"All Users\"", 'postgres'
50
+ end
51
+
52
+ def stop_pg_server
53
+ log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'stop'
54
+ end
55
+
56
+ def setup
57
+ start_ldap_server
58
+ start_pg_server
59
+ end
60
+
61
+ def teardown
62
+ stop_ldap_server
63
+ stop_pg_server
64
+ end
65
+
66
+ def psqlre(*args)
67
+ /^\s*#{args[0]}[ |]*#{args[1]}[ |\{"]*#{args[2..-1].join('[", ]+')}["\}\s]*$/
68
+ end
69
+
70
+ def test_sanity
71
+ PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv])
72
+
73
+ ENV['LC_MESSAGES'] = 'C'
74
+ psql_du = `psql -c \\\\du postgres`
75
+ puts psql_du
76
+
77
+ assert_match(psqlre('All Users','Cannot login'), psql_du)
78
+ assert_match(psqlre('Flintstones','Cannot login'), psql_du)
79
+ assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du)
80
+ assert_match(psqlre('fred','','All Users','Flintstones'), psql_du)
81
+ assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du)
82
+
83
+ # revoke membership of 'wilma' to 'Flintstones'
84
+ @directory['cn=Flintstones,dc=example,dc=com']['member'].pop
85
+
86
+ PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv])
87
+ psql_du = `psql -c \\\\du postgres`
88
+ puts psql_du
89
+
90
+ assert_match(psqlre('All Users','Cannot login'), psql_du)
91
+ assert_match(psqlre('Flintstones','Cannot login'), psql_du)
92
+ assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du)
93
+ assert_match(psqlre('fred','','All Users','Flintstones'), psql_du)
94
+ assert_match(psqlre('wilma','','Wilmas'), psql_du)
95
+
96
+ # rename role 'wilma'
97
+ @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone']
98
+ # re-add 'Wilma' to 'Flintstones'
99
+ @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com'
100
+
101
+ PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv])
102
+ psql_du = `psql -c \\\\du postgres`
103
+ puts psql_du
104
+
105
+ assert_match(psqlre('All Users','Cannot login'), psql_du)
106
+ assert_match(psqlre('Flintstones','Cannot login'), psql_du)
107
+ assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du)
108
+ assert_match(psqlre('fred','','All Users','Flintstones'), psql_du)
109
+ assert_no_match(/wilma/, psql_du)
110
+ assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du)
111
+ end
112
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg-ldap-sync
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Lars Kanis
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-13 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: net-ldap
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 15
30
+ segments:
31
+ - 0
32
+ - 2
33
+ version: "0.2"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: kwalify
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 5
45
+ segments:
46
+ - 0
47
+ - 7
48
+ version: "0.7"
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: ruby-ldapserver
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 13
60
+ segments:
61
+ - 0
62
+ - 3
63
+ version: "0.3"
64
+ type: :development
65
+ version_requirements: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ name: hoe
68
+ prerelease: false
69
+ requirement: &id004 !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 47
75
+ segments:
76
+ - 2
77
+ - 8
78
+ - 0
79
+ version: 2.8.0
80
+ type: :development
81
+ version_requirements: *id004
82
+ description: |-
83
+ LDAP is often used to do a centralized user and role management
84
+ in an enterprise environment. PostgreSQL offers different
85
+ authentication methods, like LDAP, SSPI, GSSAPI or SSL.
86
+ However, for any method the user must already exist in the database,
87
+ before the authentication can be used. There is currently
88
+ no authorization of database users directly based on LDAP.
89
+
90
+ This program helps to solve the issue by synchronizing users,
91
+ groups and their memberships from LDAP to PostgreSQL.
92
+ Access to LDAP is read-only. <tt>pg_ldap_sync</tt> issues proper
93
+ CREATE ROLE, DROP ROLE, GRANT and REVOKE commands to synchronize
94
+ users and groups.
95
+
96
+ It is meant to be started as a cron job.
97
+ email:
98
+ - kanis@comcard.de
99
+ executables:
100
+ - pg_ldap_sync
101
+ extensions: []
102
+
103
+ extra_rdoc_files:
104
+ - History.txt
105
+ - Manifest.txt
106
+ - README.rdoc
107
+ files:
108
+ - .autotest
109
+ - History.txt
110
+ - Manifest.txt
111
+ - README.rdoc
112
+ - Rakefile
113
+ - bin/pg_ldap_sync
114
+ - config/sample-config.yaml
115
+ - config/sample-config2.yaml
116
+ - config/schema.yaml
117
+ - lib/pg_ldap_sync.rb
118
+ - lib/pg_ldap_sync/application.rb
119
+ - test/fixtures/config-ldapdb.yaml
120
+ - test/fixtures/ldapdb.yaml
121
+ - test/ldap_server.rb
122
+ - test/test_pg_ldap_sync.rb
123
+ has_rdoc: true
124
+ homepage: http://github.com/larskanis/pg-ldap-sync
125
+ licenses: []
126
+
127
+ post_install_message:
128
+ rdoc_options:
129
+ - --main
130
+ - README.rdoc
131
+ - --charset=UTF-8
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ hash: 3
140
+ segments:
141
+ - 0
142
+ version: "0"
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ hash: 3
149
+ segments:
150
+ - 0
151
+ version: "0"
152
+ requirements: []
153
+
154
+ rubyforge_project: pg-ldap-sync
155
+ rubygems_version: 1.3.7
156
+ signing_key:
157
+ specification_version: 3
158
+ summary: LDAP is often used to do a centralized user and role management in an enterprise environment
159
+ test_files:
160
+ - test/test_pg_ldap_sync.rb