pg-ldap-sync-0.5.1/0000755000004100000410000000000014772131157014053 5ustar www-datawww-datapg-ldap-sync-0.5.1/Manifest.txt0000644000004100000410000000045614772131157016367 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.1/.autotest0000644000004100000410000000074014772131157015725 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.1/.gitignore0000644000004100000410000000013514772131157016042 0ustar www-datawww-data/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /temp/ Gemfile.lock pg-ldap-sync-0.5.1/exe/0000755000004100000410000000000014772131157014634 5ustar www-datawww-datapg-ldap-sync-0.5.1/exe/pg_ldap_sync0000755000004100000410000000022714772131157017225 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.1/.github/0000755000004100000410000000000014772131157015413 5ustar www-datawww-datapg-ldap-sync-0.5.1/.github/workflows/0000755000004100000410000000000014772131157017450 5ustar www-datawww-datapg-ldap-sync-0.5.1/.github/workflows/ci.yml0000644000004100000410000000622114772131157020567 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: 17.0-1-windows-x64 PGVER: "17" - os: windows ruby: "2.4" PGVERSION: 9.4.26-1-windows-x64 PGVER: "9.4" - os: ubuntu ruby: "head" PGVER: "17" - os: ubuntu os_ver: "20.04" ruby: "2.3" PGVER: "9.3" - os: macos ruby: "head" PGVERSION: 17.0-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 "$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.1/lib/0000755000004100000410000000000014772131157014621 5ustar www-datawww-datapg-ldap-sync-0.5.1/lib/pg_ldap_sync.rb0000644000004100000410000000063714772131157017616 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.1/lib/pg_ldap_sync/0000755000004100000410000000000014772131157017263 5ustar www-datawww-datapg-ldap-sync-0.5.1/lib/pg_ldap_sync/logger.rb0000644000004100000410000000103114772131157021062 0ustar www-datawww-datarequire 'logger' module PgLdapSync class Logger < ::Logger def initialize(io) super(io) @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.1/lib/pg_ldap_sync/compat.rb0000644000004100000410000000030214772131157021066 0ustar www-datawww-data#!/usr/bin/env ruby class Hash # transform_keys was added in ruby-2.5 def transform_keys map do |k, v| [yield(k), v] end.to_h end unless method_defined? :transform_keys end pg-ldap-sync-0.5.1/lib/pg_ldap_sync/application.rb0000644000004100000410000003230214772131157022113 0ustar www-datawww-data#!/usr/bin/env ruby require 'net/ldap' require 'optparse' require 'yaml' require 'kwalify' 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.kind_of?(Hash) return hash.inject({}) do |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 end else return hash end end 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 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 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 return 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 return 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 return 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 ] 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 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 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 } 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 return 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{|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 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.inject(hash_of_arrays){|h,r| h[r.dn] << r; h } 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.inject(hash_of_arrays){|h,r| h[r.name] << r; h } 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 return 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 = self.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.1/lib/pg_ldap_sync/version.rb0000644000004100000410000000005214772131157021272 0ustar www-datawww-datamodule PgLdapSync VERSION = "0.5.1" end pg-ldap-sync-0.5.1/LICENSE.txt0000644000004100000410000000206514772131157015701 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.1/pg-ldap-sync.gemspec0000644000004100000410000000301414772131157017714 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 = %q{Use LDAP permissions in 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 "net-ldap", "~> 0.16" spec.add_runtime_dependency "kwalify", "~> 0.7" spec.add_runtime_dependency "pg", ">= 0.14", "< 2.0" spec.add_runtime_dependency "logger", "~> 1.0" spec.add_runtime_dependency "ostruct", "~> 0.1" # workaround missing dependency in net-ldap-0.19.0 spec.add_runtime_dependency "base64", "~> 0.1" # workaround missing dependency in net-ldap-0.19.0 spec.add_development_dependency "ruby-ldapserver", "~> 0.7" spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "bundler", ">= 1.16", "< 3.0" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest-hooks", "~> 1.4" end pg-ldap-sync-0.5.1/checksums.yaml.gz.sig0000444000004100000410000000060014772131157020116 0ustar www-datawww-data>n%4K rap[r/:R6.u,o*nmf4МrU{⟤.Eʋ+P"0nẀBGƴU 5]$ =8-t` w-k0oZ?t6=pNyڇR͏?2nVp~Ǜ[&Aq\v.ݟ\y54*c0}팡0<;VXh SU ̍36ˋpH,j浞G&3f: HQG{i2hX*1='x,Ag<*g4~qi)8TEjz+,Lɯ3 ׀pg-ldap-sync-0.5.1/Rakefile0000644000004100000410000000035014772131157015516 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.1/data.tar.gz.sig0000444000004100000410000000060014772131157016666 0ustar www-datawww-dataJ1 }qR6![F'uӪ_nwnӽwNS.V =uHu(Ɩ|Ή9{.f!TF|)ĒH^16]Md:wQXey޷& xR649%+Ã}Z(@k`@pg-ldap-sync-0.5.1/README.md0000644000004100000410000000646014772131157015340 0ustar www-datawww-data[![Build Status](https://app.travis-ci.com/larskanis/pg-ldap-sync.svg?branch=master)](https://app.travis-ci.com/larskanis/pg-ldap-sync) [![Build status](https://ci.appveyor.com/api/projects/status/09xn9q5p64jbxtka/branch/master?svg=true)](https://ci.appveyor.com/project/larskanis/pg-ldap-sync/branch/master) # Use LDAP permissions in PostgreSQL * http://github.com/larskanis/pg-ldap-sync ## DESCRIPTION: LDAP is often used for a centralized user and role management in an enterprise environment. PostgreSQL offers different authentication methods, like LDAP, SSPI, GSSAPI or SSL. However, for any method the user must already exist in the database, before the authentication can be used. There is currently no direct authorization of database users on LDAP. So roles and memberships has to be administered twice. This program helps to solve the issue by synchronizing users, groups and their memberships from LDAP to PostgreSQL. Access to LDAP is used read-only. `pg_ldap_sync` issues proper CREATE ROLE, DROP ROLE, GRANT and REVOKE commands to synchronize users and groups. It is meant to be started as a cron job. ## FEATURES: * User+group creation, deletion and changes in memberships are synchronized from LDAP to PostgreSQL * Nested groups/roles supported * Configurable per YAML config file * Can use Active Directory as LDAP-Server * Set scope of considered users/groups on LDAP and PG side * Test mode which doesn't do any changes to the DBMS * Both LDAP and PG connections can be secured by SSL/TLS * NTLM and Kerberos authentication to LDAP server ## REQUIREMENTS: * Ruby-2.0+ * LDAP-v3 server * PostgreSQL-server v9.0+ ## INSTALL: Install Ruby: * on Windows: http://rubyinstaller.org * on Debian/Ubuntu: `apt-get install ruby libpq-dev` Install pg-ldap-sync and required dependencies: ```sh gem install pg-ldap-sync ``` ### Install from Git: ```sh git clone https://github.com/larskanis/pg-ldap-sync.git cd pg-ldap-sync gem install bundler bundle install bundle exec rake install ``` ## USAGE: Create a config file based on [config/sample-config.yaml](https://github.com/larskanis/pg-ldap-sync/blob/master/config/sample-config.yaml) or even better [config/sample-config2.yaml](https://github.com/larskanis/pg-ldap-sync/blob/master/config/sample-config2.yaml) Run in test-mode: ```sh pg_ldap_sync -c my_config.yaml -vv -t ``` Run in modify-mode: ```sh pg_ldap_sync -c my_config.yaml -vv ``` It is recommended to avoid granting permissions to synchronized users on the PostgreSQL server, but to grant permissions to groups instead. This is because `DROP USER` statements invoked when a user leaves otherwise fail due to depending objects. `DROP GROUP` equally fails if there are depending objects, but groups are typically more stable and removed rarely. ## TEST: There is a small test suite in the `test` directory that runs against an internal LDAP server and a PostgreSQL server. Ensure `pg_ctl`, `initdb` and `psql` commands are in the `PATH` like so: ```sh cd pg-ldap-sync bundle install PATH=$PATH:/usr/lib/postgresql/10/bin/ bundle exec rake test ``` ## ISSUES: * There is currently no way to set certain user attributes in PG based on individual attributes in LDAP (expiration date etc.) ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). pg-ldap-sync-0.5.1/config/0000755000004100000410000000000014772131157015320 5ustar www-datawww-datapg-ldap-sync-0.5.1/config/sample-config.yaml0000644000004100000410000000431214772131157020730 0ustar www-datawww-data# With this sample config the distinction between PG groups and users is # done by the LOGIN/NOLOGIN attribute. Any non-superuser account # is considered as LDAP-synchronized. # Connection parameters to LDAP server # see also: http://net-ldap.rubyforge.org/Net/LDAP.html#method-c-new ldap_connection: host: ldapserver port: 389 auth: method: :simple username: CN=username,OU=!Serviceaccounts,OU=company,DC=company,DC=de password: secret # or GSSAPI / Kerberos authentication: auth: method: :gssapi hostname: ldapserver.company.de servicename: ldap # optional, defaults to "ldap" # or GSS-SPNEGO / NTLM authentication auth: method: :gss_spnego username: 'myuser' password: 'secret' domain: 'company.de' # optional # Search parameters for LDAP users which should be synchronized ldap_users: base: OU=company,OU=company,DC=company,DC=de # LDAP filter (according to RFC 2254) # defines to users in LDAP to be synchronized filter: (&(objectClass=person)(objectClass=organizationalPerson)(givenName=*)(sn=*)) # this attribute is used as PG role name name_attribute: sAMAccountName # Search parameters for LDAP groups which should be synchronized ldap_groups: base: OU=company,OU=company,DC=company,DC=de filter: (|(cn=group1)(cn=group2)(cn=group3)) # this attribute is used as PG role name name_attribute: cn # this attribute must reference to all member DN's of the given group member_attribute: member # Connection parameters to PostgreSQL server # see also: http://rubydoc.info/gems/pg/PG/Connection#initialize-instance_method pg_connection: host: dbname: postgres user: db-username 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: rolcanlogin AND NOT rolsuper # Options for CREATE RULE statements create_options: LOGIN pg_groups: # Filter for identifying LDAP generated groups in the database. # It's the WHERE-condition to "SELECT rolname, oid FROM pg_roles" filter: NOT rolcanlogin AND NOT rolsuper # Options for CREATE RULE statements create_options: NOLOGIN # Options for GRANT TO statements grant_options: pg-ldap-sync-0.5.1/config/sample-config2.yaml0000644000004100000410000000511714772131157021016 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: http://net-ldap.rubyforge.org/Net/LDAP.html#method-c-new 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: http://rubydoc.info/gems/pg/PG/Connection#initialize-instance_method 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.1/config/schema.yaml0000644000004100000410000000243214772131157017445 0ustar www-datawww-datatype: map mapping: "ldap_connection": type: any required: yes "ldap_users": type: map required: yes mapping: "base": type: str required: yes "filter": type: str required: yes "name_attribute": type: str required: yes "lowercase_name": type: bool required: no "bothcase_name": type: bool required: no "ldap_groups": type: map required: yes mapping: "base": type: str required: yes "filter": type: str required: yes "name_attribute": type: str required: yes "lowercase_name": type: bool required: no "bothcase_name": type: bool required: no "member_attribute": type: str required: yes "pg_connection": type: any required: yes "pg_users": type: map required: yes mapping: "filter": type: str required: yes "create_options": type: str "pg_groups": type: map required: yes mapping: "filter": type: str required: yes "create_options": type: str "grant_options": type: str pg-ldap-sync-0.5.1/appveyor.yml0000644000004100000410000000122514772131157016443 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.1/CHANGELOG.md0000644000004100000410000000255414772131157015672 0ustar www-datawww-data## 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!