|
| 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 | +=head1NAME |
| 15 | +
|
| 16 | +LdapServer - class for an LDAP server for testing pg_hba.conf authentication |
| 17 | +
|
| 18 | +=head1SYNOPSIS |
| 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 | +=head1DESCRIPTION |
| 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 | +packageLdapServer; |
| 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 ($^Oeq'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 ($^Oeq'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 ($^Oeq'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 ($^Oeq'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 ($^Oeq'freebsd') |
| 92 | +{ |
| 93 | +$slapd ='/usr/local/libexec/slapd'; |
| 94 | +$ldap_schema_dir ='/usr/local/etc/openldap/schema'; |
| 95 | +} |
| 96 | +elsif ($^Oeq'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 | +foreachmy$server (@servers) |
| 110 | +{ |
| 111 | +nextunless-f$server->{pidfile}; |
| 112 | +my$pid = slurp_file($server->{pidfile}); |
| 113 | +chomp$pid; |
| 114 | +kill'INT',$pid; |
| 115 | +} |
| 116 | +} |
| 117 | + |
| 118 | +=pod |
| 119 | +
|
| 120 | +=head1METHODS |
| 121 | +
|
| 122 | +=over |
| 123 | +
|
| 124 | +=itemLdapServer->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 | +subnew |
| 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_datadirordie"making$ldap_datadir:$!"; |
| 191 | +mkdir$slapd_certsordie"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_pwfileordie"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 | +=itemldap_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 | +subldapadd_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 | +=itemldapsetpw(user, password) |
| 280 | +
|
| 281 | +Set the user's password in the LDAP server |
| 282 | +
|
| 283 | +=back |
| 284 | +
|
| 285 | +=cut |
| 286 | + |
| 287 | +subldapsetpw |
| 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 | +=itemprop(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 | +subprop |
| 313 | +{ |
| 314 | +my$self =shift; |
| 315 | +my@settings; |
| 316 | +push@settings,$self->{$_}foreach (@_); |
| 317 | +return@settings; |
| 318 | +} |
| 319 | + |
| 320 | +1; |