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:
parent
d9cb63ed98
commit
257b1a5e49
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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=))
|
||||||
|
|
||||||
|
24
lib/pg_ldap_sync/logger.rb
Normal file
24
lib/pg_ldap_sync/logger.rb
Normal 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
2
test/fixtures/config-invalid.yaml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
ldap_connection:
|
4
test/fixtures/config-ldapdb.yaml
vendored
4
test/fixtures/config-ldapdb.yaml
vendored
@ -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:
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user