From 06e1d46e2ffb8a22ee16ff1ed98fc4366a53c2e2 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 18 Oct 2016 15:14:55 -0700 Subject: [PATCH 1/9] Added the ability to have users and groups with the same names. Added support for case insentive dn's within LDAP. Lloyd Albin Database Administrator Statistical Center for HIV/AIDS Research and Prevention (SCHARP) Vaccine and Infectious Disease Division (VIDD) Fred Hutchinson Cancer Research Center (FHCRC) www.fredhutch.org --- History.txt | 5 ++ README.rdoc | 2 + config/sample-config3.yaml | 54 ++++++++++++++++++++++ lib/pg_ldap_sync.rb | 2 +- lib/pg_ldap_sync/application.rb | 75 +++++++++++++++++++++++++++++- test/fixtures/config-ldapdb.yaml | 5 +- test/fixtures/ldapdb.yaml | 78 ++++++++++++++++++++++++++++++-- test/test_pg_ldap_sync.rb | 51 ++++++++++++++++++--- 8 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 config/sample-config3.yaml diff --git a/History.txt b/History.txt index 34e4f4b..82d3349 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,8 @@ +=== 0.1.2 / 2016-10-18 + +* Add ability to have ldap groups and users with the same name merge into a single PG role +* Fixed to make dn's case insensitive as LDAP is case aware but not case sensitive + === 0.1.1 / 2012-11-15 * Add ability to lowercase the LDAP name for use as PG role name diff --git a/README.rdoc b/README.rdoc index 8852905..a9328bc 100644 --- a/README.rdoc +++ b/README.rdoc @@ -29,6 +29,8 @@ It is meant to be started as a cron job. * Runs with pg.gem (C-library) or postgres-pr.gem (pure Ruby) * Test mode which doesn't do any changes to the DBMS * Both LDAP and PG connections can be secured by SSL/TLS +* Supports LDAP users and groups with the same names +* Ability to have ldap groups and users with the same name merge into a single Postgres role == REQUIREMENTS: diff --git a/config/sample-config3.yaml b/config/sample-config3.yaml new file mode 100644 index 0000000..c415a40 --- /dev/null +++ b/config/sample-config3.yaml @@ -0,0 +1,54 @@ +# With this sample config the distinction between PG groups and users is +# done by the LOGIN for users and what roles have members for groups. +# Any non-superuser account is considered as LDAP-synchronized. + +# Connection parameters to LDAP server +# see also: http://net-ldap.rubyforge.org/Net/LDAP.html#method-c-new +ldap_connection: + host: localhost + port: 389 + auth: + method: :simple + username: CN=username,OU=!Serviceaccounts,OU=company,DC=company,DC=de + password: secret + +# Search parameters for LDAP users which should be synchronized +ldap_users: + base: OU=company,OU=company,DC=company,DC=de + # LDAP filter (according to RFC 2254) + # defines to users in LDAP to be synchronized + filter: (&(objectClass=person)(objectClass=organizationalPerson)(givenName=*)(sn=*)) + # this attribute is used as PG role name + name_attribute: sAMAccountName + +# Search parameters for LDAP groups which should be synchronized +ldap_groups: + base: OU=company,OU=company,DC=company,DC=de + filter: (|(cn=group1)(cn=group2)(cn=group3)) + # this attribute is used as PG role name + name_attribute: cn + # this attribute must reference to all member DN's of the given group + member_attribute: member + +# Connection parameters to PostgreSQL server +# see also: http://rubydoc.info/gems/pg/PG/Connection#initialize-instance_method +pg_connection: + host: + dbname: postgres + user: db-username + password: + +pg_users: + # Filter for identifying LDAP generated users in the database. + # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles" + filter: rolcanlogin AND NOT rolsuper + # Options for CREATE RULE statements + create_options: LOGIN + +pg_groups: + # Filter for identifying LDAP generated groups in the database and users who have members. + # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles" + filter: rolname IN (SELECT b.rolname FROM pg_auth_members a LEFT JOIN pg_authid b ON b.oid = a.roleid UNION SELECT rolname FROM pg_roles WHERE NOT rolcanlogin) AND NOT rolsuper + # Options for CREATE RULE statements + create_options: NOLOGIN + grant_options: diff --git a/lib/pg_ldap_sync.rb b/lib/pg_ldap_sync.rb index 205344d..c4e95f2 100644 --- a/lib/pg_ldap_sync.rb +++ b/lib/pg_ldap_sync.rb @@ -1,3 +1,3 @@ module PgLdapSync - VERSION = '0.1.1' + VERSION = '0.1.2' end diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index b0136bd..064cd05 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -79,6 +79,7 @@ def search_ldap_users users = [] res = @ldap.search(:base => ldap_user_conf[:base], :filter => ldap_user_conf[:filter]) do |entry| + entry.dn = entry.dn.downcase name = entry[ldap_user_conf[:name_attribute]].first unless name @@ -106,6 +107,7 @@ def search_ldap_groups groups = [] res = @ldap.search(:base => ldap_group_conf[:base], :filter => ldap_group_conf[:filter]) do |entry| + entry.dn = entry.dn.downcase name = entry[ldap_group_conf[:name_attribute]].first unless name @@ -115,7 +117,7 @@ def search_ldap_groups name.downcase! if ldap_group_conf[:lowercase_name] log.info "found group-dn: #{entry.dn}" - group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]] + group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]].map!(&:downcase) groups << group entry.each do |attribute, values| log.debug " #{attribute}:" @@ -211,6 +213,64 @@ def match_roles(ldaps, pgs, type) return roles end + def match_users_groups(roles) + + # Find out if there are matching users and groups + # Process Users + roles.each do |ru| + next if ru.type == :group + + # Find Matching Group, if any + roles.each do |rg| + next if rg.type == :user + next if rg.name != ru.name + + if ru.state == :create && rg.state == :keep + ru.state = :alter + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + elsif ru.state == :keep && rg.state == :create + rg.state = :keep + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :create && rg.state == :drop + ru.state = :alter + rg.state = :keep + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :create + rg.state = :alter + ru.state = :keep + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :keep + rg.state = :alter + ru.state = :keep + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :keep && rg.state == :drop + ru.state = :alter + rg.state = :keep + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :create && rg.state == :create + rg.state = :keep; + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :drop + rg.state = :keep + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + end + # The ru.state == :keep && rg.state == :keep we don't care about as no changes are needed + end + end + + log.info{ + "Revised user stat: create: #{roles.count{|r| r.state==:create && r.type==:user }} drop: #{roles.count{|r| r.state==:drop && r.type==:user }} alter: #{roles.count{|r| r.state==:alter && r.type==:user}} keep: #{roles.count{|r| r.state==:keep && r.type==:user}}" + } + log.info{ + "Revised group stat: create: #{roles.count{|r| r.state==:create && r.type==:group }} drop: #{roles.count{|r| r.state==:drop && r.type==:group }} alter: #{roles.count{|r| r.state==:alter && r.type==:group}} keep: #{roles.count{|r| r.state==:keep && r.type==:group}}" + } + return roles + end + def pg_exec_modify(sql) log.info{ "SQL: #{sql}" } unless self.test @@ -232,9 +292,19 @@ def drop_pg_role(role) pg_exec_modify "DROP ROLE \"#{role.name}\"" end + def alter_pg_user(role) + pg_exec_modify "ALTER ROLE \"#{role.name}\" LOGIN" + end + + def alter_pg_group(role) + pg_exec_modify "ALTER ROLE \"#{role.name}\" NOLOGIN" + end + def sync_roles_to_pg(roles, for_state) roles.sort{|a,b| a.name<=>b.name }.each do |role| create_pg_role(role) if role.state==:create && for_state==:create + alter_pg_user(role) if role.state==:alter && for_state==:alter && role.type==:user + alter_pg_group(role) if role.state==:alter && for_state==:alter && role.type==:group drop_pg_role(role) if role.state==:drop && for_state==:drop end end @@ -321,6 +391,7 @@ def start! # compare LDAP to PG users and groups mroles = match_roles(ldap_users, pg_users, :user) mroles += match_roles(ldap_groups, pg_groups, :group) + mroles = match_users_groups(mroles) # compare LDAP to PG memberships mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) @@ -328,6 +399,8 @@ def start! # drop/revoke roles/memberships first sync_membership_to_pg(mmemberships, :revoke) sync_roles_to_pg(mroles, :drop) + # Make Login if this used to be a non-login role and Non-Login if this used to be a login role + sync_roles_to_pg(mroles, :alter) # create/grant roles/memberships sync_roles_to_pg(mroles, :create) sync_membership_to_pg(mmemberships, :grant) diff --git a/test/fixtures/config-ldapdb.yaml b/test/fixtures/config-ldapdb.yaml index 7dd6c28..8d24031 100644 --- a/test/fixtures/config-ldapdb.yaml +++ b/test/fixtures/config-ldapdb.yaml @@ -9,7 +9,7 @@ ldap_users: name_attribute: sAMAccountName ldap_groups: - base: dc=example,dc=com + base: ou=groups,dc=example,dc=com filter: (member=*) name_attribute: cn member_attribute: member @@ -27,6 +27,7 @@ pg_users: create_options: LOGIN pg_groups: - filter: NOT rolcanlogin + #filter: NOT rolcanlogin + filter: rolname IN (SELECT b.rolname FROM pg_auth_members a LEFT JOIN pg_authid b ON b.oid = a.roleid UNION SELECT rolname FROM pg_roles WHERE NOT rolcanlogin) AND NOT rolsuper create_options: NOLOGIN grant_options: diff --git a/test/fixtures/ldapdb.yaml b/test/fixtures/ldapdb.yaml index e0eb4c3..4aa13f5 100644 --- a/test/fixtures/ldapdb.yaml +++ b/test/fixtures/ldapdb.yaml @@ -2,6 +2,9 @@ dc=example,dc=com: cn: - Top object +ou=groups,dc=example,dc=com: + cn: + - groups cn=Fred Flintstone,dc=example,dc=com: cn: - Fred Flintstone @@ -19,20 +22,85 @@ cn=Wilma Flintstone,dc=example,dc=com: - wilma@bedrock.org sAMAccountName: - wilma -cn=Flintstones,dc=example,dc=com: +cn=flintstones,ou=groups,dc=example,dc=com: cn: - Flintstones member: - - cn=Fred Flintstone,dc=example,dc=com + - cn=fred flintstone,dc=example,dc=com - cn=Wilma Flintstone,dc=example,dc=com -cn=Wilmas,dc=example,dc=com: +cn=Wilmas,ou=groups,dc=example,dc=com: cn: - Wilmas member: - cn=Wilma Flintstone,dc=example,dc=com -cn=All Users,dc=example,dc=com: +cn=All Users,ou=groups,dc=example,dc=com: cn: - All Users member: - - cn=Wilmas,dc=example,dc=com + - cn=Wilmas,ou=groups,dc=example,dc=com - cn=Fred Flintstone,dc=example,dc=com +cn=Betty Rubble,dc=example,dc=com: + cn: + - Betty Rubble + mail: + - betty@bedrock.org + - betty.rubble@bedrock.org + sAMAccountName: + - betty +cn=rubble,dc=example,dc=com: + cn: + - rubble + mail: + - rubble@bedrock.org + sAMAccountName: + - rubble +cn=rubble,ou=groups,dc=example,dc=com: + cn: + - rubble + member: + - cn=Betty Rubble,dc=example,dc=com +cn=Pebbles Flintstone,dc=example,dc=com: + cn: + - Pebbles Flintstone + mail: + - pebbles@bedrock.org + - pebbles.flintstone@bedrock.org + sAMAccountName: + - pebbles +cn=kids,dc=example,dc=com: + cn: + - kids + mail: + - kids@bedrock.org + sAMAccountName: + - kids +cn=kids,ou=groups,dc=example,dc=com: + cn: + - kids + member: + - cn=Pebbles Flintstone,dc=example,dc=com +cn=flintstone_kids,dc=example,dc=com: + cn: + - flintstone_kids + mail: + - flintstone_kids@bedrock.org + sAMAccountName: + - flintstone_kids +cn=flintstone_kids,ou=groups,dc=example,dc=com: + cn: + - flintstone_kids + member: + - cn=Pebbles Flintstone,dc=example,dc=com +cn=Bamm-Bamm Rubble,dc=example,dc=com: + cn: + - Bamm-Bamm Rubble + mail: + - bamm-bamm@bedrock.org + - bamm-bamm.rubble@bedrock.org + sAMAccountName: + - bamm-bamm +cn=rubble_kids,ou=groups,dc=example,dc=com: + cn: + - rubble_kids + member: + - cn=Bamm-Bamm Rubble,dc=example,dc=com diff --git a/test/test_pg_ldap_sync.rb b/test/test_pg_ldap_sync.rb index 4f41448..ffa2aee 100644 --- a/test/test_pg_ldap_sync.rb +++ b/test/test_pg_ldap_sync.rb @@ -46,7 +46,7 @@ def start_pg_server log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale' end log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start' - log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, wilma, \"Flintstones\", \"Wilmas\", \"All Users\"", 'postgres' + log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, wilma, betty, rubble, pebbles, kids, flintstone_kids, \"Flintstones\", \"Wilmas\", \"All Users\"", 'postgres' end def stop_pg_server @@ -86,11 +86,29 @@ def test_sanity assert_match(psqlre('All Users','Cannot login'), psql_du) assert_match(psqlre('Flintstones','Cannot login'), psql_du) assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('fred','','Flintstones','All Users'), psql_du) assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', ''), psql_du) + assert_match(psqlre('pebbles', '', 'kids', 'flintstone_kids'), psql_du) + assert_match(psqlre('kids', ''), psql_du) + assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) + assert_match(psqlre('rubble_kids','Cannot login'), psql_du) + assert_match(psqlre('flintstone_kids',''), psql_du) # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'].pop + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop + # remove user 'rubble' + @directory.delete('cn=rubble,dc=example,dc=com') + # reove user and group 'flintstone_kids' + @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') + @directory.delete('cn=flintstone_kids,dc=example,dc=com') + # remove group 'kids' + @directory.delete('cn=kids,ou=groups,dc=example,dc=com') + # remove group 'rubble_kids' + @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') + # add user 'rubble_kids' + @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) psql_du = exec_psql_du @@ -98,13 +116,28 @@ def test_sanity assert_match(psqlre('All Users','Cannot login'), psql_du) assert_match(psqlre('Flintstones','Cannot login'), psql_du) assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('fred','','Flintstones','All Users'), psql_du) assert_match(psqlre('wilma','','Wilmas'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', 'Cannot login'), psql_du) + assert_match(psqlre('pebbles', ''), psql_du) + assert_match(psqlre('kids', ''), psql_du) + assert_no_match(/flintstone_kids/, psql_du) + assert_match(psqlre('bamm-bamm', ''), psql_du) + assert_match(psqlre('rubble_kids', ''), psql_du) # rename role 'wilma' @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] # re-add 'Wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + #Recreate User 'rubble' + @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} + #Recreate Group 'kids' + @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} + # remove user 'rubble_kids' + @directory.delete('cn=rubble_kids,dc=example,dc=com') + #Recreate Group 'rubble_kids' + @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) psql_du = exec_psql_du @@ -112,8 +145,14 @@ def test_sanity assert_match(psqlre('All Users','Cannot login'), psql_du) assert_match(psqlre('Flintstones','Cannot login'), psql_du) assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('fred','','Flintstones','All Users'), psql_du) assert_no_match(/wilma/, psql_du) assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) + assert_match(psqlre('pebbles', '', 'kids'), psql_du) + assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', ''), psql_du) + assert_match(psqlre('rubble_kids', 'Cannot login'), psql_du) + assert_match(psqlre('kids', ''), psql_du) end end From 77a4cd94760ce3b182ea6e22d3009f22b32f862f Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 19 Oct 2016 10:52:17 -0700 Subject: [PATCH 2/9] Updated Manifest file --- Manifest.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Manifest.txt b/Manifest.txt index cda2561..681dd44 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -6,6 +6,7 @@ Rakefile bin/pg_ldap_sync config/sample-config.yaml config/sample-config2.yaml +config/sample-config3.yaml config/schema.yaml lib/pg_ldap_sync.rb lib/pg_ldap_sync/application.rb From e1e69538a4c7fa351d373b549c476699515061cd Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 20 Oct 2016 16:52:43 -0700 Subject: [PATCH 3/9] * Script will warn instead of die if role still owns objects in the database and can't be deleted from the database * Script will warn about duplicate role. You should only run into this the first time you run this script against an existing server and you are using the "ldap_users" and "ldap_groups". * Added testing for usage of "ldap_users" and "ldap_groups" group name * Added "grant_this_group" to the pg_users and pg_groups YAML config file to support moving a user to become group to become user and to auto create these groups if they do not exist --- History.txt | 4 + Manifest.txt | 5 + config/sample-config2.yaml | 4 + config/sample-config4.yaml | 65 ++++++++++ config/schema.yaml | 4 + lib/pg_ldap_sync/application.rb | 78 ++++++++++-- test/fixtures/config-ldapdb.yaml | 5 +- test/fixtures/config-ldapdb2.yaml | 34 +++++ test/fixtures/config-ldapdb3.yaml | 34 +++++ test/fixtures/config-ldapdb4.yaml | 34 +++++ test/fixtures/ldapdb.yaml | 80 +----------- test/fixtures/ldapdb3.yaml | 106 ++++++++++++++++ test/test_pg_ldap_sync.rb | 200 ++++++++++++++++++++++++++++-- 13 files changed, 555 insertions(+), 98 deletions(-) create mode 100644 config/sample-config4.yaml create mode 100644 test/fixtures/config-ldapdb2.yaml create mode 100644 test/fixtures/config-ldapdb3.yaml create mode 100644 test/fixtures/config-ldapdb4.yaml create mode 100644 test/fixtures/ldapdb3.yaml diff --git a/History.txt b/History.txt index 82d3349..a13ded5 100644 --- a/History.txt +++ b/History.txt @@ -2,6 +2,10 @@ * Add ability to have ldap groups and users with the same name merge into a single PG role * Fixed to make dn's case insensitive as LDAP is case aware but not case sensitive +* Script will warn instead of die if role still owns objects in the database and can't be deleted from the database +* Script will warn about duplicate role. You should only run into this the first time you run this script against an existing server and you are using the "ldap_users" and "ldap_groups". +* Added testing for usage of "ldap_users" and "ldap_groups" group name +* Added "grant_this_group" to the pg_users and pg_groups YAML config file to support moving a user to become group to become user and to auto create these groups if they do not exist === 0.1.1 / 2012-11-15 diff --git a/Manifest.txt b/Manifest.txt index 681dd44..b9f0961 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -7,10 +7,15 @@ bin/pg_ldap_sync config/sample-config.yaml config/sample-config2.yaml config/sample-config3.yaml +config/sample-config4.yaml config/schema.yaml lib/pg_ldap_sync.rb lib/pg_ldap_sync/application.rb test/fixtures/config-ldapdb.yaml +test/fixtures/config-ldapdb2.yaml +test/fixtures/config-ldapdb3.yaml +test/fixtures/config-ldapdb4.yaml test/fixtures/ldapdb.yaml +test/fixtures/ldapdb3.yaml test/ldap_server.rb test/test_pg_ldap_sync.rb diff --git a/config/sample-config2.yaml b/config/sample-config2.yaml index 080b512..926504b 100644 --- a/config/sample-config2.yaml +++ b/config/sample-config2.yaml @@ -51,6 +51,8 @@ pg_users: 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') # Options for CREATE RULE statements create_options: LOGIN IN ROLE ldap_users + # If you are using IN ROLE xxxx in the create_options, put that xxxx group name below + grant_this_group: ldap_users pg_groups: # Filter for identifying LDAP generated groups in the database. @@ -59,3 +61,5 @@ pg_groups: # Options for CREATE RULE statements create_options: NOLOGIN IN ROLE ldap_groups grant_options: + # If you are using IN ROLE xxxx in the create_options, put that xxxx group name below + grant_this_group: ldap_groups diff --git a/config/sample-config4.yaml b/config/sample-config4.yaml new file mode 100644 index 0000000..926504b --- /dev/null +++ b/config/sample-config4.yaml @@ -0,0 +1,65 @@ +# With this sample config the distinction between LDAP-synchronized +# groups/users from is done by the membership to ldap_user and +# ldap_group. These two roles has to be defined manally before +# pg_ldap_sync can run. + +# Connection parameters to LDAP server +# see also: http://net-ldap.rubyforge.org/Net/LDAP.html#method-c-new +ldap_connection: + host: ldapserver + port: 636 + auth: + method: :simple + username: CN=username,OU=!Serviceaccounts,OU=company,DC=company,DC=de + password: secret + encryption: + method: :simple_tls + +# Search parameters for LDAP users which should be synchronized +ldap_users: + base: OU=company,DC=company,DC=prod + # LDAP filter (according to RFC 2254) + # defines to users in LDAP to be synchronized + filter: (&(objectClass=person)(objectClass=organizationalPerson)(givenName=*)(sn=*)(sAMAccountName=*)) + # this attribute is used as PG role name + name_attribute: sAMAccountName + # lowercase name for use as PG role name + lowercase_name: true + +# Search parameters for LDAP groups which should be synchronized +ldap_groups: + base: OU=company,DC=company,DC=prod + filter: (cn=company.*) + # this attribute is used as PG role name + name_attribute: cn + # lowercase name for use as PG role name + lowercase_name: false + # this attribute must reference to all member DN's of the given group + member_attribute: member + +# Connection parameters to PostgreSQL server +# see also: http://rubydoc.info/gems/pg/PG/Connection#initialize-instance_method +pg_connection: + host: + dbname: postgres + user: + password: + +pg_users: + # Filter for identifying LDAP generated users in the database. + # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles" + 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') + # Options for CREATE RULE statements + create_options: LOGIN IN ROLE ldap_users + # If you are using IN ROLE xxxx in the create_options, put that xxxx group name below + grant_this_group: ldap_users + +pg_groups: + # Filter for identifying LDAP generated groups in the database. + # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles" + 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') + # Options for CREATE RULE statements + create_options: NOLOGIN IN ROLE ldap_groups + grant_options: + # If you are using IN ROLE xxxx in the create_options, put that xxxx group name below + grant_this_group: ldap_groups diff --git a/config/schema.yaml b/config/schema.yaml index 29fc256..e210e2d 100644 --- a/config/schema.yaml +++ b/config/schema.yaml @@ -54,6 +54,8 @@ mapping: required: yes "create_options": type: str + "grant_this_group": + type: str "pg_groups": type: map @@ -66,3 +68,5 @@ mapping: type: str "grant_options": type: str + "grant_this_group": + type: str diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index 064cd05..a2f5fb0 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -229,30 +229,30 @@ def match_users_groups(roles) ru.state = :alter log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } elsif ru.state == :keep && rg.state == :create - rg.state = :keep + rg.state = :group_add log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } elsif ru.state == :create && rg.state == :drop ru.state = :alter - rg.state = :keep + rg.state = :group_drop log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } elsif ru.state == :drop && rg.state == :create rg.state = :alter - ru.state = :keep + ru.state = :group_drop log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } elsif ru.state == :drop && rg.state == :keep rg.state = :alter - ru.state = :keep + ru.state = :group_drop log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } elsif ru.state == :keep && rg.state == :drop ru.state = :alter - rg.state = :keep + rg.state = :group_drop log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } elsif ru.state == :create && rg.state == :create - rg.state = :keep; + rg.state = :group_add; log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } elsif ru.state == :drop && rg.state == :drop rg.state = :keep @@ -274,7 +274,13 @@ def match_users_groups(roles) def pg_exec_modify(sql) log.info{ "SQL: #{sql}" } unless self.test - res = @pgconn.exec sql + begin + res = @pgconn.exec sql + rescue PG::DuplicateObject => dup + log.warn{ dup } + rescue PG::DependentObjectStillExists => dep + log.warn{ dep } + end end end @@ -294,17 +300,43 @@ def drop_pg_role(role) def alter_pg_user(role) pg_exec_modify "ALTER ROLE \"#{role.name}\" LOGIN" + set_pg_group(role) + revoke_pg_group(role) end def alter_pg_group(role) pg_exec_modify "ALTER ROLE \"#{role.name}\" NOLOGIN" + set_pg_group(role) + revoke_pg_group(role) + end + + def revoke_pg_group(role) + pg_conf = @config[role.type!=:user ? :pg_users : :pg_groups] + if pg_conf[:grant_this_group] != nil + pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" + end + end + + def set_pg_group(role) + pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] + if pg_conf[:grant_this_group] != nil + if role.state == :group_add || role.state == :alter + pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + else + pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" + end + end end def sync_roles_to_pg(roles, for_state) roles.sort{|a,b| a.name<=>b.name }.each do |role| create_pg_role(role) if role.state==:create && for_state==:create + set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:user + set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:user + set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:group + set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:group alter_pg_user(role) if role.state==:alter && for_state==:alter && role.type==:user - alter_pg_group(role) if role.state==:alter && for_state==:alter && role.type==:group + alter_pg_group(role) if role.state==:alter && for_state==:group && role.type==:group drop_pg_role(role) if role.state==:drop && for_state==:drop end end @@ -374,6 +406,31 @@ def sync_membership_to_pg(memberships, for_state) revoke_membership(role_name, members) if for_state==:revoke end end + + def check_groups() + pg_users_conf = @config[:pg_users] + if pg_users_conf[:grant_this_group] != nil + check_make_group(pg_users_conf[:grant_this_group]) + else + log.debug{"No Users LDAP group to Check/Create"} + end + pg_groups_conf = @config[:pg_groups] + if pg_groups_conf[:grant_this_group] != nil + check_make_group(pg_groups_conf[:grant_this_group]) + else + log.debug{"No Groups LDAP group to Check/Create"} + end + end + + def check_make_group(group_name) + res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{group_name}'" + if res[0] == nil + log.debug{ "Creating Group: #{group_name}"} + pg_exec_modify "CREATE ROLE \"#{group_name}\"" + else + log.info{ "Lookup Found Group: #{res[0][0]}"} + end + end def start! read_config_file(@config_fname) @@ -395,6 +452,9 @@ def start! # compare LDAP to PG memberships mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) + + # Check to see if grant_this_group exists and if not create the group + check_groups() # drop/revoke roles/memberships first sync_membership_to_pg(mmemberships, :revoke) @@ -403,6 +463,8 @@ def start! sync_roles_to_pg(mroles, :alter) # create/grant roles/memberships sync_roles_to_pg(mroles, :create) + # Fix group memberships + sync_roles_to_pg(mroles, :group) sync_membership_to_pg(mmemberships, :grant) @pgconn.close diff --git a/test/fixtures/config-ldapdb.yaml b/test/fixtures/config-ldapdb.yaml index 8d24031..7dd6c28 100644 --- a/test/fixtures/config-ldapdb.yaml +++ b/test/fixtures/config-ldapdb.yaml @@ -9,7 +9,7 @@ ldap_users: name_attribute: sAMAccountName ldap_groups: - base: ou=groups,dc=example,dc=com + base: dc=example,dc=com filter: (member=*) name_attribute: cn member_attribute: member @@ -27,7 +27,6 @@ pg_users: create_options: LOGIN pg_groups: - #filter: NOT rolcanlogin - filter: rolname IN (SELECT b.rolname FROM pg_auth_members a LEFT JOIN pg_authid b ON b.oid = a.roleid UNION SELECT rolname FROM pg_roles WHERE NOT rolcanlogin) AND NOT rolsuper + filter: NOT rolcanlogin create_options: NOLOGIN grant_options: diff --git a/test/fixtures/config-ldapdb2.yaml b/test/fixtures/config-ldapdb2.yaml new file mode 100644 index 0000000..0551393 --- /dev/null +++ b/test/fixtures/config-ldapdb2.yaml @@ -0,0 +1,34 @@ +--- +ldap_connection: + host: localhost + port: 1389 + +ldap_users: + base: dc=example,dc=com + filter: (&(cn=*)(sAMAccountName=*)) + name_attribute: sAMAccountName + +ldap_groups: + base: dc=example,dc=com + filter: (member=*) + name_attribute: cn + member_attribute: member + +pg_connection: + dbname: postgres + host: localhost + port: 54321 +# needed for postgres-pr: +# user: insert_your_username_here +# password: + +pg_users: + 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') + create_options: LOGIN IN ROLE ldap_users + grant_this_group: ldap_users + +pg_groups: + 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') + create_options: NOLOGIN IN ROLE ldap_groups + grant_options: + grant_this_group: ldap_groups diff --git a/test/fixtures/config-ldapdb3.yaml b/test/fixtures/config-ldapdb3.yaml new file mode 100644 index 0000000..39defde --- /dev/null +++ b/test/fixtures/config-ldapdb3.yaml @@ -0,0 +1,34 @@ +--- +ldap_connection: + host: localhost + port: 1389 + +ldap_users: + base: dc=example,dc=com + filter: (&(cn=*)(sAMAccountName=*)) + name_attribute: sAMAccountName + +ldap_groups: + base: ou=groups,dc=example,dc=com + filter: (member=*) + name_attribute: cn + member_attribute: member + +pg_connection: + dbname: postgres + host: localhost + port: 54321 +# needed for postgres-pr: +# user: insert_your_username_here +# password: + +pg_users: + filter: rolcanlogin AND NOT rolsuper + create_options: LOGIN + grant_this_group: + +pg_groups: + filter: rolname IN (SELECT b.rolname FROM pg_auth_members a LEFT JOIN pg_authid b ON b.oid = a.roleid UNION SELECT rolname FROM pg_roles WHERE NOT rolcanlogin) AND NOT rolsuper + create_options: NOLOGIN + grant_options: + grant_this_group: diff --git a/test/fixtures/config-ldapdb4.yaml b/test/fixtures/config-ldapdb4.yaml new file mode 100644 index 0000000..23714ee --- /dev/null +++ b/test/fixtures/config-ldapdb4.yaml @@ -0,0 +1,34 @@ +--- +ldap_connection: + host: localhost + port: 1389 + +ldap_users: + base: dc=example,dc=com + filter: (&(cn=*)(sAMAccountName=*)) + name_attribute: sAMAccountName + +ldap_groups: + base: ou=groups,dc=example,dc=com + filter: (member=*) + name_attribute: cn + member_attribute: member + +pg_connection: + dbname: postgres + host: localhost + port: 54321 +# needed for postgres-pr: +# user: insert_your_username_here +# password: + +pg_users: + 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') + create_options: LOGIN IN ROLE ldap_users + grant_this_group: ldap_users + +pg_groups: + 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') + create_options: NOLOGIN IN ROLE ldap_groups + grant_options: + grant_this_group: ldap_groups diff --git a/test/fixtures/ldapdb.yaml b/test/fixtures/ldapdb.yaml index 4aa13f5..772220b 100644 --- a/test/fixtures/ldapdb.yaml +++ b/test/fixtures/ldapdb.yaml @@ -2,9 +2,6 @@ dc=example,dc=com: cn: - Top object -ou=groups,dc=example,dc=com: - cn: - - groups cn=Fred Flintstone,dc=example,dc=com: cn: - Fred Flintstone @@ -22,85 +19,20 @@ cn=Wilma Flintstone,dc=example,dc=com: - wilma@bedrock.org sAMAccountName: - wilma -cn=flintstones,ou=groups,dc=example,dc=com: +cn=Flintstones,dc=example,dc=com: cn: - Flintstones member: - - cn=fred flintstone,dc=example,dc=com + - cn=Fred Flintstone,dc=example,dc=com - cn=Wilma Flintstone,dc=example,dc=com -cn=Wilmas,ou=groups,dc=example,dc=com: +cn=Wilmas,dc=example,dc=com: cn: - Wilmas member: - cn=Wilma Flintstone,dc=example,dc=com -cn=All Users,ou=groups,dc=example,dc=com: +cn=All Users,dc=example,dc=com: cn: - All Users member: - - cn=Wilmas,ou=groups,dc=example,dc=com - - cn=Fred Flintstone,dc=example,dc=com -cn=Betty Rubble,dc=example,dc=com: - cn: - - Betty Rubble - mail: - - betty@bedrock.org - - betty.rubble@bedrock.org - sAMAccountName: - - betty -cn=rubble,dc=example,dc=com: - cn: - - rubble - mail: - - rubble@bedrock.org - sAMAccountName: - - rubble -cn=rubble,ou=groups,dc=example,dc=com: - cn: - - rubble - member: - - cn=Betty Rubble,dc=example,dc=com -cn=Pebbles Flintstone,dc=example,dc=com: - cn: - - Pebbles Flintstone - mail: - - pebbles@bedrock.org - - pebbles.flintstone@bedrock.org - sAMAccountName: - - pebbles -cn=kids,dc=example,dc=com: - cn: - - kids - mail: - - kids@bedrock.org - sAMAccountName: - - kids -cn=kids,ou=groups,dc=example,dc=com: - cn: - - kids - member: - - cn=Pebbles Flintstone,dc=example,dc=com -cn=flintstone_kids,dc=example,dc=com: - cn: - - flintstone_kids - mail: - - flintstone_kids@bedrock.org - sAMAccountName: - - flintstone_kids -cn=flintstone_kids,ou=groups,dc=example,dc=com: - cn: - - flintstone_kids - member: - - cn=Pebbles Flintstone,dc=example,dc=com -cn=Bamm-Bamm Rubble,dc=example,dc=com: - cn: - - Bamm-Bamm Rubble - mail: - - bamm-bamm@bedrock.org - - bamm-bamm.rubble@bedrock.org - sAMAccountName: - - bamm-bamm -cn=rubble_kids,ou=groups,dc=example,dc=com: - cn: - - rubble_kids - member: - - cn=Bamm-Bamm Rubble,dc=example,dc=com + - cn=Wilmas,dc=example,dc=com + - cn=Fred Flintstone,dc=example,dc=com \ No newline at end of file diff --git a/test/fixtures/ldapdb3.yaml b/test/fixtures/ldapdb3.yaml new file mode 100644 index 0000000..4aa13f5 --- /dev/null +++ b/test/fixtures/ldapdb3.yaml @@ -0,0 +1,106 @@ +--- +dc=example,dc=com: + cn: + - Top object +ou=groups,dc=example,dc=com: + cn: + - groups +cn=Fred Flintstone,dc=example,dc=com: + cn: + - Fred Flintstone + mail: + - fred@bedrock.org + - fred.flintstone@bedrock.org + sn: + - Flintstone + sAMAccountName: + - fred +cn=Wilma Flintstone,dc=example,dc=com: + cn: + - Wilma Flintstone + mail: + - wilma@bedrock.org + sAMAccountName: + - wilma +cn=flintstones,ou=groups,dc=example,dc=com: + cn: + - Flintstones + member: + - cn=fred flintstone,dc=example,dc=com + - cn=Wilma Flintstone,dc=example,dc=com +cn=Wilmas,ou=groups,dc=example,dc=com: + cn: + - Wilmas + member: + - cn=Wilma Flintstone,dc=example,dc=com +cn=All Users,ou=groups,dc=example,dc=com: + cn: + - All Users + member: + - cn=Wilmas,ou=groups,dc=example,dc=com + - cn=Fred Flintstone,dc=example,dc=com +cn=Betty Rubble,dc=example,dc=com: + cn: + - Betty Rubble + mail: + - betty@bedrock.org + - betty.rubble@bedrock.org + sAMAccountName: + - betty +cn=rubble,dc=example,dc=com: + cn: + - rubble + mail: + - rubble@bedrock.org + sAMAccountName: + - rubble +cn=rubble,ou=groups,dc=example,dc=com: + cn: + - rubble + member: + - cn=Betty Rubble,dc=example,dc=com +cn=Pebbles Flintstone,dc=example,dc=com: + cn: + - Pebbles Flintstone + mail: + - pebbles@bedrock.org + - pebbles.flintstone@bedrock.org + sAMAccountName: + - pebbles +cn=kids,dc=example,dc=com: + cn: + - kids + mail: + - kids@bedrock.org + sAMAccountName: + - kids +cn=kids,ou=groups,dc=example,dc=com: + cn: + - kids + member: + - cn=Pebbles Flintstone,dc=example,dc=com +cn=flintstone_kids,dc=example,dc=com: + cn: + - flintstone_kids + mail: + - flintstone_kids@bedrock.org + sAMAccountName: + - flintstone_kids +cn=flintstone_kids,ou=groups,dc=example,dc=com: + cn: + - flintstone_kids + member: + - cn=Pebbles Flintstone,dc=example,dc=com +cn=Bamm-Bamm Rubble,dc=example,dc=com: + cn: + - Bamm-Bamm Rubble + mail: + - bamm-bamm@bedrock.org + - bamm-bamm.rubble@bedrock.org + sAMAccountName: + - bamm-bamm +cn=rubble_kids,ou=groups,dc=example,dc=com: + cn: + - rubble_kids + member: + - cn=Bamm-Bamm Rubble,dc=example,dc=com diff --git a/test/test_pg_ldap_sync.rb b/test/test_pg_ldap_sync.rb index ffa2aee..da1bfdb 100644 --- a/test/test_pg_ldap_sync.rb +++ b/test/test_pg_ldap_sync.rb @@ -13,8 +13,8 @@ def log_and_run( *cmd ) raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? end - def start_ldap_server - yaml_fname = File.join(File.dirname(__FILE__), "fixtures/ldapdb.yaml") + def start_ldap_server(file_name) + yaml_fname = File.join(File.dirname(__FILE__), file_name) @directory = File.open(yaml_fname){|f| YAML::load(f.read) } # Listen for incoming LDAP connections. For each one, create a Connection @@ -46,7 +46,7 @@ def start_pg_server log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale' end log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start' - log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, wilma, betty, rubble, pebbles, kids, flintstone_kids, \"Flintstones\", \"Wilmas\", \"All Users\"", 'postgres' + log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, \"bamm-bamm\", rubble_kids, wilma, \"Wilma Flintstone\", betty, rubble, pebbles, kids, flintstone_kids, \"Flintstones\", \"Wilmas\", \"All Users\", ldap_users, ldap_groups", 'postgres' end def stop_pg_server @@ -54,7 +54,6 @@ def stop_pg_server end def setup - start_ldap_server start_pg_server end @@ -69,15 +68,18 @@ def psqlre(*args) def exec_psql_du text = if RUBY_PLATFORM=~/mingw|mswin/ - `psql -c \\du postgres` + `psql -d postgres -c "SELECT r.rolname AS \\"Role name\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\"Attributes\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\"Member of\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` else - `psql -c \\\\du postgres` + `psql -d postgres -c "SELECT r.rolname AS \\\"Role name\\\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\\"Attributes\\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\\"Member of\\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` end puts text return text end - def test_sanity +#=begin + def test_sanity1 + start_ldap_server("fixtures/ldapdb.yaml") + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) ENV['LC_MESSAGES'] = 'C' @@ -86,11 +88,99 @@ def test_sanity assert_match(psqlre('All Users','Cannot login'), psql_du) assert_match(psqlre('Flintstones','Cannot login'), psql_du) assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','Flintstones','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'].pop + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Wilmas'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) + end +#=end + +#=begin + def test_sanity2 + start_ldap_server("fixtures/ldapdb.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'].pop + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) + end +#=end + +#=begin + def test_sanity3 + start_ldap_server("fixtures/ldapdb3.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) assert_match(psqlre('betty', '', 'rubble'), psql_du) assert_match(psqlre('rubble', ''), psql_du) - assert_match(psqlre('pebbles', '', 'kids', 'flintstone_kids'), psql_du) + assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids'), psql_du) assert_match(psqlre('kids', ''), psql_du) assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) assert_match(psqlre('rubble_kids','Cannot login'), psql_du) @@ -110,13 +200,13 @@ def test_sanity # add user 'rubble_kids' @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) psql_du = exec_psql_du assert_match(psqlre('All Users','Cannot login'), psql_du) assert_match(psqlre('Flintstones','Cannot login'), psql_du) assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','Flintstones','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) assert_match(psqlre('wilma','','Wilmas'), psql_du) assert_match(psqlre('betty', '', 'rubble'), psql_du) assert_match(psqlre('rubble', 'Cannot login'), psql_du) @@ -139,13 +229,13 @@ def test_sanity #Recreate Group 'rubble_kids' @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) psql_du = exec_psql_du assert_match(psqlre('All Users','Cannot login'), psql_du) assert_match(psqlre('Flintstones','Cannot login'), psql_du) assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','Flintstones','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) assert_no_match(/wilma/, psql_du) assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) assert_match(psqlre('pebbles', '', 'kids'), psql_du) @@ -155,4 +245,88 @@ def test_sanity assert_match(psqlre('rubble_kids', 'Cannot login'), psql_du) assert_match(psqlre('kids', ''), psql_du) end +#=end + +#=begin + def test_sanity4 + start_ldap_server("fixtures/ldapdb3.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) + assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids','ldap_users'), psql_du) + assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('bamm-bamm', '','ldap_users', 'rubble_kids'), psql_du) + assert_match(psqlre('rubble_kids','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('flintstone_kids','','ldap_groups','ldap_users'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop + # remove user 'rubble' + @directory.delete('cn=rubble,dc=example,dc=com') + # reove user and group 'flintstone_kids' + @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') + @directory.delete('cn=flintstone_kids,dc=example,dc=com') + # remove group 'kids' + @directory.delete('cn=kids,ou=groups,dc=example,dc=com') + # remove group 'rubble_kids' + @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') + # add user 'rubble_kids' + @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users', 'ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones', 'ldap_users'), psql_du) + assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) + assert_match(psqlre('rubble', 'Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('pebbles', '','ldap_users'), psql_du) + assert_match(psqlre('kids', '','ldap_users'), psql_du) + assert_no_match(/flintstone_kids/, psql_du) + assert_match(psqlre('bamm-bamm', '','ldap_users'), psql_du) + assert_match(psqlre('rubble_kids', '','ldap_users'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + #Recreate User 'rubble' + @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} + #Recreate Group 'kids' + @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} + # remove user 'rubble_kids' + @directory.delete('cn=rubble_kids,dc=example,dc=com') + #Recreate Group 'rubble_kids' + @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('pebbles', '', 'kids','ldap_users'), psql_du) + assert_match(psqlre('bamm-bamm', '', 'ldap_users','rubble_kids'), psql_du) + assert_match(psqlre('betty', '', 'ldap_users','rubble'), psql_du) + assert_match(psqlre('rubble', '','ldap_users'), psql_du) + assert_match(psqlre('rubble_kids', 'Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) + end +#=end end From 5833f30e4c389e4490ba5eb32903edb896578787 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 21 Oct 2016 14:58:37 -0700 Subject: [PATCH 4/9] Fixed warnings about not being able to revoke/grant when using ldap_users and ldap_groups and your are switching from user to group to user. --- lib/pg_ldap_sync/application.rb | 57 +++++++++++++++++++++++++++------ test/test_pg_ldap_sync.rb | 2 +- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index a2f5fb0..b5531c5 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -157,6 +157,37 @@ def search_pg_groups log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} groups << group end + + return groups + end + + def search_pg_ldap_groups + pg_users_conf = @config[:pg_users] + pg_groups_conf = @config[:pg_groups] + + in_group = "" + if pg_groups_conf[:grant_this_group] != nil + in_group += "'#{pg_groups_conf[:grant_this_group]}'" + end + if pg_users_conf[:grant_this_group] != nil + if in_group != "" + in_group += ", " + end + in_group += "'#{pg_users_conf[:grant_this_group]}'" + end + if pg_groups_conf[:grant_this_group] != nil || pg_users_conf[:grant_this_group] != nil + groups = [] + res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE rolname IN (#{in_group})" + res.each do |tuple| + 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])}" + member_names = res2.map{|row| row[0] } + group = PgRole.new tuple[0], member_names + log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} + groups << group + end + groups = uniq_names groups + end + return groups end @@ -301,27 +332,32 @@ def drop_pg_role(role) def alter_pg_user(role) pg_exec_modify "ALTER ROLE \"#{role.name}\" LOGIN" set_pg_group(role) - revoke_pg_group(role) end def alter_pg_group(role) pg_exec_modify "ALTER ROLE \"#{role.name}\" NOLOGIN" set_pg_group(role) - revoke_pg_group(role) - end - - def revoke_pg_group(role) - pg_conf = @config[role.type!=:user ? :pg_users : :pg_groups] - if pg_conf[:grant_this_group] != nil - pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" - end end def set_pg_group(role) pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] if pg_conf[:grant_this_group] != nil if role.state == :group_add || role.state == :alter - pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + skip_grant = :false + if !@pg_ldap_groups.empty? + @pg_ldap_groups.each do |group| + if group.name == pg_conf[:grant_this_group] + group.member_names.each do |member| + if member == role.name + skip_grant = :true + end + end + end + end + end + if skip_grant == :false + pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + end else pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" end @@ -444,6 +480,7 @@ def start! @pgconn = PGconn.connect @config[:pg_connection] pg_users = uniq_names search_pg_users pg_groups = uniq_names search_pg_groups + @pg_ldap_groups = search_pg_ldap_groups # compare LDAP to PG users and groups mroles = match_roles(ldap_users, pg_users, :user) diff --git a/test/test_pg_ldap_sync.rb b/test/test_pg_ldap_sync.rb index da1bfdb..b5bf12e 100644 --- a/test/test_pg_ldap_sync.rb +++ b/test/test_pg_ldap_sync.rb @@ -324,7 +324,7 @@ def test_sanity4 assert_match(psqlre('pebbles', '', 'kids','ldap_users'), psql_du) assert_match(psqlre('bamm-bamm', '', 'ldap_users','rubble_kids'), psql_du) assert_match(psqlre('betty', '', 'ldap_users','rubble'), psql_du) - assert_match(psqlre('rubble', '','ldap_users'), psql_du) + assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) assert_match(psqlre('rubble_kids', 'Cannot login', 'ldap_groups'), psql_du) assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) end From be3b72ec08ca2363970d169eaddc5410440b23bc Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 26 Oct 2016 16:54:30 -0700 Subject: [PATCH 5/9] Added capability user the group names on a system that already has users and groups without any errors or warnings. --- lib/pg_ldap_sync/application.rb | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index b5531c5..81df48a 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -322,7 +322,16 @@ def pg_exec(sql) def create_pg_role(role) pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] - pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" + if pg_conf[:grant_this_group] != nil + res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{role.name}'" + if res[0] == nil + pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" + else + pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + end + else + pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" + end end def drop_pg_role(role) @@ -502,6 +511,16 @@ def start! sync_roles_to_pg(mroles, :create) # Fix group memberships sync_roles_to_pg(mroles, :group) + + # Reload users and group in case they had been created but were not within the proper "grant_this_group" + pg_users = uniq_names search_pg_users + pg_groups = uniq_names search_pg_groups + @pg_ldap_groups = search_pg_ldap_groups + + # compare reloaded LDAP to PG memberships + mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) + + # The reload keeps the grants from throwing warnings about role already being part of the group sync_membership_to_pg(mmemberships, :grant) @pgconn.close From 40685053b5b862d8c05d622d0a49583478050878 Mon Sep 17 00:00:00 2001 From: Lloyd Albin Date: Fri, 3 Aug 2018 14:28:32 -0700 Subject: [PATCH 6/9] * Add pg_ldap_sync --version * Fix compatibility with PostgreSQL-10 * Don't abort on SQL errors, but print ERROR notice * Run sync within a SQL transaction, so that no partial sync happens * Todo: Add Larskanis updates to the test suite and add the automated tests. --- .autotest | 46 +- Gemfile | 4 + History.txt | 53 +- Manifest.txt | 44 +- README.rdoc | 206 +++--- Rakefile | 29 +- bin/pg_ldap_sync | 6 - exe/pg_ldap_sync | 9 + lib/pg_ldap_sync.rb | 24 +- lib/pg_ldap_sync/application.rb | 1091 +++++++++++++++---------------- lib/pg_ldap_sync/logger.rb | 24 + lib/pg_ldap_sync/version.rb | 3 + pg-ldap-sync.gemspec | 31 + test/test_pg_ldap_sync.rb | 667 +++++++++---------- 14 files changed, 1157 insertions(+), 1080 deletions(-) create mode 100644 Gemfile delete mode 100755 bin/pg_ldap_sync create mode 100755 exe/pg_ldap_sync create mode 100644 lib/pg_ldap_sync/logger.rb create mode 100644 lib/pg_ldap_sync/version.rb create mode 100644 pg-ldap-sync.gemspec diff --git a/.autotest b/.autotest index ef753ad..ae8d4b7 100644 --- a/.autotest +++ b/.autotest @@ -1,23 +1,23 @@ -# -*- ruby -*- - -require 'autotest/restart' - -# Autotest.add_hook :initialize do |at| -# at.extra_files << "../some/external/dependency.rb" -# -# at.libs << ":../some/external" -# -# at.add_exception 'vendor' -# -# at.add_mapping(/dependency.rb/) do |f, _| -# at.files_matching(/test_.*rb$/) -# end -# -# %w(TestA TestB).each do |klass| -# at.extra_class_map[klass] = "test/test_misc.rb" -# end -# end - -# Autotest.add_hook :run_command do |at| -# system "rake build" -# end +# -*- ruby -*- + +require 'autotest/restart' + +# Autotest.add_hook :initialize do |at| +# at.extra_files << "../some/external/dependency.rb" +# +# at.libs << ":../some/external" +# +# at.add_exception 'vendor' +# +# at.add_mapping(/dependency.rb/) do |f, _| +# at.files_matching(/test_.*rb$/) +# end +# +# %w(TestA TestB).each do |klass| +# at.extra_class_map[klass] = "test/test_misc.rb" +# end +# end + +# Autotest.add_hook :run_command do |at| +# system "rake build" +# end diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..969b379 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in pg_ldap_sync.gemspec +gemspec \ No newline at end of file diff --git a/History.txt b/History.txt index a13ded5..4472cdf 100644 --- a/History.txt +++ b/History.txt @@ -1,17 +1,36 @@ -=== 0.1.2 / 2016-10-18 - -* Add ability to have ldap groups and users with the same name merge into a single PG role -* Fixed to make dn's case insensitive as LDAP is case aware but not case sensitive -* Script will warn instead of die if role still owns objects in the database and can't be deleted from the database -* Script will warn about duplicate role. You should only run into this the first time you run this script against an existing server and you are using the "ldap_users" and "ldap_groups". -* Added testing for usage of "ldap_users" and "ldap_groups" group name -* Added "grant_this_group" to the pg_users and pg_groups YAML config file to support moving a user to become group to become user and to auto create these groups if they do not exist - -=== 0.1.1 / 2012-11-15 - -* Add ability to lowercase the LDAP name for use as PG role name - -=== 0.1.0 / 2011-07-13 - -* Birthday! - +=== 0.2.1 / 2018-08-03 (Lloyd) +* Add pg_ldap_sync --version +* Fix compatibility with PostgreSQL-10 +* Don't abort on SQL errors, but print ERROR notice +* Run sync within a SQL transaction, so that no partial sync happens +* Todo: Add Larskanis updates to the test suite and add the automated tests. + +=== 0.2.0 / 2018-03-13 (Larskanis) + +* Update gem dependencies +* Fix compatibility to pg-1.0 gem +* Add pg_ldap_sync --version +* Fix compatibility with PostgreSQL-10 +* Don't abort on SQL errors, but print ERROR notice +* Run sync within a SQL transaction, so that no partial sync happens +* Lots of improvements to the test suite +* Run automated tests on Travis-CI and Appveyor +* Remove support for postgres-pr, since it's no longer maintained + +=== 0.1.2 / 2016-10-18 (Lloyd) + +* Add ability to have ldap groups and users with the same name merge into a single PG role +* Fixed to make dn's case insensitive as LDAP is case aware but not case sensitive +* Script will warn instead of die if role still owns objects in the database and can't be deleted from the database +* Script will warn about duplicate role. You should only run into this the first time you run this script against an existing server and you are using the "ldap_users" and "ldap_groups". +* Added testing for usage of "ldap_users" and "ldap_groups" group name +* Added "grant_this_group" to the pg_users and pg_groups YAML config file to support moving a user to become group to become user and to auto create these groups if they do not exist + +=== 0.1.1 / 2012-11-15 + +* Add ability to lowercase the LDAP name for use as PG role name + +=== 0.1.0 / 2011-07-13 + +* Birthday! + diff --git a/Manifest.txt b/Manifest.txt index b9f0961..8d627ec 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -1,21 +1,23 @@ -.autotest -History.txt -Manifest.txt -README.rdoc -Rakefile -bin/pg_ldap_sync -config/sample-config.yaml -config/sample-config2.yaml -config/sample-config3.yaml -config/sample-config4.yaml -config/schema.yaml -lib/pg_ldap_sync.rb -lib/pg_ldap_sync/application.rb -test/fixtures/config-ldapdb.yaml -test/fixtures/config-ldapdb2.yaml -test/fixtures/config-ldapdb3.yaml -test/fixtures/config-ldapdb4.yaml -test/fixtures/ldapdb.yaml -test/fixtures/ldapdb3.yaml -test/ldap_server.rb -test/test_pg_ldap_sync.rb +.autotest +History.txt +Manifest.txt +README.rdoc +Rakefile +exe/pg_ldap_sync +config/sample-config.yaml +config/sample-config2.yaml +config/sample-config3.yaml +config/sample-config4.yaml +config/schema.yaml +lib/pg_ldap_sync.rb +lib/pg_ldap_sync/application.rb +lib/pg_ldap_sync/logger.rb +lib/pg_ldap_sync/version.rb +test/fixtures/config-ldapdb.yaml +test/fixtures/config-ldapdb2.yaml +test/fixtures/config-ldapdb3.yaml +test/fixtures/config-ldapdb4.yaml +test/fixtures/ldapdb.yaml +test/fixtures/ldapdb3.yaml +test/ldap_server.rb +test/test_pg_ldap_sync.rb diff --git a/README.rdoc b/README.rdoc index a9328bc..0f6d958 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,113 +1,93 @@ -= Use LDAP permissions in PostgreSQL - -* http://github.com/larskanis/pg-ldap-sync - -== DESCRIPTION: - -LDAP is often used for a centralized user and role management -in an enterprise environment. PostgreSQL offers different -authentication methods, like LDAP, SSPI, GSSAPI or SSL. -However, for any method the user must already exist in the database, -before the authentication can be used. There is currently -no direct authorization of database users on LDAP. So roles -and memberships has to be administered twice. - -This program helps to solve the issue by synchronizing users, -groups and their memberships from LDAP to PostgreSQL. -Access to LDAP is used read-only. pg_ldap_sync issues proper -CREATE ROLE, DROP ROLE, GRANT and REVOKE commands to synchronize -users and groups. - -It is meant to be started as a cron job. - -== FEATURES: - -* Configurable per YAML config file -* Can use Active Directory as LDAP-Server -* Nested groups/roles supported -* Set scope of considered users/groups on LDAP and PG side -* Runs with pg.gem (C-library) or postgres-pr.gem (pure Ruby) -* Test mode which doesn't do any changes to the DBMS -* Both LDAP and PG connections can be secured by SSL/TLS -* Supports LDAP users and groups with the same names -* Ability to have ldap groups and users with the same name merge into a single Postgres role - -== REQUIREMENTS: - -* Ruby-1.8.7, Ruby-1.9.2, JRuby-1.2, Rubinius-1.2 or better -* Rubygems-1.3.5+ -* LDAP-v3 server -* PostgreSQL-server v8.1+ - -== INSTALL: - -Install Ruby and rubygems: -* on Windows: http://rubyinstaller.org -* on Debian/Ubuntu: apt-get install ruby rubygems - -Install pg-ldap-sync and a database connector for PostgreSQL: - gem install pg-ldap-sync pg -You may also use the pure ruby postgres-connector which is less mature, -but doesn't need compilation: - gem install pg-ldap-sync postgres-pr - -=== Install from Git: - git clone https://github.com/larskanis/pg-ldap-sync.git - cd pg-ldap-sync - gem install hoe - rake install_gem - -== USAGE: - -Create a config file based on -{config/sample-config.yaml}[https://github.com/larskanis/pg-ldap-sync/blob/master/config/sample-config.yaml] -or even better -{config/sample-config2.yaml}[https://github.com/larskanis/pg-ldap-sync/blob/master/config/sample-config2.yaml] - -Run in test-mode: - - pg_ldap_sync -c my_config.yaml -vv -t - -Run in modify-mode: - - pg_ldap_sync -c my_config.yaml -vv - - -== TEST: -There is a small test suite in the test directory that runs -against an internal ruby-ldapserver and PostgreSQL server. Ensure gem -ruby-ldapserver is installed and pg_ctl, initdb and psql -commands are in the PATH. Then: - - cd pg-ldap-sync - rake test - -== ISSUES: -* There is currently no way to set certain user attributes in PG - based on individual attributes in LDAP (expiration date etc.) - - -== LICENSE: - -(The MIT License) - -Copyright (c) 2011 FIX - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. += Use LDAP permissions in PostgreSQL + +* http://github.com/larskanis/pg-ldap-sync + +== DESCRIPTION: + +LDAP is often used for a centralized user and role management +in an enterprise environment. PostgreSQL offers different +authentication methods, like LDAP, SSPI, GSSAPI or SSL. +However, for any method the user must already exist in the database, +before the authentication can be used. There is currently +no direct authorization of database users on LDAP. So roles +and memberships has to be administered twice. + +This program helps to solve the issue by synchronizing users, +groups and their memberships from LDAP to PostgreSQL. +Access to LDAP is used read-only. pg_ldap_sync issues proper +CREATE ROLE, DROP ROLE, GRANT and REVOKE commands to synchronize +users and groups. + +It is meant to be started as a cron job. + +== FEATURES: + +* Configurable per YAML config file +* Can use Active Directory as LDAP-Server +* Nested groups/roles supported +* Set scope of considered users/groups on LDAP and PG side +* Runs with pg.gem (C-library) or postgres-pr.gem (pure Ruby) +* Test mode which doesn't do any changes to the DBMS +* Both LDAP and PG connections can be secured by SSL/TLS +* Supports LDAP users and groups with the same names +* Ability to have ldap groups and users with the same name merge into a single Postgres role + +== REQUIREMENTS: + +* Ruby-1.8.7, Ruby-1.9.2, JRuby-1.2, Rubinius-1.2 or better +* Rubygems-1.3.5+ +* LDAP-v3 server +* PostgreSQL-server v9.0+ + +== INSTALL: + +Install Ruby and rubygems: +* on Windows: http://rubyinstaller.org +* on Debian/Ubuntu: apt-get install ruby libpq-dev + +Install pg-ldap-sync and a database connector for PostgreSQL: + gem install pg-ldap-sync pg +You may also use the pure ruby postgres-connector which is less mature, +but doesn't need compilation: + gem install pg-ldap-sync postgres-pr + +=== Install from Git: + git clone https://github.com/larskanis/pg-ldap-sync.git + cd pg-ldap-sync + gem install bundler + bundle install + bundle exec rake install + +== USAGE: + +Create a config file based on +{config/sample-config.yaml}[https://github.com/larskanis/pg-ldap-sync/blob/master/config/sample-config.yaml] +or even better +{config/sample-config2.yaml}[https://github.com/larskanis/pg-ldap-sync/blob/master/config/sample-config2.yaml] + +Run in test-mode: + + pg_ldap_sync -c my_config.yaml -vv -t + +Run in modify-mode: + + pg_ldap_sync -c my_config.yaml -vv + + +== TEST: +There is a small test suite in the test directory that runs +against an internal ruby-ldapserver and PostgreSQL server. Ensure gem +ruby-ldapserver is installed and pg_ctl, initdb and psql +commands are in the PATH. Then: + + cd pg-ldap-sync + rake test + +== ISSUES: +* There is currently no way to set certain user attributes in PG + based on individual attributes in LDAP (expiration date etc.) + + +== LICENSE: + +The gem is available as open source under the terms of the {MIT License}[https://opensource.org/licenses/MIT]. diff --git a/Rakefile b/Rakefile index c43d774..b7824c5 100644 --- a/Rakefile +++ b/Rakefile @@ -1,18 +1,11 @@ -# -*- ruby -*- - -require 'rubygems' -require 'hoe' - -Hoe.spec 'pg-ldap-sync' do - developer('Lars Kanis', 'kanis@comcard.de') - - extra_deps << ['net-ldap', '>= 0.2'] - extra_deps << ['kwalify', '>= 0.7'] - extra_dev_deps << ['ruby-ldapserver', '>= 0.3'] - - self.readme_file = 'README.rdoc' - spec_extras[:rdoc_options] = ['--main', readme_file, "--charset=UTF-8"] - self.extra_rdoc_files << self.readme_file -end - -# vim: syntax=ruby +# -*- ruby -*- +require "bundler/gem_tasks" +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/test_*.rb"] +end + +task :gem => :build \ No newline at end of file diff --git a/bin/pg_ldap_sync b/bin/pg_ldap_sync deleted file mode 100755 index 5946086..0000000 --- a/bin/pg_ldap_sync +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby - -require 'rubygems' -require 'pg_ldap_sync/application' - -PgLdapSync::Application.run(ARGV) diff --git a/exe/pg_ldap_sync b/exe/pg_ldap_sync new file mode 100755 index 0000000..b63687b --- /dev/null +++ b/exe/pg_ldap_sync @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +require 'pg_ldap_sync' + +begin + PgLdapSync::Application.run(ARGV) +rescue PgLdapSync::ApplicationExit => ex + exit ex.exitcode +end diff --git a/lib/pg_ldap_sync.rb b/lib/pg_ldap_sync.rb index c4e95f2..72bfac4 100644 --- a/lib/pg_ldap_sync.rb +++ b/lib/pg_ldap_sync.rb @@ -1,3 +1,21 @@ -module PgLdapSync - VERSION = '0.1.2' -end +require "pg_ldap_sync/application" +require "pg_ldap_sync/version" + +module PgLdapSync + class LdapError < RuntimeError + end + + class ApplicationExit < RuntimeError + attr_reader :exitcode + + def initialize(exitcode) + @exitcode = exitcode + end + end + + class InvalidConfig < ApplicationExit + end + + class ErrorExit < ApplicationExit + end +end diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index 81df48a..a3c4061 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -1,547 +1,544 @@ -#!/usr/bin/env ruby - -require 'rubygems' -require 'net/ldap' -require 'optparse' -require 'yaml' -require 'logger' -require 'kwalify' - -begin - require 'pg' -rescue LoadError => e - begin - require 'postgres' - class PGconn - alias initialize_before_hash_change initialize - def initialize(*args) - arg = args.first - if args.length==1 && arg.kind_of?(Hash) - initialize_before_hash_change(arg[:host], arg[:port], nil, nil, arg[:dbname], arg[:user], arg[:password]) - else - initialize_before_hash_change(*args) - end - end - end - rescue LoadError - raise e - end -end - -require 'pg_ldap_sync' - -module PgLdapSync -class Application - class LdapError < RuntimeError; end - attr_accessor :config_fname - attr_accessor :log - attr_accessor :test - - def string_to_symbol(hash) - if hash.kind_of?(Hash) - return hash.inject({}){|h, v| - raise "expected String instead of #{h.inspect}" unless v[0].kind_of?(String) - h[v[0].intern] = string_to_symbol(v[1]) - h - } - else - return hash - end - end - - - def validate_config(config, schema, fname) - schema = YAML.load_file(schema) - validator = Kwalify::Validator.new(schema) - errors = validator.validate(config) - if errors && !errors.empty? - errors.each do |err| - log.fatal "error in #{fname}: [#{err.path}] #{err.message}" - end - exit(-1) - end - end - - def read_config_file(fname) - raise "Config file #{fname.inspect} does not exist" unless File.exist?(fname) - config = YAML.load(File.read(fname)) - - schema_fname = File.join(File.dirname(__FILE__), '../../config/schema.yaml') - validate_config(config, schema_fname, fname) - - @config = string_to_symbol(config) - end - - LdapRole = Struct.new :name, :dn, :member_dns - - def search_ldap_users - ldap_user_conf = @config[:ldap_users] - - users = [] - res = @ldap.search(:base => ldap_user_conf[:base], :filter => ldap_user_conf[:filter]) do |entry| - entry.dn = entry.dn.downcase - name = entry[ldap_user_conf[:name_attribute]].first - - unless name - log.warn "user attribute #{ldap_user_conf[:name_attribute].inspect} not defined for #{entry.dn}" - next - end - name.downcase! if ldap_user_conf[:lowercase_name] - - log.info "found user-dn: #{entry.dn}" - user = LdapRole.new name, entry.dn - users << user - entry.each do |attribute, values| - log.debug " #{attribute}:" - values.each do |value| - log.debug " --->#{value.inspect}" - end - end - end - raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res - return users - end - - def search_ldap_groups - ldap_group_conf = @config[:ldap_groups] - - groups = [] - res = @ldap.search(:base => ldap_group_conf[:base], :filter => ldap_group_conf[:filter]) do |entry| - entry.dn = entry.dn.downcase - name = entry[ldap_group_conf[:name_attribute]].first - - unless name - log.warn "user attribute #{ldap_group_conf[:name_attribute].inspect} not defined for #{entry.dn}" - next - end - name.downcase! if ldap_group_conf[:lowercase_name] - - log.info "found group-dn: #{entry.dn}" - group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]].map!(&:downcase) - groups << group - entry.each do |attribute, values| - log.debug " #{attribute}:" - values.each do |value| - log.debug " --->#{value.inspect}" - end - end - end - raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res - return groups - end - - PgRole = Struct.new :name, :member_names - - def search_pg_users - pg_users_conf = @config[:pg_users] - - users = [] - res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}" - res.each do |tuple| - user = PgRole.new tuple[0] - log.info{ "found pg-user: #{user.name.inspect}"} - users << user - end - return users - end - - def search_pg_groups - pg_groups_conf = @config[:pg_groups] - - groups = [] - res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}" - res.each do |tuple| - 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])}" - member_names = res2.map{|row| row[0] } - group = PgRole.new tuple[0], member_names - log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} - groups << group - end - - return groups - end - - def search_pg_ldap_groups - pg_users_conf = @config[:pg_users] - pg_groups_conf = @config[:pg_groups] - - in_group = "" - if pg_groups_conf[:grant_this_group] != nil - in_group += "'#{pg_groups_conf[:grant_this_group]}'" - end - if pg_users_conf[:grant_this_group] != nil - if in_group != "" - in_group += ", " - end - in_group += "'#{pg_users_conf[:grant_this_group]}'" - end - if pg_groups_conf[:grant_this_group] != nil || pg_users_conf[:grant_this_group] != nil - groups = [] - res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE rolname IN (#{in_group})" - res.each do |tuple| - 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])}" - member_names = res2.map{|row| row[0] } - group = PgRole.new tuple[0], member_names - log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} - groups << group - end - groups = uniq_names groups - end - - return groups - end - - def uniq_names(list) - names = {} - new_list = list.select do |entry| - name = entry.name - if names[name] - log.warn{ "duplicated group/user #{name.inspect} (#{entry.inspect})" } - next false - else - names[name] = true - next true - end - end - return new_list - end - - MatchedRole = Struct.new :ldap, :pg, :name, :state, :type - - def match_roles(ldaps, pgs, type) - ldap_by_name = ldaps.inject({}){|h,u| h[u.name] = u; h } - pg_by_name = pgs.inject({}){|h,u| h[u.name] = u; h } - - roles = [] - ldaps.each do |ld| - pg = pg_by_name[ld.name] - role = MatchedRole.new ld, pg, ld.name - roles << role - end - pgs.each do |pg| - ld = ldap_by_name[pg.name] - next if ld - role = MatchedRole.new ld, pg, pg.name - roles << role - end - - roles.each do |r| - r.state = case - when r.ldap && !r.pg then :create - when !r.ldap && r.pg then :drop - when r.pg && r.ldap then :keep - else raise "invalid user #{r.inspect}" - end - r.type = type - end - - log.info{ - roles.each do |role| - log.debug{ "#{role.state} #{role.type}: #{role.name}" } - end - "#{type} stat: create: #{roles.count{|r| r.state==:create }} drop: #{roles.count{|r| r.state==:drop }} keep: #{roles.count{|r| r.state==:keep }}" - } - return roles - end - - def match_users_groups(roles) - - # Find out if there are matching users and groups - # Process Users - roles.each do |ru| - next if ru.type == :group - - # Find Matching Group, if any - roles.each do |rg| - next if rg.type == :user - next if rg.name != ru.name - - if ru.state == :create && rg.state == :keep - ru.state = :alter - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - elsif ru.state == :keep && rg.state == :create - rg.state = :group_add - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :create && rg.state == :drop - ru.state = :alter - rg.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :drop && rg.state == :create - rg.state = :alter - ru.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :drop && rg.state == :keep - rg.state = :alter - ru.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :keep && rg.state == :drop - ru.state = :alter - rg.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :create && rg.state == :create - rg.state = :group_add; - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :drop && rg.state == :drop - rg.state = :keep - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - end - # The ru.state == :keep && rg.state == :keep we don't care about as no changes are needed - end - end - - log.info{ - "Revised user stat: create: #{roles.count{|r| r.state==:create && r.type==:user }} drop: #{roles.count{|r| r.state==:drop && r.type==:user }} alter: #{roles.count{|r| r.state==:alter && r.type==:user}} keep: #{roles.count{|r| r.state==:keep && r.type==:user}}" - } - log.info{ - "Revised group stat: create: #{roles.count{|r| r.state==:create && r.type==:group }} drop: #{roles.count{|r| r.state==:drop && r.type==:group }} alter: #{roles.count{|r| r.state==:alter && r.type==:group}} keep: #{roles.count{|r| r.state==:keep && r.type==:group}}" - } - return roles - end - - def pg_exec_modify(sql) - log.info{ "SQL: #{sql}" } - unless self.test - begin - res = @pgconn.exec sql - rescue PG::DuplicateObject => dup - log.warn{ dup } - rescue PG::DependentObjectStillExists => dep - log.warn{ dep } - end - end - end - - def pg_exec(sql) - res = @pgconn.exec sql - (0...res.num_tuples).map{|t| (0...res.num_fields).map{|i| res.getvalue(t, i) } } - end - - def create_pg_role(role) - pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] - if pg_conf[:grant_this_group] != nil - res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{role.name}'" - if res[0] == nil - pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" - else - pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" - end - else - pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" - end - end - - def drop_pg_role(role) - pg_exec_modify "DROP ROLE \"#{role.name}\"" - end - - def alter_pg_user(role) - pg_exec_modify "ALTER ROLE \"#{role.name}\" LOGIN" - set_pg_group(role) - end - - def alter_pg_group(role) - pg_exec_modify "ALTER ROLE \"#{role.name}\" NOLOGIN" - set_pg_group(role) - end - - def set_pg_group(role) - pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] - if pg_conf[:grant_this_group] != nil - if role.state == :group_add || role.state == :alter - skip_grant = :false - if !@pg_ldap_groups.empty? - @pg_ldap_groups.each do |group| - if group.name == pg_conf[:grant_this_group] - group.member_names.each do |member| - if member == role.name - skip_grant = :true - end - end - end - end - end - if skip_grant == :false - pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" - end - else - pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" - end - end - end - - def sync_roles_to_pg(roles, for_state) - roles.sort{|a,b| a.name<=>b.name }.each do |role| - create_pg_role(role) if role.state==:create && for_state==:create - set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:user - set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:user - set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:group - set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:group - alter_pg_user(role) if role.state==:alter && for_state==:alter && role.type==:user - alter_pg_group(role) if role.state==:alter && for_state==:group && role.type==:group - drop_pg_role(role) if role.state==:drop && for_state==:drop - end - end - - MatchedMembership = Struct.new :role_name, :has_member, :state - - def match_memberships(ldap_roles, pg_roles) - ldap_by_dn = ldap_roles.inject({}){|h,r| h[r.dn] = r; h } - ldap_by_m2m = ldap_roles.inject([]){|a,r| - next a unless r.member_dns - a + r.member_dns.map{|dn| - if has_member=ldap_by_dn[dn] - [r.name, has_member.name] - else - log.warn{"ldap member with dn #{dn} is unknown"} - nil - end - }.compact - } - - pg_by_name = pg_roles.inject({}){|h,r| h[r.name] = r; h } - pg_by_m2m = pg_roles.inject([]){|a,r| - next a unless r.member_names - a + r.member_names.map{|name| - if has_member=pg_by_name[name] - [r.name, has_member.name] - else - log.warn{"pg member with name #{name} is unknown"} - nil - end - }.compact - } - - memberships = (ldap_by_m2m & pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :keep } - memberships += (ldap_by_m2m - pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :grant } - memberships += (pg_by_m2m - ldap_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :revoke } - - log.info{ - memberships.each do |membership| - log.debug{ "#{membership.state} #{membership.role_name} to #{membership.has_member}" } - end - "membership stat: grant: #{memberships.count{|u| u.state==:grant }} revoke: #{memberships.count{|u| u.state==:revoke }} keep: #{memberships.count{|u| u.state==:keep }}" - } - return memberships - end - - def grant_membership(role_name, add_members) - pg_conf = @config[:pg_groups] - add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",") - pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}" - end - - def revoke_membership(role_name, rm_members) - rm_members_escaped = rm_members.map{|m| "\"#{m}\"" }.join(",") - pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}" - end - - def sync_membership_to_pg(memberships, for_state) - grants = {} - memberships.select{|ms| ms.state==for_state }.each do |ms| - grants[ms.role_name] ||= [] - grants[ms.role_name] << ms.has_member - end - - grants.each do |role_name, members| - grant_membership(role_name, members) if for_state==:grant - revoke_membership(role_name, members) if for_state==:revoke - end - end - - def check_groups() - pg_users_conf = @config[:pg_users] - if pg_users_conf[:grant_this_group] != nil - check_make_group(pg_users_conf[:grant_this_group]) - else - log.debug{"No Users LDAP group to Check/Create"} - end - pg_groups_conf = @config[:pg_groups] - if pg_groups_conf[:grant_this_group] != nil - check_make_group(pg_groups_conf[:grant_this_group]) - else - log.debug{"No Groups LDAP group to Check/Create"} - end - end - - def check_make_group(group_name) - res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{group_name}'" - if res[0] == nil - log.debug{ "Creating Group: #{group_name}"} - pg_exec_modify "CREATE ROLE \"#{group_name}\"" - else - log.info{ "Lookup Found Group: #{res[0][0]}"} - end - end - - def start! - read_config_file(@config_fname) - - # gather LDAP users and groups - @ldap = Net::LDAP.new @config[:ldap_connection] - ldap_users = uniq_names search_ldap_users - ldap_groups = uniq_names search_ldap_groups - - # gather PGs users and groups - @pgconn = PGconn.connect @config[:pg_connection] - pg_users = uniq_names search_pg_users - pg_groups = uniq_names search_pg_groups - @pg_ldap_groups = search_pg_ldap_groups - - # compare LDAP to PG users and groups - mroles = match_roles(ldap_users, pg_users, :user) - mroles += match_roles(ldap_groups, pg_groups, :group) - mroles = match_users_groups(mroles) - - # compare LDAP to PG memberships - mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) - - # Check to see if grant_this_group exists and if not create the group - check_groups() - - # drop/revoke roles/memberships first - sync_membership_to_pg(mmemberships, :revoke) - sync_roles_to_pg(mroles, :drop) - # Make Login if this used to be a non-login role and Non-Login if this used to be a login role - sync_roles_to_pg(mroles, :alter) - # create/grant roles/memberships - sync_roles_to_pg(mroles, :create) - # Fix group memberships - sync_roles_to_pg(mroles, :group) - - # Reload users and group in case they had been created but were not within the proper "grant_this_group" - pg_users = uniq_names search_pg_users - pg_groups = uniq_names search_pg_groups - @pg_ldap_groups = search_pg_ldap_groups - - # compare reloaded LDAP to PG memberships - mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) - - # The reload keeps the grants from throwing warnings about role already being part of the group - sync_membership_to_pg(mmemberships, :grant) - - @pgconn.close - end - - def self.run(argv) - s = self.new - s.config_fname = '/etc/pg_ldap_sync.yaml' - s.log = Logger.new(STDOUT) - s.log.level = Logger::ERROR - - OptionParser.new do |opts| - opts.banner = "Usage: #{$0} [options]" - opts.on("-v", "--[no-]verbose", "Increase verbose level"){ s.log.level-=1 } - opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=)) - opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=)) - - opts.parse!(argv) - end - - s.start! - end -end -end +#!/usr/bin/env ruby + +require 'net/ldap' +require 'optparse' +require 'yaml' +require 'kwalify' +require 'pg' +require "pg_ldap_sync/logger" + +module PgLdapSync +class Application + attr_accessor :config_fname + attr_accessor :log + attr_accessor :test + + def string_to_symbol(hash) + if hash.kind_of?(Hash) + return hash.inject({}){|h, v| + raise "expected String instead of #{h.inspect}" unless v[0].kind_of?(String) + h[v[0].intern] = string_to_symbol(v[1]) + h + } + else + return hash + end + end + + + def validate_config(config, schema, fname) + schema = YAML.load_file(schema) + validator = Kwalify::Validator.new(schema) + errors = validator.validate(config) + if errors && !errors.empty? + errors.each do |err| + log.fatal "error in #{fname}: [#{err.path}] #{err.message}" + end + raise InvalidConfig, 78 # EX_CONFIG + end + end + + def read_config_file(fname) + raise "Config file #{fname.inspect} does not exist" unless File.exist?(fname) + config = YAML.load(File.read(fname)) + + schema_fname = File.join(File.dirname(__FILE__), '../../config/schema.yaml') + validate_config(config, schema_fname, fname) + + @config = string_to_symbol(config) + end + + LdapRole = Struct.new :name, :dn, :member_dns + + def search_ldap_users + ldap_user_conf = @config[:ldap_users] + + users = [] + res = @ldap.search(:base => ldap_user_conf[:base], :filter => ldap_user_conf[:filter]) do |entry| + entry.dn = entry.dn.downcase + name = entry[ldap_user_conf[:name_attribute]].first + + unless name + log.warn "user attribute #{ldap_user_conf[:name_attribute].inspect} not defined for #{entry.dn}" + next + end + name.downcase! if ldap_user_conf[:lowercase_name] + + log.info "found user-dn: #{entry.dn}" + user = LdapRole.new name, entry.dn + users << user + entry.each do |attribute, values| + log.debug " #{attribute}:" + values.each do |value| + log.debug " --->#{value.inspect}" + end + end + end + raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res + return users + end + + def search_ldap_groups + ldap_group_conf = @config[:ldap_groups] + + groups = [] + res = @ldap.search(:base => ldap_group_conf[:base], :filter => ldap_group_conf[:filter]) do |entry| + entry.dn = entry.dn.downcase + name = entry[ldap_group_conf[:name_attribute]].first + + unless name + log.warn "user attribute #{ldap_group_conf[:name_attribute].inspect} not defined for #{entry.dn}" + next + end + name.downcase! if ldap_group_conf[:lowercase_name] + + log.info "found group-dn: #{entry.dn}" + group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]].map!(&:downcase) + groups << group + entry.each do |attribute, values| + log.debug " #{attribute}:" + values.each do |value| + log.debug " --->#{value.inspect}" + end + end + end + raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res + return groups + end + + PgRole = Struct.new :name, :member_names + + # List of default roles taken from https://www.postgresql.org/docs/current/static/default-roles.html + PG_BUILTIN_ROLES = %w[ pg_signal_backend pg_monitor pg_read_all_settings pg_read_all_stats pg_stat_scan_tables] + + def search_pg_users + pg_users_conf = @config[:pg_users] + + users = [] + res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}" + res.each do |tuple| + user = PgRole.new tuple[0] + next if PG_BUILTIN_ROLES.include?(user.name) + log.info{ "found pg-user: #{user.name.inspect}"} + users << user + end + return users + end + + def search_pg_groups + pg_groups_conf = @config[:pg_groups] + + groups = [] + res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}" + res.each do |tuple| + 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])}" + member_names = res2.map{|row| row[0] } + group = PgRole.new tuple[0], member_names + next if PG_BUILTIN_ROLES.include?(group.name) + log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} + groups << group + end + + return groups + end + + def search_pg_ldap_groups + pg_users_conf = @config[:pg_users] + pg_groups_conf = @config[:pg_groups] + + in_group = "" + if pg_groups_conf[:grant_this_group] != nil + in_group += "'#{pg_groups_conf[:grant_this_group]}'" + end + if pg_users_conf[:grant_this_group] != nil + if in_group != "" + in_group += ", " + end + in_group += "'#{pg_users_conf[:grant_this_group]}'" + end + if pg_groups_conf[:grant_this_group] != nil || pg_users_conf[:grant_this_group] != nil + groups = [] + res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE rolname IN (#{in_group})" + res.each do |tuple| + 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])}" + member_names = res2.map{|row| row[0] } + group = PgRole.new tuple[0], member_names + log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} + groups << group + end + groups = uniq_names groups + end + + return groups + end + + def uniq_names(list) + names = {} + new_list = list.select do |entry| + name = entry.name + if names[name] + log.warn{ "duplicated group/user #{name.inspect} (#{entry.inspect})" } + next false + else + names[name] = true + next true + end + end + return new_list + end + + MatchedRole = Struct.new :ldap, :pg, :name, :state, :type + + def match_roles(ldaps, pgs, type) + ldap_by_name = ldaps.inject({}){|h,u| h[u.name] = u; h } + pg_by_name = pgs.inject({}){|h,u| h[u.name] = u; h } + + roles = [] + ldaps.each do |ld| + pg = pg_by_name[ld.name] + role = MatchedRole.new ld, pg, ld.name + roles << role + end + pgs.each do |pg| + ld = ldap_by_name[pg.name] + next if ld + role = MatchedRole.new ld, pg, pg.name + roles << role + end + + roles.each do |r| + r.state = case + when r.ldap && !r.pg then :create + when !r.ldap && r.pg then :drop + when r.pg && r.ldap then :keep + else raise "invalid user #{r.inspect}" + end + r.type = type + end + + log.info{ + roles.each do |role| + log.debug{ "#{role.state} #{role.type}: #{role.name}" } + end + "#{type} stat: create: #{roles.count{|r| r.state==:create }} drop: #{roles.count{|r| r.state==:drop }} keep: #{roles.count{|r| r.state==:keep }}" + } + return roles + end + + def match_users_groups(roles) + + # Find out if there are matching users and groups + # Process Users + roles.each do |ru| + next if ru.type == :group + + # Find Matching Group, if any + roles.each do |rg| + next if rg.type == :user + next if rg.name != ru.name + + if ru.state == :create && rg.state == :keep + ru.state = :alter + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + elsif ru.state == :keep && rg.state == :create + rg.state = :group_add + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :create && rg.state == :drop + ru.state = :alter + rg.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :create + rg.state = :alter + ru.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :keep + rg.state = :alter + ru.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :keep && rg.state == :drop + ru.state = :alter + rg.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :create && rg.state == :create + rg.state = :group_add; + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :drop + rg.state = :keep + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + end + # The ru.state == :keep && rg.state == :keep we don't care about as no changes are needed + end + end + + log.info{ + "Revised user stat: create: #{roles.count{|r| r.state==:create && r.type==:user }} drop: #{roles.count{|r| r.state==:drop && r.type==:user }} alter: #{roles.count{|r| r.state==:alter && r.type==:user}} keep: #{roles.count{|r| r.state==:keep && r.type==:user}}" + } + log.info{ + "Revised group stat: create: #{roles.count{|r| r.state==:create && r.type==:group }} drop: #{roles.count{|r| r.state==:drop && r.type==:group }} alter: #{roles.count{|r| r.state==:alter && r.type==:group}} keep: #{roles.count{|r| r.state==:keep && r.type==:group}}" + } + return roles + end + + def try_sql(text) + begin + @pgconn.exec "SAVEPOINT try_sql;" + @pgconn.exec text + rescue PG::Error => err + @pgconn.exec "ROLLBACK TO try_sql;" + log.error{ "#{err} (#{err.class})" } + end + end + + + def pg_exec_modify(sql) + log.info{ "SQL: #{sql}" } + unless self.test + try_sql sql + end + end + + def pg_exec(sql) + res = @pgconn.exec sql + (0...res.num_tuples).map{|t| (0...res.num_fields).map{|i| res.getvalue(t, i) } } + end + + def create_pg_role(role) + pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] + if pg_conf[:grant_this_group] != nil + res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{role.name}'" + if res[0] == nil + pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" + else + pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + end + else + pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" + end + end + + def drop_pg_role(role) + pg_exec_modify "DROP ROLE \"#{role.name}\"" + end + + def alter_pg_user(role) + pg_exec_modify "ALTER ROLE \"#{role.name}\" LOGIN" + set_pg_group(role) + end + + def alter_pg_group(role) + pg_exec_modify "ALTER ROLE \"#{role.name}\" NOLOGIN" + set_pg_group(role) + end + + def set_pg_group(role) + pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] + if pg_conf[:grant_this_group] != nil + if role.state == :group_add || role.state == :alter + skip_grant = :false + if !@pg_ldap_groups.empty? + @pg_ldap_groups.each do |group| + if group.name == pg_conf[:grant_this_group] + group.member_names.each do |member| + if member == role.name + skip_grant = :true + end + end + end + end + end + if skip_grant == :false + pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + end + else + pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" + end + end + end + + def sync_roles_to_pg(roles, for_state) + roles.sort{|a,b| a.name<=>b.name }.each do |role| + create_pg_role(role) if role.state==:create && for_state==:create + set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:user + set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:user + set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:group + set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:group + alter_pg_user(role) if role.state==:alter && for_state==:alter && role.type==:user + alter_pg_group(role) if role.state==:alter && for_state==:group && role.type==:group + drop_pg_role(role) if role.state==:drop && for_state==:drop + end + end + + MatchedMembership = Struct.new :role_name, :has_member, :state + + def match_memberships(ldap_roles, pg_roles) + ldap_by_dn = ldap_roles.inject({}){|h,r| h[r.dn] = r; h } + ldap_by_m2m = ldap_roles.inject([]){|a,r| + next a unless r.member_dns + a + r.member_dns.map{|dn| + if has_member=ldap_by_dn[dn] + [r.name, has_member.name] + else + log.warn{"ldap member with dn #{dn} is unknown"} + nil + end + }.compact + } + + pg_by_name = pg_roles.inject({}){|h,r| h[r.name] = r; h } + pg_by_m2m = pg_roles.inject([]){|a,r| + next a unless r.member_names + a + r.member_names.map{|name| + if has_member=pg_by_name[name] + [r.name, has_member.name] + else + log.warn{"pg member with name #{name} is unknown"} + nil + end + }.compact + } + + memberships = (ldap_by_m2m & pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :keep } + memberships += (ldap_by_m2m - pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :grant } + memberships += (pg_by_m2m - ldap_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :revoke } + + log.info{ + memberships.each do |membership| + log.debug{ "#{membership.state} #{membership.role_name} to #{membership.has_member}" } + end + "membership stat: grant: #{memberships.count{|u| u.state==:grant }} revoke: #{memberships.count{|u| u.state==:revoke }} keep: #{memberships.count{|u| u.state==:keep }}" + } + return memberships + end + + def grant_membership(role_name, add_members) + pg_conf = @config[:pg_groups] + add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",") + pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}" + end + + def revoke_membership(role_name, rm_members) + rm_members_escaped = rm_members.map{|m| "\"#{m}\"" }.join(",") + pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}" + end + + def sync_membership_to_pg(memberships, for_state) + grants = {} + memberships.select{|ms| ms.state==for_state }.each do |ms| + grants[ms.role_name] ||= [] + grants[ms.role_name] << ms.has_member + end + + grants.each do |role_name, members| + grant_membership(role_name, members) if for_state==:grant + revoke_membership(role_name, members) if for_state==:revoke + end + end + + def check_groups() + pg_users_conf = @config[:pg_users] + if pg_users_conf[:grant_this_group] != nil + check_make_group(pg_users_conf[:grant_this_group]) + else + log.debug{"No Users LDAP group to Check/Create"} + end + pg_groups_conf = @config[:pg_groups] + if pg_groups_conf[:grant_this_group] != nil + check_make_group(pg_groups_conf[:grant_this_group]) + else + log.debug{"No Groups LDAP group to Check/Create"} + end + end + + def check_make_group(group_name) + res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{group_name}'" + if res[0] == nil + log.debug{ "Creating Group: #{group_name}"} + pg_exec_modify "CREATE ROLE \"#{group_name}\"" + else + log.info{ "Lookup Found Group: #{res[0][0]}"} + end + end + + def start! + read_config_file(@config_fname) + + # gather LDAP users and groups + @ldap = Net::LDAP.new @config[:ldap_connection] + ldap_users = uniq_names search_ldap_users + ldap_groups = uniq_names search_ldap_groups + + # gather PGs users and groups + @pgconn = PG.connect @config[:pg_connection] + begin + @pgconn.transaction do + pg_users = uniq_names search_pg_users + pg_groups = uniq_names search_pg_groups + @pg_ldap_groups = search_pg_ldap_groups + + # compare LDAP to PG users and groups + mroles = match_roles(ldap_users, pg_users, :user) + mroles += match_roles(ldap_groups, pg_groups, :group) + mroles = match_users_groups(mroles) + + # compare LDAP to PG memberships + mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) + + # Check to see if grant_this_group exists and if not create the group + check_groups() + + # drop/revoke roles/memberships first + sync_membership_to_pg(mmemberships, :revoke) + sync_roles_to_pg(mroles, :drop) + # Make Login if this used to be a non-login role and Non-Login if this used to be a login role + sync_roles_to_pg(mroles, :alter) + # create/grant roles/memberships + sync_roles_to_pg(mroles, :create) + # Fix group memberships + sync_roles_to_pg(mroles, :group) + + # Reload users and group in case they had been created but were not within the proper "grant_this_group" + pg_users = uniq_names search_pg_users + pg_groups = uniq_names search_pg_groups + @pg_ldap_groups = search_pg_ldap_groups + + # compare reloaded LDAP to PG memberships + mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) + + # The reload keeps the grants from throwing warnings about role already being part of the group + sync_membership_to_pg(mmemberships, :grant) + + end + ensure + @pgconn.close + end + + # Determine exitcode + if log.had_errors? + raise ErrorExit, 1 + end + end + + def self.run(argv) + s = self.new + s.config_fname = '/etc/pg_ldap_sync.yaml' + s.log = Logger.new($stdout, @error_counters) + s.log.level = Logger::ERROR + + OptionParser.new do |opts| + opts.version = VERSION + opts.banner = "Usage: #{$0} [options]" + opts.on("-v", "--[no-]verbose", "Increase verbose level"){|v| s.log.level += v ? -1 : 1 } + opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=)) + opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=)) + + opts.parse!(argv) + end + + s.start! + end +end +end diff --git a/lib/pg_ldap_sync/logger.rb b/lib/pg_ldap_sync/logger.rb new file mode 100644 index 0000000..3ea5575 --- /dev/null +++ b/lib/pg_ldap_sync/logger.rb @@ -0,0 +1,24 @@ +require 'logger' + +module PgLdapSync +class Logger < ::Logger + def initialize(io, counters) + super(io) + @counters = {} + end + + def add(severity, *args) + @counters[severity] ||= 0 + @counters[severity] += 1 + super + end + + def had_logged?(severity) + @counters[severity] && @counters[severity] > 0 + end + + def had_errors? + had_logged?(Logger::FATAL) || had_logged?(Logger::ERROR) + end +end +end \ No newline at end of file diff --git a/lib/pg_ldap_sync/version.rb b/lib/pg_ldap_sync/version.rb new file mode 100644 index 0000000..683c027 --- /dev/null +++ b/lib/pg_ldap_sync/version.rb @@ -0,0 +1,3 @@ +module PgLdapSync + VERSION = "0.2.1" +end \ No newline at end of file diff --git a/pg-ldap-sync.gemspec b/pg-ldap-sync.gemspec new file mode 100644 index 0000000..d931900 --- /dev/null +++ b/pg-ldap-sync.gemspec @@ -0,0 +1,31 @@ +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "pg_ldap_sync/version" + +Gem::Specification.new do |spec| + spec.name = "pg-ldap-sync" + spec.version = PgLdapSync::VERSION + spec.authors = ["Lars Kanis"] + spec.email = ["lars@greiz-reinsdorf.de"] + + spec.summary = %q{Use LDAP permissions in PostgreSQL} + spec.homepage = "https://github.com/larskanis/pg-ldap-sync" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + spec.rdoc_options = %w[--main README.md --charset=UTF-8] + + spec.add_runtime_dependency "net-ldap", "~> 0.16" + spec.add_runtime_dependency "kwalify", "~> 0.7" + spec.add_runtime_dependency "pg", ">= 0.14", "< 2.0" + spec.add_development_dependency "ruby-ldapserver", "~> 0.3" + spec.add_development_dependency "minitest", "~> 5.0" + spec.add_development_dependency "bundler", "~> 1.16" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "minitest-hooks", "~> 1.4" +end \ No newline at end of file diff --git a/test/test_pg_ldap_sync.rb b/test/test_pg_ldap_sync.rb index b5bf12e..7a54d29 100644 --- a/test/test_pg_ldap_sync.rb +++ b/test/test_pg_ldap_sync.rb @@ -1,332 +1,335 @@ -#!/usr/bin/env ruby - -require "test/unit" -require "pg_ldap_sync/application" -require 'yaml' -require 'test/ldap_server' -require 'fileutils' - -class TestPgLdapSync < Test::Unit::TestCase - def log_and_run( *cmd ) - puts cmd.join(' ') - system( *cmd ) - raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? - end - - def start_ldap_server(file_name) - yaml_fname = File.join(File.dirname(__FILE__), file_name) - @directory = File.open(yaml_fname){|f| YAML::load(f.read) } - - # Listen for incoming LDAP connections. For each one, create a Connection - # object, which will invoke a HashOperation object for each request. - - @ldap_server = LDAP::Server.new( - :port => 1389, - :nodelay => true, - :listen => 10, - # :ssl_key_file => "key.pem", - # :ssl_cert_file => "cert.pem", - # :ssl_on_connect => true, - :operation_class => HashOperation, - :operation_args => [@directory] - ) - @ldap_server.run_tcpserver - end - - def stop_ldap_server - @ldap_server.stop - end - - def start_pg_server - @port = 54321 - ENV['PGPORT'] = @port.to_s - ENV['PGHOST'] = 'localhost' - unless File.exist?('temp/pg_data/PG_VERSION') - FileUtils.mkdir_p 'temp/pg_data' - log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale' - end - log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start' - log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, \"bamm-bamm\", rubble_kids, wilma, \"Wilma Flintstone\", betty, rubble, pebbles, kids, flintstone_kids, \"Flintstones\", \"Wilmas\", \"All Users\", ldap_users, ldap_groups", 'postgres' - end - - def stop_pg_server - log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'stop' - end - - def setup - start_pg_server - end - - def teardown - stop_ldap_server - stop_pg_server - end - - def psqlre(*args) - /^\s*#{args[0]}[ |]*#{args[1]}[ |\{"]*#{args[2..-1].join('[", ]+')}["\}\s]*$/ - end - - def exec_psql_du - text = if RUBY_PLATFORM=~/mingw|mswin/ - `psql -d postgres -c "SELECT r.rolname AS \\"Role name\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\"Attributes\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\"Member of\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` - else - `psql -d postgres -c "SELECT r.rolname AS \\\"Role name\\\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\\"Attributes\\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\\"Member of\\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` - end - puts text - return text - end - -#=begin - def test_sanity1 - start_ldap_server("fixtures/ldapdb.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'].pop - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Wilmas'), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) - end -#=end - -#=begin - def test_sanity2 - start_ldap_server("fixtures/ldapdb.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'].pop - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) - end -#=end - -#=begin - def test_sanity3 - start_ldap_server("fixtures/ldapdb3.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) - assert_match(psqlre('betty', '', 'rubble'), psql_du) - assert_match(psqlre('rubble', ''), psql_du) - assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids'), psql_du) - assert_match(psqlre('kids', ''), psql_du) - assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) - assert_match(psqlre('rubble_kids','Cannot login'), psql_du) - assert_match(psqlre('flintstone_kids',''), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop - # remove user 'rubble' - @directory.delete('cn=rubble,dc=example,dc=com') - # reove user and group 'flintstone_kids' - @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') - @directory.delete('cn=flintstone_kids,dc=example,dc=com') - # remove group 'kids' - @directory.delete('cn=kids,ou=groups,dc=example,dc=com') - # remove group 'rubble_kids' - @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') - # add user 'rubble_kids' - @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Wilmas'), psql_du) - assert_match(psqlre('betty', '', 'rubble'), psql_du) - assert_match(psqlre('rubble', 'Cannot login'), psql_du) - assert_match(psqlre('pebbles', ''), psql_du) - assert_match(psqlre('kids', ''), psql_du) - assert_no_match(/flintstone_kids/, psql_du) - assert_match(psqlre('bamm-bamm', ''), psql_du) - assert_match(psqlre('rubble_kids', ''), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - #Recreate User 'rubble' - @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} - #Recreate Group 'kids' - @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} - # remove user 'rubble_kids' - @directory.delete('cn=rubble_kids,dc=example,dc=com') - #Recreate Group 'rubble_kids' - @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) - assert_match(psqlre('pebbles', '', 'kids'), psql_du) - assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) - assert_match(psqlre('betty', '', 'rubble'), psql_du) - assert_match(psqlre('rubble', ''), psql_du) - assert_match(psqlre('rubble_kids', 'Cannot login'), psql_du) - assert_match(psqlre('kids', ''), psql_du) - end -#=end - -#=begin - def test_sanity4 - start_ldap_server("fixtures/ldapdb3.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) - assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) - assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) - assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids','ldap_users'), psql_du) - assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) - assert_match(psqlre('bamm-bamm', '','ldap_users', 'rubble_kids'), psql_du) - assert_match(psqlre('rubble_kids','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('flintstone_kids','','ldap_groups','ldap_users'), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop - # remove user 'rubble' - @directory.delete('cn=rubble,dc=example,dc=com') - # reove user and group 'flintstone_kids' - @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') - @directory.delete('cn=flintstone_kids,dc=example,dc=com') - # remove group 'kids' - @directory.delete('cn=kids,ou=groups,dc=example,dc=com') - # remove group 'rubble_kids' - @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') - # add user 'rubble_kids' - @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login', 'ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login', 'ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users', 'ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones', 'ldap_users'), psql_du) - assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) - assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) - assert_match(psqlre('rubble', 'Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('pebbles', '','ldap_users'), psql_du) - assert_match(psqlre('kids', '','ldap_users'), psql_du) - assert_no_match(/flintstone_kids/, psql_du) - assert_match(psqlre('bamm-bamm', '','ldap_users'), psql_du) - assert_match(psqlre('rubble_kids', '','ldap_users'), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - #Recreate User 'rubble' - @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} - #Recreate Group 'kids' - @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} - # remove user 'rubble_kids' - @directory.delete('cn=rubble_kids,dc=example,dc=com') - #Recreate Group 'rubble_kids' - @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) - assert_match(psqlre('pebbles', '', 'kids','ldap_users'), psql_du) - assert_match(psqlre('bamm-bamm', '', 'ldap_users','rubble_kids'), psql_du) - assert_match(psqlre('betty', '', 'ldap_users','rubble'), psql_du) - assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) - assert_match(psqlre('rubble_kids', 'Cannot login', 'ldap_groups'), psql_du) - assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) - end -#=end -end +#!/usr/bin/env ruby + +require 'test/unit' +#require "pg_ldap_sync/application" +require "pg_ldap_sync" +require 'yaml' +#require 'test/ldap_server' +require 'ldap_server' +require 'fileutils' +require_relative 'ldap_server' + +class TestPgLdapSync < Test::Unit::TestCase + def log_and_run( *cmd ) + puts cmd.join(' ') + system( *cmd ) + raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? + end + + def start_ldap_server(file_name) + yaml_fname = File.join(File.dirname(__FILE__), file_name) + @directory = File.open(yaml_fname){|f| YAML::load(f.read) } + + # Listen for incoming LDAP connections. For each one, create a Connection + # object, which will invoke a HashOperation object for each request. + + @ldap_server = LDAP::Server.new( + :port => 1389, + :nodelay => true, + :listen => 10, + # :ssl_key_file => "key.pem", + # :ssl_cert_file => "cert.pem", + # :ssl_on_connect => true, + :operation_class => HashOperation, + :operation_args => [@directory] + ) + @ldap_server.run_tcpserver + end + + def stop_ldap_server + @ldap_server.stop + end + + def start_pg_server + @port = 54321 + ENV['PGPORT'] = @port.to_s + ENV['PGHOST'] = 'localhost' + unless File.exist?('temp/pg_data/PG_VERSION') + FileUtils.mkdir_p 'temp/pg_data' + log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale' + end + log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start' + log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, \"bamm-bamm\", rubble_kids, wilma, \"Wilma Flintstone\", betty, rubble, pebbles, kids, flintstone_kids, \"Flintstones\", \"Wilmas\", \"All Users\", ldap_users, ldap_groups", 'postgres' + end + + def stop_pg_server + log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'stop' + end + + def setup + start_pg_server + end + + def teardown + stop_ldap_server + stop_pg_server + end + + def psqlre(*args) + /^\s*#{args[0]}[ |]*#{args[1]}[ |\{"]*#{args[2..-1].join('[", ]+')}["\}\s]*$/ + end + + def exec_psql_du + text = if RUBY_PLATFORM=~/mingw|mswin/ + `psql -d postgres -c "SELECT r.rolname AS \\"Role name\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\"Attributes\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\"Member of\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` + else + `psql -d postgres -c "SELECT r.rolname AS \\\"Role name\\\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\\"Attributes\\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\\"Member of\\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` + end + puts text + return text + end + +#=begin + def test_sanity1 + start_ldap_server("fixtures/ldapdb.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'].pop + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Wilmas'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) + end +#=end + +#=begin + def test_sanity2 + start_ldap_server("fixtures/ldapdb.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'].pop + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) + end +#=end + +#=begin + def test_sanity3 + start_ldap_server("fixtures/ldapdb3.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', ''), psql_du) + assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids'), psql_du) + assert_match(psqlre('kids', ''), psql_du) + assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) + assert_match(psqlre('rubble_kids','Cannot login'), psql_du) + assert_match(psqlre('flintstone_kids',''), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop + # remove user 'rubble' + @directory.delete('cn=rubble,dc=example,dc=com') + # reove user and group 'flintstone_kids' + @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') + @directory.delete('cn=flintstone_kids,dc=example,dc=com') + # remove group 'kids' + @directory.delete('cn=kids,ou=groups,dc=example,dc=com') + # remove group 'rubble_kids' + @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') + # add user 'rubble_kids' + @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Wilmas'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', 'Cannot login'), psql_du) + assert_match(psqlre('pebbles', ''), psql_du) + assert_match(psqlre('kids', ''), psql_du) + assert_no_match(/flintstone_kids/, psql_du) + assert_match(psqlre('bamm-bamm', ''), psql_du) + assert_match(psqlre('rubble_kids', ''), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + #Recreate User 'rubble' + @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} + #Recreate Group 'kids' + @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} + # remove user 'rubble_kids' + @directory.delete('cn=rubble_kids,dc=example,dc=com') + #Recreate Group 'rubble_kids' + @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) + assert_match(psqlre('pebbles', '', 'kids'), psql_du) + assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', ''), psql_du) + assert_match(psqlre('rubble_kids', 'Cannot login'), psql_du) + assert_match(psqlre('kids', ''), psql_du) + end +#=end + +#=begin + def test_sanity4 + start_ldap_server("fixtures/ldapdb3.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) + assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids','ldap_users'), psql_du) + assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('bamm-bamm', '','ldap_users', 'rubble_kids'), psql_du) + assert_match(psqlre('rubble_kids','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('flintstone_kids','','ldap_groups','ldap_users'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop + # remove user 'rubble' + @directory.delete('cn=rubble,dc=example,dc=com') + # reove user and group 'flintstone_kids' + @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') + @directory.delete('cn=flintstone_kids,dc=example,dc=com') + # remove group 'kids' + @directory.delete('cn=kids,ou=groups,dc=example,dc=com') + # remove group 'rubble_kids' + @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') + # add user 'rubble_kids' + @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users', 'ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones', 'ldap_users'), psql_du) + assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) + assert_match(psqlre('rubble', 'Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('pebbles', '','ldap_users'), psql_du) + assert_match(psqlre('kids', '','ldap_users'), psql_du) + assert_no_match(/flintstone_kids/, psql_du) + assert_match(psqlre('bamm-bamm', '','ldap_users'), psql_du) + assert_match(psqlre('rubble_kids', '','ldap_users'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + #Recreate User 'rubble' + @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} + #Recreate Group 'kids' + @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} + # remove user 'rubble_kids' + @directory.delete('cn=rubble_kids,dc=example,dc=com') + #Recreate Group 'rubble_kids' + @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('pebbles', '', 'kids','ldap_users'), psql_du) + assert_match(psqlre('bamm-bamm', '', 'ldap_users','rubble_kids'), psql_du) + assert_match(psqlre('betty', '', 'ldap_users','rubble'), psql_du) + assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('rubble_kids', 'Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) + end +#=end +end From 8b3c288fcdefd2de0141359cfb60d58cf51d6268 Mon Sep 17 00:00:00 2001 From: Lloyd Albin Date: Mon, 2 Nov 2020 22:09:10 -0800 Subject: [PATCH 7/9] Changed from DOS to Unix --- exe/pg_ldap_sync | 18 +- lib/pg_ldap_sync.rb | 42 +- lib/pg_ldap_sync/application.rb | 1088 +++++++++++++++---------------- lib/pg_ldap_sync/logger.rb | 46 +- lib/pg_ldap_sync/version.rb | 4 +- test/test_pg_ldap_sync.rb | 670 +++++++++---------- 6 files changed, 934 insertions(+), 934 deletions(-) diff --git a/exe/pg_ldap_sync b/exe/pg_ldap_sync index b63687b..c822722 100755 --- a/exe/pg_ldap_sync +++ b/exe/pg_ldap_sync @@ -1,9 +1,9 @@ -#!/usr/bin/env ruby - -require 'pg_ldap_sync' - -begin - PgLdapSync::Application.run(ARGV) -rescue PgLdapSync::ApplicationExit => ex - exit ex.exitcode -end +#!/usr/bin/env ruby + +require 'pg_ldap_sync' + +begin + PgLdapSync::Application.run(ARGV) +rescue PgLdapSync::ApplicationExit => ex + exit ex.exitcode +end diff --git a/lib/pg_ldap_sync.rb b/lib/pg_ldap_sync.rb index 72bfac4..21a6185 100644 --- a/lib/pg_ldap_sync.rb +++ b/lib/pg_ldap_sync.rb @@ -1,21 +1,21 @@ -require "pg_ldap_sync/application" -require "pg_ldap_sync/version" - -module PgLdapSync - class LdapError < RuntimeError - end - - class ApplicationExit < RuntimeError - attr_reader :exitcode - - def initialize(exitcode) - @exitcode = exitcode - end - end - - class InvalidConfig < ApplicationExit - end - - class ErrorExit < ApplicationExit - end -end +require "pg_ldap_sync/application" +require "pg_ldap_sync/version" + +module PgLdapSync + class LdapError < RuntimeError + end + + class ApplicationExit < RuntimeError + attr_reader :exitcode + + def initialize(exitcode) + @exitcode = exitcode + end + end + + class InvalidConfig < ApplicationExit + end + + class ErrorExit < ApplicationExit + end +end diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index a3c4061..e2ca37d 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -1,544 +1,544 @@ -#!/usr/bin/env ruby - -require 'net/ldap' -require 'optparse' -require 'yaml' -require 'kwalify' -require 'pg' -require "pg_ldap_sync/logger" - -module PgLdapSync -class Application - attr_accessor :config_fname - attr_accessor :log - attr_accessor :test - - def string_to_symbol(hash) - if hash.kind_of?(Hash) - return hash.inject({}){|h, v| - raise "expected String instead of #{h.inspect}" unless v[0].kind_of?(String) - h[v[0].intern] = string_to_symbol(v[1]) - h - } - else - return hash - end - end - - - def validate_config(config, schema, fname) - schema = YAML.load_file(schema) - validator = Kwalify::Validator.new(schema) - errors = validator.validate(config) - if errors && !errors.empty? - errors.each do |err| - log.fatal "error in #{fname}: [#{err.path}] #{err.message}" - end - raise InvalidConfig, 78 # EX_CONFIG - end - end - - def read_config_file(fname) - raise "Config file #{fname.inspect} does not exist" unless File.exist?(fname) - config = YAML.load(File.read(fname)) - - schema_fname = File.join(File.dirname(__FILE__), '../../config/schema.yaml') - validate_config(config, schema_fname, fname) - - @config = string_to_symbol(config) - end - - LdapRole = Struct.new :name, :dn, :member_dns - - def search_ldap_users - ldap_user_conf = @config[:ldap_users] - - users = [] - res = @ldap.search(:base => ldap_user_conf[:base], :filter => ldap_user_conf[:filter]) do |entry| - entry.dn = entry.dn.downcase - name = entry[ldap_user_conf[:name_attribute]].first - - unless name - log.warn "user attribute #{ldap_user_conf[:name_attribute].inspect} not defined for #{entry.dn}" - next - end - name.downcase! if ldap_user_conf[:lowercase_name] - - log.info "found user-dn: #{entry.dn}" - user = LdapRole.new name, entry.dn - users << user - entry.each do |attribute, values| - log.debug " #{attribute}:" - values.each do |value| - log.debug " --->#{value.inspect}" - end - end - end - raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res - return users - end - - def search_ldap_groups - ldap_group_conf = @config[:ldap_groups] - - groups = [] - res = @ldap.search(:base => ldap_group_conf[:base], :filter => ldap_group_conf[:filter]) do |entry| - entry.dn = entry.dn.downcase - name = entry[ldap_group_conf[:name_attribute]].first - - unless name - log.warn "user attribute #{ldap_group_conf[:name_attribute].inspect} not defined for #{entry.dn}" - next - end - name.downcase! if ldap_group_conf[:lowercase_name] - - log.info "found group-dn: #{entry.dn}" - group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]].map!(&:downcase) - groups << group - entry.each do |attribute, values| - log.debug " #{attribute}:" - values.each do |value| - log.debug " --->#{value.inspect}" - end - end - end - raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res - return groups - end - - PgRole = Struct.new :name, :member_names - - # List of default roles taken from https://www.postgresql.org/docs/current/static/default-roles.html - PG_BUILTIN_ROLES = %w[ pg_signal_backend pg_monitor pg_read_all_settings pg_read_all_stats pg_stat_scan_tables] - - def search_pg_users - pg_users_conf = @config[:pg_users] - - users = [] - res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}" - res.each do |tuple| - user = PgRole.new tuple[0] - next if PG_BUILTIN_ROLES.include?(user.name) - log.info{ "found pg-user: #{user.name.inspect}"} - users << user - end - return users - end - - def search_pg_groups - pg_groups_conf = @config[:pg_groups] - - groups = [] - res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}" - res.each do |tuple| - 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])}" - member_names = res2.map{|row| row[0] } - group = PgRole.new tuple[0], member_names - next if PG_BUILTIN_ROLES.include?(group.name) - log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} - groups << group - end - - return groups - end - - def search_pg_ldap_groups - pg_users_conf = @config[:pg_users] - pg_groups_conf = @config[:pg_groups] - - in_group = "" - if pg_groups_conf[:grant_this_group] != nil - in_group += "'#{pg_groups_conf[:grant_this_group]}'" - end - if pg_users_conf[:grant_this_group] != nil - if in_group != "" - in_group += ", " - end - in_group += "'#{pg_users_conf[:grant_this_group]}'" - end - if pg_groups_conf[:grant_this_group] != nil || pg_users_conf[:grant_this_group] != nil - groups = [] - res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE rolname IN (#{in_group})" - res.each do |tuple| - 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])}" - member_names = res2.map{|row| row[0] } - group = PgRole.new tuple[0], member_names - log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} - groups << group - end - groups = uniq_names groups - end - - return groups - end - - def uniq_names(list) - names = {} - new_list = list.select do |entry| - name = entry.name - if names[name] - log.warn{ "duplicated group/user #{name.inspect} (#{entry.inspect})" } - next false - else - names[name] = true - next true - end - end - return new_list - end - - MatchedRole = Struct.new :ldap, :pg, :name, :state, :type - - def match_roles(ldaps, pgs, type) - ldap_by_name = ldaps.inject({}){|h,u| h[u.name] = u; h } - pg_by_name = pgs.inject({}){|h,u| h[u.name] = u; h } - - roles = [] - ldaps.each do |ld| - pg = pg_by_name[ld.name] - role = MatchedRole.new ld, pg, ld.name - roles << role - end - pgs.each do |pg| - ld = ldap_by_name[pg.name] - next if ld - role = MatchedRole.new ld, pg, pg.name - roles << role - end - - roles.each do |r| - r.state = case - when r.ldap && !r.pg then :create - when !r.ldap && r.pg then :drop - when r.pg && r.ldap then :keep - else raise "invalid user #{r.inspect}" - end - r.type = type - end - - log.info{ - roles.each do |role| - log.debug{ "#{role.state} #{role.type}: #{role.name}" } - end - "#{type} stat: create: #{roles.count{|r| r.state==:create }} drop: #{roles.count{|r| r.state==:drop }} keep: #{roles.count{|r| r.state==:keep }}" - } - return roles - end - - def match_users_groups(roles) - - # Find out if there are matching users and groups - # Process Users - roles.each do |ru| - next if ru.type == :group - - # Find Matching Group, if any - roles.each do |rg| - next if rg.type == :user - next if rg.name != ru.name - - if ru.state == :create && rg.state == :keep - ru.state = :alter - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - elsif ru.state == :keep && rg.state == :create - rg.state = :group_add - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :create && rg.state == :drop - ru.state = :alter - rg.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :drop && rg.state == :create - rg.state = :alter - ru.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :drop && rg.state == :keep - rg.state = :alter - ru.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :keep && rg.state == :drop - ru.state = :alter - rg.state = :group_drop - log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :create && rg.state == :create - rg.state = :group_add; - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - elsif ru.state == :drop && rg.state == :drop - rg.state = :keep - log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } - end - # The ru.state == :keep && rg.state == :keep we don't care about as no changes are needed - end - end - - log.info{ - "Revised user stat: create: #{roles.count{|r| r.state==:create && r.type==:user }} drop: #{roles.count{|r| r.state==:drop && r.type==:user }} alter: #{roles.count{|r| r.state==:alter && r.type==:user}} keep: #{roles.count{|r| r.state==:keep && r.type==:user}}" - } - log.info{ - "Revised group stat: create: #{roles.count{|r| r.state==:create && r.type==:group }} drop: #{roles.count{|r| r.state==:drop && r.type==:group }} alter: #{roles.count{|r| r.state==:alter && r.type==:group}} keep: #{roles.count{|r| r.state==:keep && r.type==:group}}" - } - return roles - end - - def try_sql(text) - begin - @pgconn.exec "SAVEPOINT try_sql;" - @pgconn.exec text - rescue PG::Error => err - @pgconn.exec "ROLLBACK TO try_sql;" - log.error{ "#{err} (#{err.class})" } - end - end - - - def pg_exec_modify(sql) - log.info{ "SQL: #{sql}" } - unless self.test - try_sql sql - end - end - - def pg_exec(sql) - res = @pgconn.exec sql - (0...res.num_tuples).map{|t| (0...res.num_fields).map{|i| res.getvalue(t, i) } } - end - - def create_pg_role(role) - pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] - if pg_conf[:grant_this_group] != nil - res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{role.name}'" - if res[0] == nil - pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" - else - pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" - end - else - pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" - end - end - - def drop_pg_role(role) - pg_exec_modify "DROP ROLE \"#{role.name}\"" - end - - def alter_pg_user(role) - pg_exec_modify "ALTER ROLE \"#{role.name}\" LOGIN" - set_pg_group(role) - end - - def alter_pg_group(role) - pg_exec_modify "ALTER ROLE \"#{role.name}\" NOLOGIN" - set_pg_group(role) - end - - def set_pg_group(role) - pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] - if pg_conf[:grant_this_group] != nil - if role.state == :group_add || role.state == :alter - skip_grant = :false - if !@pg_ldap_groups.empty? - @pg_ldap_groups.each do |group| - if group.name == pg_conf[:grant_this_group] - group.member_names.each do |member| - if member == role.name - skip_grant = :true - end - end - end - end - end - if skip_grant == :false - pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" - end - else - pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" - end - end - end - - def sync_roles_to_pg(roles, for_state) - roles.sort{|a,b| a.name<=>b.name }.each do |role| - create_pg_role(role) if role.state==:create && for_state==:create - set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:user - set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:user - set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:group - set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:group - alter_pg_user(role) if role.state==:alter && for_state==:alter && role.type==:user - alter_pg_group(role) if role.state==:alter && for_state==:group && role.type==:group - drop_pg_role(role) if role.state==:drop && for_state==:drop - end - end - - MatchedMembership = Struct.new :role_name, :has_member, :state - - def match_memberships(ldap_roles, pg_roles) - ldap_by_dn = ldap_roles.inject({}){|h,r| h[r.dn] = r; h } - ldap_by_m2m = ldap_roles.inject([]){|a,r| - next a unless r.member_dns - a + r.member_dns.map{|dn| - if has_member=ldap_by_dn[dn] - [r.name, has_member.name] - else - log.warn{"ldap member with dn #{dn} is unknown"} - nil - end - }.compact - } - - pg_by_name = pg_roles.inject({}){|h,r| h[r.name] = r; h } - pg_by_m2m = pg_roles.inject([]){|a,r| - next a unless r.member_names - a + r.member_names.map{|name| - if has_member=pg_by_name[name] - [r.name, has_member.name] - else - log.warn{"pg member with name #{name} is unknown"} - nil - end - }.compact - } - - memberships = (ldap_by_m2m & pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :keep } - memberships += (ldap_by_m2m - pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :grant } - memberships += (pg_by_m2m - ldap_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :revoke } - - log.info{ - memberships.each do |membership| - log.debug{ "#{membership.state} #{membership.role_name} to #{membership.has_member}" } - end - "membership stat: grant: #{memberships.count{|u| u.state==:grant }} revoke: #{memberships.count{|u| u.state==:revoke }} keep: #{memberships.count{|u| u.state==:keep }}" - } - return memberships - end - - def grant_membership(role_name, add_members) - pg_conf = @config[:pg_groups] - add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",") - pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}" - end - - def revoke_membership(role_name, rm_members) - rm_members_escaped = rm_members.map{|m| "\"#{m}\"" }.join(",") - pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}" - end - - def sync_membership_to_pg(memberships, for_state) - grants = {} - memberships.select{|ms| ms.state==for_state }.each do |ms| - grants[ms.role_name] ||= [] - grants[ms.role_name] << ms.has_member - end - - grants.each do |role_name, members| - grant_membership(role_name, members) if for_state==:grant - revoke_membership(role_name, members) if for_state==:revoke - end - end - - def check_groups() - pg_users_conf = @config[:pg_users] - if pg_users_conf[:grant_this_group] != nil - check_make_group(pg_users_conf[:grant_this_group]) - else - log.debug{"No Users LDAP group to Check/Create"} - end - pg_groups_conf = @config[:pg_groups] - if pg_groups_conf[:grant_this_group] != nil - check_make_group(pg_groups_conf[:grant_this_group]) - else - log.debug{"No Groups LDAP group to Check/Create"} - end - end - - def check_make_group(group_name) - res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{group_name}'" - if res[0] == nil - log.debug{ "Creating Group: #{group_name}"} - pg_exec_modify "CREATE ROLE \"#{group_name}\"" - else - log.info{ "Lookup Found Group: #{res[0][0]}"} - end - end - - def start! - read_config_file(@config_fname) - - # gather LDAP users and groups - @ldap = Net::LDAP.new @config[:ldap_connection] - ldap_users = uniq_names search_ldap_users - ldap_groups = uniq_names search_ldap_groups - - # gather PGs users and groups - @pgconn = PG.connect @config[:pg_connection] - begin - @pgconn.transaction do - pg_users = uniq_names search_pg_users - pg_groups = uniq_names search_pg_groups - @pg_ldap_groups = search_pg_ldap_groups - - # compare LDAP to PG users and groups - mroles = match_roles(ldap_users, pg_users, :user) - mroles += match_roles(ldap_groups, pg_groups, :group) - mroles = match_users_groups(mroles) - - # compare LDAP to PG memberships - mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) - - # Check to see if grant_this_group exists and if not create the group - check_groups() - - # drop/revoke roles/memberships first - sync_membership_to_pg(mmemberships, :revoke) - sync_roles_to_pg(mroles, :drop) - # Make Login if this used to be a non-login role and Non-Login if this used to be a login role - sync_roles_to_pg(mroles, :alter) - # create/grant roles/memberships - sync_roles_to_pg(mroles, :create) - # Fix group memberships - sync_roles_to_pg(mroles, :group) - - # Reload users and group in case they had been created but were not within the proper "grant_this_group" - pg_users = uniq_names search_pg_users - pg_groups = uniq_names search_pg_groups - @pg_ldap_groups = search_pg_ldap_groups - - # compare reloaded LDAP to PG memberships - mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) - - # The reload keeps the grants from throwing warnings about role already being part of the group - sync_membership_to_pg(mmemberships, :grant) - - end - ensure - @pgconn.close - end - - # Determine exitcode - if log.had_errors? - raise ErrorExit, 1 - end - end - - def self.run(argv) - s = self.new - s.config_fname = '/etc/pg_ldap_sync.yaml' - s.log = Logger.new($stdout, @error_counters) - s.log.level = Logger::ERROR - - OptionParser.new do |opts| - opts.version = VERSION - opts.banner = "Usage: #{$0} [options]" - opts.on("-v", "--[no-]verbose", "Increase verbose level"){|v| s.log.level += v ? -1 : 1 } - opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=)) - opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=)) - - opts.parse!(argv) - end - - s.start! - end -end -end +#!/usr/bin/env ruby + +require 'net/ldap' +require 'optparse' +require 'yaml' +require 'kwalify' +require 'pg' +require "pg_ldap_sync/logger" + +module PgLdapSync +class Application + attr_accessor :config_fname + attr_accessor :log + attr_accessor :test + + def string_to_symbol(hash) + if hash.kind_of?(Hash) + return hash.inject({}){|h, v| + raise "expected String instead of #{h.inspect}" unless v[0].kind_of?(String) + h[v[0].intern] = string_to_symbol(v[1]) + h + } + else + return hash + end + end + + + def validate_config(config, schema, fname) + schema = YAML.load_file(schema) + validator = Kwalify::Validator.new(schema) + errors = validator.validate(config) + if errors && !errors.empty? + errors.each do |err| + log.fatal "error in #{fname}: [#{err.path}] #{err.message}" + end + raise InvalidConfig, 78 # EX_CONFIG + end + end + + def read_config_file(fname) + raise "Config file #{fname.inspect} does not exist" unless File.exist?(fname) + config = YAML.load(File.read(fname)) + + schema_fname = File.join(File.dirname(__FILE__), '../../config/schema.yaml') + validate_config(config, schema_fname, fname) + + @config = string_to_symbol(config) + end + + LdapRole = Struct.new :name, :dn, :member_dns + + def search_ldap_users + ldap_user_conf = @config[:ldap_users] + + users = [] + res = @ldap.search(:base => ldap_user_conf[:base], :filter => ldap_user_conf[:filter]) do |entry| + entry.dn = entry.dn.downcase + name = entry[ldap_user_conf[:name_attribute]].first + + unless name + log.warn "user attribute #{ldap_user_conf[:name_attribute].inspect} not defined for #{entry.dn}" + next + end + name.downcase! if ldap_user_conf[:lowercase_name] + + log.info "found user-dn: #{entry.dn}" + user = LdapRole.new name, entry.dn + users << user + entry.each do |attribute, values| + log.debug " #{attribute}:" + values.each do |value| + log.debug " --->#{value.inspect}" + end + end + end + raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res + return users + end + + def search_ldap_groups + ldap_group_conf = @config[:ldap_groups] + + groups = [] + res = @ldap.search(:base => ldap_group_conf[:base], :filter => ldap_group_conf[:filter]) do |entry| + entry.dn = entry.dn.downcase + name = entry[ldap_group_conf[:name_attribute]].first + + unless name + log.warn "user attribute #{ldap_group_conf[:name_attribute].inspect} not defined for #{entry.dn}" + next + end + name.downcase! if ldap_group_conf[:lowercase_name] + + log.info "found group-dn: #{entry.dn}" + group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]].map!(&:downcase) + groups << group + entry.each do |attribute, values| + log.debug " #{attribute}:" + values.each do |value| + log.debug " --->#{value.inspect}" + end + end + end + raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res + return groups + end + + PgRole = Struct.new :name, :member_names + + # List of default roles taken from https://www.postgresql.org/docs/current/static/default-roles.html + PG_BUILTIN_ROLES = %w[ pg_signal_backend pg_monitor pg_read_all_settings pg_read_all_stats pg_stat_scan_tables] + + def search_pg_users + pg_users_conf = @config[:pg_users] + + users = [] + res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}" + res.each do |tuple| + user = PgRole.new tuple[0] + next if PG_BUILTIN_ROLES.include?(user.name) + log.info{ "found pg-user: #{user.name.inspect}"} + users << user + end + return users + end + + def search_pg_groups + pg_groups_conf = @config[:pg_groups] + + groups = [] + res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}" + res.each do |tuple| + 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])}" + member_names = res2.map{|row| row[0] } + group = PgRole.new tuple[0], member_names + next if PG_BUILTIN_ROLES.include?(group.name) + log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} + groups << group + end + + return groups + end + + def search_pg_ldap_groups + pg_users_conf = @config[:pg_users] + pg_groups_conf = @config[:pg_groups] + + in_group = "" + if pg_groups_conf[:grant_this_group] != nil + in_group += "'#{pg_groups_conf[:grant_this_group]}'" + end + if pg_users_conf[:grant_this_group] != nil + if in_group != "" + in_group += ", " + end + in_group += "'#{pg_users_conf[:grant_this_group]}'" + end + if pg_groups_conf[:grant_this_group] != nil || pg_users_conf[:grant_this_group] != nil + groups = [] + res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE rolname IN (#{in_group})" + res.each do |tuple| + 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])}" + member_names = res2.map{|row| row[0] } + group = PgRole.new tuple[0], member_names + log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"} + groups << group + end + groups = uniq_names groups + end + + return groups + end + + def uniq_names(list) + names = {} + new_list = list.select do |entry| + name = entry.name + if names[name] + log.warn{ "duplicated group/user #{name.inspect} (#{entry.inspect})" } + next false + else + names[name] = true + next true + end + end + return new_list + end + + MatchedRole = Struct.new :ldap, :pg, :name, :state, :type + + def match_roles(ldaps, pgs, type) + ldap_by_name = ldaps.inject({}){|h,u| h[u.name] = u; h } + pg_by_name = pgs.inject({}){|h,u| h[u.name] = u; h } + + roles = [] + ldaps.each do |ld| + pg = pg_by_name[ld.name] + role = MatchedRole.new ld, pg, ld.name + roles << role + end + pgs.each do |pg| + ld = ldap_by_name[pg.name] + next if ld + role = MatchedRole.new ld, pg, pg.name + roles << role + end + + roles.each do |r| + r.state = case + when r.ldap && !r.pg then :create + when !r.ldap && r.pg then :drop + when r.pg && r.ldap then :keep + else raise "invalid user #{r.inspect}" + end + r.type = type + end + + log.info{ + roles.each do |role| + log.debug{ "#{role.state} #{role.type}: #{role.name}" } + end + "#{type} stat: create: #{roles.count{|r| r.state==:create }} drop: #{roles.count{|r| r.state==:drop }} keep: #{roles.count{|r| r.state==:keep }}" + } + return roles + end + + def match_users_groups(roles) + + # Find out if there are matching users and groups + # Process Users + roles.each do |ru| + next if ru.type == :group + + # Find Matching Group, if any + roles.each do |rg| + next if rg.type == :user + next if rg.name != ru.name + + if ru.state == :create && rg.state == :keep + ru.state = :alter + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + elsif ru.state == :keep && rg.state == :create + rg.state = :group_add + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :create && rg.state == :drop + ru.state = :alter + rg.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :create + rg.state = :alter + ru.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :keep + rg.state = :alter + ru.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :keep && rg.state == :drop + ru.state = :alter + rg.state = :group_drop + log.info{ "Changed #{ru.state} #{ru.type}: #{ru.name}" } + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :create && rg.state == :create + rg.state = :group_add; + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + elsif ru.state == :drop && rg.state == :drop + rg.state = :keep + log.info{ "Changed #{rg.state} #{rg.type}: #{rg.name}" } + end + # The ru.state == :keep && rg.state == :keep we don't care about as no changes are needed + end + end + + log.info{ + "Revised user stat: create: #{roles.count{|r| r.state==:create && r.type==:user }} drop: #{roles.count{|r| r.state==:drop && r.type==:user }} alter: #{roles.count{|r| r.state==:alter && r.type==:user}} keep: #{roles.count{|r| r.state==:keep && r.type==:user}}" + } + log.info{ + "Revised group stat: create: #{roles.count{|r| r.state==:create && r.type==:group }} drop: #{roles.count{|r| r.state==:drop && r.type==:group }} alter: #{roles.count{|r| r.state==:alter && r.type==:group}} keep: #{roles.count{|r| r.state==:keep && r.type==:group}}" + } + return roles + end + + def try_sql(text) + begin + @pgconn.exec "SAVEPOINT try_sql;" + @pgconn.exec text + rescue PG::Error => err + @pgconn.exec "ROLLBACK TO try_sql;" + log.error{ "#{err} (#{err.class})" } + end + end + + + def pg_exec_modify(sql) + log.info{ "SQL: #{sql}" } + unless self.test + try_sql sql + end + end + + def pg_exec(sql) + res = @pgconn.exec sql + (0...res.num_tuples).map{|t| (0...res.num_fields).map{|i| res.getvalue(t, i) } } + end + + def create_pg_role(role) + pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] + if pg_conf[:grant_this_group] != nil + res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{role.name}'" + if res[0] == nil + pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" + else + pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + end + else + pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" + end + end + + def drop_pg_role(role) + pg_exec_modify "DROP ROLE \"#{role.name}\"" + end + + def alter_pg_user(role) + pg_exec_modify "ALTER ROLE \"#{role.name}\" LOGIN" + set_pg_group(role) + end + + def alter_pg_group(role) + pg_exec_modify "ALTER ROLE \"#{role.name}\" NOLOGIN" + set_pg_group(role) + end + + def set_pg_group(role) + pg_conf = @config[role.type==:user ? :pg_users : :pg_groups] + if pg_conf[:grant_this_group] != nil + if role.state == :group_add || role.state == :alter + skip_grant = :false + if !@pg_ldap_groups.empty? + @pg_ldap_groups.each do |group| + if group.name == pg_conf[:grant_this_group] + group.member_names.each do |member| + if member == role.name + skip_grant = :true + end + end + end + end + end + if skip_grant == :false + pg_exec_modify "GRANT \"#{pg_conf[:grant_this_group]}\" TO \"#{role.name}\"" + end + else + pg_exec_modify "REVOKE \"#{pg_conf[:grant_this_group]}\" FROM \"#{role.name}\"" + end + end + end + + def sync_roles_to_pg(roles, for_state) + roles.sort{|a,b| a.name<=>b.name }.each do |role| + create_pg_role(role) if role.state==:create && for_state==:create + set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:user + set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:user + set_pg_group(role) if role.state==:group_add && for_state==:group && role.type==:group + set_pg_group(role) if role.state==:group_drop && for_state==:group && role.type==:group + alter_pg_user(role) if role.state==:alter && for_state==:alter && role.type==:user + alter_pg_group(role) if role.state==:alter && for_state==:group && role.type==:group + drop_pg_role(role) if role.state==:drop && for_state==:drop + end + end + + MatchedMembership = Struct.new :role_name, :has_member, :state + + def match_memberships(ldap_roles, pg_roles) + ldap_by_dn = ldap_roles.inject({}){|h,r| h[r.dn] = r; h } + ldap_by_m2m = ldap_roles.inject([]){|a,r| + next a unless r.member_dns + a + r.member_dns.map{|dn| + if has_member=ldap_by_dn[dn] + [r.name, has_member.name] + else + log.warn{"ldap member with dn #{dn} is unknown"} + nil + end + }.compact + } + + pg_by_name = pg_roles.inject({}){|h,r| h[r.name] = r; h } + pg_by_m2m = pg_roles.inject([]){|a,r| + next a unless r.member_names + a + r.member_names.map{|name| + if has_member=pg_by_name[name] + [r.name, has_member.name] + else + log.warn{"pg member with name #{name} is unknown"} + nil + end + }.compact + } + + memberships = (ldap_by_m2m & pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :keep } + memberships += (ldap_by_m2m - pg_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :grant } + memberships += (pg_by_m2m - ldap_by_m2m).map{|r,mo| MatchedMembership.new r, mo, :revoke } + + log.info{ + memberships.each do |membership| + log.debug{ "#{membership.state} #{membership.role_name} to #{membership.has_member}" } + end + "membership stat: grant: #{memberships.count{|u| u.state==:grant }} revoke: #{memberships.count{|u| u.state==:revoke }} keep: #{memberships.count{|u| u.state==:keep }}" + } + return memberships + end + + def grant_membership(role_name, add_members) + pg_conf = @config[:pg_groups] + add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",") + pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}" + end + + def revoke_membership(role_name, rm_members) + rm_members_escaped = rm_members.map{|m| "\"#{m}\"" }.join(",") + pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}" + end + + def sync_membership_to_pg(memberships, for_state) + grants = {} + memberships.select{|ms| ms.state==for_state }.each do |ms| + grants[ms.role_name] ||= [] + grants[ms.role_name] << ms.has_member + end + + grants.each do |role_name, members| + grant_membership(role_name, members) if for_state==:grant + revoke_membership(role_name, members) if for_state==:revoke + end + end + + def check_groups() + pg_users_conf = @config[:pg_users] + if pg_users_conf[:grant_this_group] != nil + check_make_group(pg_users_conf[:grant_this_group]) + else + log.debug{"No Users LDAP group to Check/Create"} + end + pg_groups_conf = @config[:pg_groups] + if pg_groups_conf[:grant_this_group] != nil + check_make_group(pg_groups_conf[:grant_this_group]) + else + log.debug{"No Groups LDAP group to Check/Create"} + end + end + + def check_make_group(group_name) + res = pg_exec "SELECT pr.rolname FROM pg_roles pr WHERE pr.rolname='#{group_name}'" + if res[0] == nil + log.debug{ "Creating Group: #{group_name}"} + pg_exec_modify "CREATE ROLE \"#{group_name}\"" + else + log.info{ "Lookup Found Group: #{res[0][0]}"} + end + end + + def start! + read_config_file(@config_fname) + + # gather LDAP users and groups + @ldap = Net::LDAP.new @config[:ldap_connection] + ldap_users = uniq_names search_ldap_users + ldap_groups = uniq_names search_ldap_groups + + # gather PGs users and groups + @pgconn = PG.connect @config[:pg_connection] + begin + @pgconn.transaction do + pg_users = uniq_names search_pg_users + pg_groups = uniq_names search_pg_groups + @pg_ldap_groups = search_pg_ldap_groups + + # compare LDAP to PG users and groups + mroles = match_roles(ldap_users, pg_users, :user) + mroles += match_roles(ldap_groups, pg_groups, :group) + mroles = match_users_groups(mroles) + + # compare LDAP to PG memberships + mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) + + # Check to see if grant_this_group exists and if not create the group + check_groups() + + # drop/revoke roles/memberships first + sync_membership_to_pg(mmemberships, :revoke) + sync_roles_to_pg(mroles, :drop) + # Make Login if this used to be a non-login role and Non-Login if this used to be a login role + sync_roles_to_pg(mroles, :alter) + # create/grant roles/memberships + sync_roles_to_pg(mroles, :create) + # Fix group memberships + sync_roles_to_pg(mroles, :group) + + # Reload users and group in case they had been created but were not within the proper "grant_this_group" + pg_users = uniq_names search_pg_users + pg_groups = uniq_names search_pg_groups + @pg_ldap_groups = search_pg_ldap_groups + + # compare reloaded LDAP to PG memberships + mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups) + + # The reload keeps the grants from throwing warnings about role already being part of the group + sync_membership_to_pg(mmemberships, :grant) + + end + ensure + @pgconn.close + end + + # Determine exitcode + if log.had_errors? + raise ErrorExit, 1 + end + end + + def self.run(argv) + s = self.new + s.config_fname = '/etc/pg_ldap_sync.yaml' + s.log = Logger.new($stdout, @error_counters) + s.log.level = Logger::ERROR + + OptionParser.new do |opts| + opts.version = VERSION + opts.banner = "Usage: #{$0} [options]" + opts.on("-v", "--[no-]verbose", "Increase verbose level"){|v| s.log.level += v ? -1 : 1 } + opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=)) + opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=)) + + opts.parse!(argv) + end + + s.start! + end +end +end diff --git a/lib/pg_ldap_sync/logger.rb b/lib/pg_ldap_sync/logger.rb index 3ea5575..e877c1b 100644 --- a/lib/pg_ldap_sync/logger.rb +++ b/lib/pg_ldap_sync/logger.rb @@ -1,24 +1,24 @@ -require 'logger' - -module PgLdapSync -class Logger < ::Logger - def initialize(io, counters) - super(io) - @counters = {} - end - - def add(severity, *args) - @counters[severity] ||= 0 - @counters[severity] += 1 - super - end - - def had_logged?(severity) - @counters[severity] && @counters[severity] > 0 - end - - def had_errors? - had_logged?(Logger::FATAL) || had_logged?(Logger::ERROR) - end -end +require 'logger' + +module PgLdapSync +class Logger < ::Logger + def initialize(io, counters) + super(io) + @counters = {} + end + + def add(severity, *args) + @counters[severity] ||= 0 + @counters[severity] += 1 + super + end + + def had_logged?(severity) + @counters[severity] && @counters[severity] > 0 + end + + def had_errors? + had_logged?(Logger::FATAL) || had_logged?(Logger::ERROR) + end +end end \ No newline at end of file diff --git a/lib/pg_ldap_sync/version.rb b/lib/pg_ldap_sync/version.rb index 683c027..31d357b 100644 --- a/lib/pg_ldap_sync/version.rb +++ b/lib/pg_ldap_sync/version.rb @@ -1,3 +1,3 @@ -module PgLdapSync - VERSION = "0.2.1" +module PgLdapSync + VERSION = "0.2.1" end \ No newline at end of file diff --git a/test/test_pg_ldap_sync.rb b/test/test_pg_ldap_sync.rb index 7a54d29..301d309 100644 --- a/test/test_pg_ldap_sync.rb +++ b/test/test_pg_ldap_sync.rb @@ -1,335 +1,335 @@ -#!/usr/bin/env ruby - -require 'test/unit' -#require "pg_ldap_sync/application" -require "pg_ldap_sync" -require 'yaml' -#require 'test/ldap_server' -require 'ldap_server' -require 'fileutils' -require_relative 'ldap_server' - -class TestPgLdapSync < Test::Unit::TestCase - def log_and_run( *cmd ) - puts cmd.join(' ') - system( *cmd ) - raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? - end - - def start_ldap_server(file_name) - yaml_fname = File.join(File.dirname(__FILE__), file_name) - @directory = File.open(yaml_fname){|f| YAML::load(f.read) } - - # Listen for incoming LDAP connections. For each one, create a Connection - # object, which will invoke a HashOperation object for each request. - - @ldap_server = LDAP::Server.new( - :port => 1389, - :nodelay => true, - :listen => 10, - # :ssl_key_file => "key.pem", - # :ssl_cert_file => "cert.pem", - # :ssl_on_connect => true, - :operation_class => HashOperation, - :operation_args => [@directory] - ) - @ldap_server.run_tcpserver - end - - def stop_ldap_server - @ldap_server.stop - end - - def start_pg_server - @port = 54321 - ENV['PGPORT'] = @port.to_s - ENV['PGHOST'] = 'localhost' - unless File.exist?('temp/pg_data/PG_VERSION') - FileUtils.mkdir_p 'temp/pg_data' - log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale' - end - log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start' - log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, \"bamm-bamm\", rubble_kids, wilma, \"Wilma Flintstone\", betty, rubble, pebbles, kids, flintstone_kids, \"Flintstones\", \"Wilmas\", \"All Users\", ldap_users, ldap_groups", 'postgres' - end - - def stop_pg_server - log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'stop' - end - - def setup - start_pg_server - end - - def teardown - stop_ldap_server - stop_pg_server - end - - def psqlre(*args) - /^\s*#{args[0]}[ |]*#{args[1]}[ |\{"]*#{args[2..-1].join('[", ]+')}["\}\s]*$/ - end - - def exec_psql_du - text = if RUBY_PLATFORM=~/mingw|mswin/ - `psql -d postgres -c "SELECT r.rolname AS \\"Role name\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\"Attributes\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\"Member of\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` - else - `psql -d postgres -c "SELECT r.rolname AS \\\"Role name\\\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\\"Attributes\\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\\"Member of\\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` - end - puts text - return text - end - -#=begin - def test_sanity1 - start_ldap_server("fixtures/ldapdb.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'].pop - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Wilmas'), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) - end -#=end - -#=begin - def test_sanity2 - start_ldap_server("fixtures/ldapdb.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'].pop - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) - end -#=end - -#=begin - def test_sanity3 - start_ldap_server("fixtures/ldapdb3.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) - assert_match(psqlre('betty', '', 'rubble'), psql_du) - assert_match(psqlre('rubble', ''), psql_du) - assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids'), psql_du) - assert_match(psqlre('kids', ''), psql_du) - assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) - assert_match(psqlre('rubble_kids','Cannot login'), psql_du) - assert_match(psqlre('flintstone_kids',''), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop - # remove user 'rubble' - @directory.delete('cn=rubble,dc=example,dc=com') - # reove user and group 'flintstone_kids' - @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') - @directory.delete('cn=flintstone_kids,dc=example,dc=com') - # remove group 'kids' - @directory.delete('cn=kids,ou=groups,dc=example,dc=com') - # remove group 'rubble_kids' - @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') - # add user 'rubble_kids' - @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_match(psqlre('wilma','','Wilmas'), psql_du) - assert_match(psqlre('betty', '', 'rubble'), psql_du) - assert_match(psqlre('rubble', 'Cannot login'), psql_du) - assert_match(psqlre('pebbles', ''), psql_du) - assert_match(psqlre('kids', ''), psql_du) - assert_no_match(/flintstone_kids/, psql_du) - assert_match(psqlre('bamm-bamm', ''), psql_du) - assert_match(psqlre('rubble_kids', ''), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - #Recreate User 'rubble' - @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} - #Recreate Group 'kids' - @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} - # remove user 'rubble_kids' - @directory.delete('cn=rubble_kids,dc=example,dc=com') - #Recreate Group 'rubble_kids' - @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login'), psql_du) - assert_match(psqlre('Flintstones','Cannot login'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) - assert_match(psqlre('pebbles', '', 'kids'), psql_du) - assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) - assert_match(psqlre('betty', '', 'rubble'), psql_du) - assert_match(psqlre('rubble', ''), psql_du) - assert_match(psqlre('rubble_kids', 'Cannot login'), psql_du) - assert_match(psqlre('kids', ''), psql_du) - end -#=end - -#=begin - def test_sanity4 - start_ldap_server("fixtures/ldapdb3.yaml") - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) - - ENV['LC_MESSAGES'] = 'C' - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) - assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) - assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) - assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids','ldap_users'), psql_du) - assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) - assert_match(psqlre('bamm-bamm', '','ldap_users', 'rubble_kids'), psql_du) - assert_match(psqlre('rubble_kids','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('flintstone_kids','','ldap_groups','ldap_users'), psql_du) - - # revoke membership of 'wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop - # remove user 'rubble' - @directory.delete('cn=rubble,dc=example,dc=com') - # reove user and group 'flintstone_kids' - @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') - @directory.delete('cn=flintstone_kids,dc=example,dc=com') - # remove group 'kids' - @directory.delete('cn=kids,ou=groups,dc=example,dc=com') - # remove group 'rubble_kids' - @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') - # add user 'rubble_kids' - @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login', 'ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login', 'ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users', 'ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones', 'ldap_users'), psql_du) - assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) - assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) - assert_match(psqlre('rubble', 'Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('pebbles', '','ldap_users'), psql_du) - assert_match(psqlre('kids', '','ldap_users'), psql_du) - assert_no_match(/flintstone_kids/, psql_du) - assert_match(psqlre('bamm-bamm', '','ldap_users'), psql_du) - assert_match(psqlre('rubble_kids', '','ldap_users'), psql_du) - - # rename role 'wilma' - @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] - # re-add 'Wilma' to 'Flintstones' - @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' - #Recreate User 'rubble' - @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} - #Recreate Group 'kids' - @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} - # remove user 'rubble_kids' - @directory.delete('cn=rubble_kids,dc=example,dc=com') - #Recreate Group 'rubble_kids' - @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} - - PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) - psql_du = exec_psql_du - - assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) - assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) - assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) - assert_no_match(/wilma/, psql_du) - assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) - assert_match(psqlre('pebbles', '', 'kids','ldap_users'), psql_du) - assert_match(psqlre('bamm-bamm', '', 'ldap_users','rubble_kids'), psql_du) - assert_match(psqlre('betty', '', 'ldap_users','rubble'), psql_du) - assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) - assert_match(psqlre('rubble_kids', 'Cannot login', 'ldap_groups'), psql_du) - assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) - end -#=end -end +#!/usr/bin/env ruby + +require 'test/unit' +#require "pg_ldap_sync/application" +require "pg_ldap_sync" +require 'yaml' +#require 'test/ldap_server' +require 'ldap_server' +require 'fileutils' +require_relative 'ldap_server' + +class TestPgLdapSync < Test::Unit::TestCase + def log_and_run( *cmd ) + puts cmd.join(' ') + system( *cmd ) + raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? + end + + def start_ldap_server(file_name) + yaml_fname = File.join(File.dirname(__FILE__), file_name) + @directory = File.open(yaml_fname){|f| YAML::load(f.read) } + + # Listen for incoming LDAP connections. For each one, create a Connection + # object, which will invoke a HashOperation object for each request. + + @ldap_server = LDAP::Server.new( + :port => 1389, + :nodelay => true, + :listen => 10, + # :ssl_key_file => "key.pem", + # :ssl_cert_file => "cert.pem", + # :ssl_on_connect => true, + :operation_class => HashOperation, + :operation_args => [@directory] + ) + @ldap_server.run_tcpserver + end + + def stop_ldap_server + @ldap_server.stop + end + + def start_pg_server + @port = 54321 + ENV['PGPORT'] = @port.to_s + ENV['PGHOST'] = 'localhost' + unless File.exist?('temp/pg_data/PG_VERSION') + FileUtils.mkdir_p 'temp/pg_data' + log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale' + end + log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start' + log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, \"bamm-bamm\", rubble_kids, wilma, \"Wilma Flintstone\", betty, rubble, pebbles, kids, flintstone_kids, \"Flintstones\", \"Wilmas\", \"All Users\", ldap_users, ldap_groups", 'postgres' + end + + def stop_pg_server + log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'stop' + end + + def setup + start_pg_server + end + + def teardown + stop_ldap_server + stop_pg_server + end + + def psqlre(*args) + /^\s*#{args[0]}[ |]*#{args[1]}[ |\{"]*#{args[2..-1].join('[", ]+')}["\}\s]*$/ + end + + def exec_psql_du + text = if RUBY_PLATFORM=~/mingw|mswin/ + `psql -d postgres -c "SELECT r.rolname AS \\"Role name\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\"Attributes\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\"Member of\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` + else + `psql -d postgres -c "SELECT r.rolname AS \\\"Role name\\\\", CASE WHEN r.rolcanlogin THEN '' ELSE 'Cannot login' END AS \\\"Attributes\\\", ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid ORDER BY lower(b.rolname) ASC) AS \\\"Member of\\\" FROM pg_catalog.pg_roles r ORDER BY 1;"` + end + puts text + return text + end + +#=begin + def test_sanity1 + start_ldap_server("fixtures/ldapdb.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'].pop + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Wilmas'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) + end +#=end + +#=begin + def test_sanity2 + start_ldap_server("fixtures/ldapdb.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'].pop + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=Flintstones,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb2.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) + end +#=end + +#=begin + def test_sanity3 + start_ldap_server("fixtures/ldapdb3.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', ''), psql_du) + assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids'), psql_du) + assert_match(psqlre('kids', ''), psql_du) + assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) + assert_match(psqlre('rubble_kids','Cannot login'), psql_du) + assert_match(psqlre('flintstone_kids',''), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop + # remove user 'rubble' + @directory.delete('cn=rubble,dc=example,dc=com') + # reove user and group 'flintstone_kids' + @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') + @directory.delete('cn=flintstone_kids,dc=example,dc=com') + # remove group 'kids' + @directory.delete('cn=kids,ou=groups,dc=example,dc=com') + # remove group 'rubble_kids' + @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') + # add user 'rubble_kids' + @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_match(psqlre('wilma','','Wilmas'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', 'Cannot login'), psql_du) + assert_match(psqlre('pebbles', ''), psql_du) + assert_match(psqlre('kids', ''), psql_du) + assert_no_match(/flintstone_kids/, psql_du) + assert_match(psqlre('bamm-bamm', ''), psql_du) + assert_match(psqlre('rubble_kids', ''), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + #Recreate User 'rubble' + @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} + #Recreate Group 'kids' + @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} + # remove user 'rubble_kids' + @directory.delete('cn=rubble_kids,dc=example,dc=com') + #Recreate Group 'rubble_kids' + @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb3.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login'), psql_du) + assert_match(psqlre('Flintstones','Cannot login'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) + assert_match(psqlre('pebbles', '', 'kids'), psql_du) + assert_match(psqlre('bamm-bamm', '', 'rubble_kids'), psql_du) + assert_match(psqlre('betty', '', 'rubble'), psql_du) + assert_match(psqlre('rubble', ''), psql_du) + assert_match(psqlre('rubble_kids', 'Cannot login'), psql_du) + assert_match(psqlre('kids', ''), psql_du) + end +#=end + +#=begin + def test_sanity4 + start_ldap_server("fixtures/ldapdb3.yaml") + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + + ENV['LC_MESSAGES'] = 'C' + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_match(psqlre('wilma','','Flintstones','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) + assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('pebbles', '', 'flintstone_kids', 'kids','ldap_users'), psql_du) + assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('bamm-bamm', '','ldap_users', 'rubble_kids'), psql_du) + assert_match(psqlre('rubble_kids','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('flintstone_kids','','ldap_groups','ldap_users'), psql_du) + + # revoke membership of 'wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'].pop + # remove user 'rubble' + @directory.delete('cn=rubble,dc=example,dc=com') + # reove user and group 'flintstone_kids' + @directory.delete('cn=flintstone_kids,ou=groups,dc=example,dc=com') + @directory.delete('cn=flintstone_kids,dc=example,dc=com') + # remove group 'kids' + @directory.delete('cn=kids,ou=groups,dc=example,dc=com') + # remove group 'rubble_kids' + @directory.delete('cn=rubble_kids,ou=groups,dc=example,dc=com') + # add user 'rubble_kids' + @directory['cn=rubble_kids,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'mail'=>['rubble_kids@bedrock.org'], 'sn'=>['rubble_kids'], 'sAMAccountName'=>['rubble_kids']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users', 'ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones', 'ldap_users'), psql_du) + assert_match(psqlre('wilma','','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('betty', '','ldap_users', 'rubble'), psql_du) + assert_match(psqlre('rubble', 'Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('pebbles', '','ldap_users'), psql_du) + assert_match(psqlre('kids', '','ldap_users'), psql_du) + assert_no_match(/flintstone_kids/, psql_du) + assert_match(psqlre('bamm-bamm', '','ldap_users'), psql_du) + assert_match(psqlre('rubble_kids', '','ldap_users'), psql_du) + + # rename role 'wilma' + @directory['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] + # re-add 'Wilma' to 'Flintstones' + @directory['cn=flintstones,ou=groups,dc=example,dc=com']['member'] << 'cn=Wilma Flintstone,dc=example,dc=com' + #Recreate User 'rubble' + @directory['cn=rubble,dc=example,dc=com'] = {'cn' => ['rubble'], 'mail'=>['rubble@bedrock.org'], 'sn'=>['rubble'], 'sAMAccountName'=>['rubble']} + #Recreate Group 'kids' + @directory['cn=kids,ou=groups,dc=example,dc=com'] = {'cn' => ['kids'], 'member'=>['cn=Pebbles Flintstone,dc=example,dc=com']} + # remove user 'rubble_kids' + @directory.delete('cn=rubble_kids,dc=example,dc=com') + #Recreate Group 'rubble_kids' + @directory['cn=rubble_kids,ou=groups,dc=example,dc=com'] = {'cn' => ['rubble_kids'], 'member'=>['cn=Bamm-Bamm Rubble,dc=example,dc=com']} + + PgLdapSync::Application.run(%w[-c test/fixtures/config-ldapdb4.yaml -vv]) + psql_du = exec_psql_du + + assert_match(psqlre('All Users','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Flintstones','Cannot login','ldap_groups'), psql_du) + assert_match(psqlre('Wilmas','Cannot login','All Users','ldap_groups'), psql_du) + assert_match(psqlre('fred','','All Users','Flintstones','ldap_users'), psql_du) + assert_no_match(/wilma/, psql_du) + assert_match(psqlre('Wilma Flintstone','','Flintstones','ldap_users','Wilmas'), psql_du) + assert_match(psqlre('pebbles', '', 'kids','ldap_users'), psql_du) + assert_match(psqlre('bamm-bamm', '', 'ldap_users','rubble_kids'), psql_du) + assert_match(psqlre('betty', '', 'ldap_users','rubble'), psql_du) + assert_match(psqlre('rubble', '','ldap_groups','ldap_users'), psql_du) + assert_match(psqlre('rubble_kids', 'Cannot login', 'ldap_groups'), psql_du) + assert_match(psqlre('kids', '','ldap_groups','ldap_users'), psql_du) + end +#=end +end From 545cfc0c415f44faf6dbe975609bebbae708e9da Mon Sep 17 00:00:00 2001 From: Lloyd Albin Date: Mon, 2 Nov 2020 22:12:31 -0800 Subject: [PATCH 8/9] Adding Travis --- .travis.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..921896c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +sudo: required +language: ruby +rvm: + - "2.0.0" + - ruby-head +env: + - "PGVERSION=10.0-1-linux-x64 PATH=\"/opt/PostgreSQL/10/bin:$PATH\"" + - "PGVERSION=9.3.19-1-linux-x64 PATH=\"/opt/PostgreSQL/9.3/bin:$PATH\"" +before_install: + - gem install bundler + - bundle install + # Download and install postgresql version to test against in /opt + - | + wget http://get.enterprisedb.com/postgresql/postgresql-$PGVERSION.run && \ + chmod +x postgresql-$PGVERSION.run && \ + sudo ./postgresql-$PGVERSION.run --extract-only 1 --mode unattended +script: rake test \ No newline at end of file From dc5c8bf1e6ccacdff1e1198df151771974169409 Mon Sep 17 00:00:00 2001 From: Lloyd Albin Date: Mon, 2 Nov 2020 22:37:12 -0800 Subject: [PATCH 9/9] Updated require statements --- exe/pg_ldap_sync | 3 ++- lib/pg_ldap_sync.rb | 6 ++++-- lib/pg_ldap_sync/application.rb | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/exe/pg_ldap_sync b/exe/pg_ldap_sync index c822722..82c4b90 100755 --- a/exe/pg_ldap_sync +++ b/exe/pg_ldap_sync @@ -1,6 +1,7 @@ #!/usr/bin/env ruby -require 'pg_ldap_sync' +require_relative '../lib/pg_ldap_sync' +#require 'pg_ldap_sync' begin PgLdapSync::Application.run(ARGV) diff --git a/lib/pg_ldap_sync.rb b/lib/pg_ldap_sync.rb index 21a6185..f96aee2 100644 --- a/lib/pg_ldap_sync.rb +++ b/lib/pg_ldap_sync.rb @@ -1,5 +1,7 @@ -require "pg_ldap_sync/application" -require "pg_ldap_sync/version" +require_relative "pg_ldap_sync/application" +require_relative "pg_ldap_sync/version" +#require "pg_ldap_sync/application" +#require "pg_ldap_sync/version" module PgLdapSync class LdapError < RuntimeError diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index e2ca37d..1c5d9f2 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -5,7 +5,8 @@ require 'yaml' require 'kwalify' require 'pg' -require "pg_ldap_sync/logger" +#require "pg_ldap_sync/logger" +require_relative 'logger' module PgLdapSync class Application