|
| 1 | + |
| 2 | +############################################################################ |
| 3 | +# |
| 4 | +# LdapServer.pm |
| 5 | +# |
| 6 | +# Module to set up an LDAP server for testing pg_hba.conf ldap authentication |
| 7 | +# |
| 8 | +# Copyright (c) 2023, PostgreSQL Global Development Group |
| 9 | +# |
| 10 | +############################################################################ |
| 11 | + |
| 12 | +=pod |
| 13 | +
|
| 14 | +=head1 NAME |
| 15 | +
|
| 16 | +LdapServer - class for an LDAP server for testing pg_hba.conf authentication |
| 17 | +
|
| 18 | +=head1 SYNOPSIS |
| 19 | +
|
| 20 | + use LdapServer; |
| 21 | +
|
| 22 | + # have we found openldap binaies suitable for setting up a server? |
| 23 | + my $ldap_binaries_found = $LdapServer::setup; |
| 24 | +
|
| 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); |
| 28 | +
|
| 29 | + # Add the contents of an LDIF file to the server |
| 30 | + $server->ldapadd_file ($path_to_ldif_data); |
| 31 | +
|
| 32 | + # set the Ldap password for a user |
| 33 | + $server->ldapsetpw($user, $password); |
| 34 | +
|
| 35 | + # get details of some settings for the server |
| 36 | + my @properties = $server->prop($propname1, $propname2, ...); |
| 37 | +
|
| 38 | +=head1 DESCRIPTION |
| 39 | +
|
| 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. |
| 43 | +
|
| 44 | +=cut |
| 45 | + |
| 46 | +package LdapServer; |
| 47 | + |
| 48 | +use strict; |
| 49 | +use warnings; |
| 50 | + |
| 51 | +use PostgreSQL::Test::Utils; |
| 52 | +use Test::More; |
| 53 | + |
| 54 | +use File::Copy; |
| 55 | +use File::Basename; |
| 56 | + |
| 57 | +# private variables |
| 58 | +my ($slapd, $ldap_schema_dir, @servers); |
| 59 | + |
| 60 | +# visible variable |
| 61 | +our ($setup); |
| 62 | + |
| 63 | +INIT |
| 64 | +{ |
| 65 | + $setup = 1; |
| 66 | + if ($^O eq 'darwin' && -d '/opt/homebrew/opt/openldap') |
| 67 | + { |
| 68 | + # typical paths for Homebrew on ARM |
| 69 | + $slapd = '/opt/homebrew/opt/openldap/libexec/slapd'; |
| 70 | + $ldap_schema_dir = '/opt/homebrew/etc/openldap/schema'; |
| 71 | + } |
| 72 | + elsif ($^O eq 'darwin' && -d '/usr/local/opt/openldap') |
| 73 | + { |
| 74 | + # typical paths for Homebrew on Intel |
| 75 | + $slapd = '/usr/local/opt/openldap/libexec/slapd'; |
| 76 | + $ldap_schema_dir = '/usr/local/etc/openldap/schema'; |
| 77 | + } |
| 78 | + elsif ($^O eq 'darwin' && -d '/opt/local/etc/openldap') |
| 79 | + { |
| 80 | + # typical paths for MacPorts |
| 81 | + $slapd = '/opt/local/libexec/slapd'; |
| 82 | + $ldap_schema_dir = '/opt/local/etc/openldap/schema'; |
| 83 | + } |
| 84 | + elsif ($^O eq 'linux') |
| 85 | + { |
| 86 | + $slapd = '/usr/sbin/slapd'; |
| 87 | + $ldap_schema_dir = '/etc/ldap/schema' if -d '/etc/ldap/schema'; |
| 88 | + $ldap_schema_dir = '/etc/openldap/schema' |
| 89 | + if -d '/etc/openldap/schema'; |
| 90 | + } |
| 91 | + elsif ($^O eq 'freebsd') |
| 92 | + { |
| 93 | + $slapd = '/usr/local/libexec/slapd'; |
| 94 | + $ldap_schema_dir = '/usr/local/etc/openldap/schema'; |
| 95 | + } |
| 96 | + elsif ($^O eq 'openbsd') |
| 97 | + { |
| 98 | + $slapd = '/usr/local/libexec/slapd'; |
| 99 | + $ldap_schema_dir = '/usr/local/share/examples/openldap/schema'; |
| 100 | + } |
| 101 | + else |
| 102 | + { |
| 103 | + $setup = 0; |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +END |
| 108 | +{ |
| 109 | + foreach my $server (@servers) |
| 110 | + { |
| 111 | + next unless -f $server->{pidfile}; |
| 112 | + my $pid = slurp_file($server->{pidfile}); |
| 113 | + chomp $pid; |
| 114 | + kill 'INT', $pid; |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +=pod |
| 119 | +
|
| 120 | +=head1 METHODS |
| 121 | +
|
| 122 | +=over |
| 123 | +
|
| 124 | +=item LdapServer->new($rootpw, $auth_type) |
| 125 | +
|
| 126 | +Create a new LDAP server. |
| 127 | +
|
| 128 | +The rootpw can be used when authenticating with the ldapbindpasswd option. |
| 129 | +
|
| 130 | +The auth_type is either 'users' or 'anonymous'. |
| 131 | +
|
| 132 | +=back |
| 133 | +
|
| 134 | +=cut |
| 135 | + |
| 136 | +sub new |
| 137 | +{ |
| 138 | + die "no suitable binaries found" unless $setup; |
| 139 | + |
| 140 | + my $class = shift; |
| 141 | + my $rootpw = shift; |
| 142 | + my $authtype = shift; # 'users' or 'anonymous' |
| 143 | + my $testname = basename((caller)[1], '.pl'); |
| 144 | + my $self = {}; |
| 145 | + |
| 146 | + my $test_temp = PostgreSQL::Test::Utils::tempdir("ldap-$testname"); |
| 147 | + |
| 148 | + my $ldap_datadir = "$test_temp/openldap-data"; |
| 149 | + my $slapd_certs = "$test_temp/slapd-certs"; |
| 150 | + my $slapd_pidfile = "$test_temp/slapd.pid"; |
| 151 | + my $slapd_conf = "$test_temp/slapd.conf"; |
| 152 | + my $slapd_logfile = |
| 153 | + "${PostgreSQL::Test::Utils::log_path}/slapd-$testname.log"; |
| 154 | + my $ldap_server = 'localhost'; |
| 155 | + my $ldap_port = PostgreSQL::Test::Cluster::get_free_port(); |
| 156 | + my $ldaps_port = PostgreSQL::Test::Cluster::get_free_port(); |
| 157 | + my $ldap_url = "ldap://$ldap_server:$ldap_port"; |
| 158 | + my $ldaps_url = "ldaps://$ldap_server:$ldaps_port"; |
| 159 | + my $ldap_basedn = 'dc=example,dc=net'; |
| 160 | + my $ldap_rootdn = 'cn=Manager,dc=example,dc=net'; |
| 161 | + my $ldap_rootpw = $rootpw; |
| 162 | + my $ldap_pwfile = "$test_temp/ldappassword"; |
| 163 | + |
| 164 | + (my $conf = <<"EOC") =~ s/^\t\t//gm; |
| 165 | + include $ldap_schema_dir/core.schema |
| 166 | + include $ldap_schema_dir/cosine.schema |
| 167 | + include $ldap_schema_dir/nis.schema |
| 168 | + include $ldap_schema_dir/inetorgperson.schema |
| 169 | +
|
| 170 | + pidfile $slapd_pidfile |
| 171 | + logfile $slapd_logfile |
| 172 | +
|
| 173 | + access to * |
| 174 | + by * read |
| 175 | + by $authtype auth |
| 176 | +
|
| 177 | + database ldif |
| 178 | + directory $ldap_datadir |
| 179 | +
|
| 180 | + TLSCACertificateFile $slapd_certs/ca.crt |
| 181 | + TLSCertificateFile $slapd_certs/server.crt |
| 182 | + TLSCertificateKeyFile $slapd_certs/server.key |
| 183 | +
|
| 184 | + suffix "dc=example,dc=net" |
| 185 | + rootdn "$ldap_rootdn" |
| 186 | + rootpw "$ldap_rootpw" |
| 187 | +EOC |
| 188 | + append_to_file($slapd_conf, $conf); |
| 189 | + |
| 190 | + mkdir $ldap_datadir or die "making $ldap_datadir: $!"; |
| 191 | + mkdir $slapd_certs or die "making $slapd_certs: $!"; |
| 192 | + |
| 193 | + my $certdir = dirname(__FILE__) . "/../ssl/ssl"; |
| 194 | + |
| 195 | + copy "$certdir/server_ca.crt", "$slapd_certs/ca.crt" |
| 196 | + || die "copying ca.crt: $!"; |
| 197 | + # check we actually have the file, as copy() sometimes gives a false success |
| 198 | + -f "$slapd_certs/ca.crt" || die "copying ca.crt (error unknown)"; |
| 199 | + copy "$certdir/server-cn-only.crt", "$slapd_certs/server.crt" |
| 200 | + || die "copying server.crt: $!"; |
| 201 | + copy "$certdir/server-cn-only.key", "$slapd_certs/server.key" |
| 202 | + || die "copying server.key: $!"; |
| 203 | + |
| 204 | + append_to_file($ldap_pwfile, $ldap_rootpw); |
| 205 | + chmod 0600, $ldap_pwfile or die "chmod on $ldap_pwfile"; |
| 206 | + |
| 207 | + system_or_bail $slapd, '-f', $slapd_conf, '-h', "$ldap_url $ldaps_url"; |
| 208 | + |
| 209 | + # wait until slapd accepts requests |
| 210 | + my $retries = 0; |
| 211 | + while (1) |
| 212 | + { |
| 213 | + last |
| 214 | + if ( |
| 215 | + system_log( |
| 216 | + "ldapsearch", "-sbase", |
| 217 | + "-H", $ldap_url, |
| 218 | + "-b", $ldap_basedn, |
| 219 | + "-D", $ldap_rootdn, |
| 220 | + "-y", $ldap_pwfile, |
| 221 | + "-n", "'objectclass=*'") == 0); |
| 222 | + die "cannot connect to slapd" if ++$retries >= 300; |
| 223 | + note "waiting for slapd to accept requests..."; |
| 224 | + Time::HiRes::usleep(1000000); |
| 225 | + } |
| 226 | + |
| 227 | + $self->{pidfile} = $slapd_pidfile; |
| 228 | + $self->{pwfile} = $ldap_pwfile; |
| 229 | + $self->{url} = $ldap_url; |
| 230 | + $self->{s_url} = $ldaps_url; |
| 231 | + $self->{server} = $ldap_server; |
| 232 | + $self->{port} = $ldap_port; |
| 233 | + $self->{s_port} = $ldaps_port; |
| 234 | + $self->{basedn} = $ldap_basedn; |
| 235 | + $self->{rootdn} = $ldap_rootdn; |
| 236 | + |
| 237 | + bless $self, $class; |
| 238 | + push @servers, $self; |
| 239 | + return $self; |
| 240 | +} |
| 241 | + |
| 242 | +# private routine to set up the environment for methods below |
| 243 | +sub _ldapenv |
| 244 | +{ |
| 245 | + my $self = shift; |
| 246 | + my %env = %ENV; |
| 247 | + $env{'LDAPURI'} = $self->{url}; |
| 248 | + $env{'LDAPBINDDN'} = $self->{rootdn}; |
| 249 | + return %env; |
| 250 | +} |
| 251 | + |
| 252 | +=pod |
| 253 | +
|
| 254 | +=over |
| 255 | +
|
| 256 | +=item ldap_add(filename) |
| 257 | +
|
| 258 | +filename is the path to a file containing LDIF data which is added to the LDAP |
| 259 | +server. |
| 260 | +
|
| 261 | +=back |
| 262 | +
|
| 263 | +=cut |
| 264 | + |
| 265 | +sub ldapadd_file |
| 266 | +{ |
| 267 | + my $self = shift; |
| 268 | + my $file = shift; |
| 269 | + |
| 270 | + local %ENV = $self->_ldapenv; |
| 271 | + |
| 272 | + system_or_bail 'ldapadd', '-x', '-y', $self->{pwfile}, '-f', $file; |
| 273 | +} |
| 274 | + |
| 275 | +=pod |
| 276 | +
|
| 277 | +=over |
| 278 | +
|
| 279 | +=item ldapsetpw(user, password) |
| 280 | +
|
| 281 | +Set the user's password in the LDAP server |
| 282 | +
|
| 283 | +=back |
| 284 | +
|
| 285 | +=cut |
| 286 | + |
| 287 | +sub ldapsetpw |
| 288 | +{ |
| 289 | + my $self = shift; |
| 290 | + my $user = shift; |
| 291 | + my $password = shift; |
| 292 | + |
| 293 | + local %ENV = $self->_ldapenv; |
| 294 | + |
| 295 | + system_or_bail 'ldappasswd', '-x', '-y', $self->{pwfile}, '-s', $password, |
| 296 | + $user; |
| 297 | +} |
| 298 | + |
| 299 | +=pod |
| 300 | +
|
| 301 | +=over |
| 302 | +
|
| 303 | +=item prop(name1, ...) |
| 304 | +
|
| 305 | +Returns the list of values for the specified properties of the instance, such |
| 306 | +as 'url', 'port', 'basedn'. |
| 307 | +
|
| 308 | +=back |
| 309 | +
|
| 310 | +=cut |
| 311 | + |
| 312 | +sub prop |
| 313 | +{ |
| 314 | + my $self = shift; |
| 315 | + my @settings; |
| 316 | + push @settings, $self->{$_} foreach (@_); |
| 317 | + return @settings; |
| 318 | +} |
| 319 | + |
| 320 | +1; |
0 commit comments