From 419a8dd8142afef790dafd91ba39afac2ca48aaf Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Wed, 15 Mar 2023 16:37:28 -0400 Subject: [PATCH] Add a hook for modifying the ldapbind password The hook can be installed by a shared_preload library. A similar mechanism could be used for radius paswords, for example, and the type name auth_password_hook_typ has been shosen with that in mind. John Naylor and Andrew Dunstan Discussion: https://postgr.es/m/469b06ed-69de-ba59-c13a-91d2372e52a9@dunslane.net --- src/backend/libpq/auth.c | 12 +- src/include/libpq/auth.h | 6 + src/test/modules/Makefile | 11 ++ src/test/modules/ldap_password_func/Makefile | 25 +++++ .../ldap_password_func/ldap_password_func.c | 65 +++++++++++ .../modules/ldap_password_func/meson.build | 33 ++++++ .../t/001_mutated_bindpasswd.pl | 103 ++++++++++++++++++ src/test/modules/meson.build | 1 + 8 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/ldap_password_func/Makefile create mode 100644 src/test/modules/ldap_password_func/ldap_password_func.c create mode 100644 src/test/modules/ldap_password_func/meson.build create mode 100644 src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 25b3a781cdc..bc0cf26b122 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -144,6 +144,10 @@ static int CheckLDAPAuth(Port *port); #define LDAP_OPT_DIAGNOSTIC_MESSAGE LDAP_OPT_ERROR_STRING #endif +/* Default LDAP password mutator hook, can be overridden by a shared library */ +static char *dummy_ldap_password_mutator(char *input); +auth_password_hook_typ ldap_password_hook = dummy_ldap_password_mutator; + #endif /* USE_LDAP */ /*---------------------------------------------------------------- @@ -2370,6 +2374,12 @@ InitializeLDAPConnection(Port *port, LDAP **ldap) #define LDAPS_PORT 636 #endif +static char * +dummy_ldap_password_mutator(char *input) +{ + return input; +} + /* * Return a newly allocated C string copied from "pattern" with all * occurrences of the placeholder "$username" replaced with "user_name". @@ -2498,7 +2508,7 @@ CheckLDAPAuth(Port *port) */ r = ldap_simple_bind_s(ldap, port->hba->ldapbinddn ? port->hba->ldapbinddn : "", - port->hba->ldapbindpasswd ? port->hba->ldapbindpasswd : ""); + port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : ""); if (r != LDAP_SUCCESS) { ereport(LOG, diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index 137bee7c455..9916c99df17 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -28,4 +28,10 @@ extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, typedef void (*ClientAuthentication_hook_type) (Port *, int); extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook; +/* hook type for password manglers */ +typedef char *(*auth_password_hook_typ) (char *input); + +/* Default LDAP password mutator hook, can be overridden by a shared library */ +extern PGDLLIMPORT auth_password_hook_typ ldap_password_hook; + #endif /* AUTH_H */ diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index c629cbe3830..79e3033ec28 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -42,5 +42,16 @@ else ALWAYS_SUBDIRS += ssl_passphrase_callback endif +# Test runs an LDAP server, so only run if ldap is in PG_TEST_EXTRA +ifeq ($(with_ldap),yes) +ifneq (,$(filter ldap,$(PG_TEST_EXTRA))) +SUBDIRS += ldap_password_func +else +ALWAYS_SUBDIRS += ldap_password_func +endif +else +ALWAYS_SUBDIRS += ldap_password_func +endif + $(recurse) $(recurse_always) diff --git a/src/test/modules/ldap_password_func/Makefile b/src/test/modules/ldap_password_func/Makefile new file mode 100644 index 00000000000..3324e04248d --- /dev/null +++ b/src/test/modules/ldap_password_func/Makefile @@ -0,0 +1,25 @@ +# Copyright (c) 2022, PostgreSQL Global Development Group + +# ldap_password_func Makefile + +export with_ldap + +MODULE_big = ldap_password_func +OBJS = ldap_password_func.o $(WIN32RES) +PGFILEDESC = "set hook to mutate ldapbindpasswd" + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/ldap_password_func +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + + + diff --git a/src/test/modules/ldap_password_func/ldap_password_func.c b/src/test/modules/ldap_password_func/ldap_password_func.c new file mode 100644 index 00000000000..4d980d28b1e --- /dev/null +++ b/src/test/modules/ldap_password_func/ldap_password_func.c @@ -0,0 +1,65 @@ +/*------------------------------------------------------------------------- + * + * Copyright (c) 2022, PostgreSQL Global Development Group + * + * ldap_password_func.c + * + * Loadable PostgreSQL module to mutate the ldapbindpasswd. This + * implementation just hands back the configured password rot13'd. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include +#include + +#include "libpq/libpq.h" +#include "libpq/libpq-be.h" +#include "libpq/auth.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); +void _PG_fini(void); + +/* hook function */ +static char *rot13_passphrase(char *password); + +/* + * Module load callback + */ +void +_PG_init(void) +{ + ldap_password_hook = rot13_passphrase; +} + +void +_PG_fini(void) +{ + /* do nothing yet */ +} + +static char * +rot13_passphrase(char *pw) +{ + size_t size = strlen(pw) + 1; + + char *new_pw = (char *) palloc(size); + + strlcpy(new_pw, pw, size); + for (char *p = new_pw; *p; p++) + { + char c = *p; + + if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) + *p = c + 13; + else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) + *p = c - 13; + } + + return new_pw; +} diff --git a/src/test/modules/ldap_password_func/meson.build b/src/test/modules/ldap_password_func/meson.build new file mode 100644 index 00000000000..653a5e94792 --- /dev/null +++ b/src/test/modules/ldap_password_func/meson.build @@ -0,0 +1,33 @@ +if not ldap.found() + subdir_done() +endif + +ldap_password_func_sources = files( + 'ldap_password_func.c', +) + +if host_system == 'windows' + ldap_password_func_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'ldap_password_func', + '--FILEDESC', 'set hook to mutate ldapbindpassw',]) +endif + +ldap_password_func = shared_module('ldap_password_func', + ldap_password_func_sources, + kwargs: pg_mod_args + { + 'dependencies': [ldap, pg_mod_args['dependencies']], + }, +) +test_install_libs += ldap_password_func + +tests += { + 'name': 'ldap_password_func', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_mutated_bindpasswd.pl', + ], + 'env': {'with_ldap': 'yes'} + }, +} diff --git a/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl b/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl new file mode 100644 index 00000000000..4174292d2df --- /dev/null +++ b/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl @@ -0,0 +1,103 @@ + +# Copyright (c) 2022, PostgreSQL Global Development Group + +use strict; +use warnings; +use File::Copy; +use FindBin; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + +use lib "$FindBin::RealBin/../../../ldap"; +use LdapServer; + +my ($slapd, $ldap_bin_dir, $ldap_schema_dir); + +$ldap_bin_dir = undef; # usually in PATH + +if ($ENV{with_ldap} ne 'yes') +{ + plan skip_all => 'LDAP not supported by this build'; +} +elsif ($ENV{PG_TEST_EXTRA} !~ /\bldap\b/) +{ + plan skip_all => + 'Potentially unsafe test LDAP not enabled in PG_TEST_EXTRA'; +} +elsif (!$LdapServer::setup) +{ + plan skip_all => + "ldap tests not supported on $^O or dependencies not installed"; +} + +my $clear_ldap_rootpw = "FooBaR1"; +my $rot13_ldap_rootpw = "SbbOnE1"; + +my $ldap = LdapServer->new($clear_ldap_rootpw, 'users'); # no anonymous auth +$ldap->ldapadd_file("$FindBin::RealBin/../../../ldap/authdata.ldif"); +$ldap->ldapsetpw('uid=test1,dc=example,dc=net', 'secret1'); + +my ($ldap_server, $ldap_port, $ldap_basedn, $ldap_rootdn) = + $ldap->prop(qw(server port basedn rootdn)); + + +note "setting up PostgreSQL instance"; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->append_conf('postgresql.conf', "log_connections = on\n"); +$node->append_conf('postgresql.conf', "shared_preload_libraries = 'ldap_password_func'"); +$node->start; + +$node->safe_psql('postgres', 'CREATE USER test1;'); + +note "running tests"; + +sub test_access +{ + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ($node, $role, $expected_res, $test_name, %params) = @_; + my $connstr = "user=$role"; + + if ($expected_res eq 0) + { + $node->connect_ok($connstr, $test_name, %params); + } + else + { + # No checks of the error message, only the status code. + $node->connect_fails($connstr, $test_name, %params); + } +} + +note "use ldapbindpasswd"; + +$ENV{"PGPASSWORD"} = 'secret1'; + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf('pg_hba.conf', + qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapbinddn="$ldap_rootdn" ldapbindpasswd=wrong} +); +$node->restart; + +test_access($node, 'test1', 2, 'search+bind authentication fails with wrong ldapbindpasswd'); + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf('pg_hba.conf', + qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapbinddn="$ldap_rootdn" ldapbindpasswd="$clear_ldap_rootpw"} +); +$node->restart; + +test_access($node, 'test1', 2, 'search+bind authentication fails with clear password'); + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf('pg_hba.conf', + qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapbinddn="$ldap_rootdn" ldapbindpasswd="$rot13_ldap_rootpw"} +); +$node->restart; + +test_access($node, 'test1', 0, 'search+bind authentication succeeds with rot13ed password'); + +done_testing(); diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 1baa6b558d1..dcb82ed68f4 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -5,6 +5,7 @@ subdir('commit_ts') subdir('delay_execution') subdir('dummy_index_am') subdir('dummy_seclabel') +subdir('ldap_password_func') subdir('libpq_pipeline') subdir('plsample') subdir('snapshot_too_old')