Improve check in LDAP test to find the OpenLDAP installation
[pgsql.git] / src / test / ldap / LdapServer.pm
blob91cd9e3762218c0cb8b434a2ace16a9beecdfe4d
2 ############################################################################
4 # LdapServer.pm
6 # Module to set up an LDAP server for testing pg_hba.conf ldap authentication
8 # Copyright (c) 2023-2024, PostgreSQL Global Development Group
10 ############################################################################
12 =pod
14 =head1 NAME
16 LdapServer - class for an LDAP server for testing pg_hba.conf authentication
18 =head1 SYNOPSIS
20 use LdapServer;
22 # have we found openldap binaries suitable for setting up a server?
23 my $ldap_binaries_found = $LdapServer::setup;
25 # create a server with the given root password and auth type
26 # (users or anonymous)
27 my $server = LdapServer->new($root_password, $auth_type);
29 # Add the contents of an LDIF file to the server
30 $server->ldapadd_file ($path_to_ldif_data);
32 # set the Ldap password for a user
33 $server->ldapsetpw($user, $password);
35 # get details of some settings for the server
36 my @properties = $server->prop($propname1, $propname2, ...);
38 =head1 DESCRIPTION
40 LdapServer tests in its INIT phase for the presence of suitable openldap
41 binaries. Its constructor method sets up and runs an LDAP server, and any
42 servers that are set up are terminated during its END phase.
44 =cut
46 package LdapServer;
48 use strict;
49 use warnings FATAL => 'all';
51 use PostgreSQL::Test::Utils;
52 use Test::More;
54 use File::Copy;
55 use File::Basename;
57 # private variables
58 my ($slapd, $ldap_schema_dir, @servers);
60 # visible variables
61 our ($setup, $setup_error);
63 INIT
65 # Find the OpenLDAP server binary and directory containing schema
66 # definition files. On success, $setup is set to 1. On failure,
67 # it's set to 0, and an error message is set in $setup_error.
68 $setup = 1;
69 if ($^O eq 'darwin')
71 if (-d '/opt/homebrew/opt/openldap')
73 # typical paths for Homebrew on ARM
74 $slapd = '/opt/homebrew/opt/openldap/libexec/slapd';
75 $ldap_schema_dir = '/opt/homebrew/etc/openldap/schema';
77 elsif (-d '/usr/local/opt/openldap')
79 # typical paths for Homebrew on Intel
80 $slapd = '/usr/local/opt/openldap/libexec/slapd';
81 $ldap_schema_dir = '/usr/local/etc/openldap/schema';
83 elsif (-d '/opt/local/etc/openldap')
85 # typical paths for MacPorts
86 $slapd = '/opt/local/libexec/slapd';
87 $ldap_schema_dir = '/opt/local/etc/openldap/schema';
89 else
91 $setup_error = "OpenLDAP server installation not found";
92 $setup = 0;
95 elsif ($^O eq 'linux')
97 if (-d '/etc/ldap/schema')
99 $slapd = '/usr/sbin/slapd';
100 $ldap_schema_dir = '/etc/ldap/schema';
102 elsif (-d '/etc/openldap/schema')
104 $slapd = '/usr/sbin/slapd';
105 $ldap_schema_dir = '/etc/openldap/schema';
107 else
109 $setup_error = "OpenLDAP server installation not found";
110 $setup = 0;
113 elsif ($^O eq 'freebsd')
115 if (-d '/usr/local/etc/openldap/schema')
117 $slapd = '/usr/local/libexec/slapd';
118 $ldap_schema_dir = '/usr/local/etc/openldap/schema';
120 else
122 $setup_error = "OpenLDAP server installation not found";
123 $setup = 0;
126 elsif ($^O eq 'openbsd')
128 if (-d '/usr/local/share/examples/openldap/schema')
130 $slapd = '/usr/local/libexec/slapd';
131 $ldap_schema_dir = '/usr/local/share/examples/openldap/schema';
133 else
135 $setup_error = "OpenLDAP server installation not found";
136 $setup = 0;
139 else
141 $setup_error = "ldap tests not supported on $^O";
142 $setup = 0;
148 foreach my $server (@servers)
150 next unless -f $server->{pidfile};
151 my $pid = slurp_file($server->{pidfile});
152 chomp $pid;
153 kill 'INT', $pid;
157 =pod
159 =head1 METHODS
161 =over
163 =item LdapServer->new($rootpw, $auth_type)
165 Create a new LDAP server.
167 The rootpw can be used when authenticating with the ldapbindpasswd option.
169 The auth_type is either 'users' or 'anonymous'.
171 =back
173 =cut
175 sub new
177 die "no suitable binaries found" unless $setup;
179 my $class = shift;
180 my $rootpw = shift;
181 my $authtype = shift; # 'users' or 'anonymous'
182 my $testname = basename((caller)[1], '.pl');
183 my $self = {};
185 my $test_temp = PostgreSQL::Test::Utils::tempdir("ldap-$testname");
187 my $ldap_datadir = "$test_temp/openldap-data";
188 my $slapd_certs = "$test_temp/slapd-certs";
189 my $slapd_pidfile = "$test_temp/slapd.pid";
190 my $slapd_conf = "$test_temp/slapd.conf";
191 my $slapd_logfile =
192 "${PostgreSQL::Test::Utils::log_path}/slapd-$testname.log";
193 my $ldap_server = 'localhost';
194 my $ldap_port = PostgreSQL::Test::Cluster::get_free_port();
195 my $ldaps_port = PostgreSQL::Test::Cluster::get_free_port();
196 my $ldap_url = "ldap://$ldap_server:$ldap_port";
197 my $ldaps_url = "ldaps://$ldap_server:$ldaps_port";
198 my $ldap_basedn = 'dc=example,dc=net';
199 my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
200 my $ldap_rootpw = $rootpw;
201 my $ldap_pwfile = "$test_temp/ldappassword";
203 (my $conf = <<"EOC") =~ s/^\t\t//gm;
204 include $ldap_schema_dir/core.schema
205 include $ldap_schema_dir/cosine.schema
206 include $ldap_schema_dir/nis.schema
207 include $ldap_schema_dir/inetorgperson.schema
209 pidfile $slapd_pidfile
210 logfile $slapd_logfile
212 access to *
213 by * read
214 by $authtype auth
216 database ldif
217 directory $ldap_datadir
219 TLSCACertificateFile $slapd_certs/ca.crt
220 TLSCertificateFile $slapd_certs/server.crt
221 TLSCertificateKeyFile $slapd_certs/server.key
223 suffix "dc=example,dc=net"
224 rootdn "$ldap_rootdn"
225 rootpw "$ldap_rootpw"
227 append_to_file($slapd_conf, $conf);
229 mkdir $ldap_datadir or die "making $ldap_datadir: $!";
230 mkdir $slapd_certs or die "making $slapd_certs: $!";
232 my $certdir = dirname(__FILE__) . "/../ssl/ssl";
234 copy "$certdir/server_ca.crt", "$slapd_certs/ca.crt"
235 || die "copying ca.crt: $!";
236 # check we actually have the file, as copy() sometimes gives a false success
237 -f "$slapd_certs/ca.crt" || die "copying ca.crt (error unknown)";
238 copy "$certdir/server-cn-only.crt", "$slapd_certs/server.crt"
239 || die "copying server.crt: $!";
240 copy "$certdir/server-cn-only.key", "$slapd_certs/server.key"
241 || die "copying server.key: $!";
243 append_to_file($ldap_pwfile, $ldap_rootpw);
244 chmod 0600, $ldap_pwfile or die "chmod on $ldap_pwfile";
246 # -s0 prevents log messages ending up in syslog
247 system_or_bail $slapd, '-f', $slapd_conf, '-s0', '-h',
248 "$ldap_url $ldaps_url";
250 # wait until slapd accepts requests
251 my $retries = 0;
252 while (1)
254 last
255 if (
256 system_log(
257 "ldapsearch", "-sbase",
258 "-H", $ldap_url,
259 "-b", $ldap_basedn,
260 "-D", $ldap_rootdn,
261 "-y", $ldap_pwfile,
262 "-n", "'objectclass=*'") == 0);
263 die "cannot connect to slapd" if ++$retries >= 300;
264 note "waiting for slapd to accept requests...";
265 Time::HiRes::usleep(1000000);
268 $self->{pidfile} = $slapd_pidfile;
269 $self->{pwfile} = $ldap_pwfile;
270 $self->{url} = $ldap_url;
271 $self->{s_url} = $ldaps_url;
272 $self->{server} = $ldap_server;
273 $self->{port} = $ldap_port;
274 $self->{s_port} = $ldaps_port;
275 $self->{basedn} = $ldap_basedn;
276 $self->{rootdn} = $ldap_rootdn;
278 bless $self, $class;
279 push @servers, $self;
280 return $self;
283 # private routine to set up the environment for methods below
284 sub _ldapenv
286 my $self = shift;
287 my %env = %ENV;
288 $env{'LDAPURI'} = $self->{url};
289 $env{'LDAPBINDDN'} = $self->{rootdn};
290 return %env;
293 =pod
295 =over
297 =item ldapadd_file(filename)
299 filename is the path to a file containing LDIF data which is added to the LDAP
300 server.
302 =back
304 =cut
306 sub ldapadd_file
308 my $self = shift;
309 my $file = shift;
311 local %ENV = $self->_ldapenv;
313 system_or_bail 'ldapadd', '-x', '-y', $self->{pwfile}, '-f', $file;
316 =pod
318 =over
320 =item ldapsetpw(user, password)
322 Set the user's password in the LDAP server
324 =back
326 =cut
328 sub ldapsetpw
330 my $self = shift;
331 my $user = shift;
332 my $password = shift;
334 local %ENV = $self->_ldapenv;
336 system_or_bail 'ldappasswd', '-x', '-y', $self->{pwfile}, '-s', $password,
337 $user;
340 =pod
342 =over
344 =item prop(name1, ...)
346 Returns the list of values for the specified properties of the instance, such
347 as 'url', 'port', 'basedn'.
349 =back
351 =cut
353 sub prop
355 my $self = shift;
356 my @settings;
357 push @settings, $self->{$_} foreach (@_);
358 return @settings;