From d7f57dfe4a316c8ac1270a5f35f837447e335153 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sat, 31 Dec 2022 14:41:57 +1300
Subject: [PATCH v2 1/3] Add simple test for RADIUS authentication.

This is similar to the existing tests for ldap and kerberos.  It
requires FreeRADIUS to be installed, and opens ports that may be
considered insecure, so users have to opt in with PG_EXTRA_TESTS=radius.

Discussion: https://postgr.es/m/CA%2BhUKGKxNoVjkMCksnj6z3BwiS3y2v6LN6z7_CisLK%2Brv%2B0V4g%40mail.gmail.com
---
 src/test/Makefile             |   2 +-
 src/test/meson.build          |   1 +
 src/test/radius/Makefile      |  23 +++++
 src/test/radius/meson.build   |  12 +++
 src/test/radius/t/001_auth.pl | 187 ++++++++++++++++++++++++++++++++++
 5 files changed, 224 insertions(+), 1 deletion(-)
 create mode 100644 src/test/radius/Makefile
 create mode 100644 src/test/radius/meson.build
 create mode 100644 src/test/radius/t/001_auth.pl

diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..687164412c 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,7 @@ subdir = src/test
 top_builddir = ../..
 include $(top_builddir)/src/Makefile.global
 
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl regress isolation modules authentication recovery radius subscription
 
 ifeq ($(with_icu),yes)
 SUBDIRS += icu
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..b5da17b531 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -5,6 +5,7 @@ subdir('isolation')
 
 subdir('authentication')
 subdir('recovery')
+subdir('radius')
 subdir('subscription')
 subdir('modules')
 
diff --git a/src/test/radius/Makefile b/src/test/radius/Makefile
new file mode 100644
index 0000000000..56768a3ca9
--- /dev/null
+++ b/src/test/radius/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/radius
+#
+# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/radius/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/radius
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -rf tmp_check
diff --git a/src/test/radius/meson.build b/src/test/radius/meson.build
new file mode 100644
index 0000000000..ea7afc4555
--- /dev/null
+++ b/src/test/radius/meson.build
@@ -0,0 +1,12 @@
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'radius',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_auth.pl',
+    ],
+  },
+}
diff --git a/src/test/radius/t/001_auth.pl b/src/test/radius/t/001_auth.pl
new file mode 100644
index 0000000000..44db62a3d7
--- /dev/null
+++ b/src/test/radius/t/001_auth.pl
@@ -0,0 +1,187 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Debian: apt-get install freeradius
+# Homebrew: brew install freeradius-server
+# FreeBSD: pkg install freeradius3
+# MacPorts: port install freeradius
+
+use strict;
+use warnings;
+use File::Copy;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+my $radiusd_dir = "${PostgreSQL::Test::Utils::tmp_check}/radiusd_data";
+my $radiusd_conf = "radiusd.conf";
+my $radiusd_users = "users.txt";
+my $radiusd_prefix;
+my $radiusd;
+
+if ($ENV{PG_TEST_EXTRA} !~ /\bradius\b/)
+{
+	plan skip_all => 'Potentially unsafe test RADIUS not enabled in PG_TEST_EXTRA';
+}
+elsif ($^O eq 'freebsd')
+{
+	$radiusd         = '/usr/local/sbin/radiusd';
+}
+elsif ($^O eq 'linux' && -f '/usr/sbin/freeradius')
+{
+	$radiusd         = '/usr/sbin/freeradius';
+}
+elsif ($^O eq 'linux')
+{
+	$radiusd         = '/usr/sbin/radiusd';
+}
+elsif ($^O eq 'darwin' && -d '/opt/local')
+{
+	# typical path for MacPorts
+	$radiusd         = '/opt/local/sbin/radiusd';
+	$radiusd_prefix  = '/opt/local';
+}
+elsif ($^O eq 'darwin' && -d '/opt/homebrew')
+{
+	# typical path for Homebrew on ARM
+	$radiusd         = '/opt/homebrew/bin/radiusd';
+	$radiusd_prefix  = '/opt/homebrew';
+}
+elsif ($^O eq 'darwin' && -d '/usr/local')
+{
+	# typical path for Homebrew on Intel
+	$radiusd         = '/usr/local/bin/radiusd';
+	$radiusd_prefix  = '/usr/local';
+}
+else
+{
+	plan skip_all =>
+	  "radius tests not supported on $^O or dependencies not installed";
+}
+
+my $radius_port     = PostgreSQL::Test::Cluster::get_free_port();
+
+note "setting up radiusd";
+
+mkdir $radiusd_dir or die "cannot create $radiusd_dir";
+
+append_to_file(
+	"$radiusd_dir/$radiusd_conf",
+	qq{client default {
+  ipaddr = "127.0.0.1"
+  secret = "secret"
+}
+
+modules {
+  files {
+    filename = "$radiusd_dir/users.txt"
+  }
+  pap {
+  }
+}
+
+server default {
+  listen {
+    type   = "auth"
+    ipv4addr = "127.0.0.1"
+    port = "$radius_port"
+  }
+  authenticate {
+    Auth-Type PAP {
+      pap
+    }
+  }
+  authorize {
+    files
+    pap
+  }
+}
+
+log {
+  destination = "files"
+  localstatedir = "$radiusd_dir"
+  logdir = "$radiusd_dir"
+  file = "$radiusd_dir/radius.log"
+}
+
+pidfile = "$radiusd_dir/radiusd.pid"
+});
+
+# help to find libraries that radiusd dlopens
+if ($radiusd_prefix)
+{
+	append_to_file(
+		"$radiusd_dir/$radiusd_conf",
+		qq{prefix="$radiusd_prefix"\n})
+}
+
+append_to_file(
+	"$radiusd_dir/$radiusd_users",
+	qq{test2 Cleartext-Password := "secret2"});
+
+system_or_bail $radiusd, '-xx', '-d', $radiusd_dir;
+
+END
+{
+	kill 'INT', `cat $radiusd_dir/radiusd.pid` if -f "$radiusd_dir/radiusd.pid";
+}
+
+note "setting up PostgreSQL instance";
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER test1;');
+$node->safe_psql('postgres', 'CREATE USER test2;');
+
+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 "enable RADIUS auth";
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf',
+	qq{local all all radius radiusservers="127.0.0.1" radiussecrets="secret" radiusports="$radius_port"}
+);
+$node->restart;
+
+note "simple negative and positive tests";
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access(
+	$node, 'test1', 2,
+	'authentication fails if user not found in RADIUS',
+	log_unlike => [qr/connection authenticated:/]);
+test_access(
+	$node, 'test2', 2,
+	'authentication fails with wrong password',
+	log_unlike => [qr/connection authenticated:/]);
+
+$ENV{"PGPASSWORD"} = 'secret2';
+test_access(
+	$node, 'test2', 0,
+	'authentication succeeds with right password',
+	log_like => [
+		qr/connection authenticated: identity="test2" method=radius/
+	],);
+
+done_testing();
-- 
2.39.2

