Add paths of extensions to pg_available_extensions

Add a new "location" column to the pg_available_extensions and
pg_available_extension_versions views, exposing the directory where
the extension is located.

The default system location is shown as '$system', the same value
that can be used to configure the extension_control_path GUC.

User-defined locations are only visible for super users, otherwise
'<insufficient privilege>' is returned as a column value, the same
behaviour that we already use in pg_stat_activity.

I failed to resist the temptation to do a little extra editorializing of
the TAP test script.

Catalog version bumped.

Author: Matheus Alcantara <mths.dev@pm.me>
Reviewed-By: Chao Li <li.evan.chao@gmail.com>
Reviewed-By: Rohit Prasad <rohit.prasad@arm.com>
Reviewed-By: Michael Banck <mbanck@gmx.net>
Reviewed-By: Manni Wood <manni.wood@enterprisedb.com>
Reviewed-By: Euler Taveira <euler@eulerto.com>
Reviewed-By: Quan Zongliang <quanzongliang@yeah.net>
This commit is contained in:
Andrew Dunstan
2026-01-01 12:11:37 -05:00
parent 85d5bd308b
commit f3c9e341cd
8 changed files with 165 additions and 48 deletions

View File

@@ -599,6 +599,20 @@
</para></entry> </para></entry>
</row> </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>location</structfield> <type>text</type>
</para>
<para>
The location where the extension is installed. If it is in the standard
system location, then the value will be <literal>$system</literal>,
while if it is found in the path specified by the
<link linkend="guc-extension-control-path"><structname>extension_control_path</structname></link>
GUC then the full path will be shown.
Only superusers can see this information.
</para></entry>
</row>
<row> <row>
<entry role="catalog_table_entry"><para role="column_definition"> <entry role="catalog_table_entry"><para role="column_definition">
<structfield>comment</structfield> <type>text</type> <structfield>comment</structfield> <type>text</type>
@@ -723,6 +737,20 @@
</para></entry> </para></entry>
</row> </row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>location</structfield> <type>text</type>
</para>
<para>
The location where the extension is installed. If it is in the standard
system location, then the value will be <literal>$system</literal>,
while if it is found in the path specified by the
<link linkend="guc-extension-control-path"><structname>extension_control_path</structname></link>
GUC then the full path will be shown.
Only superusers can see this information.
</para></entry>
</row>
<row> <row>
<entry role="catalog_table_entry"><para role="column_definition"> <entry role="catalog_table_entry"><para role="column_definition">
<structfield>comment</structfield> <type>text</type> <structfield>comment</structfield> <type>text</type>

View File

@@ -412,14 +412,14 @@ CREATE VIEW pg_cursors AS
CREATE VIEW pg_available_extensions AS CREATE VIEW pg_available_extensions AS
SELECT E.name, E.default_version, X.extversion AS installed_version, SELECT E.name, E.default_version, X.extversion AS installed_version,
E.comment E.location, E.comment
FROM pg_available_extensions() AS E FROM pg_available_extensions() AS E
LEFT JOIN pg_extension AS X ON E.name = X.extname; LEFT JOIN pg_extension AS X ON E.name = X.extname;
CREATE VIEW pg_available_extension_versions AS CREATE VIEW pg_available_extension_versions AS
SELECT E.name, E.version, (X.extname IS NOT NULL) AS installed, SELECT E.name, E.version, (X.extname IS NOT NULL) AS installed,
E.superuser, E.trusted, E.relocatable, E.superuser, E.trusted, E.relocatable,
E.schema, E.requires, E.comment E.schema, E.requires, E.location, E.comment
FROM pg_available_extension_versions() AS E FROM pg_available_extension_versions() AS E
LEFT JOIN pg_extension AS X LEFT JOIN pg_extension AS X
ON E.name = X.extname AND E.version = X.extversion; ON E.name = X.extname AND E.version = X.extversion;

View File

@@ -126,6 +126,21 @@ typedef struct
ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */
} script_error_callback_arg; } script_error_callback_arg;
/*
* A location based on the extension_control_path GUC.
*
* The macro field stores the name of a macro (for example “$system”) that
* the extension_control_path processing supports, and which can be replaced
* by a system value stored in loc.
*
* For non-system paths the macro field is NULL.
*/
typedef struct
{
char *macro;
char *loc;
} ExtensionLocation;
/* Local functions */ /* Local functions */
static List *find_update_path(List *evi_list, static List *find_update_path(List *evi_list,
ExtensionVersionInfo *evi_start, ExtensionVersionInfo *evi_start,
@@ -140,7 +155,8 @@ static Oid get_required_extension(char *reqExtensionName,
bool is_create); bool is_create);
static void get_available_versions_for_extension(ExtensionControlFile *pcontrol, static void get_available_versions_for_extension(ExtensionControlFile *pcontrol,
Tuplestorestate *tupstore, Tuplestorestate *tupstore,
TupleDesc tupdesc); TupleDesc tupdesc,
ExtensionLocation *location);
static Datum convert_requires_to_datum(List *requires); static Datum convert_requires_to_datum(List *requires);
static void ApplyExtensionUpdates(Oid extensionOid, static void ApplyExtensionUpdates(Oid extensionOid,
ExtensionControlFile *pcontrol, ExtensionControlFile *pcontrol,
@@ -157,6 +173,29 @@ static ExtensionControlFile *new_ExtensionControlFile(const char *extname);
char *find_in_paths(const char *basename, List *paths); char *find_in_paths(const char *basename, List *paths);
/*
* Return the extension location. If the current user doesn't have sufficient
* privilege, don't show the location.
*/
static char *
get_extension_location(ExtensionLocation *loc)
{
/* We only want to show extension paths for superusers. */
if (superuser())
{
/* Return the macro value if present to avoid showing system paths. */
if (loc->macro != NULL)
return loc->macro;
else
return loc->loc;
}
else
{
/* Similar to pg_stat_activity for unprivileged users */
return "<insufficient privilege>";
}
}
/* /*
* get_extension_oid - given an extension name, look up the OID * get_extension_oid - given an extension name, look up the OID
* *
@@ -354,7 +393,11 @@ get_extension_control_directories(void)
if (strlen(Extension_control_path) == 0) if (strlen(Extension_control_path) == 0)
{ {
paths = lappend(paths, system_dir); ExtensionLocation *location = palloc_object(ExtensionLocation);
location->macro = NULL;
location->loc = system_dir;
paths = lappend(paths, location);
} }
else else
{ {
@@ -366,6 +409,7 @@ get_extension_control_directories(void)
int len; int len;
char *mangled; char *mangled;
char *piece = first_path_var_separator(ecp); char *piece = first_path_var_separator(ecp);
ExtensionLocation *location = palloc_object(ExtensionLocation);
/* Get the length of the next path on ecp */ /* Get the length of the next path on ecp */
if (piece == NULL) if (piece == NULL)
@@ -382,15 +426,21 @@ get_extension_control_directories(void)
* suffix if it is a custom extension control path. * suffix if it is a custom extension control path.
*/ */
if (strcmp(piece, "$system") == 0) if (strcmp(piece, "$system") == 0)
{
location->macro = pstrdup(piece);
mangled = substitute_path_macro(piece, "$system", system_dir); mangled = substitute_path_macro(piece, "$system", system_dir);
}
else else
{
location->macro = NULL;
mangled = psprintf("%s/extension", piece); mangled = psprintf("%s/extension", piece);
}
pfree(piece); pfree(piece);
/* Canonicalize the path based on the OS and add to the list */ /* Canonicalize the path based on the OS and add to the list */
canonicalize_path(mangled); canonicalize_path(mangled);
paths = lappend(paths, mangled); location->loc = mangled;
paths = lappend(paths, location);
/* Break if ecp is empty or move to the next path on ecp */ /* Break if ecp is empty or move to the next path on ecp */
if (ecp[len] == '\0') if (ecp[len] == '\0')
@@ -2215,9 +2265,9 @@ pg_available_extensions(PG_FUNCTION_ARGS)
locations = get_extension_control_directories(); locations = get_extension_control_directories();
foreach_ptr(char, location, locations) foreach_ptr(ExtensionLocation, location, locations)
{ {
dir = AllocateDir(location); dir = AllocateDir(location->loc);
/* /*
* If the control directory doesn't exist, we want to silently return * If the control directory doesn't exist, we want to silently return
@@ -2229,13 +2279,13 @@ pg_available_extensions(PG_FUNCTION_ARGS)
} }
else else
{ {
while ((de = ReadDir(dir, location)) != NULL) while ((de = ReadDir(dir, location->loc)) != NULL)
{ {
ExtensionControlFile *control; ExtensionControlFile *control;
char *extname; char *extname;
String *extname_str; String *extname_str;
Datum values[3]; Datum values[4];
bool nulls[3]; bool nulls[4];
if (!is_extension_control_filename(de->d_name)) if (!is_extension_control_filename(de->d_name))
continue; continue;
@@ -2259,7 +2309,7 @@ pg_available_extensions(PG_FUNCTION_ARGS)
found_ext = lappend(found_ext, extname_str); found_ext = lappend(found_ext, extname_str);
control = new_ExtensionControlFile(extname); control = new_ExtensionControlFile(extname);
control->control_dir = pstrdup(location); control->control_dir = pstrdup(location->loc);
parse_extension_control_file(control, NULL); parse_extension_control_file(control, NULL);
memset(values, 0, sizeof(values)); memset(values, 0, sizeof(values));
@@ -2273,11 +2323,15 @@ pg_available_extensions(PG_FUNCTION_ARGS)
nulls[1] = true; nulls[1] = true;
else else
values[1] = CStringGetTextDatum(control->default_version); values[1] = CStringGetTextDatum(control->default_version);
/* location */
values[2] = CStringGetTextDatum(get_extension_location(location));
/* comment */ /* comment */
if (control->comment == NULL) if (control->comment == NULL)
nulls[2] = true; nulls[3] = true;
else else
values[2] = CStringGetTextDatum(control->comment); values[3] = CStringGetTextDatum(control->comment);
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
values, nulls); values, nulls);
@@ -2313,9 +2367,9 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
locations = get_extension_control_directories(); locations = get_extension_control_directories();
foreach_ptr(char, location, locations) foreach_ptr(ExtensionLocation, location, locations)
{ {
dir = AllocateDir(location); dir = AllocateDir(location->loc);
/* /*
* If the control directory doesn't exist, we want to silently return * If the control directory doesn't exist, we want to silently return
@@ -2327,7 +2381,7 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
} }
else else
{ {
while ((de = ReadDir(dir, location)) != NULL) while ((de = ReadDir(dir, location->loc)) != NULL)
{ {
ExtensionControlFile *control; ExtensionControlFile *control;
char *extname; char *extname;
@@ -2356,12 +2410,13 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
/* read the control file */ /* read the control file */
control = new_ExtensionControlFile(extname); control = new_ExtensionControlFile(extname);
control->control_dir = pstrdup(location); control->control_dir = pstrdup(location->loc);
parse_extension_control_file(control, NULL); parse_extension_control_file(control, NULL);
/* scan extension's script directory for install scripts */ /* scan extension's script directory for install scripts */
get_available_versions_for_extension(control, rsinfo->setResult, get_available_versions_for_extension(control, rsinfo->setResult,
rsinfo->setDesc); rsinfo->setDesc,
location);
} }
FreeDir(dir); FreeDir(dir);
@@ -2378,7 +2433,8 @@ pg_available_extension_versions(PG_FUNCTION_ARGS)
static void static void
get_available_versions_for_extension(ExtensionControlFile *pcontrol, get_available_versions_for_extension(ExtensionControlFile *pcontrol,
Tuplestorestate *tupstore, Tuplestorestate *tupstore,
TupleDesc tupdesc) TupleDesc tupdesc,
ExtensionLocation *location)
{ {
List *evi_list; List *evi_list;
ListCell *lc; ListCell *lc;
@@ -2391,8 +2447,8 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol,
{ {
ExtensionVersionInfo *evi = (ExtensionVersionInfo *) lfirst(lc); ExtensionVersionInfo *evi = (ExtensionVersionInfo *) lfirst(lc);
ExtensionControlFile *control; ExtensionControlFile *control;
Datum values[8]; Datum values[9];
bool nulls[8]; bool nulls[9];
ListCell *lc2; ListCell *lc2;
if (!evi->installable) if (!evi->installable)
@@ -2428,11 +2484,15 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol,
nulls[6] = true; nulls[6] = true;
else else
values[6] = convert_requires_to_datum(control->requires); values[6] = convert_requires_to_datum(control->requires);
/* location */
values[7] = CStringGetTextDatum(get_extension_location(location));
/* comment */ /* comment */
if (control->comment == NULL) if (control->comment == NULL)
nulls[7] = true; nulls[8] = true;
else else
values[7] = CStringGetTextDatum(control->comment); values[8] = CStringGetTextDatum(control->comment);
tuplestore_putvalues(tupstore, tupdesc, values, nulls); tuplestore_putvalues(tupstore, tupdesc, values, nulls);
@@ -2473,7 +2533,7 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol,
values[6] = convert_requires_to_datum(control->requires); values[6] = convert_requires_to_datum(control->requires);
nulls[6] = false; nulls[6] = false;
} }
/* comment stays the same */ /* comment and location stay the same */
tuplestore_putvalues(tupstore, tupdesc, values, nulls); tuplestore_putvalues(tupstore, tupdesc, values, nulls);
} }
@@ -3903,7 +3963,8 @@ find_in_paths(const char *basename, List *paths)
foreach(cell, paths) foreach(cell, paths)
{ {
char *path = lfirst(cell); ExtensionLocation *location = lfirst(cell);
char *path = location->loc;
char *full; char *full;
Assert(path != NULL); Assert(path != NULL);

View File

@@ -57,6 +57,6 @@
*/ */
/* yyyymmddN */ /* yyyymmddN */
#define CATALOG_VERSION_NO 202512301 #define CATALOG_VERSION_NO 202601011
#endif #endif

View File

@@ -10750,16 +10750,16 @@
{ oid => '3082', descr => 'list available extensions', { oid => '3082', descr => 'list available extensions',
proname => 'pg_available_extensions', procost => '10', prorows => '100', proname => 'pg_available_extensions', procost => '10', prorows => '100',
proretset => 't', provolatile => 's', prorettype => 'record', proretset => 't', provolatile => 's', prorettype => 'record',
proargtypes => '', proallargtypes => '{name,text,text}', proargtypes => '', proallargtypes => '{name,text,text,text}',
proargmodes => '{o,o,o}', proargnames => '{name,default_version,comment}', proargmodes => '{o,o,o,o}', proargnames => '{name,default_version,location,comment}',
prosrc => 'pg_available_extensions' }, prosrc => 'pg_available_extensions' },
{ oid => '3083', descr => 'list available extension versions', { oid => '3083', descr => 'list available extension versions',
proname => 'pg_available_extension_versions', procost => '10', proname => 'pg_available_extension_versions', procost => '10',
prorows => '100', proretset => 't', provolatile => 's', prorows => '100', proretset => 't', provolatile => 's',
prorettype => 'record', proargtypes => '', prorettype => 'record', proargtypes => '',
proallargtypes => '{name,text,bool,bool,bool,name,_name,text}', proallargtypes => '{name,text,bool,bool,bool,name,_name,text,text}',
proargmodes => '{o,o,o,o,o,o,o,o}', proargmodes => '{o,o,o,o,o,o,o,o,o}',
proargnames => '{name,version,superuser,trusted,relocatable,schema,requires,comment}', proargnames => '{name,version,superuser,trusted,relocatable,schema,requires,location,comment}',
prosrc => 'pg_available_extension_versions' }, prosrc => 'pg_available_extension_versions' },
{ oid => '3084', descr => 'list an extension\'s version update paths', { oid => '3084', descr => 'list an extension\'s version update paths',
proname => 'pg_extension_update_paths', procost => '10', prorows => '100', proname => 'pg_extension_update_paths', procost => '10', prorows => '100',

View File

@@ -25,6 +25,11 @@ my $ext_name2 = "test_custom_ext_paths_using_directory";
mkpath("$ext_dir/$ext_name2"); mkpath("$ext_dir/$ext_name2");
create_extension($ext_name2, $ext_dir, $ext_name2); create_extension($ext_name2, $ext_dir, $ext_name2);
# Make windows path use Unix slashes as canonicalize_path() is called when
# collecting extension control paths. See get_extension_control_directories().
my $ext_dir_canonicalized = $ext_dir;
$ext_dir_canonicalized =~ s!\\!/!g if $windows_os;
# Use the correct separator and escape \ when running on Windows. # Use the correct separator and escape \ when running on Windows.
my $sep = $windows_os ? ";" : ":"; my $sep = $windows_os ? ";" : ":";
$node->append_conf( $node->append_conf(
@@ -35,6 +40,10 @@ extension_control_path = '\$system$sep@{[ $windows_os ? ($ext_dir =~ s/\\/\\\\/g
# Start node # Start node
$node->start; $node->start;
# Create an user to test permissions to read extension locations.
my $user = "user01";
$node->safe_psql('postgres', "CREATE USER $user");
my $ecp = $node->safe_psql('postgres', 'show extension_control_path;'); my $ecp = $node->safe_psql('postgres', 'show extension_control_path;');
is($ecp, "\$system$sep$ext_dir$sep$ext_dir2", is($ecp, "\$system$sep$ext_dir$sep$ext_dir2",
@@ -46,48 +55,65 @@ $node->safe_psql('postgres', "CREATE EXTENSION $ext_name2");
my $ret = $node->safe_psql('postgres', my $ret = $node->safe_psql('postgres',
"select * from pg_available_extensions where name = '$ext_name'"); "select * from pg_available_extensions where name = '$ext_name'");
is( $ret, is( $ret,
"test_custom_ext_paths|1.0|1.0|Test extension_control_path", "test_custom_ext_paths|1.0|1.0|$ext_dir_canonicalized/extension|Test extension_control_path",
"extension is installed correctly on pg_available_extensions"); "extension is shown correctly in pg_available_extensions");
$ret = $node->safe_psql('postgres', $ret = $node->safe_psql('postgres',
"select * from pg_available_extension_versions where name = '$ext_name'"); "select * from pg_available_extension_versions where name = '$ext_name'");
is( $ret, is( $ret,
"test_custom_ext_paths|1.0|t|t|f|t|||Test extension_control_path", "test_custom_ext_paths|1.0|t|t|f|t|||$ext_dir_canonicalized/extension|Test extension_control_path",
"extension is installed correctly on pg_available_extension_versions"); "extension is shown correctly in pg_available_extension_versions");
$ret = $node->safe_psql('postgres', $ret = $node->safe_psql('postgres',
"select * from pg_available_extensions where name = '$ext_name2'"); "select * from pg_available_extensions where name = '$ext_name2'");
is( $ret, is( $ret,
"test_custom_ext_paths_using_directory|1.0|1.0|Test extension_control_path", "test_custom_ext_paths_using_directory|1.0|1.0|$ext_dir_canonicalized/extension|Test extension_control_path",
"extension is installed correctly on pg_available_extensions"); "extension is shown correctly in pg_available_extensions");
$ret = $node->safe_psql('postgres', $ret = $node->safe_psql('postgres',
"select * from pg_available_extension_versions where name = '$ext_name2'" "select * from pg_available_extension_versions where name = '$ext_name2'"
); );
is( $ret, is( $ret,
"test_custom_ext_paths_using_directory|1.0|t|t|f|t|||Test extension_control_path", "test_custom_ext_paths_using_directory|1.0|t|t|f|t|||$ext_dir_canonicalized/extension|Test extension_control_path",
"extension is installed correctly on pg_available_extension_versions"); "extension is shown correctly in pg_available_extension_versions");
# Ensure that extensions installed on $system is still visible when using with # Test that a non-superuser is not able to read the extension location in
# pg_available_extensions
$ret = $node->safe_psql('postgres',
"select location from pg_available_extensions where name = '$ext_name2'",
connstr => "user=$user");
is( $ret,
"<insufficient privilege>",
"extension location is hidden in pg_available_extensions for users with insufficient privilege");
# Test that a non-superuser is not able to read the extension location in
# pg_available_extension_versions
$ret = $node->safe_psql('postgres',
"select location from pg_available_extension_versions where name = '$ext_name2'",
connstr => "user=$user");
is( $ret,
"<insufficient privilege>",
"extension location is hidden in pg_available_extension_versions for users with insufficient privilege");
# Ensure that extensions installed in $system are still visible when used with
# custom extension control path. # custom extension control path.
$ret = $node->safe_psql('postgres', $ret = $node->safe_psql('postgres',
"select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'" "select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'"
); );
is($ret, "t", is($ret, "t",
"\$system extension is installed correctly on pg_available_extensions"); "\$system extension is shown correctly in pg_available_extensions");
$ret = $node->safe_psql('postgres', $ret = $node->safe_psql('postgres',
"set extension_control_path = ''; select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'" "set extension_control_path = ''; select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'"
); );
is($ret, "t", is($ret, "t",
"\$system extension is installed correctly on pg_available_extensions with empty extension_control_path" "\$system extension is shown correctly in pg_available_extensions with empty extension_control_path"
); );
# Test with an extension that does not exists # Test with an extension that does not exists
my ($code, $stdout, $stderr) = my ($code, $stdout, $stderr) =
$node->psql('postgres', "CREATE EXTENSION invalid"); $node->psql('postgres', "CREATE EXTENSION invalid");
is($code, 3, 'error to create an extension that does not exists'); is($code, 3, 'error creating an extension that does not exist');
like($stderr, qr/ERROR: extension "invalid" is not available/); like($stderr, qr/ERROR: extension "invalid" is not available/);
sub create_extension sub create_extension

View File

@@ -1310,14 +1310,16 @@ pg_available_extension_versions| SELECT e.name,
e.relocatable, e.relocatable,
e.schema, e.schema,
e.requires, e.requires,
e.location,
e.comment e.comment
FROM (pg_available_extension_versions() e(name, version, superuser, trusted, relocatable, schema, requires, comment) FROM (pg_available_extension_versions() e(name, version, superuser, trusted, relocatable, schema, requires, location, comment)
LEFT JOIN pg_extension x ON (((e.name = x.extname) AND (e.version = x.extversion)))); LEFT JOIN pg_extension x ON (((e.name = x.extname) AND (e.version = x.extversion))));
pg_available_extensions| SELECT e.name, pg_available_extensions| SELECT e.name,
e.default_version, e.default_version,
x.extversion AS installed_version, x.extversion AS installed_version,
e.location,
e.comment e.comment
FROM (pg_available_extensions() e(name, default_version, comment) FROM (pg_available_extensions() e(name, default_version, location, comment)
LEFT JOIN pg_extension x ON ((e.name = x.extname))); LEFT JOIN pg_extension x ON ((e.name = x.extname)));
pg_backend_memory_contexts| SELECT name, pg_backend_memory_contexts| SELECT name,
ident, ident,

View File

@@ -800,6 +800,7 @@ ExtensibleNodeEntry
ExtensibleNodeMethods ExtensibleNodeMethods
ExtensionControlFile ExtensionControlFile
ExtensionInfo ExtensionInfo
ExtensionLocation
ExtensionVersionInfo ExtensionVersionInfo
FDWCollateState FDWCollateState
FD_SET FD_SET
@@ -1583,7 +1584,6 @@ LoadStmt
LocalBufferLookupEnt LocalBufferLookupEnt
LocalPgBackendStatus LocalPgBackendStatus
LocalTransactionId LocalTransactionId
Location
LocationIndex LocationIndex
LocationLen LocationLen
LockAcquireResult LockAcquireResult