# Copyright (c) 2021-2023, PostgreSQL Global Development Group # Tests for include directives in HBA and ident files. This test can # only run with Unix-domain sockets. use strict; use warnings; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use File::Basename qw(basename); use Test::More; use Data::Dumper; if (!$use_unix_sockets) { plan skip_all => "authentication tests cannot run without Unix-domain sockets"; } # Stores the number of lines created for each file. hba_rule and ident_rule # are used to respectively track pg_hba_file_rules.rule_number and # pg_ident_file_mappings.map_number, which are the global counters associated # to each view tracking the priority of each entry processed. my %line_counters = ('hba_rule' => 0, 'ident_rule' => 0); # Add some data to the given HBA configuration file, generating the contents # expected to match pg_hba_file_rules. # # Note that this function maintains %line_counters, used to generate the # catalog output for file lines and rule numbers. # # If the entry starts with "include", the function does not increase # the general hba rule number as an include directive generates no data # in pg_hba_file_rules. # # This function returns the entry of pg_hba_file_rules expected when this # is loaded by the backend. sub add_hba_line { my $node = shift; my $filename = shift; my $entry = shift; my $globline; my $fileline; my @tokens; my $line; # Append the entry to the given file $node->append_conf($filename, $entry); my $base_filename = basename($filename); # Get the current %line_counters for the file. if (not defined $line_counters{$filename}) { $line_counters{$filename} = 0; } $fileline = ++$line_counters{$filename}; # Include directive, that does not generate a view entry. return '' if ($entry =~ qr/^include/); # Increment pg_hba_file_rules.rule_number and save it. $globline = ++$line_counters{'hba_rule'}; # Generate the expected pg_hba_file_rules line @tokens = split(/ /, $entry); $tokens[1] = '{' . $tokens[1] . '}'; # database $tokens[2] = '{' . $tokens[2] . '}'; # user_name # Append empty options and error push @tokens, ''; push @tokens, ''; # Final line expected, output of the SQL query. $line = ""; $line .= "\n" if ($globline > 1); $line .= "$globline|$base_filename|$fileline|"; $line .= join('|', @tokens); return $line; } # Add some data to the given ident configuration file, generating the # contents expected to match pg_ident_file_mappings. # # Note that this function maintains %line_counters, generating catalog # entries for the file line and the map number. # # If the entry starts with "include", the function does not increase # the general map number as an include directive generates no data in # pg_ident_file_mappings. # # This works pretty much the same as add_hba_line() above, except that it # returns an entry to match with pg_ident_file_mappings. sub add_ident_line { my $node = shift; my $filename = shift; my $entry = shift; my $globline; my $fileline; my @tokens; my $line; my $base_filename = basename($filename); # Append the entry to the given file $node->append_conf($filename, $entry); # Get the current %line_counters counter for the file if (not defined $line_counters{$filename}) { $line_counters{$filename} = 0; } $fileline = ++$line_counters{$filename}; # Include directive, that does not generate a view entry. return '' if ($entry =~ qr/^include/); # Increment pg_ident_file_mappings.map_number and get it. $globline = ++$line_counters{'ident_rule'}; # Generate the expected pg_ident_file_mappings line @tokens = split(/ /, $entry); # Append empty error push @tokens, ''; # Final line expected, output of the SQL query. $line = ""; $line .= "\n" if ($globline > 1); $line .= "$globline|$base_filename|$fileline|"; $line .= join('|', @tokens); return $line; } # Locations for the entry points of the HBA and ident files. my $hba_file = 'subdir1/pg_hba_custom.conf'; my $ident_file = 'subdir2/pg_ident_custom.conf'; my $node = PostgreSQL::Test::Cluster->new('primary'); $node->init; $node->start; my $data_dir = $node->data_dir; note "Generating HBA structure with include directives"; my $hba_expected = ''; my $ident_expected = ''; # customise main auth file names $node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'"); $node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'"); # Remove the original ones, this node links to non-default ones now. unlink("$data_dir/pg_hba.conf"); unlink("$data_dir/pg_ident.conf"); # Generate HBA contents with include directives. mkdir("$data_dir/subdir1"); mkdir("$data_dir/hba_inc"); mkdir("$data_dir/hba_inc_if"); mkdir("$data_dir/hba_pos"); # First, make sure that we will always be able to connect. $hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust'); # "include". Note that as $hba_file is located in $data_dir/subdir1, # pg_hba_pre.conf is located at the root of the data directory. $hba_expected .= add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf"); $hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject"); $hba_expected .= add_hba_line($node, "$hba_file", "local all all reject"); add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf"); $hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject"); # When an include directive refers to a relative path, it is compiled # from the base location of the file loaded from. $hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf"); $hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject"); $hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos3 all reject"); # include_if_exists data, nothing generated for the catalog. # Missing file, no catalog entries. $hba_expected .= add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none"); # File with some contents loaded. $hba_expected .= add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some"); $hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject"); # include_dir $hba_expected .= add_hba_line($node, "$hba_file", "include_dir ../hba_inc"); $hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject"); $hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject"); # Garbage file not suffixed by .conf, so it will be ignored. $node->append_conf('hba_inc/garbageconf', "should not be included"); # Authentication file expanded in an existing entry for database names. # As it is expanded, ignore the output generated. add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject'); $node->append_conf('dbnames.conf', "db1"); $node->append_conf('dbnames.conf', "db3"); $hba_expected .= "\n" . $line_counters{'hba_rule'} . "|" . basename($hba_file) . "|" . $line_counters{$hba_file} . '|local|{db1,db3}|{all}|reject||'; note "Generating ident structure with include directives"; mkdir("$data_dir/subdir2"); mkdir("$data_dir/ident_inc"); mkdir("$data_dir/ident_inc_if"); mkdir("$data_dir/ident_pos"); # include. Note that pg_ident_pre.conf is located at the root of the data # directory. $ident_expected .= add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf"); $ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar"); $ident_expected .= add_ident_line($node, "$ident_file", "test a b"); $ident_expected .= add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf"); $ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar"); # When an include directive refers to a relative path, it is compiled # from the base location of the file loaded from. $ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf"); $ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar"); $ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos3 foo bar"); # include_if_exists # Missing file, no catalog entries. $ident_expected .= add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none"); # File with some contents loaded. $ident_expected .= add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some"); $ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar"); # include_dir $ident_expected .= add_ident_line($node, "$ident_file", "include_dir ../ident_inc"); $ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar"); $ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar"); # Garbage file not suffixed by .conf, so it will be ignored. $node->append_conf('ident_inc/garbageconf', "should not be included"); $node->restart; # Note that the base path is filtered out, keeping only the file name # to bypass portability issues. The configuration files had better # have unique names. my $contents = $node->safe_psql( 'postgres', qq(SELECT rule_number, regexp_replace(file_name, '.*/', ''), line_number, type, database, user_name, auth_method, options, error FROM pg_hba_file_rules ORDER BY rule_number;)); is($contents, $hba_expected, 'check contents of pg_hba_file_rules'); $contents = $node->safe_psql( 'postgres', qq(SELECT map_number, regexp_replace(file_name, '.*/', ''), line_number, map_name, sys_name, pg_username, error FROM pg_ident_file_mappings ORDER BY map_number)); is($contents, $ident_expected, 'check contents of pg_ident_file_mappings'); done_testing();