pg-ldap-sync-0.5.3/0000755000004100000410000000000015102756211014045 5ustar www-datawww-datapg-ldap-sync-0.5.3/Manifest.txt0000644000004100000410000000045615102756211016361 0ustar www-datawww-data.autotest History.txt Manifest.txt README.rdoc Rakefile bin/pg_ldap_sync config/sample-config.yaml config/sample-config2.yaml config/schema.yaml lib/pg_ldap_sync.rb lib/pg_ldap_sync/application.rb test/fixtures/config-ldapdb.yaml test/fixtures/ldapdb.yaml test/ldap_server.rb test/test_pg_ldap_sync.rb pg-ldap-sync-0.5.3/.autotest0000644000004100000410000000074015102756211015717 0ustar www-datawww-data# -*- 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 pg-ldap-sync-0.5.3/.gitignore0000644000004100000410000000013515102756211016034 0ustar www-datawww-data/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /temp/ Gemfile.lock pg-ldap-sync-0.5.3/.standard.yml0000644000004100000410000000002215102756211016440 0ustar www-datawww-dataruby_version: 2.3 pg-ldap-sync-0.5.3/exe/0000755000004100000410000000000015102756211014626 5ustar www-datawww-datapg-ldap-sync-0.5.3/exe/pg_ldap_sync0000755000004100000410000000022715102756211017217 0ustar www-datawww-data#!/usr/bin/env ruby require "pg_ldap_sync" begin PgLdapSync::Application.run(ARGV) rescue PgLdapSync::ApplicationExit => ex exit ex.exitcode end pg-ldap-sync-0.5.3/.github/0000755000004100000410000000000015102756211015405 5ustar www-datawww-datapg-ldap-sync-0.5.3/.github/workflows/0000755000004100000410000000000015102756211017442 5ustar www-datawww-datapg-ldap-sync-0.5.3/.github/workflows/ci.yml0000644000004100000410000000631415102756211020564 0ustar www-datawww-dataname: CI on: workflow_dispatch: schedule: - cron: "0 1 2 * *" # At 01:00 on the second day of each month - https://crontab.guru/#0_1_2_*_* push: branches: - master tags: - "*.*.*" pull_request: types: [opened, synchronize] branches: - "*" jobs: job_test_gem: name: Test built gem strategy: fail-fast: false matrix: include: - os: windows ruby: "head" PGVERSION: 18.0-1-windows-x64 PGVER: "18" - os: windows ruby: "2.4" PGVERSION: 9.4.26-1-windows-x64 PGVER: "9.4" - os: ubuntu ruby: "head" PGVER: "18" - os: ubuntu os_ver: "22.04" ruby: "2.3" PGVER: "9.3" - os: macos ruby: "head" PGVERSION: 17.6-1-osx PGVER: "17" runs-on: ${{ matrix.os }}-${{ matrix.os_ver || 'latest' }} env: PGVERSION: ${{ matrix.PGVERSION }} PGVER: ${{ matrix.PGVER }} steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Download PostgreSQL Windows if: matrix.os == 'windows' run: | Add-Type -AssemblyName System.IO.Compression.FileSystem function Unzip { param([string]$zipfile, [string]$outpath) [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath) } $(new-object net.webclient).DownloadFile("http://get.enterprisedb.com/postgresql/postgresql-$env:PGVERSION-binaries.zip", "postgresql-binaries.zip") Unzip "postgresql-binaries.zip" "." echo "$env:RI_DEVKIT$env:MINGW_PREFIX/bin;$env:RI_DEVKIT/usr/bin;$pwd/pgsql/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append echo "PGUSER=$env:USERNAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append echo "PGPASSWORD=" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append md temp icacls temp /grant "Everyone:(OI)(CI)F" /T - name: Download PostgreSQL Ubuntu if: matrix.os == 'ubuntu' run: | echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main $PGVER" | sudo tee -a /etc/apt/sources.list.d/pgdg.list wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt-get -y update sudo apt-get -y --allow-downgrades install postgresql-$PGVER libpq5=$PGVER* libpq-dev=$PGVER* echo /usr/lib/postgresql/$PGVER/bin >> $GITHUB_PATH - name: Download PostgreSQL Macos if: matrix.os == 'macos' run: | wget https://get.enterprisedb.com/postgresql/postgresql-$PGVERSION-binaries.zip && \ sudo mkdir -p /Library/PostgreSQL && \ sudo unzip postgresql-$PGVERSION-binaries.zip -d /Library/PostgreSQL/$PGVER && \ sudo mv /Library/PostgreSQL/$PGVER/pgsql/* /Library/PostgreSQL/$PGVER/ && \ echo /Library/PostgreSQL/$PGVER/bin >> $GITHUB_PATH - run: bundle install - name: Run specs run: bundle exec rake test pg-ldap-sync-0.5.3/lib/0000755000004100000410000000000015102756211014613 5ustar www-datawww-datapg-ldap-sync-0.5.3/lib/pg_ldap_sync.rb0000644000004100000410000000064115102756211017603 0ustar www-datawww-datarequire "pg_ldap_sync/application" require "pg_ldap_sync/compat" require "pg_ldap_sync/version" module PgLdapSync class LdapError < RuntimeError end class ApplicationExit < RuntimeError attr_reader :exitcode def initialize(exitcode, error = nil) super(error) @exitcode = exitcode end end class InvalidConfig < ApplicationExit end class ErrorExit < ApplicationExit end end pg-ldap-sync-0.5.3/lib/pg_ldap_sync/0000755000004100000410000000000015102756211017255 5ustar www-datawww-datapg-ldap-sync-0.5.3/lib/pg_ldap_sync/logger.rb0000644000004100000410000000107515102756211021064 0ustar www-datawww-datarequire "logger" module PgLdapSync class Logger < ::Logger def initialize(io) super @counters = {} end def add(severity, *args, &block) super return unless [Logger::FATAL, Logger::ERROR].include?(severity) @counters[severity] ||= block ? block.call : args.first end def had_logged?(severity) !!@counters[severity] end def had_errors? had_logged?(Logger::FATAL) || had_logged?(Logger::ERROR) end def first_error @counters[Logger::FATAL] || @counters[Logger::ERROR] end end end pg-ldap-sync-0.5.3/lib/pg_ldap_sync/compat.rb0000644000004100000410000000032415102756211021064 0ustar www-datawww-data#!/usr/bin/env ruby class Hash # transform_keys was added in ruby-2.5 unless method_defined? :transform_keys def transform_keys map do |k, v| [yield(k), v] end.to_h end end end pg-ldap-sync-0.5.3/lib/pg_ldap_sync/application.rb0000644000004100000410000003376315102756211022121 0ustar www-datawww-data#!/usr/bin/env ruby require "net/ldap" require "optparse" require "erb" require "yaml" require "json-schema" 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.is_a?(Hash) hash.each_with_object({}) do |v, h| raise "expected String instead of #{h.inspect}" unless v[0].is_a?(String) h[v[0].intern] = string_to_symbol(v[1]) end else hash end end def validate_config(config, schema, fname) schema = YAML.load_file(schema) errors = JSON::Validator.fully_validate(schema, config, validate_schema: true, insert_defaults: true) if errors && !errors.empty? errors.each do |err| log.fatal "error in #{fname}: #{err}" 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(ERB.new(File.read(fname)).result) 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] name_attribute = ldap_user_conf[:name_attribute] users = [] res = @ldap.search( base: ldap_user_conf[:base], filter: ldap_user_conf[:filter], attributes: [name_attribute, :dn] ) do |entry| name = entry[name_attribute].first unless name log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}" next end log.info "found user-dn: #{entry.dn}" names = if ldap_user_conf[:bothcase_name] [name, name.downcase].uniq elsif ldap_user_conf[:lowercase_name] [name.downcase] else [name] end names.each do |n| users << LdapRole.new(n, entry.dn) end 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 users end 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 array end def search_ldap_groups ldap_group_conf = @config[:ldap_groups] name_attribute = ldap_group_conf[:name_attribute] member_attribute = ldap_group_conf[:member_attribute] groups = [] res = @ldap.search( base: ldap_group_conf[:base], filter: ldap_group_conf[:filter], attributes: [name_attribute, member_attribute, :dn] ) do |entry| name = entry[name_attribute].first unless name log.warn "user attribute #{name_attribute.inspect} not defined for #{entry.dn}" next end log.info "found group-dn: #{entry.dn}" names = if ldap_group_conf[:bothcase_name] [name, name.downcase].uniq elsif ldap_group_conf[:lowercase_name] [name.downcase] else [name] end names.each do |n| group_members = retrieve_array_attribute(entry, member_attribute) groups << LdapRole.new(n, entry.dn, group_members) end 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 groups end PgRole = Struct.new :name, :member_names # List of default roles taken from https://www.postgresql.org/docs/current/predefined-roles.html 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 pg_create_subscription pg_maintain pg_use_reserved_connections pg_signal_autovacuum_worker] 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 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 groups end def uniq_names(list) names = {} 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 end MatchedRole = Struct.new :ldap, :pg, :name, :state, :type def match_roles(ldaps, pgs, type) ldap_by_name = ldaps.each_with_object({}) { |u, h| h[u.name] = u } pg_by_name = pgs.each_with_object({}) { |u, h| h[u.name] = u } 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 do 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 }}" end 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] pg_exec_modify "CREATE ROLE \"#{role.name}\" #{pg_conf[:create_options]}" end def drop_pg_role(role) pg_exec_modify "DROP ROLE \"#{role.name}\"" end def sync_roles_to_pg(roles, for_state) roles.sort_by(&: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 MatchedMembership = Struct.new :role_name, :has_member, :state def match_memberships(ldap_roles, pg_roles) hash_of_arrays = Hash.new { |h, k| h[k] = [] } ldap_by_dn = ldap_roles.each_with_object(hash_of_arrays) { |r, h| h[r.dn] << r } ldap_by_m2m = ldap_roles.inject([]) do |a, r| next a unless r.member_dns 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| [r.name, has_member.name] end end end hash_of_arrays = Hash.new { |h, k| h[k] = [] } pg_by_name = pg_roles.each_with_object(hash_of_arrays) { |r, h| h[r.name] << r } pg_by_m2m = pg_roles.inject([]) do |a, r| next a unless r.member_names 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| [r.name, has_member.name] end end end 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 do 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 }}" end 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 start! read_config_file(@config_fname) 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 # gather LDAP users and groups @ldap = Net::LDAP.new ldap_conf 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.new(1, log.first_error) end end def self.run(argv) s = new s.config_fname = "/etc/pg_ldap_sync.yaml" s.log = Logger.new($stdout) 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 pg-ldap-sync-0.5.3/lib/pg_ldap_sync/version.rb0000644000004100000410000000005215102756211021264 0ustar www-datawww-datamodule PgLdapSync VERSION = "0.5.3" end pg-ldap-sync-0.5.3/LICENSE.txt0000644000004100000410000000206515102756211015673 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2018 Lars Kanis 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. pg-ldap-sync-0.5.3/pg-ldap-sync.gemspec0000644000004100000410000000247315102756211017716 0ustar www-datawww-datalib = 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 = "Sync users and groups from LDAP to 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.required_ruby_version = ">= 2.3" spec.add_runtime_dependency "erb", ">= 1" spec.add_runtime_dependency "net-ldap", "~> 0.16" spec.add_runtime_dependency "json-schema", ">= 2" spec.add_runtime_dependency "pg", ">= 0.14", "< 2.0" spec.add_runtime_dependency "logger", "~> 1.0" spec.add_development_dependency "ruby-ldapserver", "~> 0.7" spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "bundler", ">= 1.16", "< 4.0" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest-hooks", "~> 1.4" end pg-ldap-sync-0.5.3/checksums.yaml.gz.sig0000444000004100000410000000060015102756211020110 0ustar www-datawww-data_b%lls­.]0 | k?e1Ml? >%3æc#H[# k2xt|&fLG]iHvs#u,`HBKs] G|k,DAqE8ߝ>=a30t""w8jUA4[b5Gb~`[&ܬcfs?3h&潑T <| !IE|K hw`!a, ֜, U*Xq֛W? %Štkkrپmv8wxt:{8 hۙo}51 \ *W75h2 ,l[pwLpg-ldap-sync-0.5.3/Rakefile0000644000004100000410000000034515102756211015514 0ustar www-datawww-data# -*- ruby -*- require "bundler/gem_tasks" require "rake/testtask" CLEAN.include "temp" Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/test_*.rb"] end task gem: :build pg-ldap-sync-0.5.3/data.tar.gz.sig0000444000004100000410000000060015102756211016660 0ustar www-datawww-data\uB4q>Y g@~cb/-tuoD8ԋ5:Cں%}ݔt@'Pm?CJ]z%NUIpHoe@̝B }k7Mpv9R,F_"0T?}Sb;tX1aKKµ1HV9&!4\g]d_o}HȯY*+H61pg-ldap-sync-0.5.3/Gemfile0000644000004100000410000000021215102756211015333 0ustar www-datawww-datasource "https://rubygems.org" # Specify your gem's dependencies in pg_ldap_sync.gemspec gemspec group :development do gem "debug" end pg-ldap-sync-0.5.3/.travis.yml0000644000004100000410000000130115102756211016151 0ustar www-datawww-datasudo: required dist: focal language: ruby rvm: - "2.4.0" - ruby-head env: - "PGVERSION=14" - "PGVERSION=9.6" before_install: - gem install bundler --no-doc --conservative - bundle install # Download and install postgresql version to test against in /opt (for non-cross compile only) - echo "deb http://apt.postgresql.org/pub/repos/apt/ ${TRAVIS_DIST}-pgdg main $PGVERSION" | sudo tee -a /etc/apt/sources.list.d/pgdg.list - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - - sudo apt -y update - sudo apt -y --allow-downgrades install postgresql-$PGVERSION libpq-dev - export PATH=/usr/lib/postgresql/$PGVERSION/bin:$PATH script: rake test pg-ldap-sync-0.5.3/metadata.gz.sig0000444000004100000410000000060015102756211016742 0ustar www-datawww-data/ &̋ !L Vxoyw\D¢Eu z%PUjugp/"0n2` .x8M̜u{ϣ"KvDr4Ac-Cw}3qǗG;`Z=R2OtQs3۱໠jc2E𦵵=z]Xr2r ?tE"@x|ŀ2/mz TO statements grant_options: pg-ldap-sync-0.5.3/config/sample-config2.yaml0000644000004100000410000000513515102756211021010 0ustar www-datawww-data# With this sample config the distinction between LDAP-synchronized # groups/users from manually created PostgreSQL users is done by the # membership in ldap_user and ldap_group. # These two roles have to be defined manally before pg_ldap_sync can # run and all synchronized users/groups will become member of them # later on: # CREATE GROUP ldap_groups; # CREATE USER ldap_users; # # Connection parameters to LDAP server # see also: https://www.rubydoc.info/gems/net-ldap/Net%2FLDAP:initialize 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 # Add lowercase name *and* original name for use as PG role names (useful for migrating between case types) bothcase_name: false # 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: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS 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 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 # Options for GRANT TO statements grant_options: pg-ldap-sync-0.5.3/config/schema.yaml0000644000004100000410000000264115102756211017441 0ustar www-datawww-data"$schema": "http://json-schema.org/draft-04/schema#" "id": "https://github.com/larskanis/pg-ldap-sync/blob/-/config/schema.yaml" title: pg-ldap-sync config file type: object required: - ldap_connection - ldap_users - ldap_groups - pg_connection - pg_users - pg_groups properties: ldap_connection: type: object ldap_users: type: object required: - base - filter - name_attribute properties: base: type: string filter: type: string name_attribute: type: string lowercase_name: type: boolean bothcase_name: type: boolean ldap_groups: type: object required: - base - filter - name_attribute - member_attribute properties: base: type: string filter: type: string name_attribute: type: string lowercase_name: type: boolean bothcase_name: type: boolean member_attribute: type: string pg_connection: type: object pg_users: type: object required: - filter properties: filter: type: string create_options: type: ["string", "null"] pg_groups: type: object required: - filter properties: filter: type: string create_options: type: ["string", "null"] grant_options: type: ["string", "null"] pg-ldap-sync-0.5.3/appveyor.yml0000644000004100000410000000122515102756211016435 0ustar www-datawww-dataimage: Visual Studio 2022 init: - set PATH=C:/Ruby%ruby_version%/bin;c:/Program Files/Git/cmd;c:/Windows/system32;C:/Windows/System32/WindowsPowerShell/v1.0 - set RUBYOPT=--verbose install: - ver - ruby --version - gem --version - gem install bundler --no-doc --conservative - bundle install build_script: - set PATH=C:/Program Files/PostgreSQL/%PGVER%/bin;%PATH% - md temp - icacls temp /grant Everyone:(OI)(CI)F /T test_script: - bundle exec rake test environment: matrix: # ruby-3.3.6 currently fails installing stringio-3.1.5.gem # - ruby_version: "33-x64" # PGVER: 16 - ruby_version: "27-x64" PGVER: 11 pg-ldap-sync-0.5.3/CHANGELOG.md0000644000004100000410000000335615102756211015665 0ustar www-datawww-data## 0.5.3 / 2025-11-02 * Add predefined roles for compat with PostgreSQL-18 ## 0.5.2 / 2025-09-01 * Replace `kwalify` by `json-schema` to remove hundreds of warnings about keyword arguments in modern Ruby. #51 * Allow ERb syntax in config file. #52 This can be used to insert password from environment variable like so: `password: <%= ENV["PASSWORD"] %>` * Update documentation. ## 0.5.1 / 2025-03-22 * Add dependent gems for compat with ruby-3.5 * Add predefined roles for compat with PostgreSQL-16 and 17 ## 0.5.0 / 2023-08-24 * Add Kerberos and NTLM authentication support to LDAP connection * Fix retrieval of groups with over 1500 users in Active Directory server. #45 ## 0.4.0 / 2022-12-02 * Support groups with over 1500 users in Active Directory server. #32 * Retrieve only necessary attributes from LDAP server. * Add error text to exception, so that it's visible even if nothing is logged. * Fix compatibility with PostgreSQL-15 * Require ruby-2.3+ ## 0.3.0 / 2022-01-18 * Add config option :bothcase_name . This adds both spellings "Fred_Flintstone" and "fred_flintstone" as PostgreSQL users/groups. * Update gem dependencies * Fix compatibility with PostgreSQL-14 * Require ruby-2.4+ ## 0.2.0 / 2018-03-13 * 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.1 / 2012-11-15 * Add ability to lowercase the LDAP name for use as PG role name ## 0.1.0 / 2011-07-13 * Birthday!