class PgLdapSync::Application

Constants

LdapRole
MatchedMembership
MatchedRole
PG_BUILTIN_ROLES

List of default roles taken from www.postgresql.org/docs/current/static/default-roles.html

PgRole

Attributes

config_fname[RW]
log[RW]
test[RW]

Public Class Methods

run(argv) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 339
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

Public Instance Methods

create_pg_role(role) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 218
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]}"
end
drop_pg_role(role) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 223
def drop_pg_role(role)
  pg_exec_modify "DROP ROLE \"#{role.name}\""
end
grant_membership(role_name, add_members) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 276
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
match_memberships(ldap_roles, pg_roles) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 236
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
match_roles(ldaps, pgs, type) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 159
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
pg_exec(sql) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 213
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
pg_exec_modify(sql) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 206
def pg_exec_modify(sql)
  log.info{ "SQL: #{sql}" }
  unless self.test
    try_sql sql
  end
end
read_config_file(fname) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 41
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
revoke_membership(role_name, rm_members) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 282
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
search_ldap_groups() click to toggle source
# File lib/pg_ldap_sync/application.rb, line 80
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|
    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]]
    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
search_ldap_users() click to toggle source
# File lib/pg_ldap_sync/application.rb, line 53
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|
    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
search_pg_groups() click to toggle source
# File lib/pg_ldap_sync/application.rb, line 126
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
search_pg_users() click to toggle source
# File lib/pg_ldap_sync/application.rb, line 112
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
start!() click to toggle source
# File lib/pg_ldap_sync/application.rb, line 300
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

      # 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

  # Determine exitcode
  if log.had_errors?
    raise ErrorExit, 1
  end
end
string_to_symbol(hash) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 16
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
sync_membership_to_pg(memberships, for_state) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 287
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
sync_roles_to_pg(roles, for_state) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 227
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
  end
end
try_sql(text) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 195
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
uniq_names(list) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 142
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
validate_config(config, schema, fname) click to toggle source
# File lib/pg_ldap_sync/application.rb, line 29
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