2011-05-18 17:45:08 +04:00
|
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
|
|
|
|
|
require 'net/ldap'
|
|
|
|
|
require 'optparse'
|
|
|
|
|
require 'yaml'
|
2011-05-23 17:39:42 +04:00
|
|
|
|
require 'kwalify'
|
2018-02-07 00:29:48 +03:00
|
|
|
|
require 'pg'
|
2018-03-13 18:21:20 +03:00
|
|
|
|
require "pg_ldap_sync/logger"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
|
|
|
|
|
module PgLdapSync
|
|
|
|
|
class Application
|
|
|
|
|
attr_accessor :config_fname
|
2023-07-31 16:13:57 +03:00
|
|
|
|
attr_accessor :groups_fname
|
2011-05-18 17:45:08 +04:00
|
|
|
|
attr_accessor :log
|
|
|
|
|
attr_accessor :test
|
|
|
|
|
|
|
|
|
|
def string_to_symbol(hash)
|
|
|
|
|
if hash.kind_of?(Hash)
|
2022-01-17 18:08:40 +03:00
|
|
|
|
return hash.inject({}) do |h, v|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
raise "expected String instead of #{h.inspect}" unless v[0].kind_of?(String)
|
|
|
|
|
h[v[0].intern] = string_to_symbol(v[1])
|
|
|
|
|
h
|
2022-01-17 18:08:40 +03:00
|
|
|
|
end
|
2011-05-18 17:45:08 +04:00
|
|
|
|
else
|
|
|
|
|
return hash
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-05-23 17:39:42 +04:00
|
|
|
|
|
|
|
|
|
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
|
2018-03-13 18:21:20 +03:00
|
|
|
|
raise InvalidConfig, 78 # EX_CONFIG
|
2011-05-23 17:39:42 +04:00
|
|
|
|
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
|
|
|
|
|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
LdapRole = Struct.new :name, :dn, :member_dns
|
|
|
|
|
|
2023-07-31 15:21:59 +03:00
|
|
|
|
def read_groups_from_file(fname)
|
|
|
|
|
groups = []
|
|
|
|
|
File.foreach(fname) do |line|
|
|
|
|
|
# Удалите пробелы и символы новой строки с помощью strip
|
|
|
|
|
group = line.strip
|
|
|
|
|
groups << group unless group.empty?
|
|
|
|
|
end
|
|
|
|
|
groups
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def format_groups_for_ldap_users(groups, filter)
|
|
|
|
|
# Используйте метод map для преобразования каждой группы в соответствующий формат
|
|
|
|
|
formatted_groups = groups.map { |group| "(memberOf=CN=#{group},#{filter})" }
|
|
|
|
|
# Используйте метод join для объединения форматированных групп в одну строку с оператором "или" (|)
|
|
|
|
|
"(&(objectClass=organizationalPerson)(|" + formatted_groups.join + "))"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def format_groups_for_ldap_groups(groups)
|
|
|
|
|
# Используйте метод map для преобразования каждой группы в соответствующий формат
|
|
|
|
|
formatted_groups = groups.map { |group| "(cn=#{group})" }
|
|
|
|
|
# Используйте метод join для объединения форматированных групп в одну строку с оператором "или" (|)
|
|
|
|
|
"(|" + formatted_groups.join + ")"
|
|
|
|
|
end
|
|
|
|
|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
def search_ldap_users
|
|
|
|
|
ldap_user_conf = @config[:ldap_users]
|
2022-12-02 16:04:43 +03:00
|
|
|
|
name_attribute = ldap_user_conf[:name_attribute]
|
2011-05-18 17:45:08 +04:00
|
|
|
|
|
|
|
|
|
users = []
|
2023-07-31 15:21:59 +03:00
|
|
|
|
|
2023-07-31 16:24:06 +03:00
|
|
|
|
if @groups_fname != ''
|
2023-07-31 15:21:59 +03:00
|
|
|
|
@groups = read_groups_from_file(@groups_fname)
|
|
|
|
|
|
|
|
|
|
res = @ldap.search(
|
|
|
|
|
base: ldap_user_conf[:base],
|
|
|
|
|
filter: format_groups_for_ldap_users(@groups, ldap_user_conf[:filter]),
|
|
|
|
|
attributes: [name_attribute, :dn]
|
2023-07-31 15:51:47 +03:00
|
|
|
|
)
|
2023-07-31 15:21:59 +03:00
|
|
|
|
else
|
|
|
|
|
res = @ldap.search(
|
|
|
|
|
base: ldap_user_conf[:base],
|
|
|
|
|
filter: ldap_user_conf[:filter],
|
|
|
|
|
attributes: [name_attribute, :dn]
|
2023-07-31 15:51:47 +03:00
|
|
|
|
)
|
2023-07-31 15:21:59 +03:00
|
|
|
|
end
|
|
|
|
|
|
2023-07-31 15:51:47 +03:00
|
|
|
|
res.each do |entry|
|
2022-12-02 16:04:43 +03:00
|
|
|
|
name = entry[name_attribute].first
|
2011-05-18 17:45:08 +04:00
|
|
|
|
|
|
|
|
|
unless name
|
2022-12-02 16:04:43 +03:00
|
|
|
|
log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
next
|
|
|
|
|
end
|
|
|
|
|
log.info "found user-dn: #{entry.dn}"
|
2022-01-17 16:48:49 +03:00
|
|
|
|
|
|
|
|
|
names = if ldap_user_conf[:bothcase_name]
|
|
|
|
|
[name, name.downcase].uniq
|
|
|
|
|
elsif ldap_user_conf[:lowercase_name]
|
|
|
|
|
[name.downcase]
|
2022-04-18 21:31:24 +03:00
|
|
|
|
elsif ldap_user_conf[:uppercase_name]
|
|
|
|
|
[name.upcase]
|
2022-01-17 16:48:49 +03:00
|
|
|
|
else
|
|
|
|
|
[name]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
names.each do |n|
|
|
|
|
|
users << LdapRole.new(n, entry.dn)
|
|
|
|
|
end
|
2011-05-18 17:45:08 +04:00
|
|
|
|
entry.each do |attribute, values|
|
|
|
|
|
log.debug " #{attribute}:"
|
|
|
|
|
values.each do |value|
|
2011-05-23 12:49:25 +04:00
|
|
|
|
log.debug " --->#{value.inspect}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-07-07 17:07:44 +04:00
|
|
|
|
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
2011-05-18 17:45:08 +04:00
|
|
|
|
return users
|
|
|
|
|
end
|
|
|
|
|
|
2022-12-01 17:21:39 +03:00
|
|
|
|
def retrieve_array_attribute(entry, attribute_name)
|
|
|
|
|
array = entry[attribute_name]
|
|
|
|
|
if array.empty?
|
|
|
|
|
# Possibly an attribute, which must be retrieved in several ranges
|
|
|
|
|
|
|
|
|
|
ranged_attr = entry.attribute_names.find { |n| n =~ /\A#{Regexp.escape(attribute_name)};range=/ }
|
|
|
|
|
if ranged_attr
|
|
|
|
|
entry_dn = entry.dn
|
|
|
|
|
|
|
|
|
|
loop do
|
|
|
|
|
array += entry[ranged_attr]
|
|
|
|
|
log.debug "retrieved attribute range #{ranged_attr.inspect} of dn #{entry_dn}"
|
|
|
|
|
|
|
|
|
|
if ranged_attr =~ /;range=\d\-\*\z/
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
attribute_with_range = ranged_attr.to_s.gsub(/;range=.*/, ";range=#{array.size}-*")
|
|
|
|
|
entry = @ldap.search(
|
|
|
|
|
base: entry_dn,
|
|
|
|
|
scope: Net::LDAP::SearchScope_BaseObject,
|
|
|
|
|
attributes: attribute_with_range).first
|
|
|
|
|
|
|
|
|
|
ranged_attr = entry.attribute_names.find { |n| n =~ /\A#{Regexp.escape(attribute_name)};range=/ }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
# Values already received -> No ranged attribute
|
|
|
|
|
end
|
|
|
|
|
return array
|
|
|
|
|
end
|
|
|
|
|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
def search_ldap_groups
|
|
|
|
|
ldap_group_conf = @config[:ldap_groups]
|
2022-12-01 17:21:39 +03:00
|
|
|
|
name_attribute = ldap_group_conf[:name_attribute]
|
|
|
|
|
member_attribute = ldap_group_conf[:member_attribute]
|
2011-05-18 17:45:08 +04:00
|
|
|
|
|
|
|
|
|
groups = []
|
2023-07-31 15:21:59 +03:00
|
|
|
|
|
2023-07-31 16:24:06 +03:00
|
|
|
|
if @groups_fname != ''
|
2023-07-31 15:21:59 +03:00
|
|
|
|
res = @ldap.search(
|
|
|
|
|
base: ldap_group_conf[:base],
|
2023-07-31 17:42:05 +03:00
|
|
|
|
filter: format_groups_for_ldap_groups(@groups),
|
2023-07-31 15:21:59 +03:00
|
|
|
|
attributes: [name_attribute, member_attribute, :dn]
|
2023-07-31 15:51:47 +03:00
|
|
|
|
)
|
2023-07-31 15:21:59 +03:00
|
|
|
|
else
|
|
|
|
|
res = @ldap.search(
|
|
|
|
|
base: ldap_group_conf[:base],
|
|
|
|
|
filter: ldap_group_conf[:filter],
|
|
|
|
|
attributes: [name_attribute, member_attribute, :dn]
|
2023-07-31 15:51:47 +03:00
|
|
|
|
)
|
2023-07-31 15:21:59 +03:00
|
|
|
|
end
|
|
|
|
|
|
2023-07-31 15:51:47 +03:00
|
|
|
|
res.each do |entry|
|
2022-12-01 17:21:39 +03:00
|
|
|
|
name = entry[name_attribute].first
|
2011-05-18 17:45:08 +04:00
|
|
|
|
|
|
|
|
|
unless name
|
2022-12-01 17:21:39 +03:00
|
|
|
|
log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
next
|
|
|
|
|
end
|
2022-12-01 17:21:39 +03:00
|
|
|
|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
log.info "found group-dn: #{entry.dn}"
|
2022-01-17 16:48:49 +03:00
|
|
|
|
|
|
|
|
|
names = if ldap_group_conf[:bothcase_name]
|
|
|
|
|
[name, name.downcase].uniq
|
|
|
|
|
elsif ldap_group_conf[:lowercase_name]
|
|
|
|
|
[name.downcase]
|
2022-04-18 21:31:24 +03:00
|
|
|
|
elsif ldap_group_conf[:uppercase_name]
|
|
|
|
|
[name.upcase]
|
2022-01-17 16:48:49 +03:00
|
|
|
|
else
|
|
|
|
|
[name]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
names.each do |n|
|
2022-12-01 17:21:39 +03:00
|
|
|
|
group_members = retrieve_array_attribute(entry, member_attribute)
|
|
|
|
|
groups << LdapRole.new(n, entry.dn, group_members)
|
2022-01-17 16:48:49 +03:00
|
|
|
|
end
|
2011-05-18 17:45:08 +04:00
|
|
|
|
entry.each do |attribute, values|
|
|
|
|
|
log.debug " #{attribute}:"
|
|
|
|
|
values.each do |value|
|
2011-05-23 12:49:25 +04:00
|
|
|
|
log.debug " --->#{value.inspect}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-07-07 17:07:44 +04:00
|
|
|
|
raise LdapError, "LDAP: #{@ldap.get_operation_result.message}" unless res
|
2011-05-18 17:45:08 +04:00
|
|
|
|
return groups
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
PgRole = Struct.new :name, :member_names
|
|
|
|
|
|
2022-01-17 15:37:09 +03:00
|
|
|
|
# List of default roles taken from https://www.postgresql.org/docs/current/predefined-roles.html
|
2022-12-02 15:27:35 +03:00
|
|
|
|
PG_BUILTIN_ROLES = %w[ pg_read_all_data pg_write_all_data pg_read_all_settings pg_read_all_stats pg_stat_scan_tables pg_monitor pg_database_owner pg_signal_backend pg_read_server_files pg_write_server_files pg_execute_server_program pg_checkpoint]
|
2018-02-07 00:31:06 +03:00
|
|
|
|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
def search_pg_users
|
|
|
|
|
pg_users_conf = @config[:pg_users]
|
|
|
|
|
|
|
|
|
|
users = []
|
2011-07-07 18:17:01 +04:00
|
|
|
|
res = pg_exec "SELECT rolname FROM pg_roles WHERE #{pg_users_conf[:filter]}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
res.each do |tuple|
|
2011-05-24 15:52:21 +04:00
|
|
|
|
user = PgRole.new tuple[0]
|
2018-02-07 00:31:06 +03:00
|
|
|
|
next if PG_BUILTIN_ROLES.include?(user.name)
|
2011-05-24 15:52:21 +04:00
|
|
|
|
log.info{ "found pg-user: #{user.name.inspect}"}
|
2011-05-18 17:45:08 +04:00
|
|
|
|
users << user
|
|
|
|
|
end
|
|
|
|
|
return users
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def search_pg_groups
|
|
|
|
|
pg_groups_conf = @config[:pg_groups]
|
|
|
|
|
|
|
|
|
|
groups = []
|
2011-07-07 18:17:01 +04:00
|
|
|
|
res = pg_exec "SELECT rolname, oid FROM pg_roles WHERE #{pg_groups_conf[:filter]}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
res.each do |tuple|
|
2018-02-06 16:55:20 +03:00
|
|
|
|
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])}"
|
2011-05-24 15:52:21 +04:00
|
|
|
|
member_names = res2.map{|row| row[0] }
|
|
|
|
|
group = PgRole.new tuple[0], member_names
|
2018-02-07 00:31:06 +03:00
|
|
|
|
next if PG_BUILTIN_ROLES.include?(group.name)
|
2011-05-24 15:52:21 +04:00
|
|
|
|
log.info{ "found pg-group: #{group.name.inspect} with members: #{member_names.inspect}"}
|
2011-05-18 17:45:08 +04:00
|
|
|
|
groups << group
|
|
|
|
|
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 }
|
|
|
|
|
|
2011-05-23 12:34:53 +04:00
|
|
|
|
roles = []
|
2011-05-18 17:45:08 +04:00
|
|
|
|
ldaps.each do |ld|
|
|
|
|
|
pg = pg_by_name[ld.name]
|
2011-05-23 12:34:53 +04:00
|
|
|
|
role = MatchedRole.new ld, pg, ld.name
|
|
|
|
|
roles << role
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
pgs.each do |pg|
|
|
|
|
|
ld = ldap_by_name[pg.name]
|
|
|
|
|
next if ld
|
2011-05-23 12:34:53 +04:00
|
|
|
|
role = MatchedRole.new ld, pg, pg.name
|
|
|
|
|
roles << role
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
2011-05-23 12:34:53 +04:00
|
|
|
|
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}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
2011-05-23 12:34:53 +04:00
|
|
|
|
r.type = type
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
2022-01-17 18:08:40 +03:00
|
|
|
|
log.info do
|
2011-05-23 12:34:53 +04:00
|
|
|
|
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 }}"
|
2022-01-17 18:08:40 +03:00
|
|
|
|
end
|
2011-05-23 12:34:53 +04:00
|
|
|
|
return roles
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
2018-03-13 18:21:20 +03:00
|
|
|
|
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
|
|
|
|
|
|
2011-07-07 18:17:01 +04:00
|
|
|
|
def pg_exec_modify(sql)
|
2011-05-24 15:52:21 +04:00
|
|
|
|
log.info{ "SQL: #{sql}" }
|
2011-05-18 17:45:08 +04:00
|
|
|
|
unless self.test
|
2018-03-13 18:21:20 +03:00
|
|
|
|
try_sql sql
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-07-07 18:17:01 +04:00
|
|
|
|
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
|
|
|
|
|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
def create_pg_role(role)
|
|
|
|
|
pg_conf = @config[role.type==:user ? :pg_users : :pg_groups]
|
2011-07-07 18:17:01 +04:00
|
|
|
|
pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def drop_pg_role(role)
|
2011-07-07 18:17:01 +04:00
|
|
|
|
pg_exec_modify "DROP ROLE \"#{role.name}\""
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
2011-07-08 17:50:33 +04:00
|
|
|
|
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
|
|
|
|
|
drop_pg_role(role) if role.state==:drop && for_state==:drop
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-05-23 12:44:07 +04:00
|
|
|
|
MatchedMembership = Struct.new :role_name, :has_member, :state
|
2011-05-20 17:33:46 +04:00
|
|
|
|
|
|
|
|
|
def match_memberships(ldap_roles, pg_roles)
|
2022-04-18 21:38:32 +03:00
|
|
|
|
ldap_group_conf = @config[:ldap_groups]
|
2022-01-17 18:05:51 +03:00
|
|
|
|
hash_of_arrays = Hash.new { |h, k| h[k] = [] }
|
2022-04-18 21:38:32 +03:00
|
|
|
|
if ldap_group_conf[:ald_domain]
|
|
|
|
|
ldap_by_dn = ldap_roles.inject(hash_of_arrays){|h,r| h[r.name] << r; h }
|
|
|
|
|
else
|
|
|
|
|
ldap_by_dn = ldap_roles.inject(hash_of_arrays){|h,r| h[r.dn] << r; h }
|
|
|
|
|
end
|
2022-01-17 18:05:51 +03:00
|
|
|
|
ldap_by_m2m = ldap_roles.inject([]) do |a,r|
|
2011-05-20 17:33:46 +04:00
|
|
|
|
next a unless r.member_dns
|
2022-01-17 18:05:51 +03:00
|
|
|
|
a + r.member_dns.flat_map do |dn|
|
|
|
|
|
has_members = ldap_by_dn[dn]
|
|
|
|
|
log.warn{"ldap member with dn #{dn} is unknown"} if has_members.empty?
|
|
|
|
|
has_members.map do |has_member|
|
2011-05-23 12:44:07 +04:00
|
|
|
|
[r.name, has_member.name]
|
2011-05-20 17:33:46 +04:00
|
|
|
|
end
|
2022-01-17 18:05:51 +03:00
|
|
|
|
end
|
|
|
|
|
end
|
2011-05-20 17:33:46 +04:00
|
|
|
|
|
2022-01-17 18:05:51 +03:00
|
|
|
|
hash_of_arrays = Hash.new { |h, k| h[k] = [] }
|
|
|
|
|
pg_by_name = pg_roles.inject(hash_of_arrays){|h,r| h[r.name] << r; h }
|
|
|
|
|
pg_by_m2m = pg_roles.inject([]) do |a,r|
|
2011-05-20 17:33:46 +04:00
|
|
|
|
next a unless r.member_names
|
2022-01-17 18:05:51 +03:00
|
|
|
|
a + r.member_names.flat_map do |name|
|
|
|
|
|
has_members = pg_by_name[name]
|
|
|
|
|
log.warn{"pg member with name #{name} is unknown"} if has_members.empty?
|
|
|
|
|
has_members.map do |has_member|
|
2011-05-23 13:00:56 +04:00
|
|
|
|
[r.name, has_member.name]
|
2011-05-20 17:33:46 +04:00
|
|
|
|
end
|
2022-01-17 18:05:51 +03:00
|
|
|
|
end
|
|
|
|
|
end
|
2011-05-20 17:33:46 +04:00
|
|
|
|
|
|
|
|
|
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 }
|
2011-05-23 12:34:53 +04:00
|
|
|
|
|
2022-01-17 18:08:40 +03:00
|
|
|
|
log.info do
|
2011-05-23 12:34:53 +04:00
|
|
|
|
memberships.each do |membership|
|
2011-05-23 12:44:07 +04:00
|
|
|
|
log.debug{ "#{membership.state} #{membership.role_name} to #{membership.has_member}" }
|
2011-05-23 12:34:53 +04:00
|
|
|
|
end
|
|
|
|
|
"membership stat: grant: #{memberships.count{|u| u.state==:grant }} revoke: #{memberships.count{|u| u.state==:revoke }} keep: #{memberships.count{|u| u.state==:keep }}"
|
2022-01-17 18:08:40 +03:00
|
|
|
|
end
|
2011-05-20 17:33:46 +04:00
|
|
|
|
return memberships
|
|
|
|
|
end
|
|
|
|
|
|
2011-05-18 17:45:08 +04:00
|
|
|
|
def grant_membership(role_name, add_members)
|
2011-05-20 17:51:50 +04:00
|
|
|
|
pg_conf = @config[:pg_groups]
|
2011-05-18 17:45:08 +04:00
|
|
|
|
add_members_escaped = add_members.map{|m| "\"#{m}\"" }.join(",")
|
2011-07-07 18:17:01 +04:00
|
|
|
|
pg_exec_modify "GRANT \"#{role_name}\" TO #{add_members_escaped} #{pg_conf[:grant_options]}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def revoke_membership(role_name, rm_members)
|
|
|
|
|
rm_members_escaped = rm_members.map{|m| "\"#{m}\"" }.join(",")
|
2011-07-07 18:17:01 +04:00
|
|
|
|
pg_exec_modify "REVOKE \"#{role_name}\" FROM #{rm_members_escaped}"
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
2011-07-08 17:50:33 +04:00
|
|
|
|
def sync_membership_to_pg(memberships, for_state)
|
2011-05-20 17:33:46 +04:00
|
|
|
|
grants = {}
|
2011-07-08 17:50:33 +04:00
|
|
|
|
memberships.select{|ms| ms.state==for_state }.each do |ms|
|
2011-05-20 17:33:46 +04:00
|
|
|
|
grants[ms.role_name] ||= []
|
2011-05-23 12:44:07 +04:00
|
|
|
|
grants[ms.role_name] << ms.has_member
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
2011-05-20 17:33:46 +04:00
|
|
|
|
|
2011-07-08 17:50:33 +04:00
|
|
|
|
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
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def start!
|
2011-05-23 17:39:42 +04:00
|
|
|
|
read_config_file(@config_fname)
|
2011-05-18 17:45:08 +04:00
|
|
|
|
|
2023-02-03 21:45:30 +03:00
|
|
|
|
ldap_conf = @config[:ldap_connection]
|
|
|
|
|
auth_meth = ldap_conf.dig(:auth, :method).to_s
|
|
|
|
|
if auth_meth == "gssapi"
|
|
|
|
|
begin
|
|
|
|
|
require 'net/ldap/auth_adapter/gssapi'
|
|
|
|
|
rescue LoadError => err
|
|
|
|
|
raise "#{err}\nTo use GSSAPI authentication please run:\n gem install net-ldap-auth_adapter-gssapi"
|
|
|
|
|
end
|
|
|
|
|
elsif auth_meth == "gss_spnego"
|
|
|
|
|
begin
|
|
|
|
|
require 'net-ldap-gss-spnego'
|
|
|
|
|
# This doesn't work since this file is defined in net-ldap as a placeholder:
|
|
|
|
|
# require 'net/ldap/auth_adapter/gss_spnego'
|
|
|
|
|
rescue LoadError => err
|
|
|
|
|
raise "#{err}\nTo use GSSAPI authentication please run:\n gem install net-ldap-gss-spnego"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-05-23 17:39:42 +04:00
|
|
|
|
# gather LDAP users and groups
|
2023-02-03 21:45:30 +03:00
|
|
|
|
@ldap = Net::LDAP.new ldap_conf
|
2011-05-18 17:45:08 +04:00
|
|
|
|
ldap_users = uniq_names search_ldap_users
|
|
|
|
|
ldap_groups = uniq_names search_ldap_groups
|
|
|
|
|
|
2011-05-23 17:39:42 +04:00
|
|
|
|
# gather PGs users and groups
|
2018-01-31 13:47:28 +03:00
|
|
|
|
@pgconn = PG.connect @config[:pg_connection]
|
2018-03-13 18:21:20 +03:00
|
|
|
|
begin
|
|
|
|
|
@pgconn.transaction do
|
|
|
|
|
pg_users = uniq_names search_pg_users
|
|
|
|
|
pg_groups = uniq_names search_pg_groups
|
|
|
|
|
|
|
|
|
|
# compare LDAP to PG users and groups
|
|
|
|
|
mroles = match_roles(ldap_users, pg_users, :user)
|
|
|
|
|
mroles += match_roles(ldap_groups, pg_groups, :group)
|
|
|
|
|
|
|
|
|
|
# compare LDAP to PG memberships
|
|
|
|
|
mmemberships = match_memberships(ldap_users+ldap_groups, pg_users+pg_groups)
|
|
|
|
|
|
|
|
|
|
# drop/revoke roles/memberships first
|
|
|
|
|
sync_membership_to_pg(mmemberships, :revoke)
|
|
|
|
|
sync_roles_to_pg(mroles, :drop)
|
|
|
|
|
# create/grant roles/memberships
|
|
|
|
|
sync_roles_to_pg(mroles, :create)
|
|
|
|
|
sync_membership_to_pg(mmemberships, :grant)
|
|
|
|
|
end
|
|
|
|
|
ensure
|
|
|
|
|
@pgconn.close
|
|
|
|
|
end
|
2011-05-24 13:24:32 +04:00
|
|
|
|
|
2018-03-13 18:21:20 +03:00
|
|
|
|
# Determine exitcode
|
|
|
|
|
if log.had_errors?
|
2022-12-02 15:18:28 +03:00
|
|
|
|
raise ErrorExit.new(1, log.first_error)
|
2018-03-13 18:21:20 +03:00
|
|
|
|
end
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.run(argv)
|
|
|
|
|
s = self.new
|
|
|
|
|
s.config_fname = '/etc/pg_ldap_sync.yaml'
|
2023-07-31 16:01:11 +03:00
|
|
|
|
s.groups_fname = ''
|
2022-12-02 15:18:28 +03:00
|
|
|
|
s.log = Logger.new($stdout)
|
2011-05-18 17:45:08 +04:00
|
|
|
|
s.log.level = Logger::ERROR
|
|
|
|
|
|
2011-05-24 13:24:32 +04:00
|
|
|
|
OptionParser.new do |opts|
|
2018-02-06 23:58:53 +03:00
|
|
|
|
opts.version = VERSION
|
2011-05-18 17:45:08 +04:00
|
|
|
|
opts.banner = "Usage: #{$0} [options]"
|
2018-03-13 18:21:20 +03:00
|
|
|
|
opts.on("-v", "--[no-]verbose", "Increase verbose level"){|v| s.log.level += v ? -1 : 1 }
|
2011-05-18 17:45:08 +04:00
|
|
|
|
opts.on("-c", "--config FILE", "Config file [#{s.config_fname}]", &s.method(:config_fname=))
|
2023-07-31 15:21:59 +03:00
|
|
|
|
opts.on("-g", "--groups FILE", "Groups file [#{s.groups_fname}]", &s.method(:groups_fname=))
|
2011-05-18 17:45:08 +04:00
|
|
|
|
opts.on("-t", "--[no-]test", "Don't do any change in the database", &s.method(:test=))
|
|
|
|
|
|
2011-05-24 13:24:32 +04:00
|
|
|
|
opts.parse!(argv)
|
2011-05-18 17:45:08 +04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
s.start!
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|