aboutsummaryrefslogtreecommitdiff
path: root/src/test/perl/PostgreSQL/Test/Kerberos.pm
blob: f76d765368e272e4b9da054af1472dbd3fd188eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# Copyright (c) 2021-2024, PostgreSQL Global Development Group

# Sets up a stand-alone KDC for testing PostgreSQL GSSAPI / Kerberos
# functionality.

package PostgreSQL::Test::Kerberos;

use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Utils;

our (
	$krb5_bin_dir, $krb5_sbin_dir, $krb5_config, $kinit,
	$klist, $kdb5_util, $kadmin_local, $krb5kdc,
	$krb5_conf, $kdc_conf, $krb5_cache, $krb5_log,
	$kdc_log, $kdc_port, $kdc_datadir, $kdc_pidfile,
	$keytab);

INIT
{
	if ($^O eq 'darwin' && -d "/opt/homebrew")
	{
		# typical paths for Homebrew on ARM
		$krb5_bin_dir = '/opt/homebrew/opt/krb5/bin';
		$krb5_sbin_dir = '/opt/homebrew/opt/krb5/sbin';
	}
	elsif ($^O eq 'darwin')
	{
		# typical paths for Homebrew on Intel
		$krb5_bin_dir = '/usr/local/opt/krb5/bin';
		$krb5_sbin_dir = '/usr/local/opt/krb5/sbin';
	}
	elsif ($^O eq 'freebsd')
	{
		$krb5_bin_dir = '/usr/local/bin';
		$krb5_sbin_dir = '/usr/local/sbin';
	}
	elsif ($^O eq 'linux')
	{
		$krb5_sbin_dir = '/usr/sbin';
	}

	$krb5_config = 'krb5-config';
	$kinit = 'kinit';
	$klist = 'klist';
	$kdb5_util = 'kdb5_util';
	$kadmin_local = 'kadmin.local';
	$krb5kdc = 'krb5kdc';

	if ($krb5_bin_dir && -d $krb5_bin_dir)
	{
		$krb5_config = $krb5_bin_dir . '/' . $krb5_config;
		$kinit = $krb5_bin_dir . '/' . $kinit;
		$klist = $krb5_bin_dir . '/' . $klist;
	}
	if ($krb5_sbin_dir && -d $krb5_sbin_dir)
	{
		$kdb5_util = $krb5_sbin_dir . '/' . $kdb5_util;
		$kadmin_local = $krb5_sbin_dir . '/' . $kadmin_local;
		$krb5kdc = $krb5_sbin_dir . '/' . $krb5kdc;
	}

	$krb5_conf = "${PostgreSQL::Test::Utils::tmp_check}/krb5.conf";
	$kdc_conf = "${PostgreSQL::Test::Utils::tmp_check}/kdc.conf";
	$krb5_cache = "${PostgreSQL::Test::Utils::tmp_check}/krb5cc";
	$krb5_log = "${PostgreSQL::Test::Utils::log_path}/krb5libs.log";
	$kdc_log = "${PostgreSQL::Test::Utils::log_path}/krb5kdc.log";
	$kdc_port = PostgreSQL::Test::Cluster::get_free_port();
	$kdc_datadir = "${PostgreSQL::Test::Utils::tmp_check}/krb5kdc";
	$kdc_pidfile = "${PostgreSQL::Test::Utils::tmp_check}/krb5kdc.pid";
	$keytab = "${PostgreSQL::Test::Utils::tmp_check}/krb5.keytab";
}

=pod

=item PostgreSQL::Test::Kerberos->new(host, hostaddr, realm, %params)

Sets up a new Kerberos realm and KDC.  This function assigns a free
port for the KDC.  The KDC will be shut down automatically when the
test script exits.

=over

=item host => 'auth-test-localhost.postgresql.example.com'

Hostname to use in the service principal.

=item hostaddr => '127.0.0.1'

Network interface the KDC will listen on.

=item realm => 'EXAMPLE.COM'

Name of the Kerberos realm.

=back

=cut

sub new
{
	my $class = shift;
	my ($host, $hostaddr, $realm) = @_;

	my ($stdout, $krb5_version);
	run_log [ $krb5_config, '--version' ], '>', \$stdout
	  or BAIL_OUT("could not execute krb5-config");
	BAIL_OUT("Heimdal is not supported") if $stdout =~ m/heimdal/;
	$stdout =~ m/Kerberos 5 release ([0-9]+\.[0-9]+)/
	  or BAIL_OUT("could not get Kerberos version");
	$krb5_version = $1;

	# Build the krb5.conf to use.
	#
	# Explicitly specify the default (test) realm and the KDC for
	# that realm to avoid the Kerberos library trying to look up
	# that information in DNS, and also because we're using a
	# non-standard KDC port.
	#
	# Also explicitly disable DNS lookups since this isn't really
	# our domain and we shouldn't be causing random DNS requests
	# to be sent out (not to mention that broken DNS environments
	# can cause the tests to take an extra long time and timeout).
	#
	# Reverse DNS is explicitly disabled to avoid any issue with a
	# captive portal or other cases where the reverse DNS succeeds
	# and the Kerberos library uses that as the canonical name of
	# the host and then tries to acquire a cross-realm ticket.
	append_to_file(
		$krb5_conf,
		qq![logging]
default = FILE:$krb5_log
kdc = FILE:$kdc_log

[libdefaults]
dns_lookup_realm = false
dns_lookup_kdc = false
default_realm = $realm
forwardable = false
rdns = false

[realms]
$realm = {
    kdc = $hostaddr:$kdc_port
}
!);

	append_to_file(
		$kdc_conf,
		qq![kdcdefaults]
!);

	# For new-enough versions of krb5, use the _listen settings rather
	# than the _ports settings so that we can bind to localhost only.
	if ($krb5_version >= 1.15)
	{
		append_to_file(
			$kdc_conf,
			qq!kdc_listen = $hostaddr:$kdc_port
kdc_tcp_listen = $hostaddr:$kdc_port
!);
	}
	else
	{
		append_to_file(
			$kdc_conf,
			qq!kdc_ports = $kdc_port
kdc_tcp_ports = $kdc_port
!);
	}
	append_to_file(
		$kdc_conf,
		qq!
[realms]
$realm = {
    database_name = $kdc_datadir/principal
    admin_keytab = FILE:$kdc_datadir/kadm5.keytab
    acl_file = $kdc_datadir/kadm5.acl
    key_stash_file = $kdc_datadir/_k5.$realm
}!);

	mkdir $kdc_datadir
	  or BAIL_OUT("could not create directory \"$kdc_datadir\"");

	# Ensure that we use test's config and cache files, not global ones.
	$ENV{'KRB5_CONFIG'} = $krb5_conf;
	$ENV{'KRB5_KDC_PROFILE'} = $kdc_conf;
	$ENV{'KRB5CCNAME'} = $krb5_cache;

	my $service_principal = "$ENV{with_krb_srvnam}/$host";

	system_or_bail $kdb5_util, 'create', '-s', '-P', 'secret0';

	system_or_bail $kadmin_local, '-q',
	  "addprinc -randkey $service_principal";
	system_or_bail $kadmin_local, '-q', "ktadd -k $keytab $service_principal";

	system_or_bail $krb5kdc, '-P', $kdc_pidfile;

	my $self = {};
	$self->{keytab} = $keytab;

	bless $self, $class;

	return $self;
}

sub create_principal
{
	my ($self, $principal, $password) = @_;

	system_or_bail $kadmin_local, '-q', "addprinc -pw $password $principal";
}

sub create_ticket
{
	my ($self, $principal, $password, %params) = @_;

	my @cmd = ($kinit, $principal);

	push @cmd, '-f' if ($params{forwardable});

	run_log [@cmd], \$password or BAIL_OUT($?);
	run_log [ $klist, '-f' ] or BAIL_OUT($?);
}

END
{
	# take care not to change the script's exit value
	my $exit_code = $?;

	kill 'INT', `cat $kdc_pidfile`
	  if defined($kdc_pidfile) && -f $kdc_pidfile;

	$? = $exit_code;
}

1;