blob: 55d28ad58644f0126ad75c2016240514f317c9a4 [file] [log] [blame]
# 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();