From 3cb0a7e75aaa9a7826c769068970ce2200e61023 Mon Sep 17 00:00:00 2001
From: Noah Misch <noah@leadboat.com>
Date: Sat, 3 Oct 2015 20:19:57 -0400
Subject: [PATCH] Make BYPASSRLS behave like superuser RLS bypass.

Specifically, make its effect independent from the row_security GUC, and
make it affect permission checks pertinent to views the BYPASSRLS role
owns.  The row_security GUC thereby ceases to change successful-query
behavior; it can only make a query fail with an error.  Back-patch to
9.5, where BYPASSRLS was introduced.
---
 doc/src/sgml/catalogs.sgml                |  6 ++--
 doc/src/sgml/config.sgml                  | 25 ++++++---------
 doc/src/sgml/ddl.sgml                     | 19 ++++-------
 doc/src/sgml/ref/create_role.sgml         |  9 ++----
 src/backend/utils/misc/rls.c              | 39 +++++++----------------
 src/include/catalog/pg_authid.h           |  2 +-
 src/test/regress/expected/rowsecurity.out | 14 ++++----
 src/test/regress/sql/rowsecurity.sql      |  6 ++--
 8 files changed, 44 insertions(+), 76 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e8bd3a11c8c..28bb480447e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1454,7 +1454,7 @@
       <entry><structfield>rolbypassrls</structfield></entry>
       <entry><type>bool</type></entry>
       <entry>
-       Role can bypass row level security policies, see
+       Role bypasses every row level security policy, see
        <xref linkend="ddl-rowsecurity"> for more information.
       </entry>
      </row>
@@ -9413,7 +9413,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
       <entry><type>bool</type></entry>
       <entry></entry>
       <entry>
-       User can bypass row level security policies, see
+       User bypasses every row level security policy, see
        <xref linkend="ddl-rowsecurity"> for more information.
       </entry>
      </row>
@@ -9888,7 +9888,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
       <entry><structfield>usebypassrls</structfield></entry>
       <entry><type>bool</type></entry>
       <entry>
-       User can bypass row level security policies, see
+       User bypasses every row level security policy, see
        <xref linkend="ddl-rowsecurity"> for more information.
       </entry>
      </row>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 696f286b760..c174d68dc13 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5591,22 +5591,15 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </term>
       <listitem>
        <para>
-        This variable controls if row security policies are to be applied
-        to queries which are run against tables that have row security enabled.
-        The default is <literal>on</>.  When set to <literal>on</>, all users,
-        except superusers and the owner of the table, will have the row
-        policies for the table applied to their queries.  When set to
-        <literal>off</>, queries will bypass row policies for the table, if
-        possible, and error if not.
-       </para>
-
-       <para>
-        For a user who is not a superuser and not the table owner to bypass
-        row policies for the table, they must have the <literal>BYPASSRLS</>
-        role attribute. If this is set to <literal>off</> and the user queries
-        a table which has row policies enabled and the user does not have the
-        right to bypass row policies then a permission denied error will be
-        returned.
+        This variable controls whether to raise an error in lieu of applying a
+        row security policy.  When set to <literal>on</>, policies apply
+        normally.  When set to <literal>off</>, queries fail which would
+        otherwise apply at least one policy.  The default is <literal>on</>.
+        Change to <literal>off</> where limited row visibility could cause
+        incorrect results; for example, <application>pg_dump</> makes that
+        change by default.  This variable has no effect on roles which bypass
+        every row security policy, to wit, superusers and roles with
+        the <literal>BYPASSRLS</> attribute.
        </para>
 
        <para>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 052cf25ef3b..a889ffc5472 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1543,8 +1543,12 @@ REVOKE ALL ON accounts FROM PUBLIC;
    Row security policies can be specific to commands, or to roles, or to
    both.  The commands available are <literal>ALL</literal>,
    <literal>SELECT</>, <literal>INSERT</>, <literal>UPDATE</>, and
-   <literal>DELETE</>.  Multiple roles can be assigned to a given policy
-   and normal role membership and inheritance rules apply.
+   <literal>DELETE</>.  Multiple roles can be assigned to a given policy and
+   normal role membership and inheritance rules apply.  Table owners,
+   superusers, and roles with the <literal>BYPASSRLS</> attribute bypass the
+   row security system when querying a table.  Applications that expect to
+   bypass all row security through those mechanisms should
+   set <xref linkend="guc-row-security"> to <literal>off</>.
   </para>
 
   <para>
@@ -1574,17 +1578,6 @@ REVOKE ALL ON accounts FROM PUBLIC;
    <xref linkend="sql-altertable"> command.
   </para>
 
-  <para>
-   The table owners and superusers bypass the row security system when
-   querying a table.  Any user can request that row security be bypassed by
-   setting <xref linkend="guc-row-security"> to <literal>off</literal>.  If
-   the user does not have privileges to bypass row security when querying a
-   given table then an error will be returned instead.  Other users can be
-   granted the ability to bypass the row security system with
-   the <literal>BYPASSRLS</literal> role attribute.  This attribute can only
-   be set by a superuser.
-  </para>
-
   <para>
    Each policy has a name and multiple policies can be defined for a
    table.  As policies are table-specific, each policy for a table must
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index f4a176bff3f..240c21ce85f 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -196,16 +196,13 @@ CREATE ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replac
       <term><literal>NOBYPASSRLS</literal></term>
       <listitem>
        <para>
-        These clauses determine whether a role is allowed to bypass row-level security (RLS)
-        policies.  A role having the <literal>BYPASSRLS</literal> attribute will
-        be allowed to bypass row-security policies by setting
-        <literal>row_security</literal> to
-        <literal>OFF</literal>. <literal>NOBYPASSRLS</literal> is the default.
+        These clauses determine whether a role bypasses every row-level
+        security (RLS) policy.  <literal>NOBYPASSRLS</literal> is the default.
         Note that pg_dump will set <literal>row_security</literal> to
         <literal>OFF</literal> by default, to ensure all contents of a table are
         dumped out.  If the user running pg_dump does not have appropriate
         permissions, an error will be returned.  The superuser and owner of the
-        table being dumped are considered to always have the right to bypass RLS.
+        table being dumped always bypass RLS.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/backend/utils/misc/rls.c b/src/backend/utils/misc/rls.c
index c900c98848b..eaf9d6e66ff 100644
--- a/src/backend/utils/misc/rls.c
+++ b/src/backend/utils/misc/rls.c
@@ -40,10 +40,8 @@ extern int	check_enable_rls(Oid relid, Oid checkAsUser, bool noError);
  * for the table and the plan cache needs to be invalidated if the environment
  * changes.
  *
- * Handle checking as another role via checkAsUser (for views, etc). Note that
- * if *not* checking as another role, the caller should pass InvalidOid rather
- * than GetUserId(). Otherwise the check for row_security = OFF is skipped, and
- * so we may falsely report that RLS is active when the user has bypassed it.
+ * Handle checking as another role via checkAsUser (for views, etc).  Pass
+ * InvalidOid to check the current user.
  *
  * If noError is set to 'true' then we just return RLS_ENABLED instead of doing
  * an ereport() if the user has attempted to bypass RLS and they are not
@@ -78,32 +76,19 @@ check_enable_rls(Oid relid, Oid checkAsUser, bool noError)
 		return RLS_NONE;
 
 	/*
-	 * Check permissions
-	 *
-	 * Table owners always bypass RLS.  Note that superuser is always
-	 * considered an owner.  Return RLS_NONE_ENV to indicate that this
-	 * decision depends on the environment (in this case, the user_id).
+	 * Table owners and BYPASSRLS users bypass RLS.  Note that a superuser
+	 * qualifies as both.  Return RLS_NONE_ENV to indicate that this decision
+	 * depends on the environment (in this case, the user_id).
 	 */
-	if (pg_class_ownercheck(relid, user_id))
+	if (pg_class_ownercheck(relid, user_id) ||
+		has_bypassrls_privilege(user_id))
 		return RLS_NONE_ENV;
 
-	/*
-	 * If the row_security GUC is 'off', check if the user has permission to
-	 * bypass RLS.  row_security is always considered 'on' when querying
-	 * through a view or other cases where checkAsUser is valid.
-	 */
-	if (!row_security && !checkAsUser)
-	{
-		if (has_bypassrls_privilege(user_id))
-			/* OK to bypass */
-			return RLS_NONE_ENV;
-		else if (noError)
-			return RLS_ENABLED;
-		else
-			ereport(ERROR,
-					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				  errmsg("insufficient privilege to bypass row security.")));
-	}
+	/* row_security GUC says to bypass RLS, but user lacks permission */
+	if (!row_security && !noError)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("insufficient privilege to bypass row security.")));
 
 	/* RLS should be fully enabled for this relation. */
 	return RLS_ENABLED;
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index d5f19d6aabb..2c8565ea254 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -51,7 +51,7 @@ CATALOG(pg_authid,1260) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2842) BKI_SCHEMA_MAC
 	bool		rolcreatedb;	/* allowed to create databases? */
 	bool		rolcanlogin;	/* allowed to log in as session user? */
 	bool		rolreplication; /* role used for streaming replication */
-	bool		rolbypassrls;	/* allowed to bypass row level security? */
+	bool		rolbypassrls;	/* bypasses row level security? */
 	int32		rolconnlimit;	/* max connections allowed (-1=no limit) */
 
 	/* remaining fields may be null; use heap_getattr to read them! */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 0cde8fd0e3f..0363dfd07ff 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2584,10 +2584,15 @@ COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok
 SET row_security TO ON;
 COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok
 0,cfcd208495d565ef66e7dff9f98764da
+1,c4ca4238a0b923820dcc509a6f75849b
 2,c81e728d9d4c2f636f067f89cc14862c
+3,eccbc87e4b5ce2fe28308fd9f2a7baf3
 4,a87ff679a2f3e71d9181a67b7542122c
+5,e4da3b7fbbce2345d7772b0674a318d5
 6,1679091c5a880faf6fb5e6087eb1b2dc
+7,8f14e45fceea167a5a36dedd4bea2543
 8,c9f0f895fb98ab9159f51fd0297e236d
+9,45c48cce2e2d7fbdea1afc51c7c6ad26
 10,d3d9446802a44259755d38e6d163e820
 -- Check COPY TO as user without permissions. SET row_security TO OFF;
 SET SESSION AUTHORIZATION rls_regress_user2;
@@ -2627,6 +2632,7 @@ COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --ok
 1,c4ca4238a0b923820dcc509a6f75849b
 SET row_security TO ON;
 COPY copy_rel_to TO STDOUT WITH DELIMITER ','; --ok
+1,c4ca4238a0b923820dcc509a6f75849b
 -- Check COPY TO as user without permissions. SET row_security TO OFF;
 SET SESSION AUTHORIZATION rls_regress_user2;
 SET row_security TO OFF;
@@ -2650,14 +2656,10 @@ SET row_security TO ON;
 COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS.
 ERROR:  COPY FROM not supported with row level security.
 HINT:  Use direct INSERT statements instead.
--- Check COPY TO as user with permissions and BYPASSRLS
+-- Check COPY FROM as user with permissions and BYPASSRLS
 SET SESSION AUTHORIZATION rls_regress_exempt_user;
-SET row_security TO OFF;
-COPY copy_t FROM STDIN; --ok
 SET row_security TO ON;
-COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS.
-ERROR:  COPY FROM not supported with row level security.
-HINT:  Use direct INSERT statements instead.
+COPY copy_t FROM STDIN; --ok
 -- Check COPY FROM as user without permissions.
 SET SESSION AUTHORIZATION rls_regress_user2;
 SET row_security TO OFF;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6ed0daf345f..7f8772fa26c 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -1070,17 +1070,15 @@ COPY copy_t FROM STDIN; --fail - insufficient privilege to bypass rls.
 SET row_security TO ON;
 COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS.
 
--- Check COPY TO as user with permissions and BYPASSRLS
+-- Check COPY FROM as user with permissions and BYPASSRLS
 SET SESSION AUTHORIZATION rls_regress_exempt_user;
-SET row_security TO OFF;
+SET row_security TO ON;
 COPY copy_t FROM STDIN; --ok
 1	abc
 2	bcd
 3	cde
 4	def
 \.
-SET row_security TO ON;
-COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS.
 
 -- Check COPY FROM as user without permissions.
 SET SESSION AUTHORIZATION rls_regress_user2;
-- 
GitLab