Hi hackers, Row-level security is an awesome feature. I was originally won over by the simple mental model of implicitly adding WHERE clauses to all queries, and that it generally comes for free when they can be satisfied by Index Conds.
I've recently helped implement row-level security in a fairly big multi-tenant application with about 100 tables. RLS is being used "voluntarily" by the application, which switches to a role that is subject to RLS as the first thing after starting a transaction. I realize that this doesn't offer bulletproof tenant isolation, but it still makes a lot of sense as a defense-in-depth measure. It took some time to work through various performance regressions caused by selectivity estimates changing due to "WHERE tenantId = ..." implicitly being added to all relations in all queries. Extended statistics (dependencies) helped with that, as tenantId is highly correlated with the other id columns that we were already querying and joining on. The roughest edge turned out to be the well-known challenge with the LEAKPROOFness of built-in functions, previously discussed in earlier threads [1], in blog posts [2], on Stack Overflow [3] and pointed out in the docs for Row Security Policies [4]. I'm aware that the usual workaround is to ALTER FUNCTION ... LEAKPROOF for each of the relevant built-in functions after making an evaluation of whether that's acceptable for the given use case. We weren't able to do this because we're using a hosted Postgres solution where it's not permitted. We've tried to talk them (AWS) into offering a way to do it, but they haven't budged yet. As in the earlier thread we were affected by enum_eq not being leakproof, which caused our indexes involving enum columns to not be fully utilized while RLS is active (because WHERE clauses on the enum columns cannot be implemented as Index Conds). We mostly overcame that by replacing each index with a series of partial ones, each with WHERE enum_column = ... We also had some functional indexes that either explicitly used a non-leakproof built-in function, or implicitly exercised a non-leakproof conversion function such as textin or int8out. Here we ended up cheating the system by adding LEAKPROOF wrappers around the built-in functions, similar to [5]. The most difficult thing to overcome was GIN indexes on bigint[] columns not being used because the built-in arrayoverlap/arraycontained/ array_eq/... functions aren't LEAKPROOF. We eventually figured out that we could make our own version of gin_array_ops which used functions marked as LEAKPROOF, and then recreate all the GIN indices with that. My hunch is that there's a big class of applications like ours where enforcing LEAKPROOFness isn't really important. When application code is interacting with the database via its own role, you're not that concerned with invisible tuples leaking out via error messages or elaborate timing attacks. That seems more relevant when your adversaries have direct access to the database and are able to create their own functions and execute arbitrary queries. I think it would be extremely useful to be able to forefeit the LEAKPROOF guarantees when the database is being accessed by an application in exchange for avoiding the footguns described above. I dug into the code and noticed that restrictinfo->leakproof is only being checked in two places (createplan.c and equivclass.c), so it seems fairly easy to only selectively enforce it. Then there's the question of how to configure it. I can think of a few possible ways: 1) Add a BYPASSLEAKPROOF role attribute that can only be granted by a superuser, similar to the BYPASSRLS flag. 2) Add a session variable, eg. enable_security_leakproof, that can only be set or granted to another role by a superuser. 3) Make it a property of the individual POLICY that grants access to the table. This would be a bit more granular than a global switch, but there'd be some ambiguity when multiple policies are involved. I took a stab at implementing solution 1) above, mostly to understand if I had found the right places in the code and how hard it'd be. This is my first time hacking on Postgres, and I'm not really sure if it's OK to introduce this extra check in a possibly hot code path. I thought about instead adding a "does the current user have bypassleakproof" flag in some struct available to the planner, but this is beyond my current abilities. Also, it's probably better to get some feedback on the idea before spending too much time optimizing. What do you think? Best regards, Andreas Lind [1] https://www.postgresql.org/message-id/2811772.0XtDgEdalL%40peanuts2 [2] https://pganalyze.com/blog/5mins-postgres-row-level-security-bypassrls-security-invoker-views-leakproof-functions [3] https://stackoverflow.com/questions/63008838/postgresql-ignores-pg-trgm-gin-indexes-when-row-level-security-is-enabled-and-no [4] https://www.postgresql.org/docs/current/ddl-rowsecurity.html#DDL-ROWSECURITY [5] https://news.ycombinator.com/item?id=20066350
From f2eac3772557bca650b4748d2dfc635ef17f724e Mon Sep 17 00:00:00 2001 From: Andreas Lind <andreaslindpeter...@gmail.com> Date: Wed, 19 Jun 2024 23:08:37 +0200 Subject: [PATCH v1 3/4] Adjust existing tests --- src/test/regress/expected/rules.out | 3 +++ src/test/regress/expected/updatable_views.out | 13 ++++++++++++ src/test/regress/sql/updatable_views.sql | 20 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 6cf828ca8d0..63134e53495 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1509,6 +1509,7 @@ pg_roles| SELECT pg_authid.rolname, '********'::text AS rolpassword, pg_authid.rolvaliduntil, pg_authid.rolbypassrls, + pg_authid.rolbypassleakproof, s.setconfig AS rolconfig, pg_authid.oid FROM (pg_authid @@ -1746,6 +1747,7 @@ pg_shadow| SELECT pg_authid.rolname AS usename, pg_authid.rolsuper AS usesuper, pg_authid.rolreplication AS userepl, pg_authid.rolbypassrls AS usebypassrls, + pg_authid.rolbypassleakproof AS usebypassleakproof, pg_authid.rolpassword AS passwd, pg_authid.rolvaliduntil AS valuntil, s.setconfig AS useconfig @@ -2685,6 +2687,7 @@ pg_user| SELECT usename, usesuper, userepl, usebypassrls, + usebypassleakproof, '********'::text AS passwd, valuntil, useconfig diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 095df0a670c..e577c02cf81 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -2973,6 +2973,9 @@ SELECT table_name, column_name, is_updatable rw_view1 | person | YES (1 row) +CREATE USER regress_view_user4 NOBYPASSLEAKPROOF; +GRANT ALL ON rw_view1 TO regress_view_user4; +SET SESSION AUTHORIZATION regress_view_user4; SELECT * FROM rw_view1 WHERE snoop(person); NOTICE: snooped value: Tom NOTICE: snooped value: Harry @@ -3033,6 +3036,7 @@ MERGE INTO rw_view1 t -> Values Scan on "*VALUES*" (7 rows) +RESET SESSION AUTHORIZATION; -- security barrier view on top of security barrier view CREATE VIEW rw_view2 WITH (security_barrier = true) AS SELECT * FROM rw_view1 WHERE snoop(person); @@ -3061,6 +3065,9 @@ SELECT table_name, column_name, is_updatable rw_view2 | person | YES (1 row) +CREATE USER regress_view_user5 NOBYPASSLEAKPROOF; +GRANT ALL ON rw_view2 TO regress_view_user5; +SET SESSION AUTHORIZATION regress_view_user5; SELECT * FROM rw_view2 WHERE snoop(person); NOTICE: snooped value: Tom NOTICE: snooped value: Tom @@ -3130,6 +3137,7 @@ MERGE INTO rw_view2 t -> Values Scan on "*VALUES*" (6 rows) +RESET SESSION AUTHORIZATION; DROP TABLE base_tbl CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to view rw_view1 @@ -3224,6 +3232,10 @@ CREATE VIEW v1 WITH (security_barrier=true) AS SELECT *, (SELECT d FROM t11 WHERE t11.a = t1.a LIMIT 1) AS d FROM t1 WHERE a > 5 AND EXISTS(SELECT 1 FROM t12 WHERE t12.a = t1.a); +CREATE USER regress_view_user6 NOBYPASSLEAKPROOF; +GRANT ALL ON t1 TO regress_view_user6; +GRANT ALL ON v1 TO regress_view_user6; +SET SESSION AUTHORIZATION regress_view_user6; SELECT * FROM v1 WHERE a=3; -- should not see anything a | b | c | d ---+---+---+--- @@ -3381,6 +3393,7 @@ TABLE t1; -- verify all a<=5 are intact 5 | 5 | t111 (20 rows) +RESET SESSION AUTHORIZATION; DROP TABLE t1, t11, t12, t111 CASCADE; NOTICE: drop cascades to view v1 DROP FUNCTION snoop(anyelement); diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql index c071fffc116..0ef3fe35913 100644 --- a/src/test/regress/sql/updatable_views.sql +++ b/src/test/regress/sql/updatable_views.sql @@ -1539,7 +1539,12 @@ SELECT table_name, column_name, is_updatable WHERE table_name = 'rw_view1' ORDER BY ordinal_position; +CREATE USER regress_view_user4 NOBYPASSLEAKPROOF; +GRANT ALL ON rw_view1 TO regress_view_user4; +SET SESSION AUTHORIZATION regress_view_user4; + SELECT * FROM rw_view1 WHERE snoop(person); + UPDATE rw_view1 SET person=person WHERE snoop(person); DELETE FROM rw_view1 WHERE NOT snoop(person); MERGE INTO rw_view1 t @@ -1553,6 +1558,7 @@ EXPLAIN (costs off) MERGE INTO rw_view1 t USING (VALUES ('Tom'), ('Dick'), ('Harry')) AS v(person) ON t.person = v.person WHEN MATCHED AND snoop(t.person) THEN UPDATE SET person = v.person; +RESET SESSION AUTHORIZATION; -- security barrier view on top of security barrier view @@ -1572,7 +1578,12 @@ SELECT table_name, column_name, is_updatable WHERE table_name = 'rw_view2' ORDER BY ordinal_position; +CREATE USER regress_view_user5 NOBYPASSLEAKPROOF; +GRANT ALL ON rw_view2 TO regress_view_user5; +SET SESSION AUTHORIZATION regress_view_user5; + SELECT * FROM rw_view2 WHERE snoop(person); + UPDATE rw_view2 SET person=person WHERE snoop(person); DELETE FROM rw_view2 WHERE NOT snoop(person); MERGE INTO rw_view2 t @@ -1587,6 +1598,8 @@ MERGE INTO rw_view2 t USING (VALUES ('Tom'), ('Dick'), ('Harry')) AS v(person) ON t.person = v.person WHEN MATCHED AND snoop(t.person) THEN UPDATE SET person = v.person; +RESET SESSION AUTHORIZATION; + DROP TABLE base_tbl CASCADE; -- security barrier view on top of table with rules @@ -1648,6 +1661,11 @@ SELECT *, (SELECT d FROM t11 WHERE t11.a = t1.a LIMIT 1) AS d FROM t1 WHERE a > 5 AND EXISTS(SELECT 1 FROM t12 WHERE t12.a = t1.a); +CREATE USER regress_view_user6 NOBYPASSLEAKPROOF; +GRANT ALL ON t1 TO regress_view_user6; +GRANT ALL ON v1 TO regress_view_user6; +SET SESSION AUTHORIZATION regress_view_user6; + SELECT * FROM v1 WHERE a=3; -- should not see anything SELECT * FROM v1 WHERE a=8; @@ -1668,6 +1686,8 @@ DELETE FROM v1 WHERE snoop(a) AND leakproof(a); -- should not delete everything, TABLE t1; -- verify all a<=5 are intact +RESET SESSION AUTHORIZATION; + DROP TABLE t1, t11, t12, t111 CASCADE; DROP FUNCTION snoop(anyelement); DROP FUNCTION leakproof(anyelement); -- 2.39.2
From d1a0491949be85e5991ef026b455744976dae1f1 Mon Sep 17 00:00:00 2001 From: Andreas Lind <andreaslindpeter...@gmail.com> Date: Wed, 19 Jun 2024 22:20:42 +0200 Subject: [PATCH v1 2/4] Wire it up in the planner --- src/backend/optimizer/path/equivclass.c | 4 +++- src/backend/optimizer/plan/createplan.c | 6 ++++-- src/backend/optimizer/util/restrictinfo.c | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index 441f12f6c50..3c72265b496 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -31,7 +31,9 @@ #include "optimizer/planmain.h" #include "optimizer/restrictinfo.h" #include "rewrite/rewriteManip.h" +#include "utils/acl.h" #include "utils/lsyscache.h" +#include "miscadmin.h" static EquivalenceMember *make_eq_member(EquivalenceClass *ec, @@ -203,7 +205,7 @@ process_equivalence(PlannerInfo *root, Assert(restrictinfo->right_ec == NULL); /* Reject if it is potentially postponable by security considerations */ - if (restrictinfo->security_level > 0 && !restrictinfo->leakproof) + if (restrictinfo->security_level > 0 && !(restrictinfo->leakproof || has_bypassleakproof_privilege(GetUserId()))) return false; /* Extract info from given clause */ diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index a8f22a8c154..6318e635ca1 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -42,8 +42,9 @@ #include "parser/parsetree.h" #include "partitioning/partprune.h" #include "tcop/tcopprot.h" +#include "utils/acl.h" #include "utils/lsyscache.h" - +#include "miscadmin.h" /* * Flag bits that can appear in the flags argument of create_plan_recurse(). @@ -5394,7 +5395,8 @@ order_qual_clauses(PlannerInfo *root, List *clauses) * security level, which is not so great, but we can alleviate * that risk by applying the cost limit cutoff. */ - if (rinfo->leakproof && items[i].cost < 10 * cpu_operator_cost) + if ((rinfo->leakproof || has_bypassleakproof_privilege(GetUserId())) && + items[i].cost < 10 * cpu_operator_cost) items[i].security_level = 0; else items[i].security_level = rinfo->security_level; diff --git a/src/backend/optimizer/util/restrictinfo.c b/src/backend/optimizer/util/restrictinfo.c index a80083d2323..b9e0b4f7e43 100644 --- a/src/backend/optimizer/util/restrictinfo.c +++ b/src/backend/optimizer/util/restrictinfo.c @@ -19,7 +19,8 @@ #include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "optimizer/restrictinfo.h" - +#include "utils/acl.h" +#include "miscadmin.h" static Expr *make_sub_restrictinfos(PlannerInfo *root, Expr *clause, @@ -427,7 +428,7 @@ restriction_is_securely_promotable(RestrictInfo *restrictinfo, * would need to go before this one, *or* if this one is leakproof. */ if (restrictinfo->security_level <= rel->baserestrict_min_security || - restrictinfo->leakproof) + restrictinfo->leakproof || has_bypassleakproof_privilege(GetUserId())) return true; else return false; -- 2.39.2
From b0e55c4db4ec642a4a7ddc6dfc15591744d1d96e Mon Sep 17 00:00:00 2001 From: Andreas Lind <andreaslindpeter...@gmail.com> Date: Wed, 19 Jun 2024 14:51:30 +0200 Subject: [PATCH v1 1/4] Add a BYPASSLEAKPROOF role attribute --- doc/src/sgml/catalogs.sgml | 10 ++++ doc/src/sgml/ref/alter_role.sgml | 9 ++-- doc/src/sgml/ref/createuser.sgml | 20 ++++++++ doc/src/sgml/system-views.sgml | 31 +++++++++++++ src/backend/catalog/aclchk.c | 19 ++++++++ src/backend/catalog/system_views.sql | 3 ++ src/backend/commands/user.c | 40 +++++++++++++++- src/backend/parser/gram.y | 4 ++ src/bin/pg_dump/pg_dumpall.c | 23 ++++++++++ src/bin/psql/describe.c | 17 +++++++ src/bin/scripts/createuser.c | 18 +++++++- src/bin/scripts/t/040_createuser.pl | 32 ++++++++----- src/include/catalog/pg_authid.dat | 68 ++++++++++++++-------------- src/include/catalog/pg_authid.h | 1 + src/include/utils/acl.h | 1 + 15 files changed, 245 insertions(+), 51 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index cbd4e40a320..162430ee85d 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1567,6 +1567,16 @@ </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>rolbypassleakproof</structfield> <type>bool</type> + </para> + <para> + Role bypasses LEAKPROOF requirement for functions evaluated in + security sensitive contexts. + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>rolconnlimit</structfield> <type>int4</type> diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml index 7b0a04bc463..259e3927e33 100644 --- a/doc/src/sgml/ref/alter_role.sgml +++ b/doc/src/sgml/ref/alter_role.sgml @@ -32,6 +32,7 @@ ALTER ROLE <replaceable class="parameter">role_specification</replaceable> [ WIT | LOGIN | NOLOGIN | REPLICATION | NOREPLICATION | BYPASSRLS | NOBYPASSRLS + | BYPASSLEAKPROOF | NOBYPASSLEAKPROOF | CONNECTION LIMIT <replaceable class="parameter">connlimit</replaceable> | [ ENCRYPTED ] PASSWORD '<replaceable class="parameter">password</replaceable>' | PASSWORD NULL | VALID UNTIL '<replaceable class="parameter">timestamp</replaceable>' @@ -77,9 +78,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A non-replication roles for which they have been granted <literal>ADMIN OPTION</literal>. Non-superusers cannot change the <literal>SUPERUSER</literal> property and can change the - <literal>CREATEDB</literal>, <literal>REPLICATION</literal>, and - <literal>BYPASSRLS</literal> properties only if they possess the - corresponding property themselves. + <literal>CREATEDB</literal>, <literal>REPLICATION</literal>, + <literal>BYPASSRLS</literal>, and <literal>BYPASSLEAKPROOF</literal> properties + only if they possess the corresponding property themselves. Ordinary roles can only change their own password. </para> @@ -178,6 +179,8 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A <term><literal>NOREPLICATION</literal></term> <term><literal>BYPASSRLS</literal></term> <term><literal>NOBYPASSRLS</literal></term> + <term><literal>BYPASSLEAKPROOF</literal></term> + <term><literal>NOBYPASSLEAKPROOF</literal></term> <term><literal>CONNECTION LIMIT</literal> <replaceable class="parameter">connlimit</replaceable></term> <term>[ <literal>ENCRYPTED</literal> ] <literal>PASSWORD</literal> '<replaceable class="parameter">password</replaceable>'</term> <term><literal>PASSWORD NULL</literal></term> diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml index 5c34c623423..af26f1daf72 100644 --- a/doc/src/sgml/ref/createuser.sgml +++ b/doc/src/sgml/ref/createuser.sgml @@ -330,6 +330,26 @@ PostgreSQL documentation </listitem> </varlistentry> + <varlistentry> + <term><option>--bypassleakproof</option></term> + <listitem> + <para> + The new user will bypass the LEAKPROOF requirement for functions evaluated + in security sensitive contexts. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>--no-bypassleakproof</option></term> + <listitem> + <para> + The new user will not bypass the LEAKPROOF requirement for functions evaluated + in security sensitive contexts. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><option>--replication</option></term> <listitem> diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index b58c52ea50f..b67c657c0c7 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -3105,6 +3105,17 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx </para></entry> </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>rolbypassleakproof</structfield> <type>bool</type> + </para> + <para> + Role bypasses the LEAKPROOF requirement for functions evaluated in + security sensitive contexts. + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>rolconfig</structfield> <type>text[]</type> @@ -3927,6 +3938,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>usebypassleakproof</structfield> <type>bool</type> + </para> + <para> + Role bypasses the LEAKPROOF requirement for functions evaluated in + security sensitive contexts. + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>passwd</structfield> <type>text</type> @@ -5194,6 +5215,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>usebypassleakproof</structfield> <type>bool</type> + </para> + <para> + Role bypasses the LEAKPROOF requirement for functions evaluated in + security sensitive contexts. + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>passwd</structfield> <type>text</type> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..f0d5ff17b18 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -4188,6 +4188,25 @@ has_bypassrls_privilege(Oid roleid) return result; } +bool +has_bypassleakproof_privilege(Oid roleid) +{ + bool result = false; + HeapTuple utup; + + /* Superusers bypass all permission checking. */ + if (superuser_arg(roleid)) + return true; + + utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid)); + if (HeapTupleIsValid(utup)) + { + result = ((Form_pg_authid) GETSTRUCT(utup))->rolbypassleakproof; + ReleaseSysCache(utup); + } + return result; +} + /* * Fetch pg_default_acl entry for given role, namespace and object type * (object type must be given in pg_default_acl's encoding). diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 15efb02badb..8b981595907 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -27,6 +27,7 @@ CREATE VIEW pg_roles AS '********'::text as rolpassword, rolvaliduntil, rolbypassrls, + rolbypassleakproof, setconfig as rolconfig, pg_authid.oid FROM pg_authid LEFT JOIN pg_db_role_setting s @@ -40,6 +41,7 @@ CREATE VIEW pg_shadow AS rolsuper AS usesuper, rolreplication AS userepl, rolbypassrls AS usebypassrls, + rolbypassleakproof AS usebypassleakproof, rolpassword AS passwd, rolvaliduntil AS valuntil, setconfig AS useconfig @@ -65,6 +67,7 @@ CREATE VIEW pg_user AS usesuper, userepl, usebypassrls, + usebypassleakproof, '********'::text as passwd, valuntil, useconfig diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 0d638e29d00..efffec6fc0f 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -149,6 +149,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) bool canlogin = false; /* Can this user login? */ bool isreplication = false; /* Is this a replication role? */ bool bypassrls = false; /* Is this a row security enabled role? */ + bool bypassleakproof = false; /* Does it bypass leakproof + * checks? */ int connlimit = -1; /* maximum connections allowed */ List *addroleto = NIL; /* roles to make this a member of */ List *rolemembers = NIL; /* roles to be members of this role */ @@ -169,6 +171,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) DefElem *dadminmembers = NULL; DefElem *dvalidUntil = NULL; DefElem *dbypassRLS = NULL; + DefElem *dbypassLeakproof = NULL; GrantRoleOptions popt; /* The defaults can vary depending on the original statement type */ @@ -272,6 +275,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) errorConflictingDefElem(defel, pstate); dbypassRLS = defel; } + else if (strcmp(defel->defname, "bypassleakproof") == 0) + { + if (dbypassLeakproof) + errorConflictingDefElem(defel, pstate); + dbypassLeakproof = defel; + } else elog(ERROR, "option \"%s\" not recognized", defel->defname); @@ -309,6 +318,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) validUntil = strVal(dvalidUntil->arg); if (dbypassRLS) bypassrls = boolVal(dbypassRLS->arg); + if (dbypassLeakproof) + bypassleakproof = boolVal(dbypassLeakproof->arg); /* Check some permissions first */ if (!superuser_arg(currentUserId)) @@ -343,6 +354,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) errmsg("permission denied to create role"), errdetail("Only roles with the %s attribute may create roles with the %s attribute.", "BYPASSRLS", "BYPASSRLS"))); + + if (bypassleakproof && !has_bypassleakproof_privilege(currentUserId)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to create role"), + errdetail("Only roles with the %s attribute may create roles with the %s attribute.", + "BYPASSLEAKPROOF", "BYPASSLEAKPROOF"))); } /* @@ -456,6 +474,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null; new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(bypassrls); + new_record[Anum_pg_authid_rolbypassleakproof - 1] = BoolGetDatum(bypassleakproof); /* * pg_largeobject_metadata contains pg_authid.oid's, so we use the @@ -644,6 +663,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) DefElem *drolemembers = NULL; DefElem *dvalidUntil = NULL; DefElem *dbypassRLS = NULL; + DefElem *dbypassLeakproof = NULL; Oid roleid; Oid currentUserId = GetUserId(); GrantRoleOptions popt; @@ -723,6 +743,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) errorConflictingDefElem(defel, pstate); dbypassRLS = defel; } + else if (strcmp(defel->defname, "bypassleakproof") == 0) + { + if (dbypassLeakproof) + errorConflictingDefElem(defel, pstate); + dbypassLeakproof = defel; + } else elog(ERROR, "option \"%s\" not recognized", defel->defname); @@ -775,7 +801,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) { /* things an unprivileged user certainly can't do */ if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit || - dvalidUntil || disreplication || dbypassRLS) + dvalidUntil || disreplication || dbypassRLS || dbypassLeakproof) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied to alter role"), @@ -815,6 +841,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) errmsg("permission denied to alter role"), errdetail("Only roles with the %s attribute may change the %s attribute.", "BYPASSRLS", "BYPASSRLS"))); + if (dbypassLeakproof && !has_bypassleakproof_privilege(currentUserId)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to alter role"), + errdetail("Only roles with the %s attribute may change the %s attribute.", + "BYPASSLEAKPROOF", "BYPASSLEAKPROOF"))); } /* To add or drop members, you need ADMIN OPTION. */ @@ -952,6 +984,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(boolVal(dbypassRLS->arg)); new_record_repl[Anum_pg_authid_rolbypassrls - 1] = true; } + if (dbypassLeakproof) + { + new_record[Anum_pg_authid_rolbypassleakproof - 1] = BoolGetDatum(boolVal(dbypassLeakproof->arg)); + new_record_repl[Anum_pg_authid_rolbypassleakproof - 1] = true; + } + new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record, new_record_nulls, new_record_repl); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 3c4268b271a..94412edd1f6 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -1272,6 +1272,10 @@ AlterOptRoleElem: $$ = makeDefElem("bypassrls", (Node *) makeBoolean(true), @1); else if (strcmp($1, "nobypassrls") == 0) $$ = makeDefElem("bypassrls", (Node *) makeBoolean(false), @1); + else if (strcmp($1, "bypassleakproof") == 0) + $$ = makeDefElem("bypassleakproof", (Node *) makeBoolean(true), @1); + else if (strcmp($1, "nobypassleakproof") == 0) + $$ = makeDefElem("bypassleakproof", (Node *) makeBoolean(false), @1); else if (strcmp($1, "noinherit") == 0) { /* diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c index 946a6d0fafc..d84f47a881a 100644 --- a/src/bin/pg_dump/pg_dumpall.c +++ b/src/bin/pg_dump/pg_dumpall.c @@ -837,6 +837,7 @@ dumpRoles(PGconn *conn) i_rolvaliduntil, i_rolreplication, i_rolbypassrls, + i_rolbypassleakproof, i_rolcomment, i_is_current_user; int i; @@ -845,6 +846,22 @@ dumpRoles(PGconn *conn) * Notes: rolconfig is dumped later, and pg_authid must be used for * extracting rolcomment regardless of role_catalog. */ + if (server_version >= 99999) + + /* + * FIXME: When it has been assigned, set the right server_version + * where rolbypassleakproof is available + */ + printfPQExpBuffer(buf, + "SELECT oid, rolname, rolsuper, rolinherit, " + "rolcreaterole, rolcreatedb, " + "rolcanlogin, rolconnlimit, rolpassword, " + "rolvaliduntil, rolreplication, rolbypassrls, rolbypassleakproof, " + "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, " + "rolname = current_user AS is_current_user " + "FROM %s " + "WHERE rolname !~ '^pg_' " + "ORDER BY 2", role_catalog); if (server_version >= 90600) printfPQExpBuffer(buf, "SELECT oid, rolname, rolsuper, rolinherit, " @@ -892,6 +909,7 @@ dumpRoles(PGconn *conn) i_rolvaliduntil = PQfnumber(res, "rolvaliduntil"); i_rolreplication = PQfnumber(res, "rolreplication"); i_rolbypassrls = PQfnumber(res, "rolbypassrls"); + i_rolbypassleakproof = PQfnumber(res, "rolbypassleakproof"); i_rolcomment = PQfnumber(res, "rolcomment"); i_is_current_user = PQfnumber(res, "is_current_user"); @@ -971,6 +989,11 @@ dumpRoles(PGconn *conn) else appendPQExpBufferStr(buf, " NOBYPASSRLS"); + if (strcmp(PQgetvalue(res, i, i_rolbypassleakproof), "t") == 0) + appendPQExpBufferStr(buf, " BYPASSLEAKPROOF"); + else + appendPQExpBufferStr(buf, " NOBYPASSLEAKPROOF"); + if (strcmp(PQgetvalue(res, i, i_rolconnlimit), "-1") != 0) appendPQExpBuffer(buf, " CONNECTION LIMIT %s", PQgetvalue(res, i, i_rolconnlimit)); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 1d08268393e..fc5a00f3e79 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3742,6 +3742,15 @@ describeRoles(const char *pattern, bool verbose, bool showSystem) appendPQExpBufferStr(&buf, "\n, r.rolbypassrls"); } + if (pset.sversion >= 99999) + { + /* + * FIXME: When it has been assigned, set the right server_version + * where rolbypassleakproof is available + */ + appendPQExpBufferStr(&buf, "\n, r.rolbypassleakproof"); + } + appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n"); if (!showSystem && !pattern) @@ -3799,6 +3808,14 @@ describeRoles(const char *pattern, bool verbose, bool showSystem) if (strcmp(PQgetvalue(res, i, (verbose ? 10 : 9)), "t") == 0) add_role_attribute(&buf, _("Bypass RLS")); + /* + * FIXME: When it has been assigned, set the right server_version + * where rolbypassleakproof is available + */ + if (pset.sversion >= 99999) + if (strcmp(PQgetvalue(res, i, (verbose ? 11 : 10)), "t") == 0) + add_role_attribute(&buf, _("Bypass LEAKPROOF")); + conns = atoi(PQgetvalue(res, i, 6)); if (conns >= 0) { diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c index 81e6abfc46e..4c1246aa181 100644 --- a/src/bin/scripts/createuser.c +++ b/src/bin/scripts/createuser.c @@ -57,6 +57,8 @@ main(int argc, char *argv[]) {"interactive", no_argument, NULL, 3}, {"bypassrls", no_argument, NULL, 4}, {"no-bypassrls", no_argument, NULL, 5}, + {"bypassleakproof", no_argument, NULL, 6}, + {"no-bypassleakproof", no_argument, NULL, 7}, {NULL, 0, NULL, 0} }; @@ -86,7 +88,8 @@ main(int argc, char *argv[]) inherit = TRI_DEFAULT, login = TRI_DEFAULT, replication = TRI_DEFAULT, - bypassrls = TRI_DEFAULT; + bypassrls = TRI_DEFAULT, + bypassleakproof = TRI_DEFAULT; PQExpBufferData sql; @@ -190,6 +193,12 @@ main(int argc, char *argv[]) case 5: bypassrls = TRI_NO; break; + case 6: + bypassleakproof = TRI_YES; + break; + case 7: + bypassleakproof = TRI_NO; + break; default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -341,6 +350,10 @@ main(int argc, char *argv[]) appendPQExpBufferStr(&sql, " BYPASSRLS"); if (bypassrls == TRI_NO) appendPQExpBufferStr(&sql, " NOBYPASSRLS"); + if (bypassleakproof == TRI_YES) + appendPQExpBufferStr(&sql, " BYPASSLEAKPROOF"); + if (bypassleakproof == TRI_NO) + appendPQExpBufferStr(&sql, " NOBYPASSLEAKPROOF"); if (conn_limit >= -1) appendPQExpBuffer(&sql, " CONNECTION LIMIT %d", conn_limit); if (pwexpiry != NULL) @@ -444,6 +457,9 @@ help(const char *progname) printf(_(" --bypassrls role can bypass row-level security (RLS) policy\n")); printf(_(" --no-bypassrls role cannot bypass row-level security (RLS) policy\n" " (default)\n")); + printf(_(" --bypassleakproof role can bypass leakproof security requirements\n")); + printf(_(" --no-bypassleakproof role cannot bypass leakproof security requirements\n" + " (default)\n")); printf(_(" --replication role can initiate replication\n")); printf(_(" --no-replication role cannot initiate replication (default)\n")); printf(_(" -?, --help show this help, then exit\n")); diff --git a/src/bin/scripts/t/040_createuser.pl b/src/bin/scripts/t/040_createuser.pl index 54af43401bb..495a19c099a 100644 --- a/src/bin/scripts/t/040_createuser.pl +++ b/src/bin/scripts/t/040_createuser.pl @@ -18,19 +18,19 @@ $node->start; $node->issues_sql_like( [ 'createuser', 'regress_user1' ], - qr/statement: CREATE ROLE regress_user1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS;/, + qr/statement: CREATE ROLE regress_user1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF;/, 'SQL CREATE USER run'); $node->issues_sql_like( [ 'createuser', '--no-login', 'regress_role1' ], - qr/statement: CREATE ROLE regress_role1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOLOGIN NOREPLICATION NOBYPASSRLS;/, + qr/statement: CREATE ROLE regress_role1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOLOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF;/, 'create a non-login role'); $node->issues_sql_like( [ 'createuser', '--createrole', 'regress user2' ], - qr/statement: CREATE ROLE "regress user2" NOSUPERUSER NOCREATEDB CREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS;/, + qr/statement: CREATE ROLE "regress user2" NOSUPERUSER NOCREATEDB CREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF;/, 'create a CREATEROLE user'); $node->issues_sql_like( [ 'createuser', '--superuser', 'regress_user3' ], - qr/statement: CREATE ROLE regress_user3 SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS;/, + qr/statement: CREATE ROLE regress_user3 SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF;/, 'create a superuser'); $node->issues_sql_like( [ @@ -39,7 +39,7 @@ $node->issues_sql_like( '--with-admin' => 'regress user2', 'regress user #4' ], - qr/statement: CREATE ROLE "regress user #4" NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS ADMIN regress_user1,"regress user2";/, + qr/statement: CREATE ROLE "regress user #4" NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF ADMIN regress_user1,"regress user2";/, 'add a role as a member with admin option of the newly created role'); $node->issues_sql_like( [ @@ -48,11 +48,11 @@ $node->issues_sql_like( '--with-member' => 'regress_user3', '--with-member' => 'regress user #4' ], - qr/statement: CREATE ROLE "REGRESS_USER5" NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS ROLE regress_user3,"regress user #4";/, + qr/statement: CREATE ROLE "REGRESS_USER5" NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF ROLE regress_user3,"regress user #4";/, 'add a role as a member of the newly created role'); $node->issues_sql_like( [ 'createuser', '--valid-until' => '2029 12 31', 'regress_user6' ], - qr/statement: CREATE ROLE regress_user6 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS VALID UNTIL \'2029 12 31\';/, + qr/statement: CREATE ROLE regress_user6 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF VALID UNTIL \'2029 12 31\';/, 'create a role with a password expiration date'); $node->issues_sql_like( [ 'createuser', '--bypassrls', 'regress_user7' ], @@ -60,24 +60,32 @@ $node->issues_sql_like( 'create a BYPASSRLS role'); $node->issues_sql_like( [ 'createuser', '--no-bypassrls', 'regress_user8' ], - qr/statement: CREATE ROLE regress_user8 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS;/, + qr/statement: CREATE ROLE regress_user8 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF;/, 'create a role without BYPASSRLS'); $node->issues_sql_like( [ 'createuser', '--with-admin' => 'regress_user1', 'regress_user9' ], - qr/statement: CREATE ROLE regress_user9 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS ADMIN regress_user1;/, + qr/statement: CREATE ROLE regress_user9 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF ADMIN regress_user1;/, '--with-admin'); $node->issues_sql_like( [ 'createuser', '--with-member' => 'regress_user1', 'regress_user10' ], - qr/statement: CREATE ROLE regress_user10 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS ROLE regress_user1;/, + qr/statement: CREATE ROLE regress_user10 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF ROLE regress_user1;/, '--with-member'); $node->issues_sql_like( [ 'createuser', '--role' => 'regress_user1', 'regress_user11' ], - qr/statement: CREATE ROLE regress_user11 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS IN ROLE regress_user1;/, + qr/statement: CREATE ROLE regress_user11 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF IN ROLE regress_user1;/, '--role'); $node->issues_sql_like( [ 'createuser', 'regress_user12', '--member-of' => 'regress_user1' ], - qr/statement: CREATE ROLE regress_user12 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS IN ROLE regress_user1;/, + qr/statement: CREATE ROLE regress_user12 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF IN ROLE regress_user1;/, '--member-of'); +$node->issues_sql_like( + [ 'createuser', '--bypassleakproof', 'regress_user13' ], + qr/statement: CREATE ROLE regress_user7 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS BYPASSLEAKPROOF;/, + 'create a BYPASSLEAKPROOF role'); +$node->issues_sql_like( + [ 'createuser', '--no-bypassleakproof', 'regress_user14' ], + qr/statement: CREATE ROLE regress_user8 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS NOBYPASSLEAKPROOF;/, + 'create a role without BYPASSLEAKPROOF'); $node->command_fails([ 'createuser', 'regress_user1' ], 'fails if role already exists'); diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index eb4dab5c6aa..7461e6e625a 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -22,87 +22,87 @@ { oid => '10', oid_symbol => 'BOOTSTRAP_SUPERUSERID', rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't', rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't', - rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 't', rolbypassrls => 't', rolbypassleakproof => 't', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '6171', oid_symbol => 'ROLE_PG_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_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '6181', oid_symbol => 'ROLE_PG_READ_ALL_DATA', rolname => 'pg_read_all_data', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '6182', oid_symbol => 'ROLE_PG_WRITE_ALL_DATA', rolname => 'pg_write_all_data', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '3373', oid_symbol => 'ROLE_PG_MONITOR', rolname => 'pg_monitor', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '3374', oid_symbol => 'ROLE_PG_READ_ALL_SETTINGS', rolname => 'pg_read_all_settings', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '3375', oid_symbol => 'ROLE_PG_READ_ALL_STATS', rolname => 'pg_read_all_stats', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '3377', oid_symbol => 'ROLE_PG_STAT_SCAN_TABLES', rolname => 'pg_stat_scan_tables', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '4569', oid_symbol => 'ROLE_PG_READ_SERVER_FILES', rolname => 'pg_read_server_files', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '4570', oid_symbol => 'ROLE_PG_WRITE_SERVER_FILES', rolname => 'pg_write_server_files', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '4571', oid_symbol => 'ROLE_PG_EXECUTE_SERVER_PROGRAM', rolname => 'pg_execute_server_program', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '4200', oid_symbol => 'ROLE_PG_SIGNAL_BACKEND', rolname => 'pg_signal_backend', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINT', rolname => 'pg_checkpoint', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '6337', oid_symbol => 'ROLE_PG_MAINTAIN', rolname => 'pg_maintain', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '4550', oid_symbol => 'ROLE_PG_USE_RESERVED_CONNECTIONS', rolname => 'pg_use_reserved_connections', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '6304', oid_symbol => 'ROLE_PG_CREATE_SUBSCRIPTION', rolname => 'pg_create_subscription', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, { oid => '8916', oid_symbol => 'ROLE_PG_SIGNAL_AUTOVACUUM_WORKER', rolname => 'pg_signal_autovacuum_worker', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', - rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', - rolpassword => '_null_', rolvaliduntil => '_null_' }, + rolreplication => 'f', rolbypassrls => 'f', rolbypassleakproof => 'f', + rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, ] diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h index b2f3e9d01ee..e565ef88745 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -39,6 +39,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284 bool rolcanlogin; /* allowed to log in as session user? */ bool rolreplication; /* role used for streaming replication */ bool rolbypassrls; /* bypasses row-level security? */ + bool rolbypassleakproof; /* bypasses leakproof checks? */ int32 rolconnlimit; /* max connections allowed (-1=no limit) */ /* remaining fields may be null; use heap_getattr to read them! */ diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..68f5a4a76e6 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -286,5 +286,6 @@ extern void RemoveRoleFromInitPriv(Oid roleid, extern bool object_ownercheck(Oid classid, Oid objectid, Oid roleid); extern bool has_createrole_privilege(Oid roleid); extern bool has_bypassrls_privilege(Oid roleid); +extern bool has_bypassleakproof_privilege(Oid roleid); #endif /* ACL_H */ -- 2.39.2
From 383f11f674479e78b1e39487dc764704bafa4732 Mon Sep 17 00:00:00 2001 From: Andreas Lind <andreaslindpeter...@gmail.com> Date: Fri, 18 Apr 2025 15:23:20 +0200 Subject: [PATCH v1 4/4] Add bypassleakproof test --- src/test/regress/expected/bypassleakproof.out | 55 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/bypassleakproof.sql | 52 ++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/test/regress/expected/bypassleakproof.out create mode 100644 src/test/regress/sql/bypassleakproof.sql diff --git a/src/test/regress/expected/bypassleakproof.out b/src/test/regress/expected/bypassleakproof.out new file mode 100644 index 00000000000..93defc04636 --- /dev/null +++ b/src/test/regress/expected/bypassleakproof.out @@ -0,0 +1,55 @@ +-- BYPASSLEAKPROOF flag +CREATE TYPE foo_or_bar_enum AS ENUM('foo', 'bar'); +CREATE TABLE base_tbl (tenant_id int, foo_or_bar foo_or_bar_enum); +ALTER TABLE base_tbl ENABLE ROW LEVEL SECURITY; +INSERT INTO base_tbl(tenant_id, foo_or_bar) +SELECT 1, 'foo' FROM generate_series(1, 1000) AS n; +INSERT INTO base_tbl(tenant_id, foo_or_bar) +SELECT 2, 'foo' FROM generate_series(1, 1000) AS n; +INSERT INTO base_tbl(tenant_id, foo_or_bar) +VALUES(2, 'bar'); +CREATE FUNCTION is_bar(val foo_or_bar_enum) RETURNS boolean AS $$ SELECT val = 'bar'; $$ LANGUAGE SQL IMMUTABLE; +CREATE INDEX ON base_tbl(tenant_id, (is_bar(foo_or_bar))); +ANALYZE base_tbl; +CREATE USER regress_bypassleakproof_nobypassleakproof_user NOBYPASSLEAKPROOF; +CREATE USER regress_bypassleakproof_bypassleakproof_user BYPASSLEAKPROOF; +GRANT SELECT ON base_tbl TO regress_bypassleakproof_nobypassleakproof_user; +GRANT SELECT ON base_tbl TO regress_bypassleakproof_bypassleakproof_user; +-- Check that the flag gets set correctly: +SELECT rolname, rolbypassleakproof FROM pg_catalog.pg_roles +WHERE rolname LIKE 'regress_bypassleakproof_%' +ORDER BY rolname; + rolname | rolbypassleakproof +------------------------------------------------+-------------------- + regress_bypassleakproof_bypassleakproof_user | t + regress_bypassleakproof_nobypassleakproof_user | f +(2 rows) + +CREATE POLICY only_tenant_2 ON base_tbl FOR ALL +TO regress_bypassleakproof_nobypassleakproof_user, regress_bypassleakproof_bypassleakproof_user +USING (tenant_id = 2); +SET SESSION AUTHORIZATION regress_bypassleakproof_nobypassleakproof_user; +EXPLAIN (verbose, costs off) SELECT * FROM base_tbl WHERE is_bar(foo_or_bar); + QUERY PLAN +----------------------------------------------------------------------------------------- + Seq Scan on public.base_tbl + Output: tenant_id, foo_or_bar + Filter: ((base_tbl.tenant_id = 2) AND (base_tbl.foo_or_bar = 'bar'::foo_or_bar_enum)) +(3 rows) + +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION regress_bypassleakproof_bypassleakproof_user; +EXPLAIN (verbose, costs off) SELECT * FROM base_tbl WHERE is_bar(foo_or_bar); + QUERY PLAN +------------------------------------------------------------------------------------------------------ + Index Scan using base_tbl_tenant_id_is_bar_idx on public.base_tbl + Output: tenant_id, foo_or_bar + Index Cond: ((base_tbl.tenant_id = 2) AND ((base_tbl.foo_or_bar = 'bar'::foo_or_bar_enum) = true)) +(3 rows) + +RESET SESSION AUTHORIZATION; +DROP TABLE base_tbl CASCADE; +DROP USER regress_bypassleakproof_nobypassleakproof_user; +DROP USER regress_bypassleakproof_bypassleakproof_user; +DROP FUNCTION is_bar; +DROP TYPE foo_or_bar_enum; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f38caa0d24..12431a92096 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -43,7 +43,7 @@ test: copy copyselect copydml copyencoding insert insert_conflict # Note: many of the tests in later groups depend on create_index # ---------- test: create_function_c create_misc create_operator create_procedure create_table create_type create_schema -test: create_index create_index_spgist create_view index_including index_including_gist +test: create_index create_index_spgist create_view index_including index_including_gist bypassleakproof # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/bypassleakproof.sql b/src/test/regress/sql/bypassleakproof.sql new file mode 100644 index 00000000000..2ee42311e91 --- /dev/null +++ b/src/test/regress/sql/bypassleakproof.sql @@ -0,0 +1,52 @@ +-- BYPASSLEAKPROOF flag + +CREATE TYPE foo_or_bar_enum AS ENUM('foo', 'bar'); + +CREATE TABLE base_tbl (tenant_id int, foo_or_bar foo_or_bar_enum); +ALTER TABLE base_tbl ENABLE ROW LEVEL SECURITY; + +INSERT INTO base_tbl(tenant_id, foo_or_bar) +SELECT 1, 'foo' FROM generate_series(1, 1000) AS n; +INSERT INTO base_tbl(tenant_id, foo_or_bar) +SELECT 2, 'foo' FROM generate_series(1, 1000) AS n; +INSERT INTO base_tbl(tenant_id, foo_or_bar) +VALUES(2, 'bar'); + +CREATE FUNCTION is_bar(val foo_or_bar_enum) RETURNS boolean AS $$ SELECT val = 'bar'; $$ LANGUAGE SQL IMMUTABLE; + +CREATE INDEX ON base_tbl(tenant_id, (is_bar(foo_or_bar))); + +ANALYZE base_tbl; + +CREATE USER regress_bypassleakproof_nobypassleakproof_user NOBYPASSLEAKPROOF; +CREATE USER regress_bypassleakproof_bypassleakproof_user BYPASSLEAKPROOF; +GRANT SELECT ON base_tbl TO regress_bypassleakproof_nobypassleakproof_user; +GRANT SELECT ON base_tbl TO regress_bypassleakproof_bypassleakproof_user; + +-- Check that the flag gets set correctly: +SELECT rolname, rolbypassleakproof FROM pg_catalog.pg_roles +WHERE rolname LIKE 'regress_bypassleakproof_%' +ORDER BY rolname; + +CREATE POLICY only_tenant_2 ON base_tbl FOR ALL +TO regress_bypassleakproof_nobypassleakproof_user, regress_bypassleakproof_bypassleakproof_user +USING (tenant_id = 2); + +SET SESSION AUTHORIZATION regress_bypassleakproof_nobypassleakproof_user; + +EXPLAIN (verbose, costs off) SELECT * FROM base_tbl WHERE is_bar(foo_or_bar); + +RESET SESSION AUTHORIZATION; + +SET SESSION AUTHORIZATION regress_bypassleakproof_bypassleakproof_user; + +EXPLAIN (verbose, costs off) SELECT * FROM base_tbl WHERE is_bar(foo_or_bar); + +RESET SESSION AUTHORIZATION; + +DROP TABLE base_tbl CASCADE; + +DROP USER regress_bypassleakproof_nobypassleakproof_user; +DROP USER regress_bypassleakproof_bypassleakproof_user; +DROP FUNCTION is_bar; +DROP TYPE foo_or_bar_enum; -- 2.39.2