I'm attaching the patch for $SUBJECT, which applies atop the four patches from the two other threads below. For convenience of testing, I've included a rollup patch, equivalent to applying all five patches.
On Sat, Oct 31, 2020 at 09:35:18AM -0700, Noah Misch wrote: > More details on the semantics I'll use: > > 1. initdb will change like this: > @@ -1721 +1721 @@ setup_privileges(FILE *cmdfd) > - "GRANT CREATE, USAGE ON SCHEMA public TO PUBLIC;\n\n", > + "GRANT USAGE ON SCHEMA public TO PUBLIC;\n\n", > + "ALTER SCHEMA public OWNER TO DATABASE_OWNER;\n\n", (I ended up assigning the ownership via pg_namespace.dat, not here.) > 2. If schema public does not exist, pg_dump will emit nothing about it. This > is what happens today. (I suspect it would be better for pg_dump to emit > DROP SCHEMA public RESTRICT, but that is drifting offtopic for $SUBJECT.) > Otherwise, when dumping from v13 or earlier, pg_dump will always emit > REVOKE and/or GRANT statements to reproduce the old ACL. More precisely, it diffs the source database ownership and ACL to the v14 defaults, then emits ALTER, GRANT, and/or REVOKE as needed. That yields no GRANT or REVOKE if the source database is an early adopter of the new default. > When dumping from > v14 or later, pg_dump will use pg_init_privs to compute GRANT and REVOKE > statements, as it does today. (It doesn't actually use pg_init_privs, but the effect is similar.) > (This may interfere with cross-version > pg_upgrade testing. I haven't looked at how best to fix that. Perhaps add > more fix_sql in test.sh.) src/bin/pg_upgrade/test.sh doesn't need changes. Upgrades from 9.6 (the first version having pg_init_privs) or later get no new diffs. Upgrades from v8.4 or v9.5 to v14 have a relevant diff before or after this change. In master: -REVOKE ALL ON SCHEMA public FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM nm; -GRANT ALL ON SCHEMA public TO nm; -GRANT ALL ON SCHEMA public TO PUBLIC; After $SUBJECT: -REVOKE ALL ON SCHEMA public FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM nm; -GRANT ALL ON SCHEMA public TO nm; +REVOKE USAGE ON SCHEMA public FROM PUBLIC; GRANT ALL ON SCHEMA public TO PUBLIC; > 3. pg_upgrade from v13 to later versions will transfer template1's ACL for > schema public, even if that ACL was unchanged since v13 initdb. (This is > purely a consequence of the pg_dump behavior decision.) template0 will > keep the new default. > > 4. OWNER TO DATABASE_OWNER will likely be available for schemas only, though I > might propose it for all object classes if class-specific complexity proves > negligible. Class-specific complexity was negligible, so I made it available for all objects. The syntax is "OWNER TO pg_database_owner", because it's a special predefined role. That patch has its own thread: https://postgr.es/m/20201228043148.ga1053...@rfd.leadboat.com > 5. ALTER DATABASE OWNER TO changes access control decisions involving > nspowner==DATABASE_OWNER. Speed of nspacl checks is more important than > reacting swiftly to ALTER DATABASE OWNER TO. Sessions running concurrently > will be eventually-consistent with respect to the ALTER DATABASE. > (Existing access control decisions, too, allow this sort of anomaly.) > > 6. pg_dump hasn't been reproducing ALTER SCHEMA public OWNER TO. That's a > mild defect today, but it wouldn't be mild anymore. We'll need pg_dump of > v13 databases to emit "ALTER SCHEMA public OWNER TO postgres" and for a v14 > => v15 upgrade to propagate that. This project can stand by itself; would > anyone else like to own it? That patch has its own thread: https://postgr.es/m/20201229134924.ga1431...@rfd.leadboat.com Changing this ACL caused 13 of 202 tests to fail in "make check". I first intended to modify tests as needed for that suite to keep the default ACL. For complicated cases, my strategy was to make a test create a schema and change search_path. However, that created large expected output diffs (e.g. ~120 lines in updatable_views.out), mostly in EXPLAIN and \d output bearing the schema name. I didn't want that kind of obstacle to future back-patched test updates, so I did make the first test install the old ACL. All other in-tree suites do test the new default. Thanks, nm
Author: Noah Misch <n...@leadboat.com> Commit: Noah Misch <n...@leadboat.com> Revoke PUBLIC CREATE from public schema, now owned by pg_database_owner. This switches the default ACL to what the documentation has recommended since CVE-2018-1058. Upgrades will carry forward any old ownership and ACL. Sites that declined the 2018 recommendation should take a fresh look. Recipes for commissioning a new database cluster from scratch may need to create a schema, grant more privileges, etc. Out-of-tree test suites may require such updates. Reviewed by FIXME. Discussion: https://postgr.es/m/20201031163518.gb4039...@rfd.leadboat.com diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 60c7e11..8b3b37b 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -8925,7 +8925,7 @@ $d$; -- But creation of user mappings for non-superusers should fail CREATE USER MAPPING FOR public SERVER loopback_nopw; CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_nopw; -CREATE FOREIGN TABLE ft1_nopw ( +CREATE FOREIGN TABLE pg_temp.ft1_nopw ( c1 int NOT NULL, c2 int NOT NULL, c3 text, diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 151f4f1..bf35097 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -2595,7 +2595,7 @@ $d$; CREATE USER MAPPING FOR public SERVER loopback_nopw; CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_nopw; -CREATE FOREIGN TABLE ft1_nopw ( +CREATE FOREIGN TABLE pg_temp.ft1_nopw ( c1 int NOT NULL, c2 int NOT NULL, c3 text, diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 1e9a462..1b30a0d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2982,20 +2982,18 @@ SELECT 3 OPERATOR(pg_catalog.+) 4; <para> By default, users cannot access any objects in schemas they do not own. To allow that, the owner of the schema must grant the - <literal>USAGE</literal> privilege on the schema. To allow users - to make use of the objects in the schema, additional privileges - might need to be granted, as appropriate for the object. + <literal>USAGE</literal> privilege on the schema. By default, everyone + has that privilege on the schema <literal>public</literal>. To allow + users to make use of the objects in a schema, additional privileges might + need to be granted, as appropriate for the object. </para> <para> - A user can also be allowed to create objects in someone else's - schema. To allow that, the <literal>CREATE</literal> privilege on - the schema needs to be granted. Note that by default, everyone - has <literal>CREATE</literal> and <literal>USAGE</literal> privileges on - the schema - <literal>public</literal>. This allows all users that are able to - connect to a given database to create objects in its - <literal>public</literal> schema. + A user can also be allowed to create objects in someone else's schema. To + allow that, the <literal>CREATE</literal> privilege on the schema needs to + be granted. In databases upgraded from + <productname>PostgreSQL</productname> 13 or earlier, everyone has that + privilege on the schema <literal>public</literal>. Some <link linkend="ddl-schemas-patterns">usage patterns</link> call for revoking that privilege: <programlisting> @@ -3068,20 +3066,25 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; database owner attack. --> <para> Constrain ordinary users to user-private schemas. To implement this, - issue <literal>REVOKE CREATE ON SCHEMA public FROM PUBLIC</literal>, - and create a schema for each user with the same name as that user. - Recall that the default search path starts - with <literal>$user</literal>, which resolves to the user name. - Therefore, if each user has a separate schema, they access their own - schemas by default. After adopting this pattern in a database where - untrusted users had already logged in, consider auditing the public - schema for objects named like objects in + first issue <literal>REVOKE CREATE ON SCHEMA public FROM + PUBLIC</literal>. Then, for every user needing to create non-temporary + objects, create a schema with the same name as that user. Recall that + the default search path starts with <literal>$user</literal>, which + resolves to the user name. Therefore, if each user has a separate + schema, they access their own schemas by default. After adopting this + pattern in a database where untrusted users had already logged in, + consider auditing the public schema for objects named like objects in schema <literal>pg_catalog</literal>. This pattern is a secure schema usage pattern unless an untrusted user is the database owner or holds the <literal>CREATEROLE</literal> privilege, in which case no secure schema usage pattern exists. </para> <para> + If the database originated in an upgrade + from <productname>PostgreSQL</productname> 13 or earlier, + the <literal>REVOKE</literal> is essential. Otherwise, the default + configuration follows this pattern; ordinary users can create only + temporary objects until a privileged user furnishes a schema. </para> </listitem> @@ -3090,10 +3093,10 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; Remove the public schema from the default search path, by modifying <link linkend="config-setting-configuration-file"><filename>postgresql.conf</filename></link> or by issuing <literal>ALTER ROLE ALL SET search_path = - "$user"</literal>. Everyone retains the ability to create objects in - the public schema, but only qualified names will choose those objects. - While qualified table references are fine, calls to functions in the - public schema <link linkend="typeconv-func">will be unsafe or + "$user"</literal>. Then, grant privileges to create in the public + schema. Only qualified names will choose public schema objects. While + qualified table references are fine, calls to functions in the public + schema <link linkend="typeconv-func">will be unsafe or unreliable</link>. If you create functions or extensions in the public schema, use the first pattern instead. Otherwise, like the first pattern, this is secure unless an untrusted user is the database owner @@ -3103,11 +3106,14 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; <listitem> <para> - Keep the default. All users access the public schema implicitly. This + Keep the default search path, and grant privileges to create in the + public schema. All users access the public schema implicitly. This simulates the situation where schemas are not available at all, giving a smooth transition from the non-schema-aware world. However, this is never a secure pattern. It is acceptable only when the database has a - single user or a few mutually-trusting users. + single user or a few mutually-trusting users. In databases upgraded + from <productname>PostgreSQL</productname> 13 or earlier, this is the + default. </para> </listitem> </itemizedlist> diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml index 758493c..768a37f 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -577,13 +577,14 @@ DROP ROLE doomed_role; <para> The <literal>pg_database_owner</literal> role has one implicit, - situation-dependent member, namely the owner of the current database. The - role conveys no rights at first. Like any role, it can own objects or - receive grants of access privileges. Consequently, once - <literal>pg_database_owner</literal> has rights within a template database, - each owner of a database instantiated from that template will exercise those - rights. <literal>pg_database_owner</literal> cannot be a member of any - role, and it cannot have non-implicit members. + situation-dependent member, namely the owner of the current database. Like + any role, it can own objects or receive grants of access privileges. + Consequently, once <literal>pg_database_owner</literal> has rights within a + template database, each owner of a database instantiated from that template + will exercise those rights. <literal>pg_database_owner</literal> cannot be + a member of any role, and it cannot have non-implicit members. Initially, + this role owns the <literal>public</literal> schema, so each database owner + governs local use of the schema. </para> <para> @@ -632,8 +633,8 @@ GRANT pg_signal_backend TO admin_user; horse</quote> others with relative ease. The strongest protection is tight control over who can define objects. Where that is infeasible, write queries referring only to objects having trusted owners. Remove - from <varname>search_path</varname> the public schema and any other schemas - that permit untrusted users to create objects. + from <varname>search_path</varname> any schemas that permit untrusted users + to create objects. </para> <para> diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 62540a1..3a72fbb 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -1696,8 +1696,7 @@ setup_privileges(FILE *cmdfd) CppAsString2(RELKIND_VIEW) ", " CppAsString2(RELKIND_MATVIEW) ", " CppAsString2(RELKIND_SEQUENCE) ")" " AND relacl IS NULL;\n\n", - "GRANT USAGE ON SCHEMA pg_catalog TO PUBLIC;\n\n", - "GRANT CREATE, USAGE ON SCHEMA public TO PUBLIC;\n\n", + "GRANT USAGE ON SCHEMA pg_catalog, public TO PUBLIC;\n\n", "REVOKE ALL ON pg_largeobject FROM PUBLIC;\n\n", "INSERT INTO pg_init_privs " " (objoid, classoid, objsubid, initprivs, privtype)" diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 0a3f40d..d59e063 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -1568,10 +1568,11 @@ selectDumpableNamespace(NamespaceInfo *nsinfo, Archive *fout) * no-mans-land between being a system object and a user object. * CREATE SCHEMA would fail, so its DUMP_COMPONENT_DEFINITION is just * a comment and an indication of ownership. If the owner is the - * default, that DUMP_COMPONENT_DEFINITION is superfluous. + * default, omit that superfluous DUMP_COMPONENT_DEFINITION. Before + * v14, the default owner was BOOTSTRAP_SUPERUSERID. */ nsinfo->dobj.dump = DUMP_COMPONENT_ALL; - if (nsinfo->nspowner == BOOTSTRAP_SUPERUSERID) + if (nsinfo->nspowner == DEFAULT_ROLE_DATABASE_OWNER) nsinfo->dobj.dump &= ~DUMP_COMPONENT_DEFINITION; nsinfo->dobj.dump_contains = DUMP_COMPONENT_ALL; } @@ -4777,21 +4778,26 @@ getNamespaces(Archive *fout, int *numNamespaces) PQExpBuffer init_racl_subquery = createPQExpBuffer(); /* - * Bypass pg_init_privs.initprivs for the public schema. Dropping and - * recreating the schema detaches it from its pg_init_privs row, but - * an empty destination database starts with this ACL nonetheless. - * Also, we support dump/reload of public schema ownership changes. - * ALTER SCHEMA OWNER filters nspacl through aclnewowner(), but - * initprivs continues to reflect the initial owner (the bootstrap - * superuser). Hence, synthesize the value that nspacl will have - * after the restore's ALTER SCHEMA OWNER. + * Bypass pg_init_privs.initprivs for the public schema, for several + * reasons. First, dropping and recreating the schema detaches it + * from its pg_init_privs row, but an empty destination database + * starts with this ACL nonetheless. Second, we support dump/reload + * of public schema ownership changes. ALTER SCHEMA OWNER filters + * nspacl through aclnewowner(), but initprivs continues to reflect + * the initial owner. Hence, synthesize the value that nspacl will + * have after the restore's ALTER SCHEMA OWNER. Third, this makes the + * destination database match the source's ACL, even if the latter was + * an initdb-default ACL, which changed in v14. An upgrade pulls in + * changes to most system object ACLs that the DBA had not customized. + * We've made the public schema depart from that, because changing its + * ACL so easily breaks applications. */ buildACLQueries(acl_subquery, racl_subquery, init_acl_subquery, init_racl_subquery, "n.nspacl", "n.nspowner", "CASE WHEN n.nspname = 'public' THEN array[" " format('%s=UC/%s', " " n.nspowner::regrole, n.nspowner::regrole)," - " format('=UC/%s', n.nspowner::regrole)]::aclitem[] " + " format('=U/%s', n.nspowner::regrole)]::aclitem[] " "ELSE pip.initprivs END", "'n'", dopt->binary_upgrade); diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 4693292..02b4418 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -614,7 +614,9 @@ my %tests = ( }, 'ALTER SCHEMA public OWNER TO' => { - # see test "REVOKE CREATE ON SCHEMA public" for causative create_sql + create_order => 15, + create_sql => + 'ALTER SCHEMA public OWNER TO "regress_quoted \"" role";', regexp => qr/^ALTER SCHEMA public OWNER TO .+;/m, like => { %full_runs, section_pre_data => 1, @@ -3342,16 +3344,12 @@ my %tests = ( unlike => { no_privs => 1, }, }, - 'REVOKE CREATE ON SCHEMA public FROM public' => { + 'REVOKE ALL ON SCHEMA public' => { create_order => 16, - create_sql => ' - REVOKE CREATE ON SCHEMA public FROM public; - ALTER SCHEMA public OWNER TO "regress_quoted \"" role"; - REVOKE ALL ON SCHEMA public FROM "regress_quoted \"" role";', + create_sql => + 'REVOKE ALL ON SCHEMA public FROM "regress_quoted \"" role";', regexp => qr/^ \QREVOKE ALL ON SCHEMA public FROM "regress_quoted \"" role";\E - \n\QREVOKE ALL ON SCHEMA public FROM PUBLIC;\E - \n\QGRANT USAGE ON SCHEMA public TO PUBLIC;\E /xm, like => { %full_runs, section_pre_data => 1, }, unlike => { no_privs => 1, }, diff --git a/src/include/catalog/pg_namespace.dat b/src/include/catalog/pg_namespace.dat index 988f1c4..7932aa6 100644 --- a/src/include/catalog/pg_namespace.dat +++ b/src/include/catalog/pg_namespace.dat @@ -21,6 +21,6 @@ # update dumpComment() if changing this descr { oid => '2200', oid_symbol => 'PG_PUBLIC_NAMESPACE', descr => 'standard public schema', - nspname => 'public', nspacl => '_null_' }, + nspname => 'public', nspowner => 'pg_database_owner', nspacl => '_null_' }, ] diff --git a/src/pl/plperl/expected/plperl_setup.out b/src/pl/plperl/expected/plperl_setup.out index a1a24df..5234feb 100644 --- a/src/pl/plperl/expected/plperl_setup.out +++ b/src/pl/plperl/expected/plperl_setup.out @@ -25,6 +25,9 @@ CREATE EXTENSION plperl; CREATE EXTENSION plperlu; -- fail ERROR: permission denied to create extension "plperlu" HINT: Must be superuser to create this extension. +CREATE SCHEMA plperl_setup_scratch; +SET search_path = plperl_setup_scratch; +GRANT ALL ON SCHEMA plperl_setup_scratch TO regress_user2; CREATE FUNCTION foo1() returns int language plperl as '1;'; SELECT foo1(); foo1 @@ -34,6 +37,7 @@ SELECT foo1(); -- Must reconnect to avoid failure with non-MULTIPLICITY Perl interpreters \c - +SET search_path = plperl_setup_scratch; SET ROLE regress_user1; -- Should be able to change privileges on the language revoke all on language plperl from public; diff --git a/src/pl/plperl/sql/plperl_setup.sql b/src/pl/plperl/sql/plperl_setup.sql index 7484478..a89cf56 100644 --- a/src/pl/plperl/sql/plperl_setup.sql +++ b/src/pl/plperl/sql/plperl_setup.sql @@ -27,12 +27,16 @@ SET ROLE regress_user1; CREATE EXTENSION plperl; CREATE EXTENSION plperlu; -- fail +CREATE SCHEMA plperl_setup_scratch; +SET search_path = plperl_setup_scratch; +GRANT ALL ON SCHEMA plperl_setup_scratch TO regress_user2; CREATE FUNCTION foo1() returns int language plperl as '1;'; SELECT foo1(); -- Must reconnect to avoid failure with non-MULTIPLICITY Perl interpreters \c - +SET search_path = plperl_setup_scratch; SET ROLE regress_user1; diff --git a/src/test/regress/input/tablespace.source b/src/test/regress/input/tablespace.source index c133e73..cb9774e 100644 --- a/src/test/regress/input/tablespace.source +++ b/src/test/regress/input/tablespace.source @@ -388,7 +388,7 @@ CREATE INDEX k ON testschema.tablespace_acl (c) TABLESPACE regress_tblspace; ALTER TABLE testschema.tablespace_acl OWNER TO regress_tablespace_user2; SET SESSION ROLE regress_tablespace_user2; -CREATE TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail +CREATE TEMP TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail ALTER TABLE testschema.tablespace_acl ALTER c TYPE bigint; REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail REINDEX (TABLESPACE regress_tblspace, CONCURRENTLY) TABLE tablespace_table; -- fail @@ -409,3 +409,6 @@ DROP SCHEMA testschema CASCADE; DROP ROLE regress_tablespace_user1; DROP ROLE regress_tablespace_user2; + +-- Rest of this suite can use the public schema freely. +GRANT ALL ON SCHEMA public TO public; diff --git a/src/test/regress/output/tablespace.source b/src/test/regress/output/tablespace.source index 1bbe7e0..e7629d4 100644 --- a/src/test/regress/output/tablespace.source +++ b/src/test/regress/output/tablespace.source @@ -908,7 +908,7 @@ CREATE TABLE testschema.tablespace_acl (c int); CREATE INDEX k ON testschema.tablespace_acl (c) TABLESPACE regress_tblspace; ALTER TABLE testschema.tablespace_acl OWNER TO regress_tablespace_user2; SET SESSION ROLE regress_tablespace_user2; -CREATE TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail +CREATE TEMP TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail ERROR: permission denied for tablespace regress_tblspace ALTER TABLE testschema.tablespace_acl ALTER c TYPE bigint; REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail @@ -934,3 +934,5 @@ drop cascades to table testschema.atable drop cascades to table testschema.tablespace_acl DROP ROLE regress_tablespace_user1; DROP ROLE regress_tablespace_user2; +-- Rest of this suite can use the public schema freely. +GRANT ALL ON SCHEMA public TO public;
Author: Noah Misch <n...@leadboat.com> Commit: Noah Misch <n...@leadboat.com> Rollup of five patches, for easy testing. diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 60c7e11..8b3b37b 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -8925,7 +8925,7 @@ $d$; -- But creation of user mappings for non-superusers should fail CREATE USER MAPPING FOR public SERVER loopback_nopw; CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_nopw; -CREATE FOREIGN TABLE ft1_nopw ( +CREATE FOREIGN TABLE pg_temp.ft1_nopw ( c1 int NOT NULL, c2 int NOT NULL, c3 text, diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 151f4f1..bf35097 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -2595,7 +2595,7 @@ $d$; CREATE USER MAPPING FOR public SERVER loopback_nopw; CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_nopw; -CREATE FOREIGN TABLE ft1_nopw ( +CREATE FOREIGN TABLE pg_temp.ft1_nopw ( c1 int NOT NULL, c2 int NOT NULL, c3 text, diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index ea222c0..8080047 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -10105,6 +10105,9 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <primary>pg_group</primary> </indexterm> + <!-- Unlike information_schema.applicable_roles, this shows no members for + pg_database_owner. The v8.1 catalog would have shown no members if + that role had existed at the time. --> <para> The view <structname>pg_group</structname> exists for backwards compatibility: it emulates a catalog that existed in diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 1e9a462..1b30a0d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2982,20 +2982,18 @@ SELECT 3 OPERATOR(pg_catalog.+) 4; <para> By default, users cannot access any objects in schemas they do not own. To allow that, the owner of the schema must grant the - <literal>USAGE</literal> privilege on the schema. To allow users - to make use of the objects in the schema, additional privileges - might need to be granted, as appropriate for the object. + <literal>USAGE</literal> privilege on the schema. By default, everyone + has that privilege on the schema <literal>public</literal>. To allow + users to make use of the objects in a schema, additional privileges might + need to be granted, as appropriate for the object. </para> <para> - A user can also be allowed to create objects in someone else's - schema. To allow that, the <literal>CREATE</literal> privilege on - the schema needs to be granted. Note that by default, everyone - has <literal>CREATE</literal> and <literal>USAGE</literal> privileges on - the schema - <literal>public</literal>. This allows all users that are able to - connect to a given database to create objects in its - <literal>public</literal> schema. + A user can also be allowed to create objects in someone else's schema. To + allow that, the <literal>CREATE</literal> privilege on the schema needs to + be granted. In databases upgraded from + <productname>PostgreSQL</productname> 13 or earlier, everyone has that + privilege on the schema <literal>public</literal>. Some <link linkend="ddl-schemas-patterns">usage patterns</link> call for revoking that privilege: <programlisting> @@ -3068,20 +3066,25 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; database owner attack. --> <para> Constrain ordinary users to user-private schemas. To implement this, - issue <literal>REVOKE CREATE ON SCHEMA public FROM PUBLIC</literal>, - and create a schema for each user with the same name as that user. - Recall that the default search path starts - with <literal>$user</literal>, which resolves to the user name. - Therefore, if each user has a separate schema, they access their own - schemas by default. After adopting this pattern in a database where - untrusted users had already logged in, consider auditing the public - schema for objects named like objects in + first issue <literal>REVOKE CREATE ON SCHEMA public FROM + PUBLIC</literal>. Then, for every user needing to create non-temporary + objects, create a schema with the same name as that user. Recall that + the default search path starts with <literal>$user</literal>, which + resolves to the user name. Therefore, if each user has a separate + schema, they access their own schemas by default. After adopting this + pattern in a database where untrusted users had already logged in, + consider auditing the public schema for objects named like objects in schema <literal>pg_catalog</literal>. This pattern is a secure schema usage pattern unless an untrusted user is the database owner or holds the <literal>CREATEROLE</literal> privilege, in which case no secure schema usage pattern exists. </para> <para> + If the database originated in an upgrade + from <productname>PostgreSQL</productname> 13 or earlier, + the <literal>REVOKE</literal> is essential. Otherwise, the default + configuration follows this pattern; ordinary users can create only + temporary objects until a privileged user furnishes a schema. </para> </listitem> @@ -3090,10 +3093,10 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; Remove the public schema from the default search path, by modifying <link linkend="config-setting-configuration-file"><filename>postgresql.conf</filename></link> or by issuing <literal>ALTER ROLE ALL SET search_path = - "$user"</literal>. Everyone retains the ability to create objects in - the public schema, but only qualified names will choose those objects. - While qualified table references are fine, calls to functions in the - public schema <link linkend="typeconv-func">will be unsafe or + "$user"</literal>. Then, grant privileges to create in the public + schema. Only qualified names will choose public schema objects. While + qualified table references are fine, calls to functions in the public + schema <link linkend="typeconv-func">will be unsafe or unreliable</link>. If you create functions or extensions in the public schema, use the first pattern instead. Otherwise, like the first pattern, this is secure unless an untrusted user is the database owner @@ -3103,11 +3106,14 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; <listitem> <para> - Keep the default. All users access the public schema implicitly. This + Keep the default search path, and grant privileges to create in the + public schema. All users access the public schema implicitly. This simulates the situation where schemas are not available at all, giving a smooth transition from the non-schema-aware world. However, this is never a secure pattern. It is acceptable only when the database has a - single user or a few mutually-trusting users. + single user or a few mutually-trusting users. In databases upgraded + from <productname>PostgreSQL</productname> 13 or earlier, this is the + default. </para> </listitem> </itemizedlist> diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml index cc08252..768a37f 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -540,6 +540,10 @@ DROP ROLE doomed_role; <literal>pg_stat_scan_tables</literal>.</entry> </row> <row> + <entry>pg_database_owner</entry> + <entry>None. Membership consists, implicitly, of the current database owner.</entry> + </row> + <row> <entry>pg_signal_backend</entry> <entry>Signal another backend to cancel a query or terminate its session.</entry> </row> @@ -572,6 +576,18 @@ DROP ROLE doomed_role; </para> <para> + The <literal>pg_database_owner</literal> role has one implicit, + situation-dependent member, namely the owner of the current database. Like + any role, it can own objects or receive grants of access privileges. + Consequently, once <literal>pg_database_owner</literal> has rights within a + template database, each owner of a database instantiated from that template + will exercise those rights. <literal>pg_database_owner</literal> cannot be + a member of any role, and it cannot have non-implicit members. Initially, + this role owns the <literal>public</literal> schema, so each database owner + governs local use of the schema. + </para> + + <para> The <literal>pg_signal_backend</literal> role is intended to allow administrators to enable trusted, but non-superuser, roles to send signals to other backends. Currently this role enables sending of signals for @@ -617,8 +633,8 @@ GRANT pg_signal_backend TO admin_user; horse</quote> others with relative ease. The strongest protection is tight control over who can define objects. Where that is infeasible, write queries referring only to objects having trusted owners. Remove - from <varname>search_path</varname> the public schema and any other schemas - that permit untrusted users to create objects. + from <varname>search_path</varname> any schemas that permit untrusted users + to create objects. </para> <para> diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql index 4907855..9a0169b 100644 --- a/src/backend/catalog/information_schema.sql +++ b/src/backend/catalog/information_schema.sql @@ -255,7 +255,14 @@ CREATE VIEW applicable_roles AS SELECT CAST(a.rolname AS sql_identifier) AS grantee, CAST(b.rolname AS sql_identifier) AS role_name, CAST(CASE WHEN m.admin_option THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_grantable - FROM pg_auth_members m + FROM (SELECT member, roleid, admin_option FROM pg_auth_members + -- This UNION could be UNION ALL, but UNION works even if we start + -- to allow explicit pg_database_owner membership. + UNION + SELECT datdba, pg_authid.oid, false + FROM pg_database, pg_authid + WHERE datname = current_database() AND rolname = 'pg_database_owner' + ) m JOIN pg_authid a ON (m.member = a.oid) JOIN pg_authid b ON (m.roleid = b.oid) WHERE pg_has_role(a.oid, 'USAGE'); diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index ed243e3..74be182 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -1497,6 +1497,18 @@ AddRoleMems(const char *rolename, Oid roleid, } /* + * The charter of pg_database_owner is to have exactly one, implicit, + * situation-dependent member. There's no technical need for this + * restriction. (One could lift it and take the further step of making + * pg_database_ownercheck() equivalent to has_privs_of_role(roleid, + * DEFAULT_ROLE_DATABASE_OWNER), in which case explicit, + * situation-independent members could act as the owner of any database.) + */ + if (roleid == DEFAULT_ROLE_DATABASE_OWNER) + ereport(ERROR, + errmsg("role \"%s\" cannot have explicit members", rolename)); + + /* * The role membership grantor of record has little significance at * present. Nonetheless, inasmuch as users might look to it for a crude * audit trail, let only superusers impute the grant to a third party. @@ -1525,6 +1537,22 @@ AddRoleMems(const char *rolename, Oid roleid, bool new_record_repl[Natts_pg_auth_members]; /* + * pg_database_owner is never a role member. Lifting this restriction + * would require a policy decision about membership loops. One could + * prevent loops, which would include making "ALTER DATABASE x OWNER + * TO proposed_datdba" fail if is_member_of_role(pg_database_owner, + * proposed_datdba). Hence, gaining a membership could reduce what a + * role could do. Alternately, one could allow these memberships to + * complete loops. A role could then have actual WITH ADMIN OPTION on + * itself, prompting a decision about is_admin_of_role() treatment of + * the case. + */ + if (memberid == DEFAULT_ROLE_DATABASE_OWNER) + ereport(ERROR, + errmsg("role \"%s\" cannot be a member of any role", + get_rolespec_name(memberRole))); + + /* * Refuse creation of membership loops, including the trivial case * where a role is made a member of itself. We do this by checking to * see if the target role is already a member of the proposed member diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index c7f029e..34975ee 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -22,6 +22,7 @@ #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.h" #include "catalog/pg_class.h" +#include "catalog/pg_database.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" #include "commands/proclang.h" @@ -50,32 +51,25 @@ typedef struct /* * We frequently need to test whether a given role is a member of some other * role. In most of these tests the "given role" is the same, namely the - * active current user. So we can optimize it by keeping a cached list of - * all the roles the "given role" is a member of, directly or indirectly. - * - * There are actually two caches, one computed under "has_privs" rules - * (do not recurse where rolinherit isn't true) and one computed under - * "is_member" rules (recurse regardless of rolinherit). + * active current user. So we can optimize it by keeping cached lists of all + * the roles the "given role" is a member of, directly or indirectly. * * Possibly this mechanism should be generalized to allow caching membership * info for multiple roles? * - * The has_privs cache is: - * cached_privs_role is the role OID the cache is for. - * cached_privs_roles is an OID list of roles that cached_privs_role - * has the privileges of (always including itself). - * The cache is valid if cached_privs_role is not InvalidOid. - * - * The is_member cache is similarly: - * cached_member_role is the role OID the cache is for. - * cached_membership_roles is an OID list of roles that cached_member_role - * is a member of (always including itself). - * The cache is valid if cached_member_role is not InvalidOid. + * Each element of cached_roles is an OID list of constituent roles for the + * corresponding element of cached_role (always including the cached_role + * itself). One cache has ROLERECURSE_PRIVS semantics, and the other has + * ROLERECURSE_MEMBERS semantics. */ -static Oid cached_privs_role = InvalidOid; -static List *cached_privs_roles = NIL; -static Oid cached_member_role = InvalidOid; -static List *cached_membership_roles = NIL; +enum RoleRecurseType +{ + ROLERECURSE_PRIVS = 0, /* recurse if rolinherit */ + ROLERECURSE_MEMBERS = 1 /* recurse unconditionally */ +}; +static Oid cached_role[] = {InvalidOid, InvalidOid}; +static List *cached_roles[] = {NIL, NIL}; +static uint32 cached_db_hash; static const char *getid(const char *s, char *n); @@ -4673,10 +4667,14 @@ initialize_acl(void) { if (!IsBootstrapProcessingMode()) { + cached_db_hash = + GetSysCacheHashValue1(DATABASEOID, + ObjectIdGetDatum(MyDatabaseId)); + /* * In normal mode, set a callback on any syscache invalidation of rows - * of pg_auth_members (for each AUTHMEM search in this file) or - * pg_authid (for has_rolinherit()) + * of pg_auth_members (for roles_is_member_of()), pg_authid (for + * has_rolinherit()), or pg_database (for roles_is_member_of()) */ CacheRegisterSyscacheCallback(AUTHMEMROLEMEM, RoleMembershipCacheCallback, @@ -4684,6 +4682,9 @@ initialize_acl(void) CacheRegisterSyscacheCallback(AUTHOID, RoleMembershipCacheCallback, (Datum) 0); + CacheRegisterSyscacheCallback(DATABASEOID, + RoleMembershipCacheCallback, + (Datum) 0); } } @@ -4694,9 +4695,16 @@ initialize_acl(void) static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue) { + if (cacheid == DATABASEOID && + hashvalue != cached_db_hash && + hashvalue != 0) + { + return; /* ignore pg_database changes for other DBs */ + } + /* Force membership caches to be recomputed on next use */ - cached_privs_role = InvalidOid; - cached_member_role = InvalidOid; + cached_role[ROLERECURSE_PRIVS] = InvalidOid; + cached_role[ROLERECURSE_MEMBERS] = InvalidOid; } @@ -4718,115 +4726,54 @@ has_rolinherit(Oid roleid) /* - * Get a list of roles that the specified roleid has the privileges of + * Get a list of roles that the specified roleid is a member of * - * This is defined not to recurse through roles that don't have rolinherit - * set; for such roles, membership implies the ability to do SET ROLE, but - * the privileges are not available until you've done so. + * Type ROLERECURSE_PRIVS recurses only through roles that have rolinherit + * set, while ROLERECURSE_MEMBERS recurses through all roles. This sets + * *is_admin==true if and only if role "roleid" has an ADMIN OPTION membership + * in role "admin_of". * * Since indirect membership testing is relatively expensive, we cache * a list of memberships. Hence, the result is only guaranteed good until - * the next call of roles_has_privs_of()! + * the next call of roles_is_member_of()! * * For the benefit of select_best_grantor, the result is defined to be * in breadth-first order, ie, closer relationships earlier. */ static List * -roles_has_privs_of(Oid roleid) +roles_is_member_of(Oid roleid, enum RoleRecurseType type, + Oid admin_of, bool *is_admin) { + Oid dba; List *roles_list; ListCell *l; - List *new_cached_privs_roles; + List *new_cached_roles; MemoryContext oldctx; - /* If cache is already valid, just return the list */ - if (OidIsValid(cached_privs_role) && cached_privs_role == roleid) - return cached_privs_roles; + /* If cache is valid and ADMIN OPTION not sought, just return the list */ + if (cached_role[type] == roleid && !OidIsValid(admin_of) && + OidIsValid(cached_role[type])) + return cached_roles[type]; /* - * Find all the roles that roleid is a member of, including multi-level - * recursion. The role itself will always be the first element of the - * resulting list. - * - * Each element of the list is scanned to see if it adds any indirect - * memberships. We can use a single list as both the record of - * already-found memberships and the agenda of roles yet to be scanned. - * This is a bit tricky but works because the foreach() macro doesn't - * fetch the next list element until the bottom of the loop. + * Role expansion happens in a non-database backend when guc.c checks + * DEFAULT_ROLE_READ_ALL_SETTINGS for a physical walsender SHOW command. + * In that case, no role gets pg_database_owner. */ - roles_list = list_make1_oid(roleid); - - foreach(l, roles_list) + if (!OidIsValid(MyDatabaseId)) + dba = InvalidOid; + else { - Oid memberid = lfirst_oid(l); - CatCList *memlist; - int i; - - /* Ignore non-inheriting roles */ - if (!has_rolinherit(memberid)) - continue; + HeapTuple dbtup; - /* Find roles that memberid is directly a member of */ - memlist = SearchSysCacheList1(AUTHMEMMEMROLE, - ObjectIdGetDatum(memberid)); - for (i = 0; i < memlist->n_members; i++) - { - HeapTuple tup = &memlist->members[i]->tuple; - Oid otherid = ((Form_pg_auth_members) GETSTRUCT(tup))->roleid; - - /* - * Even though there shouldn't be any loops in the membership - * graph, we must test for having already seen this role. It is - * legal for instance to have both A->B and A->C->B. - */ - roles_list = list_append_unique_oid(roles_list, otherid); - } - ReleaseSysCacheList(memlist); + dbtup = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId)); + if (!HeapTupleIsValid(dbtup)) + elog(ERROR, "cache lookup failed for database %u", MyDatabaseId); + dba = ((Form_pg_database) GETSTRUCT(dbtup))->datdba; + ReleaseSysCache(dbtup); } /* - * Copy the completed list into TopMemoryContext so it will persist. - */ - oldctx = MemoryContextSwitchTo(TopMemoryContext); - new_cached_privs_roles = list_copy(roles_list); - MemoryContextSwitchTo(oldctx); - list_free(roles_list); - - /* - * Now safe to assign to state variable - */ - cached_privs_role = InvalidOid; /* just paranoia */ - list_free(cached_privs_roles); - cached_privs_roles = new_cached_privs_roles; - cached_privs_role = roleid; - - /* And now we can return the answer */ - return cached_privs_roles; -} - - -/* - * Get a list of roles that the specified roleid is a member of - * - * This is defined to recurse through roles regardless of rolinherit. - * - * Since indirect membership testing is relatively expensive, we cache - * a list of memberships. Hence, the result is only guaranteed good until - * the next call of roles_is_member_of()! - */ -static List * -roles_is_member_of(Oid roleid) -{ - List *roles_list; - ListCell *l; - List *new_cached_membership_roles; - MemoryContext oldctx; - - /* If cache is already valid, just return the list */ - if (OidIsValid(cached_member_role) && cached_member_role == roleid) - return cached_membership_roles; - - /* * Find all the roles that roleid is a member of, including multi-level * recursion. The role itself will always be the first element of the * resulting list. @@ -4845,6 +4792,9 @@ roles_is_member_of(Oid roleid) CatCList *memlist; int i; + if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid)) + continue; /* ignore non-inheriting roles */ + /* Find roles that memberid is directly a member of */ memlist = SearchSysCacheList1(AUTHMEMMEMROLE, ObjectIdGetDatum(memberid)); @@ -4854,6 +4804,15 @@ roles_is_member_of(Oid roleid) Oid otherid = ((Form_pg_auth_members) GETSTRUCT(tup))->roleid; /* + * While otherid==InvalidOid shouldn't appear in the catalog, the + * OidIsValid() avoids crashing if that arises. + */ + if (otherid == admin_of && + ((Form_pg_auth_members) GETSTRUCT(tup))->admin_option && + OidIsValid(admin_of)) + *is_admin = true; + + /* * Even though there shouldn't be any loops in the membership * graph, we must test for having already seen this role. It is * legal for instance to have both A->B and A->C->B. @@ -4861,26 +4820,31 @@ roles_is_member_of(Oid roleid) roles_list = list_append_unique_oid(roles_list, otherid); } ReleaseSysCacheList(memlist); + + /* implement pg_database_owner implicit membership */ + if (memberid == dba && OidIsValid(dba)) + roles_list = list_append_unique_oid(roles_list, + DEFAULT_ROLE_DATABASE_OWNER); } /* * Copy the completed list into TopMemoryContext so it will persist. */ oldctx = MemoryContextSwitchTo(TopMemoryContext); - new_cached_membership_roles = list_copy(roles_list); + new_cached_roles = list_copy(roles_list); MemoryContextSwitchTo(oldctx); list_free(roles_list); /* * Now safe to assign to state variable */ - cached_member_role = InvalidOid; /* just paranoia */ - list_free(cached_membership_roles); - cached_membership_roles = new_cached_membership_roles; - cached_member_role = roleid; + cached_role[type] = InvalidOid; /* just paranoia */ + list_free(cached_roles[type]); + cached_roles[type] = new_cached_roles; + cached_role[type] = roleid; /* And now we can return the answer */ - return cached_membership_roles; + return cached_roles[type]; } @@ -4906,7 +4870,9 @@ has_privs_of_role(Oid member, Oid role) * Find all the roles that member has the privileges of, including * multi-level recursion, then see if target role is any one of them. */ - return list_member_oid(roles_has_privs_of(member), role); + return list_member_oid(roles_is_member_of(member, ROLERECURSE_PRIVS, + InvalidOid, NULL), + role); } @@ -4930,7 +4896,9 @@ is_member_of_role(Oid member, Oid role) * Find all the roles that member is a member of, including multi-level * recursion, then see if target role is any one of them. */ - return list_member_oid(roles_is_member_of(member), role); + return list_member_oid(roles_is_member_of(member, ROLERECURSE_MEMBERS, + InvalidOid, NULL), + role); } /* @@ -4964,7 +4932,9 @@ is_member_of_role_nosuper(Oid member, Oid role) * Find all the roles that member is a member of, including multi-level * recursion, then see if target role is any one of them. */ - return list_member_oid(roles_is_member_of(member), role); + return list_member_oid(roles_is_member_of(member, ROLERECURSE_MEMBERS, + InvalidOid, NULL), + role); } @@ -4977,8 +4947,6 @@ bool is_admin_of_role(Oid member, Oid role) { bool result = false; - List *roles_list; - ListCell *l; if (superuser_arg(member)) return true; @@ -5016,44 +4984,7 @@ is_admin_of_role(Oid member, Oid role) return member == GetSessionUserId() && !InLocalUserIdChange() && !InSecurityRestrictedOperation(); - /* - * Find all the roles that member is a member of, including multi-level - * recursion. We build a list in the same way that is_member_of_role does - * to track visited and unvisited roles. - */ - roles_list = list_make1_oid(member); - - foreach(l, roles_list) - { - Oid memberid = lfirst_oid(l); - CatCList *memlist; - int i; - - /* Find roles that memberid is directly a member of */ - memlist = SearchSysCacheList1(AUTHMEMMEMROLE, - ObjectIdGetDatum(memberid)); - for (i = 0; i < memlist->n_members; i++) - { - HeapTuple tup = &memlist->members[i]->tuple; - Oid otherid = ((Form_pg_auth_members) GETSTRUCT(tup))->roleid; - - if (otherid == role && - ((Form_pg_auth_members) GETSTRUCT(tup))->admin_option) - { - /* Found what we came for, so can stop searching */ - result = true; - break; - } - - roles_list = list_append_unique_oid(roles_list, otherid); - } - ReleaseSysCacheList(memlist); - if (result) - break; - } - - list_free(roles_list); - + (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result); return result; } @@ -5128,7 +5059,8 @@ select_best_grantor(Oid roleId, AclMode privileges, * roles_has_privs_of() throughout this loop, because aclmask_direct() * doesn't query any role memberships. */ - roles_list = roles_has_privs_of(roleId); + roles_list = roles_is_member_of(roleId, ROLERECURSE_PRIVS, + InvalidOid, NULL); /* initialize candidate result as default */ *grantorId = roleId; diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index fa2b49c..19926f9 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -1076,8 +1076,9 @@ InitCatCachePhase2(CatCache *cache, bool touch_index) * criticalRelcachesBuilt), we don't have to worry anymore. * * Similarly, during backend startup we have to be able to use the - * pg_authid and pg_auth_members syscaches for authentication even if - * we don't yet have relcache entries for those catalogs' indexes. + * pg_authid, pg_auth_members and pg_database syscaches for + * authentication even if we don't yet have relcache entries for those + * catalogs' indexes. */ static bool IndexScanOK(CatCache *cache, ScanKey cur_skey) @@ -1110,6 +1111,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey) case AUTHNAME: case AUTHOID: case AUTHMEMMEMROLE: + case DATABASEOID: /* * Protect authentication lookups occurring before relcache has diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 62540a1..3a72fbb 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -1696,8 +1696,7 @@ setup_privileges(FILE *cmdfd) CppAsString2(RELKIND_VIEW) ", " CppAsString2(RELKIND_MATVIEW) ", " CppAsString2(RELKIND_SEQUENCE) ")" " AND relacl IS NULL;\n\n", - "GRANT USAGE ON SCHEMA pg_catalog TO PUBLIC;\n\n", - "GRANT CREATE, USAGE ON SCHEMA public TO PUBLIC;\n\n", + "GRANT USAGE ON SCHEMA pg_catalog, public TO PUBLIC;\n\n", "REVOKE ALL ON pg_largeobject FROM PUBLIC;\n\n", "INSERT INTO pg_init_privs " " (objoid, classoid, objsubid, initprivs, privtype)" diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 60d306e..ea67e52 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -725,6 +725,7 @@ void buildACLQueries(PQExpBuffer acl_subquery, PQExpBuffer racl_subquery, PQExpBuffer init_acl_subquery, PQExpBuffer init_racl_subquery, const char *acl_column, const char *acl_owner, + const char *initprivs_expr, const char *obj_kind, bool binary_upgrade) { /* @@ -765,23 +766,25 @@ buildACLQueries(PQExpBuffer acl_subquery, PQExpBuffer racl_subquery, "WITH ORDINALITY AS perm(acl,row_n) " "WHERE NOT EXISTS ( " "SELECT 1 FROM " - "pg_catalog.unnest(coalesce(pip.initprivs,pg_catalog.acldefault(%s,%s))) " + "pg_catalog.unnest(coalesce(%s,pg_catalog.acldefault(%s,%s))) " "AS init(init_acl) WHERE acl = init_acl)) as foo)", acl_column, obj_kind, acl_owner, + initprivs_expr, obj_kind, acl_owner); printfPQExpBuffer(racl_subquery, "(SELECT pg_catalog.array_agg(acl ORDER BY row_n) FROM " "(SELECT acl, row_n FROM " - "pg_catalog.unnest(coalesce(pip.initprivs,pg_catalog.acldefault(%s,%s))) " + "pg_catalog.unnest(coalesce(%s,pg_catalog.acldefault(%s,%s))) " "WITH ORDINALITY AS initp(acl,row_n) " "WHERE NOT EXISTS ( " "SELECT 1 FROM " "pg_catalog.unnest(coalesce(%s,pg_catalog.acldefault(%s,%s))) " "AS permp(orig_acl) WHERE acl = orig_acl)) as foo)", + initprivs_expr, obj_kind, acl_owner, acl_column, @@ -807,12 +810,13 @@ buildACLQueries(PQExpBuffer acl_subquery, PQExpBuffer racl_subquery, printfPQExpBuffer(init_acl_subquery, "CASE WHEN privtype = 'e' THEN " "(SELECT pg_catalog.array_agg(acl ORDER BY row_n) FROM " - "(SELECT acl, row_n FROM pg_catalog.unnest(pip.initprivs) " + "(SELECT acl, row_n FROM pg_catalog.unnest(%s) " "WITH ORDINALITY AS initp(acl,row_n) " "WHERE NOT EXISTS ( " "SELECT 1 FROM " "pg_catalog.unnest(pg_catalog.acldefault(%s,%s)) " "AS privm(orig_acl) WHERE acl = orig_acl)) as foo) END", + initprivs_expr, obj_kind, acl_owner); @@ -823,10 +827,11 @@ buildACLQueries(PQExpBuffer acl_subquery, PQExpBuffer racl_subquery, "pg_catalog.unnest(pg_catalog.acldefault(%s,%s)) " "WITH ORDINALITY AS privp(acl,row_n) " "WHERE NOT EXISTS ( " - "SELECT 1 FROM pg_catalog.unnest(pip.initprivs) " + "SELECT 1 FROM pg_catalog.unnest(%s) " "AS initp(init_acl) WHERE acl = init_acl)) as foo) END", obj_kind, - acl_owner); + acl_owner, + initprivs_expr); } else { diff --git a/src/bin/pg_dump/dumputils.h b/src/bin/pg_dump/dumputils.h index 6e97da7..f5465f1 100644 --- a/src/bin/pg_dump/dumputils.h +++ b/src/bin/pg_dump/dumputils.h @@ -54,6 +54,7 @@ extern void emitShSecLabels(PGconn *conn, PGresult *res, extern void buildACLQueries(PQExpBuffer acl_subquery, PQExpBuffer racl_subquery, PQExpBuffer init_acl_subquery, PQExpBuffer init_racl_subquery, const char *acl_column, const char *acl_owner, + const char *initprivs_expr, const char *obj_kind, bool binary_upgrade); extern bool variable_is_guc_list_quote(const char *name); diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 1f82c64..a16d149 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3597,10 +3597,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData) * Actually print the definition. * * Really crude hack for suppressing AUTHORIZATION clause that old pg_dump - * versions put into CREATE SCHEMA. We have to do this when --no-owner - * mode is selected. This is ugly, but I see no other good way ... + * versions put into CREATE SCHEMA. Don't mutate the modern definition + * for schema "public", which consists of a comment. We have to do this + * when --no-owner mode is selected. This is ugly, but I see no other + * good way ... */ - if (ropt->noOwner && strcmp(te->desc, "SCHEMA") == 0) + if (ropt->noOwner && + strcmp(te->desc, "SCHEMA") == 0 && strncmp(te->defn, "--", 2) != 0) { ahprintf(AH, "CREATE SCHEMA %s;\n\n\n", fmtId(te->tag)); } @@ -3612,11 +3615,16 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData) /* * If we aren't using SET SESSION AUTH to determine ownership, we must - * instead issue an ALTER OWNER command. We assume that anything without - * a DROP command is not a separately ownable object. All the categories - * with DROP commands must appear in one list or the other. + * instead issue an ALTER OWNER command. Schema "public" is special; a + * dump never creates it, so we use ALTER OWNER even when using SET + * SESSION for all other objects. We assume that anything without a DROP + * command is not a separately ownable object. All the categories with + * DROP commands must appear in one list or the other. */ - if (!ropt->noOwner && !ropt->use_setsessauth && + if (!ropt->noOwner && + (!ropt->use_setsessauth || + (strcmp(te->tag, "public") == 0 + && strcmp(te->desc, "SCHEMA") == 0)) && te->owner && strlen(te->owner) > 0 && te->dropStmt && strlen(te->dropStmt) > 0) { diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index d99b61e..d59e063 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -44,6 +44,7 @@ #include "catalog/pg_aggregate_d.h" #include "catalog/pg_am_d.h" #include "catalog/pg_attribute_d.h" +#include "catalog/pg_authid_d.h" #include "catalog/pg_cast_d.h" #include "catalog/pg_class_d.h" #include "catalog/pg_collation_d.h" @@ -1564,13 +1565,15 @@ selectDumpableNamespace(NamespaceInfo *nsinfo, Archive *fout) { /* * The public schema is a strange beast that sits in a sort of - * no-mans-land between being a system object and a user object. We - * don't want to dump creation or comment commands for it, because - * that complicates matters for non-superuser use of pg_dump. But we - * should dump any ACL changes that have occurred for it, and of - * course we should dump contained objects. + * no-mans-land between being a system object and a user object. + * CREATE SCHEMA would fail, so its DUMP_COMPONENT_DEFINITION is just + * a comment and an indication of ownership. If the owner is the + * default, omit that superfluous DUMP_COMPONENT_DEFINITION. Before + * v14, the default owner was BOOTSTRAP_SUPERUSERID. */ - nsinfo->dobj.dump = DUMP_COMPONENT_ACL; + nsinfo->dobj.dump = DUMP_COMPONENT_ALL; + if (nsinfo->nspowner == DEFAULT_ROLE_DATABASE_OWNER) + nsinfo->dobj.dump &= ~DUMP_COMPONENT_DEFINITION; nsinfo->dobj.dump_contains = DUMP_COMPONENT_ALL; } else @@ -3352,8 +3355,8 @@ getBlobs(Archive *fout) PQExpBuffer init_racl_subquery = createPQExpBuffer(); buildACLQueries(acl_subquery, racl_subquery, init_acl_subquery, - init_racl_subquery, "l.lomacl", "l.lomowner", "'L'", - dopt->binary_upgrade); + init_racl_subquery, "l.lomacl", "l.lomowner", + "pip.initprivs", "'L'", dopt->binary_upgrade); appendPQExpBuffer(blobQry, "SELECT l.oid, (%s l.lomowner) AS rolname, " @@ -4754,6 +4757,7 @@ getNamespaces(Archive *fout, int *numNamespaces) int i_tableoid; int i_oid; int i_nspname; + int i_nspowner; int i_rolname; int i_nspacl; int i_rnspacl; @@ -4773,11 +4777,32 @@ getNamespaces(Archive *fout, int *numNamespaces) PQExpBuffer init_acl_subquery = createPQExpBuffer(); PQExpBuffer init_racl_subquery = createPQExpBuffer(); + /* + * Bypass pg_init_privs.initprivs for the public schema, for several + * reasons. First, dropping and recreating the schema detaches it + * from its pg_init_privs row, but an empty destination database + * starts with this ACL nonetheless. Second, we support dump/reload + * of public schema ownership changes. ALTER SCHEMA OWNER filters + * nspacl through aclnewowner(), but initprivs continues to reflect + * the initial owner. Hence, synthesize the value that nspacl will + * have after the restore's ALTER SCHEMA OWNER. Third, this makes the + * destination database match the source's ACL, even if the latter was + * an initdb-default ACL, which changed in v14. An upgrade pulls in + * changes to most system object ACLs that the DBA had not customized. + * We've made the public schema depart from that, because changing its + * ACL so easily breaks applications. + */ buildACLQueries(acl_subquery, racl_subquery, init_acl_subquery, - init_racl_subquery, "n.nspacl", "n.nspowner", "'n'", - dopt->binary_upgrade); + init_racl_subquery, "n.nspacl", "n.nspowner", + "CASE WHEN n.nspname = 'public' THEN array[" + " format('%s=UC/%s', " + " n.nspowner::regrole, n.nspowner::regrole)," + " format('=U/%s', n.nspowner::regrole)]::aclitem[] " + "ELSE pip.initprivs END", + "'n'", dopt->binary_upgrade); appendPQExpBuffer(query, "SELECT n.tableoid, n.oid, n.nspname, " + "n.nspowner, " "(%s nspowner) AS rolname, " "%s as nspacl, " "%s as rnspacl, " @@ -4802,7 +4827,7 @@ getNamespaces(Archive *fout, int *numNamespaces) destroyPQExpBuffer(init_racl_subquery); } else - appendPQExpBuffer(query, "SELECT tableoid, oid, nspname, " + appendPQExpBuffer(query, "SELECT tableoid, oid, nspname, nspowner, " "(%s nspowner) AS rolname, " "nspacl, NULL as rnspacl, " "NULL AS initnspacl, NULL as initrnspacl " @@ -4818,6 +4843,7 @@ getNamespaces(Archive *fout, int *numNamespaces) i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); i_nspname = PQfnumber(res, "nspname"); + i_nspowner = PQfnumber(res, "nspowner"); i_rolname = PQfnumber(res, "rolname"); i_nspacl = PQfnumber(res, "nspacl"); i_rnspacl = PQfnumber(res, "rnspacl"); @@ -4831,6 +4857,7 @@ getNamespaces(Archive *fout, int *numNamespaces) nsinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); AssignDumpId(&nsinfo[i].dobj); nsinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_nspname)); + nsinfo[i].nspowner = atooid(PQgetvalue(res, i, i_nspowner)); nsinfo[i].rolname = pg_strdup(PQgetvalue(res, i, i_rolname)); nsinfo[i].nspacl = pg_strdup(PQgetvalue(res, i, i_nspacl)); nsinfo[i].rnspacl = pg_strdup(PQgetvalue(res, i, i_rnspacl)); @@ -5022,8 +5049,8 @@ getTypes(Archive *fout, int *numTypes) PQExpBuffer initracl_subquery = createPQExpBuffer(); buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, - initracl_subquery, "t.typacl", "t.typowner", "'T'", - dopt->binary_upgrade); + initracl_subquery, "t.typacl", "t.typowner", + "pip.initprivs", "'T'", dopt->binary_upgrade); appendPQExpBuffer(query, "SELECT t.tableoid, t.oid, t.typname, " "t.typnamespace, " @@ -5724,8 +5751,8 @@ getAggregates(Archive *fout, int *numAggs) const char *agg_check; buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, - initracl_subquery, "p.proacl", "p.proowner", "'f'", - dopt->binary_upgrade); + initracl_subquery, "p.proacl", "p.proowner", + "pip.initprivs", "'f'", dopt->binary_upgrade); agg_check = (fout->remoteVersion >= 110000 ? "p.prokind = 'a'" : "p.proisagg"); @@ -5937,8 +5964,8 @@ getFuncs(Archive *fout, int *numFuncs) const char *not_agg_check; buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, - initracl_subquery, "p.proacl", "p.proowner", "'f'", - dopt->binary_upgrade); + initracl_subquery, "p.proacl", "p.proowner", + "pip.initprivs", "'f'", dopt->binary_upgrade); not_agg_check = (fout->remoteVersion >= 110000 ? "p.prokind <> 'a'" : "NOT p.proisagg"); @@ -6234,13 +6261,14 @@ getTables(Archive *fout, int *numTables) buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, initracl_subquery, "c.relacl", "c.relowner", + "pip.initprivs", "CASE WHEN c.relkind = " CppAsString2(RELKIND_SEQUENCE) " THEN 's' ELSE 'r' END::\"char\"", dopt->binary_upgrade); buildACLQueries(attacl_subquery, attracl_subquery, attinitacl_subquery, - attinitracl_subquery, "at.attacl", "c.relowner", "'c'", - dopt->binary_upgrade); + attinitracl_subquery, "at.attacl", "c.relowner", + "pip.initprivs", "'c'", dopt->binary_upgrade); appendPQExpBuffer(query, "SELECT c.tableoid, c.oid, c.relname, " @@ -8238,8 +8266,8 @@ getProcLangs(Archive *fout, int *numProcLangs) PQExpBuffer initracl_subquery = createPQExpBuffer(); buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, - initracl_subquery, "l.lanacl", "l.lanowner", "'l'", - dopt->binary_upgrade); + initracl_subquery, "l.lanacl", "l.lanowner", + "pip.initprivs", "'l'", dopt->binary_upgrade); /* pg_language has a laninline column */ appendPQExpBuffer(query, "SELECT l.tableoid, l.oid, " @@ -9420,8 +9448,8 @@ getForeignDataWrappers(Archive *fout, int *numForeignDataWrappers) PQExpBuffer initracl_subquery = createPQExpBuffer(); buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, - initracl_subquery, "f.fdwacl", "f.fdwowner", "'F'", - dopt->binary_upgrade); + initracl_subquery, "f.fdwacl", "f.fdwowner", + "pip.initprivs", "'F'", dopt->binary_upgrade); appendPQExpBuffer(query, "SELECT f.tableoid, f.oid, f.fdwname, " "(%s f.fdwowner) AS rolname, " @@ -9587,8 +9615,8 @@ getForeignServers(Archive *fout, int *numForeignServers) PQExpBuffer initracl_subquery = createPQExpBuffer(); buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, - initracl_subquery, "f.srvacl", "f.srvowner", "'S'", - dopt->binary_upgrade); + initracl_subquery, "f.srvacl", "f.srvowner", + "pip.initprivs", "'S'", dopt->binary_upgrade); appendPQExpBuffer(query, "SELECT f.tableoid, f.oid, f.srvname, " "(%s f.srvowner) AS rolname, " @@ -9734,6 +9762,7 @@ getDefaultACLs(Archive *fout, int *numDefaultACLs) buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, initracl_subquery, "defaclacl", "defaclrole", + "pip.initprivs", "CASE WHEN defaclobjtype = 'S' THEN 's' ELSE defaclobjtype END::\"char\"", dopt->binary_upgrade); @@ -9888,6 +9917,28 @@ dumpComment(Archive *fout, const char *type, const char *name, ncomments--; } + /* + * Skip dumping the initdb-provided public schema comment, which would + * complicate matters for non-superuser use of pg_dump. When the DBA has + * removed initdb's comment, replicate that. + */ + if (strcmp(type, "SCHEMA") == 0 && strcmp(name, "public") == 0) + { + static CommentItem empty_comment = {.descr = ""}; + + if (ncomments == 0) + { + comments = &empty_comment; + ncomments = 1; + } + else if (strcmp(comments->descr, (fout->remoteVersion >= 80300 ? + "standard public schema" : + "Standard public schema")) == 0) + { + ncomments = 0; + } + } + /* If a comment exists, build COMMENT ON statement */ if (ncomments > 0) { @@ -10351,9 +10402,19 @@ dumpNamespace(Archive *fout, NamespaceInfo *nspinfo) qnspname = pg_strdup(fmtId(nspinfo->dobj.name)); - appendPQExpBuffer(delq, "DROP SCHEMA %s;\n", qnspname); - - appendPQExpBuffer(q, "CREATE SCHEMA %s;\n", qnspname); + if (strcmp(nspinfo->dobj.name, "public") == 0) + { + /* see selectDumpableNamespace() */ + appendPQExpBufferStr(delq, + "-- *not* dropping schema, since initdb creates it\n"); + appendPQExpBufferStr(q, + "-- *not* creating schema, since initdb creates it\n"); + } + else + { + appendPQExpBuffer(delq, "DROP SCHEMA %s;\n", qnspname); + appendPQExpBuffer(q, "CREATE SCHEMA %s;\n", qnspname); + } if (dopt->binary_upgrade) binary_upgrade_extension_member(q, &nspinfo->dobj, @@ -15501,8 +15562,8 @@ dumpTable(Archive *fout, TableInfo *tbinfo) PQExpBuffer initracl_subquery = createPQExpBuffer(); buildACLQueries(acl_subquery, racl_subquery, initacl_subquery, - initracl_subquery, "at.attacl", "c.relowner", "'c'", - dopt->binary_upgrade); + initracl_subquery, "at.attacl", "c.relowner", + "pip.initprivs", "'c'", dopt->binary_upgrade); appendPQExpBuffer(query, "SELECT at.attname, " diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 1290f96..51f5c03 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -142,6 +142,7 @@ typedef struct _dumpableObject typedef struct _namespaceInfo { DumpableObject dobj; + Oid nspowner; char *rolname; /* name of owner, or empty string */ char *nspacl; char *rnspacl; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 737e464..02b4418 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -125,6 +125,14 @@ my %pgdump_runs = ( 'regress_pg_dump_test', ], }, + defaults_public_owner => { + database => 'regress_public_owner', + dump_cmd => [ + 'pg_dump', '--no-sync', '-f', + "$tempdir/defaults_public_owner.sql", + 'regress_public_owner', + ], + }, # Do not use --no-sync to give test coverage for data sync. defaults_custom_format => { @@ -605,6 +613,26 @@ my %tests = ( unlike => { no_owner => 1, }, }, + 'ALTER SCHEMA public OWNER TO' => { + create_order => 15, + create_sql => + 'ALTER SCHEMA public OWNER TO "regress_quoted \"" role";', + regexp => qr/^ALTER SCHEMA public OWNER TO .+;/m, + like => { + %full_runs, section_pre_data => 1, + }, + unlike => { no_owner => 1, }, + }, + + 'ALTER SCHEMA public OWNER TO (w/o ACL changes)' => { + database => 'regress_public_owner', + create_order => 100, + create_sql => + 'ALTER SCHEMA public OWNER TO "regress_quoted \"" role";', + regexp => qr/^(GRANT|REVOKE)/m, + unlike => { defaults_public_owner => 1 }, + }, + 'ALTER SEQUENCE test_table_col1_seq' => { regexp => qr/^ \QALTER SEQUENCE dump_test.test_table_col1_seq OWNED BY dump_test.test_table.col1;\E @@ -940,6 +968,23 @@ my %tests = ( like => {}, }, + 'COMMENT ON SCHEMA public' => { + regexp => qr/^COMMENT ON SCHEMA public IS .+;/m, + # regress_public_owner emits this, due to create_sql of next test + like => { + pg_dumpall_dbprivs => 1, + pg_dumpall_exclude => 1, + }, + }, + + 'COMMENT ON SCHEMA public IS NULL' => { + database => 'regress_public_owner', + create_order => 100, + create_sql => 'COMMENT ON SCHEMA public IS NULL;', + regexp => qr/^COMMENT ON SCHEMA public IS '';/m, + like => { defaults_public_owner => 1 }, + }, + 'COMMENT ON TABLE dump_test.test_table' => { create_order => 36, create_sql => 'COMMENT ON TABLE dump_test.test_table @@ -1370,6 +1415,18 @@ my %tests = ( }, }, + 'CREATE ROLE regress_quoted...' => { + create_order => 1, + create_sql => 'CREATE ROLE "regress_quoted \"" role";', + regexp => qr/^\QCREATE ROLE "regress_quoted \"" role";\E/m, + like => { + pg_dumpall_dbprivs => 1, + pg_dumpall_exclude => 1, + pg_dumpall_globals => 1, + pg_dumpall_globals_clean => 1, + }, + }, + 'CREATE ACCESS METHOD gist2' => { create_order => 52, create_sql => @@ -3261,6 +3318,23 @@ my %tests = ( unlike => { no_privs => 1, }, }, + # With the exception of the public schema, we don't dump ownership changes + # for objects originating at initdb. Hence, any GRANT or REVOKE affecting + # owner privileges for those objects should reference the bootstrap + # superuser, not the dump-time owner. + 'REVOKE EXECUTE ON FUNCTION pg_stat_reset FROM regress_dump_test_role' => + { + create_order => 15, + create_sql => ' + ALTER FUNCTION pg_stat_reset OWNER TO regress_dump_test_role; + REVOKE EXECUTE ON FUNCTION pg_stat_reset + FROM regress_dump_test_role;', + regexp => qr/^[^-].*pg_stat_reset.* regress_dump_test_role/m, + + # this shouldn't ever get emitted + like => {}, + }, + 'REVOKE SELECT ON TABLE pg_proc FROM public' => { create_order => 45, create_sql => 'REVOKE SELECT ON TABLE pg_proc FROM public;', @@ -3270,12 +3344,12 @@ my %tests = ( unlike => { no_privs => 1, }, }, - 'REVOKE CREATE ON SCHEMA public FROM public' => { + 'REVOKE ALL ON SCHEMA public' => { create_order => 16, - create_sql => 'REVOKE CREATE ON SCHEMA public FROM public;', - regexp => qr/^ - \QREVOKE ALL ON SCHEMA public FROM PUBLIC;\E - \n\QGRANT USAGE ON SCHEMA public TO PUBLIC;\E + create_sql => + 'REVOKE ALL ON SCHEMA public FROM "regress_quoted \"" role";', + regexp => qr/^ + \QREVOKE ALL ON SCHEMA public FROM "regress_quoted \"" role";\E /xm, like => { %full_runs, section_pre_data => 1, }, unlike => { no_privs => 1, }, @@ -3376,8 +3450,9 @@ if ($collation_check_stderr !~ /ERROR: /) $collation_support = 1; } -# Create a second database for certain tests to work against +# Create additional databases for mutations of schema public $node->psql('postgres', 'create database regress_pg_dump_test;'); +$node->psql('postgres', 'create database regress_public_owner;'); # Start with number of command_fails_like()*2 tests below (each # command_fails_like is actually 2 tests) diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 20af5a9..c8e64b1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3508,6 +3508,7 @@ describeRoles(const char *pattern, bool verbose, bool showSystem) printTableAddHeader(&cont, gettext_noop("Role name"), true, align); printTableAddHeader(&cont, gettext_noop("Attributes"), true, align); + /* ignores implicit memberships from superuser & pg_database_owner */ printTableAddHeader(&cont, gettext_noop("Member of"), true, align); if (verbose && pset.sversion >= 80200) diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index 87d917f..4c2bf97 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -24,6 +24,11 @@ rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't', rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, +{ oid => '8778', oid_symbol => 'DEFAULT_ROLE_DATABASE_OWNER', + rolname => 'pg_database_owner', rolsuper => 'f', rolinherit => 't', + rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', + rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', + rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '3373', oid_symbol => 'DEFAULT_ROLE_MONITOR', rolname => 'pg_monitor', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', diff --git a/src/include/catalog/pg_namespace.dat b/src/include/catalog/pg_namespace.dat index 2ed136b..7932aa6 100644 --- a/src/include/catalog/pg_namespace.dat +++ b/src/include/catalog/pg_namespace.dat @@ -18,8 +18,9 @@ { oid => '99', oid_symbol => 'PG_TOAST_NAMESPACE', descr => 'reserved schema for TOAST tables', nspname => 'pg_toast', nspacl => '_null_' }, +# update dumpComment() if changing this descr { oid => '2200', oid_symbol => 'PG_PUBLIC_NAMESPACE', descr => 'standard public schema', - nspname => 'public', nspacl => '_null_' }, + nspname => 'public', nspowner => 'pg_database_owner', nspacl => '_null_' }, ] diff --git a/src/pl/plperl/expected/plperl_setup.out b/src/pl/plperl/expected/plperl_setup.out index a1a24df..5234feb 100644 --- a/src/pl/plperl/expected/plperl_setup.out +++ b/src/pl/plperl/expected/plperl_setup.out @@ -25,6 +25,9 @@ CREATE EXTENSION plperl; CREATE EXTENSION plperlu; -- fail ERROR: permission denied to create extension "plperlu" HINT: Must be superuser to create this extension. +CREATE SCHEMA plperl_setup_scratch; +SET search_path = plperl_setup_scratch; +GRANT ALL ON SCHEMA plperl_setup_scratch TO regress_user2; CREATE FUNCTION foo1() returns int language plperl as '1;'; SELECT foo1(); foo1 @@ -34,6 +37,7 @@ SELECT foo1(); -- Must reconnect to avoid failure with non-MULTIPLICITY Perl interpreters \c - +SET search_path = plperl_setup_scratch; SET ROLE regress_user1; -- Should be able to change privileges on the language revoke all on language plperl from public; diff --git a/src/pl/plperl/sql/plperl_setup.sql b/src/pl/plperl/sql/plperl_setup.sql index 7484478..a89cf56 100644 --- a/src/pl/plperl/sql/plperl_setup.sql +++ b/src/pl/plperl/sql/plperl_setup.sql @@ -27,12 +27,16 @@ SET ROLE regress_user1; CREATE EXTENSION plperl; CREATE EXTENSION plperlu; -- fail +CREATE SCHEMA plperl_setup_scratch; +SET search_path = plperl_setup_scratch; +GRANT ALL ON SCHEMA plperl_setup_scratch TO regress_user2; CREATE FUNCTION foo1() returns int language plperl as '1;'; SELECT foo1(); -- Must reconnect to avoid failure with non-MULTIPLICITY Perl interpreters \c - +SET search_path = plperl_setup_scratch; SET ROLE regress_user1; diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out index 46a69fc..bad4eb0 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -1719,6 +1719,67 @@ SELECT * FROM pg_largeobject LIMIT 0; SET SESSION AUTHORIZATION regress_priv_user1; SELECT * FROM pg_largeobject LIMIT 0; -- to be denied ERROR: permission denied for table pg_largeobject +-- test pg_database_owner +RESET SESSION AUTHORIZATION; +GRANT pg_database_owner TO regress_priv_user1; +ERROR: role "pg_database_owner" cannot have explicit members +GRANT regress_priv_user1 TO pg_database_owner; +ERROR: role "pg_database_owner" cannot be a member of any role +CREATE TABLE datdba_only (); +ALTER TABLE datdba_only OWNER TO pg_database_owner; +REVOKE DELETE ON datdba_only FROM pg_database_owner; +SELECT + pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv, + pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem, + pg_has_role('regress_priv_user1', 'pg_database_owner', + 'MEMBER WITH ADMIN OPTION') as admin; + priv | mem | admin +------+-----+------- + f | f | f +(1 row) + +BEGIN; +DO $$BEGIN EXECUTE format( + 'ALTER DATABASE %I OWNER TO regress_priv_group2', current_catalog); END$$; +SELECT + pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv, + pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem, + pg_has_role('regress_priv_user1', 'pg_database_owner', + 'MEMBER WITH ADMIN OPTION') as admin; + priv | mem | admin +------+-----+------- + t | t | f +(1 row) + +SET SESSION AUTHORIZATION regress_priv_user1; +TABLE information_schema.enabled_roles; + role_name +--------------------- + pg_database_owner + regress_priv_user1 + regress_priv_group2 +(3 rows) + +TABLE information_schema.applicable_roles; + grantee | role_name | is_grantable +---------------------+---------------------+-------------- + regress_priv_group2 | pg_database_owner | NO + regress_priv_user1 | regress_priv_group2 | NO +(2 rows) + +INSERT INTO datdba_only DEFAULT VALUES; +SAVEPOINT q; DELETE FROM datdba_only; ROLLBACK TO q; +ERROR: permission denied for table datdba_only +SET SESSION AUTHORIZATION regress_priv_user2; +TABLE information_schema.enabled_roles; + role_name +-------------------- + regress_priv_user2 +(1 row) + +INSERT INTO datdba_only DEFAULT VALUES; +ERROR: permission denied for table datdba_only +ROLLBACK; -- test default ACLs \c - CREATE SCHEMA testns; diff --git a/src/test/regress/input/tablespace.source b/src/test/regress/input/tablespace.source index c133e73..cb9774e 100644 --- a/src/test/regress/input/tablespace.source +++ b/src/test/regress/input/tablespace.source @@ -388,7 +388,7 @@ CREATE INDEX k ON testschema.tablespace_acl (c) TABLESPACE regress_tblspace; ALTER TABLE testschema.tablespace_acl OWNER TO regress_tablespace_user2; SET SESSION ROLE regress_tablespace_user2; -CREATE TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail +CREATE TEMP TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail ALTER TABLE testschema.tablespace_acl ALTER c TYPE bigint; REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail REINDEX (TABLESPACE regress_tblspace, CONCURRENTLY) TABLE tablespace_table; -- fail @@ -409,3 +409,6 @@ DROP SCHEMA testschema CASCADE; DROP ROLE regress_tablespace_user1; DROP ROLE regress_tablespace_user2; + +-- Rest of this suite can use the public schema freely. +GRANT ALL ON SCHEMA public TO public; diff --git a/src/test/regress/output/tablespace.source b/src/test/regress/output/tablespace.source index 1bbe7e0..e7629d4 100644 --- a/src/test/regress/output/tablespace.source +++ b/src/test/regress/output/tablespace.source @@ -908,7 +908,7 @@ CREATE TABLE testschema.tablespace_acl (c int); CREATE INDEX k ON testschema.tablespace_acl (c) TABLESPACE regress_tblspace; ALTER TABLE testschema.tablespace_acl OWNER TO regress_tablespace_user2; SET SESSION ROLE regress_tablespace_user2; -CREATE TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail +CREATE TEMP TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail ERROR: permission denied for tablespace regress_tblspace ALTER TABLE testschema.tablespace_acl ALTER c TYPE bigint; REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail @@ -934,3 +934,5 @@ drop cascades to table testschema.atable drop cascades to table testschema.tablespace_acl DROP ROLE regress_tablespace_user1; DROP ROLE regress_tablespace_user2; +-- Rest of this suite can use the public schema freely. +GRANT ALL ON SCHEMA public TO public; diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql index 6277140..b45719c 100644 --- a/src/test/regress/sql/privileges.sql +++ b/src/test/regress/sql/privileges.sql @@ -1034,6 +1034,37 @@ SELECT * FROM pg_largeobject LIMIT 0; SET SESSION AUTHORIZATION regress_priv_user1; SELECT * FROM pg_largeobject LIMIT 0; -- to be denied +-- test pg_database_owner +RESET SESSION AUTHORIZATION; +GRANT pg_database_owner TO regress_priv_user1; +GRANT regress_priv_user1 TO pg_database_owner; +CREATE TABLE datdba_only (); +ALTER TABLE datdba_only OWNER TO pg_database_owner; +REVOKE DELETE ON datdba_only FROM pg_database_owner; +SELECT + pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv, + pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem, + pg_has_role('regress_priv_user1', 'pg_database_owner', + 'MEMBER WITH ADMIN OPTION') as admin; + +BEGIN; +DO $$BEGIN EXECUTE format( + 'ALTER DATABASE %I OWNER TO regress_priv_group2', current_catalog); END$$; +SELECT + pg_has_role('regress_priv_user1', 'pg_database_owner', 'USAGE') as priv, + pg_has_role('regress_priv_user1', 'pg_database_owner', 'MEMBER') as mem, + pg_has_role('regress_priv_user1', 'pg_database_owner', + 'MEMBER WITH ADMIN OPTION') as admin; +SET SESSION AUTHORIZATION regress_priv_user1; +TABLE information_schema.enabled_roles; +TABLE information_schema.applicable_roles; +INSERT INTO datdba_only DEFAULT VALUES; +SAVEPOINT q; DELETE FROM datdba_only; ROLLBACK TO q; +SET SESSION AUTHORIZATION regress_priv_user2; +TABLE information_schema.enabled_roles; +INSERT INTO datdba_only DEFAULT VALUES; +ROLLBACK; + -- test default ACLs \c -