Hi all!

As part of a customer project we are looking to implement an reloption for views which when set, runs the subquery as invoked by the user rather than the view owner, as is currently the case. The rewrite rule's table references are then checked as if the user were referencing the table(s) directly.

This feature is similar to so-called 'SECURITY INVOKER' views in other DBMS.
Although such permission checking could be implemented using views which SELECT from a table function and further using triggers, that approach has obvious performance downsides.

Our initial thought on implementing this was to simply add another reloption for views, just like the already existing `security_barrier`. With this in place, we then can conditionally evaluate in RelationBuildRuleLock() if we need to call setRuleCheckAsUser() or not. The new reloption has been named `security`, which is an enum currently only supporting a single value: `relation_permissions`.

The code for fetching the rules and triggers in RelationBuildDesc() had to be moved after the parsing of the reloptions, since with this change RelationBuildRuleLock()now depends upon having relation->rd_options available.

The current behavior of views without that new reloption set is unaltered.
This is implemented as such in patch 0001.

Regression tests are included for both the new reloption of CREATE VIEW and the row level security side of this too, contained in patch 0002.
All regression tests are passing without errors.

Finally, patch 0003 updates the documentation for this new reloption.

An simplified example on how this feature can be used could look like this:

  CREATE TABLE people (id int, name text, company text);
  ALTER TABLE people ENABLE ROW LEVEL SECURITY;
  INSERT INTO people VALUES (1, 'alice', 'foo'), (2, 'bob', 'bar');

  CREATE VIEW customers_no_security
      AS SELECT * FROM people;

  CREATE VIEW customers
      WITH (security=relation_permissions)
      AS SELECT * FROM people;

  -- We want carol to only see people from company 'foo'
  CREATE ROLE carol;
  CREATE POLICY company_foo_only
      ON people FOR ALL TO carol USING (company = 'foo');

  GRANT SELECT ON people TO carol;
  GRANT SELECT ON customers_no_security TO carol;
  GRANT SELECT ON customers TO carol;

Now using these tables as carol:

    postgres=# SET ROLE carol;
    SET

For the `people` table, the policy is applied as expected:

    postgres=> SELECT * FROM people;
     id | name  | company
    ----+-------+---------
      1 | alice | foo
    (1 row)

If we now use the view with the new relopt set, the policy is applied too:

    postgres=> SELECT * FROM customers;
     id | name  | company
    ----+-------+---------
      1 | alice | foo
    (1 row)

But without the `security=relation_permissions` relopt, carol gets to see data they should not be able to due to the policy not being applied, since the rules are checked against the view owner:

    postgres=> SELECT * FROM customers_no_security;
     id | name  | company
    ----+-------+---------
      1 | alice | foo
      2 | bob   | bar
    (2 rows)


Excluding regression tests and documentation, the changes boil down to this:
 src/backend/access/common/reloptions.c    | 20
 src/backend/nodes/copyfuncs.c             |  1
 src/backend/nodes/equalfuncs.c            |  1
 src/backend/nodes/outfuncs.c              |  1
 src/backend/nodes/readfuncs.c             |  1
 src/backend/optimizer/plan/subselect.c    |  1
 src/backend/optimizer/prep/prepjointree.c |  1
 src/backend/rewrite/rewriteHandler.c      |  1
 src/backend/utils/cache/relcache.c        | 62
 src/include/nodes/parsenodes.h            |  3
 src/include/utils/rel.h                   | 21
 11 files changed, 84 insertions(+), 29 deletions(-)

All patches are against current master.

Thanks,
Christoph Heiss
From 2ed6b63adcebfff14965b8c9913ae0fafbe904a2 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.he...@cybertec.at>
Date: Fri, 17 Dec 2021 17:17:54 +0100
Subject: [PATCH 3/3] Add documentation for new 'security' reloption on views

---
 doc/src/sgml/ddl.sgml             |  4 ++++
 doc/src/sgml/ref/alter_view.sgml  |  9 +++++++++
 doc/src/sgml/ref/create_view.sgml | 18 ++++++++++++++++++
 3 files changed, 31 insertions(+)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 64d9030652..760ea2f794 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -2292,6 +2292,10 @@ GRANT SELECT (col1), UPDATE (col1) ON mytable TO miriam_rw;
    are not subject to row security.
   </para>
 
+  <para>
+   For views, the policies are applied as being referenced through the view owner by default, rather than the user referencing the view. To apply row security policies as defined for the invoking user, the <firstterm>security</firstterm> option can be set on views (see <link linkend="sql-createview">CREATE VIEW</link>) to get the same behavior.
+  </para>
+
   <para>
    Row security policies can be specific to commands, or to roles, or to
    both.  A policy can be specified to apply to <literal>ALL</literal>
diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..3555a61017 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -161,6 +161,15 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>security</literal> (<type>enum</type>)</term>
+        <listitem>
+         <para>
+          Changes the security option of the view.  The only valid value is
+          <literal>relation_permissions</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index bf03287592..2c7e1d5561 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -152,6 +152,24 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>security</literal> (<type>enum</type>)</term>
+        <listitem>
+         <para>
+          This parameter may be set to <literal>relation_permissions</literal>,
+          which will cause privileges on tables to be checked as referenced by
+          the invoking user, rather than the view owner.
+          It will only take effect when row level security is enabled on the
+          underlying tables (using <link linkend="sql-altertable">
+          <command>ALTER TABLE ... ENABLE ROW LEVEL SECURITY</command></link>).
+         </para>
+         <para>This option can be changed on existing views using <link
+          linkend="sql-alterview"><command>ALTER VIEW</command></link>. See
+          <xref linkend="ddl-rowsecurity"/> for more details on row level security.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist></para>
     </listitem>
    </varlistentry>
-- 
2.34.1

From ea0771bdd38f9aa12fd97f0c824736dee02f55c1 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.he...@cybertec.at>
Date: Thu, 16 Dec 2021 19:25:24 +0100
Subject: [PATCH 2/3] Add regression tests for new 'security' reloption for
 views

This expands on the current regressions tests for CREATE VIEW and ROW LEVEL
SECURITY-related matters.
---
 src/test/regress/expected/create_view.out | 31 +++++++++++-----
 src/test/regress/expected/rowsecurity.out | 43 ++++++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 16 ++++++---
 src/test/regress/sql/rowsecurity.sql      | 26 ++++++++++++++
 4 files changed, 102 insertions(+), 14 deletions(-)

diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index f50ef76685..4274bdcc15 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -261,23 +261,31 @@ CREATE VIEW mysecview3 WITH (security_barrier=false)
        AS SELECT * FROM tbl1 WHERE a < 0;
 CREATE VIEW mysecview4 WITH (security_barrier)
        AS SELECT * FROM tbl1 WHERE a <> 0;
-CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
+CREATE VIEW mysecview5 WITH (security=relation_permissions)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview6 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 ERROR:  invalid value for boolean option "security_barrier": 100
-CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
+CREATE VIEW mysecview7 WITH (security=invalid)		-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for enum option "security": invalid
+DETAIL:  Only valid value is "relation_permissions".
+CREATE VIEW mysecview8 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview5'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |           reloptions            
+------------+---------+---------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | {security_barrier=true}
  mysecview3 | v       | {security_barrier=false}
  mysecview4 | v       | {security_barrier=true}
-(4 rows)
+ mysecview5 | v       | {security=relation_permissions}
+(5 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -287,9 +295,12 @@ CREATE OR REPLACE VIEW mysecview3 WITH (security_barrier=true)
        AS SELECT * FROM tbl1 WHERE a < 256;
 CREATE OR REPLACE VIEW mysecview4 WITH (security_barrier=false)
        AS SELECT * FROM tbl1 WHERE a <> 256;
+CREATE OR REPLACE VIEW mysecview5
+       AS SELECT * FROM tbl1 WHERE a > 256;
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview5'::regclass)
        ORDER BY relname;
   relname   | relkind |        reloptions        
 ------------+---------+--------------------------
@@ -297,7 +308,8 @@ SELECT relname, relkind, reloptions FROM pg_class
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview5 | v       | 
+(5 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -1994,7 +2006,7 @@ drop cascades to view aliased_view_2
 drop cascades to view aliased_view_3
 drop cascades to view aliased_view_4
 DROP SCHEMA testviewschm2 CASCADE;
-NOTICE:  drop cascades to 73 other objects
+NOTICE:  drop cascades to 74 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2015,6 +2027,7 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview5
 drop cascades to view unspecified_types
 drop cascades to table tt1
 drop cascades to table tx1
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 89397e41f0..99ce4ce2e0 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -8,9 +8,11 @@ DROP USER IF EXISTS regress_rls_alice;
 DROP USER IF EXISTS regress_rls_bob;
 DROP USER IF EXISTS regress_rls_carol;
 DROP USER IF EXISTS regress_rls_dave;
+DROP USER IF EXISTS regress_rls_grace;
 DROP USER IF EXISTS regress_rls_exempt_user;
 DROP ROLE IF EXISTS regress_rls_group1;
 DROP ROLE IF EXISTS regress_rls_group2;
+DROP ROLE IF EXISTS regress_rls_group3;
 DROP SCHEMA IF EXISTS regress_rls_schema CASCADE;
 RESET client_min_messages;
 -- initial setup
@@ -18,11 +20,14 @@ CREATE USER regress_rls_alice NOLOGIN;
 CREATE USER regress_rls_bob NOLOGIN;
 CREATE USER regress_rls_carol NOLOGIN;
 CREATE USER regress_rls_dave NOLOGIN;
+CREATE USER regress_rls_grace NOLOGIN;
 CREATE USER regress_rls_exempt_user BYPASSRLS NOLOGIN;
 CREATE ROLE regress_rls_group1 NOLOGIN;
 CREATE ROLE regress_rls_group2 NOLOGIN;
+CREATE ROLE regress_rls_group3 NOLOGIN;
 GRANT regress_rls_group1 TO regress_rls_bob;
 GRANT regress_rls_group2 TO regress_rls_carol;
+GRANT regress_rls_group3 TO regress_rls_grace;
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
 SET search_path = regress_rls_schema;
@@ -627,6 +632,39 @@ SELECT * FROM category;
   44 | manga
 (4 rows)
 
+-- Test views with security=relation_permissions reloption set
+RESET SESSION AUTHORIZATION;
+SET row_security TO ON;
+CREATE POLICY p3 ON category FOR ALL TO regress_rls_group3 USING (cname = 'novel');
+CREATE VIEW v1 WITH (security=relation_permissions) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION relation_permissions_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security=relation_permissions) AS
+SELECT * FROM relation_permissions_func();
+GRANT SELECT ON category TO regress_rls_group3;
+GRANT SELECT ON v1 TO regress_rls_group3;
+GRANT SELECT ON v1f TO regress_rls_group3;
+SET SESSION AUTHORIZATION regress_rls_grace;
+SELECT * FROM category;
+ cid | cname 
+-----+-------
+  11 | novel
+(1 row)
+
+SELECT * FROM v1;
+ cid | cname 
+-----+-------
+  11 | novel
+(1 row)
+
+SELECT * FROM v1f;
+ cid | cname 
+-----+-------
+  11 | novel
+(1 row)
+
 --
 -- Table inheritance and RLS policy
 --
@@ -3987,11 +4025,14 @@ RESET SESSION AUTHORIZATION;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 29 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
 drop cascades to table document
+drop cascades to view v1
+drop cascades to function relation_permissions_func()
+drop cascades to view v1f
 drop cascades to table part_document
 drop cascades to table dependent
 drop cascades to table rec1
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index bdda56e8de..9e8e768b3d 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -214,13 +214,18 @@ CREATE VIEW mysecview3 WITH (security_barrier=false)
        AS SELECT * FROM tbl1 WHERE a < 0;
 CREATE VIEW mysecview4 WITH (security_barrier)
        AS SELECT * FROM tbl1 WHERE a <> 0;
-CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
+CREATE VIEW mysecview5 WITH (security=relation_permissions)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview6 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
-CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
+CREATE VIEW mysecview7 WITH (security=invalid)		-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+CREATE VIEW mysecview8 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview5'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -231,9 +236,12 @@ CREATE OR REPLACE VIEW mysecview3 WITH (security_barrier=true)
        AS SELECT * FROM tbl1 WHERE a < 256;
 CREATE OR REPLACE VIEW mysecview4 WITH (security_barrier=false)
        AS SELECT * FROM tbl1 WHERE a <> 256;
+CREATE OR REPLACE VIEW mysecview5
+       AS SELECT * FROM tbl1 WHERE a > 256;
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview5'::regclass)
        ORDER BY relname;
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 44deb42bad..f30c08b795 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -11,9 +11,11 @@ DROP USER IF EXISTS regress_rls_alice;
 DROP USER IF EXISTS regress_rls_bob;
 DROP USER IF EXISTS regress_rls_carol;
 DROP USER IF EXISTS regress_rls_dave;
+DROP USER IF EXISTS regress_rls_grace;
 DROP USER IF EXISTS regress_rls_exempt_user;
 DROP ROLE IF EXISTS regress_rls_group1;
 DROP ROLE IF EXISTS regress_rls_group2;
+DROP ROLE IF EXISTS regress_rls_group3;
 
 DROP SCHEMA IF EXISTS regress_rls_schema CASCADE;
 
@@ -24,12 +26,15 @@ CREATE USER regress_rls_alice NOLOGIN;
 CREATE USER regress_rls_bob NOLOGIN;
 CREATE USER regress_rls_carol NOLOGIN;
 CREATE USER regress_rls_dave NOLOGIN;
+CREATE USER regress_rls_grace NOLOGIN;
 CREATE USER regress_rls_exempt_user BYPASSRLS NOLOGIN;
 CREATE ROLE regress_rls_group1 NOLOGIN;
 CREATE ROLE regress_rls_group2 NOLOGIN;
+CREATE ROLE regress_rls_group3 NOLOGIN;
 
 GRANT regress_rls_group1 TO regress_rls_bob;
 GRANT regress_rls_group2 TO regress_rls_carol;
+GRANT regress_rls_group3 TO regress_rls_grace;
 
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
@@ -225,6 +230,27 @@ SET row_security TO OFF;
 SELECT * FROM document;
 SELECT * FROM category;
 
+-- Test views with security=relation_permissions reloption set
+RESET SESSION AUTHORIZATION;
+SET row_security TO ON;
+CREATE POLICY p3 ON category FOR ALL TO regress_rls_group3 USING (cname = 'novel');
+CREATE VIEW v1 WITH (security=relation_permissions) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION relation_permissions_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security=relation_permissions) AS
+SELECT * FROM relation_permissions_func();
+
+GRANT SELECT ON category TO regress_rls_group3;
+GRANT SELECT ON v1 TO regress_rls_group3;
+GRANT SELECT ON v1f TO regress_rls_group3;
+
+SET SESSION AUTHORIZATION regress_rls_grace;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
 --
 -- Table inheritance and RLS policy
 --
-- 
2.34.1

From 5ec76812a438322d39ff10b84b949ae79361878a Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.he...@cybertec.at>
Date: Thu, 16 Dec 2021 19:19:48 +0100
Subject: [PATCH 1/3] Add new reloption enum 'security' to views

Only a single value is supported for now: 'relation_permissions'.
When this relopt is set on a view, all rules table references will
be checked against the invoking user rather than the view owner, as is currently
implemented.
---
 src/backend/access/common/reloptions.c    | 20 ++++++++
 src/backend/nodes/copyfuncs.c             |  1 +
 src/backend/nodes/equalfuncs.c            |  1 +
 src/backend/nodes/outfuncs.c              |  1 +
 src/backend/nodes/readfuncs.c             |  1 +
 src/backend/optimizer/plan/subselect.c    |  1 +
 src/backend/optimizer/prep/prepjointree.c |  1 +
 src/backend/rewrite/rewriteHandler.c      |  1 +
 src/backend/utils/cache/relcache.c        | 62 +++++++++++++----------
 src/include/nodes/parsenodes.h            |  3 ++
 src/include/utils/rel.h                   | 21 +++++++-
 11 files changed, 84 insertions(+), 29 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index b5602f5323..2882a383bd 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -489,6 +489,13 @@ relopt_enum_elt_def gistBufferingOptValues[] =
 	{(const char *) NULL}		/* list terminator */
 };
 
+/* values from ViewOptSecurity */
+relopt_enum_elt_def viewSecurityOptValues[] =
+{
+	{"relation_permissions", VIEW_OPTION_SECURITY_RELATION_PERMISSIONS},
+	{(const char *) NULL}		/* list terminator */
+};
+
 /* values from ViewOptCheckOption */
 relopt_enum_elt_def viewCheckOptValues[] =
 {
@@ -522,6 +529,17 @@ static relopt_enum enumRelOpts[] =
 		GIST_OPTION_BUFFERING_AUTO,
 		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
 	},
+	{
+		{
+			"security",
+			"View has security option defined (only relation_permissions).",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		viewSecurityOptValues,
+		VIEW_OPTION_SECURITY_NOT_SET,
+		gettext_noop("Only valid value is \"relation_permissions\".")
+	},
 	{
 		{
 			"check_option",
@@ -1996,6 +2014,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security", RELOPT_TYPE_ENUM,
+		offsetof(ViewOptions, security_option)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index df0b747883..9741d63d87 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2464,6 +2464,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_NODE_FIELD(tablesample);
 	COPY_NODE_FIELD(subquery);
 	COPY_SCALAR_FIELD(security_barrier);
+	COPY_SCALAR_FIELD(security_relation_permissions);
 	COPY_SCALAR_FIELD(jointype);
 	COPY_SCALAR_FIELD(joinmergedcols);
 	COPY_NODE_FIELD(joinaliasvars);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index cb7ddd463c..d7052a96b8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2766,6 +2766,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
 	COMPARE_NODE_FIELD(tablesample);
 	COMPARE_NODE_FIELD(subquery);
 	COMPARE_SCALAR_FIELD(security_barrier);
+	COMPARE_SCALAR_FIELD(security_relation_permissions);
 	COMPARE_SCALAR_FIELD(jointype);
 	COMPARE_SCALAR_FIELD(joinmergedcols);
 	COMPARE_NODE_FIELD(joinaliasvars);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 91a89b6d51..d9884a6100 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3260,6 +3260,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 		case RTE_SUBQUERY:
 			WRITE_NODE_FIELD(subquery);
 			WRITE_BOOL_FIELD(security_barrier);
+			WRITE_BOOL_FIELD(security_relation_permissions);
 			break;
 		case RTE_JOIN:
 			WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index dcec3b728f..6ffec8a940 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1446,6 +1446,7 @@ _readRangeTblEntry(void)
 		case RTE_SUBQUERY:
 			READ_NODE_FIELD(subquery);
 			READ_BOOL_FIELD(security_barrier);
+			READ_BOOL_FIELD(security_relation_permissions);
 			break;
 		case RTE_JOIN:
 			READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index c9f7a09d10..cbdd4cfee1 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1216,6 +1216,7 @@ inline_cte_walker(Node *node, inline_cte_walker_context *context)
 			rte->rtekind = RTE_SUBQUERY;
 			rte->subquery = newquery;
 			rte->security_barrier = false;
+			rte->security_relation_permissions = false;
 
 			/* Zero out CTE-specific fields */
 			rte->ctename = NULL;
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 387a35e112..59cdc84338 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -660,6 +660,7 @@ preprocess_function_rtes(PlannerInfo *root)
 				rte->rtekind = RTE_SUBQUERY;
 				rte->subquery = funcquery;
 				rte->security_barrier = false;
+				rte->security_relation_permissions = false;
 				/* Clear fields that should not be set in a subquery RTE */
 				rte->functions = NIL;
 				rte->funcordinality = false;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 9521e81100..ba47cabf1f 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1838,6 +1838,7 @@ ApplyRetrieveRule(Query *parsetree,
 	rte->rtekind = RTE_SUBQUERY;
 	rte->subquery = rule_action;
 	rte->security_barrier = RelationIsSecurityView(relation);
+	rte->security_relation_permissions = RelationHasSecurityRelationPermissions(relation);
 	/* Clear fields that should not be set in a subquery RTE */
 	rte->relid = InvalidOid;
 	rte->relkind = 0;
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 105d8d4601..d962ff2b53 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -825,11 +825,14 @@ RelationBuildRuleLock(Relation relation)
 		pfree(rule_str);
 
 		/*
-		 * We want the rule's table references to be checked as though by the
-		 * table owner, not the user referencing the rule.  Therefore, scan
-		 * through the rule's actions and set the checkAsUser field on all
-		 * rtable entries.  We have to look at the qual as well, in case it
-		 * contains sublinks.
+		 * If we're dealing with a view and that view has the security
+		 * relopt set to relation_permissions, we want the rule's table
+		 * references to be checked as the user referencing the rule.
+		 *
+		 * In all other cases, we want the rule's table references to be checked
+		 * as though by the table owner.  Therefore, scan through the rule's
+		 * actions and set the checkAsUser field on all rtable entries.  We
+		 * have to look at the qual as well, in case it contains sublinks.
 		 *
 		 * The reason for doing this when the rule is loaded, rather than when
 		 * it is stored, is that otherwise ALTER TABLE OWNER would have to
@@ -837,8 +840,11 @@ RelationBuildRuleLock(Relation relation)
 		 * the rule tree during load is relatively cheap (compared to
 		 * constructing it in the first place), so we do it here.
 		 */
-		setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
-		setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		if (!(relation->rd_rel->relkind == RELKIND_VIEW
+			  && RelationHasSecurityRelationPermissions(relation))) {
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1169,6 @@ retry:
 	 */
 	RelationBuildTupleDesc(relation);
 
-	/*
-	 * Fetch rules and triggers that affect this relation
-	 */
-	if (relation->rd_rel->relhasrules)
-		RelationBuildRuleLock(relation);
-	else
-	{
-		relation->rd_rules = NULL;
-		relation->rd_rulescxt = NULL;
-	}
-
-	if (relation->rd_rel->relhastriggers)
-		RelationBuildTriggers(relation);
-	else
-		relation->trigdesc = NULL;
-
-	if (relation->rd_rel->relrowsecurity)
-		RelationBuildRowSecurity(relation);
-	else
-		relation->rd_rsdesc = NULL;
-
 	/* foreign key data is not loaded till asked for */
 	relation->rd_fkeylist = NIL;
 	relation->rd_fkeyvalid = false;
@@ -1215,6 +1200,27 @@ retry:
 	/* extract reloptions if any */
 	RelationParseRelOptions(relation, pg_class_tuple);
 
+	/*
+	 * Fetch rules and triggers that affect this relation
+	 */
+	if (relation->rd_rel->relhasrules)
+		RelationBuildRuleLock(relation);
+	else
+	{
+		relation->rd_rules = NULL;
+		relation->rd_rulescxt = NULL;
+	}
+
+	if (relation->rd_rel->relhastriggers)
+		RelationBuildTriggers(relation);
+	else
+		relation->trigdesc = NULL;
+
+	if (relation->rd_rel->relrowsecurity)
+		RelationBuildRowSecurity(relation);
+	else
+		relation->rd_rsdesc = NULL;
+
 	/*
 	 * initialize the relation lock manager information
 	 */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4c5a8a39bf..921c80b0f2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1042,6 +1042,9 @@ typedef struct RangeTblEntry
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
 
+	/* Is from a view defined with the security option set? */
+	bool		security_relation_permissions;
+
 	/*
 	 * Fields valid for a join RTE (else NULL/zero):
 	 *
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 31281279cf..04fda5822c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -390,6 +390,13 @@ typedef enum ViewOptCheckOption
 	VIEW_OPTION_CHECK_OPTION_CASCADED
 } ViewOptCheckOption;
 
+/* ViewOptions->security values */
+typedef enum ViewOptSecurityOption
+{
+	VIEW_OPTION_SECURITY_NOT_SET,
+	VIEW_OPTION_SECURITY_RELATION_PERMISSIONS
+} ViewOptSecurityOption;
+
 /*
  * ViewOptions
  *		Contents of rd_options for views
@@ -398,7 +405,8 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
-	ViewOptCheckOption check_option;
+	ViewOptSecurityOption	security_option;
+	ViewOptCheckOption		check_option;
 } ViewOptions;
 
 /*
@@ -411,6 +419,17 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityRelationPermissions
+ *		Returns true if the relation is a view defined with the security option
+ *      set to relation_permissions.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityRelationPermissions(relation)					\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options &&												\
+	 ((ViewOptions *) (relation)->rd_options)->security_option ==			\
+	 VIEW_OPTION_SECURITY_RELATION_PERMISSIONS)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
-- 
2.34.1

Reply via email to