Whole bunch of changes

Run sync within a SQL transaction, so that no partial sync happens

Don't abort on SQL errors, but print ERROR notice

Exit with code 1 when any ERRORs were logged

Silence the test suite, so that test runs are more clearly

Use PG queries instead of psql in test. This is more flexible
than parsing text outputs per Regexp.
This commit is contained in:
Lars Kanis 2018-03-13 16:21:20 +01:00
parent d9cb63ed98
commit 257b1a5e49
7 changed files with 164 additions and 46 deletions

View File

@ -2,4 +2,8 @@
require 'pg_ldap_sync' require 'pg_ldap_sync'
begin
PgLdapSync::Application.run(ARGV) PgLdapSync::Application.run(ARGV)
rescue PgLdapSync::ApplicationExit => ex
exit ex.exitcode
end

View File

@ -1,2 +1,21 @@
require "pg_ldap_sync/application" require "pg_ldap_sync/application"
require "pg_ldap_sync/version" 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

View File

@ -1,16 +1,14 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
require 'rubygems'
require 'net/ldap' require 'net/ldap'
require 'optparse' require 'optparse'
require 'yaml' require 'yaml'
require 'logger'
require 'kwalify' require 'kwalify'
require 'pg' require 'pg'
require "pg_ldap_sync/logger"
module PgLdapSync module PgLdapSync
class Application class Application
class LdapError < RuntimeError; end
attr_accessor :config_fname attr_accessor :config_fname
attr_accessor :log attr_accessor :log
attr_accessor :test attr_accessor :test
@ -36,7 +34,7 @@ class Application
errors.each do |err| errors.each do |err|
log.fatal "error in #{fname}: [#{err.path}] #{err.message}" log.fatal "error in #{fname}: [#{err.path}] #{err.message}"
end end
exit(-1) raise InvalidConfig, 78 # EX_CONFIG
end end
end end
@ -194,10 +192,21 @@ class Application
return roles return roles
end 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) def pg_exec_modify(sql)
log.info{ "SQL: #{sql}" } log.info{ "SQL: #{sql}" }
unless self.test unless self.test
res = @pgconn.exec sql try_sql sql
end end
end end
@ -298,6 +307,8 @@ class Application
# gather PGs users and groups # gather PGs users and groups
@pgconn = PG.connect @config[:pg_connection] @pgconn = PG.connect @config[:pg_connection]
begin
@pgconn.transaction do
pg_users = uniq_names search_pg_users pg_users = uniq_names search_pg_users
pg_groups = uniq_names search_pg_groups pg_groups = uniq_names search_pg_groups
@ -314,20 +325,27 @@ class Application
# create/grant roles/memberships # create/grant roles/memberships
sync_roles_to_pg(mroles, :create) sync_roles_to_pg(mroles, :create)
sync_membership_to_pg(mmemberships, :grant) sync_membership_to_pg(mmemberships, :grant)
end
ensure
@pgconn.close @pgconn.close
end end
# Determine exitcode
if log.had_errors?
raise ErrorExit, 1
end
end
def self.run(argv) def self.run(argv)
s = self.new s = self.new
s.config_fname = '/etc/pg_ldap_sync.yaml' s.config_fname = '/etc/pg_ldap_sync.yaml'
s.log = Logger.new(STDOUT) s.log = Logger.new($stdout, @error_counters)
s.log.level = Logger::ERROR s.log.level = Logger::ERROR
OptionParser.new do |opts| OptionParser.new do |opts|
opts.version = VERSION opts.version = VERSION
opts.banner = "Usage: #{$0} [options]" opts.banner = "Usage: #{$0} [options]"
opts.on("-v", "--[no-]verbose", "Increase verbose level"){ s.log.level-=1 } 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("-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.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=))

View File

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

2
test/fixtures/config-invalid.yaml vendored Normal file
View File

@ -0,0 +1,2 @@
---
ldap_connection:

View File

@ -5,7 +5,7 @@ ldap_connection:
ldap_users: ldap_users:
base: dc=example,dc=com base: dc=example,dc=com
filter: (&(cn=*)(sAMAccountName=*)) filter: (sAMAccountName=*)
name_attribute: sAMAccountName name_attribute: sAMAccountName
ldap_groups: ldap_groups:
@ -23,7 +23,7 @@ pg_connection:
# password: # password:
pg_users: pg_users:
filter: rolcanlogin AND NOT rolsuper filter: rolcanlogin AND NOT rolsuper AND rolname!='double_user'
create_options: LOGIN create_options: LOGIN
pg_groups: pg_groups:

View File

@ -8,10 +8,23 @@ require_relative 'ldap_server'
class TestPgLdapSync < Minitest::Test class TestPgLdapSync < Minitest::Test
include Minitest::Hooks include Minitest::Hooks
@@logid = 0
def log_and_run( *cmd ) def log_and_run( *cmd )
if $DEBUG
puts cmd.join(' ') puts cmd.join(' ')
system( *cmd ) system( *cmd )
raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success? raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
else
fname = "temp/run_#{@@logid+=1}.log"
pid = Process.spawn( *cmd, [:out, :err] => [fname, "w"] )
Process.wait(pid)
unless $?.success?
$stderr.puts "Command failed: [%s]\n%s" % [cmd.join(' '), File.read(fname)]
end
File.unlink fname
raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
end
end end
def start_ldap_server def start_ldap_server
@ -46,9 +59,13 @@ class TestPgLdapSync < Minitest::Test
log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale' log_and_run 'initdb', '-D', 'temp/pg_data', '--no-locale'
end end
log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start' log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'start'
@pgconn = PG.connect dbname: 'postgres'
@pgconn.exec "SET client_min_messages to warning"
end end
def stop_pg_server def stop_pg_server
@pgconn.close if @pgconn
log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'stop' log_and_run 'pg_ctl', '-w', '-o', "-k.", '-D', 'temp/pg_data', 'stop'
end end
@ -66,11 +83,27 @@ class TestPgLdapSync < Minitest::Test
end end
def setup def setup
log_and_run 'psql', '-e', '-c', "DROP ROLE IF EXISTS fred, wilma, \"Flintstones\", \"Wilmas\", \"All Users\"", 'postgres' @pgconn.exec "DROP ROLE IF EXISTS fred, wilma, \"Flintstones\", \"Wilmas\", \"All Users\", double_user"
end end
def psqlre(*args) def assert_role(role_name, attrs, member_of=[])
/^\s*#{args[0]}[ |]*#{args[1]}[ |\{"]*#{args[2..-1].join('[", ]+')}["\}\s]*$/ res = @pgconn.exec("SELECT * FROM pg_roles WHERE rolname = '#{@pgconn.escape_string(role_name)}'")
assert_equal 1, res.ntuples, "Role #{role_name} not found"
res2 = @pgconn.exec "SELECT pr.rolname FROM pg_auth_members pam JOIN pg_roles pr ON pr.oid=pam.roleid WHERE pam.member=#{res.to_a[0]['oid']}"
rolnames = res2.map{|t| t['rolname'] }
assert_equal member_of.sort, rolnames.sort
exp_attrs = []
exp_attrs << 'Cannot login' if res[0]['rolcanlogin'] != 't'
exp_attrs << 'Superuser' if res[0]['rolsuper'] == 't'
assert_equal attrs, exp_attrs.join(", ")
end
def refute_role(role_name)
res = @pgconn.exec("SELECT oid FROM pg_roles WHERE rolname = '#{@pgconn.escape_string(role_name)}'")
assert_equal 0, res.ntuples, "Role #{role_name} not found"
end end
def exec_psql_du def exec_psql_du
@ -89,7 +122,7 @@ class TestPgLdapSync < Minitest::Test
end end
def sync_with_config(config="config-ldapdb") def sync_with_config(config="config-ldapdb")
PgLdapSync::Application.run(["-c", "test/fixtures/#{config}.yaml", "-vv"]) PgLdapSync::Application.run(["-c", "test/fixtures/#{config}.yaml"] + ($DEBUG ? ["-vv"] : ["--no-verbose"]))
end end
def sync_to_fixture(fixture: "ldapdb", config: "config-ldapdb") def sync_to_fixture(fixture: "ldapdb", config: "config-ldapdb")
@ -103,41 +136,59 @@ class TestPgLdapSync < Minitest::Test
yield(@directory) yield(@directory)
sync_with_config sync_with_config
exec_psql_du exec_psql_du if $DEBUG
end
def test_invalid_config
assert_output(/key 'ldap_users:' is required/) do
assert_raises(PgLdapSync::InvalidConfig) do
sync_with_config("config-invalid")
end
end
end end
def test_base_users_groups_memberships def test_base_users_groups_memberships
psql_du = sync_change{} sync_change{}
assert_match(psqlre('All Users','Cannot login'), psql_du) assert_role('All Users', 'Cannot login')
assert_match(psqlre('Flintstones','Cannot login'), psql_du) assert_role('Flintstones', 'Cannot login')
assert_match(psqlre('Wilmas','Cannot login','All Users'), psql_du) assert_role('Wilmas', 'Cannot login', ['All Users'])
assert_match(psqlre('fred','','All Users','Flintstones'), psql_du) assert_role('fred', '', ['All Users', 'Flintstones'])
assert_match(psqlre('wilma','','Flintstones','Wilmas'), psql_du) assert_role('wilma', '', ['Flintstones', 'Wilmas'])
end end
def test_add_membership def test_add_membership
psql_du = sync_change do |dir| sync_change do |dir|
# add 'Fred' to 'Wilmas' # add 'Fred' to 'Wilmas'
@directory[0]['cn=Wilmas,dc=example,dc=com']['member'] << 'cn=Fred Flintstone,dc=example,dc=com' @directory[0]['cn=Wilmas,dc=example,dc=com']['member'] << 'cn=Fred Flintstone,dc=example,dc=com'
end end
assert_match(psqlre('fred','','All Users','Flintstones', 'Wilmas'), psql_du) assert_role('fred', '', ['All Users', 'Flintstones', 'Wilmas'])
end end
def test_revoke_membership def test_revoke_membership
psql_du = sync_change do |dir| sync_change do |dir|
# revoke membership of 'wilma' to 'Flintstones' # revoke membership of 'wilma' to 'Flintstones'
dir[0]['cn=Flintstones,dc=example,dc=com']['member'].pop dir[0]['cn=Flintstones,dc=example,dc=com']['member'].pop
end end
assert_match(psqlre('wilma','','Wilmas'), psql_du) assert_role('wilma', '', ['Wilmas'])
end end
def test_rename_role def test_rename_role
psql_du = sync_change do |dir| sync_change do |dir|
# rename role 'wilma' # rename role 'wilma'
dir[0]['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] dir[0]['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone']
end end
refute_match(/wilma/, psql_du) refute_role('wilma')
assert_match(psqlre('Wilma Flintstone','','Flintstones','Wilmas'), psql_du) assert_role('Wilma Flintstone', '', ['Flintstones', 'Wilmas'])
end
def test_dont_stop_on_error
log_and_run 'psql', '-e', '-c', "CREATE ROLE double_user LOGIN", 'postgres'
assert_raises(PgLdapSync::ErrorExit) do
sync_change do |dir|
dir[0]['cn=double_user,dc=example,dc=com'] = {'sAMAccountName' => 'double_user'}
end
end
end end
end end