pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feedExtension security improvement: Add support for extensions with an owned schema
43+ messages / 14 participants
[nested] [flat]
* Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-02 00:08 Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 3 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2024-06-02 00:08 UTC (permalink / raw)
To: pgsql-hackers
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
One way in which certain extensions currently hack around the
non-existence of this feature is by using the approach that pg_cron
uses: Setting the schema to pg_catalog and running "CREATE SCHEMA
pg_cron" from within the extension script. While this works, it's
obviously a hack, and a big downside of it is that it doesn't allow
users to choose the schema name used by the extension.
PS. I have never added fields to pg_catalag tables before, so there's
a clear TODO in the pg_upgrade code related to that. If anyone has
some pointers for me to look at to address that one that would be
helpful, if not I'll probably figure it out myself. All other code is
in pretty finished state, although I'm considering if
AlterExtensionNamespace should maybe be split a bit somehow, because
owned_schema skips most of the code in that function.
Attachments:
v1-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 21.1K) download
From cca2fc5a820822a29d9bc3b810a811adb1d081e1 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Fri, 31 May 2024 02:04:31 -0700
Subject: [PATCH v1] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 13 ++
doc/src/sgml/ref/create_extension.sgml | 2 +-
src/backend/commands/extension.c | 139 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/commands/extension.h | 4 +-
.../expected/test_extensions.out | 50 +++++++
src/test/modules/test_extensions/meson.build | 4 +
.../test_extensions/sql/test_extensions.sql | 27 ++++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
13 files changed, 214 insertions(+), 40 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 218940ee5ce..36dc692abef 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -809,6 +809,19 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension is <firstterm>owned_schema</firstterm> if it requires a
+ new dedicated schema for its objects. Such a requirement can make
+ security concerns related to <literal>search_path</literal> injection
+ much easier to reason about. The default is <literal>false</literal>,
+ i.e., the extension can be installed into an existing schema.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index ca2b80d669c..240568f3166 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -102,7 +102,7 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. The named schema must already exist if the extension's control file does not specify <literal>owned_schema</owned_schema>.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 1643c8c69a0..a859ec2e043 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -83,6 +83,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -561,6 +563,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1547,8 +1557,11 @@ CreateExtensionInternal(char *extensionName,
*/
if (schemaName)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
+ /*
+ * If the user is giving us the schema name, it must exist already if
+ * the extension does not want to own the schema
+ */
+ schemaOid = get_namespace_oid(schemaName, control->owned_schema);
}
if (control->schema != NULL)
@@ -1570,7 +1583,10 @@ CreateExtensionInternal(char *extensionName,
/* Always use the schema from control file for current extension. */
schemaName = control->schema;
+ }
+ if (schemaName)
+ {
/* Find or create the schema in case it does not exist. */
schemaOid = get_namespace_oid(schemaName, true);
@@ -1591,8 +1607,22 @@ CreateExtensionInternal(char *extensionName,
*/
schemaOid = get_namespace_oid(schemaName, false);
}
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName)));
+ }
+
}
- else if (!OidIsValid(schemaOid))
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+ else
{
/*
* Neither user nor author of the extension specified schema; use the
@@ -1659,6 +1689,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -1671,6 +1702,16 @@ CreateExtensionInternal(char *extensionName,
if (control->comment != NULL)
CreateComments(extensionOid, ExtensionRelationId, 0, control->comment);
+ if (control->owned_schema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &address, DEPENDENCY_EXTENSION);
+ }
+
/*
* Execute the installation script file
*/
@@ -1864,7 +1905,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -1894,6 +1936,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2788,8 +2831,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -2798,22 +2839,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -2837,14 +2862,36 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
- /*
- * If the extension is already in the target schema, just silently do
- * nothing.
- */
- if (extForm->extnamespace == nspOid)
+ if (!extForm->extownedschema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -2917,6 +2964,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually have to move any objects anything for owned
+ * schemas, because we simply rename the schema.
+ */
+ if (extForm->extownedschema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -2960,18 +3014,27 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ if (extForm->extownedschema)
+ {
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ /* Now adjust pg_extension.extnamespace */
+ extForm->extnamespace = nspOid;
- table_close(extRel, RowExclusiveLock);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ table_close(extRel, RowExclusiveLock);
+
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index c54b08fe180..ccd91d4c818 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -238,6 +238,7 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
GetUserId(),
get_namespace_oid(text_to_cstring(schemaName), false),
relocatable,
+ false, /* TODO: Maybe change this??? */
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index cdfacc09303..ab20fff88ea 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index c6f3f867eb7..8e7fa574032 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -36,7 +36,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index f357cc21aaa..c0a2b7b315e 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -626,3 +626,53 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index c5f3424da51..52e8841480b 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -42,6 +42,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index 642c82ff5d3..136967db395 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -299,3 +299,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
base-commit: 8fea1bd5411b793697a4c9087c403887e050c4ac
--
2.34.1
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-05 14:09 Marco Slot <marco.slot@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
2 siblings, 0 replies; 43+ messages in thread
From: Marco Slot @ 2024-06-05 14:09 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: pgsql-hackers
On Sun, Jun 2, 2024, 02:08 Jelte Fennema-Nio <me@jeltef.nl> wrote:
> This patch adds a new "owned_schema" option to the extension control
> file that can be set to true to indicate that this extension wants to
> own the schema in which it is installed.
Huge +1
Many managed PostgreSQL services block superuser access, but provide a
way for users to trigger a create/alter extension as superuser. There
have been various extensions whose SQL scripts can be tricked into
calling a function that was pre-created in the extension schema. This
is usually done by finding an unqualified call to a pg_catalog
function/operator, and overloading it with one whose arguments types
are a closer match for the provided values, which then takes
precedence regardless of search_path order. The custom function can
then do something like "alter user foo superuser".
The sequence of steps assumes the user already has some kind of admin
role and is gaining superuser access to their own database server.
However, the superuser implicitly has shell access, so it gives
attackers an additional set of tools to poke around in the managed
service. For instance, they can change the way the machine responds to
control plane requests, which can sometimes trigger further
escalations. In addition, many applications use the relatively
privileged default user, which means SQL injection issues can also
escalate into superuser access and beyond.
There are some static analysis tools like
https://github.com/timescale/pgspot that address this issue, though it
seems like a totally unnecessary hole. Using schema = pg_catalog,
relocatable = false, and doing an explicit create schema (without "if
not exists") plugs the hole by effectively disabling extension
schemas. For extensions I'm involved in, I consider this to be a hard
requirement.
I think Jelte's solution is preferable going forward, because it
preserves the flexibility that extension schemas were meant to
provide, and makes the potential hazards of reusing a schema more
explicit.
cheers,
Marco
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-05 17:53 Jeff Davis <pgsql@j-davis.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
2 siblings, 1 reply; 43+ messages in thread
From: Jeff Davis @ 2024-06-05 17:53 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; pgsql-hackers
On Sat, 2024-06-01 at 17:08 -0700, Jelte Fennema-Nio wrote:
> This patch adds a new "owned_schema" option to the extension control
> file that can be set to true to indicate that this extension wants to
> own the schema in which it is installed. What that means is that the
> schema should not exist before creating the extension, and will be
> created during extension creation. This thus gives the extension
> author
> an easy way to use a safe search_path, while still allowing all
> objects
> to be grouped together in a schema. The implementation also has the
> pleasant side effect that the schema will be automatically dropped
> when
> the extension is dropped.
Is this orthogonal to relocatability?
When you say "an easy way to use a safe search_path": the CREATE
EXTENSION command already sets the search_path, so your patch just
ensures that it's empty (and therefore safe) first, right?
Should we go further and try to prevent creating objects in an
extension-owned schema with normal SQL?
Approximately how many extensions should be adjusted to use
owned_schema=true? What are the reasons an extension would not want to
own the schema in which the objects are created? I assume some would
still create objects in pg_catalog, but ideally we'd come up with a
better solution to that as well.
This protects the extension script, but I'm left wondering if we could
do something here to make it easier to protect extension functions
called from outside the extension script, also. It would be nice if we
could implicitly tack on a "SET search_path TO @extschema@, pg_catalog,
pg_temp" to each function in the extension. I'm not proposing that, but
perhaps a related idea might work. Probably outside the scope of your
proposal.
Regards,
Jeff Davis
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-05 20:30 Jelte Fennema-Nio <me@jeltef.nl>
parent: Jeff Davis <pgsql@j-davis.com>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2024-06-05 20:30 UTC (permalink / raw)
To: Jeff Davis <pgsql@j-davis.com>; +Cc: pgsql-hackers
On Wed, 5 Jun 2024 at 19:53, Jeff Davis <pgsql@j-davis.com> wrote:
> Is this orthogonal to relocatability?
It's fairly orthogonal, but it has some impact on relocatability: You
can only relocate to a schema name that does not exist yet (while
currently you can only relocate to a schema that already exists). This
is because, for owned_schema=true, relocation is not actually changing
the schema of the extension objects, it only renames the existing
schema to the new name.
> When you say "an easy way to use a safe search_path": the CREATE
> EXTENSION command already sets the search_path, so your patch just
> ensures that it's empty (and therefore safe) first, right?
Correct: **safe** is the key word in that sentence. Without
owned_schema, you get an **unsafe** search_path by default unless you
go out of your way to set "schema=pg_catalog" in the control file.
> Should we go further and try to prevent creating objects in an
> extension-owned schema with normal SQL?
That would be nice for sure, but security wise it doesn't matter
afaict. Only the creator of the extension would be able to add stuff
in the extension-owned schema anyway, so there's no privilege
escalation concern there.
> Approximately how many extensions should be adjusted to use
> owned_schema=true?
Adjusting existing extensions would be hard at the moment, because the
current patch does not introduce a migration path. But basically I
think for most new extension installs (even of existing extensions) it
would be fine if owned_schema=true would be the default. I didn't
propose (yet) to make it the default though, to avoid discussing the
tradeoff of security vs breaking installation for an unknown amount of
existing extensions.
I think having a generic migration path would be hard, due to the many
ways in which extensions can now be installed. But I think we might be
able to add one fairly easily for relocatable extensions: e.g. "ALTER
EXTESION SET SCHEMA new_schema OWNED_SCHEMA", which would essentially
do CREATE SCHEMA new_schema + move all objects from old_schema to
new_schema. And even for non-relocatable one you could do something
like:
CREATE SCHEMA temp_schema_{random_id};
-- move all objects from ext_schema to temp_schema_{random_id};
DROP SCHEMA ext_schema; -- if this fails, ext_schema was not empty
ALTER SCHEMA temp_schema_{random_id} RENAME TO ext_schema;
> What are the reasons an extension would not want to
> own the schema in which the objects are created? I assume some would
> still create objects in pg_catalog, but ideally we'd come up with a
> better solution to that as well.
Some extensions depend on putting stuff into the public schema. But
yeah it would be best if they didn't.
> This protects the extension script, but I'm left wondering if we could
> do something here to make it easier to protect extension functions
> called from outside the extension script, also. It would be nice if we
> could implicitly tack on a "SET search_path TO @extschema@, pg_catalog,
> pg_temp" to each function in the extension. I'm not proposing that, but
> perhaps a related idea might work. Probably outside the scope of your
> proposal.
Yeah, this proposal definitely doesn't solve all security problems
with extensions. And indeed what you're proposing would solve another
major issue, another option would be to default to the "safe"
search_path that you proposed a while back. But yeah I agree that it's
outside of the scope of this proposal. I feel like if we try to solve
every security problem at once, probably nothing gets solved instead.
That's why I tried to keep this proposal very targeted, i.e. have this
be step 1 of an N step plan to make extensions more secure by default.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-19 15:19 Jelte Fennema-Nio <me@jeltef.nl>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2024-06-19 15:19 UTC (permalink / raw)
To: Jeff Davis <pgsql@j-davis.com>; +Cc: pgsql-hackers
Attached is an updated version of this patch that fixes a few issues
that CI reported (autoconf, compiler warnings and broken docs).
I also think I changed the pg_upgrade to do the correct thing, but I'm
not sure how to test this (even manually). Because part of it would
only be relevant once we support upgrading from PG18. So for now the
upgrade_code I haven't actually run.
Attachments:
v2-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 27.5K) download
From 37b6fa45bf877bcc15ce76d7e342199b7ca76d50 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Fri, 31 May 2024 02:04:31 -0700
Subject: [PATCH v2] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 13 ++
doc/src/sgml/ref/create_extension.sgml | 3 +-
src/backend/commands/extension.c | 141 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 20 ++-
src/bin/pg_dump/pg_dump.c | 15 +-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 7 +-
.../expected/test_extensions.out | 50 +++++++
src/test/modules/test_extensions/meson.build | 4 +
.../test_extensions/sql/test_extensions.sql | 27 ++++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
17 files changed, 248 insertions(+), 53 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 218940ee5ce..36dc692abef 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -809,6 +809,19 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension is <firstterm>owned_schema</firstterm> if it requires a
+ new dedicated schema for its objects. Such a requirement can make
+ security concerns related to <literal>search_path</literal> injection
+ much easier to reason about. The default is <literal>false</literal>,
+ i.e., the extension can be installed into an existing schema.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index ca2b80d669c..6e767c7bfca 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -102,7 +102,8 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. The named schema must already exist if the extension's
+ control file does not specify <literal>owned_schema</literal>.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 1643c8c69a0..c9586ad62a1 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -83,6 +83,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -561,6 +563,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1547,8 +1557,11 @@ CreateExtensionInternal(char *extensionName,
*/
if (schemaName)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
+ /*
+ * If the user is giving us the schema name, it must exist already if
+ * the extension does not want to own the schema
+ */
+ schemaOid = get_namespace_oid(schemaName, control->owned_schema);
}
if (control->schema != NULL)
@@ -1570,7 +1583,10 @@ CreateExtensionInternal(char *extensionName,
/* Always use the schema from control file for current extension. */
schemaName = control->schema;
+ }
+ if (schemaName)
+ {
/* Find or create the schema in case it does not exist. */
schemaOid = get_namespace_oid(schemaName, true);
@@ -1591,8 +1607,22 @@ CreateExtensionInternal(char *extensionName,
*/
schemaOid = get_namespace_oid(schemaName, false);
}
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName)));
+ }
+
}
- else if (!OidIsValid(schemaOid))
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+ else
{
/*
* Neither user nor author of the extension specified schema; use the
@@ -1659,6 +1689,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -1671,6 +1702,16 @@ CreateExtensionInternal(char *extensionName,
if (control->comment != NULL)
CreateComments(extensionOid, ExtensionRelationId, 0, control->comment);
+ if (control->owned_schema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &address, DEPENDENCY_EXTENSION);
+ }
+
/*
* Execute the installation script file
*/
@@ -1864,7 +1905,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -1894,6 +1936,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2785,11 +2828,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -2798,22 +2840,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -2837,14 +2863,38 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
- /*
- * If the extension is already in the target schema, just silently do
- * nothing.
- */
- if (extForm->extnamespace == nspOid)
+ ownedSchema = extForm->extownedschema;
+
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -2917,6 +2967,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually have to move any objects anything for owned
+ * schemas, because we simply rename the schema.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -2960,18 +3017,26 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ if (ownedSchema)
+ {
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /* Now adjust pg_extension.extnamespace */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index c54b08fe180..41037d6ec98 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -187,6 +187,7 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
text *extName;
text *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
@@ -198,28 +199,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
schemaName = PG_GETARG_TEXT_PP(1);
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -238,6 +241,7 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
GetUserId(),
get_namespace_oid(text_to_cstring(schemaName), false),
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e3240708284..c9ef0b68f16 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -5702,6 +5702,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -5709,8 +5710,15 @@ getExtensions(Archive *fout, int *numExtensions)
query = createPQExpBuffer();
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
- "x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ "x.extname, n.nspname, x.extrelocatable, x.extownedschema, x.extversion, x.extconfig, x.extcondition "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, "false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -5724,6 +5732,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -5737,6 +5746,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -10719,6 +10729,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 865823868f1..6c6ea6a0191 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -181,6 +181,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index cdfacc09303..ab20fff88ea 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6a5476d3c4c..b8314cc4288 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11420,7 +11420,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index c6f3f867eb7..8e7fa574032 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -36,7 +36,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index 05272e6a40b..28f20290190 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -23,7 +24,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index f357cc21aaa..c0a2b7b315e 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -626,3 +626,53 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index c5f3424da51..52e8841480b 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -42,6 +42,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index 642c82ff5d3..136967db395 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -299,3 +299,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
base-commit: 03ec203164119f11f0eab4c83c97a8527e2b108d
--
2.34.1
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-19 15:22 David G. Johnston <david.g.johnston@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: David G. Johnston @ 2024-06-19 15:22 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Wed, Jun 19, 2024 at 8:19 AM Jelte Fennema-Nio <me@jeltef.nl> wrote:
> Because part of it would
> only be relevant once we support upgrading from PG18. So for now the
> upgrade_code I haven't actually run.
>
Does it apply against v16? If so, branch off there, apply it, then upgrade
from the v16 branch to master.
David J.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-19 15:28 Robert Haas <robertmhaas@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
2 siblings, 2 replies; 43+ messages in thread
From: Robert Haas @ 2024-06-19 15:28 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: pgsql-hackers
On Sat, Jun 1, 2024 at 8:08 PM Jelte Fennema-Nio <me@jeltef.nl> wrote:
> Writing the sql migration scripts that are run by CREATE EXTENSION and
> ALTER EXTENSION UPDATE are security minefields for extension authors.
> One big reason for this is that search_path is set to the schema of the
> extension while running these scripts, and thus if a user with lower
> privileges can create functions or operators in that schema they can do
> all kinds of search_path confusion attacks if not every function and
> operator that is used in the script is schema qualified. While doing
> such schema qualification is possible, it relies on the author to never
> make a mistake in any of the sql files. And sadly humans have a tendency
> to make mistakes.
I agree that this is a problem. I also think that the patch might be a
reasonable solution (but I haven't reviewed it).
But I wonder if there might also be another possible approach: could
we, somehow, prevent object references in extension scripts from
resolving to anything other than the system catalogs and the contents
of that extension? Perhaps with a control file setting to specify a
list of trusted extensions which we're also allowed to reference?
I have a feeling that this might be pretty annoying to implement, and
if that is true, then never mind. But if it isn't that annoying to
implement, it would make a lot of unsafe extensions safe by default,
without the extension author needing to take any action. Which could
be pretty cool. It would also make it possible for extensions to
safely share a schema, if desired.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-19 17:50 Jelte Fennema-Nio <me@jeltef.nl>
parent: Robert Haas <robertmhaas@gmail.com>
1 sibling, 3 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2024-06-19 17:50 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: pgsql-hackers
On Wed, 19 Jun 2024 at 17:28, Robert Haas <robertmhaas@gmail.com> wrote:
> But I wonder if there might also be another possible approach: could
> we, somehow, prevent object references in extension scripts from
> resolving to anything other than the system catalogs and the contents
> of that extension?
This indeed does sound like the behaviour that pretty much every
existing extension wants to have. One small addition/clarification
that I would make to your definition: fully qualified references to
other objects should still be allowed.
I do think, even if we have this, there would be other good reasons to
use "owned schemas" for extension authors. At least the following two:
1. To have a safe search_path that can be used in SET search_path on a
function (see also [1]).
2. To make it easy for extension authors to avoid conflicts with other
extensions/UDFs.
> Perhaps with a control file setting to specify a
> list of trusted extensions which we're also allowed to reference?
I think we could simply use the already existing "requires" field from
the control file. i.e. you're allowed to reference only your own
extension
> I have a feeling that this might be pretty annoying to implement, and
> if that is true, then never mind.
Based on a quick look it's not trivial, but also not super bad.
Basically it seems like in src/backend/catalog/namespace.c, every time
we loop over activeSearchPath and CurrentExtensionObject is set, then
we should skip any item that's not stored in pg_catalog, unless
there's a DEPENDENCY_EXTENSION pg_depend entry for the item (and that
pg_depend entry references the extension or the requires list).
There's quite a few loops over activeSearchPath in namespace.c, but
they all seem pretty similar. So while a bunch of code would need to
be changed, the changes could probably be well encapsulated in a
function.
[1]: https://www.postgresql.org/message-id/flat/00d8f046156e355ec0eb49585408bafc8012e4a5.camel%40j-davis....
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-19 17:55 Tom Lane <tgl@sss.pgh.pa.us>
parent: Jelte Fennema-Nio <me@jeltef.nl>
2 siblings, 1 reply; 43+ messages in thread
From: Tom Lane @ 2024-06-19 17:55 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Robert Haas <robertmhaas@gmail.com>; pgsql-hackers
Jelte Fennema-Nio <me@jeltef.nl> writes:
> On Wed, 19 Jun 2024 at 17:28, Robert Haas <robertmhaas@gmail.com> wrote:
>> I have a feeling that this might be pretty annoying to implement, and
>> if that is true, then never mind.
> Based on a quick look it's not trivial, but also not super bad.
> Basically it seems like in src/backend/catalog/namespace.c, every time
> we loop over activeSearchPath and CurrentExtensionObject is set, then
> we should skip any item that's not stored in pg_catalog, unless
> there's a DEPENDENCY_EXTENSION pg_depend entry for the item (and that
> pg_depend entry references the extension or the requires list).
We could change the lookup rules that apply during execution of
an extension script, but we already restrict search_path at that
time so I'm not sure how much further this'd move the goalposts.
The *real* problem IMO is that if you create a PL function or
(old-style) SQL function within an extension, execution of that
function is not similarly protected.
regards, tom lane
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-19 19:06 Jelte Fennema-Nio <me@jeltef.nl>
parent: Tom Lane <tgl@sss.pgh.pa.us>
0 siblings, 0 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2024-06-19 19:06 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Robert Haas <robertmhaas@gmail.com>; pgsql-hackers
On Wed, 19 Jun 2024 at 19:55, Tom Lane <tgl@sss.pgh.pa.us> wrote:
>
> Jelte Fennema-Nio <me@jeltef.nl> writes:
> > On Wed, 19 Jun 2024 at 17:28, Robert Haas <robertmhaas@gmail.com> wrote:
> >> I have a feeling that this might be pretty annoying to implement, and
> >> if that is true, then never mind.
>
> > Based on a quick look it's not trivial, but also not super bad.
> > Basically it seems like in src/backend/catalog/namespace.c, every time
> > we loop over activeSearchPath and CurrentExtensionObject is set, then
> > we should skip any item that's not stored in pg_catalog, unless
> > there's a DEPENDENCY_EXTENSION pg_depend entry for the item (and that
> > pg_depend entry references the extension or the requires list).
>
> We could change the lookup rules that apply during execution of
> an extension script, but we already restrict search_path at that
> time so I'm not sure how much further this'd move the goalposts.
The point I tried to make in my first email is that this restricted
search_path you mention, is not very helpful at preventing privilege
escalations. Since it's often possible for a non-superuser to create
functions in one of the schemas in this search_path, e.g. by having
the non-superuser first create the schema & create some functions in
it, and then asking the DBA/control plane to create the extension in
that schema.
My patch tries to address that problem by creating the schema of the
extension during extension creation, and failing if it already exists.
Thus implicitly ensuring that a non-superuser cannot mess with the
schema.
The proposal from Robert tries to instead address by changing the
lookup rules during execution of an extension script to be more strict
than they would be outside of it (i.e. even if a function/operator
matches search_path it might still not be picked).
> The *real* problem IMO is that if you create a PL function or
> (old-style) SQL function within an extension, execution of that
> function is not similarly protected.
That's definitely a big problem too, and that's the problem that [1]
tries to fix. But first the lookup in extension scripts would need to
be made secure, because it doesn't seem very useful (security wise) to
use the same lookup mechanism in functions as we do in extension
scripts, if the lookup in extension scripts is not secure in the first
place. I think the method of making the lookup secure in my patch
would transfer over well, because it adds a way for a safe search_path
to exist, so all that's needed is for the PL function to use that
search_path. Robbert's proposal would be more difficult I think. When
executing a PL function from an extension we'd need to use the same
changed lookup rules that we'd use during the extension script of that
extension. I think that should be possible, but it's definitely more
involved.
[1]: https://www.postgresql.org/message-id/flat/CAE9k0P%253DFcZ%253Dao3ZpEq29BveF%252B%253D27KBcRT2HFowJx...
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-20 11:18 Jelte Fennema-Nio <me@jeltef.nl>
parent: David G. Johnston <david.g.johnston@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2024-06-20 11:18 UTC (permalink / raw)
To: David G. Johnston <david.g.johnston@gmail.com>; +Cc: Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Wed, 19 Jun 2024 at 17:22, David G. Johnston
<david.g.johnston@gmail.com> wrote:
>
> On Wed, Jun 19, 2024 at 8:19 AM Jelte Fennema-Nio <me@jeltef.nl> wrote:
>
>>
>> Because part of it would
>> only be relevant once we support upgrading from PG18. So for now the
>> upgrade_code I haven't actually run.
>
>
> Does it apply against v16? If so, branch off there, apply it, then upgrade from the v16 branch to master.
I realized it's possible to do an "upgrade" with pg_upgrade from v17
to v17. So I was able to test both the pre and post PG18 upgrade logic
manually by changing the version in this line:
if (fout->remoteVersion >= 180000)
As expected the new pg_upgrade code was severely broken. Attached is a
new patch where the pg_upgrade code now actually works.
Attachments:
v3-0001-Add-support-for-extensions-with-an-owned-schema.patch (application/octet-stream, 30.8K) download
From 2f0118b5e30971cbf305ba951ebe67c067852a97 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <jelte.fennema@microsoft.com>
Date: Fri, 31 May 2024 02:04:31 -0700
Subject: [PATCH v3] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 13 ++
doc/src/sgml/ref/create_extension.sgml | 3 +-
src/backend/commands/extension.c | 142 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 ++++--
src/bin/pg_dump/pg_dump.c | 52 ++++++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 7 +-
.../expected/test_extensions.out | 50 ++++++
src/test/modules/test_extensions/meson.build | 4 +
.../test_extensions/sql/test_extensions.sql | 27 ++++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
17 files changed, 302 insertions(+), 62 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 218940ee5ce..36dc692abef 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -809,6 +809,19 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension is <firstterm>owned_schema</firstterm> if it requires a
+ new dedicated schema for its objects. Such a requirement can make
+ security concerns related to <literal>search_path</literal> injection
+ much easier to reason about. The default is <literal>false</literal>,
+ i.e., the extension can be installed into an existing schema.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index ca2b80d669c..6e767c7bfca 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -102,7 +102,8 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. The named schema must already exist if the extension's
+ control file does not specify <literal>owned_schema</literal>.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 1643c8c69a0..e964488128c 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -83,6 +83,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -561,6 +563,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1547,8 +1557,11 @@ CreateExtensionInternal(char *extensionName,
*/
if (schemaName)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
+ /*
+ * If the user is giving us the schema name, it must exist already if
+ * the extension does not want to own the schema
+ */
+ schemaOid = get_namespace_oid(schemaName, control->owned_schema);
}
if (control->schema != NULL)
@@ -1570,7 +1583,10 @@ CreateExtensionInternal(char *extensionName,
/* Always use the schema from control file for current extension. */
schemaName = control->schema;
+ }
+ if (schemaName)
+ {
/* Find or create the schema in case it does not exist. */
schemaOid = get_namespace_oid(schemaName, true);
@@ -1591,8 +1607,22 @@ CreateExtensionInternal(char *extensionName,
*/
schemaOid = get_namespace_oid(schemaName, false);
}
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName)));
+ }
+
}
- else if (!OidIsValid(schemaOid))
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+ else
{
/*
* Neither user nor author of the extension specified schema; use the
@@ -1659,6 +1689,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -1864,7 +1895,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -1894,6 +1926,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -1938,6 +1971,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -2785,11 +2829,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -2798,22 +2841,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -2837,14 +2864,38 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
- /*
- * If the extension is already in the target schema, just silently do
- * nothing.
- */
- if (extForm->extnamespace == nspOid)
+ ownedSchema = extForm->extownedschema;
+
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -2917,6 +2968,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually have to move any objects anything for owned
+ * schemas, because we simply rename the schema.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -2960,18 +3018,26 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ if (ownedSchema)
+ {
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /* Now adjust pg_extension.extnamespace */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index c54b08fe180..05205cfb7f4 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/origin.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e3240708284..3fe248fff42 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1707,6 +1707,18 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schem" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE && ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5512,7 +5524,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5526,19 +5538,32 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE && ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -5702,6 +5727,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -5710,7 +5736,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -5724,6 +5757,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -5737,6 +5771,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -10605,9 +10640,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -10719,6 +10754,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 865823868f1..6c6ea6a0191 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -181,6 +181,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index cdfacc09303..ab20fff88ea 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6a5476d3c4c..b8314cc4288 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11420,7 +11420,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index c6f3f867eb7..8e7fa574032 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -36,7 +36,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index 05272e6a40b..28f20290190 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -23,7 +24,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index f357cc21aaa..c0a2b7b315e 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -626,3 +626,53 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index c5f3424da51..52e8841480b 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -42,6 +42,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index 642c82ff5d3..136967db395 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -299,3 +299,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
base-commit: ab4346ebbfef44db857321d74bc0c31e03a72514
--
2.34.1
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-20 15:51 Robert Haas <robertmhaas@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
2 siblings, 0 replies; 43+ messages in thread
From: Robert Haas @ 2024-06-20 15:51 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: pgsql-hackers
On Wed, Jun 19, 2024 at 1:50 PM Jelte Fennema-Nio <me@jeltef.nl> wrote:
> I do think, even if we have this, there would be other good reasons to
> use "owned schemas" for extension authors. At least the following two:
> 1. To have a safe search_path that can be used in SET search_path on a
> function (see also [1]).
> 2. To make it easy for extension authors to avoid conflicts with other
> extensions/UDFs.
(1) is a very good point. (2) I don't know about one way or the other.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-20 17:00 David E. Wheeler <david@justatheory.com>
parent: Robert Haas <robertmhaas@gmail.com>
1 sibling, 0 replies; 43+ messages in thread
From: David E. Wheeler @ 2024-06-20 17:00 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Jelte Fennema-Nio <me@jeltef.nl>; pgsql-hackers
On Jun 19, 2024, at 11:28, Robert Haas <robertmhaas@gmail.com> wrote:
> But I wonder if there might also be another possible approach: could
> we, somehow, prevent object references in extension scripts from
> resolving to anything other than the system catalogs and the contents
> of that extension? Perhaps with a control file setting to specify a
> list of trusted extensions which we're also allowed to reference?
It would also have to allow access to other extensions it depends upon.
D
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-06-20 17:02 David E. Wheeler <david@justatheory.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
2 siblings, 0 replies; 43+ messages in thread
From: David E. Wheeler @ 2024-06-20 17:02 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Robert Haas <robertmhaas@gmail.com>; pgsql-hackers
On Jun 19, 2024, at 13:50, Jelte Fennema-Nio <me@jeltef.nl> wrote:
> This indeed does sound like the behaviour that pretty much every
> existing extension wants to have. One small addition/clarification
> that I would make to your definition: fully qualified references to
> other objects should still be allowed.
Would be tricky for referring to objects from other extensions with no defined schema, or are relatable.
> 1. To have a safe search_path that can be used in SET search_path on a
> function (see also [1]).
> 2. To make it easy for extension authors to avoid conflicts with other
> extensions/UDFs.
These would indeed be nice improvements IMO.
Best,
David
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-09-27 12:00 Tomas Vondra <tomas@vondra.me>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: Tomas Vondra @ 2024-09-27 12:00 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; David G. Johnston <david.g.johnston@gmail.com>; +Cc: Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
Hi,
I've spent a bit of time looking at this patch. It seems there's a clear
consensus that having "owned schemas" for extensions would be good for
security. To me it also seems as a convenient way to organize stuff. It
was possible to create extensions in a separate schema before, ofc, but
that's up to the DBA. With this the extension author to enforce that.
One thing that's not quite clear to me is what's the correct way for
existing extensions to switch to an "owned schema". Let's say you have
an extension. How do you transition to this? Can you just add it to the
control file and then some magic happens?
A couple minor comments:
1) doc/src/sgml/extend.sgml
An extension is <firstterm>owned_schema</firstterm> if it requires a
new dedicated schema for its objects. Such a requirement can make
security concerns related to <literal>search_path</literal> injection
much easier to reason about. The default is <literal>false</literal>,
i.e., the extension can be installed into an existing schema.
Doesn't "extension is owned_schema" sound a bit weird? I'd probably say
"extension may own a schema" or something like that.
Also, "requires a new dedicated schema" is a bit ambiguous. It's not
clear if it means the schema is expected to exist, or if it creates the
schema itself.
And perhaps it should clarify what "much easier to reason about" means.
That's pretty vague, and as a random extension author I wouldn't know
about the risks to consider. Maybe there's a section about this stuff
that we could reference?
2) doc/src/sgml/ref/create_extension.sgml
relocated. The named schema must already exist if the extension's
control file does not specify <literal>owned_schema</literal>.
Seems a bit unclear, I'd say having "owned_schema = false" in the
control file still qualifies as "specifies owned_schema". So might be
better to say it needs to be set to true?
Also, perhaps "dedicated_schema" would be better than "owned_schema"? I
mean, the point is not that it's "owned" by the extension, but that
there's nothing else in it. But that's nitpicking.
3) src/backend/commands/extension.c
I'm not sure why AlterExtensionNamespace moves the privilege check. Why
should it not check the privilege for owned schema too?
4) src/bin/pg_dump/pg_dump.c
checkExtensionMembership has typo "owned_schem".
Shouldn't binary_upgrade_extension_member still set ext=NULL in the for
loop, the way the original code does that?
The long if conditions might need some indentation, I guess. pgindent
leaves them like this, but 100 columns seems a bit too much. I'd do a
line break after each condition, I guess.
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2024-10-04 21:05 Jelte Fennema-Nio <me@jeltef.nl>
parent: Tomas Vondra <tomas@vondra.me>
0 siblings, 2 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2024-10-04 21:05 UTC (permalink / raw)
To: Tomas Vondra <tomas@vondra.me>; +Cc: David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Fri, 27 Sept 2024 at 14:00, Tomas Vondra <tomas@vondra.me> wrote:
> One thing that's not quite clear to me is what's the correct way for
> existing extensions to switch to an "owned schema". Let's say you have
> an extension. How do you transition to this? Can you just add it to the
> control file and then some magic happens?
Sadly no. As currently implemented this feature can only really be
used by new extensions. There is no upgrade path to it for existing
extensions. This is fairly common btw, the only field in the control
file that ever gets used after the taken from the control file for
ALTER EXTENSION is the version field. e.g. if you change the schema
field in the control file of an already installed extension, the
extension will not be moved to the new schema unless you DROP + CREATE
EXTENSION.
After this message I tried messing around a bit with changing an
existing extension to become an owned schema (either with a new
command or as part of ALTER EXTENSION UPDATE). But it's not as trivial
as I hoped for a few reasons:
1. There are multiple states that extensions are currently in all of
which need somewhat different conversion strategies. Specifically:
relocatable/non-relocatable &
schema=pg_catalog/public/actual_extension_schema.
2. There are two important assumptions on the schema for an
owned_schema, which we would need to check on an existing schema
converting it:
a. The owner of the extension should be the owner of the schema
b. There are no other objects in the schema appart from the extension.
I'll probably continue some efforts to allow for migration, because I
agree it's useful. But I don't think it's critical for this feature to
be committable. Even if this can only be used by new extensions (that
target PG18+ only), I think that would already be very useful. i.e. if
in 5 years time I don't have to worry about these security pitfalls
for any new extensions that I write then I'll be very happy. Also if
an extension really wants to upgrade to an owned schema, then that
should be possible by doing some manual catalog hackery in its
extension update script, because that's effectively what any automatic
conversion would also do.
> A couple minor comments:
> Doesn't "extension is owned_schema" sound a bit weird?
Updated the docs to address all of your feedback I think.
> Also, perhaps "dedicated_schema" would be better than "owned_schema"? I
> mean, the point is not that it's "owned" by the extension, but that
> there's nothing else in it. But that's nitpicking.
I agree that's probably a better name. Given the amount of effort
necessary to update everything I haven't done that yet. I'll think a
bit if there's a name I like even better in the meantime, and I'm
definitely open to other suggestions.
> 3) src/backend/commands/extension.c
>
> I'm not sure why AlterExtensionNamespace moves the privilege check. Why
> should it not check the privilege for owned schema too?
Because for an owned schema we're not creating objects in an existing
schema. We're only renaming the schema that the extension is already
in using RenameSchema, so there's no point in checking if the user can
create objects in that schema, since the objects are already there.
(RenameSchema also checks internally if the current user is the owner
of the schema)
> Shouldn't binary_upgrade_extension_member still set ext=NULL in the for
> loop, the way the original code does that?
I don't think that's necessary. It was necessary before to set extobj
to NULL, because that variable was set every loop. But now extobj is
scoped to the loop, and ext is only set right before the break. So
either we set ext in the loop and break out of the loop right away, or
ext is still set to the NULL value that's was assigned at the top of
the function.
> The long if conditions might need some indentation, I guess. pgindent
> leaves them like this, but 100 columns seems a bit too much. I'd do a
> line break after each condition, I guess.
Done
Attachments:
v4-0001-Add-support-for-extensions-with-an-owned-schema.patch (application/octet-stream, 32.1K) download
From 5bd7357f0c8cca63f3a26aa2d78adddc671a4757 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v4] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 +++++
doc/src/sgml/ref/create_extension.sgml | 4 +-
src/backend/commands/extension.c | 142 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 ++++--
src/bin/pg_dump/pg_dump.c | 54 ++++++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 7 +-
.../expected/test_extensions.out | 50 ++++++
src/test/modules/test_extensions/meson.build | 4 +
.../test_extensions/sql/test_extensions.sql | 27 ++++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
17 files changed, 326 insertions(+), 62 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 218940ee5ce..654799b2714 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -809,6 +809,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index ca2b80d669c..ffbe759f84e 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -102,7 +102,9 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. The named schema must already exist, unless
+ <literal>owned_schema</literal> is set to <literal>true</literal> in
+ the control file, then the schema must not exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index fab59ad5f66..06d8658c61f 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -84,6 +84,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -505,6 +507,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1491,8 +1501,11 @@ CreateExtensionInternal(char *extensionName,
*/
if (schemaName)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
+ /*
+ * If the user is giving us the schema name, it must exist already if
+ * the extension does not want to own the schema
+ */
+ schemaOid = get_namespace_oid(schemaName, control->owned_schema);
}
if (control->schema != NULL)
@@ -1514,7 +1527,10 @@ CreateExtensionInternal(char *extensionName,
/* Always use the schema from control file for current extension. */
schemaName = control->schema;
+ }
+ if (schemaName)
+ {
/* Find or create the schema in case it does not exist. */
schemaOid = get_namespace_oid(schemaName, true);
@@ -1535,8 +1551,22 @@ CreateExtensionInternal(char *extensionName,
*/
schemaOid = get_namespace_oid(schemaName, false);
}
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName)));
+ }
+
}
- else if (!OidIsValid(schemaOid))
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+ else
{
/*
* Neither user nor author of the extension specified schema; use the
@@ -1603,6 +1633,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -1808,7 +1839,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -1838,6 +1870,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -1882,6 +1915,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -2729,11 +2773,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -2742,22 +2785,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -2781,14 +2808,38 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
- /*
- * If the extension is already in the target schema, just silently do
- * nothing.
- */
- if (extForm->extnamespace == nspOid)
+ ownedSchema = extForm->extownedschema;
+
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -2861,6 +2912,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually have to move any objects anything for owned
+ * schemas, because we simply rename the schema.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -2904,18 +2962,26 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ if (ownedSchema)
+ {
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /* Now adjust pg_extension.extnamespace */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index c54b08fe180..05205cfb7f4 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/origin.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 1b47c388ced..395cc85285c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1776,6 +1776,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5645,7 +5658,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5659,19 +5672,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -5828,6 +5855,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -5836,7 +5864,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -5852,6 +5887,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -5865,6 +5901,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -10613,9 +10650,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -10727,6 +10764,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9f907ed5ad4..349e5219925 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -181,6 +181,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 673181b39ae..6a595fae53c 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 77f54a79e6a..3e294327a39 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11558,7 +11558,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index c6f3f867eb7..8e7fa574032 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -36,7 +36,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index 05272e6a40b..28f20290190 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -23,7 +24,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index f357cc21aaa..c0a2b7b315e 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -626,3 +626,53 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index c5f3424da51..52e8841480b 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -42,6 +42,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index 642c82ff5d3..136967db395 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -299,3 +299,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
base-commit: 259a0a99fe3d45dcf624788c1724d9989f3382dc
--
2.34.1
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-07-23 17:12 Artem Gavrilov <artem.gavrilov@percona.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
1 sibling, 1 reply; 43+ messages in thread
From: Artem Gavrilov @ 2025-07-23 17:12 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
Hello Jelte,
I reviewed your patch. Overall it looks good, I didn't find any problems
with code. Documentation is in place and clear.
Initial Run
===========
The patch applies cleanly to HEAD (196063d6761). All tests successfully
pass.
Comments
===========
1) I noticed that pg_dump changes weren't covered with tests.
2) I assume these error messages may be confusing, especially first one:
> -- Fails for an already existing schema to be provided
> CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
> ERROR: schema "test_ext_owned_schema" already exists
> -- Fails because a different schema is set in control file
> CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
> ERROR: extension "test_ext_owned_schema" must be installed in schema
> "test_ext_owned_schema"
In both cases it's not clear that the extension requires schema ownership.
Can hint messages be added there?
--
Artem Gavrilov
Senior Software Engineer, Percona
artem.gavrilov@percona.com
percona.com <http://www.percona.com;
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-07-27 22:03 Sadeq Dousti <msdousti@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
1 sibling, 2 replies; 43+ messages in thread
From: Sadeq Dousti @ 2025-07-27 22:03 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers; David E. Wheeler <david@justatheory.com>; Artem Gavrilov <artem.gavrilov@percona.com>
Dear Jelte,
I like the idea! In fact, I was thinking about the general search_path
confusion issue in Postgres (see also [1]), and what the root cause is.
IMHO, some search paths should always take priority - e.g., if a function
is defined in both pg_catalog and as a UDF, the UDF should only be called
if it is fully qualified, regardless of what the search_path is. But that
would require an overhaul of the Postgres resolution mechanism, and is out
of the scope of this patch.
For this patch, I have a few suggestions:
(a) The patch affects DROP EXTENSION in that it drops the schema as well,
if it's owned by the extension. This needs to be mentioned in the
documentation. In addition, an extra confirmation (e.g., "This will drop
schema nnnn as well, do you wish to continue?") when dropping the extension
might be desired, as the extension schema could contain user data (e.g.,
pg_cron keeps the jobs and their execution details).
(b) From the patch description:
> Writing the sql migration scripts that are run by CREATE EXTENSION
> and ALTER EXTENSION UPDATE are security minefields for extension authors.
While "ALTER EXTENSION UPDATE" is mentioned as a minefield, the patch does
not fix it (only ALTER EXTENSION ... SET SCHEMA is affected AFAICS). A
possible remedy could be that, before the update, the extension makes sure
no (sensitive?) object (e.g., UDF/Operator) created by a non-superuser
exists in its schema.
(c) Does it make sense to add the "owned_schema" option to the CREATE
EXTENSION command? Something like:
CREATE EXTENSION xyz
WITH owned_schema=true
This way, even if the extension itself is not (yet) updated to have
owned_schema in its control file, the DBA can rely on the schema lifecycle
management that comes with owned_schema=true. An alternative could be to
have it by default true (security by default), and if the DBA doesn't want
it for whatever reason, they have to explicitly set it to false during CREATE
EXTENSION.
(d) As David (Wheeler) mentioned in the thread, an extension control file
can have the "requires" field, in which an extension X installation depends
on other extensions Y & Z to be installed. I was thinking if X calls a
function from Y during installation, and Y does not have owned_schema, the
search_path confusion attack can be transitively applied. It could make
sense that X refuses to install, unless both Y & Z (= all required
extensions) are marked as owned_schema=true. Although in favor of backwards
compatibility, this can be overridable by an option in CREATE EXTENSION,
such as "WITH transitive_owned_schema=false".
[1]
https://www.cybertec-postgresql.com/en/abusing-security-definer-functions/
--
Best Regards,
Sadeq Dousti
Trade Republic Bank GmbH
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-07-28 00:27 David G. Johnston <david.g.johnston@gmail.com>
parent: Sadeq Dousti <msdousti@gmail.com>
1 sibling, 1 reply; 43+ messages in thread
From: David G. Johnston @ 2025-07-28 00:27 UTC (permalink / raw)
To: Sadeq Dousti <msdousti@gmail.com>; +Cc: Jelte Fennema-Nio <me@jeltef.nl>; Tomas Vondra <tomas@vondra.me>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers; David E. Wheeler <david@justatheory.com>; Artem Gavrilov <artem.gavrilov@percona.com>
On Sunday, July 27, 2025, Sadeq Dousti <msdousti@gmail.com> wrote:
>
> (a) The patch affects DROP EXTENSION in that it drops the schema as well,
> if it's owned by the extension. This needs to be mentioned in the
> documentation. In addition, an extra confirmation (e.g., "This will drop
> schema nnnn as well, do you wish to continue?") when dropping the
> extension might be desired, as the extension schema could contain user
> data (e.g., pg_cron keeps the jobs and their execution details).
>
SQL isn’t interactive in this sense. There isn’t a way to ask “are you
sure?”. At best the server can refuse to do something unless additional
options, like “force/cascade” are present in the command.
David J.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-07-28 00:45 Sadeq Dousti <msdousti@gmail.com>
parent: David G. Johnston <david.g.johnston@gmail.com>
0 siblings, 0 replies; 43+ messages in thread
From: Sadeq Dousti @ 2025-07-28 00:45 UTC (permalink / raw)
To: David G. Johnston <david.g.johnston@gmail.com>; +Cc: Jelte Fennema-Nio <me@jeltef.nl>; Tomas Vondra <tomas@vondra.me>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers; David E. Wheeler <david@justatheory.com>; Artem Gavrilov <artem.gavrilov@percona.com>
You're absolutely right about the lack of interactivity. I'd still go with
your suggestion of using something along the lines of cascade/force, as
dropping the schema silently can potentially delete the user data.
Bests,
Sadeq
On Mon, Jul 28, 2025, 02:27 David G. Johnston <david.g.johnston@gmail.com>
wrote:
> On Sunday, July 27, 2025, Sadeq Dousti <msdousti@gmail.com> wrote:
>
>>
>> (a) The patch affects DROP EXTENSION in that it drops the schema as well,
>> if it's owned by the extension. This needs to be mentioned in the
>> documentation. In addition, an extra confirmation (e.g., "This will drop
>> schema nnnn as well, do you wish to continue?") when dropping the
>> extension might be desired, as the extension schema could contain user
>> data (e.g., pg_cron keeps the jobs and their execution details).
>>
>
> SQL isn’t interactive in this sense. There isn’t a way to ask “are you
> sure?”. At best the server can refuse to do something unless additional
> options, like “force/cascade” are present in the command.
>
> David J.
>
>
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-07-28 08:01 Jelte Fennema-Nio <me@jeltef.nl>
parent: Sadeq Dousti <msdousti@gmail.com>
1 sibling, 0 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2025-07-28 08:01 UTC (permalink / raw)
To: Sadeq Dousti <msdousti@gmail.com>; +Cc: Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers; David E. Wheeler <david@justatheory.com>; Artem Gavrilov <artem.gavrilov@percona.com>
On Mon, 28 Jul 2025 at 00:03, Sadeq Dousti <msdousti@gmail.com> wrote:
> (a) The patch affects DROP EXTENSION in that it drops the schema as well, if it's owned by the extension. This needs to be mentioned in the documentation. In addition, an extra confirmation (e.g., "This will drop schema nnnn as well, do you wish to continue?") when dropping the extension might be desired, as the extension schema could contain user data (e.g., pg_cron keeps the jobs and their execution details).
This is already covered by docs for DROP EXTENSION:
"Dropping an extension causes its member objects <snip> to be dropped as well.
An extra confirmation or requiring of CASCADE seems unnecessary. The
schema itself will not contain user objects (that's entirely the point
of this change). It indeed might contain tables from the extension,
that contain user data, but that's no difference from extension
creating tables in other schemas. The only thing that will
additionally get removed for owned_schema extensions is the empty
schema that contained all of the other objects, that would normally be
removed.
> (b) From the patch description:
> > Writing the sql migration scripts that are run by CREATE EXTENSION
> > and ALTER EXTENSION UPDATE are security minefields for extension authors.
>
> While "ALTER EXTENSION UPDATE" is mentioned as a minefield, the patch does not fix it (only ALTER EXTENSION ... SET SCHEMA is affected AFAICS). A possible remedy could be that, before the update, the extension makes sure no (sensitive?) object (e.g., UDF/Operator) created by a non-superuser exists in its schema.
The whole security issue comes from the fact that the schema that an
extension gets installed in might be owned by a low-privileged user.
By having the schema be created by (and thus be owned by) the
extension creator, this whole problem goes away.
> (c) Does it make sense to add the "owned_schema" option to the CREATE EXTENSION command? Something like:
>
> CREATE EXTENSION xyz
> WITH owned_schema=true
I don't think that's a good idea. Since the migration script behaviour
can change significantly, I don't think it's safe to allow people to
specify it in CREATE EXTENSION. Especially because then people would
likely also be able to set it to false.
> An alternative could be to have it by default true (security by default), and if the DBA doesn't want it for whatever reason, they have to explicitly set it to false during CREATE EXTENSION.
I think changing the default would probably be good. Extension authors
can then explicitly opt-in to the less secure option if they require
that. I didn't want to do that for the initial change, as this seems
probably more contentious than adding a new option.
> (d) As David (Wheeler) mentioned in the thread, an extension control file can have the "requires" field, in which an extension X installation depends on other extensions Y & Z to be installed. I was thinking if X calls a function from Y during installation, and Y does not have owned_schema, the search_path confusion attack can be transitively applied. It could make sense that X refuses to install, unless both Y & Z (= all required extensions) are marked as owned_schema=true. Although in favor of backwards compatibility, this can be overridable by an option in CREATE EXTENSION, such as "WITH transitive_owned_schema=false".
I think it's fine to depend on extensions without owned_schema as an
owned_schema extension. It's not as if it's impossible to write safe
extension scripts now, it's just quite hard. An extension author can
choose to use owned_schema, to make their own life easier when writing
those scripts, but they can still depend on a perfectly safe extension
that did not use owned_schema but correctly hardened its extension
migration scripts.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-07-29 09:35 Jelte Fennema-Nio <postgres@jeltef.nl>
parent: Artem Gavrilov <artem.gavrilov@percona.com>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2025-07-29 09:35 UTC (permalink / raw)
To: Artem Gavrilov <artem.gavrilov@percona.com>; Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Wed Jul 23, 2025 at 7:12 PM CEST, Artem Gavrilov wrote:
> Hello Jelte,
>
> 1) I noticed that pg_dump changes weren't covered with tests.
>
> 2) I assume these error messages may be confusing, especially first one:
Attached is an updated version that addresses these issues.
Attachments:
v5-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 34.9K) download
From 7cc55c4e3a1fe2b5d958a6c5fc94defa04da29ac Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v5] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++++
doc/src/sgml/ref/create_extension.sgml | 4 +-
src/backend/commands/extension.c | 148 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 ++++--
src/bin/pg_dump/pg_dump.c | 54 ++++++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 7 +-
.../expected/test_extensions.out | 54 +++++++
src/test/modules/test_extensions/meson.build | 4 +
.../test_extensions/sql/test_extensions.sql | 27 ++++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++++
18 files changed, 366 insertions(+), 64 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..9fd2c3429e8 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,9 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. The named schema must already exist, unless
+ <literal>owned_schema</literal> is set to <literal>true</literal> in
+ the control file, then the schema must not exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd6..95eafecc06f 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -92,6 +92,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -613,6 +615,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1744,8 +1754,11 @@ CreateExtensionInternal(char *extensionName,
*/
if (schemaName)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
+ /*
+ * If the user is giving us the schema name, it must exist already if
+ * the extension does not want to own the schema
+ */
+ schemaOid = get_namespace_oid(schemaName, control->owned_schema);
}
if (control->schema != NULL)
@@ -1763,11 +1776,15 @@ CreateExtensionInternal(char *extensionName,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("extension \"%s\" must be installed in schema \"%s\"",
control->name,
- control->schema)));
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
/* Always use the schema from control file for current extension. */
schemaName = control->schema;
+ }
+ if (schemaName)
+ {
/* Find or create the schema in case it does not exist. */
schemaOid = get_namespace_oid(schemaName, true);
@@ -1788,8 +1805,23 @@ CreateExtensionInternal(char *extensionName,
*/
schemaOid = get_namespace_oid(schemaName, false);
}
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists but the extension needs to create it",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
}
- else if (!OidIsValid(schemaOid))
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+ else
{
/*
* Neither user nor author of the extension specified schema; use the
@@ -1856,6 +1888,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2061,7 +2094,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2091,6 +2125,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2135,6 +2170,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3040,7 +3086,7 @@ ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3053,11 +3099,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3066,22 +3111,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3105,14 +3134,38 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
- /*
- * If the extension is already in the target schema, just silently do
- * nothing.
- */
- if (extForm->extnamespace == nspOid)
+ ownedSchema = extForm->extownedschema;
+
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3185,6 +3238,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually have to move any objects anything for owned
+ * schemas, because we simply rename the schema.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3228,18 +3288,26 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ if (ownedSchema)
+ {
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /* Now adjust pg_extension.extnamespace */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90..15bd265375c 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 6298edb26b5..c2cd81e1e38 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1912,6 +1912,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5836,7 +5849,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5850,19 +5863,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6019,6 +6046,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6027,7 +6055,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6043,6 +6078,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6056,6 +6092,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11733,9 +11770,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11847,6 +11884,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 93a4475d51b..58e2be751b3 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 9214ebedafa..022bd6dd92b 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3ee8fed7e53..201d04e98cf 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11761,7 +11761,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 24419bfb5c9..205c5c7245f 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a3591bf3d2f..a6594c19d7e 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -25,7 +26,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index 72bae1bf254..bf9fc295280 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -668,3 +669,56 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists but the extension needs to create it
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists but the extension needs to create it
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 3c7e378bf35..c4913fb9c86 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..a97866d00ea 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index adcaa419616..bb7d414da16 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
base-commit: 258bf0a2ea8ff86257f750018bfd44397ce7e554
--
2.43.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-08-11 17:55 Robert Haas <robertmhaas@gmail.com>
parent: Jelte Fennema-Nio <postgres@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: Robert Haas @ 2025-08-11 17:55 UTC (permalink / raw)
To: Jelte Fennema-Nio <postgres@jeltef.nl>; +Cc: Artem Gavrilov <artem.gavrilov@percona.com>; Jelte Fennema-Nio <me@jeltef.nl>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Tue, Jul 29, 2025 at 5:35 AM Jelte Fennema-Nio <postgres@jeltef.nl> wrote:
> On Wed Jul 23, 2025 at 7:12 PM CEST, Artem Gavrilov wrote:
> > 1) I noticed that pg_dump changes weren't covered with tests.
> >
> > 2) I assume these error messages may be confusing, especially first one:
>
> Attached is an updated version that addresses these issues.
I generally like the direction that this is going but there are places
where the new stuff looks too much like it was bolted onto whatever
was there already. It's important to go back and edit things so that
they look natural, as if the new feature had been present all along.
- relocated. The named schema must already exist.
+ relocated. The named schema must already exist, unless
+ <literal>owned_schema</literal> is set to <literal>true</literal> in
+ the control file, then the schema must not exist.
This reads awkwardly, at least to me. The smallest possible edit that
would make it passable for me is to replace "the" with "in which
case," but possibly the whole sentence should be rephrased somehow.
+ * If the user is giving us the schema name, it must exist already if
+ * the extension does not want to own the schema
This could be made clearer.
+ errmsg("schema \"%s\" already exists but the extension needs to create it",
+ schemaName),
I don't really find this an improvement over: ERROR: schema
"test_ext_owned_schema" already exists.
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists but the extension needs to create it",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE
EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
}
- else if (!OidIsValid(schemaOid))
+ else if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+ else
It certainly seems worth asking whether this if-nest should be
rephrased in some way to make it clearer. But even if it's best to
keep it as it is, I find the absence of comments hard to justify. Who
is going to read the bit that emits "no schema has been selected to
create in" and find that self-explanatory?
I would like to see some improvements in AlterExtensionNamespace. In
the context of the patch, it's possible to puzzle out what is
happening, but I think the picture is not going to be clear to later
readers. It seems to me that this either needs some restructuring to
make the logical flow clearer, or a few well-written comments.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-08-11 19:23 Robert Haas <robertmhaas@gmail.com>
parent: Robert Haas <robertmhaas@gmail.com>
0 siblings, 2 replies; 43+ messages in thread
From: Robert Haas @ 2025-08-11 19:23 UTC (permalink / raw)
To: Jelte Fennema-Nio <postgres@jeltef.nl>; +Cc: Artem Gavrilov <artem.gavrilov@percona.com>; Jelte Fennema-Nio <me@jeltef.nl>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Mon, Aug 11, 2025 at 1:55 PM Robert Haas <robertmhaas@gmail.com> wrote:
> [ some review ]
Another thing that's occurring to me here is that nothing prevents
other objects from making their way into the owned schema. Sure, if we
create a new schema with nobody having any permissions, then only the
creating role or some role that has its privileges can add anything in
there. But that could happen by accident, or privileges could later be
granted and somebody could add something into the extension schema
after that. I wonder whether we should lock this down tighter somehow
and altogether forbid creating objects in that schema except from an
extension create/upgrade script for the owning extension.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-01 14:44 Jelte Fennema-Nio <postgres@jeltef.nl>
parent: Robert Haas <robertmhaas@gmail.com>
1 sibling, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2025-09-01 14:44 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Artem Gavrilov <artem.gavrilov@percona.com>; Jelte Fennema-Nio <me@jeltef.nl>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Mon Aug 11, 2025 at 9:23 PM CEST, Robert Haas wrote:
> On Mon, Aug 11, 2025 at 1:55 PM Robert Haas <robertmhaas@gmail.com> wrote:
>> [ some review ]
Attached is a patch that addresses your comments I think. I restructured
the schema creation code, and added more comments to the
AlterExtensionNamespace code (I couldn't find a way to make the
structure clearer).
> Another thing that's occurring to me here is that nothing prevents
> other objects from making their way into the owned schema. Sure, if we
> create a new schema with nobody having any permissions, then only the
> creating role or some role that has its privileges can add anything in
> there. But that could happen by accident, or privileges could later be
> granted and somebody could add something into the extension schema
> after that. I wonder whether we should lock this down tighter somehow
> and altogether forbid creating objects in that schema except from an
> extension create/upgrade script for the owning extension.
I think that's an interesting idea, and I started with a change to try
this out, that I intend to finish soon. It doesn't seem strictly
necessary, though.
Attachments:
v6-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 40.9K) download
From 61bdb411e6c7b979df7b8ff5ee8245f06219444e Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v6] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++
doc/src/sgml/ref/create_extension.sgml | 6 +-
src/backend/commands/extension.c | 363 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 ++-
src/bin/pg_dump/pg_dump.c | 54 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 7 +-
.../expected/test_extensions.out | 54 +++
src/test/modules/test_extensions/meson.build | 4 +
.../test_extensions/sql/test_extensions.sql | 27 ++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++
18 files changed, 517 insertions(+), 130 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..ab125d56263 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,11 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. Whether this schema should already exist or not depends on
+ the value of <literal>owned_schema</literal> in the extensions control
+ file: If it's <literal>true</literal>, the schema must
+ <emphasis>not</emphasis> exist; if it's <literal>false</literal> it
+ must exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd6..b42c25a7630 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -92,6 +92,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -613,6 +615,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1643,6 +1653,164 @@ find_install_path(List *evi_list, ExtensionVersionInfo *evi_target,
return evi_start;
}
+/*
+ * Create a schema with the given name, as part of CREATE EXTENSION.
+ */
+static Oid
+CreateSchemaForExtension(char *schemaName)
+{
+
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ /*
+ * CreateSchemaCommand includes CommandCounterIncrement, so new schema is
+ * now visible.
+ */
+ return get_namespace_oid(schemaName, false);
+}
+
+/*
+ * Create a owned schema with the given name, as part of CREATE EXTENSION, and
+ * fails if the schema already exist.
+ */
+static Oid
+CreateOwnedSchemaForExtension(char *schemaName)
+{
+ /* Find or create the schema in case it does not exist. */
+ Oid schemaOid = get_namespace_oid(schemaName, true);
+
+ if (OidIsValid(schemaOid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
+ return CreateSchemaForExtension(schemaName);
+}
+
+/*
+ * Gets or creates the schema than an extension should be created in.
+ *
+ * Returns the OID of the schema and updates schemaName to the name of the schema.
+ */
+static Oid
+GetOrCreateSchemaForExtension(char **schemaName, ExtensionControlFile *control, bool cascade)
+{
+ /*
+ * The simplest case is when the user provides a schema name.
+ */
+ if (*schemaName)
+ {
+ Oid schemaOid;
+
+ if (!control->owned_schema)
+ {
+ /*
+ * For non-owned schemas, this schema must already exist. We want
+ * to check this now, so it fails even if we bail out of this
+ * block due to the CASCADE logic.
+ */
+ schemaOid = get_namespace_oid(*schemaName, true);
+ }
+
+ if (control->schema && strcmp(control->schema, *schemaName) != 0)
+ {
+ /*
+ * The extension is not relocatable and the author gave us a
+ * schema for it.
+ *
+ * Unless CASCADE parameter was given, it's an error to give a
+ * schema different from control->schema if control->schema is
+ * specified.
+ */
+ if (!cascade)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("extension \"%s\" must be installed in schema \"%s\"",
+ control->name,
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
+ }
+
+ /*
+ * If the schema mismatches and CASCADE was given, we pretend the
+ * user did not specify a schema and use the normal logic below to
+ * get or create the schema from the control file.
+ */
+ }
+ else if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(*schemaName);
+ }
+ else
+ {
+ return schemaOid;
+ }
+ }
+
+ if (control->schema)
+ {
+ Oid schemaOid;
+
+ *schemaName = control->schema;
+
+ if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(control->schema);
+ }
+
+ /* Find or create the schema in case it does not exist. */
+ schemaOid = get_namespace_oid(control->schema, true);
+ if (OidIsValid(schemaOid))
+ {
+ return schemaOid;
+ }
+ return CreateSchemaForExtension(control->schema);
+ }
+
+ if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+
+ {
+ /*
+ * Neither user nor author of the extension specified schema; use the
+ * current default creation namespace, which is the first explicit
+ * entry in the search_path.
+ */
+ List *search_path = fetch_search_path(false);
+ Oid schemaOid;
+
+ if (search_path == NIL) /* nothing valid in search_path? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ schemaOid = linitial_oid(search_path);
+ *schemaName = get_namespace_name(schemaOid);
+ if (*schemaName == NULL) /* recently-deleted namespace? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+
+ list_free(search_path);
+ return schemaOid;
+ }
+}
+
/*
* CREATE EXTENSION worker
*
@@ -1739,78 +1907,7 @@ CreateExtensionInternal(char *extensionName,
*/
control = read_extension_aux_control_file(pcontrol, versionName);
- /*
- * Determine the target schema to install the extension into
- */
- if (schemaName)
- {
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
- }
-
- if (control->schema != NULL)
- {
- /*
- * The extension is not relocatable and the author gave us a schema
- * for it.
- *
- * Unless CASCADE parameter was given, it's an error to give a schema
- * different from control->schema if control->schema is specified.
- */
- if (schemaName && strcmp(control->schema, schemaName) != 0 &&
- !cascade)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("extension \"%s\" must be installed in schema \"%s\"",
- control->name,
- control->schema)));
-
- /* Always use the schema from control file for current extension. */
- schemaName = control->schema;
-
- /* Find or create the schema in case it does not exist. */
- schemaOid = get_namespace_oid(schemaName, true);
-
- if (!OidIsValid(schemaOid))
- {
- CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
-
- csstmt->schemaname = schemaName;
- csstmt->authrole = NULL; /* will be created by current user */
- csstmt->schemaElts = NIL;
- csstmt->if_not_exists = false;
- CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
- -1, -1);
-
- /*
- * CreateSchemaCommand includes CommandCounterIncrement, so new
- * schema is now visible.
- */
- schemaOid = get_namespace_oid(schemaName, false);
- }
- }
- else if (!OidIsValid(schemaOid))
- {
- /*
- * Neither user nor author of the extension specified schema; use the
- * current default creation namespace, which is the first explicit
- * entry in the search_path.
- */
- List *search_path = fetch_search_path(false);
-
- if (search_path == NIL) /* nothing valid in search_path? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
- schemaOid = linitial_oid(search_path);
- schemaName = get_namespace_name(schemaOid);
- if (schemaName == NULL) /* recently-deleted namespace? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
-
- list_free(search_path);
- }
+ schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
/*
* Make note if a temporary namespace has been accessed in this
@@ -1856,6 +1953,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2061,7 +2159,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2091,6 +2190,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2135,6 +2235,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3035,12 +3146,16 @@ extension_config_remove(Oid extensionoid, Oid tableoid)
/*
* Execute ALTER EXTENSION SET SCHEMA
+ *
+ * For owned schemas, this boils down to changing the name of its schema. For
+ * non-owned schemas this requires moving all the member objects into the new
+ * schema.
*/
ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3053,11 +3168,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3066,22 +3180,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3105,14 +3203,43 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
+ ownedSchema = extForm->extownedschema;
+
/*
- * If the extension is already in the target schema, just silently do
- * nothing.
+ * For non-owned schemas, we should now evaluate if the target schema is a
+ * valid target. For owned schemas, no such checks are needed, because
+ * we'll simply rename the existing schema.
*/
- if (extForm->extnamespace == nspOid)
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3185,6 +3312,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually move any objects for owned schemas because we
+ * simply rename the schema that these objects are already in.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3228,18 +3362,35 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ /* Now actually update the schema of the extension. */
+ if (ownedSchema)
+ {
+ /*
+ * For owned schemas, we simply rename the schema. This means that we
+ * don't need to update the extension its catalog entry, because the
+ * oid of the schema will stay the same.
+ */
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /*
+ * For non-owned schemas, we now have to update the extension's schema
+ * entry, and also update the dependencies.
+ */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90..15bd265375c 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index fc7a6639163..a8c25cb3f44 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1921,6 +1921,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5855,7 +5868,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5869,19 +5882,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6038,6 +6065,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6046,7 +6074,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6062,6 +6097,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6075,6 +6111,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11782,9 +11819,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11896,6 +11933,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156c..ebd391b447e 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 9214ebedafa..022bd6dd92b 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 118d6da1ace..e47c087cd9b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11761,7 +11761,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 24419bfb5c9..205c5c7245f 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a3591bf3d2f..a6594c19d7e 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -25,7 +26,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index 72bae1bf254..bf9fc295280 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -668,3 +669,56 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists but the extension needs to create it
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists but the extension needs to create it
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 3c7e378bf35..c4913fb9c86 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..a97866d00ea 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index adcaa419616..bb7d414da16 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
base-commit: 36aed19fd99021ad9f727e4fd4bceb086a7cf54d
--
2.51.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-01 20:27 Jelte Fennema-Nio <postgres@jeltef.nl>
parent: Jelte Fennema-Nio <postgres@jeltef.nl>
0 siblings, 0 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2025-09-01 20:27 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Artem Gavrilov <artem.gavrilov@percona.com>; Jelte Fennema-Nio <me@jeltef.nl>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Mon Sep 1, 2025 at 4:44 PM CEST, Jelte Fennema-Nio wrote:
> On Mon Aug 11, 2025 at 9:23 PM CEST, Robert Haas wrote:
>> On Mon, Aug 11, 2025 at 1:55 PM Robert Haas <robertmhaas@gmail.com> wrote:
>>> [ some review ]
>
> Attached is a patch that addresses your comments I think.
Attached is a version that doesn't fail the tests.
Attachments:
v7-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 40.8K) download
From 1c919176e0d90d91ec234db343695f80b6715f07 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v7] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++
doc/src/sgml/ref/create_extension.sgml | 6 +-
src/backend/commands/extension.c | 363 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 ++-
src/bin/pg_dump/pg_dump.c | 54 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 7 +-
.../expected/test_extensions.out | 54 +++
src/test/modules/test_extensions/meson.build | 4 +
.../test_extensions/sql/test_extensions.sql | 27 ++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++
18 files changed, 517 insertions(+), 130 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..ab125d56263 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,11 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. Whether this schema should already exist or not depends on
+ the value of <literal>owned_schema</literal> in the extensions control
+ file: If it's <literal>true</literal>, the schema must
+ <emphasis>not</emphasis> exist; if it's <literal>false</literal> it
+ must exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index e6f9ab6dfd6..d2c8aa3e8a0 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -92,6 +92,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -613,6 +615,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1643,6 +1653,164 @@ find_install_path(List *evi_list, ExtensionVersionInfo *evi_target,
return evi_start;
}
+/*
+ * Create a schema with the given name, as part of CREATE EXTENSION.
+ */
+static Oid
+CreateSchemaForExtension(char *schemaName)
+{
+
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ /*
+ * CreateSchemaCommand includes CommandCounterIncrement, so new schema is
+ * now visible.
+ */
+ return get_namespace_oid(schemaName, false);
+}
+
+/*
+ * Create a owned schema with the given name, as part of CREATE EXTENSION, and
+ * fails if the schema already exist.
+ */
+static Oid
+CreateOwnedSchemaForExtension(char *schemaName)
+{
+ /* Find or create the schema in case it does not exist. */
+ Oid schemaOid = get_namespace_oid(schemaName, true);
+
+ if (OidIsValid(schemaOid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
+ return CreateSchemaForExtension(schemaName);
+}
+
+/*
+ * Gets or creates the schema than an extension should be created in.
+ *
+ * Returns the OID of the schema and updates schemaName to the name of the schema.
+ */
+static Oid
+GetOrCreateSchemaForExtension(char **schemaName, ExtensionControlFile *control, bool cascade)
+{
+ /*
+ * The simplest case is when the user provides a schema name.
+ */
+ if (*schemaName)
+ {
+ Oid schemaOid;
+
+ if (!control->owned_schema)
+ {
+ /*
+ * For non-owned schemas, this schema must already exist. We want
+ * to check this now, so it fails even if we bail out of this
+ * block due to the CASCADE logic.
+ */
+ schemaOid = get_namespace_oid(*schemaName, false);
+ }
+
+ if (control->schema && strcmp(control->schema, *schemaName) != 0)
+ {
+ /*
+ * The extension is not relocatable and the author gave us a
+ * schema for it.
+ *
+ * Unless CASCADE parameter was given, it's an error to give a
+ * schema different from control->schema if control->schema is
+ * specified.
+ */
+ if (!cascade)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("extension \"%s\" must be installed in schema \"%s\"",
+ control->name,
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
+ }
+
+ /*
+ * If the schema mismatches and CASCADE was given, we pretend the
+ * user did not specify a schema and use the normal logic below to
+ * get or create the schema from the control file.
+ */
+ }
+ else if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(*schemaName);
+ }
+ else
+ {
+ return schemaOid;
+ }
+ }
+
+ if (control->schema)
+ {
+ Oid schemaOid;
+
+ *schemaName = control->schema;
+
+ if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(control->schema);
+ }
+
+ /* Find or create the schema in case it does not exist. */
+ schemaOid = get_namespace_oid(control->schema, true);
+ if (OidIsValid(schemaOid))
+ {
+ return schemaOid;
+ }
+ return CreateSchemaForExtension(control->schema);
+ }
+
+ if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+
+ {
+ /*
+ * Neither user nor author of the extension specified schema; use the
+ * current default creation namespace, which is the first explicit
+ * entry in the search_path.
+ */
+ List *search_path = fetch_search_path(false);
+ Oid schemaOid;
+
+ if (search_path == NIL) /* nothing valid in search_path? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ schemaOid = linitial_oid(search_path);
+ *schemaName = get_namespace_name(schemaOid);
+ if (*schemaName == NULL) /* recently-deleted namespace? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+
+ list_free(search_path);
+ return schemaOid;
+ }
+}
+
/*
* CREATE EXTENSION worker
*
@@ -1739,78 +1907,7 @@ CreateExtensionInternal(char *extensionName,
*/
control = read_extension_aux_control_file(pcontrol, versionName);
- /*
- * Determine the target schema to install the extension into
- */
- if (schemaName)
- {
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
- }
-
- if (control->schema != NULL)
- {
- /*
- * The extension is not relocatable and the author gave us a schema
- * for it.
- *
- * Unless CASCADE parameter was given, it's an error to give a schema
- * different from control->schema if control->schema is specified.
- */
- if (schemaName && strcmp(control->schema, schemaName) != 0 &&
- !cascade)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("extension \"%s\" must be installed in schema \"%s\"",
- control->name,
- control->schema)));
-
- /* Always use the schema from control file for current extension. */
- schemaName = control->schema;
-
- /* Find or create the schema in case it does not exist. */
- schemaOid = get_namespace_oid(schemaName, true);
-
- if (!OidIsValid(schemaOid))
- {
- CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
-
- csstmt->schemaname = schemaName;
- csstmt->authrole = NULL; /* will be created by current user */
- csstmt->schemaElts = NIL;
- csstmt->if_not_exists = false;
- CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
- -1, -1);
-
- /*
- * CreateSchemaCommand includes CommandCounterIncrement, so new
- * schema is now visible.
- */
- schemaOid = get_namespace_oid(schemaName, false);
- }
- }
- else if (!OidIsValid(schemaOid))
- {
- /*
- * Neither user nor author of the extension specified schema; use the
- * current default creation namespace, which is the first explicit
- * entry in the search_path.
- */
- List *search_path = fetch_search_path(false);
-
- if (search_path == NIL) /* nothing valid in search_path? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
- schemaOid = linitial_oid(search_path);
- schemaName = get_namespace_name(schemaOid);
- if (schemaName == NULL) /* recently-deleted namespace? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
-
- list_free(search_path);
- }
+ schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
/*
* Make note if a temporary namespace has been accessed in this
@@ -1856,6 +1953,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2061,7 +2159,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2091,6 +2190,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2135,6 +2235,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3035,12 +3146,16 @@ extension_config_remove(Oid extensionoid, Oid tableoid)
/*
* Execute ALTER EXTENSION SET SCHEMA
+ *
+ * For owned schemas, this boils down to changing the name of its schema. For
+ * non-owned schemas this requires moving all the member objects into the new
+ * schema.
*/
ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3053,11 +3168,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3066,22 +3180,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3105,14 +3203,43 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
+ ownedSchema = extForm->extownedschema;
+
/*
- * If the extension is already in the target schema, just silently do
- * nothing.
+ * For non-owned schemas, we should now evaluate if the target schema is a
+ * valid target. For owned schemas, no such checks are needed, because
+ * we'll simply rename the existing schema.
*/
- if (extForm->extnamespace == nspOid)
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3185,6 +3312,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually move any objects for owned schemas because we
+ * simply rename the schema that these objects are already in.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3228,18 +3362,35 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ /* Now actually update the schema of the extension. */
+ if (ownedSchema)
+ {
+ /*
+ * For owned schemas, we simply rename the schema. This means that we
+ * don't need to update the extension its catalog entry, because the
+ * oid of the schema will stay the same.
+ */
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /*
+ * For non-owned schemas, we now have to update the extension's schema
+ * entry, and also update the dependencies.
+ */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index a4f8b4faa90..15bd265375c 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index fc7a6639163..a8c25cb3f44 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1921,6 +1921,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5855,7 +5868,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5869,19 +5882,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6038,6 +6065,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6046,7 +6074,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6062,6 +6097,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6075,6 +6111,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11782,9 +11819,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11896,6 +11933,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index dde85ed156c..ebd391b447e 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 9214ebedafa..022bd6dd92b 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 118d6da1ace..e47c087cd9b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11761,7 +11761,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 24419bfb5c9..205c5c7245f 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a3591bf3d2f..a6594c19d7e 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -25,7 +26,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index 72bae1bf254..eb3ef01996b 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -668,3 +669,56 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema;
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 3c7e378bf35..c4913fb9c86 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,10 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..a97866d00ea 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,30 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+DROP EXTENSION test_ext_owned_schema;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index adcaa419616..bb7d414da16 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
base-commit: 36aed19fd99021ad9f727e4fd4bceb086a7cf54d
--
2.51.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-02 00:03 Julien Rouhaud <rjuju123@gmail.com>
parent: Robert Haas <robertmhaas@gmail.com>
1 sibling, 1 reply; 43+ messages in thread
From: Julien Rouhaud @ 2025-09-02 00:03 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Jelte Fennema-Nio <postgres@jeltef.nl>; Artem Gavrilov <artem.gavrilov@percona.com>; Jelte Fennema-Nio <me@jeltef.nl>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Tue, 12 Aug 2025, 03:24 Robert Haas, <robertmhaas@gmail.com> wrote:
> On Mon, Aug 11, 2025 at 1:55 PM Robert Haas <robertmhaas@gmail.com> wrote:
> > [ some review ]
>
> Another thing that's occurring to me here is that nothing prevents
> other objects from making their way into the owned schema. Sure, if we
> create a new schema with nobody having any permissions, then only the
> creating role or some role that has its privileges can add anything in
> there. But that could happen by accident, or privileges could later be
> granted and somebody could add something into the extension schema
> after that. I wonder whether we should lock this down tighter somehow
> and altogether forbid creating objects in that schema except from an
> extension create/upgrade script for the owning extension.
>
I think that it would be too strict. One not too uncommon scenario is an
extension in a dedicated schema that creates additional objects
dynamically, for instance creating new partitions using triggers on one of
the extension table. Such objects are not part of the extension and yet
are in control of the extension.
As an example powa already relies on that a lot (it creates new tables if
you register a new extension dynamically), and I'm about to add a feature
that create/drops s a bunch of inherited tables via a trigger when a remote
server is added / removed. I'm sure that there are a lot of other
extensions doing something similar.
>
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-02 07:37 Jelte Fennema-Nio <me@jeltef.nl>
parent: Julien Rouhaud <rjuju123@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2025-09-02 07:37 UTC (permalink / raw)
To: Julien Rouhaud <rjuju123@gmail.com>; +Cc: Robert Haas <robertmhaas@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Tue, 2 Sept 2025 at 02:03, Julien Rouhaud <rjuju123@gmail.com> wrote:
> One not too uncommon scenario is an extension in a dedicated schema that creates additional objects dynamically, for instance creating new partitions using triggers on one of the extension table.
Interesting. I didn't know there were extensions that did that. That
definitely doesn't seem like a very common pattern though.
But I don't think that's a problem for this idea. In the
implementation I'm working on, superuser would still be allowed to
create objects in such locked down owned schemas. So as long as the
extension upgrades its permissions to superuser during these DDLs it
should still be fine. (easy to do with SECURITY DEFINER or by
temporarily changing permissions from C)
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-02 09:02 Julien Rouhaud <rjuju123@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: Julien Rouhaud @ 2025-09-02 09:02 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Robert Haas <robertmhaas@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Tue, Sep 02, 2025 at 09:37:31AM +0200, Jelte Fennema-Nio wrote:
> On Tue, 2 Sept 2025 at 02:03, Julien Rouhaud <rjuju123@gmail.com> wrote:
> > One not too uncommon scenario is an extension in a dedicated schema that creates additional objects dynamically, for instance creating new partitions using triggers on one of the extension table.
>
> Interesting. I didn't know there were extensions that did that. That
> definitely doesn't seem like a very common pattern though.
I think that there are way more extensions that dynamically create objects than
what you think. Some years ago I was working on such an extension at work, and
pgtt is also creating some objects under the hood. That's already 3 extensions
that I know on top of my head without having to think about it.
> But I don't think that's a problem for this idea. In the
> implementation I'm working on, superuser would still be allowed to
> create objects in such locked down owned schemas. So as long as the
> extension upgrades its permissions to superuser during these DDLs it
> should still be fine. (easy to do with SECURITY DEFINER or by
> temporarily changing permissions from C)
Requiring superuser permission seems like a big penalty, especially since the
last few years have been all about *not* requiring superuser privileges. Note
also that not all extensions embeds compiled code, some are just doing plain
plpgsql and work just fine.
Why not requiring schema owner privileges?
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-02 13:35 Robert Haas <robertmhaas@gmail.com>
parent: Julien Rouhaud <rjuju123@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Robert Haas @ 2025-09-02 13:35 UTC (permalink / raw)
To: Julien Rouhaud <rjuju123@gmail.com>; +Cc: Jelte Fennema-Nio <me@jeltef.nl>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Tue, Sep 2, 2025 at 5:02 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
> Requiring superuser permission seems like a big penalty, especially since the
> last few years have been all about *not* requiring superuser privileges. Note
> also that not all extensions embeds compiled code, some are just doing plain
> plpgsql and work just fine.
>
> Why not requiring schema owner privileges?
I agree with you that requiring superuser privileges is undesirable.
However, requiring schema owner privileges isn't really requiring
anything above and beyond what the permissions system would require
anyway, since at the start, nobody else will have privileges on that
schema. And that's where what Jelte was proposing -- and also what you
propose here -- seems very accident-prone to me. It would be quite
easy for the user who created the extension, and who therefore also
owns the schema IIUC, to accidentally put other stuff there, or allow
others to do so, and that might undermine the safe search_path
guarantee that is the purpose of this patch. I am not fixed on any
particular method of making that guarantee more robust, but I am in
favor of trying to come up with some way of making it more robust.
The first thought that popped into my head was that maybe your
extension should make the objects it creates part of the extension,
but I think that doesn't actually work, because it would mean that
they would not be dumped. I think what you've got is something
intermediate between full extension membership (where installing the
extension creates the objects) and non-membership (where the objects
are created by the user and unaffiliated with the extension) but we
have no concept of that in the system today. Should we?
Another thing to consider here is what happens when somebody tries to
drop the extension. As Jelte coded it in the last version I read, it
just blows away the schema. I don't remember whether it used RESTRICT
(in which case the presence of unrelated objects in the schema would
result in failure) or CASCADE (in which case they would be silently
blown away) but I don't like either option very much. The former would
be pretty inconvenient in a use case like yours, and the latter would
be terrible if there were valuable, unrelated objects in the schema. I
think CASCADE is the right answer as long as there's something that
makes it unlikely that the schema has any unanticipated contents.
Here are a few possible ways forward:
(1) Invent, as proposed above, an intermediate level of extension
membership where objects are dropped with the extension but are still
dumped, and find a way for extensions to create objects in an owned
schema only if the new object will be either fully owned by the
extension or at least at this intermediate level.
(2) Decide that extensions that create additional objects in the
schema shouldn't use owned schemas, and document this.
(3) Add a mechanism to track when an extension-owned function is
executing, and only allow objects to be created in an extension-owned
schema when that's the case.
(4) Add a GUC that overrides the usual prohibition on creating objects
in an extension-owned schema, and let users or the extension set it if
they wish.
(5) Decide I'm wrong and that we should just let objects be created
freely in the owned schema. Document the consequences of this and
blame any problems on user error.
(6) Your superior idea goes here!
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-06 00:17 Julien Rouhaud <rjuju123@gmail.com>
parent: Robert Haas <robertmhaas@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Julien Rouhaud @ 2025-09-06 00:17 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Jelte Fennema-Nio <me@jeltef.nl>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
Hi,
On Tue, Sep 02, 2025 at 09:35:45AM -0400, Robert Haas wrote:
> On Tue, Sep 2, 2025 at 5:02 AM Julien Rouhaud <rjuju123@gmail.com> wrote:
> > Requiring superuser permission seems like a big penalty, especially since the
> > last few years have been all about *not* requiring superuser privileges. Note
> > also that not all extensions embeds compiled code, some are just doing plain
> > plpgsql and work just fine.
> >
> > Why not requiring schema owner privileges?
>
> I agree with you that requiring superuser privileges is undesirable.
> However, requiring schema owner privileges isn't really requiring
> anything above and beyond what the permissions system would require
> anyway, since at the start, nobody else will have privileges on that
> schema. And that's where what Jelte was proposing -- and also what you
> propose here -- seems very accident-prone to me. It would be quite
> easy for the user who created the extension, and who therefore also
> owns the schema IIUC, to accidentally put other stuff there, or allow
> others to do so
Requiring schema owner privilege wouldn't allow the user who created the
extension to allow other users to mess up with the extension's private schema?
At least not with a simple GRANT on the schema.
I'm also wondering how much you can prevent the owner from doing changes in the
owned schema without leading to unhelpful behavior. Would the owner still be
allowed to create extra indexes on extension owned table for instance, change
the TOAST setting, move them to other tablespace or ...?
> The first thought that popped into my head was that maybe your
> extension should make the objects it creates part of the extension,
> but I think that doesn't actually work, because it would mean that
> they would not be dumped.
Arguably there is pg_extension_config_dump() for that, assuming that this would
become allowed in scenario like the one I described (and modified to also emit
the DDL in such case). But it would be hard for extension authors to use as
you would have to call it only with some major versions.
We could do it automatically, but extensions may not need to dump all tables
and/or all rows. For instance in powa we have some unlogged tables that are
use transiently during a snapshot, and those tables shouldn't be dumped. Right
now if such tables are created by the extension they're always dumped, but it
would be good to make it configurable if dynamically created tables become part
of the extension, one way or another.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-06 07:35 Jelte Fennema-Nio <me@jeltef.nl>
parent: Julien Rouhaud <rjuju123@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2025-09-06 07:35 UTC (permalink / raw)
To: Julien Rouhaud <rjuju123@gmail.com>; +Cc: Robert Haas <robertmhaas@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Sat, 6 Sept 2025 at 02:17, Julien Rouhaud <rjuju123@gmail.com> wrote:
> Requiring schema owner privilege wouldn't allow the user who created the
> extension to allow other users to mess up with the extension's private schema?
> At least not with a simple GRANT on the schema.
I think that sounds like reasonable change to Roberts initial
proposal: Allowing the schema owner and superusers to add objects in
the schema, but disallow all other users (even if they have CREATE
privileges on the schema).
I think this seems reasonable from a security perspective. The thing
owned_schema protects against, is accidentally executing code with
permissions of the extension script runner. The owner of the schema is
always the same user as the extension script runner. But it protects
users from the somewhat easy to make mistake of GRANT ALL ON SCHEMA
(instead of GRANT USAGE ON SCHEMA).
Note that this means that even with trusted=true, a non-superuser
extension owner would still not be able to the schema. For that
superuser=false is needed in the control file.
The only thing I'm wondering is if we should allow changing the schema
owner with ALTER SCHEMA OWNER TO. Because that would break this
assumption:
> The owner of the schema is always the same user as the extension script runner.
But that command seems unlikely to be run by accident. But on the
other hand, I don't really see a usecase for changing the schema
owner, except for breaking this protection. So I'm leaning towards
disallowing ALTER SCHEMA OWNER TO on the schema, probably even for
superusers.
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-11 13:01 Robert Haas <robertmhaas@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: Robert Haas @ 2025-09-11 13:01 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Julien Rouhaud <rjuju123@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Sat, Sep 6, 2025 at 3:35 AM Jelte Fennema-Nio <me@jeltef.nl> wrote:
> I think that sounds like reasonable change to Roberts initial
> proposal: Allowing the schema owner and superusers to add objects in
> the schema, but disallow all other users (even if they have CREATE
> privileges on the schema).
I don't know, I'm not really convinced. I feel like this isn't really
a security issue but more of a could-be-an-unpleasant-surprise issue.
What the patch does (IIRC) is make it so that dropping the extension
just cascade-drops the schema. If the schema contains anything
unrelated to the extension, that's going to remove stuff that it
shouldn't remove. In Julien's examples, the other stuff that gets
introduced into the schema is logically part of the extension even if
it doesn't formally have membership in the extension, but somebody
could equally well just install an unrelated extension in the same
schema and then drop the first extension and, whoops.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-11 13:29 Jelte Fennema-Nio <me@jeltef.nl>
parent: Robert Haas <robertmhaas@gmail.com>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2025-09-11 13:29 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Julien Rouhaud <rjuju123@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Thu, 11 Sept 2025 at 15:02, Robert Haas <robertmhaas@gmail.com> wrote:
> What the patch does (IIRC) is make it so that dropping the extension
> just cascade-drops the schema.
You recall incorrectly ;) It only does that when you do:
DROP EXTENSION ... CASCADE
Otherwise you get errors like this:
DROP EXTENSION test_ext_owned_schema;
ERROR: cannot drop extension test_ext_owned_schema because other
objects depend on it
DETAIL: function test_owned_schema_defaults.new_owned() depends on
schema test_owned_schema_defaults
> but somebody
> could equally well just install an unrelated extension in the same
> schema and then drop the first extension and, whoops.
To be clear, that could only happen when that unrelated extension does
not have owned_schema=true. Because creating such an extension
requires the schema to not exist yet. (And even then as explained
above the accidental drop only happens when the user uses CASCADE.)
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2025-09-11 14:52 Robert Haas <robertmhaas@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 2 replies; 43+ messages in thread
From: Robert Haas @ 2025-09-11 14:52 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Julien Rouhaud <rjuju123@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Thu, Sep 11, 2025 at 9:29 AM Jelte Fennema-Nio <me@jeltef.nl> wrote:
> You recall incorrectly ;) It only does that when you do:
> DROP EXTENSION ... CASCADE
>
> Otherwise you get errors like this:
>
> DROP EXTENSION test_ext_owned_schema;
> ERROR: cannot drop extension test_ext_owned_schema because other
> objects depend on it
> DETAIL: function test_owned_schema_defaults.new_owned() depends on
> schema test_owned_schema_defaults
OK. Perhaps that needs some associated tests?
To be honest, I'm kind of leaning at this point toward saying we
shouldn't impose any special restrictions here. If the DROP doesn't
cascade, then the worst thing that can happen is that you make it hard
for yourself to drop your own extension cleanly. I think letting the
superuser and the schema owner do things and other people not is too
weird -- it basically boils down to ignoring GRANT sometimes, and I
think users will find it confusing. If we were going to have
special_tinkering_mode=true|false that affected everyone equally, that
would make sense to me, but it sounds like nobody else really likes
that, so it's probably just a bad idea.
> > but somebody
> > could equally well just install an unrelated extension in the same
> > schema and then drop the first extension and, whoops.
>
> To be clear, that could only happen when that unrelated extension does
> not have owned_schema=true. Because creating such an extension
> requires the schema to not exist yet. (And even then as explained
> above the accidental drop only happens when the user uses CASCADE.)
Sure.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-02-10 23:19 Jelte Fennema-Nio <me@jeltef.nl>
parent: Robert Haas <robertmhaas@gmail.com>
1 sibling, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2026-02-10 23:19 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Julien Rouhaud <rjuju123@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Thu, 11 Sept 2025 at 16:52, Robert Haas <robertmhaas@gmail.com> wrote:
> OK. Perhaps that needs some associated tests?
Added now in v8, as well as a bunch of other tests. Including a test for
trusted extensions, and a fix so that for trusted extensions the owned
schema is owned by the bootstrap superuser. Changes made since v7 can be
found in nocfbot.changes.diff.
> To be honest, I'm kind of leaning at this point toward saying we
> shouldn't impose any special restrictions here. If the DROP doesn't
> cascade, then the worst thing that can happen is that you make it hard
> for yourself to drop your own extension cleanly. I think letting the
> superuser and the schema owner do things and other people not is too
> weird -- it basically boils down to ignoring GRANT sometimes, and I
> think users will find it confusing.
I agree. I kept it like that.
Attachments: download all (2 files, .tar.gz)
v8-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 51.6K) download
From f4b88e50f9ebe1f36974c91b6723345e47a4317c Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v8] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++
doc/src/sgml/ref/create_extension.sgml | 6 +-
src/backend/commands/extension.c | 429 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 +-
src/bin/pg_dump/pg_dump.c | 54 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 13 +-
.../expected/test_extensions.out | 157 +++++++
src/test/modules/test_extensions/meson.build | 11 +
.../test_extensions/sql/test_extensions.sql | 75 +++
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++
18 files changed, 734 insertions(+), 143 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..ab125d56263 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,11 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. Whether this schema should already exist or not depends on
+ the value of <literal>owned_schema</literal> in the extensions control
+ file: If it's <literal>true</literal>, the schema must
+ <emphasis>not</emphasis> exist; if it's <literal>false</literal> it
+ must exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 81f24615d51..03b3de357bd 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -94,6 +94,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -790,6 +792,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1233,6 +1243,44 @@ extension_is_trusted(ExtensionControlFile *control)
return false;
}
+/*
+ * Enforce superuser requirements for extension operations.
+ *
+ * Returns true if we should switch to superuser for trusted extensions.
+ * Throws an error if superuser is required but not available.
+ *
+ * This function should only be called after choosing the appropriate
+ * control file (including any secondary control files for updates).
+ */
+static bool
+check_extension_superuser_requirements(ExtensionControlFile *control,
+ const char *from_version)
+{
+ if (control->superuser && !superuser())
+ {
+ if (extension_is_trusted(control))
+ return true;
+ else if (from_version == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to create this extension.")
+ : errhint("Must be superuser to create this extension.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to update extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to update this extension.")
+ : errhint("Must be superuser to update this extension.")));
+ }
+
+ return false;
+}
+
/*
* Execute the appropriate script file for installing or updating the extension
*
@@ -1261,27 +1309,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
* here so that the control flags are correctly associated with the right
* script(s) if they happen to be set in secondary control files.
*/
- if (control->superuser && !superuser())
- {
- if (extension_is_trusted(control))
- switch_to_superuser = true;
- else if (from_version == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to create this extension.")
- : errhint("Must be superuser to create this extension.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to update extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to update this extension.")
- : errhint("Must be superuser to update this extension.")));
- }
+ switch_to_superuser = check_extension_superuser_requirements(control, from_version);
filename = get_extension_script_filename(control, from_version, version);
@@ -1820,6 +1848,164 @@ find_install_path(List *evi_list, ExtensionVersionInfo *evi_target,
return evi_start;
}
+/*
+ * Create a schema with the given name, as part of CREATE EXTENSION.
+ */
+static Oid
+CreateSchemaForExtension(char *schemaName)
+{
+
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ /*
+ * CreateSchemaCommand includes CommandCounterIncrement, so new schema is
+ * now visible.
+ */
+ return get_namespace_oid(schemaName, false);
+}
+
+/*
+ * Create an owned schema with the given name, as part of CREATE EXTENSION, and
+ * fails if the schema already exist.
+ */
+static Oid
+CreateOwnedSchemaForExtension(char *schemaName)
+{
+ /* Find or create the schema in case it does not exist. */
+ Oid schemaOid = get_namespace_oid(schemaName, true);
+
+ if (OidIsValid(schemaOid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
+ return CreateSchemaForExtension(schemaName);
+}
+
+/*
+ * Gets or creates the schema than an extension should be created in.
+ *
+ * Returns the OID of the schema and updates schemaName to the name of the schema.
+ */
+static Oid
+GetOrCreateSchemaForExtension(char **schemaName, ExtensionControlFile *control, bool cascade)
+{
+ /*
+ * The simplest case is when the user provides a schema name.
+ */
+ if (*schemaName)
+ {
+ Oid schemaOid;
+
+ if (!control->owned_schema)
+ {
+ /*
+ * For non-owned schemas, this schema must already exist. We want
+ * to check this now, so it fails even if we bail out of this
+ * block due to the CASCADE logic.
+ */
+ schemaOid = get_namespace_oid(*schemaName, false);
+ }
+
+ if (control->schema && strcmp(control->schema, *schemaName) != 0)
+ {
+ /*
+ * The extension is not relocatable and the author gave us a
+ * schema for it.
+ *
+ * Unless CASCADE parameter was given, it's an error to give a
+ * schema different from control->schema if control->schema is
+ * specified.
+ */
+ if (!cascade)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("extension \"%s\" must be installed in schema \"%s\"",
+ control->name,
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
+ }
+
+ /*
+ * If the schema mismatches and CASCADE was given, we pretend the
+ * user did not specify a schema and use the normal logic below to
+ * get or create the schema from the control file.
+ */
+ }
+ else if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(*schemaName);
+ }
+ else
+ {
+ return schemaOid;
+ }
+ }
+
+ if (control->schema)
+ {
+ Oid schemaOid;
+
+ *schemaName = control->schema;
+
+ if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(control->schema);
+ }
+
+ /* Find or create the schema in case it does not exist. */
+ schemaOid = get_namespace_oid(control->schema, true);
+ if (OidIsValid(schemaOid))
+ {
+ return schemaOid;
+ }
+ return CreateSchemaForExtension(control->schema);
+ }
+
+ if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+
+ {
+ /*
+ * Neither user nor author of the extension specified schema; use the
+ * current default creation namespace, which is the first explicit
+ * entry in the search_path.
+ */
+ List *search_path = fetch_search_path(false);
+ Oid schemaOid;
+
+ if (search_path == NIL) /* nothing valid in search_path? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ schemaOid = linitial_oid(search_path);
+ *schemaName = get_namespace_name(schemaOid);
+ if (*schemaName == NULL) /* recently-deleted namespace? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+
+ list_free(search_path);
+ return schemaOid;
+ }
+}
+
/*
* CREATE EXTENSION worker
*
@@ -1849,6 +2035,9 @@ CreateExtensionInternal(char *extensionName,
Oid extensionOid;
ObjectAddress address;
ListCell *lc;
+ bool switch_to_superuser = false;
+ Oid save_userid = 0;
+ int save_sec_context = 0;
/*
* Read the primary control file. Note we assume that it does not contain
@@ -1917,77 +2106,25 @@ CreateExtensionInternal(char *extensionName,
control = read_extension_aux_control_file(pcontrol, versionName);
/*
- * Determine the target schema to install the extension into
+ * For trusted extensions with owned schemas, we need to create the schema
+ * as superuser to ensure proper ownership.
*/
- if (schemaName)
- {
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
- }
-
- if (control->schema != NULL)
+ if (control->owned_schema)
{
- /*
- * The extension is not relocatable and the author gave us a schema
- * for it.
- *
- * Unless CASCADE parameter was given, it's an error to give a schema
- * different from control->schema if control->schema is specified.
- */
- if (schemaName && strcmp(control->schema, schemaName) != 0 &&
- !cascade)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("extension \"%s\" must be installed in schema \"%s\"",
- control->name,
- control->schema)));
-
- /* Always use the schema from control file for current extension. */
- schemaName = control->schema;
-
- /* Find or create the schema in case it does not exist. */
- schemaOid = get_namespace_oid(schemaName, true);
-
- if (!OidIsValid(schemaOid))
+ switch_to_superuser = check_extension_superuser_requirements(control, NULL);
+ if (switch_to_superuser)
{
- CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
-
- csstmt->schemaname = schemaName;
- csstmt->authrole = NULL; /* will be created by current user */
- csstmt->schemaElts = NIL;
- csstmt->if_not_exists = false;
- CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
- -1, -1);
-
- /*
- * CreateSchemaCommand includes CommandCounterIncrement, so new
- * schema is now visible.
- */
- schemaOid = get_namespace_oid(schemaName, false);
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
}
}
- else if (!OidIsValid(schemaOid))
- {
- /*
- * Neither user nor author of the extension specified schema; use the
- * current default creation namespace, which is the first explicit
- * entry in the search_path.
- */
- List *search_path = fetch_search_path(false);
- if (search_path == NIL) /* nothing valid in search_path? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
- schemaOid = linitial_oid(search_path);
- schemaName = get_namespace_name(schemaOid);
- if (schemaName == NULL) /* recently-deleted namespace? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
+ schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
- list_free(search_path);
- }
+ /* Restore authentication state after schema creation if needed */
+ if (switch_to_superuser)
+ SetUserIdAndSecContext(save_userid, save_sec_context);
/*
* Make note if a temporary namespace has been accessed in this
@@ -2033,6 +2170,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2238,7 +2376,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2268,6 +2407,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2312,6 +2452,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3246,12 +3397,16 @@ extension_config_remove(Oid extensionoid, Oid tableoid)
/*
* Execute ALTER EXTENSION SET SCHEMA
+ *
+ * For owned schemas, this boils down to changing the name of its schema. For
+ * non-owned schemas this requires moving all the member objects into the new
+ * schema.
*/
ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3264,11 +3419,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3277,22 +3431,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3316,14 +3454,43 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
+ ownedSchema = extForm->extownedschema;
+
/*
- * If the extension is already in the target schema, just silently do
- * nothing.
+ * For non-owned schemas, we should now evaluate if the target schema is a
+ * valid target. For owned schemas, no such checks are needed, because
+ * we'll simply rename the existing schema.
*/
- if (extForm->extnamespace == nspOid)
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3396,6 +3563,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually move any objects for owned schemas because we
+ * simply rename the schema that these objects are already in.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3439,18 +3613,35 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ /* Now actually update the schema of the extension. */
+ if (ownedSchema)
+ {
+ /*
+ * For owned schemas, we simply rename the schema. This means that we
+ * don't need to update the extension its catalog entry, because the
+ * oid of the schema will stay the same.
+ */
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /*
+ * For non-owned schemas, we now have to update the extension's schema
+ * entry, and also update the dependencies.
+ */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index b505a6b4fee..a0dbf119de9 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2c3754d020f..f27490fa825 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1951,6 +1951,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5951,7 +5964,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5965,19 +5978,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6134,6 +6161,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6142,7 +6170,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6158,6 +6193,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6171,6 +6207,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11884,9 +11921,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11998,6 +12035,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4c4b14e5fc7..ef74020d171 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 8ab5e3141d0..bbc79773e6e 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..ea0ef57b7e0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11805,7 +11805,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 7a76bdebcfa..03166b75502 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a3591bf3d2f..0a7cf692214 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,9 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_nosuperuser \
+ test_ext_owned_schema_trusted test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -25,7 +27,14 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema--1.0--1.1.sql \
+ test_ext_owned_schema_nosuperuser--1.0.sql \
+ test_ext_owned_schema_nosuperuser--1.0--1.1.sql \
+ test_ext_owned_schema_trusted--1.0.sql \
+ test_ext_owned_schema_trusted--1.0--1.1.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index fdae52d6ab2..1820e76d746 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -667,3 +668,159 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ function test_ext_owned_schema.owned2()
+ schema test_ext_owned_schema
+(3 rows)
+
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+ count
+-------
+ 0
+(1 row)
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+ERROR: cannot drop extension test_ext_owned_schema because other objects depend on it
+DETAIL: function test_ext_owned_schema.non_ext_func() depends on schema test_ext_owned_schema
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+NOTICE: drop cascades to function test_ext_owned_schema.non_ext_func()
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+ERROR: schema "some_other_name" does not exist
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ schema test_owned_schema_nosuperuser
+(2 rows)
+
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+ nspname | owned_by_current_user
+-------------------------------+-----------------------
+ test_owned_schema_nosuperuser | t
+(1 row)
+
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ function test_owned_schema_nosuperuser.owned2()
+ schema test_owned_schema_nosuperuser
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ schema test_owned_schema_trusted
+(2 rows)
+
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+ nspname | owned_by_bootstrap_superuser
+---------------------------+------------------------------
+ test_owned_schema_trusted | t
+(1 row)
+
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ function test_owned_schema_trusted.owned2()
+ schema test_owned_schema_trusted
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index be9c9ae593f..019e0d2a243 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,17 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema--1.0--1.1.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_nosuperuser--1.0.sql',
+ 'test_ext_owned_schema_nosuperuser--1.0--1.1.sql',
+ 'test_ext_owned_schema_nosuperuser.control',
+ 'test_ext_owned_schema_trusted--1.0.sql',
+ 'test_ext_owned_schema_trusted--1.0--1.1.sql',
+ 'test_ext_owned_schema_trusted.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..700033fc13b 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,78 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index 3d65ce4497a..4db2957318d 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
base-commit: adbad833f3d9e9176e8d7005f15ea6056900227d
--
2.52.0
nocfbot.changes.diff (text/x-patch, 14.9K) download
commit a738745681ab435d91557f05e6c8bdad15f898d9
Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Tue Feb 10 23:23:55 2026 +0100
Add test
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 77bc6a82987..03b3de357bd 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -1243,6 +1243,44 @@ extension_is_trusted(ExtensionControlFile *control)
return false;
}
+/*
+ * Enforce superuser requirements for extension operations.
+ *
+ * Returns true if we should switch to superuser for trusted extensions.
+ * Throws an error if superuser is required but not available.
+ *
+ * This function should only be called after choosing the appropriate
+ * control file (including any secondary control files for updates).
+ */
+static bool
+check_extension_superuser_requirements(ExtensionControlFile *control,
+ const char *from_version)
+{
+ if (control->superuser && !superuser())
+ {
+ if (extension_is_trusted(control))
+ return true;
+ else if (from_version == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to create this extension.")
+ : errhint("Must be superuser to create this extension.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to update extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to update this extension.")
+ : errhint("Must be superuser to update this extension.")));
+ }
+
+ return false;
+}
+
/*
* Execute the appropriate script file for installing or updating the extension
*
@@ -1271,27 +1309,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
* here so that the control flags are correctly associated with the right
* script(s) if they happen to be set in secondary control files.
*/
- if (control->superuser && !superuser())
- {
- if (extension_is_trusted(control))
- switch_to_superuser = true;
- else if (from_version == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to create this extension.")
- : errhint("Must be superuser to create this extension.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to update extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to update this extension.")
- : errhint("Must be superuser to update this extension.")));
- }
+ switch_to_superuser = check_extension_superuser_requirements(control, from_version);
filename = get_extension_script_filename(control, from_version, version);
@@ -1854,7 +1872,7 @@ CreateSchemaForExtension(char *schemaName)
}
/*
- * Create a owned schema with the given name, as part of CREATE EXTENSION, and
+ * Create an owned schema with the given name, as part of CREATE EXTENSION, and
* fails if the schema already exist.
*/
static Oid
@@ -2017,6 +2035,9 @@ CreateExtensionInternal(char *extensionName,
Oid extensionOid;
ObjectAddress address;
ListCell *lc;
+ bool switch_to_superuser = false;
+ Oid save_userid = 0;
+ int save_sec_context = 0;
/*
* Read the primary control file. Note we assume that it does not contain
@@ -2084,8 +2105,27 @@ CreateExtensionInternal(char *extensionName,
*/
control = read_extension_aux_control_file(pcontrol, versionName);
+ /*
+ * For trusted extensions with owned schemas, we need to create the schema
+ * as superuser to ensure proper ownership.
+ */
+ if (control->owned_schema)
+ {
+ switch_to_superuser = check_extension_superuser_requirements(control, NULL);
+ if (switch_to_superuser)
+ {
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ }
+ }
+
schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
+ /* Restore authentication state after schema creation if needed */
+ if (switch_to_superuser)
+ SetUserIdAndSecContext(save_userid, save_sec_context);
+
/*
* Make note if a temporary namespace has been accessed in this
* transaction.
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a6594c19d7e..0a7cf692214 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -10,7 +10,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_evttrig \
test_ext_set_schema \
test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
- test_ext_owned_schema test_ext_owned_schema_relocatable
+ test_ext_owned_schema test_ext_owned_schema_nosuperuser \
+ test_ext_owned_schema_trusted test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -28,6 +29,11 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_req_schema2--1.0.sql \
test_ext_req_schema3--1.0.sql \
test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema--1.0--1.1.sql \
+ test_ext_owned_schema_nosuperuser--1.0.sql \
+ test_ext_owned_schema_nosuperuser--1.0--1.1.sql \
+ test_ext_owned_schema_trusted--1.0.sql \
+ test_ext_owned_schema_trusted--1.0--1.1.sql \
test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index b82b3979a90..1820e76d746 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -690,7 +690,36 @@ Objects in extension "test_ext_owned_schema"
schema test_ext_owned_schema
(2 rows)
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ function test_ext_owned_schema.owned2()
+ schema test_ext_owned_schema
+(3 rows)
+
+-- Verify that the owned schema is dropped together with the extension
DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+ count
+-------
+ 0
+(1 row)
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+ERROR: cannot drop extension test_ext_owned_schema because other objects depend on it
+DETAIL: function test_ext_owned_schema.non_ext_func() depends on schema test_ext_owned_schema
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+NOTICE: drop cascades to function test_ext_owned_schema.non_ext_func()
CREATE SCHEMA already_existing;
-- Fails for an already existing schema to be provided
CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
@@ -721,3 +750,77 @@ Objects in extension "test_ext_owned_schema_relocatable"
(2 rows)
DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+ERROR: schema "some_other_name" does not exist
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ schema test_owned_schema_nosuperuser
+(2 rows)
+
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+ nspname | owned_by_current_user
+-------------------------------+-----------------------
+ test_owned_schema_nosuperuser | t
+(1 row)
+
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ function test_owned_schema_nosuperuser.owned2()
+ schema test_owned_schema_nosuperuser
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ schema test_owned_schema_trusted
+(2 rows)
+
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+ nspname | owned_by_bootstrap_superuser
+---------------------------+------------------------------
+ test_owned_schema_trusted | t
+(1 row)
+
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ function test_owned_schema_trusted.owned2()
+ schema test_owned_schema_trusted
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 3e25c4466ec..019e0d2a243 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -45,7 +45,14 @@ test_install_data += files(
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema--1.0--1.1.sql',
'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_nosuperuser--1.0.sql',
+ 'test_ext_owned_schema_nosuperuser--1.0--1.1.sql',
+ 'test_ext_owned_schema_nosuperuser.control',
+ 'test_ext_owned_schema_trusted--1.0.sql',
+ 'test_ext_owned_schema_trusted--1.0--1.1.sql',
+ 'test_ext_owned_schema_trusted.control',
'test_ext_owned_schema_relocatable--1.0.sql',
'test_ext_owned_schema_relocatable.control',
)
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index a97866d00ea..700033fc13b 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -316,7 +316,20 @@ CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
DROP SCHEMA test_ext_owned_schema;
CREATE EXTENSION test_ext_owned_schema;
\dx+ test_ext_owned_schema;
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
DROP EXTENSION test_ext_owned_schema;
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
CREATE SCHEMA already_existing;
-- Fails for an already existing schema to be provided
@@ -330,3 +343,38 @@ ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
\dx+ test_ext_owned_schema_relocatable
DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-02-10 23:32 Jelte Fennema-Nio <postgres@jeltef.nl>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2026-02-10 23:32 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; Robert Haas <robertmhaas@gmail.com>; +Cc: Julien Rouhaud <rjuju123@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Wed Feb 11, 2026 at 12:19 AM CET, Jelte Fennema-Nio wrote:
> Added now in v8
Ugh, I didn't include some of the new files in the attached patch. Fixed
now with v9.
Attachments: download all (2 files, .tar.gz)
v9-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 55.9K) download
From 3e07020b4d64a09cf97e067736d173ca9b255033 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v9] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++
doc/src/sgml/ref/create_extension.sgml | 6 +-
src/backend/commands/extension.c | 429 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 +-
src/bin/pg_dump/pg_dump.c | 54 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 13 +-
.../expected/test_extensions.out | 157 +++++++
src/test/modules/test_extensions/meson.build | 11 +
.../test_extensions/sql/test_extensions.sql | 75 +++
.../test_ext_owned_schema--1.0--1.1.sql | 3 +
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...ext_owned_schema_nosuperuser--1.0--1.1.sql | 5 +
...test_ext_owned_schema_nosuperuser--1.0.sql | 3 +
.../test_ext_owned_schema_nosuperuser.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
...est_ext_owned_schema_trusted--1.0--1.1.sql | 3 +
.../test_ext_owned_schema_trusted--1.0.sql | 3 +
.../test_ext_owned_schema_trusted.control | 5 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++
25 files changed, 761 insertions(+), 143 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..ab125d56263 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,11 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. Whether this schema should already exist or not depends on
+ the value of <literal>owned_schema</literal> in the extensions control
+ file: If it's <literal>true</literal>, the schema must
+ <emphasis>not</emphasis> exist; if it's <literal>false</literal> it
+ must exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 81f24615d51..03b3de357bd 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -94,6 +94,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -790,6 +792,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1233,6 +1243,44 @@ extension_is_trusted(ExtensionControlFile *control)
return false;
}
+/*
+ * Enforce superuser requirements for extension operations.
+ *
+ * Returns true if we should switch to superuser for trusted extensions.
+ * Throws an error if superuser is required but not available.
+ *
+ * This function should only be called after choosing the appropriate
+ * control file (including any secondary control files for updates).
+ */
+static bool
+check_extension_superuser_requirements(ExtensionControlFile *control,
+ const char *from_version)
+{
+ if (control->superuser && !superuser())
+ {
+ if (extension_is_trusted(control))
+ return true;
+ else if (from_version == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to create this extension.")
+ : errhint("Must be superuser to create this extension.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to update extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to update this extension.")
+ : errhint("Must be superuser to update this extension.")));
+ }
+
+ return false;
+}
+
/*
* Execute the appropriate script file for installing or updating the extension
*
@@ -1261,27 +1309,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
* here so that the control flags are correctly associated with the right
* script(s) if they happen to be set in secondary control files.
*/
- if (control->superuser && !superuser())
- {
- if (extension_is_trusted(control))
- switch_to_superuser = true;
- else if (from_version == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to create this extension.")
- : errhint("Must be superuser to create this extension.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to update extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to update this extension.")
- : errhint("Must be superuser to update this extension.")));
- }
+ switch_to_superuser = check_extension_superuser_requirements(control, from_version);
filename = get_extension_script_filename(control, from_version, version);
@@ -1820,6 +1848,164 @@ find_install_path(List *evi_list, ExtensionVersionInfo *evi_target,
return evi_start;
}
+/*
+ * Create a schema with the given name, as part of CREATE EXTENSION.
+ */
+static Oid
+CreateSchemaForExtension(char *schemaName)
+{
+
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ /*
+ * CreateSchemaCommand includes CommandCounterIncrement, so new schema is
+ * now visible.
+ */
+ return get_namespace_oid(schemaName, false);
+}
+
+/*
+ * Create an owned schema with the given name, as part of CREATE EXTENSION, and
+ * fails if the schema already exist.
+ */
+static Oid
+CreateOwnedSchemaForExtension(char *schemaName)
+{
+ /* Find or create the schema in case it does not exist. */
+ Oid schemaOid = get_namespace_oid(schemaName, true);
+
+ if (OidIsValid(schemaOid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
+ return CreateSchemaForExtension(schemaName);
+}
+
+/*
+ * Gets or creates the schema than an extension should be created in.
+ *
+ * Returns the OID of the schema and updates schemaName to the name of the schema.
+ */
+static Oid
+GetOrCreateSchemaForExtension(char **schemaName, ExtensionControlFile *control, bool cascade)
+{
+ /*
+ * The simplest case is when the user provides a schema name.
+ */
+ if (*schemaName)
+ {
+ Oid schemaOid;
+
+ if (!control->owned_schema)
+ {
+ /*
+ * For non-owned schemas, this schema must already exist. We want
+ * to check this now, so it fails even if we bail out of this
+ * block due to the CASCADE logic.
+ */
+ schemaOid = get_namespace_oid(*schemaName, false);
+ }
+
+ if (control->schema && strcmp(control->schema, *schemaName) != 0)
+ {
+ /*
+ * The extension is not relocatable and the author gave us a
+ * schema for it.
+ *
+ * Unless CASCADE parameter was given, it's an error to give a
+ * schema different from control->schema if control->schema is
+ * specified.
+ */
+ if (!cascade)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("extension \"%s\" must be installed in schema \"%s\"",
+ control->name,
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
+ }
+
+ /*
+ * If the schema mismatches and CASCADE was given, we pretend the
+ * user did not specify a schema and use the normal logic below to
+ * get or create the schema from the control file.
+ */
+ }
+ else if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(*schemaName);
+ }
+ else
+ {
+ return schemaOid;
+ }
+ }
+
+ if (control->schema)
+ {
+ Oid schemaOid;
+
+ *schemaName = control->schema;
+
+ if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(control->schema);
+ }
+
+ /* Find or create the schema in case it does not exist. */
+ schemaOid = get_namespace_oid(control->schema, true);
+ if (OidIsValid(schemaOid))
+ {
+ return schemaOid;
+ }
+ return CreateSchemaForExtension(control->schema);
+ }
+
+ if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+
+ {
+ /*
+ * Neither user nor author of the extension specified schema; use the
+ * current default creation namespace, which is the first explicit
+ * entry in the search_path.
+ */
+ List *search_path = fetch_search_path(false);
+ Oid schemaOid;
+
+ if (search_path == NIL) /* nothing valid in search_path? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ schemaOid = linitial_oid(search_path);
+ *schemaName = get_namespace_name(schemaOid);
+ if (*schemaName == NULL) /* recently-deleted namespace? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+
+ list_free(search_path);
+ return schemaOid;
+ }
+}
+
/*
* CREATE EXTENSION worker
*
@@ -1849,6 +2035,9 @@ CreateExtensionInternal(char *extensionName,
Oid extensionOid;
ObjectAddress address;
ListCell *lc;
+ bool switch_to_superuser = false;
+ Oid save_userid = 0;
+ int save_sec_context = 0;
/*
* Read the primary control file. Note we assume that it does not contain
@@ -1917,77 +2106,25 @@ CreateExtensionInternal(char *extensionName,
control = read_extension_aux_control_file(pcontrol, versionName);
/*
- * Determine the target schema to install the extension into
+ * For trusted extensions with owned schemas, we need to create the schema
+ * as superuser to ensure proper ownership.
*/
- if (schemaName)
- {
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
- }
-
- if (control->schema != NULL)
+ if (control->owned_schema)
{
- /*
- * The extension is not relocatable and the author gave us a schema
- * for it.
- *
- * Unless CASCADE parameter was given, it's an error to give a schema
- * different from control->schema if control->schema is specified.
- */
- if (schemaName && strcmp(control->schema, schemaName) != 0 &&
- !cascade)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("extension \"%s\" must be installed in schema \"%s\"",
- control->name,
- control->schema)));
-
- /* Always use the schema from control file for current extension. */
- schemaName = control->schema;
-
- /* Find or create the schema in case it does not exist. */
- schemaOid = get_namespace_oid(schemaName, true);
-
- if (!OidIsValid(schemaOid))
+ switch_to_superuser = check_extension_superuser_requirements(control, NULL);
+ if (switch_to_superuser)
{
- CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
-
- csstmt->schemaname = schemaName;
- csstmt->authrole = NULL; /* will be created by current user */
- csstmt->schemaElts = NIL;
- csstmt->if_not_exists = false;
- CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
- -1, -1);
-
- /*
- * CreateSchemaCommand includes CommandCounterIncrement, so new
- * schema is now visible.
- */
- schemaOid = get_namespace_oid(schemaName, false);
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
}
}
- else if (!OidIsValid(schemaOid))
- {
- /*
- * Neither user nor author of the extension specified schema; use the
- * current default creation namespace, which is the first explicit
- * entry in the search_path.
- */
- List *search_path = fetch_search_path(false);
- if (search_path == NIL) /* nothing valid in search_path? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
- schemaOid = linitial_oid(search_path);
- schemaName = get_namespace_name(schemaOid);
- if (schemaName == NULL) /* recently-deleted namespace? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
+ schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
- list_free(search_path);
- }
+ /* Restore authentication state after schema creation if needed */
+ if (switch_to_superuser)
+ SetUserIdAndSecContext(save_userid, save_sec_context);
/*
* Make note if a temporary namespace has been accessed in this
@@ -2033,6 +2170,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2238,7 +2376,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2268,6 +2407,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2312,6 +2452,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3246,12 +3397,16 @@ extension_config_remove(Oid extensionoid, Oid tableoid)
/*
* Execute ALTER EXTENSION SET SCHEMA
+ *
+ * For owned schemas, this boils down to changing the name of its schema. For
+ * non-owned schemas this requires moving all the member objects into the new
+ * schema.
*/
ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3264,11 +3419,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3277,22 +3431,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3316,14 +3454,43 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
+ ownedSchema = extForm->extownedschema;
+
/*
- * If the extension is already in the target schema, just silently do
- * nothing.
+ * For non-owned schemas, we should now evaluate if the target schema is a
+ * valid target. For owned schemas, no such checks are needed, because
+ * we'll simply rename the existing schema.
*/
- if (extForm->extnamespace == nspOid)
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3396,6 +3563,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually move any objects for owned schemas because we
+ * simply rename the schema that these objects are already in.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3439,18 +3613,35 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ /* Now actually update the schema of the extension. */
+ if (ownedSchema)
+ {
+ /*
+ * For owned schemas, we simply rename the schema. This means that we
+ * don't need to update the extension its catalog entry, because the
+ * oid of the schema will stay the same.
+ */
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /*
+ * For non-owned schemas, we now have to update the extension's schema
+ * entry, and also update the dependencies.
+ */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index b505a6b4fee..a0dbf119de9 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2c3754d020f..f27490fa825 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1951,6 +1951,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5951,7 +5964,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -5965,19 +5978,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6134,6 +6161,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6142,7 +6170,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6158,6 +6193,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6171,6 +6207,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11884,9 +11921,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11998,6 +12035,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4c4b14e5fc7..ef74020d171 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 8ab5e3141d0..bbc79773e6e 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -34,6 +34,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..ea0ef57b7e0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11805,7 +11805,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 7a76bdebcfa..03166b75502 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a3591bf3d2f..0a7cf692214 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,9 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_nosuperuser \
+ test_ext_owned_schema_trusted test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -25,7 +27,14 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema--1.0--1.1.sql \
+ test_ext_owned_schema_nosuperuser--1.0.sql \
+ test_ext_owned_schema_nosuperuser--1.0--1.1.sql \
+ test_ext_owned_schema_trusted--1.0.sql \
+ test_ext_owned_schema_trusted--1.0--1.1.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index fdae52d6ab2..1820e76d746 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -667,3 +668,159 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ function test_ext_owned_schema.owned2()
+ schema test_ext_owned_schema
+(3 rows)
+
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+ count
+-------
+ 0
+(1 row)
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+ERROR: cannot drop extension test_ext_owned_schema because other objects depend on it
+DETAIL: function test_ext_owned_schema.non_ext_func() depends on schema test_ext_owned_schema
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+NOTICE: drop cascades to function test_ext_owned_schema.non_ext_func()
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+ERROR: schema "some_other_name" does not exist
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ schema test_owned_schema_nosuperuser
+(2 rows)
+
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+ nspname | owned_by_current_user
+-------------------------------+-----------------------
+ test_owned_schema_nosuperuser | t
+(1 row)
+
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ function test_owned_schema_nosuperuser.owned2()
+ schema test_owned_schema_nosuperuser
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ schema test_owned_schema_trusted
+(2 rows)
+
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+ nspname | owned_by_bootstrap_superuser
+---------------------------+------------------------------
+ test_owned_schema_trusted | t
+(1 row)
+
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ function test_owned_schema_trusted.owned2()
+ schema test_owned_schema_trusted
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index be9c9ae593f..019e0d2a243 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,17 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema--1.0--1.1.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_nosuperuser--1.0.sql',
+ 'test_ext_owned_schema_nosuperuser--1.0--1.1.sql',
+ 'test_ext_owned_schema_nosuperuser.control',
+ 'test_ext_owned_schema_trusted--1.0.sql',
+ 'test_ext_owned_schema_trusted--1.0--1.1.sql',
+ 'test_ext_owned_schema_trusted.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..700033fc13b 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,78 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
new file mode 100644
index 00000000000..e9ba788d751
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
@@ -0,0 +1,5 @@
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1'" to load this file. \quit
+
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
new file mode 100644
index 00000000000..6c22c777519
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (superuser=false)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
new file mode 100644
index 00000000000..caad0960996
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (superuser=false)'
+default_version = '1.0'
+superuser = false
+owned_schema = true
+schema = 'test_owned_schema_nosuperuser'
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
new file mode 100644
index 00000000000..4e5410c184d
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (trusted=true)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
new file mode 100644
index 00000000000..277b3f1603b
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (trusted=true)'
+default_version = '1.0'
+trusted = true
+owned_schema = true
+schema = 'test_owned_schema_trusted'
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index 3d65ce4497a..4db2957318d 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
base-commit: adbad833f3d9e9176e8d7005f15ea6056900227d
--
2.52.0
nocfbot.changes.diff (text/x-patch, 18.1K) download
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 77bc6a82987..03b3de357bd 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -1243,6 +1243,44 @@ extension_is_trusted(ExtensionControlFile *control)
return false;
}
+/*
+ * Enforce superuser requirements for extension operations.
+ *
+ * Returns true if we should switch to superuser for trusted extensions.
+ * Throws an error if superuser is required but not available.
+ *
+ * This function should only be called after choosing the appropriate
+ * control file (including any secondary control files for updates).
+ */
+static bool
+check_extension_superuser_requirements(ExtensionControlFile *control,
+ const char *from_version)
+{
+ if (control->superuser && !superuser())
+ {
+ if (extension_is_trusted(control))
+ return true;
+ else if (from_version == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to create this extension.")
+ : errhint("Must be superuser to create this extension.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to update extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to update this extension.")
+ : errhint("Must be superuser to update this extension.")));
+ }
+
+ return false;
+}
+
/*
* Execute the appropriate script file for installing or updating the extension
*
@@ -1271,27 +1309,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
* here so that the control flags are correctly associated with the right
* script(s) if they happen to be set in secondary control files.
*/
- if (control->superuser && !superuser())
- {
- if (extension_is_trusted(control))
- switch_to_superuser = true;
- else if (from_version == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to create this extension.")
- : errhint("Must be superuser to create this extension.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to update extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to update this extension.")
- : errhint("Must be superuser to update this extension.")));
- }
+ switch_to_superuser = check_extension_superuser_requirements(control, from_version);
filename = get_extension_script_filename(control, from_version, version);
@@ -1854,7 +1872,7 @@ CreateSchemaForExtension(char *schemaName)
}
/*
- * Create a owned schema with the given name, as part of CREATE EXTENSION, and
+ * Create an owned schema with the given name, as part of CREATE EXTENSION, and
* fails if the schema already exist.
*/
static Oid
@@ -2017,6 +2035,9 @@ CreateExtensionInternal(char *extensionName,
Oid extensionOid;
ObjectAddress address;
ListCell *lc;
+ bool switch_to_superuser = false;
+ Oid save_userid = 0;
+ int save_sec_context = 0;
/*
* Read the primary control file. Note we assume that it does not contain
@@ -2084,8 +2105,27 @@ CreateExtensionInternal(char *extensionName,
*/
control = read_extension_aux_control_file(pcontrol, versionName);
+ /*
+ * For trusted extensions with owned schemas, we need to create the schema
+ * as superuser to ensure proper ownership.
+ */
+ if (control->owned_schema)
+ {
+ switch_to_superuser = check_extension_superuser_requirements(control, NULL);
+ if (switch_to_superuser)
+ {
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+ }
+ }
+
schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
+ /* Restore authentication state after schema creation if needed */
+ if (switch_to_superuser)
+ SetUserIdAndSecContext(save_userid, save_sec_context);
+
/*
* Make note if a temporary namespace has been accessed in this
* transaction.
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a6594c19d7e..0a7cf692214 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -10,7 +10,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_evttrig \
test_ext_set_schema \
test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
- test_ext_owned_schema test_ext_owned_schema_relocatable
+ test_ext_owned_schema test_ext_owned_schema_nosuperuser \
+ test_ext_owned_schema_trusted test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -28,6 +29,11 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_req_schema2--1.0.sql \
test_ext_req_schema3--1.0.sql \
test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema--1.0--1.1.sql \
+ test_ext_owned_schema_nosuperuser--1.0.sql \
+ test_ext_owned_schema_nosuperuser--1.0--1.1.sql \
+ test_ext_owned_schema_trusted--1.0.sql \
+ test_ext_owned_schema_trusted--1.0--1.1.sql \
test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index b82b3979a90..1820e76d746 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -690,7 +690,36 @@ Objects in extension "test_ext_owned_schema"
schema test_ext_owned_schema
(2 rows)
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ function test_ext_owned_schema.owned2()
+ schema test_ext_owned_schema
+(3 rows)
+
+-- Verify that the owned schema is dropped together with the extension
DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+ count
+-------
+ 0
+(1 row)
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+ERROR: cannot drop extension test_ext_owned_schema because other objects depend on it
+DETAIL: function test_ext_owned_schema.non_ext_func() depends on schema test_ext_owned_schema
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+NOTICE: drop cascades to function test_ext_owned_schema.non_ext_func()
CREATE SCHEMA already_existing;
-- Fails for an already existing schema to be provided
CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
@@ -721,3 +750,77 @@ Objects in extension "test_ext_owned_schema_relocatable"
(2 rows)
DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+ERROR: schema "some_other_name" does not exist
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ schema test_owned_schema_nosuperuser
+(2 rows)
+
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+ nspname | owned_by_current_user
+-------------------------------+-----------------------
+ test_owned_schema_nosuperuser | t
+(1 row)
+
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ function test_owned_schema_nosuperuser.owned2()
+ schema test_owned_schema_nosuperuser
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ schema test_owned_schema_trusted
+(2 rows)
+
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+ nspname | owned_by_bootstrap_superuser
+---------------------------+------------------------------
+ test_owned_schema_trusted | t
+(1 row)
+
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ function test_owned_schema_trusted.owned2()
+ schema test_owned_schema_trusted
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 3e25c4466ec..019e0d2a243 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -45,7 +45,14 @@ test_install_data += files(
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema--1.0--1.1.sql',
'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_nosuperuser--1.0.sql',
+ 'test_ext_owned_schema_nosuperuser--1.0--1.1.sql',
+ 'test_ext_owned_schema_nosuperuser.control',
+ 'test_ext_owned_schema_trusted--1.0.sql',
+ 'test_ext_owned_schema_trusted--1.0--1.1.sql',
+ 'test_ext_owned_schema_trusted.control',
'test_ext_owned_schema_relocatable--1.0.sql',
'test_ext_owned_schema_relocatable.control',
)
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index a97866d00ea..700033fc13b 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -316,7 +316,20 @@ CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
DROP SCHEMA test_ext_owned_schema;
CREATE EXTENSION test_ext_owned_schema;
\dx+ test_ext_owned_schema;
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
DROP EXTENSION test_ext_owned_schema;
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
CREATE SCHEMA already_existing;
-- Fails for an already existing schema to be provided
@@ -330,3 +343,38 @@ ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
\dx+ test_ext_owned_schema_relocatable
DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+
+-- Test owned_schema + superuser=false extension
+CREATE USER test_ext_user;
+GRANT CREATE ON DATABASE regression_test_extensions TO test_ext_user;
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression_test_extensions FROM test_ext_user;
+DROP USER test_ext_user;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
new file mode 100644
index 00000000000..e9ba788d751
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
@@ -0,0 +1,5 @@
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1'" to load this file. \quit
+
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
new file mode 100644
index 00000000000..6c22c777519
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (superuser=false)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
new file mode 100644
index 00000000000..caad0960996
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (superuser=false)'
+default_version = '1.0'
+superuser = false
+owned_schema = true
+schema = 'test_owned_schema_nosuperuser'
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
new file mode 100644
index 00000000000..4e5410c184d
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (trusted=true)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
new file mode 100644
index 00000000000..277b3f1603b
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (trusted=true)'
+default_version = '1.0'
+trusted = true
+owned_schema = true
+schema = 'test_owned_schema_trusted'
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-03-15 20:19 Jelte Fennema-Nio <postgres@jeltef.nl>
parent: Jelte Fennema-Nio <postgres@jeltef.nl>
0 siblings, 0 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2026-03-15 20:19 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; Robert Haas <robertmhaas@gmail.com>; +Cc: Julien Rouhaud <rjuju123@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
On Wed Feb 11, 2026 at 12:32 AM CET, Jelte Fennema-Nio wrote:
> Ugh, I didn't include some of the new files in the attached patch. Fixed
> now with v9.
Fixed tests that were failing in CI.
Attachments:
v10-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 56.4K) download
From 22407040953a8a3fb1b6c732370cef536d960243 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v10] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++
doc/src/sgml/ref/create_extension.sgml | 6 +-
src/backend/commands/extension.c | 429 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 45 +-
src/bin/pg_dump/pg_dump.c | 54 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 13 +-
.../expected/test_extensions.out | 159 +++++++
src/test/modules/test_extensions/meson.build | 11 +
.../test_extensions/sql/test_extensions.sql | 77 ++++
.../test_ext_owned_schema--1.0--1.1.sql | 3 +
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...ext_owned_schema_nosuperuser--1.0--1.1.sql | 5 +
...test_ext_owned_schema_nosuperuser--1.0.sql | 3 +
.../test_ext_owned_schema_nosuperuser.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
...est_ext_owned_schema_trusted--1.0--1.1.sql | 3 +
.../test_ext_owned_schema_trusted--1.0.sql | 3 +
.../test_ext_owned_schema_trusted.control | 5 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++
25 files changed, 765 insertions(+), 143 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..ab125d56263 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,11 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. Whether this schema should already exist or not depends on
+ the value of <literal>owned_schema</literal> in the extensions control
+ file: If it's <literal>true</literal>, the schema must
+ <emphasis>not</emphasis> exist; if it's <literal>false</literal> it
+ must exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 963618a64c4..5e88c1b0e12 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -94,6 +94,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -791,6 +793,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1234,6 +1244,44 @@ extension_is_trusted(ExtensionControlFile *control)
return false;
}
+/*
+ * Enforce superuser requirements for extension operations.
+ *
+ * Returns true if we should switch to superuser for trusted extensions.
+ * Throws an error if superuser is required but not available.
+ *
+ * This function should only be called after choosing the appropriate
+ * control file (including any secondary control files for updates).
+ */
+static bool
+check_extension_superuser_requirements(ExtensionControlFile *control,
+ const char *from_version)
+{
+ if (control->superuser && !superuser())
+ {
+ if (extension_is_trusted(control))
+ return true;
+ else if (from_version == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to create this extension.")
+ : errhint("Must be superuser to create this extension.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to update extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to update this extension.")
+ : errhint("Must be superuser to update this extension.")));
+ }
+
+ return false;
+}
+
/*
* Execute the appropriate script file for installing or updating the extension
*
@@ -1262,27 +1310,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
* here so that the control flags are correctly associated with the right
* script(s) if they happen to be set in secondary control files.
*/
- if (control->superuser && !superuser())
- {
- if (extension_is_trusted(control))
- switch_to_superuser = true;
- else if (from_version == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to create this extension.")
- : errhint("Must be superuser to create this extension.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to update extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to update this extension.")
- : errhint("Must be superuser to update this extension.")));
- }
+ switch_to_superuser = check_extension_superuser_requirements(control, from_version);
filename = get_extension_script_filename(control, from_version, version);
@@ -1821,6 +1849,164 @@ find_install_path(List *evi_list, ExtensionVersionInfo *evi_target,
return evi_start;
}
+/*
+ * Create a schema with the given name, as part of CREATE EXTENSION.
+ */
+static Oid
+CreateSchemaForExtension(char *schemaName)
+{
+
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ /*
+ * CreateSchemaCommand includes CommandCounterIncrement, so new schema is
+ * now visible.
+ */
+ return get_namespace_oid(schemaName, false);
+}
+
+/*
+ * Create an owned schema with the given name, as part of CREATE EXTENSION, and
+ * fails if the schema already exist.
+ */
+static Oid
+CreateOwnedSchemaForExtension(char *schemaName)
+{
+ /* Find or create the schema in case it does not exist. */
+ Oid schemaOid = get_namespace_oid(schemaName, true);
+
+ if (OidIsValid(schemaOid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
+ return CreateSchemaForExtension(schemaName);
+}
+
+/*
+ * Gets or creates the schema than an extension should be created in.
+ *
+ * Returns the OID of the schema and updates schemaName to the name of the schema.
+ */
+static Oid
+GetOrCreateSchemaForExtension(char **schemaName, ExtensionControlFile *control, bool cascade)
+{
+ /*
+ * The simplest case is when the user provides a schema name.
+ */
+ if (*schemaName)
+ {
+ Oid schemaOid;
+
+ if (!control->owned_schema)
+ {
+ /*
+ * For non-owned schemas, this schema must already exist. We want
+ * to check this now, so it fails even if we bail out of this
+ * block due to the CASCADE logic.
+ */
+ schemaOid = get_namespace_oid(*schemaName, false);
+ }
+
+ if (control->schema && strcmp(control->schema, *schemaName) != 0)
+ {
+ /*
+ * The extension is not relocatable and the author gave us a
+ * schema for it.
+ *
+ * Unless CASCADE parameter was given, it's an error to give a
+ * schema different from control->schema if control->schema is
+ * specified.
+ */
+ if (!cascade)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("extension \"%s\" must be installed in schema \"%s\"",
+ control->name,
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
+ }
+
+ /*
+ * If the schema mismatches and CASCADE was given, we pretend the
+ * user did not specify a schema and use the normal logic below to
+ * get or create the schema from the control file.
+ */
+ }
+ else if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(*schemaName);
+ }
+ else
+ {
+ return schemaOid;
+ }
+ }
+
+ if (control->schema)
+ {
+ Oid schemaOid;
+
+ *schemaName = control->schema;
+
+ if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(control->schema);
+ }
+
+ /* Find or create the schema in case it does not exist. */
+ schemaOid = get_namespace_oid(control->schema, true);
+ if (OidIsValid(schemaOid))
+ {
+ return schemaOid;
+ }
+ return CreateSchemaForExtension(control->schema);
+ }
+
+ if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+
+ {
+ /*
+ * Neither user nor author of the extension specified schema; use the
+ * current default creation namespace, which is the first explicit
+ * entry in the search_path.
+ */
+ List *search_path = fetch_search_path(false);
+ Oid schemaOid;
+
+ if (search_path == NIL) /* nothing valid in search_path? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ schemaOid = linitial_oid(search_path);
+ *schemaName = get_namespace_name(schemaOid);
+ if (*schemaName == NULL) /* recently-deleted namespace? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+
+ list_free(search_path);
+ return schemaOid;
+ }
+}
+
/*
* CREATE EXTENSION worker
*
@@ -1850,6 +2036,9 @@ CreateExtensionInternal(char *extensionName,
Oid extensionOid;
ObjectAddress address;
ListCell *lc;
+ bool switch_to_superuser = false;
+ Oid save_userid = 0;
+ int save_sec_context = 0;
/*
* Read the primary control file. Note we assume that it does not contain
@@ -1918,77 +2107,25 @@ CreateExtensionInternal(char *extensionName,
control = read_extension_aux_control_file(pcontrol, versionName);
/*
- * Determine the target schema to install the extension into
+ * For trusted extensions with owned schemas, we need to create the schema
+ * as superuser to ensure proper ownership.
*/
- if (schemaName)
- {
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
- }
-
- if (control->schema != NULL)
+ if (control->owned_schema)
{
- /*
- * The extension is not relocatable and the author gave us a schema
- * for it.
- *
- * Unless CASCADE parameter was given, it's an error to give a schema
- * different from control->schema if control->schema is specified.
- */
- if (schemaName && strcmp(control->schema, schemaName) != 0 &&
- !cascade)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("extension \"%s\" must be installed in schema \"%s\"",
- control->name,
- control->schema)));
-
- /* Always use the schema from control file for current extension. */
- schemaName = control->schema;
-
- /* Find or create the schema in case it does not exist. */
- schemaOid = get_namespace_oid(schemaName, true);
-
- if (!OidIsValid(schemaOid))
+ switch_to_superuser = check_extension_superuser_requirements(control, NULL);
+ if (switch_to_superuser)
{
- CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
-
- csstmt->schemaname = schemaName;
- csstmt->authrole = NULL; /* will be created by current user */
- csstmt->schemaElts = NIL;
- csstmt->if_not_exists = false;
- CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
- -1, -1);
-
- /*
- * CreateSchemaCommand includes CommandCounterIncrement, so new
- * schema is now visible.
- */
- schemaOid = get_namespace_oid(schemaName, false);
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
}
}
- else if (!OidIsValid(schemaOid))
- {
- /*
- * Neither user nor author of the extension specified schema; use the
- * current default creation namespace, which is the first explicit
- * entry in the search_path.
- */
- List *search_path = fetch_search_path(false);
- if (search_path == NIL) /* nothing valid in search_path? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
- schemaOid = linitial_oid(search_path);
- schemaName = get_namespace_name(schemaOid);
- if (schemaName == NULL) /* recently-deleted namespace? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
+ schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
- list_free(search_path);
- }
+ /* Restore authentication state after schema creation if needed */
+ if (switch_to_superuser)
+ SetUserIdAndSecContext(save_userid, save_sec_context);
/*
* Make note if a temporary namespace has been accessed in this
@@ -2034,6 +2171,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2239,7 +2377,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2269,6 +2408,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2313,6 +2453,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3247,12 +3398,16 @@ extension_config_remove(Oid extensionoid, Oid tableoid)
/*
* Execute ALTER EXTENSION SET SCHEMA
+ *
+ * For owned schemas, this boils down to changing the name of its schema. For
+ * non-owned schemas this requires moving all the member objects into the new
+ * schema.
*/
ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3265,11 +3420,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3278,22 +3432,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3317,14 +3455,43 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
+ ownedSchema = extForm->extownedschema;
+
/*
- * If the extension is already in the target schema, just silently do
- * nothing.
+ * For non-owned schemas, we should now evaluate if the target schema is a
+ * valid target. For owned schemas, no such checks are needed, because
+ * we'll simply rename the existing schema.
*/
- if (extForm->extnamespace == nspOid)
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3397,6 +3564,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually move any objects for owned schemas because we
+ * simply rename the schema that these objects are already in.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3440,18 +3614,35 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ /* Now actually update the schema of the extension. */
+ if (ownedSchema)
+ {
+ /*
+ * For owned schemas, we simply rename the schema. This means that we
+ * don't need to update the extension its catalog entry, because the
+ * oid of the schema will stay the same.
+ */
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /*
+ * For non-owned schemas, we now have to update the extension's schema
+ * entry, and also update the dependencies.
+ */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index b505a6b4fee..a0dbf119de9 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
+ -1, -1);
+
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 137161aa5e0..1474e9ec279 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1936,6 +1936,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -6040,7 +6053,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -6054,19 +6067,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6223,6 +6250,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6231,7 +6259,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6247,6 +6282,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6260,6 +6296,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11984,9 +12021,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -12098,6 +12135,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1c11a79083f..6db4f1b9d5d 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 19ec291ad7e..b4ebb94b42c 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -36,6 +36,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 361e2cfffeb..e4dd7cbf085 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11917,7 +11917,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 7a76bdebcfa..03166b75502 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index a3591bf3d2f..0a7cf692214 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -9,7 +9,9 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_nosuperuser \
+ test_ext_owned_schema_trusted test_ext_owned_schema_relocatable
DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \
@@ -25,7 +27,14 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema--1.0--1.1.sql \
+ test_ext_owned_schema_nosuperuser--1.0.sql \
+ test_ext_owned_schema_nosuperuser--1.0--1.1.sql \
+ test_ext_owned_schema_trusted--1.0.sql \
+ test_ext_owned_schema_trusted--1.0--1.1.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index fdae52d6ab2..eb1c38ab126 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -667,3 +668,161 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ function test_ext_owned_schema.owned2()
+ schema test_ext_owned_schema
+(3 rows)
+
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+ count
+-------
+ 0
+(1 row)
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+ERROR: cannot drop extension test_ext_owned_schema because other objects depend on it
+DETAIL: function test_ext_owned_schema.non_ext_func() depends on schema test_ext_owned_schema
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+NOTICE: drop cascades to function test_ext_owned_schema.non_ext_func()
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+ERROR: schema "some_other_name" does not exist
+-- Test owned_schema + superuser=false extension
+CREATE USER regress_test_ext_user;
+-- The database name differs between meson and autoconf builds, so we can't
+-- hardcode it in the GRANT/REVOKE statements.
+DO $$ BEGIN EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_test_ext_user', current_database()); END $$;
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ schema test_owned_schema_nosuperuser
+(2 rows)
+
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+ nspname | owned_by_current_user
+-------------------------------+-----------------------
+ test_owned_schema_nosuperuser | t
+(1 row)
+
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ function test_owned_schema_nosuperuser.owned2()
+ schema test_owned_schema_nosuperuser
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ schema test_owned_schema_trusted
+(2 rows)
+
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by regress_test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+ nspname | owned_by_bootstrap_superuser
+---------------------------+------------------------------
+ test_owned_schema_trusted | t
+(1 row)
+
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ function test_owned_schema_trusted.owned2()
+ schema test_owned_schema_trusted
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+DO $$ BEGIN EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_test_ext_user', current_database()); END $$;
+DROP USER regress_test_ext_user;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index be9c9ae593f..019e0d2a243 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,17 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema--1.0--1.1.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_nosuperuser--1.0.sql',
+ 'test_ext_owned_schema_nosuperuser--1.0--1.1.sql',
+ 'test_ext_owned_schema_nosuperuser.control',
+ 'test_ext_owned_schema_trusted--1.0.sql',
+ 'test_ext_owned_schema_trusted--1.0--1.1.sql',
+ 'test_ext_owned_schema_trusted.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
tests += {
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..59a64c0ddcd 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,80 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+
+-- Test owned_schema + superuser=false extension
+CREATE USER regress_test_ext_user;
+-- The database name differs between meson and autoconf builds, so we can't
+-- hardcode it in the GRANT/REVOKE statements.
+DO $$ BEGIN EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_test_ext_user', current_database()); END $$;
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by regress_test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+DO $$ BEGIN EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_test_ext_user', current_database()); END $$;
+DROP USER regress_test_ext_user;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
new file mode 100644
index 00000000000..e9ba788d751
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
@@ -0,0 +1,5 @@
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1'" to load this file. \quit
+
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
new file mode 100644
index 00000000000..6c22c777519
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (superuser=false)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
new file mode 100644
index 00000000000..caad0960996
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (superuser=false)'
+default_version = '1.0'
+superuser = false
+owned_schema = true
+schema = 'test_owned_schema_nosuperuser'
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
new file mode 100644
index 00000000000..4e5410c184d
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (trusted=true)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
new file mode 100644
index 00000000000..277b3f1603b
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (trusted=true)'
+default_version = '1.0'
+trusted = true
+owned_schema = true
+schema = 'test_owned_schema_trusted'
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index 3d65ce4497a..4db2957318d 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
base-commit: a793677e57bc27c674cb94b230164b2c28f4cbae
--
2.53.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-07-11 10:24 Jelte Fennema-Nio <me@jeltef.nl>
parent: Robert Haas <robertmhaas@gmail.com>
1 sibling, 1 reply; 43+ messages in thread
From: Jelte Fennema-Nio @ 2026-07-11 10:24 UTC (permalink / raw)
To: Robert Haas <robertmhaas@gmail.com>; +Cc: Julien Rouhaud <rjuju123@gmail.com>; Artem Gavrilov <artem.gavrilov@percona.com>; Tomas Vondra <tomas@vondra.me>; David G. Johnston <david.g.johnston@gmail.com>; Jeff Davis <pgsql@j-davis.com>; pgsql-hackers
Rebased again
Attachments:
v11-0001-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 57.2K) download
From c7aa4b3b5bad68f30ff97f63245a834ebca10351 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v11] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++
doc/src/sgml/ref/create_extension.sgml | 6 +-
src/backend/commands/extension.c | 450 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 44 +-
src/bin/pg_dump/pg_dump.c | 54 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 13 +-
.../expected/test_extensions.out | 161 +++++++
src/test/modules/test_extensions/meson.build | 11 +
.../test_extensions/sql/test_extensions.sql | 79 +++
.../test_ext_owned_schema--1.0--1.1.sql | 3 +
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...ext_owned_schema_nosuperuser--1.0--1.1.sql | 5 +
...test_ext_owned_schema_nosuperuser--1.0.sql | 3 +
.../test_ext_owned_schema_nosuperuser.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
...est_ext_owned_schema_trusted--1.0--1.1.sql | 3 +
.../test_ext_owned_schema_trusted--1.0.sql | 3 +
.../test_ext_owned_schema_trusted.control | 5 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++
25 files changed, 786 insertions(+), 146 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..ab125d56263 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,11 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. Whether this schema should already exist or not depends on
+ the value of <literal>owned_schema</literal> in the extensions control
+ file: If it's <literal>true</literal>, the schema must
+ <emphasis>not</emphasis> exist; if it's <literal>false</literal> it
+ must exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index d073585c421..3894d68f124 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -95,6 +95,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -794,6 +796,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1237,6 +1247,44 @@ extension_is_trusted(ExtensionControlFile *control)
return false;
}
+/*
+ * Enforce superuser requirements for extension operations.
+ *
+ * Returns true if we should switch to superuser for trusted extensions.
+ * Throws an error if superuser is required but not available.
+ *
+ * This function should only be called after choosing the appropriate
+ * control file (including any secondary control files for updates).
+ */
+static bool
+check_extension_superuser_requirements(ExtensionControlFile *control,
+ const char *from_version)
+{
+ if (control->superuser && !superuser())
+ {
+ if (extension_is_trusted(control))
+ return true;
+ else if (from_version == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to create this extension.")
+ : errhint("Must be superuser to create this extension.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to update extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to update this extension.")
+ : errhint("Must be superuser to update this extension.")));
+ }
+
+ return false;
+}
+
/*
* Execute the appropriate script file for installing or updating the extension
*
@@ -1265,27 +1313,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
* here so that the control flags are correctly associated with the right
* script(s) if they happen to be set in secondary control files.
*/
- if (control->superuser && !superuser())
- {
- if (extension_is_trusted(control))
- switch_to_superuser = true;
- else if (from_version == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to create this extension.")
- : errhint("Must be superuser to create this extension.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to update extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to update this extension.")
- : errhint("Must be superuser to update this extension.")));
- }
+ switch_to_superuser = check_extension_superuser_requirements(control, from_version);
filename = get_extension_script_filename(control, from_version, version);
@@ -1824,6 +1852,169 @@ find_install_path(List *evi_list, ExtensionVersionInfo *evi_target,
return evi_start;
}
+/*
+ * Create a schema with the given name, as part of CREATE EXTENSION.
+ */
+static Oid
+CreateSchemaForExtension(char *schemaName)
+{
+ ParseState *pstate = make_parsestate(NULL);
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ pstate->p_sourcetext = "(generated CREATE SCHEMA command)";
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+
+ CreateSchemaCommand(pstate, csstmt, -1, -1);
+
+ /*
+ * CreateSchemaCommand includes CommandCounterIncrement, so new schema is
+ * now visible.
+ */
+ return get_namespace_oid(schemaName, false);
+}
+
+/*
+ * Create an owned schema with the given name, as part of CREATE EXTENSION.
+ * Errors if the schema already exists.
+ */
+static Oid
+CreateOwnedSchemaForExtension(char *schemaName)
+{
+ /*
+ * An owned schema must be created by the extension, so a pre-existing
+ * schema with the same name is an error.
+ */
+ Oid schemaOid = get_namespace_oid(schemaName, true);
+
+ if (OidIsValid(schemaOid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
+ return CreateSchemaForExtension(schemaName);
+}
+
+/*
+ * Gets or creates the schema than an extension should be created in.
+ *
+ * Returns the OID of the schema and updates schemaName to the name of the schema.
+ */
+static Oid
+GetOrCreateSchemaForExtension(char **schemaName, ExtensionControlFile *control, bool cascade)
+{
+ /*
+ * The simplest case is when the user provides a schema name.
+ */
+ if (*schemaName)
+ {
+ Oid schemaOid;
+
+ if (!control->owned_schema)
+ {
+ /*
+ * For non-owned schemas, this schema must already exist. We want
+ * to check this now, so it fails even if we bail out of this
+ * block due to the CASCADE logic.
+ */
+ schemaOid = get_namespace_oid(*schemaName, false);
+ }
+
+ if (control->schema && strcmp(control->schema, *schemaName) != 0)
+ {
+ /*
+ * The extension is not relocatable and the author gave us a
+ * schema for it.
+ *
+ * Unless CASCADE parameter was given, it's an error to give a
+ * schema different from control->schema if control->schema is
+ * specified.
+ */
+ if (!cascade)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("extension \"%s\" must be installed in schema \"%s\"",
+ control->name,
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
+ }
+
+ /*
+ * If the schema mismatches and CASCADE was given, we pretend the
+ * user did not specify a schema and use the normal logic below to
+ * get or create the schema from the control file.
+ */
+ }
+ else if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(*schemaName);
+ }
+ else
+ {
+ return schemaOid;
+ }
+ }
+
+ if (control->schema)
+ {
+ Oid schemaOid;
+
+ *schemaName = control->schema;
+
+ if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(control->schema);
+ }
+
+ /* Find or create the schema in case it does not exist. */
+ schemaOid = get_namespace_oid(control->schema, true);
+ if (OidIsValid(schemaOid))
+ {
+ return schemaOid;
+ }
+ return CreateSchemaForExtension(control->schema);
+ }
+
+ if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+
+ {
+ /*
+ * Neither user nor author of the extension specified schema; use the
+ * current default creation namespace, which is the first explicit
+ * entry in the search_path.
+ */
+ List *search_path = fetch_search_path(false);
+ Oid schemaOid;
+
+ if (search_path == NIL) /* nothing valid in search_path? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ schemaOid = linitial_oid(search_path);
+ *schemaName = get_namespace_name(schemaOid);
+ if (*schemaName == NULL) /* recently-deleted namespace? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+
+ list_free(search_path);
+ return schemaOid;
+ }
+}
+
/*
* CREATE EXTENSION worker
*
@@ -1853,6 +2044,9 @@ CreateExtensionInternal(char *extensionName,
Oid extensionOid;
ObjectAddress address;
ListCell *lc;
+ bool switch_to_superuser = false;
+ Oid save_userid = 0;
+ int save_sec_context = 0;
/*
* Read the primary control file. Note we assume that it does not contain
@@ -1921,80 +2115,25 @@ CreateExtensionInternal(char *extensionName,
control = read_extension_aux_control_file(pcontrol, versionName);
/*
- * Determine the target schema to install the extension into
+ * For trusted extensions with owned schemas, we need to create the schema
+ * as superuser to ensure proper ownership.
*/
- if (schemaName)
+ if (control->owned_schema)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
- }
-
- if (control->schema != NULL)
- {
- /*
- * The extension is not relocatable and the author gave us a schema
- * for it.
- *
- * Unless CASCADE parameter was given, it's an error to give a schema
- * different from control->schema if control->schema is specified.
- */
- if (schemaName && strcmp(control->schema, schemaName) != 0 &&
- !cascade)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("extension \"%s\" must be installed in schema \"%s\"",
- control->name,
- control->schema)));
-
- /* Always use the schema from control file for current extension. */
- schemaName = control->schema;
-
- /* Find or create the schema in case it does not exist. */
- schemaOid = get_namespace_oid(schemaName, true);
-
- if (!OidIsValid(schemaOid))
+ switch_to_superuser = check_extension_superuser_requirements(control, NULL);
+ if (switch_to_superuser)
{
- ParseState *pstate = make_parsestate(NULL);
- CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
-
- pstate->p_sourcetext = "(generated CREATE SCHEMA command)";
-
- csstmt->schemaname = schemaName;
- csstmt->authrole = NULL; /* will be created by current user */
- csstmt->schemaElts = NIL;
- csstmt->if_not_exists = false;
-
- CreateSchemaCommand(pstate, csstmt, -1, -1);
-
- /*
- * CreateSchemaCommand includes CommandCounterIncrement, so new
- * schema is now visible.
- */
- schemaOid = get_namespace_oid(schemaName, false);
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
}
}
- else if (!OidIsValid(schemaOid))
- {
- /*
- * Neither user nor author of the extension specified schema; use the
- * current default creation namespace, which is the first explicit
- * entry in the search_path.
- */
- List *search_path = fetch_search_path(false);
- if (search_path == NIL) /* nothing valid in search_path? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
- schemaOid = linitial_oid(search_path);
- schemaName = get_namespace_name(schemaOid);
- if (schemaName == NULL) /* recently-deleted namespace? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
+ schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
- list_free(search_path);
- }
+ /* Restore authentication state after schema creation if needed */
+ if (switch_to_superuser)
+ SetUserIdAndSecContext(save_userid, save_sec_context);
/*
* Make note if a temporary namespace has been accessed in this
@@ -2040,6 +2179,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2245,7 +2385,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2275,6 +2416,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2319,6 +2461,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3253,12 +3406,16 @@ extension_config_remove(Oid extensionoid, Oid tableoid)
/*
* Execute ALTER EXTENSION SET SCHEMA
+ *
+ * For owned schemas, this boils down to changing the name of its schema. For
+ * non-owned schemas this requires moving all the member objects into the new
+ * schema.
*/
ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3271,11 +3428,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3284,22 +3440,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3323,14 +3463,56 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
+ ownedSchema = extForm->extownedschema;
+
/*
- * If the extension is already in the target schema, just silently do
- * nothing.
+ * For non-owned schemas, we should now evaluate if the target schema is a
+ * valid target. For owned schemas, no such checks are needed, because
+ * we'll simply rename the existing schema.
*/
- if (extForm->extnamespace == nspOid)
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
+ }
+ else
+ {
+ /*
+ * If the owned schema already has the target name, just silently do
+ * nothing. We cannot compare namespace OIDs like the non-owned case
+ * does, because renaming means the target schema doesn't exist yet.
+ */
+ if (strcmp(get_namespace_name(extForm->extnamespace), newschema) == 0)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3403,6 +3585,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually move any objects for owned schemas because we
+ * simply rename the schema that these objects are already in.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3446,18 +3635,35 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ /* Now actually update the schema of the extension. */
+ if (ownedSchema)
+ {
+ /*
+ * For owned schemas, we simply rename the schema. This means that we
+ * don't need to update the extension its catalog entry, because the
+ * oid of the schema will stay the same.
+ */
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /*
+ * For non-owned schemas, we now have to update the extension's schema
+ * entry, and also update the dependencies.
+ */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index b505a6b4fee..f2785cd6747 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -185,12 +186,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -198,28 +201,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -234,10 +239,27 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ ParseState *pstate = make_parsestate(NULL);
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(pstate, csstmt, -1, -1);
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4d660d14b4c..120cf099326 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1927,6 +1927,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -5996,7 +6009,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -6010,19 +6023,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6179,6 +6206,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6187,7 +6215,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 180000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6203,6 +6238,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6216,6 +6252,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11865,9 +11902,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11979,6 +12016,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5a6726d8b12..71eb3acd962 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index 19ec291ad7e..b4ebb94b42c 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -36,6 +36,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..2d74e58e17f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11948,7 +11948,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 7a76bdebcfa..03166b75502 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index d1b0b81e5fd..0c91383d622 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -10,7 +10,9 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_nosuperuser \
+ test_ext_owned_schema_trusted test_ext_owned_schema_relocatable
OBJS = test_ext.o
@@ -28,7 +30,14 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema--1.0--1.1.sql \
+ test_ext_owned_schema_nosuperuser--1.0.sql \
+ test_ext_owned_schema_nosuperuser--1.0--1.1.sql \
+ test_ext_owned_schema_trusted--1.0.sql \
+ test_ext_owned_schema_trusted--1.0--1.1.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index fdae52d6ab2..d01dd62be27 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -667,3 +668,163 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ function test_ext_owned_schema.owned2()
+ schema test_ext_owned_schema
+(3 rows)
+
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+ count
+-------
+ 0
+(1 row)
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+ERROR: cannot drop extension test_ext_owned_schema because other objects depend on it
+DETAIL: function test_ext_owned_schema.non_ext_func() depends on schema test_ext_owned_schema
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+NOTICE: drop cascades to function test_ext_owned_schema.non_ext_func()
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+-- Setting the schema to the current one is a no-op
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+ERROR: schema "some_other_name" does not exist
+-- Test owned_schema + superuser=false extension
+CREATE USER regress_test_ext_user;
+-- The database name differs between meson and autoconf builds, so we can't
+-- hardcode it in the GRANT/REVOKE statements.
+DO $$ BEGIN EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_test_ext_user', current_database()); END $$;
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ schema test_owned_schema_nosuperuser
+(2 rows)
+
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+ nspname | owned_by_current_user
+-------------------------------+-----------------------
+ test_owned_schema_nosuperuser | t
+(1 row)
+
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ function test_owned_schema_nosuperuser.owned2()
+ schema test_owned_schema_nosuperuser
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ schema test_owned_schema_trusted
+(2 rows)
+
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by regress_test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+ nspname | owned_by_bootstrap_superuser
+---------------------------+------------------------------
+ test_owned_schema_trusted | t
+(1 row)
+
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ function test_owned_schema_trusted.owned2()
+ schema test_owned_schema_trusted
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+DO $$ BEGIN EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_test_ext_user', current_database()); END $$;
+DROP USER regress_test_ext_user;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 2c7cea189e2..bfff0bc01ba 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,17 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema--1.0--1.1.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_nosuperuser--1.0.sql',
+ 'test_ext_owned_schema_nosuperuser--1.0--1.1.sql',
+ 'test_ext_owned_schema_nosuperuser.control',
+ 'test_ext_owned_schema_trusted--1.0.sql',
+ 'test_ext_owned_schema_trusted--1.0--1.1.sql',
+ 'test_ext_owned_schema_trusted.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
test_ext_sources = files('test_ext.c')
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..d496d7816c9 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,82 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+-- Setting the schema to the current one is a no-op
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+
+-- Test owned_schema + superuser=false extension
+CREATE USER regress_test_ext_user;
+-- The database name differs between meson and autoconf builds, so we can't
+-- hardcode it in the GRANT/REVOKE statements.
+DO $$ BEGIN EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_test_ext_user', current_database()); END $$;
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by regress_test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+DO $$ BEGIN EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_test_ext_user', current_database()); END $$;
+DROP USER regress_test_ext_user;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
new file mode 100644
index 00000000000..e9ba788d751
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
@@ -0,0 +1,5 @@
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1'" to load this file. \quit
+
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
new file mode 100644
index 00000000000..6c22c777519
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (superuser=false)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
new file mode 100644
index 00000000000..caad0960996
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (superuser=false)'
+default_version = '1.0'
+superuser = false
+owned_schema = true
+schema = 'test_owned_schema_nosuperuser'
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
new file mode 100644
index 00000000000..4e5410c184d
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (trusted=true)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
new file mode 100644
index 00000000000..277b3f1603b
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (trusted=true)'
+default_version = '1.0'
+trusted = true
+owned_schema = true
+schema = 'test_owned_schema_trusted'
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index 3d65ce4497a..4db2957318d 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
base-commit: c71d43025d7aaf12fe461ec635bd2eda220c9f4a
--
2.54.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-09-22 20:58 Manu <manuelreyesbravo@gmail.com>
parent: Jelte Fennema-Nio <me@jeltef.nl>
0 siblings, 1 reply; 43+ messages in thread
From: Manu @ 2026-09-22 20:58 UTC (permalink / raw)
To: Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Robert Haas <robertmhaas@gmail.com>; Tomas Vondra <tomas@vondra.me>; Julien Rouhaud <rjuju123@gmail.com>; pgsql-hackers@lists.postgresql.org
Hello Jelte, everyone,
v11 applies cleanly to master (09a579abaca) and CI is green, so I went
looking at the one part that CI cannot reach. In June 2024 you wrote:
> I also think I changed the pg_upgrade to do the correct thing, but I'm
> not sure how to test this (even manually). Because part of it would
> only be relevant once we support upgrading from PG18. So for now the
> upgrade_code I haven't actually run.
That is still true, and unfortunately that code does not work. It is
easy to run now: pg_upgrade invokes the NEW pg_dump against the OLD
server, so all it takes is a real PG18 cluster and the patched binary.
Everything below was measured on master 09a579abaca with clean builds
(--enable-cassert), against a PG18.6 cluster built from REL_18_STABLE,
with unpatched master as the control. Scripts attached.
1. pg_dump against any older server fails
------------------------------------------
getExtensions() asks:
if (fout->remoteVersion >= 180000)
appendPQExpBufferStr(query, ", x.extownedschema ");
The patch was written when 18 was the development target. master is
20devel now, so the column would appear in 20, not in 18. Against an
18 or 19 server the query asks for a column that does not exist:
pg_dump (master), PG18 server: ok
pg_dump (v11), PG18 server: ERROR: column x.extownedschema
does not exist
pg_dumpall (v11), PG18 server: same error
pg_upgrade PG18 -> v11: Failure, exiting
This is not only pg_upgrade. getExtensions() runs for every pg_dump,
so dumping an older server with new binaries - the normal way to move
between major versions - fails too.
The one-line change is attached as nocfbot-fix-remoteVersion.diff.txt
(180000 -> 200000). With it, on the same clusters:
pg_dump (fixed), PG18 server: ok, same output as master except
for the random \restrict token
pg_upgrade PG18 -> fixed: rc=0
It might be worth a comment there, since this number has to be bumped
again every time the patch misses a release.
2. Every pg_dump of a database with an owned-schema extension warns
--------------------------------------------------------------------
pg_dump: warning: could not resolve dependency loop among these items:
pg_dump: detail: EXTENSION loopdemo_owned (ID 2 OID 16386)
pg_dump: detail: SCHEMA loopdemo_owned (ID 8 OID 16385)
Control: the same trivial extension installed twice, where the only
difference is owned_schema in the control file.
owned_schema = true -> warning on every dump (plain and
--binary-upgrade)
owned_schema = false -> no warning
The dump still restores correctly, because pg_dump breaks the loop on
its own, but it is a loop it knows nothing about, and users of the
feature would see this on every single dump.
The loop is real: the extension is in the schema, and the schema is a
member of the extension. pg_dump_sort.c already repairs the loops it
knows (type/function, view/rule), so the second attached diff adds
this case: drop the extension's dependency on the schema, since CREATE
EXTENSION is what creates the schema. That is the direction that can
go, and it leaves the schema-is-a-member dependency intact.
With that diff the warning is gone, and make check, src/test/modules,
src/bin/pg_dump and src/bin/pg_upgrade all still pass.
3. test_pg_dump cannot be run on its own any more
--------------------------------------------------
make -C src/test/modules/test_pg_dump check
master: PASS (1044 tests)
v11: FAIL - extension "test_ext_owned_schema" is not available
The new case in test_pg_dump/t/001_base.pl installs an extension that
lives in src/test/modules/test_extensions, and nothing makes that
module available to this one. Running the whole src/test/modules
directory passes, which is presumably why CI never noticed.
4. What I checked that does work
---------------------------------
Same-version pg_upgrade of a cluster with an owned-schema extension
installed keeps the catalog right (extownedschema still true, schema
still the extension's own), the --binary-upgrade dump emits the
expected binary_upgrade_create_empty_extension(..., true, ...) and no
separate CREATE SCHEMA, and a plain dump/restore round trip works.
make check, src/test/modules and src/bin/pg_dump pass with v11 as is.
I have not reviewed the security model itself; the thread has already
covered that ground at length and I did not want to reopen it. These
are only the things I could measure.
Regards,
Manu
Attachments:
nocfbot-fix-remoteVersion.diff.txt (text/plain, 530B) download
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 72ed8fc..4492479 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6222,7 +6222,7 @@ getExtensions(Archive *fout, int *numExtensions)
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
);
- if (fout->remoteVersion >= 180000)
+ if (fout->remoteVersion >= 200000)
appendPQExpBufferStr(query, ", x.extownedschema ");
else
appendPQExpBufferStr(query, ", false AS extownedschema ");
nocfbot-fix-dependency-loop.diff.txt (text/plain, 2.1K) download
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4f3469d..b10ec6b 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -174,6 +174,8 @@ static int findLoop(DumpableObject *obj,
DumpId *searchFailed,
DumpableObject **workspace,
int depth);
+static void repairExtensionOwnedSchemaLoop(DumpableObject *extobj,
+ DumpableObject *nspobj);
static void repairDependencyLoop(DumpableObject **loop,
int nLoop);
static void describeDumpableObject(DumpableObject *obj,
@@ -928,6 +930,24 @@ findLoop(DumpableObject *obj,
return 0;
}
+/*
+ * An extension whose control file sets owned_schema=true has a dependency
+ * loop with its schema: the extension is in the schema, and the schema is a
+ * member of the extension. Break the loop by dropping the extension's
+ * dependency on the schema, since CREATE EXTENSION creates the schema.
+ */
+static void
+repairExtensionOwnedSchemaLoop(DumpableObject *extobj, DumpableObject *nspobj)
+{
+ /*
+ * An extension with owned_schema=true creates its schema itself, as part
+ * of CREATE EXTENSION, so the extension need not wait for the schema to
+ * be created first. (The schema is a member of the extension, hence the
+ * dependency in the other direction, which is the one we keep.)
+ */
+ removeObjectDependency(extobj, nspobj->dumpId);
+}
+
/*
* A user-defined datatype will have a dependency loop with each of its
* I/O functions (since those have the datatype as input or output).
@@ -1191,6 +1211,22 @@ repairDependencyLoop(DumpableObject **loop,
return;
}
+ /* Extension and the schema that the extension itself creates */
+ if (nLoop == 2 &&
+ loop[0]->objType == DO_EXTENSION &&
+ loop[1]->objType == DO_NAMESPACE)
+ {
+ repairExtensionOwnedSchemaLoop(loop[0], loop[1]);
+ return;
+ }
+ if (nLoop == 2 &&
+ loop[1]->objType == DO_EXTENSION &&
+ loop[0]->objType == DO_NAMESPACE)
+ {
+ repairExtensionOwnedSchemaLoop(loop[1], loop[0]);
+ return;
+ }
+
/* View (including matview) and its ON SELECT rule */
if (nLoop == 2 &&
loop[0]->objType == DO_TABLE &&
upgrade_18.sh.txt (text/plain, 1.7K) download
#!/usr/bin/env bash
# #5018: el camino de pg_upgrade que el autor dijo (2024-06-19) no haber
# ejecutado nunca: "I'm not sure how to test this (even manually) ... the
# upgrade code I haven't actually run."
#
# pg_upgrade corre el pg_dump NUEVO contra el servidor VIEJO. Asi que basta
# un cluster PG18 real y el pg_dump del build parchado.
#
# Control: el mismo pg_dump de master (sin el patch) contra el mismo cluster.
set -u
BASE=$HOME/pg5018
D18=$BASE/data18
P18=55418
rm -rf "$D18"
"$BASE/i-rel18/bin/initdb" -D "$D18" -U postgres --no-sync -A trust > "$BASE/initdb18.log" 2>&1 \
|| { echo "initdb 18 FALLO"; tail -5 "$BASE/initdb18.log"; exit 1; }
"$BASE/i-rel18/bin/pg_ctl" -D "$D18" -o "-p $P18" -l "$BASE/pg18.log" -w start > /dev/null 2>&1 \
|| { echo "start 18 FALLO"; tail -5 "$BASE/pg18.log"; exit 1; }
echo "== servidor viejo: $("$BASE/i-rel18/bin/psql" -p $P18 -U postgres -tAc 'select version()' | cut -c1-40)"
# Una extension cualquiera, de las que trae el propio arbol.
"$BASE/i-rel18/bin/psql" -p $P18 -U postgres -qc 'CREATE EXTENSION IF NOT EXISTS plpgsql' 2>/dev/null
"$BASE/i-rel18/bin/psql" -p $P18 -U postgres -tAc \
"select extname from pg_extension order by 1" | sed 's/^/ extension: /'
for w in master v11; do
echo "== pg_dump --binary-upgrade del build '$w' contra el servidor PG18"
if "$BASE/i-$w/bin/pg_dump" -p $P18 -U postgres --binary-upgrade -s -d postgres \
> "$BASE/dump_${w}_desde18.sql" 2> "$BASE/dump_${w}_desde18.err"; then
echo " OK ($(wc -l < "$BASE/dump_${w}_desde18.sql") lineas)"
else
echo " FALLA:"
sed 's/^/ /' "$BASE/dump_${w}_desde18.err" | head -5
fi
done
"$BASE/i-rel18/bin/pg_ctl" -D "$D18" -w stop > /dev/null 2>&1
echo "== FIN"
control_loop.sh.txt (text/plain, 1.8K) download
#!/usr/bin/env bash
# Control limpio del warning "could not resolve dependency loop":
# la MISMA extension trivial, dos veces, y lo unico que cambia entre las dos
# es owned_schema = true|false en el control file.
set -u
B=$HOME/pg5018/i-fix
EXT=$B/share/postgresql/extension
D=$HOME/pg5018/data_ctl
P=55422
for n in loopdemo_owned loopdemo_plain; do
cat > "$EXT/$n--1.0.sql" <<EOF
CREATE FUNCTION hola() RETURNS int LANGUAGE sql AS 'SELECT 1';
EOF
done
cat > "$EXT/loopdemo_owned.control" <<'EOF'
comment = 'demo con schema propio'
default_version = '1.0'
relocatable = false
owned_schema = true
schema = 'loopdemo_owned'
EOF
cat > "$EXT/loopdemo_plain.control" <<'EOF'
comment = 'demo sin schema propio'
default_version = '1.0'
relocatable = false
schema = 'loopdemo_plain'
EOF
rm -rf "$D"; "$B/bin/initdb" -D "$D" -U postgres --no-sync -A trust > /dev/null 2>&1
"$B/bin/pg_ctl" -D "$D" -o "-p $P" -l "$HOME/pg5018/ctl.log" -w start > /dev/null 2>&1
for n in loopdemo_owned loopdemo_plain; do
"$B/bin/psql" -p $P -U postgres -qc "CREATE DATABASE d_$n" 2>/dev/null
# la extension "plain" necesita su schema creado a mano
[ "$n" = loopdemo_plain ] && "$B/bin/psql" -p $P -U postgres -d "d_$n" -qc "CREATE SCHEMA $n" 2>/dev/null
if ! "$B/bin/psql" -p $P -U postgres -d "d_$n" -qc "CREATE EXTENSION $n" 2>"/tmp/ce_$n.err"; then
echo " $n: no se pudo crear -> $(head -1 "/tmp/ce_$n.err")"; continue
fi
"$B/bin/pg_dump" -p $P -U postgres -d "d_$n" > "/tmp/dump_$n.sql" 2> "/tmp/dump_$n.err"
printf " %-16s owned_schema=%-5s -> " "$n" "$([ "$n" = loopdemo_owned ] && echo true || echo false)"
if [ -s "/tmp/dump_$n.err" ]; then
echo "WARNING"; sed 's/^/ /' "/tmp/dump_$n.err" | head -4
else
echo "sin warnings"
fi
done
"$B/bin/pg_ctl" -D "$D" -w stop > /dev/null 2>&1
rm -f "$EXT"/loopdemo_*
echo "== FIN"
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-09-26 16:16 Jelte Fennema-Nio <postgres@jeltef.nl>
parent: Manu <manuelreyesbravo@gmail.com>
0 siblings, 2 replies; 43+ messages in thread
From: Jelte Fennema-Nio @ 2026-09-26 16:16 UTC (permalink / raw)
To: Manu <manuelreyesbravo@gmail.com>; Jelte Fennema-Nio <me@jeltef.nl>; +Cc: Robert Haas <robertmhaas@gmail.com>; Tomas Vondra <tomas@vondra.me>; Julien Rouhaud <rjuju123@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue Sep 22, 2026 at 10:58 PM CEST, Manu wrote:
>> I also think I changed the pg_upgrade to do the correct thing, but I'm
>> not sure how to test this (even manually). Because part of it would
>> only be relevant once we support upgrading from PG18. So for now the
>> upgrade_code I haven't actually run.
>
> That is still true, and unfortunately that code does not work.
Thank you for testing the patch and finding these issues. And to be
clear I had by this point tested this code manually (but that was a long
time ago) and written regression tests that should catch these issues.
> 1. pg_dump against any older server fails
> ------------------------------------------
Yeah this version number bitrotted over time due to new releases
without this patch in it. Fixed now. (this is a gap in our CI, but the
buildfarm would have caught this quickly after commit)
> 2. Every pg_dump of a database with an owned-schema extension warns
> --------------------------------------------------------------------
>
> pg_dump: warning: could not resolve dependency loop among these items:
> pg_dump: detail: EXTENSION loopdemo_owned (ID 2 OID 16386)
> pg_dump: detail: SCHEMA loopdemo_owned (ID 8 OID 16385)
I think this was the best find. Fixed now. And I also included a
separate patch that fails the test_pg_dump regression test if any
warning is emitted by pg_dump/pg_restore so that CI will catch these
kind of failures in the future.
> 3. test_pg_dump cannot be run on its own any more
> --------------------------------------------------
>
> make -C src/test/modules/test_pg_dump check
Fixed now (needed a small Makefile addition). I think I had only tested
it with meson (and CI runs the full test suite not specific tests).
Attachments: download all (2 files, .tar.gz)
v12-0001-Check-for-pg_dump-restore-warnings-in-test_pg_du.patch (text/x-patch, 1.4K) download
From 1ddea192f030f1c5244375e31314c634e3fa89d8 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Sat, 26 Sep 2026 14:54:54 +0200
Subject: [PATCH v12 1/2] Check for pg_dump/restore warnings in test_pg_dump
Warnings are unexpected during the pg_dump tests in test_pg_dump. This
makes the test fail when they occur anyway.
---
src/test/modules/test_pg_dump/t/001_base.pl | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index 3d65ce4497a..dd6a0a3c73f 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -938,13 +938,19 @@ foreach my $run (sort keys %pgdump_runs)
next;
}
- $node->command_ok(\@{ $pgdump_runs{$run}->{dump_cmd} },
- "$run: pg_dump runs");
+ $node->command_checks_all(
+ \@{ $pgdump_runs{$run}->{dump_cmd} },
+ 0, [qr/^/],
+ [qr/^(?!.*pg_dump: warning:)/s],
+ "$run: pg_dump runs without warnings");
if ($pgdump_runs{$run}->{restore_cmd})
{
- $node->command_ok(\@{ $pgdump_runs{$run}->{restore_cmd} },
- "$run: pg_restore runs");
+ $node->command_checks_all(
+ \@{ $pgdump_runs{$run}->{restore_cmd} },
+ 0, [qr/^/],
+ [qr/^(?!.*pg_restore: warning:)/s],
+ "$run: pg_restore runs without warnings");
}
if ($pgdump_runs{$run}->{test_key})
--
2.55.0
v12-0002-Add-support-for-extensions-with-an-owned-schema.patch (text/x-patch, 58.5K) download
From 120a020abdfe04a328096ddff814a778b157e2d3 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 4 Oct 2024 22:34:51 +0200
Subject: [PATCH v12 2/2] Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and
ALTER EXTENSION UPDATE are security minefields for extension authors.
One big reason for this is that search_path is set to the schema of the
extension while running these scripts, and thus if a user with lower
privileges can create functions or operators in that schema they can do
all kinds of search_path confusion attacks if not every function and
operator that is used in the script is schema qualified. While doing
such schema qualification is possible, it relies on the author to never
make a mistake in any of the sql files. And sadly humans have a tendency
to make mistakes.
This patch adds a new "owned_schema" option to the extension control
file that can be set to true to indicate that this extension wants to
own the schema in which it is installed. What that means is that the
schema should not exist before creating the extension, and will be
created during extension creation. This thus gives the extension author
an easy way to use a safe search_path, while still allowing all objects
to be grouped together in a schema. The implementation also has the
pleasant side effect that the schema will be automatically dropped when
the extension is dropped.
---
doc/src/sgml/extend.sgml | 34 ++
doc/src/sgml/ref/create_extension.sgml | 6 +-
src/backend/commands/extension.c | 450 +++++++++++++-----
src/backend/utils/adt/pg_upgrade_support.c | 44 +-
src/bin/pg_dump/pg_dump.c | 68 ++-
src/bin/pg_dump/pg_dump.h | 1 +
src/include/catalog/pg_extension.h | 1 +
src/include/catalog/pg_proc.dat | 2 +-
src/include/commands/extension.h | 4 +-
src/test/modules/test_extensions/Makefile | 13 +-
.../expected/test_extensions.out | 161 +++++++
src/test/modules/test_extensions/meson.build | 11 +
.../test_extensions/sql/test_extensions.sql | 79 +++
.../test_ext_owned_schema--1.0--1.1.sql | 3 +
.../test_ext_owned_schema--1.0.sql | 2 +
.../test_ext_owned_schema.control | 5 +
...ext_owned_schema_nosuperuser--1.0--1.1.sql | 5 +
...test_ext_owned_schema_nosuperuser--1.0.sql | 3 +
.../test_ext_owned_schema_nosuperuser.control | 5 +
...test_ext_owned_schema_relocatable--1.0.sql | 2 +
.../test_ext_owned_schema_relocatable.control | 4 +
...est_ext_owned_schema_trusted--1.0--1.1.sql | 3 +
.../test_ext_owned_schema_trusted--1.0.sql | 3 +
.../test_ext_owned_schema_trusted.control | 5 +
src/test/modules/test_pg_dump/Makefile | 3 +
src/test/modules/test_pg_dump/t/001_base.pl | 32 ++
26 files changed, 803 insertions(+), 146 deletions(-)
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
create mode 100644 src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
diff --git a/doc/src/sgml/extend.sgml b/doc/src/sgml/extend.sgml
index 63c5ec6d1eb..ddfb4ebfbf5 100644
--- a/doc/src/sgml/extend.sgml
+++ b/doc/src/sgml/extend.sgml
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
</listitem>
</varlistentry>
+ <varlistentry id="extend-extensions-files-owned-schema">
+ <term><varname>owned_schema</varname> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ An extension should set <firstterm>owned_schema</firstterm> to
+ <literal>true</literal> in its control file if the extension wants a
+ dedicated schema for its objects. Such a schema should not exist yet at
+ the time of extension creation, and will be created automatically by
+ <literal>CREATE EXTENSION</literal>. The default is
+ <literal>false</literal>, i.e., the extension can be installed into an
+ existing schema.
+ </para>
+ <para>
+ Having a schema owned by the extension can make it much easier to
+ reason about possible <literal>search_path</literal> injection attacks.
+ For instance with an owned schema, it is generally safe to set the
+ <literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
+ function to the schema of the extension. While without an owned schema
+ it might not be safe to do so, because a malicious user could insert
+ objects in that schema and thus <link
+ linkend="sql-createfunction-security"> cause malicious to be executed
+ as superuser</link>. Similarly, having an owned schema can make it safe
+ by default to execute general-purpose SQL in the extension script,
+ because the search_path now only contains trusted schemas. Without an
+ owned schema it's <link linkend="extend-extensions-security-scripts">
+ recommended to manually change the search_path</link>.
+ </para>
+ <para>
+ Apart from the security considerations, having an owned schema can help
+ prevent naming conflicts between objects of different extensions.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="extend-extensions-files-schema">
<term><varname>schema</varname> (<type>string</type>)</term>
<listitem>
diff --git a/doc/src/sgml/ref/create_extension.sgml b/doc/src/sgml/ref/create_extension.sgml
index 713abd9c494..ab125d56263 100644
--- a/doc/src/sgml/ref/create_extension.sgml
+++ b/doc/src/sgml/ref/create_extension.sgml
@@ -104,7 +104,11 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
<para>
The name of the schema in which to install the extension's
objects, given that the extension allows its contents to be
- relocated. The named schema must already exist.
+ relocated. Whether this schema should already exist or not depends on
+ the value of <literal>owned_schema</literal> in the extensions control
+ file: If it's <literal>true</literal>, the schema must
+ <emphasis>not</emphasis> exist; if it's <literal>false</literal> it
+ must exist.
If not specified, and the extension's control file does not specify a
schema either, the current default object creation schema is used.
</para>
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 5855668c5ca..2181aceaa6d 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -95,6 +95,8 @@ typedef struct ExtensionControlFile
* MODULE_PATHNAME */
char *comment; /* comment, if any */
char *schema; /* target schema (allowed if !relocatable) */
+ bool owned_schema; /* if the schema should be owned by the
+ * extension */
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
bool superuser; /* must be superuser to install? */
bool trusted; /* allow becoming superuser on the fly? */
@@ -794,6 +796,14 @@ parse_extension_control_file(ExtensionControlFile *control,
{
control->schema = pstrdup(item->value);
}
+ else if (strcmp(item->name, "owned_schema") == 0)
+ {
+ if (!parse_bool(item->value, &control->owned_schema))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" requires a Boolean value",
+ item->name)));
+ }
else if (strcmp(item->name, "relocatable") == 0)
{
if (!parse_bool(item->value, &control->relocatable))
@@ -1237,6 +1247,44 @@ extension_is_trusted(ExtensionControlFile *control)
return false;
}
+/*
+ * Enforce superuser requirements for extension operations.
+ *
+ * Returns true if we should switch to superuser for trusted extensions.
+ * Throws an error if superuser is required but not available.
+ *
+ * This function should only be called after choosing the appropriate
+ * control file (including any secondary control files for updates).
+ */
+static bool
+check_extension_superuser_requirements(ExtensionControlFile *control,
+ const char *from_version)
+{
+ if (control->superuser && !superuser())
+ {
+ if (extension_is_trusted(control))
+ return true;
+ else if (from_version == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to create this extension.")
+ : errhint("Must be superuser to create this extension.")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to update extension \"%s\"",
+ control->name),
+ control->trusted
+ ? errhint("Must have CREATE privilege on current database to update this extension.")
+ : errhint("Must be superuser to update this extension.")));
+ }
+
+ return false;
+}
+
/*
* Execute the appropriate script file for installing or updating the extension
*
@@ -1265,27 +1313,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
* here so that the control flags are correctly associated with the right
* script(s) if they happen to be set in secondary control files.
*/
- if (control->superuser && !superuser())
- {
- if (extension_is_trusted(control))
- switch_to_superuser = true;
- else if (from_version == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to create this extension.")
- : errhint("Must be superuser to create this extension.")));
- else
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to update extension \"%s\"",
- control->name),
- control->trusted
- ? errhint("Must have CREATE privilege on current database to update this extension.")
- : errhint("Must be superuser to update this extension.")));
- }
+ switch_to_superuser = check_extension_superuser_requirements(control, from_version);
filename = get_extension_script_filename(control, from_version, version);
@@ -1824,6 +1852,169 @@ find_install_path(List *evi_list, ExtensionVersionInfo *evi_target,
return evi_start;
}
+/*
+ * Create a schema with the given name, as part of CREATE EXTENSION.
+ */
+static Oid
+CreateSchemaForExtension(char *schemaName)
+{
+ ParseState *pstate = make_parsestate(NULL);
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ pstate->p_sourcetext = "(generated CREATE SCHEMA command)";
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+
+ CreateSchemaCommand(pstate, csstmt, -1, -1);
+
+ /*
+ * CreateSchemaCommand includes CommandCounterIncrement, so new schema is
+ * now visible.
+ */
+ return get_namespace_oid(schemaName, false);
+}
+
+/*
+ * Create an owned schema with the given name, as part of CREATE EXTENSION.
+ * Errors if the schema already exists.
+ */
+static Oid
+CreateOwnedSchemaForExtension(char *schemaName)
+{
+ /*
+ * An owned schema must be created by the extension, so a pre-existing
+ * schema with the same name is an error.
+ */
+ Oid schemaOid = get_namespace_oid(schemaName, true);
+
+ if (OidIsValid(schemaOid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_SCHEMA),
+ errmsg("schema \"%s\" already exists",
+ schemaName),
+ errhint("Drop schema \"%s\" or specify another schema using CREATE EXTENSION ... SCHEMA ...", schemaName)));
+ }
+
+ return CreateSchemaForExtension(schemaName);
+}
+
+/*
+ * Gets or creates the schema than an extension should be created in.
+ *
+ * Returns the OID of the schema and updates schemaName to the name of the schema.
+ */
+static Oid
+GetOrCreateSchemaForExtension(char **schemaName, ExtensionControlFile *control, bool cascade)
+{
+ /*
+ * The simplest case is when the user provides a schema name.
+ */
+ if (*schemaName)
+ {
+ Oid schemaOid;
+
+ if (!control->owned_schema)
+ {
+ /*
+ * For non-owned schemas, this schema must already exist. We want
+ * to check this now, so it fails even if we bail out of this
+ * block due to the CASCADE logic.
+ */
+ schemaOid = get_namespace_oid(*schemaName, false);
+ }
+
+ if (control->schema && strcmp(control->schema, *schemaName) != 0)
+ {
+ /*
+ * The extension is not relocatable and the author gave us a
+ * schema for it.
+ *
+ * Unless CASCADE parameter was given, it's an error to give a
+ * schema different from control->schema if control->schema is
+ * specified.
+ */
+ if (!cascade)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("extension \"%s\" must be installed in schema \"%s\"",
+ control->name,
+ control->schema),
+ errhint("Do not specify SCHEMA when running CREATE EXTENSION for extension \"%s\"", control->name)));
+ }
+
+ /*
+ * If the schema mismatches and CASCADE was given, we pretend the
+ * user did not specify a schema and use the normal logic below to
+ * get or create the schema from the control file.
+ */
+ }
+ else if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(*schemaName);
+ }
+ else
+ {
+ return schemaOid;
+ }
+ }
+
+ if (control->schema)
+ {
+ Oid schemaOid;
+
+ *schemaName = control->schema;
+
+ if (control->owned_schema)
+ {
+ return CreateOwnedSchemaForExtension(control->schema);
+ }
+
+ /* Find or create the schema in case it does not exist. */
+ schemaOid = get_namespace_oid(control->schema, true);
+ if (OidIsValid(schemaOid))
+ {
+ return schemaOid;
+ }
+ return CreateSchemaForExtension(control->schema);
+ }
+
+ if (control->owned_schema)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ }
+
+ {
+ /*
+ * Neither user nor author of the extension specified schema; use the
+ * current default creation namespace, which is the first explicit
+ * entry in the search_path.
+ */
+ List *search_path = fetch_search_path(false);
+ Oid schemaOid;
+
+ if (search_path == NIL) /* nothing valid in search_path? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+ schemaOid = linitial_oid(search_path);
+ *schemaName = get_namespace_name(schemaOid);
+ if (*schemaName == NULL) /* recently-deleted namespace? */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("no schema has been selected to create in")));
+
+ list_free(search_path);
+ return schemaOid;
+ }
+}
+
/*
* CREATE EXTENSION worker
*
@@ -1853,6 +2044,9 @@ CreateExtensionInternal(char *extensionName,
Oid extensionOid;
ObjectAddress address;
ListCell *lc;
+ bool switch_to_superuser = false;
+ Oid save_userid = 0;
+ int save_sec_context = 0;
/*
* Read the primary control file. Note we assume that it does not contain
@@ -1921,80 +2115,25 @@ CreateExtensionInternal(char *extensionName,
control = read_extension_aux_control_file(pcontrol, versionName);
/*
- * Determine the target schema to install the extension into
+ * For trusted extensions with owned schemas, we need to create the schema
+ * as superuser to ensure proper ownership.
*/
- if (schemaName)
+ if (control->owned_schema)
{
- /* If the user is giving us the schema name, it must exist already. */
- schemaOid = get_namespace_oid(schemaName, false);
- }
-
- if (control->schema != NULL)
- {
- /*
- * The extension is not relocatable and the author gave us a schema
- * for it.
- *
- * Unless CASCADE parameter was given, it's an error to give a schema
- * different from control->schema if control->schema is specified.
- */
- if (schemaName && strcmp(control->schema, schemaName) != 0 &&
- !cascade)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("extension \"%s\" must be installed in schema \"%s\"",
- control->name,
- control->schema)));
-
- /* Always use the schema from control file for current extension. */
- schemaName = control->schema;
-
- /* Find or create the schema in case it does not exist. */
- schemaOid = get_namespace_oid(schemaName, true);
-
- if (!OidIsValid(schemaOid))
+ switch_to_superuser = check_extension_superuser_requirements(control, NULL);
+ if (switch_to_superuser)
{
- ParseState *pstate = make_parsestate(NULL);
- CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
-
- pstate->p_sourcetext = "(generated CREATE SCHEMA command)";
-
- csstmt->schemaname = schemaName;
- csstmt->authrole = NULL; /* will be created by current user */
- csstmt->schemaElts = NIL;
- csstmt->if_not_exists = false;
-
- CreateSchemaCommand(pstate, csstmt, -1, -1);
-
- /*
- * CreateSchemaCommand includes CommandCounterIncrement, so new
- * schema is now visible.
- */
- schemaOid = get_namespace_oid(schemaName, false);
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(BOOTSTRAP_SUPERUSERID,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
}
}
- else if (!OidIsValid(schemaOid))
- {
- /*
- * Neither user nor author of the extension specified schema; use the
- * current default creation namespace, which is the first explicit
- * entry in the search_path.
- */
- List *search_path = fetch_search_path(false);
- if (search_path == NIL) /* nothing valid in search_path? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
- schemaOid = linitial_oid(search_path);
- schemaName = get_namespace_name(schemaOid);
- if (schemaName == NULL) /* recently-deleted namespace? */
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("no schema has been selected to create in")));
+ schemaOid = GetOrCreateSchemaForExtension(&schemaName, control, cascade);
- list_free(search_path);
- }
+ /* Restore authentication state after schema creation if needed */
+ if (switch_to_superuser)
+ SetUserIdAndSecContext(save_userid, save_sec_context);
/*
* Make note if a temporary namespace has been accessed in this
@@ -2040,6 +2179,7 @@ CreateExtensionInternal(char *extensionName,
*/
address = InsertExtensionTuple(control->name, extowner,
schemaOid, control->relocatable,
+ control->owned_schema,
versionName,
PointerGetDatum(NULL),
PointerGetDatum(NULL),
@@ -2245,7 +2385,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
*/
ObjectAddress
InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable, bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions)
{
@@ -2275,6 +2416,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
+ values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
if (extConfig == PointerGetDatum(NULL))
@@ -2319,6 +2461,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
free_object_addresses(refobjs);
+ if (ownedSchema)
+ {
+ ObjectAddress schemaAddress = {
+ .classId = NamespaceRelationId,
+ .objectId = schemaOid,
+ };
+
+ recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
+ }
+
+
/* Post creation hook for new extension */
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
@@ -3253,12 +3406,16 @@ extension_config_remove(Oid extensionoid, Oid tableoid)
/*
* Execute ALTER EXTENSION SET SCHEMA
+ *
+ * For owned schemas, this boils down to changing the name of its schema. For
+ * non-owned schemas this requires moving all the member objects into the new
+ * schema.
*/
ObjectAddress
AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema)
{
Oid extensionOid;
- Oid nspOid;
+ Oid nspOid = InvalidOid;
Oid oldNspOid;
AclResult aclresult;
Relation extRel;
@@ -3271,11 +3428,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
HeapTuple depTup;
ObjectAddresses *objsMoved;
ObjectAddress extAddr;
+ bool ownedSchema;
extensionOid = get_extension_oid(extensionName, false);
- nspOid = LookupCreationNamespace(newschema);
-
/*
* Permission check: must own extension. Note that we don't bother to
* check ownership of the individual member objects ...
@@ -3284,22 +3440,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
extensionName);
- /* Permission check: must have creation rights in target namespace */
- aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
- if (aclresult != ACLCHECK_OK)
- aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
-
- /*
- * If the schema is currently a member of the extension, disallow moving
- * the extension into the schema. That would create a dependency loop.
- */
- if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("cannot move extension \"%s\" into schema \"%s\" "
- "because the extension contains the schema",
- extensionName, newschema)));
-
/* Locate the pg_extension tuple */
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
@@ -3323,14 +3463,56 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
systable_endscan(extScan);
+ ownedSchema = extForm->extownedschema;
+
/*
- * If the extension is already in the target schema, just silently do
- * nothing.
+ * For non-owned schemas, we should now evaluate if the target schema is a
+ * valid target. For owned schemas, no such checks are needed, because
+ * we'll simply rename the existing schema.
*/
- if (extForm->extnamespace == nspOid)
+ if (!ownedSchema)
{
- table_close(extRel, RowExclusiveLock);
- return InvalidObjectAddress;
+ nspOid = LookupCreationNamespace(newschema);
+
+ /* Permission check: must have creation rights in target namespace */
+ aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
+
+ /*
+ * If the schema is currently a member of the extension, disallow
+ * moving the extension into the schema. That would create a
+ * dependency loop.
+ */
+ if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot move extension \"%s\" into schema \"%s\" "
+ "because the extension contains the schema",
+ extensionName, newschema)));
+
+ /*
+ * If the extension is already in the target schema, just silently do
+ * nothing.
+ */
+ if (extForm->extnamespace == nspOid)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
+ }
+ else
+ {
+ /*
+ * If the owned schema already has the target name, just silently do
+ * nothing. We cannot compare namespace OIDs like the non-owned case
+ * does, because renaming means the target schema doesn't exist yet.
+ */
+ if (strcmp(get_namespace_name(extForm->extnamespace), newschema) == 0)
+ {
+ table_close(extRel, RowExclusiveLock);
+ return InvalidObjectAddress;
+ }
}
/* Check extension is supposed to be relocatable */
@@ -3403,6 +3585,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
}
}
+ /*
+ * We don't actually move any objects for owned schemas because we
+ * simply rename the schema that these objects are already in.
+ */
+ if (ownedSchema)
+ continue;
+
/*
* Otherwise, ignore non-membership dependencies. (Currently, the
* only other case we could see here is a normal dependency from
@@ -3446,18 +3635,35 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
relation_close(depRel, AccessShareLock);
- /* Now adjust pg_extension.extnamespace */
- extForm->extnamespace = nspOid;
+ /* Now actually update the schema of the extension. */
+ if (ownedSchema)
+ {
+ /*
+ * For owned schemas, we simply rename the schema. This means that we
+ * don't need to update the extension its catalog entry, because the
+ * oid of the schema will stay the same.
+ */
+ RenameSchema(get_namespace_name(oldNspOid), newschema);
+ table_close(extRel, RowExclusiveLock);
+ }
+ else
+ {
+ /*
+ * For non-owned schemas, we now have to update the extension's schema
+ * entry, and also update the dependencies.
+ */
+ extForm->extnamespace = nspOid;
- CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
+ CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
- table_close(extRel, RowExclusiveLock);
+ table_close(extRel, RowExclusiveLock);
- /* update dependency to point to the new schema */
- if (changeDependencyFor(ExtensionRelationId, extensionOid,
- NamespaceRelationId, oldNspOid, nspOid) != 1)
- elog(ERROR, "could not change schema dependency for extension %s",
- NameStr(extForm->extname));
+ /* update dependency to point to the new schema */
+ if (changeDependencyFor(ExtensionRelationId, extensionOid,
+ NamespaceRelationId, oldNspOid, nspOid) != 1)
+ elog(ERROR, "could not change schema dependency for extension %s",
+ NameStr(extForm->extname));
+ }
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index f5017022d2e..6fcd279d607 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
+#include "commands/schemacmds.h"
#include "miscadmin.h"
#include "replication/logical.h"
#include "replication/logicallauncher.h"
@@ -196,12 +197,14 @@ Datum
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
{
text *extName;
- text *schemaName;
+ char *schemaName;
bool relocatable;
+ bool ownedschema;
text *extVersion;
Datum extConfig;
Datum extCondition;
List *requiredExtensions;
+ Oid schemaOid;
CHECK_IS_BINARY_UPGRADE;
@@ -209,28 +212,30 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0) ||
PG_ARGISNULL(1) ||
PG_ARGISNULL(2) ||
- PG_ARGISNULL(3))
+ PG_ARGISNULL(3) ||
+ PG_ARGISNULL(4))
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
extName = PG_GETARG_TEXT_PP(0);
- schemaName = PG_GETARG_TEXT_PP(1);
+ schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
relocatable = PG_GETARG_BOOL(2);
- extVersion = PG_GETARG_TEXT_PP(3);
+ ownedschema = PG_GETARG_BOOL(3);
+ extVersion = PG_GETARG_TEXT_PP(4);
- if (PG_ARGISNULL(4))
+ if (PG_ARGISNULL(5))
extConfig = PointerGetDatum(NULL);
else
- extConfig = PG_GETARG_DATUM(4);
+ extConfig = PG_GETARG_DATUM(5);
- if (PG_ARGISNULL(5))
+ if (PG_ARGISNULL(6))
extCondition = PointerGetDatum(NULL);
else
- extCondition = PG_GETARG_DATUM(5);
+ extCondition = PG_GETARG_DATUM(6);
requiredExtensions = NIL;
- if (!PG_ARGISNULL(6))
+ if (!PG_ARGISNULL(7))
{
- ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
+ ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
Datum *textDatums;
int ndatums;
int i;
@@ -245,10 +250,27 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
}
}
+ if (ownedschema)
+ {
+ ParseState *pstate = make_parsestate(NULL);
+ CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
+
+ csstmt->schemaname = schemaName;
+ csstmt->authrole = NULL; /* will be created by current user */
+ csstmt->schemaElts = NIL;
+ csstmt->if_not_exists = false;
+ schemaOid = CreateSchemaCommand(pstate, csstmt, -1, -1);
+ }
+ else
+ {
+ schemaOid = get_namespace_oid(schemaName, false);
+ }
+
InsertExtensionTuple(text_to_cstring(extName),
GetUserId(),
- get_namespace_oid(text_to_cstring(schemaName), false),
+ schemaOid,
relocatable,
+ ownedschema,
text_to_cstring(extVersion),
extConfig,
extCondition,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 388c3b9c346..b956f6da0a0 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1928,6 +1928,19 @@ checkExtensionMembership(DumpableObject *dobj, Archive *fout)
if (ext == NULL)
return false;
+ /*
+ * If this is the "owned_schema" of the extension, then we don't want to
+ * create it manually, because it gets created together with the
+ * extension.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ NamespaceInfo *nsinfo = (NamespaceInfo *) dobj;
+
+ nsinfo->create = false;
+ }
+
dobj->ext_member = true;
/* Record dependency so that getDependencies needn't deal with that */
@@ -6001,7 +6014,7 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
const char *objname,
const char *objnamespace)
{
- DumpableObject *extobj = NULL;
+ ExtensionInfo *ext = NULL;
int i;
if (!dobj->ext_member)
@@ -6015,19 +6028,33 @@ binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
*/
for (i = 0; i < dobj->nDeps; i++)
{
- extobj = findObjectByDumpId(dobj->dependencies[i]);
+ DumpableObject *extobj = findObjectByDumpId(dobj->dependencies[i]);
+
if (extobj && extobj->objType == DO_EXTENSION)
+ {
+ ext = (ExtensionInfo *) extobj;
break;
- extobj = NULL;
+ }
}
- if (extobj == NULL)
+ if (ext == NULL)
pg_fatal("could not find parent extension for %s %s",
objtype, objname);
+ /*
+ * If the object is the "owned_schema" of the extension, we don't need to
+ * add it to the extension because it was already made a member of the
+ * extension when the extension was created.
+ */
+ if (dobj->objType == DO_NAMESPACE &&
+ ext->ownedschema && strcmp(ext->namespace, dobj->name) == 0)
+ {
+ return;
+ }
+
appendPQExpBufferStr(upgrade_buffer,
"\n-- For binary upgrade, handle extension membership the hard way\n");
appendPQExpBuffer(upgrade_buffer, "ALTER EXTENSION %s ADD %s ",
- fmtId(extobj->name),
+ fmtId(ext->dobj.name),
objtype);
if (objnamespace && *objnamespace)
appendPQExpBuffer(upgrade_buffer, "%s.", fmtId(objnamespace));
@@ -6184,6 +6211,7 @@ getExtensions(Archive *fout, int *numExtensions)
int i_extname;
int i_nspname;
int i_extrelocatable;
+ int i_extownedschema;
int i_extversion;
int i_extconfig;
int i_extcondition;
@@ -6192,7 +6220,14 @@ getExtensions(Archive *fout, int *numExtensions)
appendPQExpBufferStr(query, "SELECT x.tableoid, x.oid, "
"x.extname, n.nspname, x.extrelocatable, x.extversion, x.extconfig, x.extcondition "
- "FROM pg_extension x "
+ );
+
+ if (fout->remoteVersion >= 200000)
+ appendPQExpBufferStr(query, ", x.extownedschema ");
+ else
+ appendPQExpBufferStr(query, ", false AS extownedschema ");
+
+ appendPQExpBufferStr(query, "FROM pg_extension x "
"JOIN pg_namespace n ON n.oid = x.extnamespace");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -6208,6 +6243,7 @@ getExtensions(Archive *fout, int *numExtensions)
i_extname = PQfnumber(res, "extname");
i_nspname = PQfnumber(res, "nspname");
i_extrelocatable = PQfnumber(res, "extrelocatable");
+ i_extownedschema = PQfnumber(res, "extownedschema");
i_extversion = PQfnumber(res, "extversion");
i_extconfig = PQfnumber(res, "extconfig");
i_extcondition = PQfnumber(res, "extcondition");
@@ -6221,6 +6257,7 @@ getExtensions(Archive *fout, int *numExtensions)
extinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_extname));
extinfo[i].namespace = pg_strdup(PQgetvalue(res, i, i_nspname));
extinfo[i].relocatable = *(PQgetvalue(res, i, i_extrelocatable)) == 't';
+ extinfo[i].ownedschema = *(PQgetvalue(res, i, i_extownedschema)) == 't';
extinfo[i].extversion = pg_strdup(PQgetvalue(res, i, i_extversion));
extinfo[i].extconfig = pg_strdup(PQgetvalue(res, i, i_extconfig));
extinfo[i].extcondition = pg_strdup(PQgetvalue(res, i, i_extcondition));
@@ -11825,9 +11862,9 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
{
/* see selectDumpableNamespace() */
appendPQExpBufferStr(delq,
- "-- *not* dropping schema, since initdb creates it\n");
+ "-- *not* dropping schema, since initdb or CREATE EXTENSION creates it\n");
appendPQExpBufferStr(q,
- "-- *not* creating schema, since initdb creates it\n");
+ "-- *not* creating schema, since initdb or CREATE EXTENSION creates it\n");
}
if (dopt->binary_upgrade)
@@ -11939,6 +11976,7 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
appendStringLiteralAH(q, extinfo->namespace, fout);
appendPQExpBufferStr(q, ", ");
appendPQExpBuffer(q, "%s, ", extinfo->relocatable ? "true" : "false");
+ appendPQExpBuffer(q, "%s, ", extinfo->ownedschema ? "true" : "false");
appendStringLiteralAH(q, extinfo->extversion, fout);
appendPQExpBufferStr(q, ", ");
@@ -20246,6 +20284,20 @@ getDependencies(Archive *fout)
if (deptype == 'x')
dobj->depends_on_ext = true;
+ /*
+ * An extension with an owned schema depends on its schema, but that
+ * schema is also a member of the extension, so keeping both
+ * dependencies would create a loop. We never actually dump a CREATE
+ * SCHEMA command for an owned schema anyway, so the dependency of the
+ * extension on the schema can be ignored. We still keep the schema's
+ * dependency on the extension so the schema's ACL and comment are
+ * dumped after CREATE EXTENSION creates the schema.
+ */
+ if (dobj->objType == DO_EXTENSION &&
+ refdobj->objType == DO_NAMESPACE &&
+ ((ExtensionInfo *) dobj)->ownedschema)
+ continue;
+
/*
* Ordinarily, table rowtypes have implicit dependencies on their
* tables. However, for a composite type the implicit dependency goes
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2bbb5d5773b..0bf54f00de4 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -195,6 +195,7 @@ typedef struct _extensionInfo
DumpableObject dobj;
char *namespace; /* schema containing extension's objects */
bool relocatable;
+ bool ownedschema;
char *extversion;
char *extconfig; /* info about configuration tables */
char *extcondition;
diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h
index c8711418435..26934f5d589 100644
--- a/src/include/catalog/pg_extension.h
+++ b/src/include/catalog/pg_extension.h
@@ -36,6 +36,7 @@ CATALOG(pg_extension,3079,ExtensionRelationId)
Oid extnamespace BKI_LOOKUP(pg_namespace); /* namespace of
* contained objects */
bool extrelocatable; /* if true, allow ALTER EXTENSION SET SCHEMA */
+ bool extownedschema; /* if true, schema is owned by extension */
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* extversion may never be null, but the others can be. */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f46427258e3..219f82c6fa0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12008,7 +12008,7 @@
{ oid => '3591', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_create_empty_extension', proisstrict => 'f',
provolatile => 'v', proparallel => 'u', prorettype => 'void',
- proargtypes => 'text text bool text _oid _text _text',
+ proargtypes => 'text text bool bool text _oid _text _text',
prosrc => 'binary_upgrade_create_empty_extension' },
{ oid => '4083', descr => 'for use by pg_upgrade',
proname => 'binary_upgrade_set_record_init_privs', provolatile => 'v',
diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h
index 7a76bdebcfa..03166b75502 100644
--- a/src/include/commands/extension.h
+++ b/src/include/commands/extension.h
@@ -38,7 +38,9 @@ extern ObjectAddress CreateExtension(ParseState *pstate, CreateExtensionStmt *st
extern void RemoveExtensionById(Oid extId);
extern ObjectAddress InsertExtensionTuple(const char *extName, Oid extOwner,
- Oid schemaOid, bool relocatable, const char *extVersion,
+ Oid schemaOid, bool relocatable,
+ bool ownedSchema,
+ const char *extVersion,
Datum extConfig, Datum extCondition,
List *requiredExtensions);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index d1b0b81e5fd..0c91383d622 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -10,7 +10,9 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
test_ext_extschema \
test_ext_evttrig \
test_ext_set_schema \
- test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
+ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 \
+ test_ext_owned_schema test_ext_owned_schema_nosuperuser \
+ test_ext_owned_schema_trusted test_ext_owned_schema_relocatable
OBJS = test_ext.o
@@ -28,7 +30,14 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
test_ext_set_schema--1.0.sql \
test_ext_req_schema1--1.0.sql \
test_ext_req_schema2--1.0.sql \
- test_ext_req_schema3--1.0.sql
+ test_ext_req_schema3--1.0.sql \
+ test_ext_owned_schema--1.0.sql \
+ test_ext_owned_schema--1.0--1.1.sql \
+ test_ext_owned_schema_nosuperuser--1.0.sql \
+ test_ext_owned_schema_nosuperuser--1.0--1.1.sql \
+ test_ext_owned_schema_trusted--1.0.sql \
+ test_ext_owned_schema_trusted--1.0--1.1.sql \
+ test_ext_owned_schema_relocatable--1.0.sql
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index 1b5debdeeb1..712d1d8c9a3 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -9,6 +9,7 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext;
ERROR: schema "test_ext" does not exist
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext1"
-- finally success
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
NOTICE: installing required extension "test_ext2"
@@ -667,3 +668,163 @@ SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
NOTICE: drop cascades to extension test_ext_req_schema2
+--
+-- Test owned schema extensions
+--
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+ERROR: schema "test_ext_owned_schema" already exists
+HINT: Drop schema "test_ext_owned_schema" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+ERROR: extension "test_ext_owned_schema" must be installed in schema "test_ext_owned_schema"
+HINT: Do not specify SCHEMA when running CREATE EXTENSION for extension "test_ext_owned_schema"
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ schema test_ext_owned_schema
+(2 rows)
+
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+Objects in extension "test_ext_owned_schema"
+ Object description
+-----------------------------------------
+ function test_ext_owned_schema.owned1()
+ function test_ext_owned_schema.owned2()
+ schema test_ext_owned_schema
+(3 rows)
+
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+ count
+-------
+ 0
+(1 row)
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+ERROR: cannot drop extension test_ext_owned_schema because other objects depend on it
+DETAIL: function test_ext_owned_schema.non_ext_func() depends on schema test_ext_owned_schema
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+NOTICE: drop cascades to function test_ext_owned_schema.non_ext_func()
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+HINT: Drop schema "already_existing" or specify another schema using CREATE EXTENSION ... SCHEMA ...
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+ERROR: no schema has been selected to create in
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-------------------------------
+ function test_schema.owned2()
+ schema test_schema
+(2 rows)
+
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ERROR: schema "already_existing" already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+-- Setting the schema to the current one is a no-op
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+Objects in extension "test_ext_owned_schema_relocatable"
+ Object description
+-----------------------------------
+ function some_other_name.owned2()
+ schema some_other_name
+(2 rows)
+
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+ERROR: schema "some_other_name" does not exist
+-- Test owned_schema + superuser=false extension
+CREATE USER regress_test_ext_user;
+-- The database name differs between meson and autoconf builds, so we can't
+-- hardcode it in the GRANT/REVOKE statements.
+DO $$ BEGIN EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_test_ext_user', current_database()); END $$;
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ schema test_owned_schema_nosuperuser
+(2 rows)
+
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+ nspname | owned_by_current_user
+-------------------------------+-----------------------
+ test_owned_schema_nosuperuser | t
+(1 row)
+
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+Objects in extension "test_ext_owned_schema_nosuperuser"
+ Object description
+-------------------------------------------------
+ function test_owned_schema_nosuperuser.owned1()
+ function test_owned_schema_nosuperuser.owned2()
+ schema test_owned_schema_nosuperuser
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ schema test_owned_schema_trusted
+(2 rows)
+
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by regress_test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+ nspname | owned_by_bootstrap_superuser
+---------------------------+------------------------------
+ test_owned_schema_trusted | t
+(1 row)
+
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+Objects in extension "test_ext_owned_schema_trusted"
+ Object description
+---------------------------------------------
+ function test_owned_schema_trusted.owned1()
+ function test_owned_schema_trusted.owned2()
+ schema test_owned_schema_trusted
+(3 rows)
+
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+DO $$ BEGIN EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_test_ext_user', current_database()); END $$;
+DROP USER regress_test_ext_user;
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 2c7cea189e2..bfff0bc01ba 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -44,6 +44,17 @@ test_install_data += files(
'test_ext_req_schema3.control',
'test_ext_set_schema--1.0.sql',
'test_ext_set_schema.control',
+ 'test_ext_owned_schema--1.0.sql',
+ 'test_ext_owned_schema--1.0--1.1.sql',
+ 'test_ext_owned_schema.control',
+ 'test_ext_owned_schema_nosuperuser--1.0.sql',
+ 'test_ext_owned_schema_nosuperuser--1.0--1.1.sql',
+ 'test_ext_owned_schema_nosuperuser.control',
+ 'test_ext_owned_schema_trusted--1.0.sql',
+ 'test_ext_owned_schema_trusted--1.0--1.1.sql',
+ 'test_ext_owned_schema_trusted.control',
+ 'test_ext_owned_schema_relocatable--1.0.sql',
+ 'test_ext_owned_schema_relocatable.control',
)
test_ext_sources = files('test_ext.c')
diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql
index b5878f6f80f..d496d7816c9 100644
--- a/src/test/modules/test_extensions/sql/test_extensions.sql
+++ b/src/test/modules/test_extensions/sql/test_extensions.sql
@@ -303,3 +303,82 @@ ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok
SELECT test_s_dep2.dep_req1();
SELECT test_s_dep.dep_req2();
DROP EXTENSION test_ext_req_schema1 CASCADE;
+
+--
+-- Test owned schema extensions
+--
+
+CREATE SCHEMA test_ext_owned_schema;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_ext_owned_schema;
+-- Fails because a different schema is set in control file
+CREATE EXTENSION test_ext_owned_schema SCHEMA test_schema;
+DROP SCHEMA test_ext_owned_schema;
+CREATE EXTENSION test_ext_owned_schema;
+\dx+ test_ext_owned_schema;
+-- Test ALTER EXTENSION UPDATE with owned schema
+ALTER EXTENSION test_ext_owned_schema UPDATE TO '1.1';
+\dx+ test_ext_owned_schema;
+-- Verify that the owned schema is dropped together with the extension
+DROP EXTENSION test_ext_owned_schema;
+SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'test_ext_owned_schema';
+
+-- Test drop behavior with non-extension objects in the owned schema
+CREATE EXTENSION test_ext_owned_schema;
+CREATE FUNCTION test_ext_owned_schema.non_ext_func() RETURNS int LANGUAGE SQL AS $$ SELECT 1 $$;
+-- Fails because of the non-extension object
+DROP EXTENSION test_ext_owned_schema;
+-- Succeeds and cascades to the non-extension object
+DROP EXTENSION test_ext_owned_schema CASCADE;
+
+CREATE SCHEMA already_existing;
+-- Fails for an already existing schema to be provided
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA already_existing;
+-- Fails because no schema is set in control file
+CREATE EXTENSION test_ext_owned_schema_relocatable;
+CREATE EXTENSION test_ext_owned_schema_relocatable SCHEMA test_schema;
+\dx+ test_ext_owned_schema_relocatable
+-- Fails because schema already exists
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA already_existing;
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+-- Setting the schema to the current one is a no-op
+ALTER EXTENSION test_ext_owned_schema_relocatable SET SCHEMA some_other_name;
+\dx+ test_ext_owned_schema_relocatable
+DROP EXTENSION test_ext_owned_schema_relocatable;
+-- Verify the schema was dropped with the extension
+DROP SCHEMA some_other_name;
+
+-- Test owned_schema + superuser=false extension
+CREATE USER regress_test_ext_user;
+-- The database name differs between meson and autoconf builds, so we can't
+-- hardcode it in the GRANT/REVOKE statements.
+DO $$ BEGIN EXECUTE format('GRANT CREATE ON DATABASE %I TO regress_test_ext_user', current_database()); END $$;
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_nosuperuser;
+\dx+ test_ext_owned_schema_nosuperuser;
+-- Check that schema is owned by the creating user (not bootstrap superuser)
+SELECT n.nspname, n.nspowner = current_user::regrole as owned_by_current_user
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_nosuperuser';
+-- Upgrades should work for superuser=false extensions
+ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_nosuperuser;
+DROP EXTENSION test_ext_owned_schema_nosuperuser;
+RESET SESSION AUTHORIZATION;
+
+-- Test owned_schema + trusted=true extension
+SET SESSION AUTHORIZATION regress_test_ext_user;
+CREATE EXTENSION test_ext_owned_schema_trusted;
+\dx+ test_ext_owned_schema_trusted;
+-- Check that schema is owned by bootstrap superuser for trusted extension,
+-- even though the extension is owned by regress_test_ext_user
+SELECT n.nspname, n.nspowner = 10 as owned_by_bootstrap_superuser
+FROM pg_namespace n
+WHERE n.nspname = 'test_owned_schema_trusted';
+-- Updating trusted extensions should work normally
+ALTER EXTENSION test_ext_owned_schema_trusted UPDATE TO '1.1';
+\dx+ test_ext_owned_schema_trusted;
+DROP EXTENSION test_ext_owned_schema_trusted;
+RESET SESSION AUTHORIZATION;
+DO $$ BEGIN EXECUTE format('REVOKE CREATE ON DATABASE %I FROM regress_test_ext_user', current_database()); END $$;
+DROP USER regress_test_ext_user;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
new file mode 100644
index 00000000000..672ab8e607f
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned1() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema.control b/src/test/modules/test_extensions/test_ext_owned_schema.control
new file mode 100644
index 00000000000..531c38daefd
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = false
+schema = test_ext_owned_schema
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
new file mode 100644
index 00000000000..e9ba788d751
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0--1.1.sql
@@ -0,0 +1,5 @@
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION test_ext_owned_schema_nosuperuser UPDATE TO '1.1'" to load this file. \quit
+
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
new file mode 100644
index 00000000000..6c22c777519
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (superuser=false)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
new file mode 100644
index 00000000000..caad0960996
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_nosuperuser.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (superuser=false)'
+default_version = '1.0'
+superuser = false
+owned_schema = true
+schema = 'test_owned_schema_nosuperuser'
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
new file mode 100644
index 00000000000..bfccaf4af82
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable--1.0.sql
@@ -0,0 +1,2 @@
+CREATE FUNCTION owned2() RETURNS text
+LANGUAGE SQL AS $$ SELECT 1 $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
new file mode 100644
index 00000000000..3cda1e12341
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_relocatable.control
@@ -0,0 +1,4 @@
+comment = 'Test extension with an owned schema'
+default_version = '1.0'
+relocatable = true
+owned_schema = true
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
new file mode 100644
index 00000000000..a87be1ac967
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0--1.1.sql
@@ -0,0 +1,3 @@
+-- Test extension upgrade script
+CREATE FUNCTION owned2() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned2'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
new file mode 100644
index 00000000000..4e5410c184d
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted--1.0.sql
@@ -0,0 +1,3 @@
+-- Test extension with owned schema (trusted=true)
+CREATE FUNCTION owned1() RETURNS text
+ LANGUAGE SQL AS $$ SELECT 'owned1'::text $$;
diff --git a/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
new file mode 100644
index 00000000000..277b3f1603b
--- /dev/null
+++ b/src/test/modules/test_extensions/test_ext_owned_schema_trusted.control
@@ -0,0 +1,5 @@
+comment = 'Test extension with owned schema (trusted=true)'
+default_version = '1.0'
+trusted = true
+owned_schema = true
+schema = 'test_owned_schema_trusted'
diff --git a/src/test/modules/test_pg_dump/Makefile b/src/test/modules/test_pg_dump/Makefile
index 6123b994f60..cca34a95aa4 100644
--- a/src/test/modules/test_pg_dump/Makefile
+++ b/src/test/modules/test_pg_dump/Makefile
@@ -9,6 +9,9 @@ DATA = test_pg_dump--1.0.sql
REGRESS = test_pg_dump
TAP_TESTS = 1
+# for test_ext_owned_schema
+EXTRA_INSTALL = src/test/modules/test_extensions
+
ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl
index dd6a0a3c73f..7e71c7f5865 100644
--- a/src/test/modules/test_pg_dump/t/001_base.pl
+++ b/src/test/modules/test_pg_dump/t/001_base.pl
@@ -381,6 +381,38 @@ my %tests = (
},
},
+ 'CREATE EXTENSION test_ext_owned_schema' => {
+ create_order => 1,
+ create_sql => 'CREATE EXTENSION test_ext_owned_schema;',
+ regexp => qr/^
+ \QCREATE EXTENSION IF NOT EXISTS test_ext_owned_schema WITH SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {
+ %full_runs,
+ schema_only => 1,
+ section_pre_data => 1,
+ },
+ unlike => {
+ binary_upgrade => 1,
+ with_extension => 1,
+ without_extension => 1
+ }
+ },
+
+ 'CREATE SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QCREATE SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
+ 'ALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema' => {
+ regexp => qr/^
+ \QALTER EXTENSION test_ext_owned_schema ADD SCHEMA test_ext_owned_schema;\E
+ \n/xm,
+ like => {},
+ },
+
'CREATE ROLE regress_dump_test_role' => {
create_order => 1,
create_sql => 'CREATE ROLE regress_dump_test_role;',
--
2.55.0
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-09-26 18:09 Manu <manuelreyesbravo@gmail.com>
parent: Jelte Fennema-Nio <postgres@jeltef.nl>
1 sibling, 0 replies; 43+ messages in thread
From: Manu @ 2026-09-26 18:09 UTC (permalink / raw)
To: Jelte Fennema-Nio <postgres@jeltef.nl>; +Cc: Robert Haas <robertmhaas@gmail.com>; Tomas Vondra <tomas@vondra.me>; Julien Rouhaud <rjuju123@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi Jelte,
> I also included a
> separate patch that fails the test_pg_dump regression test if any
> warning is emitted by pg_dump/pg_restore so that CI will catch these
> kind of failures in the future.
I checked that it would have caught this one. With v12-0001 on top
of v11, test_pg_dump fails 25 checks, all of them "pg_dump runs
without warnings" with the dependency loop warning. With v12 all
1182 pass. (v11 needed test_extensions added to EXTRA_INSTALL to run
on its own, which is the Makefile fix in v12.)
I re-ran the three cases from my earlier mail with the same scripts,
on master 3c5d9d914fa against a REL_18_STABLE (18.6) cluster:
- pg_dump, pg_dumpall and pg_upgrade from PG18 all work now;
pg_upgrade exits 0.
- The owned-schema extension dumps without warnings, plain and
--binary-upgrade, and restores with extownedschema = t.
- make -C src/test/modules/test_pg_dump check passes on its own.
Both patches apply cleanly and build without warnings, and
test_extensions, src/bin/pg_dump and src/bin/pg_upgrade pass.
One thing about the new comment in pg_dump.c:
We still keep the schema's dependency on the extension so the
schema's ACL and comment are dumped after CREATE EXTENSION creates
the schema.
The ACL is: a GRANT on the owned schema comes after CREATE EXTENSION
in the dump and survives a plain, directory and parallel restore.
The COMMENT is not dumped at all, so it is lost on restore. That
looks like the existing rule for extension members rather than
something this patch broke; on unpatched master a comment on a
function of an extension is lost the same way. But the schema is now
a member, so a schema comment that used to survive a dump/restore no
longer does, while a same-version pg_upgrade keeps it. Either the
code comment should only mention the ACL, or the owned schema's
comment could be dumped like its ACL.
Regards,
Manu
^ permalink raw reply [nested|flat] 43+ messages in thread
* Re: Extension security improvement: Add support for extensions with an owned schema
@ 2026-09-29 17:03 Rui Zhao <zhaorui126@gmail.com>
parent: Jelte Fennema-Nio <postgres@jeltef.nl>
1 sibling, 0 replies; 43+ messages in thread
From: Rui Zhao @ 2026-09-29 17:03 UTC (permalink / raw)
To: Jelte Fennema-Nio <postgres@jeltef.nl>; +Cc: Manu <manuelreyesbravo@gmail.com>; Jelte Fennema-Nio <me@jeltef.nl>; Robert Haas <robertmhaas@gmail.com>; Tomas Vondra <tomas@vondra.me>; Julien Rouhaud <rjuju123@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi Jelte,
1. There is another dependency cycle reachable through CREATE EXTENSION
... SCHEMA ... CASCADE. Unlike the earlier two-object cycle, this one
produces a dump that fails to restore. A same-version pg_upgrade
completes but loses the prerequisite-extension dependency.
On v12 over 3c5d9d914fa, I used these SQL-only extensions:
os_base.control:
default_version = '1.0'
relocatable = true
superuser = false
os_owned.control:
default_version = '1.0'
relocatable = true
superuser = false
owned_schema = true
requires = 'os_base'
os_base--1.0.sql:
CREATE FUNCTION os_base_value() RETURNS integer
LANGUAGE SQL AS 'SELECT 1';
os_owned--1.0.sql:
CREATE FUNCTION os_owned_value() RETURNS integer
LANGUAGE SQL AS 'SELECT 1';
In a fresh database, this succeeds:
CREATE EXTENSION os_owned SCHEMA shared_schema CASCADE;
Both extensions end up in shared_schema. pg_depend then contains the
following cycle, where each arrow means "depends on":
os_owned -> os_base -> shared_schema -> os_owned
In v12, getDependencies() already skips the direct dependency from
os_owned to shared_schema when ordering the dump. However, the longer
cycle above remains. Plain and directory dumps both warn:
pg_dump: warning: could not resolve dependency loop among these items:
pg_dump: detail: EXTENSION os_owned (ID 2 OID 16389)
pg_dump: detail: EXTENSION os_base (ID 3 OID 16387)
pg_dump: detail: SCHEMA shared_schema (ID 9 OID 16386)
The plain dump and the SQL generated by pg_restore from the directory
archive contain these extension commands in this order:
CREATE EXTENSION IF NOT EXISTS os_owned WITH SCHEMA shared_schema;
CREATE EXTENSION IF NOT EXISTS os_base WITH SCHEMA shared_schema;
The first command requires os_base to be installed already, but it has
no CASCADE to install it. Plain restore with ON_ERROR_STOP and directory
restore with --exit-on-error --jobs=2 therefore fail at that command:
ERROR: required extension "os_base" is not installed
However, pg_upgrade reports success but loses os_owned's dependency
on os_base.
I used the v12 build for both the old and new clusters. Before the
upgrade, pg_depend recorded that os_owned depends on os_base.
Both extensions still existed after the upgrade, but pg_depend
no longer recorded that os_owned depends on os_base.
Two possible approaches are:
a) Reject cross-extension dependency cycles involving an owned schema.
Check CREATE EXTENSION, ALTER EXTENSION UPDATE and SET SCHEMA,
including indirect dependencies. For this case, the error could
suggest installing os_base in another schema first.
b) Support this layout in dump/restore and binary upgrade. Adding
CASCADE to normal CREATE EXTENSION output might address ordinary
restore, but binary upgrade needs its own handling, without running
extension installation scripts or dropping prerequisite dependencies.
I'd prefer (b). It preserves the existing schema-placement rules and
keeps the reconstruction logic in the dump/upgrade path. Option (a)
would need checks across several DDL paths, including concurrent
changes, to avoid admitting the same cycle another way.
Binary upgrade still needs to preserve the original extension
dependencies, independently of any changes made for dump ordering.
2. In doc/src/sgml/ref/alter_extension.sgml:
> This form moves the extension's objects into another schema.
Could we replace that paragraph with:
For an extension without an owned schema, this form moves its
objects to an existing schema. If the extension owns its schema,
this form instead renames that schema to new_schema, which must
not already exist unless it is the current name. The extension
must be relocatable.
3. In doc/src/sgml/catalogs.sgml, the pg_extension section is missing
extownedschema. Could we add the boolean column, for example with:
True if the extension creates and owns the schema identified by
extnamespace.
Regards,
Rui
^ permalink raw reply [nested|flat] 43+ messages in thread
end of thread, other threads:[~2026-09-29 17:03 UTC | newest]
Thread overview: 43+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-06-02 00:08 Extension security improvement: Add support for extensions with an owned schema Jelte Fennema-Nio <me@jeltef.nl>
2024-06-05 14:09 ` Marco Slot <marco.slot@gmail.com>
2024-06-05 17:53 ` Jeff Davis <pgsql@j-davis.com>
2024-06-05 20:30 ` Jelte Fennema-Nio <me@jeltef.nl>
2024-06-19 15:19 ` Jelte Fennema-Nio <me@jeltef.nl>
2024-06-19 15:22 ` David G. Johnston <david.g.johnston@gmail.com>
2024-06-20 11:18 ` Jelte Fennema-Nio <me@jeltef.nl>
2024-09-27 12:00 ` Tomas Vondra <tomas@vondra.me>
2024-10-04 21:05 ` Jelte Fennema-Nio <me@jeltef.nl>
2025-07-23 17:12 ` Artem Gavrilov <artem.gavrilov@percona.com>
2025-07-29 09:35 ` Jelte Fennema-Nio <postgres@jeltef.nl>
2025-08-11 17:55 ` Robert Haas <robertmhaas@gmail.com>
2025-08-11 19:23 ` Robert Haas <robertmhaas@gmail.com>
2025-09-01 14:44 ` Jelte Fennema-Nio <postgres@jeltef.nl>
2025-09-01 20:27 ` Jelte Fennema-Nio <postgres@jeltef.nl>
2025-09-02 00:03 ` Julien Rouhaud <rjuju123@gmail.com>
2025-09-02 07:37 ` Jelte Fennema-Nio <me@jeltef.nl>
2025-09-02 09:02 ` Julien Rouhaud <rjuju123@gmail.com>
2025-09-02 13:35 ` Robert Haas <robertmhaas@gmail.com>
2025-09-06 00:17 ` Julien Rouhaud <rjuju123@gmail.com>
2025-09-06 07:35 ` Jelte Fennema-Nio <me@jeltef.nl>
2025-09-11 13:01 ` Robert Haas <robertmhaas@gmail.com>
2025-09-11 13:29 ` Jelte Fennema-Nio <me@jeltef.nl>
2025-09-11 14:52 ` Robert Haas <robertmhaas@gmail.com>
2026-02-10 23:19 ` Jelte Fennema-Nio <me@jeltef.nl>
2026-02-10 23:32 ` Jelte Fennema-Nio <postgres@jeltef.nl>
2026-03-15 20:19 ` Jelte Fennema-Nio <postgres@jeltef.nl>
2026-07-11 10:24 ` Jelte Fennema-Nio <me@jeltef.nl>
2026-09-22 20:58 ` Manu <manuelreyesbravo@gmail.com>
2026-09-26 16:16 ` Jelte Fennema-Nio <postgres@jeltef.nl>
2026-09-26 18:09 ` Manu <manuelreyesbravo@gmail.com>
2026-09-29 17:03 ` Rui Zhao <zhaorui126@gmail.com>
2025-07-27 22:03 ` Sadeq Dousti <msdousti@gmail.com>
2025-07-28 00:27 ` David G. Johnston <david.g.johnston@gmail.com>
2025-07-28 00:45 ` Sadeq Dousti <msdousti@gmail.com>
2025-07-28 08:01 ` Jelte Fennema-Nio <me@jeltef.nl>
2024-06-19 15:28 ` Robert Haas <robertmhaas@gmail.com>
2024-06-19 17:50 ` Jelte Fennema-Nio <me@jeltef.nl>
2024-06-19 17:55 ` Tom Lane <tgl@sss.pgh.pa.us>
2024-06-19 19:06 ` Jelte Fennema-Nio <me@jeltef.nl>
2024-06-20 15:51 ` Robert Haas <robertmhaas@gmail.com>
2024-06-20 17:02 ` David E. Wheeler <david@justatheory.com>
2024-06-20 17:00 ` David E. Wheeler <david@justatheory.com>
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox