diff --git a/.travis.yml b/.travis.yml index 995b692..3b67d62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,20 @@ sudo: required +dist: focal language: ruby rvm: - - "2.0.0" + - "2.4.0" - ruby-head env: - - "PGVERSION=10.0-1-linux-x64 PATH=\"/opt/PostgreSQL/10/bin:$PATH\"" - - "PGVERSION=9.3.19-1-linux-x64 PATH=\"/opt/PostgreSQL/9.3/bin:$PATH\"" + - "PGVERSION=14" + - "PGVERSION=9.6" before_install: - - gem install bundler + - gem install bundler --no-doc --conservative - bundle install - # Download and install postgresql version to test against in /opt - - | - wget http://get.enterprisedb.com/postgresql/postgresql-$PGVERSION.run && \ - chmod +x postgresql-$PGVERSION.run && \ - sudo ./postgresql-$PGVERSION.run --extract-only 1 --mode unattended + # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3214f9e..1f76978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 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 diff --git a/README.md b/README.md index 615a508..a7ca025 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/larskanis/pg-ldap-sync.svg?branch=master)](https://travis-ci.org/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) +[![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 diff --git a/appveyor.yml b/appveyor.yml index 51b2a1b..e558224 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,5 @@ +image: Visual Studio 2019 + 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 @@ -6,7 +8,7 @@ install: - ver - ruby --version - gem --version - - gem install bundler --conservative + - gem install bundler --no-doc --conservative - bundle install build_script: @@ -19,7 +21,7 @@ test_script: environment: matrix: - - ruby_version: "25-x64" - PGVER: 10 - - ruby_version: "22" + - ruby_version: "27-x64" + PGVER: 13 + - ruby_version: "24" PGVER: 10 diff --git a/config/sample-config2.yaml b/config/sample-config2.yaml index 080b512..dd96ea1 100644 --- a/config/sample-config2.yaml +++ b/config/sample-config2.yaml @@ -1,6 +1,6 @@ # With this sample config the distinction between LDAP-synchronized # groups/users from is done by the membership to ldap_user and -# ldap_group. These two roles has to be defined manally before +# ldap_group. These two roles have to be defined manally before # pg_ldap_sync can run. # Connection parameters to LDAP server @@ -25,6 +25,8 @@ ldap_users: 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: diff --git a/config/schema.yaml b/config/schema.yaml index 8ed6a29..bee2ea8 100644 --- a/config/schema.yaml +++ b/config/schema.yaml @@ -23,6 +23,9 @@ mapping: "uppercase_name": type: bool required: no + "bothcase_name": + type: bool + required: no "ldap_groups": type: map @@ -43,6 +46,9 @@ mapping: "uppercase_name": type: bool required: no + "bothcase_name": + type: bool + required: no "member_attribute": type: str required: yes diff --git a/lib/pg_ldap_sync/application.rb b/lib/pg_ldap_sync/application.rb index 5f9b8b6..0176dbd 100644 --- a/lib/pg_ldap_sync/application.rb +++ b/lib/pg_ldap_sync/application.rb @@ -15,11 +15,11 @@ class Application def string_to_symbol(hash) if hash.kind_of?(Hash) - return hash.inject({}){|h, v| + 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 @@ -61,12 +61,22 @@ class Application 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] - name.upcase! if ldap_user_conf[:uppercase_name] log.info "found user-dn: #{entry.dn}" - user = LdapRole.new name, entry.dn - users << user + + names = if ldap_user_conf[:bothcase_name] + [name, name.downcase].uniq + elsif ldap_user_conf[:lowercase_name] + [name.downcase] + elsif ldap_user_conf[:uppercase_name] + [name.upcase] + 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| @@ -89,12 +99,22 @@ class Application 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] - name.upcase! if ldap_group_conf[:uppercase_name] log.info "found group-dn: #{entry.dn}" - group = LdapRole.new name, entry.dn, entry[ldap_group_conf[:member_attribute]] - groups << group + + names = if ldap_group_conf[:bothcase_name] + [name, name.downcase].uniq + elsif ldap_group_conf[:lowercase_name] + [name.downcase] + elsif ldap_group_conf[:uppercase_name] + [name.upcase] + else + [name] + end + + names.each do |n| + groups << LdapRole.new(n, entry.dn, entry[ldap_group_conf[:member_attribute]]) + end entry.each do |attribute, values| log.debug " #{attribute}:" values.each do |value| @@ -108,8 +128,8 @@ class Application PgRole = Struct.new :name, :member_names - # List of default roles taken from https://www.postgresql.org/docs/current/static/default-roles.html - PG_BUILTIN_ROLES = %w[ pg_signal_backend pg_monitor pg_read_all_settings pg_read_all_stats pg_stat_scan_tables] + # 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 ] def search_pg_users pg_users_conf = @config[:pg_users] @@ -185,12 +205,12 @@ class Application r.type = type end - log.info{ + 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 @@ -236,42 +256,42 @@ class Application MatchedMembership = Struct.new :role_name, :has_member, :state 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| + 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.map{|dn| - if has_member=ldap_by_dn[dn] + 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] - else - log.warn{"ldap member with dn #{dn} is unknown"} - nil end - }.compact - } + end + end - pg_by_name = pg_roles.inject({}){|h,r| h[r.name] = r; h } - pg_by_m2m = pg_roles.inject([]){|a,r| + 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.map{|name| - if has_member=pg_by_name[name] + 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] - else - log.warn{"pg member with name #{name} is unknown"} - nil end - }.compact - } + 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{ + 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 diff --git a/lib/pg_ldap_sync/version.rb b/lib/pg_ldap_sync/version.rb index 74c011a..c2fba6c 100644 --- a/lib/pg_ldap_sync/version.rb +++ b/lib/pg_ldap_sync/version.rb @@ -1,3 +1,3 @@ module PgLdapSync - VERSION = "0.2.0" + VERSION = "0.3.0" end diff --git a/pg-ldap-sync.gemspec b/pg-ldap-sync.gemspec index e7d1901..7072230 100644 --- a/pg-ldap-sync.gemspec +++ b/pg-ldap-sync.gemspec @@ -19,13 +19,14 @@ Gem::Specification.new do |spec| 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.4" 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_development_dependency "ruby-ldapserver", "~> 0.3" spec.add_development_dependency "minitest", "~> 5.0" - spec.add_development_dependency "bundler", "~> 1.16" - spec.add_development_dependency "rake", "~> 10.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 diff --git a/test/fixtures/config-ldapdb-bothcase.yaml b/test/fixtures/config-ldapdb-bothcase.yaml new file mode 100644 index 0000000..49c1583 --- /dev/null +++ b/test/fixtures/config-ldapdb-bothcase.yaml @@ -0,0 +1,34 @@ +--- +ldap_connection: + host: localhost + port: 1389 + +ldap_users: + base: dc=example,dc=com + filter: (sAMAccountName=*) + name_attribute: sAMAccountName + bothcase_name: true + +ldap_groups: + base: dc=example,dc=com + filter: (member=*) + name_attribute: cn + bothcase_name: true + member_attribute: member + +pg_connection: + dbname: postgres + host: localhost + port: 54321 +# needed for postgres-pr: +# user: insert_your_username_here +# password: + +pg_users: + filter: rolcanlogin AND NOT rolsuper AND rolname!='double_user' + create_options: LOGIN + +pg_groups: + filter: NOT rolcanlogin + create_options: NOLOGIN + grant_options: diff --git a/test/fixtures/ldapdb.yaml b/test/fixtures/ldapdb.yaml index e0eb4c3..cac7e90 100644 --- a/test/fixtures/ldapdb.yaml +++ b/test/fixtures/ldapdb.yaml @@ -11,14 +11,14 @@ cn=Fred Flintstone,dc=example,dc=com: sn: - Flintstone sAMAccountName: - - fred + - Fred cn=Wilma Flintstone,dc=example,dc=com: cn: - Wilma Flintstone mail: - wilma@bedrock.org sAMAccountName: - - wilma + - Wilma cn=Flintstones,dc=example,dc=com: cn: - Flintstones diff --git a/test/test_pg_ldap_sync.rb b/test/test_pg_ldap_sync.rb index 0081ba7..245e3bb 100644 --- a/test/test_pg_ldap_sync.rb +++ b/test/test_pg_ldap_sync.rb @@ -83,7 +83,7 @@ class TestPgLdapSync < Minitest::Test end def setup - @pgconn.exec "DROP ROLE IF EXISTS fred, wilma, \"Flintstones\", \"Wilmas\", \"All Users\", double_user" + @pgconn.exec "DROP ROLE IF EXISTS \"Fred\", fred, \"Wilma\", wilma, \"Flintstones\", \"flintstones\", \"Wilmas\", \"wilmas\", \"All Users\", double_user" end def assert_role(role_name, attrs, member_of=[]) @@ -130,12 +130,12 @@ class TestPgLdapSync < Minitest::Test sync_with_config(config) end - def sync_change - sync_to_fixture + def sync_change(fixture: "ldapdb", config: "config-ldapdb") + sync_to_fixture(fixture: fixture, config: config) yield(@directory) - sync_with_config + sync_with_config(config) exec_psql_du if $DEBUG end @@ -153,8 +153,8 @@ class TestPgLdapSync < Minitest::Test assert_role('All Users', 'Cannot login') assert_role('Flintstones', 'Cannot login') assert_role('Wilmas', 'Cannot login', ['All Users']) - assert_role('fred', '', ['All Users', 'Flintstones']) - assert_role('wilma', '', ['Flintstones', 'Wilmas']) + assert_role('Fred', '', ['All Users', 'Flintstones']) + assert_role('Wilma', '', ['Flintstones', 'Wilmas']) end def test_add_membership @@ -162,7 +162,17 @@ class TestPgLdapSync < Minitest::Test # add 'Fred' to 'Wilmas' @directory[0]['cn=Wilmas,dc=example,dc=com']['member'] << 'cn=Fred Flintstone,dc=example,dc=com' end - assert_role('fred', '', ['All Users', 'Flintstones', 'Wilmas']) + refute_role('fred') + assert_role('Fred', '', ['All Users', 'Flintstones', 'Wilmas']) + end + + def test_add_membership_bothcase + sync_change(config: "config-ldapdb-bothcase") do |dir| + # add 'Fred' to 'Wilmas' + @directory[0]['cn=Wilmas,dc=example,dc=com']['member'] << 'cn=Fred Flintstone,dc=example,dc=com' + end + assert_role('fred', '', ['All Users', 'all users', 'Flintstones', 'flintstones', 'Wilmas', 'wilmas']) + assert_role('Fred', '', ['All Users', 'all users', 'Flintstones', 'flintstones', 'Wilmas', 'wilmas']) end def test_revoke_membership @@ -170,7 +180,7 @@ class TestPgLdapSync < Minitest::Test # revoke membership of 'wilma' to 'Flintstones' dir[0]['cn=Flintstones,dc=example,dc=com']['member'].pop end - assert_role('wilma', '', ['Wilmas']) + assert_role('Wilma', '', ['Wilmas']) end def test_rename_role @@ -179,6 +189,7 @@ class TestPgLdapSync < Minitest::Test dir[0]['cn=Wilma Flintstone,dc=example,dc=com']['sAMAccountName'] = ['Wilma Flintstone'] end refute_role('wilma') + refute_role('Wilma') assert_role('Wilma Flintstone', '', ['Flintstones', 'Wilmas']) end