[PATCH] Add reloption for views to enable RLS

Started by Christoph Heissabout 4 years ago34 messages
#1Christoph Heiss
christoph.heiss@cybertec.at
3 attachment(s)

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

Attachments:

0003-Add-documentation-for-new-security-reloption-on-view.patchtext/x-patch; charset=UTF-8; name=0003-Add-documentation-for-new-security-reloption-on-view.patchDownload
From 2ed6b63adcebfff14965b8c9913ae0fafbe904a2 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@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

0002-Add-regression-tests-for-new-security-reloption-for-.patchtext/x-patch; charset=UTF-8; name=0002-Add-regression-tests-for-new-security-reloption-for-.patchDownload
From ea0771bdd38f9aa12fd97f0c824736dee02f55c1 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@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

0001-Add-new-reloption-enum-security-to-views.patchtext/x-patch; charset=UTF-8; name=0001-Add-new-reloption-enum-security-to-views.patchDownload
From 5ec76812a438322d39ff10b84b949ae79361878a Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@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

#2Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Christoph Heiss (#1)
Re: [PATCH] Add reloption for views to enable RLS

On Fri, 2021-12-17 at 18:31 +0100, Christoph Heiss wrote:

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.

This has been requested before, see for example
https://stackoverflow.com/q/33858030/6464308

Row Level Security is only one use case; there may be other situations
when it is useful to check permissions on the underlying objects with
the current user rather than with the view owner.

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`.

You made that an enum with only a single value.
What other values could you imagine in the future?

I think that this should be a boolean reloption, for example "security_definer".
If unset or set to "off", you would get the current behavior.

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

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>

Please avoid long lines like that. Also, I don't think that the documentation on
RLS policies is the correct place for this. It should be on a page dedicated to views
or permissions.

The CREATE VIEW page already has a paragraph about this, starting with
"Access to tables referenced in the view is determined by permissions of the view owner."
This looks like the best place to me (and it would need to be adapted anyway).

Yours,
Laurenz Albe

#3Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Laurenz Albe (#2)
3 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

Hi Laurenz,

thanks for the review!
I've attached a v2 where I addressed the things you mentioned.

On 1/11/22 19:59, Laurenz Albe wrote:

[..]

You made that an enum with only a single value.
What other values could you imagine in the future?

I think that this should be a boolean reloption, for example "security_definer".
If unset or set to "off", you would get the current behavior.

A boolean option would have been indeed the better choice, I agree.
I haven't though of any specific other values for this enum, it was
rather a decision following a off-list discussion.

I've changed the option to be boolean and renamed it to
"security_invoker". This puts it in line with how other systems (e.g.
MySQL) name their equivalent feature, so I think this should be an
appropriate choice.

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

[..]

Please avoid long lines like that.

Fixed.

Also, I don't think that the documentation on
RLS policies is the correct place for this. It should be on a page dedicated to views
or permissions.

The CREATE VIEW page already has a paragraph about this, starting with
"Access to tables referenced in the view is determined by permissions of the view owner."
This looks like the best place to me (and it would need to be adapted anyway).

It makes sense to put it there, thanks for the pointer! I wasn't really
that sure where to put the documentation to start with, and this seems
like a more appropriate place.

Please review further.

Thanks,
Christoph Heiss

Attachments:

0001-PATCH-v2-1-3-Add-new-boolean-reloption-security_invo.patchtext/x-patch; charset=UTF-8; name=0001-PATCH-v2-1-3-Add-new-boolean-reloption-security_invo.patchDownload
From 25267e6b8a2ffd81f14acbee95ef08d9edf3d31c Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 18 Jan 2022 15:42:58 +0100
Subject: [PATCH 1/3] [PATCH v2 1/3] Add new boolean reloption security_invoker
 to views

When this reloption is set to true, all references to the underlying tables will
be checked against the invoking user rather than the view owner, as is currently
implemented.
Row level security must be enabled on the tables for this to take effect.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 src/backend/access/common/reloptions.c    | 11 ++++
 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        | 63 +++++++++++++----------
 src/include/nodes/parsenodes.h            |  1 +
 src/include/utils/rel.h                   | 11 ++++
 11 files changed, 65 insertions(+), 28 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index b5602f5323..3c84982fda 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"security_invoker",
+			"View subquery in invoked within the current security context.",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"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..6efaa07523 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_invoker);
 	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..7f0401fa84 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_invoker);
 	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..7dee550856 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_invoker);
 			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..95f006cf04 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_invoker);
 			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..c2b1a201ba 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_invoker = 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..18b209ef7d 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_invoker = 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..68830412c5 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_invoker = RelationHasSecurityInvoker(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..e9515ee896 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 that has the security_invoker relopt
+		 * set to true, 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,12 @@ 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
+			  && RelationHasSecurityInvoker(relation)))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1170,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 +1201,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..31d8a951ea 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1041,6 +1041,7 @@ typedef struct RangeTblEntry
 	 */
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
+	bool		security_invoker;	/* from a view with security_invoker set? */
 
 	/*
 	 * 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..d84491baa7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityRelationPermissions
+ *		Returns true if the relation has the security invoker property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
-- 
2.34.1

0002-PATCH-v2-2-3-Add-regression-tests-for-new-security_i.patchtext/x-patch; charset=UTF-8; name=0002-PATCH-v2-2-3-Add-regression-tests-for-new-security_i.patchDownload
From 2ee36aba5cb9c042bb9492d8c9fd0ab2aa1512c6 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 18 Jan 2022 15:34:13 +0100
Subject: [PATCH 2/3] [PATCH v2 2/3] Add regression tests for new
 security_invoker reloption on views

This expands on the current regressions tests for CREATE VIEW and
ROW LEVEL SECURITY-related matters.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 src/test/regress/expected/create_view.out | 42 ++++++++++++++++++----
 src/test/regress/expected/rowsecurity.out | 43 ++++++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 26 +++++++++++---
 src/test/regress/sql/rowsecurity.sql      | 26 ++++++++++++++
 4 files changed, 125 insertions(+), 12 deletions(-)

diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index f50ef76685..5df4bf389f 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -261,15 +261,26 @@ 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_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview6 WITH (security_invoker=false)
        AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview7 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview8 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 mysecview9 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a = 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
+CREATE VIEW mysecview10 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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
   relname   | relkind |        reloptions        
 ------------+---------+--------------------------
@@ -277,7 +288,10 @@ SELECT relname, relkind, reloptions FROM pg_class
  mysecview2 | v       | {security_barrier=true}
  mysecview3 | v       | {security_barrier=false}
  mysecview4 | v       | {security_barrier=true}
-(4 rows)
+ mysecview5 | v       | {security_invoker=true}
+ mysecview6 | v       | {security_invoker=false}
+ mysecview7 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -287,9 +301,17 @@ 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;
+CREATE OR REPLACE VIEW mysecview6 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview7 WITH (security_invoker=false)
+       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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
   relname   | relkind |        reloptions        
 ------------+---------+--------------------------
@@ -297,7 +319,10 @@ SELECT relname, relkind, reloptions FROM pg_class
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview5 | v       | 
+ mysecview6 | v       | {security_invoker=true}
+ mysecview7 | v       | {security_invoker=false}
+(7 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 +2019,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 76 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2015,6 +2040,9 @@ 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 mysecview6
+drop cascades to view mysecview7
 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..d10ecaa272 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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 security_invoker_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..9736fba8fd 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -214,13 +214,23 @@ 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_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview6 WITH (security_invoker=false)
        AS SELECT * FROM tbl1 WHERE a > 100;
-CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
+CREATE VIEW mysecview7 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview8 WITH (security_barrier=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+CREATE VIEW mysecview9 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview10 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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -231,9 +241,17 @@ 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;
+CREATE OR REPLACE VIEW mysecview6 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview7 WITH (security_invoker=false)
+       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, 'mysecview6'::regclass,
+                     'mysecview7'::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..239a15e3d1 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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

0003-PATCH-v2-3-3-Add-documentation-for-new-security_invo.patchtext/x-patch; charset=UTF-8; name=0003-PATCH-v2-3-3-Add-documentation-for-new-security_invo.patchDownload
From 55a0982d1124f8a26926d172542fdc771ccdc594 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 18 Jan 2022 16:05:07 +0100
Subject: [PATCH 3/3] [PATCH v2 3/3] Add documentation for new security_invoker
 reloption on views

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 doc/src/sgml/ref/alter_view.sgml  | 10 ++++++++
 doc/src/sgml/ref/create_view.sgml | 38 +++++++++++++++++++++++++------
 2 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..cb9df185e2 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -161,6 +161,16 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the security-invoker property of the view.  The value must
+          be Boolean value, such as <literal>true</literal>
+          or <literal>false</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..0507551c2d 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -152,6 +152,23 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set, it will cause all access to the underlying
+          tables to be checked as referenced by the invoking user, rather than
+          the view owner.  This 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>
@@ -265,13 +282,20 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
-    a view must have permissions to call all functions used by the view.
+    By default, access to tables referenced in the view is determined by
+    permissions of the view owner.  In some cases, this can be used to provide
+    secure but restricted access to the underlying tables.  However, not all
+    views are secure against tampering; see <xref linkend="rules-privileges"/>
+    for details.  Functions called in the view are treated the same as if they
+    had been called directly from the query using the view.  Therefore the user
+    of a view must have permissions to call all functions used by the view.
+   </para>
+
+   <para>
+    If the <firstterm>security_invoker</firstterm> option is set on the view,
+    access to tables is determined by permissions of the invoking user, rather
+    than the view owner.  This can be used to provide stricter permission
+    checking to the underlying tables than by default.
    </para>
 
    <para>
-- 
2.34.1

#4Julien Rouhaud
rjuju123@gmail.com
In reply to: Christoph Heiss (#3)
Re: [PATCH] Add reloption for views to enable RLS

Hi,

On Tue, Jan 18, 2022 at 04:16:53PM +0100, Christoph Heiss wrote:

I've attached a v2 where I addressed the things you mentioned.

This version unfortunately doesn't apply anymore:
http://cfbot.cputube.org/patch_36_3466.log
=== Applying patches on top of PostgreSQL commit ID e0e567a106726f6709601ee7cffe73eb6da8084e ===
=== applying patch ./0001-PATCH-v2-1-3-Add-new-boolean-reloption-security_invo.patch
=== applying patch ./0002-PATCH-v2-2-3-Add-regression-tests-for-new-security_i.patch
patching file src/test/regress/expected/create_view.out
Hunk #5 FAILED at 2019.
Hunk #6 succeeded at 2056 (offset 16 lines).
1 out of 6 hunks FAILED -- saving rejects to file src/test/regress/expected/create_view.out.rej

Could you send a rebased version?

#5Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Julien Rouhaud (#4)
3 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

Hi,

On 1/19/22 09:30, Julien Rouhaud wrote:

Hi,

On Tue, Jan 18, 2022 at 04:16:53PM +0100, Christoph Heiss wrote:

I've attached a v2 where I addressed the things you mentioned.

This version unfortunately doesn't apply anymore:
http://cfbot.cputube.org/patch_36_3466.log
=== Applying patches on top of PostgreSQL commit ID e0e567a106726f6709601ee7cffe73eb6da8084e ===
=== applying patch ./0001-PATCH-v2-1-3-Add-new-boolean-reloption-security_invo.patch
=== applying patch ./0002-PATCH-v2-2-3-Add-regression-tests-for-new-security_i.patch
patching file src/test/regress/expected/create_view.out
Hunk #5 FAILED at 2019.
Hunk #6 succeeded at 2056 (offset 16 lines).
1 out of 6 hunks FAILED -- saving rejects to file src/test/regress/expected/create_view.out.rej

Could you send a rebased version?

My bad - I attached a new version rebased on latest master.

Thanks,
Christoph Heiss

Attachments:

0001-PATCH-v3-1-3-Add-new-boolean-reloption-security_invo.patchtext/x-patch; charset=UTF-8; name=0001-PATCH-v3-1-3-Add-new-boolean-reloption-security_invo.patchDownload
From b1b5a6c6bd63c509b3bcdef6d1e7a548413c2a9d Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 18 Jan 2022 15:42:58 +0100
Subject: [PATCH 1/3] [PATCH v3 1/3] Add new boolean reloption security_invoker
 to views

When this reloption is set to true, all references to the underlying tables will
be checked against the invoking user rather than the view owner, as is currently
implemented.
Row level security must be enabled on the tables for this to take effect.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 src/backend/access/common/reloptions.c    | 11 ++++
 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        | 63 +++++++++++++----------
 src/include/nodes/parsenodes.h            |  1 +
 src/include/utils/rel.h                   | 11 ++++
 11 files changed, 65 insertions(+), 28 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..beb170b5e6 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"security_invoker",
+			"View subquery in invoked within the current security context.",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 90b5da51c9..b171992ba8 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2465,6 +2465,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_NODE_FIELD(tablesample);
 	COPY_NODE_FIELD(subquery);
 	COPY_SCALAR_FIELD(security_barrier);
+	COPY_SCALAR_FIELD(security_invoker);
 	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 06345da3ba..a832c5fefe 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_invoker);
 	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 2b0236937a..883284ad0d 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3261,6 +3261,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 		case RTE_SUBQUERY:
 			WRITE_NODE_FIELD(subquery);
 			WRITE_BOOL_FIELD(security_barrier);
+			WRITE_BOOL_FIELD(security_invoker);
 			break;
 		case RTE_JOIN:
 			WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3f68f7c18d..ad825bb27f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1444,6 +1444,7 @@ _readRangeTblEntry(void)
 		case RTE_SUBQUERY:
 			READ_NODE_FIELD(subquery);
 			READ_BOOL_FIELD(security_barrier);
+			READ_BOOL_FIELD(security_invoker);
 			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 41bd1ae7d4..30c66b9c6d 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_invoker = 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 282589dec8..bd7dc1c348 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_invoker = 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 3d82138cb3..0dcdbe2968 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_invoker = RelationHasSecurityInvoker(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 2e760e8a3b..9ae03e3e8d 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 that has the security_invoker relopt
+		 * set to true, 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,12 @@ 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
+			  && RelationHasSecurityInvoker(relation)))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1170,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 +1201,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 3e9bdc781f..1362f5d111 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1042,6 +1042,7 @@ typedef struct RangeTblEntry
 	 */
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
+	bool		security_invoker;	/* from a view with security_invoker set? */
 
 	/*
 	 * Fields valid for a join RTE (else NULL/zero):
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 6da1b220cd..9a8866c91d 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityRelationPermissions
+ *		Returns true if the relation has the security invoker property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
-- 
2.34.1

0002-PATCH-v3-2-3-Add-regression-tests-for-new-security_i.patchtext/x-patch; charset=UTF-8; name=0002-PATCH-v3-2-3-Add-regression-tests-for-new-security_i.patchDownload
From a0c221a3e601e303d860fbacd5b4f02474d4093d Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 18 Jan 2022 15:34:13 +0100
Subject: [PATCH 2/3] [PATCH v3 2/3] Add regression tests for new
 security_invoker reloption on views

This expands on the current regressions tests for CREATE VIEW and
ROW LEVEL SECURITY-related matters.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 src/test/regress/expected/create_view.out | 42 ++++++++++++++++++----
 src/test/regress/expected/rowsecurity.out | 43 ++++++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 26 +++++++++++---
 src/test/regress/sql/rowsecurity.sql      | 26 ++++++++++++++
 4 files changed, 125 insertions(+), 12 deletions(-)

diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index 509e930fc7..fea893569f 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -261,15 +261,26 @@ 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_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview6 WITH (security_invoker=false)
        AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview7 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview8 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 mysecview9 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a = 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
+CREATE VIEW mysecview10 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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
   relname   | relkind |        reloptions        
 ------------+---------+--------------------------
@@ -277,7 +288,10 @@ SELECT relname, relkind, reloptions FROM pg_class
  mysecview2 | v       | {security_barrier=true}
  mysecview3 | v       | {security_barrier=false}
  mysecview4 | v       | {security_barrier=true}
-(4 rows)
+ mysecview5 | v       | {security_invoker=true}
+ mysecview6 | v       | {security_invoker=false}
+ mysecview7 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -287,9 +301,17 @@ 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;
+CREATE OR REPLACE VIEW mysecview6 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview7 WITH (security_invoker=false)
+       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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
   relname   | relkind |        reloptions        
 ------------+---------+--------------------------
@@ -297,7 +319,10 @@ SELECT relname, relkind, reloptions FROM pg_class
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview5 | v       | 
+ mysecview6 | v       | {security_invoker=true}
+ mysecview7 | v       | {security_invoker=false}
+(7 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -2010,7 +2035,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 74 other objects
+NOTICE:  drop cascades to 77 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2031,6 +2056,9 @@ 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 mysecview6
+drop cascades to view mysecview7
 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..d10ecaa272 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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 security_invoker_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 82df4b7cac..290bf59c32 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -214,13 +214,23 @@ 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_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview6 WITH (security_invoker=false)
        AS SELECT * FROM tbl1 WHERE a > 100;
-CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
+CREATE VIEW mysecview7 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview8 WITH (security_barrier=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+CREATE VIEW mysecview9 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview10 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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -231,9 +241,17 @@ 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;
+CREATE OR REPLACE VIEW mysecview6 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview7 WITH (security_invoker=false)
+       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, 'mysecview6'::regclass,
+                     'mysecview7'::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..239a15e3d1 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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

0003-PATCH-v3-3-3-Add-documentation-for-new-security_invo.patchtext/x-patch; charset=UTF-8; name=0003-PATCH-v3-3-3-Add-documentation-for-new-security_invo.patchDownload
From f4dd3a68c740ecdb09a97c3ef4ddc3925b85fe85 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 18 Jan 2022 16:05:07 +0100
Subject: [PATCH 3/3] [PATCH v3 3/3] Add documentation for new security_invoker
 reloption on views

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 doc/src/sgml/ref/alter_view.sgml  | 10 ++++++++
 doc/src/sgml/ref/create_view.sgml | 38 +++++++++++++++++++++++++------
 2 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..cb9df185e2 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -161,6 +161,16 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the security-invoker property of the view.  The value must
+          be Boolean value, such as <literal>true</literal>
+          or <literal>false</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..0507551c2d 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -152,6 +152,23 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set, it will cause all access to the underlying
+          tables to be checked as referenced by the invoking user, rather than
+          the view owner.  This 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>
@@ -265,13 +282,20 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
-    a view must have permissions to call all functions used by the view.
+    By default, access to tables referenced in the view is determined by
+    permissions of the view owner.  In some cases, this can be used to provide
+    secure but restricted access to the underlying tables.  However, not all
+    views are secure against tampering; see <xref linkend="rules-privileges"/>
+    for details.  Functions called in the view are treated the same as if they
+    had been called directly from the query using the view.  Therefore the user
+    of a view must have permissions to call all functions used by the view.
+   </para>
+
+   <para>
+    If the <firstterm>security_invoker</firstterm> option is set on the view,
+    access to tables is determined by permissions of the invoking user, rather
+    than the view owner.  This can be used to provide stricter permission
+    checking to the underlying tables than by default.
    </para>
 
    <para>
-- 
2.34.1

#6Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Christoph Heiss (#3)
Re: [PATCH] Add reloption for views to enable RLS

On Tue, 2022-01-18 at 16:16 +0100, Christoph Heiss wrote:

I think that this should be a boolean reloption, for example "security_definer".
If unset or set to "off", you would get the current behavior.

A boolean option would have been indeed the better choice, I agree.
I haven't though of any specific other values for this enum, it was
rather a decision following a off-list discussion.

I've changed the option to be boolean and renamed it to
"security_invoker". This puts it in line with how other systems (e.g.
MySQL) name their equivalent feature, so I think this should be an
appropriate choice.

Also, I don't think that the documentation on
RLS policies is the correct place for this.  It should be on a page dedicated to views
or permissions.

The CREATE VIEW page already has a paragraph about this, starting with
"Access to tables referenced in the view is determined by permissions of the view owner."
This looks like the best place to me (and it would need to be adapted anyway).

It makes sense to put it there, thanks for the pointer! I wasn't really
that sure where to put the documentation to start with, and this seems
like a more appropriate place.

I gave the new patch a spin, and got a surprising result:

CREATE TABLE tab (id integer);

CREATE ROLE duff LOGIN;

CREATE ROLE jock LOGIN;

GRANT INSERT, UPDATE, DELETE ON tab TO jock;

GRANT SELECT ON tab TO duff;

CREATE VIEW v WITH (security_invoker = TRUE) AS SELECT * FROM tab;

ALTER VIEW v OWNER TO jock;

GRANT SELECT, INSERT, UPDATE, DELETE ON v TO duff;

SET SESSION AUTHORIZATION duff;

SELECT * FROM v;
id
════
(0 rows)

That's ok, "duff" has permissions to read "tab".

INSERT INTO v VALUES (1);
INSERT 0 1

Huh? "duff" has no permission to insert into "tab"!

RESET SESSION AUTHORIZATION;

ALTER VIEW v SET (security_invoker = FALSE);

SET SESSION AUTHORIZATION duff;

SELECT * FROM v;
ERROR: permission denied for table tab

As expected.

INSERT INTO v VALUES (1);
INSERT 0 1

As expected.

About the documentation:

--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set, it will cause all access to the underlying
+          tables to be checked as referenced by the invoking user, rather than
+          the view owner.  This 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>

Why should this *only* take effect if (not "when") RLS is enabled?
The above test shows that there is an effect even without RLS.

+         <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>

I don't think that it is necessary to mention that this can be changed with
ALTER VIEW - all storage parameters can be. I guess you copied that from
the "check_option" documentation, but I would say it need not be mentioned
there either.

+   <para>
+    If the <firstterm>security_invoker</firstterm> option is set on the view,
+    access to tables is determined by permissions of the invoking user, rather
+    than the view owner.  This can be used to provide stricter permission
+    checking to the underlying tables than by default.
    </para>

Since you are talking about use cases here, RLS might deserve a mention.

--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
+   {
+       {
+           "security_invoker",
+           "View subquery in invoked within the current security context.",
+           RELOPT_KIND_VIEW,
+           AccessExclusiveLock
+       },
+       false
+   },

That doesn't seem to be proper English.

Yours,
Laurenz Albe

#7Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Laurenz Albe (#6)
3 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

Hi Laurenz,

thank you again for the review!

On 1/20/22 15:20, Laurenz Albe wrote:

[..]
I gave the new patch a spin, and got a surprising result:

[..]

INSERT INTO v VALUES (1);
INSERT 0 1

Huh? "duff" has no permission to insert into "tab"!

That really should not happen, thanks for finding that and helping me
investigating on how to fix that!

This is now solved by checking the security_invoker property on the view
in rewriteTargetView().

I've also added a testcase for this in v4 to catch that in future.

[..]

About the documentation:

--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set, it will cause all access to the underlying
+          tables to be checked as referenced by the invoking user, rather than
+          the view owner.  This 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>

Why should this *only* take effect if (not "when") RLS is enabled?
The above test shows that there is an effect even without RLS.

+         <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>

I don't think that it is necessary to mention that this can be changed with
ALTER VIEW - all storage parameters can be. I guess you copied that from
the "check_option" documentation, but I would say it need not be mentioned
there either.

Exactly, I tried to fit it in with the existing parameters.
I moved the link to ALTER VIEW to the end of the paragraph, as it
applies to all options anyways.

+   <para>
+    If the <firstterm>security_invoker</firstterm> option is set on the view,
+    access to tables is determined by permissions of the invoking user, rather
+    than the view owner.  This can be used to provide stricter permission
+    checking to the underlying tables than by default.
</para>

Since you are talking about use cases here, RLS might deserve a mention.

Expanded upon a little bit in v4.

--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
+   {
+       {
+           "security_invoker",
+           "View subquery in invoked within the current security context.",
+           RELOPT_KIND_VIEW,
+           AccessExclusiveLock
+       },
+       false
+   },

That doesn't seem to be proper English.

Yes, that happened when rewriting this for v1 -> v2.
Fixed.

Thanks,
Christoph Heiss

Attachments:

v4-0001-Add-new-boolean-reloption-security_invoker-to-vie.patchtext/x-patch; charset=UTF-8; name=v4-0001-Add-new-boolean-reloption-security_invoker-to-vie.patchDownload
From 01437a45bfd069080ffe0eb45288bfddd3de6009 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Wed, 2 Feb 2022 16:44:38 +0100
Subject: [PATCH v4 1/3] Add new boolean reloption security_invoker to views

When this reloption is set to true, all references to the underlying tables will
be checked against the invoking user rather than the view owner, as is currently
implemented.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 src/backend/access/common/reloptions.c    | 11 ++++
 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      | 17 ++++--
 src/backend/utils/cache/relcache.c        | 63 +++++++++++++----------
 src/include/nodes/parsenodes.h            |  1 +
 src/include/utils/rel.h                   | 11 ++++
 11 files changed, 77 insertions(+), 32 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..c7c62a0076 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"security_invoker",
+			"Check permissions against underlying tables as the calling user, not as view owner",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 90b5da51c9..b171992ba8 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2465,6 +2465,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_NODE_FIELD(tablesample);
 	COPY_NODE_FIELD(subquery);
 	COPY_SCALAR_FIELD(security_barrier);
+	COPY_SCALAR_FIELD(security_invoker);
 	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 06345da3ba..a832c5fefe 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_invoker);
 	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 2b0236937a..883284ad0d 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3261,6 +3261,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 		case RTE_SUBQUERY:
 			WRITE_NODE_FIELD(subquery);
 			WRITE_BOOL_FIELD(security_barrier);
+			WRITE_BOOL_FIELD(security_invoker);
 			break;
 		case RTE_JOIN:
 			WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3f68f7c18d..ad825bb27f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1444,6 +1444,7 @@ _readRangeTblEntry(void)
 		case RTE_SUBQUERY:
 			READ_NODE_FIELD(subquery);
 			READ_BOOL_FIELD(security_barrier);
+			READ_BOOL_FIELD(security_invoker);
 			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 41bd1ae7d4..30c66b9c6d 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_invoker = 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 282589dec8..bd7dc1c348 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_invoker = 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 3d82138cb3..22a7dbb0a5 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_invoker = RelationHasSecurityInvoker(relation);
 	/* Clear fields that should not be set in a subquery RTE */
 	rte->relid = InvalidOid;
 	rte->relkind = 0;
@@ -3242,9 +3243,13 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
+	 * If the view has security_invoker set, mark the new target RTE for the
+	 * permissions checks that we want to enforce against the query caller, as
+	 * distince from the view owner.
+	 * In all other cases, we want to enforce them against the view owner,
+	 * not the query caller.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE permissions
 	 * that the query caller needs against the view.  We drop the ACL_SELECT
 	 * bit that is presumably in new_rte->requiredPerms initially.
 	 *
@@ -3253,7 +3258,11 @@ rewriteTargetView(Query *parsetree, Relation view)
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationHasSecurityInvoker(view))
+		new_rte->checkAsUser = view_rte->checkAsUser;
+	else
+		new_rte->checkAsUser = view->rd_rel->relowner;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2e760e8a3b..9ae03e3e8d 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 that has the security_invoker relopt
+		 * set to true, 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,12 @@ 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
+			  && RelationHasSecurityInvoker(relation)))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1170,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 +1201,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 3e9bdc781f..1362f5d111 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1042,6 +1042,7 @@ typedef struct RangeTblEntry
 	 */
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
+	bool		security_invoker;	/* from a view with security_invoker set? */
 
 	/*
 	 * Fields valid for a join RTE (else NULL/zero):
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 6da1b220cd..9a8866c91d 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityRelationPermissions
+ *		Returns true if the relation has the security invoker property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
-- 
2.35.1

v4-0002-Add-regression-tests-for-new-security_invoker-rel.patchtext/x-patch; charset=UTF-8; name=v4-0002-Add-regression-tests-for-new-security_invoker-rel.patchDownload
From b12d83248047ea4e5b01c07392f2ffbc1347b41c Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Wed, 2 Feb 2022 17:33:00 +0100
Subject: [PATCH v4 2/3] Add regression tests for new security_invoker
 reloption on views

This expands on the current regressions tests for CREATE VIEW and
ROW LEVEL SECURITY-related matters.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 src/test/regress/expected/create_view.out | 42 ++++++++++++---
 src/test/regress/expected/rowsecurity.out | 65 ++++++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 26 +++++++--
 src/test/regress/sql/rowsecurity.sql      | 44 +++++++++++++++
 4 files changed, 165 insertions(+), 12 deletions(-)

diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index 509e930fc7..fea893569f 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -261,15 +261,26 @@ 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_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview6 WITH (security_invoker=false)
        AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview7 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview8 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 mysecview9 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a = 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
+CREATE VIEW mysecview10 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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
   relname   | relkind |        reloptions        
 ------------+---------+--------------------------
@@ -277,7 +288,10 @@ SELECT relname, relkind, reloptions FROM pg_class
  mysecview2 | v       | {security_barrier=true}
  mysecview3 | v       | {security_barrier=false}
  mysecview4 | v       | {security_barrier=true}
-(4 rows)
+ mysecview5 | v       | {security_invoker=true}
+ mysecview6 | v       | {security_invoker=false}
+ mysecview7 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -287,9 +301,17 @@ 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;
+CREATE OR REPLACE VIEW mysecview6 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview7 WITH (security_invoker=false)
+       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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
   relname   | relkind |        reloptions        
 ------------+---------+--------------------------
@@ -297,7 +319,10 @@ SELECT relname, relkind, reloptions FROM pg_class
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview5 | v       | 
+ mysecview6 | v       | {security_invoker=true}
+ mysecview7 | v       | {security_invoker=false}
+(7 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -2010,7 +2035,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 74 other objects
+NOTICE:  drop cascades to 77 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2031,6 +2056,9 @@ 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 mysecview6
+drop cascades to view mysecview7
 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..856c25c085 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,59 @@ SELECT * FROM category;
   44 | manga
 (4 rows)
 
+-- Test views with security_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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)
+
+RESET SESSION AUTHORIZATION;
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (security_invoker=true) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM v1t;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1t values (1);
+ERROR:  permission denied for table sivt1
+UPDATE v1t SET x = 2;
+ERROR:  permission denied for table sivt1
+DELETE FROM v1t;
+ERROR:  permission denied for table sivt1
 --
 -- Table inheritance and RLS policy
 --
@@ -3987,11 +4045,16 @@ RESET SESSION AUTHORIZATION;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 29 other objects
+NOTICE:  drop cascades to 34 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 security_invoker_func()
+drop cascades to view v1f
+drop cascades to table sivt1
+drop cascades to view v1t
 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 82df4b7cac..290bf59c32 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -214,13 +214,23 @@ 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_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview6 WITH (security_invoker=false)
        AS SELECT * FROM tbl1 WHERE a > 100;
-CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
+CREATE VIEW mysecview7 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview8 WITH (security_barrier=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+CREATE VIEW mysecview9 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview10 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, 'mysecview6'::regclass,
+                     'mysecview7'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -231,9 +241,17 @@ 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;
+CREATE OR REPLACE VIEW mysecview6 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview7 WITH (security_invoker=false)
+       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, 'mysecview6'::regclass,
+                     'mysecview7'::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..fc005af98f 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,45 @@ SET row_security TO OFF;
 SELECT * FROM document;
 SELECT * FROM category;
 
+-- Test views with security_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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;
+
+RESET SESSION AUTHORIZATION;
+
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (security_invoker=true) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+
+SELECT * FROM v1t;
+INSERT INTO v1t values (1);
+UPDATE v1t SET x = 2;
+DELETE FROM v1t;
+
 --
 -- Table inheritance and RLS policy
 --
-- 
2.35.1

v4-0003-Add-documentation-for-new-security_invoker-relopt.patchtext/x-patch; charset=UTF-8; name=v4-0003-Add-documentation-for-new-security_invoker-relopt.patchDownload
From 8f04c782b525315a5524dc67798f43465f7ef0c0 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Wed, 2 Feb 2022 18:11:47 +0100
Subject: [PATCH v4 3/3] Add documentation for new security_invoker reloption
 on views

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 doc/src/sgml/ref/alter_view.sgml  | 10 +++++++
 doc/src/sgml/ref/create_view.sgml | 43 ++++++++++++++++++++++++-------
 2 files changed, 43 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..cb9df185e2 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -161,6 +161,16 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the security-invoker property of the view.  The value must
+          be Boolean value, such as <literal>true</literal>
+          or <literal>false</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..33d4fde6fc 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,22 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set, it will cause all access to the underlying
+          tables to be checked as referenced by the invoking user, rather than
+          the view owner.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,13 +278,23 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
-    a view must have permissions to call all functions used by the view.
+    By default, access to tables referenced in the view is determined by
+    permissions of the view owner.  In some cases, this can be used to provide
+    secure but restricted access to the underlying tables.  However, not all
+    views are secure against tampering; see <xref linkend="rules-privileges"/>
+    for details.  Functions called in the view are treated the same as if they
+    had been called directly from the query using the view.  Therefore the user
+    of a view must have permissions to call all functions used by the view.
+   </para>
+
+   <para>
+    If the <firstterm>security_invoker</firstterm> option is set on the view,
+    access to tables is determined by permissions of the invoking user, rather
+    than the view owner.  This can be used to provide stricter permission
+    checking to the underlying tables than by default.  Policies defined on the
+    underlying tables are then also checked against the invoking user when
+    <link linkend="ddl-rowsecurity">row-level security</link> is enabled on
+    these tables.
    </para>
 
    <para>
-- 
2.35.1

#8Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Christoph Heiss (#7)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

On Wed, 2022-02-02 at 18:23 +0100, Christoph Heiss wrote:

Huh?  "duff" has no permission to insert into "tab"!

That really should not happen, thanks for finding that and helping me
investigating on how to fix that!

This is now solved by checking the security_invoker property on the view
in rewriteTargetView().

I've also added a testcase for this in v4 to catch that in future.

I tested it, and the patch works fine now.

Some little comments:

--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3242,9 +3243,13 @@ rewriteTargetView(Query *parsetree, Relation view)
0);
/*
-    * Mark the new target RTE for the permissions checks that we want to
-    * enforce against the view owner, as distinct from the query caller.  At
-    * the relation level, require the same INSERT/UPDATE/DELETE permissions
+    * If the view has security_invoker set, mark the new target RTE for the
+    * permissions checks that we want to enforce against the query caller, as
+    * distince from the view owner.

Typo: distince

diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index 509e930fc7..fea893569f 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -261,15 +261,26 @@ 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_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview6 WITH (security_invoker=false)
        AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview7 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview8 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 mysecview9 WITH (security_invoker=100) -- Error
+       AS SELECT * FROM tbl1 WHERE a = 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
+CREATE VIEW mysecview10 WITH (invalid_option)      -- Error

I see no reasons to remove two of the existing tests.

+++ 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;

But the name has to start with "e"!

I also see no reason to split a small patch like this into three parts.

In the attached, I dealt with the above and went over the comments.
How do you like it?

Yours,
Laurenz Albe

Show quoted text

Attachments:

v5-0001-Add-new-boolean-reloption-security_invoker-to-views.patchtext/x-patch; charset=UTF-8; name=v5-0001-Add-new-boolean-reloption-security_invoker-to-views.patchDownload
From 46130acdc2649718fe81e289ef86ac8bf3eae124 Mon Sep 17 00:00:00 2001
From: Laurenz Albe <laurenz.albe@cybertec.at>
Date: Fri, 4 Feb 2022 16:58:58 +0100
Subject: [PATCH] Add new boolean reloption "security_invoker" to views

When this reloption is set to true, all permissions on the underlying
objects will be checked against the invoking user rather than the view
owner, as is currently implemented.

Author: Christoph Heiss <christoph.heiss@cybertec.at>
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
---
 doc/src/sgml/ref/alter_view.sgml          | 12 ++++-
 doc/src/sgml/ref/create_view.sgml         | 32 +++++++++--
 src/backend/access/common/reloptions.c    | 11 ++++
 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      | 20 ++++---
 src/backend/utils/cache/relcache.c        | 63 ++++++++++++----------
 src/include/nodes/parsenodes.h            |  1 +
 src/include/utils/rel.h                   | 11 ++++
 src/test/regress/expected/create_view.out | 46 ++++++++++++----
 src/test/regress/expected/rowsecurity.out | 65 ++++++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 22 +++++++-
 src/test/regress/sql/rowsecurity.sql      | 44 +++++++++++++++
 17 files changed, 282 insertions(+), 51 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..8bdc90a5a1 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,7 +156,17 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
+          or <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the security-invoker property of the view.  The value must
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index bf03287592..d0561e6253 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,22 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set, it will cause all access to the underlying
+          tables to be checked as referenced by the invoking user, rather than
+          the view owner.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,7 +278,8 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
+    By default, access to tables, functions and other objects referenced in
+    the view is determined by permissions of
     the view owner.  In some cases, this can be used to provide secure but
     restricted access to the underlying tables.  However, not all views are
     secure against tampering; see <xref linkend="rules-privileges"/> for
@@ -274,6 +288,16 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
     a view must have permissions to call all functions used by the view.
    </para>
 
+   <para>
+    If the <literal>security_invoker</literal> option is set on the view,
+    access to tables, functions and other objects referenced in 
+    the view is determined by permissions of the invoking user, rather
+    than the view owner.  If <link linkend="ddl-rowsecurity">row-level
+    security</link> is enabled on the referenced tables, policies are also
+    invoked for the invoking user.  This is useful if you want the view
+    to behave just as if the defining query had been used instead.
+   </para>
+
    <para>
     When <command>CREATE OR REPLACE VIEW</command> is used on an
     existing view, only the view's defining SELECT rule is changed.
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..083b70a1b9 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"security_invoker",
+			"Permissions on the underlying objects are checked for the calling user, not the view owner",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 6bd95bbce2..8aaa44fcc7 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2465,6 +2465,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_NODE_FIELD(tablesample);
 	COPY_NODE_FIELD(subquery);
 	COPY_SCALAR_FIELD(security_barrier);
+	COPY_SCALAR_FIELD(security_invoker);
 	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 4126516222..d0e2e2784a 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2768,6 +2768,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_invoker);
 	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 6bdad462c7..1beeb202ad 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3262,6 +3262,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 		case RTE_SUBQUERY:
 			WRITE_NODE_FIELD(subquery);
 			WRITE_BOOL_FIELD(security_barrier);
+			WRITE_BOOL_FIELD(security_invoker);
 			break;
 		case RTE_JOIN:
 			WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3f68f7c18d..ad825bb27f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1444,6 +1444,7 @@ _readRangeTblEntry(void)
 		case RTE_SUBQUERY:
 			READ_NODE_FIELD(subquery);
 			READ_BOOL_FIELD(security_barrier);
+			READ_BOOL_FIELD(security_invoker);
 			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 41bd1ae7d4..30c66b9c6d 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_invoker = 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 282589dec8..bd7dc1c348 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_invoker = 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 3d82138cb3..4674fc4171 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_invoker = RelationHasSecurityInvoker(relation);
 	/* Clear fields that should not be set in a subquery RTE */
 	rte->relid = InvalidOid;
 	rte->relkind = 0;
@@ -3242,18 +3243,25 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "security_invoker" set, mark the new target RTE for the
+	 * permissions checks that we want to enforce against the query caller.
+	 * Otherwise, we want to enforce them against the view owner, not the
+	 * query caller.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationHasSecurityInvoker(view))
+		new_rte->checkAsUser = view_rte->checkAsUser;
+	else
+		new_rte->checkAsUser = view->rd_rel->relowner;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2707fed12f..0f10c964a3 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 that has the "security_invoker" relopt
+		 * set to true, we want the rule's table references to be checked as
+		 * the user invoking 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,12 @@ 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
+			  && RelationHasSecurityInvoker(relation)))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1170,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 +1201,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 37fcc4c9b5..3e04eba0d9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1042,6 +1042,7 @@ typedef struct RangeTblEntry
 	 */
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
+	bool		security_invoker;	/* from a view with security_invoker set? */
 
 	/*
 	 * Fields valid for a join RTE (else NULL/zero):
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 6da1b220cd..9a8866c91d 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityRelationPermissions
+ *		Returns true if the relation has the security invoker property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index 509e930fc7..611475f1c2 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -267,17 +267,31 @@ ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {security_invoker=true}
+ mysecview8 | v       | {security_invoker=false,security_barrier=true}
+ mysecview9 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -287,17 +301,28 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                   reloptions                   
+------------+---------+------------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview7 | v       | 
+ mysecview8 | v       | {security_invoker=true}
+ mysecview9 | v       | {security_invoker=false,security_barrier=true}
+(7 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -2010,7 +2035,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 74 other objects
+NOTICE:  drop cascades to 77 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2031,6 +2056,9 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
 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..070dc2cf0c 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_emily;
 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_emily 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_emily;
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
 SET search_path = regress_rls_schema;
@@ -627,6 +632,59 @@ SELECT * FROM category;
   44 | manga
 (4 rows)
 
+-- Test views with security_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+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)
+
+RESET SESSION AUTHORIZATION;
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (security_invoker=true) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM v1t;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1t values (1);
+ERROR:  permission denied for table sivt1
+UPDATE v1t SET x = 2;
+ERROR:  permission denied for table sivt1
+DELETE FROM v1t;
+ERROR:  permission denied for table sivt1
 --
 -- Table inheritance and RLS policy
 --
@@ -3987,11 +4045,16 @@ RESET SESSION AUTHORIZATION;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 29 other objects
+NOTICE:  drop cascades to 34 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 security_invoker_func()
+drop cascades to view v1f
+drop cascades to table sivt1
+drop cascades to view v1t
 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 82df4b7cac..5b2fcf1cb5 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -218,9 +218,19 @@ CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -231,9 +241,17 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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..f1a632b22f 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_emily;
 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_emily 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_emily;
 
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
@@ -225,6 +230,45 @@ SET row_security TO OFF;
 SELECT * FROM document;
 SELECT * FROM category;
 
+-- Test views with security_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
+RESET SESSION AUTHORIZATION;
+
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (security_invoker=true) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+
+SELECT * FROM v1t;
+INSERT INTO v1t values (1);
+UPDATE v1t SET x = 2;
+DELETE FROM v1t;
+
 --
 -- Table inheritance and RLS policy
 --
-- 
2.34.1

#9Noname
walther@technowledgy.de
In reply to: Laurenz Albe (#8)
Re: [PATCH] Add reloption for views to enable RLS

Christoph Heiss wrote:

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.

This is a feature I have long been looking for. I tested the patch (v5)
and found two cases that I feel need to be either fixed or documented
explicitly.

Case 1 - Schema privileges:

create schema a;
create table a.t();

create schema b;
create view b.v with (security_invoker=true) as table a.t;

create role alice;
grant usage on schema b to alice; -- missing schema a
grant select on table a.t, b.v to alice;

set role alice;
table a.t; -- ERROR: permission denied for schema a (good)
table b.v; -- no error (good or bad?)

User alice does not have USAGE privileges on schema a, but only on table
a.t. A SELECT directly on the table fails as expected, but a SELECT on
the view succeeds. I assume the schema access is checked when the query
is parsed - and at that stage, the user is still the view owner?
The docs mention explicitly that *all* objects are accessed with invoker
privileges, which is not the case.

Personally I actually like this. It allows to keep a view-based api in a
separate schema, while:
- preserving full RLS capabilities and
- forcing the user to go through the api, because a direct access to the
data schema is not possible.

However, since this behavior was likely unintended until now, it raises
the question whether there are any other privilege checks that are not
taking the invoking user into account properly?

Case 2 - Chained views:

create schema a;
create table a.t();

create role bob;
grant create on database postgres to bob;
grant usage on schema a to bob;
set role bob;
create schema b;
create view b.v1 with (security_invoker=true) as table a.t;
create view b.v2 with (security_invoker=false) as table b.v1;

reset role;
create role alice;
grant usage on schema a, b to alice;
grant select on table a.t to bob;
grant select on table b.v2 to alice;

set role alice;
table b.v2; -- ERROR: permission denied for table t (bad)

When alice runs the SELECT on b.v2, the query on b.v1 is made with bob
privileges as the view owner of b.v2. This is verified, because alice
does not have privileges to access b.v1, but no such error is thrown.

b.v1 will then access a.t - and my first assumption was, that in this
case a.t should be accessed by bob, still as the view owner of b.v2.
Clearly, this is not the case as the permission denied error shows.

This is not actually a problem with this patch, I think, but just
highlighting a quirk in the current implementation of views
(security_invoker=false) in general: While the query will be run with
the view owner, the CURRENT_USER is still the invoker, even "after" the
view. In other words, the current implementation is *not* the same as
"security definer". It's somewhere between "security definer" and
"security invoker" - a strange mix really.

Afaik this mix is not documented explicitly so far. But the
security_invoker reloption exposes it in a much less expected way, so I
only see two options really:
a) make the current implementation of security_invoker=false a true
"security definer", i.e. change the CURRENT_USER "after" the view for good.
b) document the "security infiner/devoker" default behavior as a feature.

I really like a), as this would make a clear cut between security
definer and security invoker views - but this would be a big breaking
change, which I don't think is acceptable.

Best,

Wolfgang

#10Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Noname (#9)
Re: [PATCH] Add reloption for views to enable RLS

On Fri, 2022-02-04 at 22:28 +0100, walther@technowledgy.de wrote:

This is a feature I have long been looking for. I tested the patch (v5)
and found two cases that I feel need to be either fixed or documented
explicitly.

Thanks for testing and weighing in!

Case 1 - Schema privileges:

create schema a;
create table a.t();

create schema b;
create view b.v with (security_invoker=true) as table a.t;

create role alice;
grant usage on schema b to alice; -- missing schema a
grant select on table a.t, b.v to alice;

set role alice;
table a.t; -- ERROR: permission denied for schema a (good)
table b.v; -- no error (good or bad?)

User alice does not have USAGE privileges on schema a, but only on table
a.t. A SELECT directly on the table fails as expected, but a SELECT on
the view succeeds. I assume the schema access is checked when the query
is parsed - and at that stage, the user is still the view owner?
The docs mention explicitly that *all* objects are accessed with invoker
privileges, which is not the case.

Personally I actually like this. It allows to keep a view-based api in a
separate schema, while:
- preserving full RLS capabilities and
- forcing the user to go through the api, because a direct access to the
data schema is not possible.

However, since this behavior was likely unintended until now, it raises
the question whether there are any other privilege checks that are not
taking the invoking user into account properly?

This behavior is not new:

CREATE SCHEMA viewtest;

CREATE ROLE duff LOGIN;
CREATE ROLE jock LOGIN;

CREATE TABLE viewtest.tab (id integer);
GRANT SELECT ON viewtest.tab TO duff;

CREATE VIEW v AS SELECT * FROM viewtest.tab;
ALTER VIEW v OWNER TO duff;
GRANT SELECT ON v TO jock;

SET ROLE jock;

SELECT * FROM v;
id
════
(0 rows)

So even though the view owner "duff" has no permissions
on the schema "viewtest", we can still select from the table.
Permissions on the schema containing the table are not
checked, only permissions on the table itself.

I am not sure how to feel about this. It is not what I would have
expected, but changing it would be a compatibility break.
Should this be considered a live bug in PostgreSQL?

If not, I don't know if it is the business of this patch to
change the behavior.

Case 2 - Chained views:

create schema a;
create table a.t();

create role bob;
grant create on database postgres to bob;
grant usage on schema a to bob;
set role bob;
create schema b;
create view b.v1 with (security_invoker=true) as table a.t;
create view b.v2 with (security_invoker=false) as table b.v1;

reset role;
create role alice;
grant usage on schema a, b to alice;
grant select on table a.t to bob;
grant select on table b.v2 to alice;

set role alice;
table b.v2; -- ERROR: permission denied for table t (bad)

When alice runs the SELECT on b.v2, the query on b.v1 is made with bob
privileges as the view owner of b.v2. This is verified, because alice
does not have privileges to access b.v1, but no such error is thrown.

b.v1 will then access a.t - and my first assumption was, that in this
case a.t should be accessed by bob, still as the view owner of b.v2.
Clearly, this is not the case as the permission denied error shows.

This is not actually a problem with this patch, I think, but just
highlighting a quirk in the current implementation of views
(security_invoker=false) in general: While the query will be run with
the view owner, the CURRENT_USER is still the invoker, even "after" the
view. In other words, the current implementation is *not* the same as
"security definer". It's somewhere between "security definer" and
"security invoker" - a strange mix really.

Right. Even though permissions on "v1" are checked for user "bob",
permissions on the table are checked for the current user, which remains
"alice".

I agree that the name "security_invoker" is suggestive of SECURITY INVOKER
in CREATE FUNCTION, but the behavior is different.
Perhaps the solution is as simple as choosing a different name that does
not prompt this association, for example "permissions_invoker".

Afaik this mix is not documented explicitly so far. But the
security_invoker reloption exposes it in a much less expected way, so I
only see two options really:
a) make the current implementation of security_invoker=false a true
"security definer", i.e. change the CURRENT_USER "after" the view for good.
b) document the "security infiner/devoker" default behavior as a feature.

I really like a), as this would make a clear cut between security
definer and security invoker views - but this would be a big breaking
change, which I don't think is acceptable.

I agree that changing the current behavior is not acceptable.

I guess more documentation how this works would be a good idea.
Not sure if this is the job of this patch, but since it exposes this
in new ways, it might as well clarify how all this works.

Yours,
Laurenz Albe

#11Noname
walther@technowledgy.de
In reply to: Laurenz Albe (#10)
Re: [PATCH] Add reloption for views to enable RLS

Laurenz Albe:

So even though the view owner "duff" has no permissions
on the schema "viewtest", we can still select from the table.
Permissions on the schema containing the table are not
checked, only permissions on the table itself.

[...]

If not, I don't know if it is the business of this patch to
change the behavior.

Ah, good find. In that case, I suggest to change the docs slightly to
say that the schema will not be checked.

In one place it's described as "it will cause all access to the
underlying tables to be checked as ..." which is fine, I think. But in
another place it's "access to tables, functions and *other objects*
referenced in the view, ..." which is misleading.

I agree that the name "security_invoker" is suggestive of SECURITY INVOKER
in CREATE FUNCTION, but the behavior is different.
Perhaps the solution is as simple as choosing a different name that does
not prompt this association, for example "permissions_invoker".

Yes, given that there is not much that can be done about the
functionality anymore, a different name would be better. This would also
avoid the implicit "if security_invoker=false, the view behaves like
SECURITY DEFINER" association, which is also clearly wrong. And this
assumption is actually what made me think the chained views example was
somehow off.

I am not convinced "permissions_invoker" is much better, though. The
difference between SECURITY INVOKER and SECURITY DEFINER is invoker vs
definer... where, I think, we need something else to describe what we
currently have and what the patch provides.

Maybe we can look at it from the other perspective: Both ways of
operating keep the CURRENT_USER the same, pretty much like what we
understand "security invoker" should do. The difference, however, is the
current default in which the permissions are checked with the view
*owner*. Let's treat this difference as the thing that can be set:
security_owner=true|false. Or run_as_owner=true|false.

xxx_owner=true would be the default and xxx_owner=false could be set
explicitly to get the behavior we are looking for in this patch?

I guess more documentation how this works would be a good idea.
[...] but since it exposes this
in new ways, it might as well clarify how all this works.

+1

Best

Wolfgang

#12Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Noname (#11)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

Hi all,

again, many thanks for the reviews and testing!

On 2/4/22 17:09, Laurenz Albe wrote:

I also see no reason to split a small patch like this into three parts.

I've split it into the three unrelated parts (code, docs, tests) to ease
review, but I happily carry it as one patch too.

In the attached, I dealt with the above and went over the comments.
How do you like it?

That is really nice, I used it to base v6 on.

On 2/9/22 17:40, walther@technowledgy.de wrote:

Ah, good find. In that case, I suggest to change the docs slightly to
say that the schema will not be checked.

In one place it's described as "it will cause all access to the
underlying tables to be checked as ..." which is fine, I think. But in
another place it's "access to tables, functions and *other objects*
referenced in the view, ..." which is misleading

I removed the reference to "other objects" for now in v6.

I agree that the name "security_invoker" is suggestive of SECURITY
INVOKER
in CREATE FUNCTION, but the behavior is different.
Perhaps the solution is as simple as choosing a different name that does
not prompt this association, for example "permissions_invoker".

Yes, given that there is not much that can be done about the
functionality anymore, a different name would be better. This would also
avoid the implicit "if security_invoker=false, the view behaves like
SECURITY DEFINER" association, which is also clearly wrong. And this
assumption is actually what made me think the chained views example was
somehow off.

I am not convinced "permissions_invoker" is much better, though. The
difference between SECURITY INVOKER and SECURITY DEFINER is invoker vs
definer... where, I think, we need something else to describe what we
currently have and what the patch provides.

Maybe we can look at it from the other perspective: Both ways of
operating keep the CURRENT_USER the same, pretty much like what we
understand "security invoker" should do. The difference, however, is the
current default in which the permissions are checked with the view
*owner*. Let's treat this difference as the thing that can be set:
security_owner=true|false. Or run_as_owner=true|false.

xxx_owner=true would be the default and xxx_owner=false could be set
explicitly to get the behavior we are looking for in this patch?

I'm not sure if an option which is on by default would be best, IMHO. I
would rather have an off-by-default option, so that you explicitly have
to turn *on* that behavior rather than turning *off* the current.

[ Pretty much bike-shedding here, but if the agreement comes to one of
"xxx_owner" I won't mind it either. ]

My best suggestions is maybe something like run_as_invoker=t|f, but that
would probably raise the same "invoker vs definer" association.

I left it for now as-is.

I guess more documentation how this works would be a good idea.
[...] but since it exposes this
in new ways, it might as well clarify how all this works.

I tried to clarify this situation in the documentation in a concise
matter, I'd appreciate further feedback on that.

Thanks,
Christoph Heiss

Attachments:

v6-0001-Add-new-boolean-reloption-security_invoker-to-vie.patchtext/x-patch; charset=UTF-8; name=v6-0001-Add-new-boolean-reloption-security_invoker-to-vie.patchDownload
From 7d8f8e1cb27a3d22d049a5d820ddd66ec77a3297 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Mon, 14 Feb 2022 17:54:35 +0100
Subject: [PATCH v6 1/1] Add new boolean reloption "security_invoker" to views

When this reloption is set to true, all permissions on the underlying
objects will be checked against the invoking user rather than the view
owner, as is currently implemented.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
Co-Author: Laurenz Albe <laurenz.albe@cybertec.at>
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 doc/src/sgml/ref/alter_view.sgml          | 12 +++-
 doc/src/sgml/ref/create_view.sgml         | 73 ++++++++++++++++++-----
 src/backend/access/common/reloptions.c    | 11 ++++
 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      | 20 +++++--
 src/backend/utils/cache/relcache.c        | 63 ++++++++++---------
 src/include/nodes/parsenodes.h            |  1 +
 src/include/utils/rel.h                   | 11 ++++
 src/test/regress/expected/create_view.out | 46 +++++++++++---
 src/test/regress/expected/rowsecurity.out | 65 +++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 22 ++++++-
 src/test/regress/sql/rowsecurity.sql      | 44 ++++++++++++++
 17 files changed, 313 insertions(+), 61 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..8bdc90a5a1 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,7 +156,17 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
+          or <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the security-invoker property of the view.  The value must
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index bf03287592..f01ca98c3d 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,22 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set, it will cause all access to the underlying
+          tables to be checked as referenced by the invoking user, rather than
+          the view owner.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,13 +278,39 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
-    a view must have permissions to call all functions used by the view.
+    By default, access to tables and functions referenced in the view is
+    determined by permissions of the view owner.  In some cases, this can be
+    used to provide secure but restricted access to the underlying tables.
+    However, not all views are secure against tampering; see
+    <xref linkend="rules-privileges"/> for details.  Functions called in the
+    view are treated the same as if they had been called directly from the
+    query using the view.  Therefore the user of a view must have permissions
+    to call all functions used by the view.  This also means that functions
+    are executed as the invoking user, not the view owner.
+   </para>
+
+   <para>
+    However, when using chained views, the <literal>CURRENT_USER</literal> user
+    will always stay the invoking user, regardless of wethever the query is run
+    as the view owner (the default) or the invoking user (when the
+    <literal>security_invoker</literal> property is set) and the depth of the
+    current invocation.
+   </para>
+
+   <para>
+    If the <literal>security_invoker</literal> option is set on the view, access
+    to tables and functions referenced in the view is determined by permissions
+    of the invoking user, rather than the view owner.  If
+    <link linkend="ddl-rowsecurity">row-level security</link> is enabled on the
+    referenced tables, policies are also invoked for the invoking user.  This
+    is useful if you want the view to behave just as if the defining query had
+    been used instead.
+   </para>
+
+   <para>
+    Be aware that <literal>USAGE</literal> privileges on schemas are not checked
+    when referencing the underlying base relations, even if they are part of a
+    different schema.
    </para>
 
    <para>
@@ -387,10 +426,16 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    <para>
     Note that the user performing the insert, update or delete on the view
     must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    view.
+   </para>
+
+   <para>
+    Additionally, by default the view's owner must have the relevant privileges
+    on the underlying base relations, but the user performing the update does
+    not need any permissions on the underlying base relations. (see
+    <xref linkend="rules-privileges"/>)  If the view has the
+    <literal>security_invoker</literal> property set, the invoking user
+    will need to have the relevant privileges rather than the view owner.
    </para>
   </refsect2>
  </refsect1>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..083b70a1b9 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"security_invoker",
+			"Permissions on the underlying objects are checked for the calling user, not the view owner",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 6bd95bbce2..8aaa44fcc7 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2465,6 +2465,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_NODE_FIELD(tablesample);
 	COPY_NODE_FIELD(subquery);
 	COPY_SCALAR_FIELD(security_barrier);
+	COPY_SCALAR_FIELD(security_invoker);
 	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 4126516222..d0e2e2784a 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2768,6 +2768,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_invoker);
 	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 6bdad462c7..1beeb202ad 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3262,6 +3262,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 		case RTE_SUBQUERY:
 			WRITE_NODE_FIELD(subquery);
 			WRITE_BOOL_FIELD(security_barrier);
+			WRITE_BOOL_FIELD(security_invoker);
 			break;
 		case RTE_JOIN:
 			WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3f68f7c18d..ad825bb27f 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1444,6 +1444,7 @@ _readRangeTblEntry(void)
 		case RTE_SUBQUERY:
 			READ_NODE_FIELD(subquery);
 			READ_BOOL_FIELD(security_barrier);
+			READ_BOOL_FIELD(security_invoker);
 			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 41bd1ae7d4..30c66b9c6d 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_invoker = 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 282589dec8..bd7dc1c348 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_invoker = 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 3d82138cb3..4674fc4171 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_invoker = RelationHasSecurityInvoker(relation);
 	/* Clear fields that should not be set in a subquery RTE */
 	rte->relid = InvalidOid;
 	rte->relkind = 0;
@@ -3242,18 +3243,25 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "security_invoker" set, mark the new target RTE for the
+	 * permissions checks that we want to enforce against the query caller.
+	 * Otherwise, we want to enforce them against the view owner, not the
+	 * query caller.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationHasSecurityInvoker(view))
+		new_rte->checkAsUser = view_rte->checkAsUser;
+	else
+		new_rte->checkAsUser = view->rd_rel->relowner;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2707fed12f..0f10c964a3 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 that has the "security_invoker" relopt
+		 * set to true, we want the rule's table references to be checked as
+		 * the user invoking 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,12 @@ 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
+			  && RelationHasSecurityInvoker(relation)))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1170,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 +1201,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 37fcc4c9b5..3e04eba0d9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1042,6 +1042,7 @@ typedef struct RangeTblEntry
 	 */
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
+	bool		security_invoker;	/* from a view with security_invoker set? */
 
 	/*
 	 * Fields valid for a join RTE (else NULL/zero):
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 6da1b220cd..9a8866c91d 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityRelationPermissions
+ *		Returns true if the relation has the security invoker property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index ca1833dc66..66bacbc768 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -288,17 +288,31 @@ ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {security_invoker=true}
+ mysecview8 | v       | {security_invoker=false,security_barrier=true}
+ mysecview9 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -308,17 +322,28 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                   reloptions                   
+------------+---------+------------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview7 | v       | 
+ mysecview8 | v       | {security_invoker=true}
+ mysecview9 | v       | {security_invoker=false,security_barrier=true}
+(7 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -2031,7 +2056,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 74 other objects
+NOTICE:  drop cascades to 77 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2052,6 +2077,9 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
 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..070dc2cf0c 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_emily;
 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_emily 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_emily;
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
 SET search_path = regress_rls_schema;
@@ -627,6 +632,59 @@ SELECT * FROM category;
   44 | manga
 (4 rows)
 
+-- Test views with security_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+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)
+
+RESET SESSION AUTHORIZATION;
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (security_invoker=true) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM v1t;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1t values (1);
+ERROR:  permission denied for table sivt1
+UPDATE v1t SET x = 2;
+ERROR:  permission denied for table sivt1
+DELETE FROM v1t;
+ERROR:  permission denied for table sivt1
 --
 -- Table inheritance and RLS policy
 --
@@ -3987,11 +4045,16 @@ RESET SESSION AUTHORIZATION;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 29 other objects
+NOTICE:  drop cascades to 34 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 security_invoker_func()
+drop cascades to view v1f
+drop cascades to table sivt1
+drop cascades to view v1t
 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 6bb5b8df5e..102afcb9ba 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -245,9 +245,19 @@ CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -258,9 +268,17 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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..f1a632b22f 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_emily;
 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_emily 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_emily;
 
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
@@ -225,6 +230,45 @@ SET row_security TO OFF;
 SELECT * FROM document;
 SELECT * FROM category;
 
+-- Test views with security_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
+RESET SESSION AUTHORIZATION;
+
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (security_invoker=true) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+
+SELECT * FROM v1t;
+INSERT INTO v1t values (1);
+UPDATE v1t SET x = 2;
+DELETE FROM v1t;
+
 --
 -- Table inheritance and RLS policy
 --
-- 
2.35.1

#13Noname
walther@technowledgy.de
In reply to: Laurenz Albe (#10)
Re: [PATCH] Add reloption for views to enable RLS

Laurenz Albe:

So even though the view owner "duff" has no permissions
on the schema "viewtest", we can still select from the table.
Permissions on the schema containing the table are not
checked, only permissions on the table itself.

I am not sure how to feel about this. It is not what I would have
expected, but changing it would be a compatibility break.
Should this be considered a live bug in PostgreSQL?

I now found the docs to say:

USAGE:
For schemas, allows access to objects contained in the schema (assuming
that the objects' own privilege requirements are also met). Essentially
this allows the grantee to “look up” objects within the schema. Without
this permission, it is still possible to see the object names, e.g., by
querying system catalogs. Also, after revoking this permission, existing
sessions might have statements that have previously performed this
lookup, so this is not a completely secure way to prevent object access.

So, this seems to be perfectly fine.

Best

Wolfgang

#14Noname
walther@technowledgy.de
In reply to: Christoph Heiss (#12)
Re: [PATCH] Add reloption for views to enable RLS

Christoph Heiss:

xxx_owner=true would be the default and xxx_owner=false could be set
explicitly to get the behavior we are looking for in this patch?

I'm not sure if an option which is on by default would be best, IMHO. I
would rather have an off-by-default option, so that you explicitly have
to turn *on* that behavior rather than turning *off* the current.

Just out of curiosity I asked myself whether there were any other
boolean options that default to true in postgres - and there are plenty.
./configure options, client connection settings, server config options,
etc - but also some SQL statements:
- CREATE USER defaults to LOGIN
- CREATE ROLE defaults to INHERIT
- CREATE COLLATION defaults to DETERMINISTIC=true

There's even reloptions, that do, e.g. vacuum_truncate.

My best suggestions is maybe something like run_as_invoker=t|f, but that
would probably raise the same "invoker vs definer" association.

It is slightly better, I agree. But, yes, that same association is
raised easily. The more I think about it, the more it becomes clear that
really the current default behavior of "running the query as the view
owner" is the special thing here, not the behavior you are introducing.

If we were to start from scratch, it would be pretty obvious - to me -
that run_as_owner=false would be the default, and the run_as_owner=true
would need to be turned on explicitly. I'm thinking about "run_as_owner"
as the better design and "defaults to true" as a backwards compatibility
thing.

But yeah, it would be good to hear other opinions on that, too.

Best

Wolfgang

#15Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Noname (#14)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

Hi,

On 2/15/22 09:37, walther@technowledgy.de wrote:

Christoph Heiss:

xxx_owner=true would be the default and xxx_owner=false could be set
explicitly to get the behavior we are looking for in this patch?

I'm not sure if an option which is on by default would be best, IMHO.
I would rather have an off-by-default option, so that you explicitly
have to turn *on* that behavior rather than turning *off* the current.

Just out of curiosity I asked myself whether there were any other
boolean options that default to true in postgres - and there are plenty.
./configure options, client connection settings, server config options,
etc - but also some SQL statements:
- CREATE USER defaults to LOGIN
- CREATE ROLE defaults to INHERIT
- CREATE COLLATION defaults to DETERMINISTIC=true

There's even reloptions, that do, e.g. vacuum_truncate.

Knowing that I happily drop my objection about that. :^)

[..] The more I think about it, the more it becomes clear that
really the current default behavior of "running the query as the view
owner" is the special thing here, not the behavior you are introducing.

If we were to start from scratch, it would be pretty obvious - to me -
that run_as_owner=false would be the default, and the run_as_owner=true
would need to be turned on explicitly. I'm thinking about "run_as_owner"
as the better design and "defaults to true" as a backwards compatibility
thing.

Right, if we treat that as a kind of "backwards-compatible" feature,
having an reloption that is on by default makes sense.

I converted the option to run_as_owner=true|false in the attached v7.
It now definitely seems like the right way to move forward and getting
more feedback.

Thanks,
Christoph Heiss

Attachments:

v7-0001-Add-new-boolean-reloption-security_invoker-to-vie.patchtext/x-patch; charset=UTF-8; name=v7-0001-Add-new-boolean-reloption-security_invoker-to-vie.patchDownload
From 27b3c425bc000d32253a6c33057c75dfb2deb6ef Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 15 Feb 2022 12:25:46 +0100
Subject: [PATCH v7 1/1] Add new boolean reloption "security_invoker" to views

When this reloption is set to true, all permissions on the underlying
objects will be checked against the invoking user rather than the view
owner, as is currently implemented.

Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
Co-Author: Laurenz Albe <laurenz.albe@cybertec.at>
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
---
 doc/src/sgml/ref/alter_view.sgml          | 12 +++-
 doc/src/sgml/ref/create_view.sgml         | 74 ++++++++++++++++++-----
 src/backend/access/common/reloptions.c    | 11 ++++
 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    |  3 +
 src/backend/optimizer/prep/prepjointree.c |  2 +
 src/backend/rewrite/rewriteHandler.c      | 19 ++++--
 src/backend/utils/cache/relcache.c        | 63 ++++++++++---------
 src/include/nodes/parsenodes.h            |  1 +
 src/include/utils/rel.h                   | 11 ++++
 src/test/regress/expected/create_view.out | 46 +++++++++++---
 src/test/regress/expected/rowsecurity.out | 65 +++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 22 ++++++-
 src/test/regress/sql/rowsecurity.sql      | 44 ++++++++++++++
 17 files changed, 316 insertions(+), 61 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..bc6184bfea 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,11 +156,21 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>run_as_owner</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the user as which the subquery is run. Default is
+          <literal>true</literal>.  The value must be a Boolean value, such as
+          <literal>true</literal> or <literal>false</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..c34df4e746 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,22 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>run_as_owner</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Set by default.  If this option is set to <literal>true</literal>,
+          it will cause all access to underlying tables to be checked as
+          referenced by the view owner, otherwise as the invoking user.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,13 +278,39 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
-    a view must have permissions to call all functions used by the view.
+    By default, access to tables and functions referenced in the view is
+    determined by permissions of the view owner.  In some cases, this can be
+    used to provide secure but restricted access to the underlying tables.
+    However, not all views are secure against tampering; see
+    <xref linkend="rules-privileges"/> for details.  Functions called in the
+    view are treated the same as if they had been called directly from the
+    query using the view.  Therefore the user of a view must have permissions
+    to call all functions used by the view.  This also means that functions
+    are executed as the invoking user, not the view owner.
+   </para>
+
+   <para>
+    However, when using chained views, the <literal>CURRENT_USER</literal> user
+    will always stay the invoking user, regardless of whether the query is run
+    as the view owner (the default) or the invoking user (when
+    <literal>run_as_owner</literal> is set to <literal>false</literal>)
+    and the depth of the current invocation.
+   </para>
+
+   <para>
+    If the <literal>run_as_owner</literal> property is set to
+    <literal>false</literal> on the view, access to tables and functions
+    referenced in the view is determined by permissions of the invoking user,
+    rather than the view owner.  If <link linkend="ddl-rowsecurity">row-level
+    security</link> is enabled on the referenced tables, policies are also
+    invoked for the invoking user.  This is useful if you want the view to
+    behave just as if the defining query had been used instead.
+   </para>
+
+   <para>
+    Be aware that <literal>USAGE</literal> privileges on schemas are not checked
+    when referencing the underlying base relations, even if they are part of a
+    different schema.
    </para>
 
    <para>
@@ -387,10 +426,17 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    <para>
     Note that the user performing the insert, update or delete on the view
     must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    view.
+   </para>
+
+   <para>
+    Additionally, by default the view's owner must have the relevant privileges
+    on the underlying base relations, but the user performing the update does
+    not need any permissions on the underlying base relations. (see
+    <xref linkend="rules-privileges"/>)  If the view has the
+    <literal>run_as_owner</literal> property is set to <literal>false</literal>,
+    the invoking user will need to have the relevant privileges rather than the
+    view owner.
    </para>
   </refsect2>
  </refsect1>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..ca0461f52a 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"run_as_owner",
+			"Privileges on underlying relations and functions are checked as the view owner, not the calling user",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		true
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"run_as_owner", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, run_as_owner)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bc0d90b4b1..d529782377 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2465,6 +2465,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_NODE_FIELD(tablesample);
 	COPY_NODE_FIELD(subquery);
 	COPY_SCALAR_FIELD(security_barrier);
+	COPY_SCALAR_FIELD(run_as_owner);
 	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 2e7122ad2f..857fc0a9ac 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2776,6 +2776,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
 	COMPARE_NODE_FIELD(tablesample);
 	COMPARE_NODE_FIELD(subquery);
 	COMPARE_SCALAR_FIELD(security_barrier);
+	COMPARE_SCALAR_FIELD(run_as_owner);
 	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 6bdad462c7..08d3abfdbf 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3262,6 +3262,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 		case RTE_SUBQUERY:
 			WRITE_NODE_FIELD(subquery);
 			WRITE_BOOL_FIELD(security_barrier);
+			WRITE_BOOL_FIELD(run_as_owner);
 			break;
 		case RTE_JOIN:
 			WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3f68f7c18d..be06f57422 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1444,6 +1444,7 @@ _readRangeTblEntry(void)
 		case RTE_SUBQUERY:
 			READ_NODE_FIELD(subquery);
 			READ_BOOL_FIELD(security_barrier);
+			READ_BOOL_FIELD(run_as_owner);
 			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 41bd1ae7d4..c85ad430c7 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1217,6 +1217,9 @@ inline_cte_walker(Node *node, inline_cte_walker_context *context)
 			rte->subquery = newquery;
 			rte->security_barrier = false;
 
+			/* Run sub-query as owner by default */
+			rte->run_as_owner = true;
+
 			/* Zero out CTE-specific fields */
 			rte->ctename = NULL;
 			rte->ctelevelsup = 0;
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 282589dec8..54eeef8a13 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -660,6 +660,8 @@ preprocess_function_rtes(PlannerInfo *root)
 				rte->rtekind = RTE_SUBQUERY;
 				rte->subquery = funcquery;
 				rte->security_barrier = false;
+				/* Run sub-query as owner by default */
+				rte->run_as_owner = true;
 				/* 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 3d82138cb3..796553bbcc 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->run_as_owner = RelationSubqueryRunAsOwner(relation);
 	/* Clear fields that should not be set in a subquery RTE */
 	rte->relid = InvalidOid;
 	rte->relkind = 0;
@@ -3242,18 +3243,24 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "run_as_owner" set, mark the new target RTE for the
+	 * permissions checks that we want to enforce against the view owner.
+	 * Otherwise we want to enforce them against the query caller.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationSubqueryRunAsOwner(view))
+		new_rte->checkAsUser = view->rd_rel->relowner;
+	else
+		new_rte->checkAsUser = view_rte->checkAsUser;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2707fed12f..aa322f99bd 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 that has the "run_as_owner" relopt
+		 * set to false, we want the rule's table references to be checked as
+		 * the user invoking 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,12 @@ 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
+			  && !RelationSubqueryRunAsOwner(relation)))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1170,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 +1201,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 34218b718c..407d664d9c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1042,6 +1042,7 @@ typedef struct RangeTblEntry
 	 */
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
+	bool		run_as_owner;		/* run the sub-query as view owner */
 
 	/*
 	 * Fields valid for a join RTE (else NULL/zero):
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 6da1b220cd..dbdd8b3967 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		run_as_owner;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationSubqueryRunAsOwner
+ *		Returns true if the relation has the run_as_owner property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationSubqueryRunAsOwner(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->run_as_owner : true)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index ca1833dc66..42b54357a4 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -288,17 +288,31 @@ ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (run_as_owner=false)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (run_as_owner=true, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (run_as_owner)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (run_as_owner=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "run_as_owner": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {run_as_owner=false}
+ mysecview8 | v       | {run_as_owner=true,security_barrier=true}
+ mysecview9 | v       | {run_as_owner=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -308,17 +322,28 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (run_as_owner=false)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (run_as_owner=true, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                reloptions                 
+------------+---------+-------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview7 | v       | 
+ mysecview8 | v       | {run_as_owner=false}
+ mysecview9 | v       | {run_as_owner=true,security_barrier=true}
+(7 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -2031,7 +2056,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 74 other objects
+NOTICE:  drop cascades to 77 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2052,6 +2077,9 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
 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..02d3f63c6f 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_emily;
 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_emily 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_emily;
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
 SET search_path = regress_rls_schema;
@@ -627,6 +632,59 @@ SELECT * FROM category;
   44 | manga
 (4 rows)
 
+-- Test views with run_as_owner reloption set to false
+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 (run_as_owner=false) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION run_as_owner_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (run_as_owner=false) AS
+SELECT * FROM run_as_owner_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_emily;
+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)
+
+RESET SESSION AUTHORIZATION;
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (run_as_owner=false) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM v1t;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1t values (1);
+ERROR:  permission denied for table sivt1
+UPDATE v1t SET x = 2;
+ERROR:  permission denied for table sivt1
+DELETE FROM v1t;
+ERROR:  permission denied for table sivt1
 --
 -- Table inheritance and RLS policy
 --
@@ -3987,11 +4045,16 @@ RESET SESSION AUTHORIZATION;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 29 other objects
+NOTICE:  drop cascades to 34 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 run_as_owner_func()
+drop cascades to view v1f
+drop cascades to table sivt1
+drop cascades to view v1t
 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 6bb5b8df5e..2e4457cbf6 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -245,9 +245,19 @@ CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (run_as_owner=false)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (run_as_owner=true, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (run_as_owner)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (run_as_owner=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -258,9 +268,17 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (run_as_owner=false)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (run_as_owner=true, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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..a5b5a86108 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_emily;
 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_emily 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_emily;
 
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
@@ -225,6 +230,45 @@ SET row_security TO OFF;
 SELECT * FROM document;
 SELECT * FROM category;
 
+-- Test views with run_as_owner reloption set to false
+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 (run_as_owner=false) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION run_as_owner_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (run_as_owner=false) AS
+SELECT * FROM run_as_owner_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_emily;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
+RESET SESSION AUTHORIZATION;
+
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (run_as_owner=false) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+
+SELECT * FROM v1t;
+INSERT INTO v1t values (1);
+UPDATE v1t SET x = 2;
+DELETE FROM v1t;
+
 --
 -- Table inheritance and RLS policy
 --
-- 
2.35.1

#16Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Christoph Heiss (#15)
Re: [PATCH] Add reloption for views to enable RLS

On Tue, 2022-02-15 at 13:02 +0100, Christoph Heiss wrote:

I converted the option to run_as_owner=true|false in the attached v7.
It now definitely seems like the right way to move forward and getting
more feedback.

I think we are straying from the target.

"run_as_owner" seems wrong to me, because it is all about permission
checking and *not* about running. As we have established, the query
is always executed by the caller.

So my preferred bikeshed colors would be "permissions_owner" or
"permissions_caller".

About the documentation:

--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,11 +156,21 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>run_as_owner</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the user as which the subquery is run. Default is
+          <literal>true</literal>.  The value must be a Boolean value, such as
+          <literal>true</literal> or <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>

Correct would be

If set to <literal>true</literal> (which is the default value), permissions
on the underlying relations are checked as view owner, otherwise as the user
executing the query.

(I used "relation" to express that it doesn't hold for functions.)

--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -265,13 +278,39 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
-    a view must have permissions to call all functions used by the view.
+    By default, access to tables and functions referenced in the view is
+    determined by permissions of the view owner.

No, access to the functions is checked for the caller.

+ [...] Therefore the user of a view must have permissions

Comma after "therefore".

+    to call all functions used by the view.  This also means that functions
+    are executed as the invoking user, not the view owner.
+   </para>
+
+   <para>
+    However, when using chained views, the <literal>CURRENT_USER</literal> user
+    will always stay the invoking user,

"However" would introduce something that is different from what came before,
which this doesn't seem to be.

Perhaps "In particular" or "moreover".

+                                        regardless of whether the query is run
+    as the view owner (the default) or the invoking user (when
+    <literal>run_as_owner</literal> is set to <literal>false</literal>)
+    and the depth of the current invocation.
+   </para>

The query is *always* run as the invoking user. Better:

regardless of whether relation permissions are checked as the view owner or ...

+   <para>
+    Be aware that <literal>USAGE</literal> privileges on schemas are not checked
+    when referencing the underlying base relations, even if they are part of a
+    different schema.
    </para>

"referencing" is a bit unclear.
Perhaps "when checking permissions on the underlying base relations".

Otherwise, this looks good!

Yours,
Laurenz Albe

#17Noname
walther@technowledgy.de
In reply to: Laurenz Albe (#16)
Re: [PATCH] Add reloption for views to enable RLS

Laurenz Albe:

I converted the option to run_as_owner=true|false in the attached v7.
It now definitely seems like the right way to move forward and getting
more feedback.

I think we are straying from the target.

"run_as_owner" seems wrong to me, because it is all about permission
checking and*not* about running. As we have established, the query
is always executed by the caller.

So my preferred bikeshed colors would be "permissions_owner" or
"permissions_caller".

My main point was the "xxx_owner = true by default" thing. Whether xxx
is "permissions" or "run_as" doesn't change that. permissions_caller,
however, would be a step backwards.

I can see how permissions_owner is better than run_as_owner. The code
uses checkAsUser, so check_as_owner would be an option, too. Although
that could easily be associated with WITH CHECK OPTION. Thinking about
that, the difference between LOCAL and CASCADED for CHECK OPTION pretty
much sums up one of the confusing bits about the whole thing, too.

Maybe "local_permissions_owner = true | false"? That would make it
crystal-clear, that this is only about the very first permissions check
and not about any checks later in a chain of multiple views.

"local_permissions = owner | caller" could also work - as long as we're
not using any of definer or invoker.

Best

Wolfgang

#18Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Noname (#17)
Re: [PATCH] Add reloption for views to enable RLS

On Tue, 2022-02-15 at 16:07 +0100, walther@technowledgy.de wrote:

Laurenz Albe:

I converted the option to run_as_owner=true|false in the attached v7.
It now definitely seems like the right way to move forward and getting
more feedback.

I think we are straying from the target.

"run_as_owner" seems wrong to me, because it is all about permission
checking and*not*  about running.  As we have established, the query
is always executed by the caller.

So my preferred bikeshed colors would be "permissions_owner" or
"permissions_caller".

My main point was the "xxx_owner = true by default" thing. Whether xxx
is "permissions" or "run_as" doesn't change that. permissions_caller,
however, would be a step backwards.

I can see how permissions_owner is better than run_as_owner. The code
uses checkAsUser, so check_as_owner would be an option, too. Although
that could easily be associated with WITH CHECK OPTION. Thinking about
that, the difference between LOCAL and CASCADED for CHECK OPTION pretty
much sums up one of the confusing bits about the whole thing, too.

Maybe "local_permissions_owner = true | false"? That would make it
crystal-clear, that this is only about the very first permissions check
and not about any checks later in a chain of multiple views.

"local_permissions = owner | caller" could also work - as long as we're
not using any of definer or invoker.

I don't think that "local" will make this clearer.
I'd be happy with "check_as_owner", except it is unclear *what* is checked.
"check_permissions_as_owner" is ok with me, but a bit long.

How about "check_permissions_owner"?

Yours,
Laurenz

#19Noname
walther@technowledgy.de
In reply to: Laurenz Albe (#18)
Re: [PATCH] Add reloption for views to enable RLS

Laurenz Albe:

I'd be happy with "check_as_owner", except it is unclear *what* is checked.

Yeah, that could be associated with WITH CHECK OPTION, too, as in "do
the CHECK OPTION stuff as the owner".

"check_permissions_as_owner" is ok with me, but a bit long.

check_permissions_as_owner is exactly what happens. The additional "as"
shouldn't be a problem in length - but is much better to read. I
wouldn't associate that with CHECK OPTION either. +1

Best

Wolfgang

#20Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Noname (#19)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

On Tue, 2022-02-15 at 16:32 +0100, walther@technowledgy.de wrote:

"check_permissions_as_owner" is ok with me, but a bit long.

check_permissions_as_owner is exactly what happens. The additional "as"
shouldn't be a problem in length - but is much better to read. I
wouldn't associate that with CHECK OPTION either. +1

Here is a new version, with improved documentation and the option renamed
to "check_permissions_owner". I just prefer the shorter form.

Yours,
Laurenz Albe

Attachments:

v8-0001-Add-new-boolean-reloption-check_permissions_owner-to.patchtext/x-patch; charset=UTF-8; name*0=v8-0001-Add-new-boolean-reloption-check_permissions_owner-to.patc; name*1=hDownload
From e31ea3de2838dcfdc8c364fc08e54e5d37f00882 Mon Sep 17 00:00:00 2001
From: Laurenz Albe <laurenz.albe@cybertec.at>
Date: Fri, 18 Feb 2022 15:53:06 +0100
Subject: [PATCH] Add new boolean reloption "check_permissions_owner" to views

When this reloption is set to "false", all permissions on the underlying
relations will be checked against the invoking user rather than the view
owner.  The latter remains the default behavior.

Author: Christoph Heiss <christoph.heiss@cybertec.at>
Reviewed-By: Laurenz Albe, Wolfgang Walther
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
---
 doc/src/sgml/ref/alter_view.sgml          | 13 ++++-
 doc/src/sgml/ref/create_view.sgml         | 68 ++++++++++++++++++-----
 src/backend/access/common/reloptions.c    | 11 ++++
 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      | 19 +++++--
 src/backend/utils/cache/relcache.c        | 63 +++++++++++----------
 src/include/nodes/parsenodes.h            |  1 +
 src/include/utils/rel.h                   | 11 ++++
 src/test/regress/expected/create_view.out | 46 ++++++++++++---
 src/test/regress/expected/rowsecurity.out | 65 +++++++++++++++++++++-
 src/test/regress/sql/create_view.sql      | 22 +++++++-
 src/test/regress/sql/rowsecurity.sql      | 44 +++++++++++++++
 17 files changed, 309 insertions(+), 60 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..0ea764738a 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,11 +156,22 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>check_permissions_owner</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes whether permission checks on the underlying relations are
+          performed as the view owner or as the calling user.  Default is
+          <literal>true</literal>.  The value must be a Boolean value, such as
+          <literal>true</literal> or <literal>false</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..cb253388e7 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,22 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>check_permissions_owner</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Set by default.  If this option is set to <literal>true</literal>,
+          it will cause all access to underlying tables to be checked as
+          referenced by the view owner, otherwise as the invoking user.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,13 +278,35 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
+    By default, access to relations referenced in the view is determined
+    by permissions of the view owner.  This can be used to provide secure
+    but restricted access to the underlying tables.  However, not all views
+    are secure against tampering; see <xref linkend="rules-privileges"/>
+    for details.
+   </para>
+
+   <para>
+    Functions called in the view are treated the same as if they had been
+    called directly from the query using the view.  Therefore, the user of
     a view must have permissions to call all functions used by the view.
+    This also means that functions are executed as the invoking user, not
+    the view owner.  In particular, <literal>CURRENT_USER</literal>
+    will always return the invoking user.
+   </para>
+
+   <para>
+    If the <literal>check_permissions_owner</literal> property is set to
+    <literal>false</literal> on the view, access to relations referenced in
+    the view is determined by permissions of the invoking user, rather than
+    the view owner.  If <link linkend="ddl-rowsecurity">row-level
+    security</link> is enabled on the referenced tables, policies are also
+    invoked for the invoking user.  This is useful if you want the view to
+    behave just as if the defining query had been used instead.
+   </para>
+
+   <para>
+    Be aware that <literal>USAGE</literal> privileges on schemas containing
+    the underlying base relations are <emphasis>not</emphasis> checked.
    </para>
 
    <para>
@@ -387,10 +422,17 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    <para>
     Note that the user performing the insert, update or delete on the view
     must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    view.
+   </para>
+
+   <para>
+    Additionally, by default the view's owner must have the relevant privileges
+    on the underlying base relations, but the user performing the update does
+    not need any permissions on the underlying base relations. (see
+    <xref linkend="rules-privileges"/>)  If the view has the
+    <literal>check_permissions_owner</literal> property is set to <literal>false</literal>,
+    the invoking user will need to have the relevant privileges rather than the
+    view owner.
    </para>
   </refsect2>
  </refsect1>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..64d31cd032 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"check_permissions_owner",
+			"Privileges on underlying relations are checked as the view owner, not the calling user",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		true
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"check_permissions_owner", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, check_permissions_owner)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bc0d90b4b1..8ad4c62d39 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2465,6 +2465,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_NODE_FIELD(tablesample);
 	COPY_NODE_FIELD(subquery);
 	COPY_SCALAR_FIELD(security_barrier);
+	COPY_SCALAR_FIELD(check_permissions_owner);
 	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 2e7122ad2f..5197c2c9f8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2776,6 +2776,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
 	COMPARE_NODE_FIELD(tablesample);
 	COMPARE_NODE_FIELD(subquery);
 	COMPARE_SCALAR_FIELD(security_barrier);
+	COMPARE_SCALAR_FIELD(check_permissions_owner);
 	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 6bdad462c7..cd18111fb0 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3262,6 +3262,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 		case RTE_SUBQUERY:
 			WRITE_NODE_FIELD(subquery);
 			WRITE_BOOL_FIELD(security_barrier);
+			WRITE_BOOL_FIELD(check_permissions_owner);
 			break;
 		case RTE_JOIN:
 			WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 3f68f7c18d..fcb5273caf 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1444,6 +1444,7 @@ _readRangeTblEntry(void)
 		case RTE_SUBQUERY:
 			READ_NODE_FIELD(subquery);
 			READ_BOOL_FIELD(security_barrier);
+			READ_BOOL_FIELD(check_permissions_owner);
 			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 41bd1ae7d4..710b317072 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->check_permissions_owner = true;
 
 			/* 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 282589dec8..a29f1eb75c 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->check_permissions_owner = true;
 				/* 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 3d82138cb3..5014a6809a 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->check_permissions_owner = RelationSubqueryCheckPermsOwner(relation);
 	/* Clear fields that should not be set in a subquery RTE */
 	rte->relid = InvalidOid;
 	rte->relkind = 0;
@@ -3242,18 +3243,24 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "check_permissions_owner" set, mark the new target RTE
+	 * for the permissions checks that we want to enforce against the view
+	 * owner.  Otherwise we want to enforce them against the query caller.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationSubqueryCheckPermsOwner(view))
+		new_rte->checkAsUser = view->rd_rel->relowner;
+	else
+		new_rte->checkAsUser = view_rte->checkAsUser;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2707fed12f..c02613498a 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 that has the "check_permissions_owner"
+		 * relopt set to false, we want the rule's table references to be
+		 * checked as the user invoking 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,12 @@ 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
+			  && !RelationSubqueryCheckPermsOwner(relation)))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1163,27 +1170,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 +1201,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 34218b718c..dc64197586 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1042,6 +1042,7 @@ typedef struct RangeTblEntry
 	 */
 	Query	   *subquery;		/* the sub-query */
 	bool		security_barrier;	/* is from security_barrier view? */
+	bool		check_permissions_owner;	/* is from check_permissions_owner view? */
 
 	/*
 	 * Fields valid for a join RTE (else NULL/zero):
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 6da1b220cd..95b597eb93 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		check_permissions_owner;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationSubqueryRunAsOwner
+ *		Returns true if the relation has the check_permissions_owner property
+ *		set, or	not.  Note multiple eval of argument!
+ */
+#define RelationSubqueryCheckPermsOwner(relation)							\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->check_permissions_owner : true)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index ae7c04353c..5ece1d157b 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -296,17 +296,31 @@ ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (check_permissions_owner=true, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (check_permissions_owner)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (check_permissions_owner=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "check_permissions_owner": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {check_permissions_owner=false}
+ mysecview8 | v       | {check_permissions_owner=true,security_barrier=true}
+ mysecview9 | v       | {check_permissions_owner=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -316,17 +330,28 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (check_permissions_owner=true, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                      reloptions                      
+------------+---------+------------------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview7 | v       | 
+ mysecview8 | v       | {check_permissions_owner=false}
+ mysecview9 | v       | {check_permissions_owner=true,security_barrier=true}
+(7 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -2039,7 +2064,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 74 other objects
+NOTICE:  drop cascades to 77 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2060,6 +2085,9 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
 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..c6eae9af9c 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_emily;
 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_emily 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_emily;
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
 SET search_path = regress_rls_schema;
@@ -627,6 +632,59 @@ SELECT * FROM category;
   44 | manga
 (4 rows)
 
+-- Test views with check_permissions_owner reloption set to false
+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 (check_permissions_owner=false) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION check_permissions_owner_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (check_permissions_owner=false) AS
+SELECT * FROM check_permissions_owner_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_emily;
+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)
+
+RESET SESSION AUTHORIZATION;
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (check_permissions_owner=false) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM v1t;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1t values (1);
+ERROR:  permission denied for table sivt1
+UPDATE v1t SET x = 2;
+ERROR:  permission denied for table sivt1
+DELETE FROM v1t;
+ERROR:  permission denied for table sivt1
 --
 -- Table inheritance and RLS policy
 --
@@ -3987,11 +4045,16 @@ RESET SESSION AUTHORIZATION;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 29 other objects
+NOTICE:  drop cascades to 34 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 check_permissions_owner_func()
+drop cascades to view v1f
+drop cascades to table sivt1
+drop cascades to view v1t
 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 829f3ddbe6..7205debf41 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -254,9 +254,19 @@ CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (check_permissions_owner=true, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (check_permissions_owner)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (check_permissions_owner=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -267,9 +277,17 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (check_permissions_owner=true, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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..126cb626e9 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_emily;
 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_emily 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_emily;
 
 CREATE SCHEMA regress_rls_schema;
 GRANT ALL ON SCHEMA regress_rls_schema to public;
@@ -225,6 +230,45 @@ SET row_security TO OFF;
 SELECT * FROM document;
 SELECT * FROM category;
 
+-- Test views with check_permissions_owner reloption set to false
+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 (check_permissions_owner=false) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION check_permissions_owner_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (check_permissions_owner=false) AS
+SELECT * FROM check_permissions_owner_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_emily;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
+RESET SESSION AUTHORIZATION;
+
+CREATE TABLE sivt1 (x int);
+CREATE VIEW v1t WITH (check_permissions_owner=false) AS
+SELECT * FROM sivt1;
+ALTER VIEW v1t OWNER TO regress_rls_group1;
+
+GRANT INSERT, UPDATE, DELETE ON sivt1 TO regress_rls_group1;
+GRANT SELECT ON sivt1 TO regress_rls_group2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1t TO regress_rls_group2;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+
+SELECT * FROM v1t;
+INSERT INTO v1t values (1);
+UPDATE v1t SET x = 2;
+DELETE FROM v1t;
+
 --
 -- Table inheritance and RLS policy
 --
-- 
2.34.1

#21Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Laurenz Albe (#20)
Re: [PATCH] Add reloption for views to enable RLS

On Fri, 18 Feb 2022 at 14:57, Laurenz Albe <laurenz.albe@cybertec.at> wrote:

Here is a new version, with improved documentation and the option renamed
to "check_permissions_owner". I just prefer the shorter form.

Re-reading this thread, I think I preferred the name
"security_invoker". The main objection seemed to come from the
potential confusion with SECURITY INVOKER/DEFINER functions, but I
think that's really a different thing. As long as the documentation
for the default behaviour is clear (which I think it was), then it
should be easy to explain how a security invoker view behaves
differently. Also, there's value in using the same terminology as
other databases, because many users will already be familiar with the
feature from those databases.

Some other review comments:

1). This new comment:

+   <para>
+    Be aware that <literal>USAGE</literal> privileges on schemas containing
+    the underlying base relations are <emphasis>not</emphasis> checked.
+   </para>

is not entirely accurate. It's more accurate to say that a user
creating or replacing a view must have CREATE privileges on the schema
containing the view and USAGE privileges on any schemas referred to in
the view query, whereas a user using the view only needs USAGE
privileges on the schema containing the view.

(Note that, for the view creator, USAGE is required on any schema
referred to in the query -- e.g., schemas containing functions as well
as base relations.)

2). The patch is adding a new field to RangeTblEntry which seems to be
unnecessary -- it's set, and copied around, but never read, so it
should just be removed.

3). Looking at this change:

-        setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
-        setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+        if (!(relation->rd_rel->relkind == RELKIND_VIEW
+              && !RelationSubqueryCheckPermsOwner(relation)))
+        {
+            setRuleCheckAsUser((Node *) rule->actions,
relation->rd_rel->relowner);
+            setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+        }

I think it should call setRuleCheckAsUser() in all cases. It might be
true that the rule fetched has checkAsUser set to InvalidOid
throughout its action and quals, but it seems unwise to rely on that
-- better to code defensively and explicitly set it in all cases.

4). In the same code block, I think the new behaviour should be
applied to SELECT rules only. The view may have other non-SELECT rules
(just as a table may have non-SELECT rules), created using CREATE
RULE, but their actions are independent of the view definition.
Currently their permissions are checked as the view/table owner, and
if anyone wanted to change that, it should be an option on the rule,
not the view (just as triggers can be made security definer or
invoker, depending on how the trigger function is defined).

(Note: I'm not suggesting that anyone actually spend any time adding
such an option to rules. Given all the pitfalls associated with rules,
I think their use should be discouraged, and no development effort
should be expended enhancing them.)

5). In the same function, the block of code that fetches rules and
triggers has been moved. I think it would be worth adding a comment to
explain why it's now important to extract the reloptions *before*
fetching the relation's rules and triggers.

6). The second set of tests added to rowsecurity.sql seem to have
nothing to do with RLS, and probably belong in updatable_views.sql,
and I think it would be worth adding a few more tests for things like
views on top of views.

Regards,
Dean

#22Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Dean Rasheed (#21)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

Thanks for reviewing!

On 2/25/22 19:22, Dean Rasheed wrote:

Re-reading this thread, I think I preferred the name
"security_invoker". The main objection seemed to come from the
potential confusion with SECURITY INVOKER/DEFINER functions, but I
think that's really a different thing. As long as the documentation
for the default behaviour is clear (which I think it was), then it
should be easy to explain how a security invoker view behaves
differently. Also, there's value in using the same terminology as
other databases, because many users will already be familiar with the
feature from those databases.

That is also the main reason I preferred naming it "security_invoker" -
it is consistent with other databases and eases transition from such
systems.

I kept "check_permissions_owner" for now. Constantly changing it around
with each iteration doesn't really bring any value IMHO, I'd rather have
a final consensus on how to name the option and *then* change it for good.

Some other review comments:

1). This new comment:

+   <para>
+    Be aware that <literal>USAGE</literal> privileges on schemas containing
+    the underlying base relations are <emphasis>not</emphasis> checked.
+   </para>

is not entirely accurate. It's more accurate to say that a user
creating or replacing a view must have CREATE privileges on the schema
containing the view and USAGE privileges on any schemas referred to in
the view query, whereas a user using the view only needs USAGE
privileges on the schema containing the view.

(Note that, for the view creator, USAGE is required on any schema
referred to in the query -- e.g., schemas containing functions as well
as base relations.)

Improved in the attached v9.

2). The patch is adding a new field to RangeTblEntry which seems to be
unnecessary -- it's set, and copied around, but never read, so it
should just be removed.

I removed that field in v9 since it is indeed completely unused. I
initially added it to be consistent with the "security_barrier"
implementation and than somewhat forgot about it.

3). Looking at this change:

[..]

I think it should call setRuleCheckAsUser() in all cases. It might be
true that the rule fetched has checkAsUser set to InvalidOid
throughout its action and quals, but it seems unwise to rely on that
-- better to code defensively and explicitly set it in all cases.

It probably doesn't really matter, but I agree that coding defensively
is always a good thing.
Changed that in v9 to call setRuleCheckAsUser() either with ->relowner
or InvalidOid.

4). In the same code block, I think the new behaviour should be
applied to SELECT rules only. The view may have other non-SELECT rules
(just as a table may have non-SELECT rules), created using CREATE
RULE, but their actions are independent of the view definition.
Currently their permissions are checked as the view/table owner, and
if anyone wanted to change that, it should be an option on the rule,
not the view (just as triggers can be made security definer or
invoker, depending on how the trigger function is defined).

Good catch, I added a additional check for rule->event and a test for
that in v9.
[ I also had to add a missing DROP statement to some previous test, just
a heads up. ]

It makes sense to mimic the behavior of triggers and further,
user-created rules otherwise might behave differently for tables and
views, depending on the view definition.
[ But I'm not _that_ familiar with CREATE RULE, FWIW. ]

5). In the same function, the block of code that fetches rules and
triggers has been moved. I think it would be worth adding a comment to
explain why it's now important to extract the reloptions *before*
fetching the relation's rules and triggers.

Added a small comment explaining that in v9.

6). The second set of tests added to rowsecurity.sql seem to have
nothing to do with RLS, and probably belong in updatable_views.sql,
and I think it would be worth adding a few more tests for things like
views on top of views.

Seems reasonable to move them into updatable_views.sql, done that for
v9. Further I added two (simple) tests for chained views as you
mentioned, hope they reflect what you had in mind.

Thanks,
Christoph

Attachments:

v9-0001-Add-new-boolean-reloption-check_permissions_owner.patchtext/x-patch; charset=UTF-8; name=v9-0001-Add-new-boolean-reloption-check_permissions_owner.patchDownload
From a7e84c92761881419bf8d63ebfcc528417dc8d24 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 1 Mar 2022 17:36:42 +0100
Subject: [PATCH v9 1/1] Add new boolean reloption "check_permissions_owner" to
 views

When this reloption is set to "false", all permissions on the underlying
relations will be checked against the invoking user rather than the view
owner.  The latter remains the default behavior.

Author: Christoph Heiss <christoph.heiss@cybertec.at>
Co-Author: Laurenz Albe <laurenz.albe@cybertec.at>
Reviewed-By: Laurenz Albe, Wolfgang Walther
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 doc/src/sgml/ref/alter_view.sgml              | 13 ++-
 doc/src/sgml/ref/create_view.sgml             | 72 ++++++++++++++---
 src/backend/access/common/reloptions.c        | 11 +++
 src/backend/rewrite/rewriteHandler.c          | 18 +++--
 src/backend/utils/cache/relcache.c            | 79 ++++++++++++-------
 src/include/utils/rel.h                       | 11 +++
 src/test/regress/expected/create_view.out     | 73 ++++++++++++++---
 src/test/regress/expected/rowsecurity.out     | 43 +++++++++-
 src/test/regress/expected/rules.out           | 20 +++++
 src/test/regress/expected/updatable_views.out | 28 +++++++
 src/test/regress/sql/create_view.sql          | 44 ++++++++++-
 src/test/regress/sql/rowsecurity.sql          | 26 ++++++
 src/test/regress/sql/rules.sql                | 23 ++++++
 src/test/regress/sql/updatable_views.sql      | 27 +++++++
 14 files changed, 426 insertions(+), 62 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..0ea764738a 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,11 +156,22 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>check_permissions_owner</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes whether permission checks on the underlying relations are
+          performed as the view owner or as the calling user.  Default is
+          <literal>true</literal>.  The value must be a Boolean value, such as
+          <literal>true</literal> or <literal>false</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..15655c346c 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,22 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>check_permissions_owner</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Set by default.  If this option is set to <literal>true</literal>,
+          it will cause all access to underlying tables to be checked as
+          referenced by the view owner, otherwise as the invoking user.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,13 +278,39 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
+    By default, access to relations referenced in the view is determined
+    by permissions of the view owner.  This can be used to provide secure
+    but restricted access to the underlying tables.  However, not all views
+    are secure against tampering; see <xref linkend="rules-privileges"/>
+    for details.
+   </para>
+
+   <para>
+    Functions called in the view are treated the same as if they had been
+    called directly from the query using the view.  Therefore, the user of
     a view must have permissions to call all functions used by the view.
+    This also means that functions are executed as the invoking user, not
+    the view owner.  In particular, <literal>CURRENT_USER</literal>
+    will always return the invoking user.
+   </para>
+
+   <para>
+    If the <literal>check_permissions_owner</literal> property is set to
+    <literal>false</literal> on the view, access to relations referenced in
+    the view is determined by permissions of the invoking user, rather than
+    the view owner.  If <link linkend="ddl-rowsecurity">row-level
+    security</link> is enabled on the referenced tables, policies are also
+    invoked for the invoking user.  This is useful if you want the view to
+    behave just as if the defining query had been used instead.
+   </para>
+
+   <para>
+    When creating (or replacing) a view, the user must have
+    <literal>CREATE</literal> privileges on the schema containing the view and
+    <literal>USAGE</literal> privileges on any schemas referred to in the view
+    query.  The invoking user (which might be different from the view owner if
+    <literal>check_permissions_owner</literal> is used) only needs to have
+    <literal>USAGE</literal> privileges on the schema containing the view.
    </para>
 
    <para>
@@ -387,10 +426,17 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    <para>
     Note that the user performing the insert, update or delete on the view
     must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    view.
+   </para>
+
+   <para>
+    Additionally, by default the view's owner must have the relevant privileges
+    on the underlying base relations, but the user performing the update does
+    not need any permissions on the underlying base relations. (see
+    <xref linkend="rules-privileges"/>)  If the view has the
+    <literal>check_permissions_owner</literal> property is set to <literal>false</literal>,
+    the invoking user will need to have the relevant privileges rather than the
+    view owner.
    </para>
   </refsect2>
  </refsect1>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..64d31cd032 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"check_permissions_owner",
+			"Privileges on underlying relations are checked as the view owner, not the calling user",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		true
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"check_permissions_owner", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, check_permissions_owner)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 3d82138cb3..be8beba436 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3242,18 +3242,24 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "check_permissions_owner" set, mark the new target RTE
+	 * for the permissions checks that we want to enforce against the view
+	 * owner.  Otherwise we want to enforce them against the query caller.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationSubqueryCheckPermsOwner(view))
+		new_rte->checkAsUser = view->rd_rel->relowner;
+	else
+		new_rte->checkAsUser = view_rte->checkAsUser;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fccffce572..d04b92a4e4 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -826,11 +826,19 @@ 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 that has the "check_permissions_owner"
+		 * relopt set to false, we want the rule's table references to be
+		 * checked as the user invoking the rule by setting the checkAsUser
+		 * field to "InvalidOid".
+		 * This also should only apply for SELECT statements, thus check for
+		 * that too. The view might have some non-SELECT rules created using
+		 * CREATE RULE, but their actions should be independent of the view
+		 * definition.
+		 *
+		 * 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
@@ -838,8 +846,18 @@ 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 (rule->event == CMD_SELECT
+			&& relation->rd_rel->relkind == RELKIND_VIEW
+			&& !RelationSubqueryCheckPermsOwner(relation))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, InvalidOid);
+			setRuleCheckAsUser(rule->qual, InvalidOid);
+		}
+		else
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1164,27 +1182,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;
@@ -1213,9 +1210,33 @@ retry:
 	else
 		Assert(relation->rd_rel->relam == InvalidOid);
 
-	/* extract reloptions if any */
+	/*
+	 * Extract reloptions (if any) before fetching rules and triggers.
+	 * RelationBuildRuleLock() depends on having ->rd_options already set up.
+	 */
 	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/utils/rel.h b/src/include/utils/rel.h
index 3b4ab65ae2..c8c8b7ba0b 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		check_permissions_owner;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationSubqueryRunAsOwner
+ *		Returns true if the relation has the check_permissions_owner property
+ *		set, or	not.  Note multiple eval of argument!
+ */
+#define RelationSubqueryCheckPermsOwner(relation)							\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->check_permissions_owner : true)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index ae7c04353c..9813b6973d 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -296,17 +296,31 @@ ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (check_permissions_owner=true, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (check_permissions_owner)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (check_permissions_owner=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "check_permissions_owner": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {check_permissions_owner=false}
+ mysecview8 | v       | {check_permissions_owner=true,security_barrier=true}
+ mysecview9 | v       | {check_permissions_owner=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -316,18 +330,50 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (check_permissions_owner=true, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                      reloptions                      
+------------+---------+------------------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
-
+ mysecview7 | v       | 
+ mysecview8 | v       | {check_permissions_owner=false}
+ mysecview9 | v       | {check_permissions_owner=true,security_barrier=true}
+(7 rows)
+
+-- Test chained views when using "check_permissions_owner"
+CREATE TABLE ct1 (x int);
+CREATE VIEW cv1 WITH (check_permissions_owner=true) AS
+SELECT * FROM ct1;
+CREATE VIEW cv2 WITH (check_permissions_owner=false) AS
+SELECT * FROM cv1;
+CREATE ROLE alice NOLOGIN;
+GRANT USAGE ON SCHEMA testviewschm2 TO alice;
+GRANT SELECT ON cv1, cv2 TO alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+ x 
+---
+(0 rows)
+
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT ON cv1 FROM alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+ERROR:  permission denied for view cv1
+RESET SESSION AUTHORIZATION;
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
 CREATE VIEW unspecified_types AS
@@ -2039,7 +2085,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 74 other objects
+NOTICE:  drop cascades to 80 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2060,6 +2106,12 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
+drop cascades to table ct1
+drop cascades to view cv1
+drop cascades to view cv2
 drop cascades to view unspecified_types
 drop cascades to table tt1
 drop cascades to table tx1
@@ -2114,3 +2166,4 @@ drop cascades to view tt23v
 drop cascades to view tt24v
 drop cascades to view tt25v
 drop cascades to view tt26v
+DROP ROLE alice;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 89397e41f0..948cb7d3d9 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_emily;
 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_emily 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_emily;
 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 check_permissions_owner reloption set to false
+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 (check_permissions_owner=false) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION check_permissions_owner_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (check_permissions_owner=false) AS
+SELECT * FROM check_permissions_owner_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_emily;
+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 check_permissions_owner_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/expected/rules.out b/src/test/regress/expected/rules.out
index ac468568a1..c9d23613a4 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2823,6 +2823,10 @@ select * from only t1_2;
 (10 rows)
 
 reset constraint_exclusion;
+drop table t1 cascade;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table t1_1
+drop cascades to table t1_2
 -- test FOR UPDATE in rules
 create table rules_base(f1 int, f2 int);
 insert into rules_base values(1,2), (11,12);
@@ -3496,3 +3500,19 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+-- Test rules on views with "check_permissions_owner" set to false.
+CREATE ROLE role1;
+CREATE TABLE t1 (x int);
+CREATE TABLE t2 (x int);
+CREATE VIEW v1 WITH (check_permissions_owner=false) AS
+SELECT * FROM t1;
+GRANT INSERT ON t1, v1 TO role1;
+CREATE RULE testrule1 AS ON INSERT TO v1
+DO INSTEAD INSERT INTO t2 VALUES (NEW.*);
+SET ROLE role1;
+INSERT INTO v1 VALUES (1);
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t2;
+DROP TABLE t1;
+DROP ROLE role1;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index cdff914b93..e2e61fc34d 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2578,6 +2578,34 @@ DROP VIEW v2;
 DROP VIEW v1;
 DROP TABLE t2;
 DROP TABLE t1;
+-- Check UPDATE/INSERT/DELETE on views with check_permissions_owner set to false
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role1;
+CREATE ROLE regress_role2;
+CREATE TABLE t1 (x int);
+CREATE VIEW v1 WITH (check_permissions_owner=false) AS
+SELECT * FROM t1;
+ALTER VIEW v1 OWNER TO regress_role1;
+GRANT INSERT, UPDATE, DELETE ON t1 TO regress_role1;
+GRANT SELECT ON t1 TO regress_role2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1 TO regress_role2;
+SET SESSION AUTHORIZATION regress_role2;
+SELECT * FROM v1;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1 values (1);
+ERROR:  permission denied for table t1
+UPDATE v1 SET x = 2;
+ERROR:  permission denied for table t1
+DELETE FROM v1;
+ERROR:  permission denied for table t1
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t1;
+DROP ROLE regress_role2;
+DROP ROLE regress_role1;
 --
 -- Test CREATE OR REPLACE VIEW turning a non-updatable view into an
 -- auto-updatable view and adding check options in a single step
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index 829f3ddbe6..cc42f21b12 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -254,9 +254,19 @@ CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (check_permissions_owner=true, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (check_permissions_owner)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (check_permissions_owner=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -267,11 +277,40 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (check_permissions_owner=false)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (check_permissions_owner=true, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
+-- Test chained views when using "check_permissions_owner"
+CREATE TABLE ct1 (x int);
+CREATE VIEW cv1 WITH (check_permissions_owner=true) AS
+SELECT * FROM ct1;
+CREATE VIEW cv2 WITH (check_permissions_owner=false) AS
+SELECT * FROM cv1;
+
+CREATE ROLE alice NOLOGIN;
+GRANT USAGE ON SCHEMA testviewschm2 TO alice;
+GRANT SELECT ON cv1, cv2 TO alice;
+
+SET ROLE alice;
+SELECT * FROM cv2;
+
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT ON cv1 FROM alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+
+RESET SESSION AUTHORIZATION;
+
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
 
@@ -722,3 +761,4 @@ select pg_get_viewdef('tt26v', true);
 -- clean up all the random objects we made above
 DROP SCHEMA temp_view_test CASCADE;
 DROP SCHEMA testviewschm2 CASCADE;
+DROP ROLE alice;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 44deb42bad..ba91a9be34 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_emily;
 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_emily 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_emily;
 
 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 check_permissions_owner reloption set to false
+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 (check_permissions_owner=false) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION check_permissions_owner_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (check_permissions_owner=false) AS
+SELECT * FROM check_permissions_owner_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_emily;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
 --
 -- Table inheritance and RLS policy
 --
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index 8bdab6dec3..782a2a30af 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -991,6 +991,7 @@ select * from only t1_1;
 select * from only t1_2;
 
 reset constraint_exclusion;
+drop table t1 cascade;
 
 -- test FOR UPDATE in rules
 
@@ -1257,3 +1258,25 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+
+-- Test rules on views with "check_permissions_owner" set to false.
+CREATE ROLE role1;
+
+CREATE TABLE t1 (x int);
+CREATE TABLE t2 (x int);
+CREATE VIEW v1 WITH (check_permissions_owner=false) AS
+SELECT * FROM t1;
+GRANT INSERT ON t1, v1 TO role1;
+
+CREATE RULE testrule1 AS ON INSERT TO v1
+DO INSTEAD INSERT INTO t2 VALUES (NEW.*);
+
+SET ROLE role1;
+
+INSERT INTO v1 VALUES (1);
+
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t2;
+DROP TABLE t1;
+DROP ROLE role1;
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index 09328e582b..45ff1a6e48 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1226,6 +1226,33 @@ DROP VIEW v1;
 DROP TABLE t2;
 DROP TABLE t1;
 
+-- Check UPDATE/INSERT/DELETE on views with check_permissions_owner set to false
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role1;
+CREATE ROLE regress_role2;
+
+CREATE TABLE t1 (x int);
+CREATE VIEW v1 WITH (check_permissions_owner=false) AS
+SELECT * FROM t1;
+ALTER VIEW v1 OWNER TO regress_role1;
+
+GRANT INSERT, UPDATE, DELETE ON t1 TO regress_role1;
+GRANT SELECT ON t1 TO regress_role2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1 TO regress_role2;
+
+SET SESSION AUTHORIZATION regress_role2;
+
+SELECT * FROM v1;
+INSERT INTO v1 values (1);
+UPDATE v1 SET x = 2;
+DELETE FROM v1;
+
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t1;
+DROP ROLE regress_role2;
+DROP ROLE regress_role1;
+
 --
 -- Test CREATE OR REPLACE VIEW turning a non-updatable view into an
 -- auto-updatable view and adding check options in a single step
-- 
2.35.1

#23Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Christoph Heiss (#22)
Re: [PATCH] Add reloption for views to enable RLS

On Tue, 1 Mar 2022 at 16:40, Christoph Heiss
<christoph.heiss@cybertec.at> wrote:

That is also the main reason I preferred naming it "security_invoker" -
it is consistent with other databases and eases transition from such
systems.

I kept "check_permissions_owner" for now. Constantly changing it around
with each iteration doesn't really bring any value IMHO, I'd rather have
a final consensus on how to name the option and *then* change it for good.

Yes indeed, it's annoying to keep changing the name between patch
versions, so let's try to get a consensus now.

For my part, I find myself more and more convinced that
"security_invoker" is the right name, because it matches the
terminology used for functions, and in other database systems. I think
the parallels between security invoker functions and security invoker
views are quite strong.

There are a couple of additional considerations that lend weight to
that choice of name, though not uniquely to it:

1). There is a slight advantage to having an option that defaults to
false/off, like the existing "security_barrier" option -- it allows a
shorthand to turn the option on, because the system automatically
turns "WITH (security_barrier)" into "WITH (security_barrier=true)".

2). Grammatically, a name like this works better, because it serves
both as the name of the boolean option, and as an adjective that can
be used to describe and name the feature -- as in "security barrier
views are cool" -- making it easier to talk about the feature.

"check_permissions_owner=false" doesn't work as well in either regard,
and just feels much more clumsy.

When we come to write the release notes for this feature, saying that
this version of PG now supports security invoker views is going to
mean a lot more to people who already use that feature in other
databases.

What are other people's opinions?

Regards,
Dean

#24Wolfgang Walther
walther@technowledgy.de
In reply to: Dean Rasheed (#23)
Re: [PATCH] Add reloption for views to enable RLS

Dean Rasheed:

That is also the main reason I preferred naming it "security_invoker" -
it is consistent with other databases and eases transition from such
systems.

[...]

For my part, I find myself more and more convinced that
"security_invoker" is the right name, because it matches the
terminology used for functions, and in other database systems. I think
the parallels between security invoker functions and security invoker
views are quite strong.

[...]

When we come to write the release notes for this feature, saying that
this version of PG now supports security invoker views is going to
mean a lot more to people who already use that feature in other
databases.

What are other people's opinions?

All those points in favor of security_invoker are very good indeed. The
main objection was not the term invoker, though, but the implicit
association it creates as in "security_invoker=false behaves like
security definer". But this is clearly wrong, the "security definer"
semantics as used for functions or in other databases just don't apply
as the default in PG.

I think renaming the reloption was a shortcut to avoid that association,
while the best way to deal with that would be explicit documentation.
Meanwhile, the patch has added a mention about CURRENT_USER, so that's a
first step. Maybe an explicit mention that security_invoker=false, is
NOT the same as "security definer" and explaining why would already be
enough?

Best

Wolfgang

#25Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Dean Rasheed (#23)
Re: [PATCH] Add reloption for views to enable RLS

On Wed, 2022-03-02 at 10:10 +0000, Dean Rasheed wrote:

I kept "check_permissions_owner" for now. Constantly changing it around
with each iteration doesn't really bring any value IMHO, I'd rather have
a final consensus on how to name the option and *then* change it for good.

Yes indeed, it's annoying to keep changing the name between patch
versions, so let's try to get a consensus now.

For my part, I find myself more and more convinced that
"security_invoker" is the right name [...]

What are other people's opinions?

I am fine with "security_invoker". If there are other databases that use the
same term for the same thing, that is a strong argument.

I also agree that having "off" for the default setting is nicer.

My main worry is that other people misunderstand it in the same way that
Walter did, namely that this behaves just like security invoker functions.
But if the behavior is well documented, I think that is ok.

Yours,
Laurenz Albe

#26Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Dean Rasheed (#23)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

On 3/2/22 11:10, Dean Rasheed wrote:

For my part, I find myself more and more convinced that
"security_invoker" is the right name, because it matches the
terminology used for functions, and in other database systems. I think
the parallels between security invoker functions and security invoker
views are quite strong.

[..]

What are other people's opinions?

Since there don't seem to be any more objections to "security_invoker" I
attached v10 renaming it again.

I've tried to better clarify the whole invoker vs. definer thing in the
CREATE VIEW documentation by explicitly mentioning that
"security_invoker=false" is _not_ the same as "security definer", based
on the earlier discussions.

This should hopefully avoid any implicit associations.

Thanks,
Christoph

Attachments:

v10-0001-Add-new-boolean-reloption-security_invoker-to-vi.patchtext/x-patch; charset=UTF-8; name=v10-0001-Add-new-boolean-reloption-security_invoker-to-vi.patchDownload
From 89efb198694f7ff6e05068662a72a0bdcb43e13d Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Tue, 8 Mar 2022 17:54:46 +0100
Subject: [PATCH v10 1/1] Add new boolean reloption "security_invoker" to views

When this reloption is set to "true", all permissions on the underlying
relations will be checked against the invoking user rather than the view
owner.  The latter remains the default behavior.

Author: Christoph Heiss <christoph.heiss@cybertec.at>
Co-Author: Laurenz Albe <laurenz.albe@cybertec.at>
Reviewed-By: Laurenz Albe, Wolfgang Walther
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 doc/src/sgml/ref/alter_view.sgml              | 13 ++-
 doc/src/sgml/ref/create_view.sgml             | 76 +++++++++++++++---
 src/backend/access/common/reloptions.c        | 11 +++
 src/backend/rewrite/rewriteHandler.c          | 18 +++--
 src/backend/utils/cache/relcache.c            | 79 ++++++++++++-------
 src/include/utils/rel.h                       | 11 +++
 src/test/regress/expected/create_view.out     | 73 ++++++++++++++---
 src/test/regress/expected/rowsecurity.out     | 43 +++++++++-
 src/test/regress/expected/rules.out           | 20 +++++
 src/test/regress/expected/updatable_views.out | 28 +++++++
 src/test/regress/sql/create_view.sql          | 44 ++++++++++-
 src/test/regress/sql/rowsecurity.sql          | 26 ++++++
 src/test/regress/sql/rules.sql                | 23 ++++++
 src/test/regress/sql/updatable_views.sql      | 27 +++++++
 14 files changed, 430 insertions(+), 62 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..f6bb084117 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,11 +156,22 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes whether permission checks on the underlying relations are
+          performed as the view owner or as the invoking user.  Default is
+          <literal>false</literal>.  The value must be a Boolean value, such as
+          <literal>true</literal> or <literal>false</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..1f55379f73 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,23 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set to <literal>true</literal>, it will cause all
+          access to underlying tables to be checked as referenced by the
+          invoking user, otherwise as the view owner (default).  See below for
+          implementation details and quirks.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,13 +279,42 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
+    By default, access to relations referenced in the view is determined
+    by permissions of the view owner.  This can be used to provide secure
+    but restricted access to the underlying tables.  However, not all views
+    are secure against tampering; see <xref linkend="rules-privileges"/>
+    for details.
+   </para>
+
+   <para>
+    Functions called in the view are treated the same as if they had been
+    called directly from the query using the view.  Therefore, the user of
     a view must have permissions to call all functions used by the view.
+    This also means that functions are executed as the invoking user, not
+    the view owner.  In particular, <literal>CURRENT_USER</literal>
+    will always return the invoking user.  Note that
+    <literal>security_invoker</literal> set to <literal>false</literal>
+    is therefore <emphasis>not</emphasis> equivalent to
+    <literal>SECURITY DEFINER</literal> on functions and should not be confused.
+   </para>
+
+   <para>
+    If the <literal>security_invoker</literal> property is set to
+    <literal>true</literal> on a view, access to relations referenced in the
+    view is determined by permissions of the invoking user, rather than the
+    view owner.  If <link linkend="ddl-rowsecurity">row-level security</link>
+    is enabled on the referenced tables, policies are also invoked for the
+    invoking user.  This is useful if you want the view to behave just as if
+    the defining query had been used instead.
+   </para>
+
+   <para>
+    When creating (or replacing) a view, the user must have
+    <literal>CREATE</literal> privileges on the schema containing the view and
+    <literal>USAGE</literal> privileges on any schemas referred to in the view
+    query.  The view owner (or the invoking user if
+    <literal>security_invoker</literal> is set) only needs to have
+    <literal>USAGE</literal> privileges on the schema containing the view.
    </para>
 
    <para>
@@ -387,10 +430,17 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    <para>
     Note that the user performing the insert, update or delete on the view
     must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    view.
+   </para>
+
+   <para>
+    Additionally, by default the view's owner must have the relevant privileges
+    on the underlying base relations, but the user performing the update does
+    not need any permissions on the underlying base relations. (see
+    <xref linkend="rules-privileges"/>)  If the view has the
+    <literal>security_invoker</literal> property is set to
+    <literal>true</literal>, the invoking user will need to have the relevant
+    privileges rather than the view owner.
    </para>
   </refsect2>
  </refsect1>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..599e160ca6 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"security_invoker",
+			"Privileges on underlying relations are checked as the invoking user, not the view owner",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 3d82138cb3..303195fea4 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3242,18 +3242,24 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "security_invoker" set, mark the new target RTE
+	 * for the permissions checks that we want to enforce against the query
+	 * caller.  Otherwise we want to enforce them against the view owner.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationHasSecurityInvoker(view))
+		new_rte->checkAsUser = view_rte->checkAsUser;
+	else
+		new_rte->checkAsUser = view->rd_rel->relowner;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fccffce572..2bde343056 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -826,11 +826,19 @@ 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 that has the "security_invoker" relopt
+		 * set to true, we want the rule's table references to be checked as
+		 * the user invoking the rule by setting the checkAsUser field to
+		 * "InvalidOid".
+		 * This also should only apply for SELECT statements, thus check for
+		 * that too. The view might have some non-SELECT rules created using
+		 * CREATE RULE, but their actions should be independent of the view
+		 * definition.
+		 *
+		 * 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
@@ -838,8 +846,18 @@ 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 (rule->event == CMD_SELECT
+			&& relation->rd_rel->relkind == RELKIND_VIEW
+			&& RelationHasSecurityInvoker(relation))
+		{
+			setRuleCheckAsUser((Node *) rule->actions, InvalidOid);
+			setRuleCheckAsUser(rule->qual, InvalidOid);
+		}
+		else
+		{
+			setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+			setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+		}
 
 		if (numlocks >= maxlocks)
 		{
@@ -1164,27 +1182,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;
@@ -1213,9 +1210,33 @@ retry:
 	else
 		Assert(relation->rd_rel->relam == InvalidOid);
 
-	/* extract reloptions if any */
+	/*
+	 * Extract reloptions (if any) before fetching rules and triggers.
+	 * RelationBuildRuleLock() depends on having ->rd_options already set up.
+	 */
 	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/utils/rel.h b/src/include/utils/rel.h
index 3b4ab65ae2..67985e2ff8 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityInvoker
+ *		Returns true if the relation has the security_invoker property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index ae7c04353c..b2c28f9457 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -296,17 +296,31 @@ ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {security_invoker=true}
+ mysecview8 | v       | {security_invoker=false,security_barrier=true}
+ mysecview9 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -316,18 +330,50 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                   reloptions                   
+------------+---------+------------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
-
+ mysecview7 | v       | 
+ mysecview8 | v       | {security_invoker=true}
+ mysecview9 | v       | {security_invoker=false,security_barrier=true}
+(7 rows)
+
+-- Test chained views when using security_invoker
+CREATE TABLE ct1 (x int);
+CREATE VIEW cv1 WITH (security_invoker=false) AS
+SELECT * FROM ct1;
+CREATE VIEW cv2 WITH (security_invoker=true) AS
+SELECT * FROM cv1;
+CREATE ROLE alice NOLOGIN;
+GRANT USAGE ON SCHEMA testviewschm2 TO alice;
+GRANT SELECT ON cv1, cv2 TO alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+ x 
+---
+(0 rows)
+
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT ON cv1 FROM alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+ERROR:  permission denied for view cv1
+RESET SESSION AUTHORIZATION;
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
 CREATE VIEW unspecified_types AS
@@ -2039,7 +2085,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 74 other objects
+NOTICE:  drop cascades to 80 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2060,6 +2106,12 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
+drop cascades to table ct1
+drop cascades to view cv1
+drop cascades to view cv2
 drop cascades to view unspecified_types
 drop cascades to table tt1
 drop cascades to table tx1
@@ -2114,3 +2166,4 @@ drop cascades to view tt23v
 drop cascades to view tt24v
 drop cascades to view tt25v
 drop cascades to view tt26v
+DROP ROLE alice;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 89397e41f0..dec0ed5882 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_emily;
 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_emily 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_emily;
 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+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 security_invoker_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/expected/rules.out b/src/test/regress/expected/rules.out
index ac468568a1..cc95d1b267 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2823,6 +2823,10 @@ select * from only t1_2;
 (10 rows)
 
 reset constraint_exclusion;
+drop table t1 cascade;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table t1_1
+drop cascades to table t1_2
 -- test FOR UPDATE in rules
 create table rules_base(f1 int, f2 int);
 insert into rules_base values(1,2), (11,12);
@@ -3496,3 +3500,19 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+-- Test rules on views with security_invoker set
+CREATE ROLE role1;
+CREATE TABLE t1 (x int);
+CREATE TABLE t2 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+GRANT INSERT ON t1, v1 TO role1;
+CREATE RULE testrule1 AS ON INSERT TO v1
+DO INSTEAD INSERT INTO t2 VALUES (NEW.*);
+SET ROLE role1;
+INSERT INTO v1 VALUES (1);
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t2;
+DROP TABLE t1;
+DROP ROLE role1;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index cdff914b93..622820047b 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2578,6 +2578,34 @@ DROP VIEW v2;
 DROP VIEW v1;
 DROP TABLE t2;
 DROP TABLE t1;
+-- Check UPDATE/INSERT/DELETE on views with security_invoker set
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role1;
+CREATE ROLE regress_role2;
+CREATE TABLE t1 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+ALTER VIEW v1 OWNER TO regress_role1;
+GRANT INSERT, UPDATE, DELETE ON t1 TO regress_role1;
+GRANT SELECT ON t1 TO regress_role2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1 TO regress_role2;
+SET SESSION AUTHORIZATION regress_role2;
+SELECT * FROM v1;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1 values (1);
+ERROR:  permission denied for table t1
+UPDATE v1 SET x = 2;
+ERROR:  permission denied for table t1
+DELETE FROM v1;
+ERROR:  permission denied for table t1
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t1;
+DROP ROLE regress_role2;
+DROP ROLE regress_role1;
 --
 -- Test CREATE OR REPLACE VIEW turning a non-updatable view into an
 -- auto-updatable view and adding check options in a single step
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index 829f3ddbe6..84ca080ca9 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -254,9 +254,19 @@ CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -267,11 +277,40 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
+-- Test chained views when using security_invoker
+CREATE TABLE ct1 (x int);
+CREATE VIEW cv1 WITH (security_invoker=false) AS
+SELECT * FROM ct1;
+CREATE VIEW cv2 WITH (security_invoker=true) AS
+SELECT * FROM cv1;
+
+CREATE ROLE alice NOLOGIN;
+GRANT USAGE ON SCHEMA testviewschm2 TO alice;
+GRANT SELECT ON cv1, cv2 TO alice;
+
+SET ROLE alice;
+SELECT * FROM cv2;
+
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT ON cv1 FROM alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+
+RESET SESSION AUTHORIZATION;
+
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
 
@@ -722,3 +761,4 @@ select pg_get_viewdef('tt26v', true);
 -- clean up all the random objects we made above
 DROP SCHEMA temp_view_test CASCADE;
 DROP SCHEMA testviewschm2 CASCADE;
+DROP ROLE alice;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 44deb42bad..2e294cc775 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_emily;
 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_emily 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_emily;
 
 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
 --
 -- Table inheritance and RLS policy
 --
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index 8bdab6dec3..d12e915107 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -991,6 +991,7 @@ select * from only t1_1;
 select * from only t1_2;
 
 reset constraint_exclusion;
+drop table t1 cascade;
 
 -- test FOR UPDATE in rules
 
@@ -1257,3 +1258,25 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+
+-- Test rules on views with security_invoker set
+CREATE ROLE role1;
+
+CREATE TABLE t1 (x int);
+CREATE TABLE t2 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+GRANT INSERT ON t1, v1 TO role1;
+
+CREATE RULE testrule1 AS ON INSERT TO v1
+DO INSTEAD INSERT INTO t2 VALUES (NEW.*);
+
+SET ROLE role1;
+
+INSERT INTO v1 VALUES (1);
+
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t2;
+DROP TABLE t1;
+DROP ROLE role1;
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index 09328e582b..a00d5dabf7 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1226,6 +1226,33 @@ DROP VIEW v1;
 DROP TABLE t2;
 DROP TABLE t1;
 
+-- Check UPDATE/INSERT/DELETE on views with security_invoker set
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role1;
+CREATE ROLE regress_role2;
+
+CREATE TABLE t1 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+ALTER VIEW v1 OWNER TO regress_role1;
+
+GRANT INSERT, UPDATE, DELETE ON t1 TO regress_role1;
+GRANT SELECT ON t1 TO regress_role2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1 TO regress_role2;
+
+SET SESSION AUTHORIZATION regress_role2;
+
+SELECT * FROM v1;
+INSERT INTO v1 values (1);
+UPDATE v1 SET x = 2;
+DELETE FROM v1;
+
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t1;
+DROP ROLE regress_role2;
+DROP ROLE regress_role1;
+
 --
 -- Test CREATE OR REPLACE VIEW turning a non-updatable view into an
 -- auto-updatable view and adding check options in a single step
-- 
2.35.1

#27Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Christoph Heiss (#26)
Re: [PATCH] Add reloption for views to enable RLS

On Tue, 2022-03-08 at 18:17 +0100, Christoph Heiss wrote:

Since there don't seem to be any more objections to "security_invoker" I
attached v10 renaming it again.

I've tried to better clarify the whole invoker vs. definer thing in the
CREATE VIEW documentation by explicitly mentioning that
"security_invoker=false" is _not_ the same as "security definer", based
on the earlier discussions.

This should hopefully avoid any implicit associations.

I have only some minor comments:

--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -387,10 +430,17 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
<para>
Note that the user performing the insert, update or delete on the view
must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    view.
+   </para>
+
+   <para>
+    Additionally, by default the view's owner must have the relevant privileges
+    on the underlying base relations, but the user performing the update does
+    not need any permissions on the underlying base relations. (see
+    <xref linkend="rules-privileges"/>)  If the view has the
+    <literal>security_invoker</literal> property is set to
+    <literal>true</literal>, the invoking user will need to have the relevant
+    privileges rather than the view owner.
</para>
</refsect2>
</refsect1>

This paragraph contains a couple of grammatical errors.
How about

<para>
Note that the user performing the insert, update or delete on the view
must have the corresponding insert, update or delete privilege on the
view. Unless <literal>security_invoker</literal> is set to
<literal>true</literal>, the view's owner must additionally have the
relevant privileges on the underlying base relations, but the user
performing the update does not need any permissions on the underlying
base relations (see <xref linkend="rules-privileges"/>).
If <literal>security_invoker</literal> is set to <literal>true</literal>,
it is the invoking user rather than the view owner that must have the
relevant privileges on the underlying base relations.
</para>

Also, this:

--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -838,8 +846,18 @@ 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 (rule->event == CMD_SELECT
+           && relation->rd_rel->relkind == RELKIND_VIEW
+           && RelationHasSecurityInvoker(relation))
+       {
+           setRuleCheckAsUser((Node *) rule->actions, InvalidOid);
+           setRuleCheckAsUser(rule->qual, InvalidOid);
+       }
+       else
+       {
+           setRuleCheckAsUser((Node *) rule->actions, relation->rd_rel->relowner);
+           setRuleCheckAsUser(rule->qual, relation->rd_rel->relowner);
+       }

could be written like this (introducing a new variable):

if (rule->event == CMD_SELECT
&& relation->rd_rel->relkind == RELKIND_VIEW
&& RelationHasSecurityInvoker(relation))
user_for_check = InvalidOid;
else
user_for_check = relation->rd_rel->relowner;

setRuleCheckAsUser((Node *) rule->actions, user_for_check);
setRuleCheckAsUser(rule->qual, user_for_check);

This might be easier to read.

Yours,
Laurenz Albe

#28Christoph Heiss
christoph.heiss@cybertec.at
In reply to: Laurenz Albe (#27)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

On 3/9/22 16:06, Laurenz Albe wrote:

This paragraph contains a couple of grammatical errors.
How about

<para>
Note that the user performing the insert, update or delete on the view
must have the corresponding insert, update or delete privilege on the
view. Unless <literal>security_invoker</literal> is set to
<literal>true</literal>, the view's owner must additionally have the
relevant privileges on the underlying base relations, but the user
performing the update does not need any permissions on the underlying
base relations (see <xref linkend="rules-privileges"/>).
If <literal>security_invoker</literal> is set to <literal>true</literal>,
it is the invoking user rather than the view owner that must have the
relevant privileges on the underlying base relations.
</para>

Replaced the two paragraphs with your suggestion, it is indeed easier to
read.

Also, this:

[..]

could be written like this (introducing a new variable):

if (rule->event == CMD_SELECT
&& relation->rd_rel->relkind == RELKIND_VIEW
&& RelationHasSecurityInvoker(relation))
user_for_check = InvalidOid;
else
user_for_check = relation->rd_rel->relowner;

setRuleCheckAsUser((Node *) rule->actions, user_for_check);
setRuleCheckAsUser(rule->qual, user_for_check);

This might be easier to read.

Makes sense, I've changed that. This also seems to be more in line with
all the other code.
While at it I also split the comment alongside it to match, hopefully
that makes sense.

Thanks,
Christoph Heiss

Attachments:

v11-0001-Add-new-boolean-reloption-security_invoker-to-vi.patchtext/x-patch; charset=UTF-8; name=v11-0001-Add-new-boolean-reloption-security_invoker-to-vi.patchDownload
From 9837e98f31fdbf1bcd1929309a14b1fd99fd6ec1 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.heiss@cybertec.at>
Date: Mon, 14 Mar 2022 13:32:28 +0100
Subject: [PATCH v11 1/1] Add new boolean reloption "security_invoker" to views

When this reloption is set to "true", all permissions on the underlying
relations will be checked against the invoking user rather than the view
owner.  The latter remains the default behavior.

Author: Christoph Heiss <christoph.heiss@cybertec.at>
Co-Author: Laurenz Albe <laurenz.albe@cybertec.at>
Reviewed-By: Laurenz Albe, Wolfgang Walther
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
Signed-off-by: Christoph Heiss <christoph.heiss@cybertec.at>
---
 doc/src/sgml/ref/alter_view.sgml              | 13 ++-
 doc/src/sgml/ref/create_view.sgml             | 77 ++++++++++++++----
 src/backend/access/common/reloptions.c        | 11 +++
 src/backend/rewrite/rewriteHandler.c          | 18 +++--
 src/backend/utils/cache/relcache.c            | 80 ++++++++++++-------
 src/include/utils/rel.h                       | 11 +++
 src/test/regress/expected/create_view.out     | 73 ++++++++++++++---
 src/test/regress/expected/rowsecurity.out     | 43 +++++++++-
 src/test/regress/expected/rules.out           | 20 +++++
 src/test/regress/expected/updatable_views.out | 28 +++++++
 src/test/regress/sql/create_view.sql          | 44 +++++++++-
 src/test/regress/sql/rowsecurity.sql          | 26 ++++++
 src/test/regress/sql/rules.sql                | 23 ++++++
 src/test/regress/sql/updatable_views.sql      | 27 +++++++
 14 files changed, 430 insertions(+), 64 deletions(-)

diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
index 98c312c5bf..f6bb084117 100644
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,11 +156,22 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
        </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes whether permission checks on the underlying relations are
+          performed as the view owner or as the invoking user.  Default is
+          <literal>false</literal>.  The value must be a Boolean value, such as
+          <literal>true</literal> or <literal>false</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..29e2b4a480 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,23 @@ CREATE VIEW [ <replaceable>schema</replaceable> . ] <replaceable>view_name</repl
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          If this option is set to <literal>true</literal>, it will cause all
+          access to underlying tables to be checked as referenced by the
+          invoking user, otherwise as the view owner (default).  See below for
+          implementation details and quirks.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,13 +279,42 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
+    By default, access to relations referenced in the view is determined
+    by permissions of the view owner.  This can be used to provide secure
+    but restricted access to the underlying tables.  However, not all views
+    are secure against tampering; see <xref linkend="rules-privileges"/>
+    for details.
+   </para>
+
+   <para>
+    Functions called in the view are treated the same as if they had been
+    called directly from the query using the view.  Therefore, the user of
     a view must have permissions to call all functions used by the view.
+    This also means that functions are executed as the invoking user, not
+    the view owner.  In particular, <literal>CURRENT_USER</literal>
+    will always return the invoking user.  Note that
+    <literal>security_invoker</literal> set to <literal>false</literal>
+    is therefore <emphasis>not</emphasis> equivalent to
+    <literal>SECURITY DEFINER</literal> on functions and should not be confused.
+   </para>
+
+   <para>
+    If the <literal>security_invoker</literal> property is set to
+    <literal>true</literal> on a view, access to relations referenced in the
+    view is determined by permissions of the invoking user, rather than the
+    view owner.  If <link linkend="ddl-rowsecurity">row-level security</link>
+    is enabled on the referenced tables, policies are also invoked for the
+    invoking user.  This is useful if you want the view to behave just as if
+    the defining query had been used instead.
+   </para>
+
+   <para>
+    When creating (or replacing) a view, the user must have
+    <literal>CREATE</literal> privileges on the schema containing the view and
+    <literal>USAGE</literal> privileges on any schemas referred to in the view
+    query.  The view owner (or the invoking user if
+    <literal>security_invoker</literal> is set) only needs to have
+    <literal>USAGE</literal> privileges on the schema containing the view.
    </para>
 
    <para>
@@ -385,12 +428,16 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
    </para>
 
    <para>
-    Note that the user performing the insert, update or delete on the view
-    must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    Note that the user performing the insert, update or delete on the view must
+    have the corresponding insert, update or delete privilege on the view.
+    Unless <literal>security_invoker</literal> is set to
+    <literal>true</literal>, the view's owner must additionally have the
+    relevant privileges on the underlying base relations, but the user
+    performing the update does not need any permissions on the underlying base
+    relations (see <xref linkend="rules-privileges"/>). If
+    <literal>security_invoker</literal> is set to <literal>true</literal>, it
+    is the invoking user rather than the view owner that must have the relevant
+    privileges on the underlying base relations.
    </para>
   </refsect2>
  </refsect1>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d592655258..599e160ca6 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -140,6 +140,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
+	{
+		{
+			"security_invoker",
+			"Privileges on underlying relations are checked as the invoking user, not the view owner",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
 	{
 		{
 			"vacuum_truncate",
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool validate)
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 3d82138cb3..303195fea4 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3242,18 +3242,24 @@ rewriteTargetView(Query *parsetree, Relation view)
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "security_invoker" set, mark the new target RTE
+	 * for the permissions checks that we want to enforce against the query
+	 * caller.  Otherwise we want to enforce them against the view owner.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationHasSecurityInvoker(view))
+		new_rte->checkAsUser = view_rte->checkAsUser;
+	else
+		new_rte->checkAsUser = view->rd_rel->relowner;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fccffce572..cd6e95eb22 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -787,6 +787,7 @@ RelationBuildRuleLock(Relation relation)
 		Datum		rule_datum;
 		char	   *rule_str;
 		RewriteRule *rule;
+		Oid			check_as_user;
 
 		rule = (RewriteRule *) MemoryContextAlloc(rulescxt,
 												  sizeof(RewriteRule));
@@ -826,11 +827,29 @@ 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 that has the "security_invoker" relopt
+		 * set to true, we want the rule's table references to be checked as
+		 * the user invoking the rule by setting the checkAsUser field to
+		 * "InvalidOid".
+		 * This also should only apply for SELECT statements, thus check for
+		 * that too. The view might have some non-SELECT rules created using
+		 * CREATE RULE, but their actions should be independent of the view
+		 * definition.
+		 *
+		 * In all other cases, we want the rule's table references to be
+		 * checked as though by the table owner.
+		 */
+		if (rule->event == CMD_SELECT
+			&& relation->rd_rel->relkind == RELKIND_VIEW
+			&& RelationHasSecurityInvoker(relation))
+			check_as_user = InvalidOid;
+		else
+			check_as_user = relation->rd_rel->relowner;
+
+		/*
+		 * 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
@@ -838,8 +857,8 @@ 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);
+		setRuleCheckAsUser((Node *) rule->actions, check_as_user);
+		setRuleCheckAsUser(rule->qual, check_as_user);
 
 		if (numlocks >= maxlocks)
 		{
@@ -1164,27 +1183,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;
@@ -1213,9 +1211,33 @@ retry:
 	else
 		Assert(relation->rd_rel->relam == InvalidOid);
 
-	/* extract reloptions if any */
+	/*
+	 * Extract reloptions (if any) before fetching rules and triggers.
+	 * RelationBuildRuleLock() depends on having ->rd_options already set up.
+	 */
 	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/utils/rel.h b/src/include/utils/rel.h
index 3b4ab65ae2..67985e2ff8 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -411,6 +412,16 @@ typedef struct ViewOptions
 	 (relation)->rd_options ?												\
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
+/*
+ * RelationHasSecurityInvoker
+ *		Returns true if the relation has the security_invoker property set, or
+ *		not.  Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
 /*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index ae7c04353c..b2c28f9457 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -296,17 +296,31 @@ ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {security_invoker=true}
+ mysecview8 | v       | {security_invoker=false,security_barrier=true}
+ mysecview9 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -316,18 +330,50 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                   reloptions                   
+------------+---------+------------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
-
+ mysecview7 | v       | 
+ mysecview8 | v       | {security_invoker=true}
+ mysecview9 | v       | {security_invoker=false,security_barrier=true}
+(7 rows)
+
+-- Test chained views when using security_invoker
+CREATE TABLE ct1 (x int);
+CREATE VIEW cv1 WITH (security_invoker=false) AS
+SELECT * FROM ct1;
+CREATE VIEW cv2 WITH (security_invoker=true) AS
+SELECT * FROM cv1;
+CREATE ROLE alice NOLOGIN;
+GRANT USAGE ON SCHEMA testviewschm2 TO alice;
+GRANT SELECT ON cv1, cv2 TO alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+ x 
+---
+(0 rows)
+
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT ON cv1 FROM alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+ERROR:  permission denied for view cv1
+RESET SESSION AUTHORIZATION;
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
 CREATE VIEW unspecified_types AS
@@ -2039,7 +2085,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 74 other objects
+NOTICE:  drop cascades to 80 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2060,6 +2106,12 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
+drop cascades to table ct1
+drop cascades to view cv1
+drop cascades to view cv2
 drop cascades to view unspecified_types
 drop cascades to table tt1
 drop cascades to table tx1
@@ -2114,3 +2166,4 @@ drop cascades to view tt23v
 drop cascades to view tt24v
 drop cascades to view tt25v
 drop cascades to view tt26v
+DROP ROLE alice;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 89397e41f0..dec0ed5882 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_emily;
 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_emily 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_emily;
 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+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 security_invoker_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/expected/rules.out b/src/test/regress/expected/rules.out
index ac468568a1..cc95d1b267 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2823,6 +2823,10 @@ select * from only t1_2;
 (10 rows)
 
 reset constraint_exclusion;
+drop table t1 cascade;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table t1_1
+drop cascades to table t1_2
 -- test FOR UPDATE in rules
 create table rules_base(f1 int, f2 int);
 insert into rules_base values(1,2), (11,12);
@@ -3496,3 +3500,19 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+-- Test rules on views with security_invoker set
+CREATE ROLE role1;
+CREATE TABLE t1 (x int);
+CREATE TABLE t2 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+GRANT INSERT ON t1, v1 TO role1;
+CREATE RULE testrule1 AS ON INSERT TO v1
+DO INSTEAD INSERT INTO t2 VALUES (NEW.*);
+SET ROLE role1;
+INSERT INTO v1 VALUES (1);
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t2;
+DROP TABLE t1;
+DROP ROLE role1;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index cdff914b93..622820047b 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2578,6 +2578,34 @@ DROP VIEW v2;
 DROP VIEW v1;
 DROP TABLE t2;
 DROP TABLE t1;
+-- Check UPDATE/INSERT/DELETE on views with security_invoker set
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role1;
+CREATE ROLE regress_role2;
+CREATE TABLE t1 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+ALTER VIEW v1 OWNER TO regress_role1;
+GRANT INSERT, UPDATE, DELETE ON t1 TO regress_role1;
+GRANT SELECT ON t1 TO regress_role2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1 TO regress_role2;
+SET SESSION AUTHORIZATION regress_role2;
+SELECT * FROM v1;
+ x 
+---
+(0 rows)
+
+INSERT INTO v1 values (1);
+ERROR:  permission denied for table t1
+UPDATE v1 SET x = 2;
+ERROR:  permission denied for table t1
+DELETE FROM v1;
+ERROR:  permission denied for table t1
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t1;
+DROP ROLE regress_role2;
+DROP ROLE regress_role1;
 --
 -- Test CREATE OR REPLACE VIEW turning a non-updatable view into an
 -- auto-updatable view and adding check options in a single step
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index 829f3ddbe6..84ca080ca9 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -254,9 +254,19 @@ CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -267,11 +277,40 @@ 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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
+-- Test chained views when using security_invoker
+CREATE TABLE ct1 (x int);
+CREATE VIEW cv1 WITH (security_invoker=false) AS
+SELECT * FROM ct1;
+CREATE VIEW cv2 WITH (security_invoker=true) AS
+SELECT * FROM cv1;
+
+CREATE ROLE alice NOLOGIN;
+GRANT USAGE ON SCHEMA testviewschm2 TO alice;
+GRANT SELECT ON cv1, cv2 TO alice;
+
+SET ROLE alice;
+SELECT * FROM cv2;
+
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT ON cv1 FROM alice;
+SET ROLE alice;
+SELECT * FROM cv2;
+
+RESET SESSION AUTHORIZATION;
+
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
 
@@ -722,3 +761,4 @@ select pg_get_viewdef('tt26v', true);
 -- clean up all the random objects we made above
 DROP SCHEMA temp_view_test CASCADE;
 DROP SCHEMA testviewschm2 CASCADE;
+DROP ROLE alice;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 44deb42bad..2e294cc775 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_emily;
 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_emily 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_emily;
 
 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_invoker 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_invoker=true) AS
+SELECT * FROM category;
+CREATE OR REPLACE FUNCTION security_invoker_func() RETURNS SETOF category
+    AS 'SELECT * FROM category'
+    LANGUAGE SQL STABLE STRICT;
+CREATE VIEW v1f WITH (security_invoker=true) AS
+SELECT * FROM security_invoker_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_emily;
+SELECT * FROM category;
+SELECT * FROM v1;
+SELECT * FROM v1f;
+
 --
 -- Table inheritance and RLS policy
 --
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index 8bdab6dec3..d12e915107 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -991,6 +991,7 @@ select * from only t1_1;
 select * from only t1_2;
 
 reset constraint_exclusion;
+drop table t1 cascade;
 
 -- test FOR UPDATE in rules
 
@@ -1257,3 +1258,25 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+
+-- Test rules on views with security_invoker set
+CREATE ROLE role1;
+
+CREATE TABLE t1 (x int);
+CREATE TABLE t2 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+GRANT INSERT ON t1, v1 TO role1;
+
+CREATE RULE testrule1 AS ON INSERT TO v1
+DO INSTEAD INSERT INTO t2 VALUES (NEW.*);
+
+SET ROLE role1;
+
+INSERT INTO v1 VALUES (1);
+
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t2;
+DROP TABLE t1;
+DROP ROLE role1;
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index 09328e582b..a00d5dabf7 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1226,6 +1226,33 @@ DROP VIEW v1;
 DROP TABLE t2;
 DROP TABLE t1;
 
+-- Check UPDATE/INSERT/DELETE on views with security_invoker set
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role1;
+CREATE ROLE regress_role2;
+
+CREATE TABLE t1 (x int);
+CREATE VIEW v1 WITH (security_invoker=true) AS
+SELECT * FROM t1;
+ALTER VIEW v1 OWNER TO regress_role1;
+
+GRANT INSERT, UPDATE, DELETE ON t1 TO regress_role1;
+GRANT SELECT ON t1 TO regress_role2;
+GRANT SELECT, INSERT, UPDATE, DELETE ON v1 TO regress_role2;
+
+SET SESSION AUTHORIZATION regress_role2;
+
+SELECT * FROM v1;
+INSERT INTO v1 values (1);
+UPDATE v1 SET x = 2;
+DELETE FROM v1;
+
+RESET SESSION AUTHORIZATION;
+DROP VIEW v1;
+DROP TABLE t1;
+DROP ROLE regress_role2;
+DROP ROLE regress_role1;
+
 --
 -- Test CREATE OR REPLACE VIEW turning a non-updatable view into an
 -- auto-updatable view and adding check options in a single step
-- 
2.35.1

#29Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Christoph Heiss (#28)
Re: [PATCH] Add reloption for views to enable RLS

On Mon, 2022-03-14 at 13:40 +0100, Christoph Heiss wrote:

On 3/9/22 16:06, Laurenz Albe wrote:

This paragraph contains a couple of grammatical errors.

Replaced the two paragraphs with your suggestion, it is indeed easier to
read.

Also, this:
could be written like this (introducing a new variable):

   if (rule->event == CMD_SELECT
       && relation->rd_rel->relkind == RELKIND_VIEW
       && RelationHasSecurityInvoker(relation))
       user_for_check = InvalidOid;
   else
       user_for_check = relation->rd_rel->relowner;

   setRuleCheckAsUser((Node *) rule->actions, user_for_check);
   setRuleCheckAsUser(rule->qual, user_for_check);

This might be easier to read.

Makes sense, I've changed that. This also seems to be more in line with
all the other code.
While at it I also split the comment alongside it to match, hopefully
that makes sense.

The patch is fine from my point of view.

It passes "make check-world".

I'll mark it as "ready for committer".

Yours,
Laurenz Albe

#30Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Laurenz Albe (#29)
1 attachment(s)
Re: [PATCH] Add reloption for views to enable RLS

On Mon, 14 Mar 2022 at 16:16, Laurenz Albe <laurenz.albe@cybertec.at> wrote:

The patch is fine from my point of view.

It passes "make check-world".

I'll mark it as "ready for committer".

Cool, thanks. I think this will make a useful addition to PG15.

I have been hacking on it a bit, and attached is an updated version.
Aside from some general copy editing, the most notable changes are:

In the updatable_views tests, I have moved the new tests to
immediately after the existing permission checking tests, which seems
like a more logical place to put them, and modified them to use the
same style as those existing tests. IMO, this test style makes the
task of writing tests simpler, since the expected output is a little
more obvious.

Similarly in the rowsecurity tests, I have moved the new tests to
immediately after the existing tests for RLS policies on tables
accessed via views, and added a few new tests in the same style,
including verifying permission checks on relations in subqueries in
RLS policies, when the table is accessed via a view.

I wasn't happy with the overall level of test coverage for this new
feature, so I have expanded on them quite a bit. This includes tests
for a bug in rewriteTargetView() -- it wasn't consistently handling
the case of an update involving an ordinary view on top of a security
invoker view.

I have added explicit documentation for the fact that a security
invoker view always does permission checks as the current user, even
if it is accessed from a non-security invoker view, since that was the
cause of some discussion on this thread.

I've also added some more detailed documentation describing how all
this affects RLS, since that's likely to be a common use case.

I've done a fairly extensive doc search, and I *think* I've identified
all the other places that needed updating.

One additional thing that had been missed was that the LOCK command
can be used to lock views, which includes locking all underlying base
relations, after checking permissions as the view owner. The
logical/consistent thing to do for security invoker views is to do the
permission checks as the invoking user, so I've done that.

Barring any other comments or objections, I'll push this in a couple
of days or so, after a bit more proof-reading.

Regards,
Dean

Attachments:

v12-security-invoker-views.patchtext/x-patch; charset=US-ASCII; name=v12-security-invoker-views.patchDownload
diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml
new file mode 100644
index 98c312c..8bdc90a
--- a/doc/src/sgml/ref/alter_view.sgml
+++ b/doc/src/sgml/ref/alter_view.sgml
@@ -156,7 +156,17 @@ ALTER VIEW [ IF EXISTS ] <replaceable cl
         <listitem>
          <para>
           Changes the security-barrier property of the view.  The value must
-          be Boolean value, such as <literal>true</literal>
+          be a Boolean value, such as <literal>true</literal>
+          or <literal>false</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          Changes the security-invoker property of the view.  The value must
+          be a Boolean value, such as <literal>true</literal>
           or <literal>false</literal>.
          </para>
         </listitem>
diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
new file mode 100644
index 9f53206..f898b7a
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -608,7 +608,9 @@ AND
    This does not change how views
    work, however.  As with normal queries and views, permission checks and
    policies for the tables which are referenced by a view will use the view
-   owner's rights and any policies which apply to the view owner.
+   owner's rights and any policies which apply to the view owner, except if
+   the view is defined using the <literal>security_invoker</literal> option
+   (see <link linkend="sql-createview"><command>CREATE VIEW</command></link>).
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index bf03287..72a92bb
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -137,8 +137,6 @@ CREATE VIEW [ <replaceable>schema</repla
           This parameter may be either <literal>local</literal> or
           <literal>cascaded</literal>, and is equivalent to specifying
           <literal>WITH [ CASCADED | LOCAL ] CHECK OPTION</literal> (see below).
-          This option can be changed on existing views using <link
-          linkend="sql-alterview"><command>ALTER VIEW</command></link>.
          </para>
         </listitem>
        </varlistentry>
@@ -152,7 +150,22 @@ CREATE VIEW [ <replaceable>schema</repla
          </para>
         </listitem>
        </varlistentry>
-      </variablelist></para>
+
+       <varlistentry>
+        <term><literal>security_invoker</literal> (<type>boolean</type>)</term>
+        <listitem>
+         <para>
+          This option causes the underlying base relations to be checked
+          against the privileges of the user of the view rather than the view
+          owner.  See the notes below for full details.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+
+      All of the above options can be changed on existing views using <link
+      linkend="sql-alterview"><command>ALTER VIEW</command></link>.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -265,18 +278,74 @@ CREATE VIEW vista AS SELECT text 'Hello
    </para>
 
    <para>
-    Access to tables referenced in the view is determined by permissions of
-    the view owner.  In some cases, this can be used to provide secure but
-    restricted access to the underlying tables.  However, not all views are
-    secure against tampering; see <xref linkend="rules-privileges"/> for
-    details.  Functions called in the view are treated the same as if they had
-    been called directly from the query using the view.  Therefore the user of
+    By default, access to the underlying base relations referenced in the view
+    is determined by the permissions of the view owner.  In some cases, this
+    can be used to provide secure but restricted access to the underlying
+    tables.  However, not all views are secure against tampering; see <xref
+    linkend="rules-privileges"/> for details.
+   </para>
+
+   <para>
+    If the view has the <literal>security_invoker</literal> property set to
+    <literal>true</literal>, access to the underlying base relations is
+    determined by the permissions of the user executing the query, rather than
+    the view owner.  Thus, the user of a security invoker view must have the
+    relevant permissions on the view and its underlying base relations.
+   </para>
+
+   <para>
+    If any of the underlying base relations is a security invoker view, it
+    will be treated as if it had been accessed directly from the original
+    query.  Thus, a security invoker view will always check its underlying
+    base relations using the permissions of the current user, even if it is
+    accessed from a view without the <literal>security_invoker</literal>
+    property.
+   </para>
+
+   <para>
+    If any of the underlying base relations has
+    <link linkend="ddl-rowsecurity">row-level security</link> enabled, then
+    by default, the row-level security policies of the view owner are applied,
+    and access to any additional relations referred to by those policies is
+    determined by the permissions of the view owner.  However, if the view has
+    <literal>security_invoker</literal> set to <literal>true</literal>, then
+    the policies and permissions of the invoking user are used instead, as if
+    the base relations had been referenced directly from the query using the
+    view.
+   </para>
+
+   <para>
+    Functions called in the view are treated the same as if they had been
+    called directly from the query using the view.  Therefore, the user of
     a view must have permissions to call all functions used by the view.
+    Functions in the view are executed with the privileges of the user
+    executing the query or the function owner, depending on whether the
+    functions are defined as <literal>SECURITY INVOKER</literal> or
+    <literal>SECURITY DEFINER</literal>.  Thus, for example, calling
+    <literal>CURRENT_USER</literal> directly in a view will always return the
+    invoking user, not the view owner.  This is not affected by the view's
+    <literal>security_invoker</literal> setting, and so a view with
+    <literal>security_invoker</literal> set to <literal>false</literal> is
+    <emphasis>not</emphasis> equivalent to a
+    <literal>SECURITY DEFINER</literal> function and those concepts should not
+    be confused.
    </para>
 
    <para>
-    When <command>CREATE OR REPLACE VIEW</command> is used on an
-    existing view, only the view's defining SELECT rule is changed.
+    The user creating or replacing a view must have <literal>USAGE</literal>
+    privileges on any schemas referred to in the view query, in order to look
+    up the referenced objects in those schemas.  Note, however, that this
+    lookup only happens when the view is created or replaced.  Therefore, the
+    user of the view only requires the <literal>USAGE</literal> privilege on
+    the schema containing the view, not on the schemas referred to in the view
+    query, even for a security invoker view.
+   </para>
+
+   <para>
+    When <command>CREATE OR REPLACE VIEW</command> is used on an existing
+    view, only the view's defining SELECT rule plus any
+    <literal>WITH ( ... )</literal> parameters and its
+    <literal>CHECK OPTION</literal> are changed.
     Other view properties, including ownership, permissions, and non-SELECT
     rules, remain unchanged.  You must own the view
     to replace it (this includes being a member of the owning role).
@@ -387,10 +456,13 @@ CREATE VIEW vista AS SELECT text 'Hello
    <para>
     Note that the user performing the insert, update or delete on the view
     must have the corresponding insert, update or delete privilege on the
-    view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the update does
-    not need any permissions on the underlying base relations (see
-    <xref linkend="rules-privileges"/>).
+    view.  In addition, by default, the view's owner must have the relevant
+    privileges on the underlying base relations, whereas the user performing
+    the update does not need any permissions on the underlying base relations
+    (see <xref linkend="rules-privileges"/>).  However, if the view has
+    <literal>security_invoker</literal> set to <literal>true</literal>, the
+    user performing the update, rather than the view owner, must have the
+    relevant privileges on the underlying base relations.
    </para>
   </refsect2>
  </refsect1>
@@ -486,7 +558,8 @@ UNION ALL
    <command>CREATE OR REPLACE VIEW</command> is a
    <productname>PostgreSQL</productname> language extension.
    So is the concept of a temporary view.
-   The <literal>WITH ( ... )</literal> clause is an extension as well.
+   The <literal>WITH ( ... )</literal> clause is an extension as well, as are
+   security barrier views and security invoker views.
   </para>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/lock.sgml b/doc/src/sgml/ref/lock.sgml
new file mode 100644
index 4cdfae2..19e7194
--- a/doc/src/sgml/ref/lock.sgml
+++ b/doc/src/sgml/ref/lock.sgml
@@ -174,10 +174,15 @@ LOCK [ TABLE ] [ ONLY ] <replaceable cla
    </para>
 
    <para>
-    The user performing the lock on the view must have the corresponding privilege
-    on the view.  In addition the view's owner must have the relevant privileges on
-    the underlying base relations, but the user performing the lock does
-    not need any permissions on the underlying base relations.
+    The user performing the lock on the view must have the corresponding
+    privilege on the view.  In addition, by default, the view's owner must
+    have the relevant privileges on the underlying base relations, whereas the
+    user performing the lock does not need any permissions on the underlying
+    base relations.  However, if the view has
+    <literal>security_invoker</literal> set to <literal>true</literal>
+    (see <link linkend="sql-createview"><command>CREATE VIEW</command></link>),
+    the user performing the lock, rather than the view owner, must have the
+    relevant privileges on the underlying base relations.
    </para>
 
    <para>
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
new file mode 100644
index 4aa4e00..4b2ba5a
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -2007,11 +2007,14 @@ SELECT * FROM shoelace;
     a relation (table or view) is automatically the owner of the
     rewrite rules that are defined for it.
     The <productname>PostgreSQL</productname> rule system changes the
-    behavior of the default access control system. Relations that
-    are used due to rules get checked against the
+    behavior of the default access control system. With the exception of
+    <literal>SELECT</literal> rules associated with security invoker views
+    (see <link linkend="sql-createview"><command>CREATE VIEW</command></link>),
+    all relations that are used due to rules get checked against the
     privileges of the rule owner, not the user invoking the rule.
-    This means that users only need the required privileges
-    for the tables/views that are explicitly named in their queries.
+    This means that, except for security invoker views, users only need the
+    required privileges for the tables/views that are explicitly named in
+    their queries.
 </para>
 
 <para>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
new file mode 100644
index d592655..599e160
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -142,6 +142,15 @@ static relopt_bool boolRelOpts[] =
 	},
 	{
 		{
+			"security_invoker",
+			"Privileges on underlying relations are checked as the invoking user, not the view owner",
+			RELOPT_KIND_VIEW,
+			AccessExclusiveLock
+		},
+		false
+	},
+	{
+		{
 			"vacuum_truncate",
 			"Enables vacuum to truncate empty pages at the end of this table",
 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
@@ -1996,6 +2005,8 @@ view_reloptions(Datum reloptions, bool v
 	static const relopt_parse_elt tab[] = {
 		{"security_barrier", RELOPT_TYPE_BOOL,
 		offsetof(ViewOptions, security_barrier)},
+		{"security_invoker", RELOPT_TYPE_BOOL,
+		offsetof(ViewOptions, security_invoker)},
 		{"check_option", RELOPT_TYPE_ENUM,
 		offsetof(ViewOptions, check_option)}
 	};
diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c
new file mode 100644
index 4b3f797..b97b8b0
--- a/src/backend/commands/lockcmds.c
+++ b/src/backend/commands/lockcmds.c
@@ -169,7 +169,7 @@ typedef struct
 {
 	LOCKMODE	lockmode;		/* lock mode to use */
 	bool		nowait;			/* no wait mode */
-	Oid			viewowner;		/* view owner for checking the privilege */
+	Oid			check_as_user;	/* user for checking the privilege */
 	Oid			viewoid;		/* OID of the view to be locked */
 	List	   *ancestor_views; /* OIDs of ancestor views */
 } LockViewRecurse_context;
@@ -215,8 +215,12 @@ LockViewRecurse_walker(Node *node, LockV
 			if (list_member_oid(context->ancestor_views, relid))
 				continue;
 
-			/* Check permissions with the view owner's privilege. */
-			aclresult = LockTableAclCheck(relid, context->lockmode, context->viewowner);
+			/*
+			 * Check permissions as the specified user.  This will either be
+			 * the view owner or the current user.
+			 */
+			aclresult = LockTableAclCheck(relid, context->lockmode,
+										  context->check_as_user);
 			if (aclresult != ACLCHECK_OK)
 				aclcheck_error(aclresult, get_relkind_objtype(relkind), relname);
 
@@ -259,9 +263,16 @@ LockViewRecurse(Oid reloid, LOCKMODE loc
 	view = table_open(reloid, NoLock);
 	viewquery = get_view_query(view);
 
+	/*
+	 * If the view has the security_invoker property set, check permissions as
+	 * the current user.  Otherwise, check permissions as the view owner.
+	 */
 	context.lockmode = lockmode;
 	context.nowait = nowait;
-	context.viewowner = view->rd_rel->relowner;
+	if (RelationHasSecurityInvoker(view))
+		context.check_as_user = GetUserId();
+	else
+		context.check_as_user = view->rd_rel->relowner;
 	context.viewoid = reloid;
 	context.ancestor_views = lappend_oid(ancestor_views, reloid);
 
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 3d82138..4eeed58
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3242,18 +3242,24 @@ rewriteTargetView(Query *parsetree, Rela
 				   0);
 
 	/*
-	 * Mark the new target RTE for the permissions checks that we want to
-	 * enforce against the view owner, as distinct from the query caller.  At
-	 * the relation level, require the same INSERT/UPDATE/DELETE permissions
-	 * that the query caller needs against the view.  We drop the ACL_SELECT
-	 * bit that is presumably in new_rte->requiredPerms initially.
+	 * If the view has "security_invoker" set, mark the new target RTE for the
+	 * permissions checks that we want to enforce against the query caller.
+	 * Otherwise we want to enforce them against the view owner.
+	 *
+	 * At the relation level, require the same INSERT/UPDATE/DELETE
+	 * permissions that the query caller needs against the view.  We drop the
+	 * ACL_SELECT bit that is presumably in new_rte->requiredPerms initially.
 	 *
 	 * Note: the original view RTE remains in the query's rangetable list.
 	 * Although it will be unused in the query plan, we need it there so that
 	 * the executor still performs appropriate permissions checks for the
 	 * query caller's use of the view.
 	 */
-	new_rte->checkAsUser = view->rd_rel->relowner;
+	if (RelationHasSecurityInvoker(view))
+		new_rte->checkAsUser = InvalidOid;
+	else
+		new_rte->checkAsUser = view->rd_rel->relowner;
+
 	new_rte->requiredPerms = view_rte->requiredPerms;
 
 	/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
new file mode 100644
index fccffce..fbd1188
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -722,6 +722,8 @@ RelationBuildTupleDesc(Relation relation
  * entry, because that keeps the update logic in RelationClearRelation()
  * manageable.  The other subsidiary data structures are simple enough
  * to be easy to free explicitly, anyway.
+ *
+ * Note: The relation's reloptions must have been extracted first.
  */
 static void
 RelationBuildRuleLock(Relation relation)
@@ -787,6 +789,7 @@ RelationBuildRuleLock(Relation relation)
 		Datum		rule_datum;
 		char	   *rule_str;
 		RewriteRule *rule;
+		Oid			check_as_user;
 
 		rule = (RewriteRule *) MemoryContextAlloc(rulescxt,
 												  sizeof(RewriteRule));
@@ -826,10 +829,23 @@ 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
+		 * If this is a SELECT rule defining a view, and the view has
+		 * "security_invoker" set, we must perform all permissions checks on
+		 * relations referred to by the rule as the invoking user.
+		 *
+		 * In all other cases (including non-SELECT rules on security invoker
+		 * views), perform the permissions checks as the relation owner.
+		 */
+		if (rule->event == CMD_SELECT &&
+			relation->rd_rel->relkind == RELKIND_VIEW &&
+			RelationHasSecurityInvoker(relation))
+			check_as_user = InvalidOid;
+		else
+			check_as_user = relation->rd_rel->relowner;
+
+		/*
+		 * 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
@@ -838,8 +854,8 @@ 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);
+		setRuleCheckAsUser((Node *) rule->actions, check_as_user);
+		setRuleCheckAsUser(rule->qual, check_as_user);
 
 		if (numlocks >= maxlocks)
 		{
@@ -1164,27 +1180,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;
@@ -1217,6 +1212,30 @@ retry:
 	RelationParseRelOptions(relation, pg_class_tuple);
 
 	/*
+	 * Fetch rules and triggers that affect this relation.
+	 *
+	 * Note that RelationBuildRuleLock() relies on this being done after
+	 * extracting the relation's reloptions.
+	 */
+	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
 	 */
 	RelationInitLockInfo(relation); /* see lmgr.c */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
new file mode 100644
index 3b4ab65..7a8ed94
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -398,6 +398,7 @@ typedef struct ViewOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	bool		security_barrier;
+	bool		security_invoker;
 	ViewOptCheckOption check_option;
 } ViewOptions;
 
@@ -412,6 +413,16 @@ typedef struct ViewOptions
 	  ((ViewOptions *) (relation)->rd_options)->security_barrier : false)
 
 /*
+ * RelationHasSecurityInvoker
+ *		Returns true if the relation has the security_invoker property set.
+ *		Note multiple eval of argument!
+ */
+#define RelationHasSecurityInvoker(relation)								\
+	(AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW),				\
+	 (relation)->rd_options ?												\
+	  ((ViewOptions *) (relation)->rd_options)->security_invoker : false)
+
+/*
  * RelationHasCheckOption
  *		Returns true if the relation is a view defined with either the local
  *		or the cascaded check option.  Note multiple eval of argument!
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
new file mode 100644
index ae7c043..32385bb
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -296,17 +296,31 @@ ERROR:  invalid value for boolean option
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- Error
+       AS SELECT * FROM tbl1 WHERE a <> 100;
+ERROR:  invalid value for boolean option "security_invoker": 100
 SELECT relname, relkind, reloptions FROM pg_class
        WHERE oid in ('mysecview1'::regclass, 'mysecview2'::regclass,
-                     'mysecview3'::regclass, 'mysecview4'::regclass)
+                     'mysecview3'::regclass, 'mysecview4'::regclass,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::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)
+ mysecview7 | v       | {security_invoker=true}
+ mysecview8 | v       | {security_invoker=false,security_barrier=true}
+ mysecview9 | v       | {security_invoker=true}
+(7 rows)
 
 CREATE OR REPLACE VIEW mysecview1
        AS SELECT * FROM tbl1 WHERE a = 256;
@@ -316,17 +330,28 @@ CREATE OR REPLACE VIEW mysecview3 WITH (
        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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
-  relname   | relkind |        reloptions        
-------------+---------+--------------------------
+  relname   | relkind |                   reloptions                   
+------------+---------+------------------------------------------------
  mysecview1 | v       | 
  mysecview2 | v       | 
  mysecview3 | v       | {security_barrier=true}
  mysecview4 | v       | {security_barrier=false}
-(4 rows)
+ mysecview7 | v       | 
+ mysecview8 | v       | {security_invoker=true}
+ mysecview9 | v       | {security_invoker=false,security_barrier=true}
+(7 rows)
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
 -- so that we don't end up with unknown-type columns.
@@ -2039,7 +2064,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 74 other objects
+NOTICE:  drop cascades to 77 other objects
 DETAIL:  drop cascades to table t1
 drop cascades to view temporal1
 drop cascades to view temporal2
@@ -2060,6 +2085,9 @@ drop cascades to view mysecview1
 drop cascades to view mysecview2
 drop cascades to view mysecview3
 drop cascades to view mysecview4
+drop cascades to view mysecview7
+drop cascades to view mysecview8
+drop cascades to view mysecview9
 drop cascades to view unspecified_types
 drop cascades to table tt1
 drop cascades to table tx1
diff --git a/src/test/regress/expected/lock.out b/src/test/regress/expected/lock.out
new file mode 100644
index 01d467a..ad137d3
--- a/src/test/regress/expected/lock.out
+++ b/src/test/regress/expected/lock.out
@@ -156,9 +156,75 @@ BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
 RESET ROLE;
+REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
+-- Tables referred to by views are locked without explicit permission to do so
+-- as long as we have permission to lock the view itself.
+SET ROLE regress_rol_lock1;
+-- fail without permissions on the view
+BEGIN;
+LOCK TABLE lock_view1;
+ERROR:  permission denied for view lock_view1
+ROLLBACK;
+RESET ROLE;
+GRANT UPDATE ON TABLE lock_view1 TO regress_rol_lock1;
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE lock_view1 IN ACCESS EXCLUSIVE MODE;
+-- lock_view1 and lock_tbl1 (plus children lock_tbl2 and lock_tbl3) are locked.
+select relname from pg_locks l, pg_class c
+ where l.relation = c.oid and relname like '%lock_%' and mode = 'AccessExclusiveLock'
+ order by relname;
+  relname   
+------------
+ lock_tbl1
+ lock_tbl2
+ lock_tbl3
+ lock_view1
+(4 rows)
+
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE lock_view1 FROM regress_rol_lock1;
+-- Tables referred to by security invoker views require explicit permission to
+-- be locked.
+CREATE VIEW lock_view8 WITH (security_invoker) AS SELECT * FROM lock_tbl1;
+SET ROLE regress_rol_lock1;
+-- fail without permissions on the view
+BEGIN;
+LOCK TABLE lock_view8;
+ERROR:  permission denied for view lock_view8
+ROLLBACK;
+RESET ROLE;
+GRANT UPDATE ON TABLE lock_view8 TO regress_rol_lock1;
+SET ROLE regress_rol_lock1;
+-- fail without permissions on the table referenced by the view
+BEGIN;
+LOCK TABLE lock_view8;
+ERROR:  permission denied for table lock_tbl1
+ROLLBACK;
+RESET ROLE;
+GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
+BEGIN;
+LOCK TABLE lock_view8 IN ACCESS EXCLUSIVE MODE;
+-- lock_view8 and lock_tbl1 (plus children lock_tbl2 and lock_tbl3) are locked.
+select relname from pg_locks l, pg_class c
+ where l.relation = c.oid and relname like '%lock_%' and mode = 'AccessExclusiveLock'
+ order by relname;
+  relname   
+------------
+ lock_tbl1
+ lock_tbl2
+ lock_tbl3
+ lock_view8
+(4 rows)
+
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE lock_view8 FROM regress_rol_lock1;
 --
 -- Clean up
 --
+DROP VIEW lock_view8;
 DROP VIEW lock_view7;
 DROP VIEW lock_view6;
 DROP VIEW lock_view5;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 89397e4..d32a40e
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2431,6 +2431,7 @@ ERROR:  permission denied for view rls_v
 -- Query as role that is not the owner of the table or view with permissions.
 SET SESSION AUTHORIZATION regress_rls_bob;
 GRANT SELECT ON rls_view TO regress_rls_carol;
+SET SESSION AUTHORIZATION regress_rls_carol;
 SELECT * FROM rls_view;
 NOTICE:  f_leak => bbb
 NOTICE:  f_leak => dad
@@ -2447,6 +2448,259 @@ EXPLAIN (COSTS OFF) SELECT * FROM rls_vi
    Filter: (((a % 2) = 0) AND f_leak(b))
 (2 rows)
 
+-- Policy requiring access to another table.
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE TABLE z1_blacklist (a int);
+INSERT INTO z1_blacklist VALUES (3), (4);
+CREATE POLICY p3 ON z1 AS RESTRICTIVE USING (a NOT IN (SELECT a FROM z1_blacklist));
+-- Query as role that is not owner of table but is owner of view without permissions.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+-- Query as role that is not the owner of the table or view without permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+-- Query as role that is not owner of table but is owner of view with permissions.
+SET SESSION AUTHORIZATION regress_rls_alice;
+GRANT SELECT ON z1_blacklist TO regress_rls_bob;
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => bbb
+ a |  b  
+---+-----
+ 2 | bbb
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Seq Scan on z1
+   Filter: ((NOT (hashed SubPlan 1)) AND ((a % 2) = 0) AND f_leak(b))
+   SubPlan 1
+     ->  Seq Scan on z1_blacklist
+(4 rows)
+
+-- Query as role that is not the owner of the table or view with permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => bbb
+ a |  b  
+---+-----
+ 2 | bbb
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Seq Scan on z1
+   Filter: ((NOT (hashed SubPlan 1)) AND ((a % 2) = 0) AND f_leak(b))
+   SubPlan 1
+     ->  Seq Scan on z1_blacklist
+(4 rows)
+
+SET SESSION AUTHORIZATION regress_rls_alice;
+REVOKE SELECT ON z1_blacklist FROM regress_rls_bob;
+DROP POLICY p3 ON z1;
+SET SESSION AUTHORIZATION regress_rls_bob;
+DROP VIEW rls_view;
+--
+-- Security invoker views should follow policy for current user.
+--
+-- View and table owner are the same.
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE VIEW rls_view WITH (security_invoker) AS
+    SELECT * FROM z1 WHERE f_leak(b);
+GRANT SELECT ON rls_view TO regress_rls_bob;
+GRANT SELECT ON rls_view TO regress_rls_carol;
+-- Query as table owner.  Should return all records.
+SELECT * FROM rls_view;
+NOTICE:  f_leak => aba
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => dad
+ a |  b  
+---+-----
+ 1 | aba
+ 2 | bbb
+ 3 | ccc
+ 4 | dad
+(4 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+     QUERY PLAN      
+---------------------
+ Seq Scan on z1
+   Filter: f_leak(b)
+(2 rows)
+
+-- Queries as other users.
+-- Should return records based on current user's policies.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => dad
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | dad
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on z1
+   Filter: (((a % 2) = 0) AND f_leak(b))
+(2 rows)
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => aba
+NOTICE:  f_leak => ccc
+ a |  b  
+---+-----
+ 1 | aba
+ 3 | ccc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on z1
+   Filter: (((a % 2) = 1) AND f_leak(b))
+(2 rows)
+
+-- View and table owners are different.
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP VIEW rls_view;
+SET SESSION AUTHORIZATION regress_rls_bob;
+CREATE VIEW rls_view WITH (security_invoker) AS
+    SELECT * FROM z1 WHERE f_leak(b);
+GRANT SELECT ON rls_view TO regress_rls_alice;
+GRANT SELECT ON rls_view TO regress_rls_carol;
+-- Query as table owner.  Should return all records.
+SET SESSION AUTHORIZATION regress_rls_alice;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => aba
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => dad
+ a |  b  
+---+-----
+ 1 | aba
+ 2 | bbb
+ 3 | ccc
+ 4 | dad
+(4 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+     QUERY PLAN      
+---------------------
+ Seq Scan on z1
+   Filter: f_leak(b)
+(2 rows)
+
+-- Queries as other users.
+-- Should return records based on current user's policies.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => dad
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | dad
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on z1
+   Filter: (((a % 2) = 0) AND f_leak(b))
+(2 rows)
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => aba
+NOTICE:  f_leak => ccc
+ a |  b  
+---+-----
+ 1 | aba
+ 3 | ccc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on z1
+   Filter: (((a % 2) = 1) AND f_leak(b))
+(2 rows)
+
+-- Policy requiring access to another table.
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE POLICY p3 ON z1 AS RESTRICTIVE USING (a NOT IN (SELECT a FROM z1_blacklist));
+-- Query as role that is not owner of table but is owner of view without permissions.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+-- Query as role that is not the owner of the table or view without permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+-- Query as role that is not owner of table but is owner of view with permissions.
+SET SESSION AUTHORIZATION regress_rls_alice;
+GRANT SELECT ON z1_blacklist TO regress_rls_bob;
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => bbb
+ a |  b  
+---+-----
+ 2 | bbb
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Seq Scan on z1
+   Filter: ((NOT (hashed SubPlan 1)) AND ((a % 2) = 0) AND f_leak(b))
+   SubPlan 1
+     ->  Seq Scan on z1_blacklist
+(4 rows)
+
+-- Query as role that is not the owner of the table or view without permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+ERROR:  permission denied for table z1_blacklist
+-- Query as role that is not the owner of the table or view with permissions.
+SET SESSION AUTHORIZATION regress_rls_alice;
+GRANT SELECT ON z1_blacklist TO regress_rls_carol;
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+NOTICE:  f_leak => aba
+ a |  b  
+---+-----
+ 1 | aba
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Seq Scan on z1
+   Filter: ((NOT (hashed SubPlan 1)) AND ((a % 2) = 1) AND f_leak(b))
+   SubPlan 1
+     ->  Seq Scan on z1_blacklist
+(4 rows)
+
 SET SESSION AUTHORIZATION regress_rls_bob;
 DROP VIEW rls_view;
 --
@@ -3987,7 +4241,7 @@ RESET SESSION AUTHORIZATION;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 29 other objects
+NOTICE:  drop cascades to 30 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -4005,6 +4259,7 @@ drop cascades to table b1
 drop cascades to view bv1
 drop cascades to table z1
 drop cascades to table z2
+drop cascades to table z1_blacklist
 drop cascades to table x1
 drop cascades to table y1
 drop cascades to table y2
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index ac46856..6cb6388
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3496,3 +3496,33 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+--
+-- Test non-SELECT rule on security invoker view.
+-- Should use view owner's permissions.
+-- 
+CREATE USER regress_rule_user1;
+CREATE TABLE ruletest_t1 (x int);
+CREATE TABLE ruletest_t2 (x int);
+CREATE VIEW ruletest_v1 WITH (security_invoker=true) AS
+    SELECT * FROM ruletest_t1;
+GRANT INSERT ON ruletest_v1 TO regress_rule_user1;
+CREATE RULE rule1 AS ON INSERT TO ruletest_v1
+    DO INSTEAD INSERT INTO ruletest_t2 VALUES (NEW.*);
+SET SESSION AUTHORIZATION regress_rule_user1;
+INSERT INTO ruletest_v1 VALUES (1);
+RESET SESSION AUTHORIZATION;
+SELECT * FROM ruletest_t1;
+ x 
+---
+(0 rows)
+
+SELECT * FROM ruletest_t2;
+ x 
+---
+ 1
+(1 row)
+
+DROP VIEW ruletest_v1;
+DROP TABLE ruletest_t2;
+DROP TABLE ruletest_t1;
+DROP USER regress_rule_user1;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index cdff914..d57eeb7
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -979,6 +979,7 @@ drop cascades to function rw_view1_aa(rw
 -- permissions checks
 CREATE USER regress_view_user1;
 CREATE USER regress_view_user2;
+CREATE USER regress_view_user3;
 SET SESSION AUTHORIZATION regress_view_user1;
 CREATE TABLE base_tbl(a int, b text, c float);
 INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0);
@@ -1205,8 +1206,244 @@ DROP TABLE base_tbl CASCADE;
 NOTICE:  drop cascades to 2 other objects
 DETAIL:  drop cascades to view rw_view1
 drop cascades to view rw_view2
+-- security invoker view permissions
+SET SESSION AUTHORIZATION regress_view_user1;
+CREATE TABLE base_tbl(a int, b text, c float);
+INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0);
+CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+ALTER VIEW rw_view1 SET (security_invoker = true);
+INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2);
+GRANT SELECT ON rw_view1 TO regress_view_user2;
+GRANT UPDATE (bb,cc) ON rw_view1 TO regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM base_tbl; -- not allowed
+ERROR:  permission denied for table base_tbl
+SELECT * FROM rw_view1; -- not allowed
+ERROR:  permission denied for table base_tbl
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- not allowed
+ERROR:  permission denied for table base_tbl
+INSERT INTO rw_view1 VALUES ('Row 3', 3.0, 3); -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE base_tbl SET a=a; -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view1 SET bb=bb, cc=cc; -- not allowed
+ERROR:  permission denied for table base_tbl
+DELETE FROM base_tbl; -- not allowed
+ERROR:  permission denied for table base_tbl
+DELETE FROM rw_view1; -- not allowed
+ERROR:  permission denied for view rw_view1
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT SELECT ON base_tbl TO regress_view_user2;
+GRANT UPDATE (a,c) ON base_tbl TO regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM base_tbl; -- ok
+ a |   b   | c 
+---+-------+---
+ 1 | Row 1 | 1
+ 2 | Row 2 | 2
+(2 rows)
+
+SELECT * FROM rw_view1; -- ok
+  bb   | cc | aa 
+-------+----+----
+ Row 1 |  1 |  1
+ Row 2 |  2 |  2
+(2 rows)
+
+UPDATE base_tbl SET a=a, c=c; -- ok
+UPDATE base_tbl SET b=b; -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view1 SET cc=cc; -- ok
+UPDATE rw_view1 SET aa=aa; -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view1 SET bb=bb; -- not allowed
+ERROR:  permission denied for table base_tbl
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT INSERT, DELETE ON base_tbl TO regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- ok
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed
+ERROR:  permission denied for view rw_view1
+DELETE FROM base_tbl WHERE a=1; -- ok
+DELETE FROM rw_view1 WHERE aa=2; -- not allowed
+ERROR:  permission denied for view rw_view1
+SET SESSION AUTHORIZATION regress_view_user1;
+REVOKE INSERT, DELETE ON base_tbl FROM regress_view_user2;
+GRANT INSERT, DELETE ON rw_view1 TO regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed
+ERROR:  permission denied for table base_tbl
+DELETE FROM rw_view1 WHERE aa=2; -- not allowed
+ERROR:  permission denied for table base_tbl
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT INSERT, DELETE ON base_tbl TO regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- ok
+DELETE FROM rw_view1 WHERE aa=2; -- ok
+SELECT * FROM base_tbl; -- ok
+ a |   b   | c 
+---+-------+---
+ 3 | Row 3 | 3
+ 4 | Row 4 | 4
+(2 rows)
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to view rw_view1
+-- ordinary view on top of security invoker view permissions
+CREATE TABLE base_tbl(a int, b text, c float);
+INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0);
+SET SESSION AUTHORIZATION regress_view_user1;
+CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+ALTER VIEW rw_view1 SET (security_invoker = true);
+SELECT * FROM rw_view1;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view1 SET aa=aa;  -- not allowed
+ERROR:  permission denied for table base_tbl
+SET SESSION AUTHORIZATION regress_view_user2;
+CREATE VIEW rw_view2 AS SELECT cc AS ccc, aa AS aaa, bb AS bbb FROM rw_view1;
+GRANT SELECT, UPDATE ON rw_view2 TO regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+RESET SESSION AUTHORIZATION;
+GRANT SELECT ON base_tbl TO regress_view_user1;
+GRANT UPDATE (a, b) ON base_tbl TO regress_view_user1;
+SET SESSION AUTHORIZATION regress_view_user1;
+SELECT * FROM rw_view1; -- ok
+  bb   | cc | aa 
+-------+----+----
+ Row 1 |  1 |  1
+(1 row)
+
+UPDATE rw_view1 SET aa=aa, bb=bb;  -- ok
+UPDATE rw_view1 SET cc=cc;  -- not allowed
+ERROR:  permission denied for table base_tbl
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT SELECT ON rw_view1 TO regress_view_user2;
+GRANT UPDATE (bb, cc) ON rw_view1 TO regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+RESET SESSION AUTHORIZATION;
+GRANT SELECT ON base_tbl TO regress_view_user2;
+GRANT UPDATE (a, c) ON base_tbl TO regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- ok
+ ccc | aaa |  bbb  
+-----+-----+-------
+   1 |   1 | Row 1
+(1 row)
+
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET ccc=ccc;  -- not allowed
+ERROR:  permission denied for table base_tbl
+RESET SESSION AUTHORIZATION;
+GRANT SELECT ON base_tbl TO regress_view_user3;
+GRANT UPDATE (a, c) ON base_tbl TO regress_view_user3;
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- ok
+ ccc | aaa |  bbb  
+-----+-----+-------
+   1 |   1 | Row 1
+(1 row)
+
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT, UPDATE ON base_tbl FROM regress_view_user1;
+SET SESSION AUTHORIZATION regress_view_user1;
+SELECT * FROM rw_view1;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view1 SET aa=aa;  -- not allowed
+ERROR:  permission denied for table base_tbl
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- ok
+ ccc | aaa |  bbb  
+-----+-----+-------
+   1 |   1 | Row 1
+(1 row)
+
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- ok
+ ccc | aaa |  bbb  
+-----+-----+-------
+   1 |   1 | Row 1
+(1 row)
+
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+RESET SESSION AUTHORIZATION;
+REVOKE SELECT, UPDATE ON base_tbl FROM regress_view_user2;
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET ccc=ccc;  -- not allowed
+ERROR:  permission denied for table base_tbl
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- ok
+ ccc | aaa |  bbb  
+-----+-----+-------
+   1 |   1 | Row 1
+(1 row)
+
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+ERROR:  permission denied for view rw_view1
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+ERROR:  permission denied for table base_tbl
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+RESET SESSION AUTHORIZATION;
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to view rw_view1
+drop cascades to view rw_view2
 DROP USER regress_view_user1;
 DROP USER regress_view_user2;
+DROP USER regress_view_user3;
 -- column defaults
 CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified', c serial);
 INSERT INTO base_tbl VALUES (1, 'Row 1');
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
new file mode 100644
index 829f3dd..50acfe9
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -254,9 +254,19 @@ CREATE VIEW mysecview5 WITH (security_ba
        AS SELECT * FROM tbl1 WHERE a > 100;
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview7 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a = 100;
+CREATE VIEW mysecview8 WITH (security_invoker=false, security_barrier=true)
+       AS SELECT * FROM tbl1 WHERE a > 100;
+CREATE VIEW mysecview9 WITH (security_invoker)
+       AS SELECT * FROM tbl1 WHERE a < 100;
+CREATE VIEW mysecview10 WITH (security_invoker=100)	-- 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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 CREATE OR REPLACE VIEW mysecview1
@@ -267,9 +277,17 @@ CREATE OR REPLACE VIEW mysecview3 WITH (
        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 mysecview7
+       AS SELECT * FROM tbl1 WHERE a > 256;
+CREATE OR REPLACE VIEW mysecview8 WITH (security_invoker=true)
+       AS SELECT * FROM tbl1 WHERE a < 256;
+CREATE OR REPLACE VIEW mysecview9 WITH (security_invoker=false, security_barrier=true)
+       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,
+                     'mysecview7'::regclass, 'mysecview8'::regclass,
+                     'mysecview9'::regclass)
        ORDER BY relname;
 
 -- Check that unknown literals are converted to "text" in CREATE VIEW,
diff --git a/src/test/regress/sql/lock.sql b/src/test/regress/sql/lock.sql
new file mode 100644
index b867e0f..b88488c
--- a/src/test/regress/sql/lock.sql
+++ b/src/test/regress/sql/lock.sql
@@ -122,10 +122,59 @@ BEGIN;
 LOCK TABLE ONLY lock_tbl1;
 ROLLBACK;
 RESET ROLE;
+REVOKE UPDATE ON TABLE lock_tbl1 FROM regress_rol_lock1;
+
+-- Tables referred to by views are locked without explicit permission to do so
+-- as long as we have permission to lock the view itself.
+SET ROLE regress_rol_lock1;
+-- fail without permissions on the view
+BEGIN;
+LOCK TABLE lock_view1;
+ROLLBACK;
+RESET ROLE;
+GRANT UPDATE ON TABLE lock_view1 TO regress_rol_lock1;
+SET ROLE regress_rol_lock1;
+BEGIN;
+LOCK TABLE lock_view1 IN ACCESS EXCLUSIVE MODE;
+-- lock_view1 and lock_tbl1 (plus children lock_tbl2 and lock_tbl3) are locked.
+select relname from pg_locks l, pg_class c
+ where l.relation = c.oid and relname like '%lock_%' and mode = 'AccessExclusiveLock'
+ order by relname;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE lock_view1 FROM regress_rol_lock1;
+
+-- Tables referred to by security invoker views require explicit permission to
+-- be locked.
+CREATE VIEW lock_view8 WITH (security_invoker) AS SELECT * FROM lock_tbl1;
+SET ROLE regress_rol_lock1;
+-- fail without permissions on the view
+BEGIN;
+LOCK TABLE lock_view8;
+ROLLBACK;
+RESET ROLE;
+GRANT UPDATE ON TABLE lock_view8 TO regress_rol_lock1;
+SET ROLE regress_rol_lock1;
+-- fail without permissions on the table referenced by the view
+BEGIN;
+LOCK TABLE lock_view8;
+ROLLBACK;
+RESET ROLE;
+GRANT UPDATE ON TABLE lock_tbl1 TO regress_rol_lock1;
+BEGIN;
+LOCK TABLE lock_view8 IN ACCESS EXCLUSIVE MODE;
+-- lock_view8 and lock_tbl1 (plus children lock_tbl2 and lock_tbl3) are locked.
+select relname from pg_locks l, pg_class c
+ where l.relation = c.oid and relname like '%lock_%' and mode = 'AccessExclusiveLock'
+ order by relname;
+ROLLBACK;
+RESET ROLE;
+REVOKE UPDATE ON TABLE lock_view8 FROM regress_rol_lock1;
 
 --
 -- Clean up
 --
+DROP VIEW lock_view8;
 DROP VIEW lock_view7;
 DROP VIEW lock_view6;
 DROP VIEW lock_view5;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index 44deb42..b310acd
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -912,6 +912,128 @@ EXPLAIN (COSTS OFF) SELECT * FROM rls_vi
 -- Query as role that is not the owner of the table or view with permissions.
 SET SESSION AUTHORIZATION regress_rls_bob;
 GRANT SELECT ON rls_view TO regress_rls_carol;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+-- Policy requiring access to another table.
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE TABLE z1_blacklist (a int);
+INSERT INTO z1_blacklist VALUES (3), (4);
+CREATE POLICY p3 ON z1 AS RESTRICTIVE USING (a NOT IN (SELECT a FROM z1_blacklist));
+
+-- Query as role that is not owner of table but is owner of view without permissions.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view; --fail - permission denied.
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+
+-- Query as role that is not the owner of the table or view without permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view; --fail - permission denied.
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+
+-- Query as role that is not owner of table but is owner of view with permissions.
+SET SESSION AUTHORIZATION regress_rls_alice;
+GRANT SELECT ON z1_blacklist TO regress_rls_bob;
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+-- Query as role that is not the owner of the table or view with permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+SET SESSION AUTHORIZATION regress_rls_alice;
+REVOKE SELECT ON z1_blacklist FROM regress_rls_bob;
+DROP POLICY p3 ON z1;
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+DROP VIEW rls_view;
+
+--
+-- Security invoker views should follow policy for current user.
+--
+-- View and table owner are the same.
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE VIEW rls_view WITH (security_invoker) AS
+    SELECT * FROM z1 WHERE f_leak(b);
+GRANT SELECT ON rls_view TO regress_rls_bob;
+GRANT SELECT ON rls_view TO regress_rls_carol;
+
+-- Query as table owner.  Should return all records.
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+-- Queries as other users.
+-- Should return records based on current user's policies.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+-- View and table owners are different.
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP VIEW rls_view;
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+CREATE VIEW rls_view WITH (security_invoker) AS
+    SELECT * FROM z1 WHERE f_leak(b);
+GRANT SELECT ON rls_view TO regress_rls_alice;
+GRANT SELECT ON rls_view TO regress_rls_carol;
+
+-- Query as table owner.  Should return all records.
+SET SESSION AUTHORIZATION regress_rls_alice;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+-- Queries as other users.
+-- Should return records based on current user's policies.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+-- Policy requiring access to another table.
+SET SESSION AUTHORIZATION regress_rls_alice;
+CREATE POLICY p3 ON z1 AS RESTRICTIVE USING (a NOT IN (SELECT a FROM z1_blacklist));
+
+-- Query as role that is not owner of table but is owner of view without permissions.
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view; --fail - permission denied.
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+
+-- Query as role that is not the owner of the table or view without permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view; --fail - permission denied.
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+
+-- Query as role that is not owner of table but is owner of view with permissions.
+SET SESSION AUTHORIZATION regress_rls_alice;
+GRANT SELECT ON z1_blacklist TO regress_rls_bob;
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+SELECT * FROM rls_view;
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
+
+-- Query as role that is not the owner of the table or view without permissions.
+SET SESSION AUTHORIZATION regress_rls_carol;
+SELECT * FROM rls_view; --fail - permission denied.
+EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied.
+
+-- Query as role that is not the owner of the table or view with permissions.
+SET SESSION AUTHORIZATION regress_rls_alice;
+GRANT SELECT ON z1_blacklist TO regress_rls_carol;
+
+SET SESSION AUTHORIZATION regress_rls_carol;
 SELECT * FROM rls_view;
 EXPLAIN (COSTS OFF) SELECT * FROM rls_view;
 
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8bdab6d..aae2ba3
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1257,3 +1257,31 @@ SELECT * FROM ruletest2;
 
 DROP TABLE ruletest1;
 DROP TABLE ruletest2;
+
+--
+-- Test non-SELECT rule on security invoker view.
+-- Should use view owner's permissions.
+-- 
+CREATE USER regress_rule_user1;
+
+CREATE TABLE ruletest_t1 (x int);
+CREATE TABLE ruletest_t2 (x int);
+CREATE VIEW ruletest_v1 WITH (security_invoker=true) AS
+    SELECT * FROM ruletest_t1;
+GRANT INSERT ON ruletest_v1 TO regress_rule_user1;
+
+CREATE RULE rule1 AS ON INSERT TO ruletest_v1
+    DO INSTEAD INSERT INTO ruletest_t2 VALUES (NEW.*);
+
+SET SESSION AUTHORIZATION regress_rule_user1;
+INSERT INTO ruletest_v1 VALUES (1);
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM ruletest_t1;
+SELECT * FROM ruletest_t2;
+
+DROP VIEW ruletest_v1;
+DROP TABLE ruletest_t2;
+DROP TABLE ruletest_t1;
+
+DROP USER regress_rule_user1;
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index 09328e5..fa206a8
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -410,6 +410,7 @@ DROP TABLE base_tbl CASCADE;
 
 CREATE USER regress_view_user1;
 CREATE USER regress_view_user2;
+CREATE USER regress_view_user3;
 
 SET SESSION AUTHORIZATION regress_view_user1;
 CREATE TABLE base_tbl(a int, b text, c float);
@@ -552,8 +553,187 @@ RESET SESSION AUTHORIZATION;
 
 DROP TABLE base_tbl CASCADE;
 
+-- security invoker view permissions
+
+SET SESSION AUTHORIZATION regress_view_user1;
+CREATE TABLE base_tbl(a int, b text, c float);
+INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0);
+CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+ALTER VIEW rw_view1 SET (security_invoker = true);
+INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2);
+GRANT SELECT ON rw_view1 TO regress_view_user2;
+GRANT UPDATE (bb,cc) ON rw_view1 TO regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM base_tbl; -- not allowed
+SELECT * FROM rw_view1; -- not allowed
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- not allowed
+INSERT INTO rw_view1 VALUES ('Row 3', 3.0, 3); -- not allowed
+UPDATE base_tbl SET a=a; -- not allowed
+UPDATE rw_view1 SET bb=bb, cc=cc; -- not allowed
+DELETE FROM base_tbl; -- not allowed
+DELETE FROM rw_view1; -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT SELECT ON base_tbl TO regress_view_user2;
+GRANT UPDATE (a,c) ON base_tbl TO regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM base_tbl; -- ok
+SELECT * FROM rw_view1; -- ok
+UPDATE base_tbl SET a=a, c=c; -- ok
+UPDATE base_tbl SET b=b; -- not allowed
+UPDATE rw_view1 SET cc=cc; -- ok
+UPDATE rw_view1 SET aa=aa; -- not allowed
+UPDATE rw_view1 SET bb=bb; -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT INSERT, DELETE ON base_tbl TO regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- ok
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed
+DELETE FROM base_tbl WHERE a=1; -- ok
+DELETE FROM rw_view1 WHERE aa=2; -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user1;
+REVOKE INSERT, DELETE ON base_tbl FROM regress_view_user2;
+GRANT INSERT, DELETE ON rw_view1 TO regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed
+DELETE FROM rw_view1 WHERE aa=2; -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT INSERT, DELETE ON base_tbl TO regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- ok
+DELETE FROM rw_view1 WHERE aa=2; -- ok
+SELECT * FROM base_tbl; -- ok
+
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE base_tbl CASCADE;
+
+-- ordinary view on top of security invoker view permissions
+
+CREATE TABLE base_tbl(a int, b text, c float);
+INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0);
+
+SET SESSION AUTHORIZATION regress_view_user1;
+CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+ALTER VIEW rw_view1 SET (security_invoker = true);
+SELECT * FROM rw_view1;  -- not allowed
+UPDATE rw_view1 SET aa=aa;  -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user2;
+CREATE VIEW rw_view2 AS SELECT cc AS ccc, aa AS aaa, bb AS bbb FROM rw_view1;
+GRANT SELECT, UPDATE ON rw_view2 TO regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+
+RESET SESSION AUTHORIZATION;
+
+GRANT SELECT ON base_tbl TO regress_view_user1;
+GRANT UPDATE (a, b) ON base_tbl TO regress_view_user1;
+
+SET SESSION AUTHORIZATION regress_view_user1;
+SELECT * FROM rw_view1; -- ok
+UPDATE rw_view1 SET aa=aa, bb=bb;  -- ok
+UPDATE rw_view1 SET cc=cc;  -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- not allowed
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user1;
+GRANT SELECT ON rw_view1 TO regress_view_user2;
+GRANT UPDATE (bb, cc) ON rw_view1 TO regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+
+RESET SESSION AUTHORIZATION;
+
+GRANT SELECT ON base_tbl TO regress_view_user2;
+GRANT UPDATE (a, c) ON base_tbl TO regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- ok
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- not allowed
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+UPDATE rw_view2 SET ccc=ccc;  -- not allowed
+
+RESET SESSION AUTHORIZATION;
+
+GRANT SELECT ON base_tbl TO regress_view_user3;
+GRANT UPDATE (a, c) ON base_tbl TO regress_view_user3;
+
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- ok
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+
+RESET SESSION AUTHORIZATION;
+
+REVOKE SELECT, UPDATE ON base_tbl FROM regress_view_user1;
+
+SET SESSION AUTHORIZATION regress_view_user1;
+SELECT * FROM rw_view1;  -- not allowed
+UPDATE rw_view1 SET aa=aa;  -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- ok
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- ok
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+
+RESET SESSION AUTHORIZATION;
+
+REVOKE SELECT, UPDATE ON base_tbl FROM regress_view_user2;
+
+SET SESSION AUTHORIZATION regress_view_user2;
+SELECT * FROM rw_view2;  -- not allowed
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+UPDATE rw_view2 SET ccc=ccc;  -- not allowed
+
+SET SESSION AUTHORIZATION regress_view_user3;
+SELECT * FROM rw_view2;  -- ok
+UPDATE rw_view2 SET aaa=aaa;  -- not allowed
+UPDATE rw_view2 SET bbb=bbb;  -- not allowed
+UPDATE rw_view2 SET ccc=ccc;  -- ok
+
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE base_tbl CASCADE;
+
 DROP USER regress_view_user1;
 DROP USER regress_view_user2;
+DROP USER regress_view_user3;
 
 -- column defaults
 
#31Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Dean Rasheed (#30)
Re: [PATCH] Add reloption for views to enable RLS

On Sat, 2022-03-19 at 01:10 +0000, Dean Rasheed wrote:

I have been hacking on it a bit, and attached is an updated version.
Aside from some general copy editing, the most notable changes are:
[...]

Thanks for your diligent work on this, and the patch looks good to me.
It is good that you found the oversight in LOCK - I wasn't even
aware that views could be locked.

Yours,
Laurenz Albe

#32Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Christoph Heiss (#1)
Re: [PATCH] Add reloption for views to enable RLS

On Mon, 2022-03-21 at 18:09 +0800, Japin Li wrote:

After apply the patch, I found pg_checksums.c also has the similar code.

In progress_report(), I'm not sure we can do this replace for this code.

    snprintf(total_size_str, sizeof(total_size_str), INT64_FORMAT,
             total_size / (1024 * 1024));
    snprintf(current_size_str, sizeof(current_size_str), INT64_FORMAT,
             current_size / (1024 * 1024));

    fprintf(stderr, _("%*s/%s MB (%d%%) computed"),
            (int) strlen(current_size_str), current_size_str, total_size_str,
            percent);

I think you replied to the wrong thread...

Yours,
Laurenz Albe

#33Japin Li
japinli@hotmail.com
In reply to: Laurenz Albe (#32)
Re: [PATCH] Add reloption for views to enable RLS

On Mon, 21 Mar 2022 at 20:40, Laurenz Albe <laurenz.albe@cybertec.at> wrote:

On Mon, 2022-03-21 at 18:09 +0800, Japin Li wrote:

After apply the patch, I found pg_checksums.c also has the similar code.

In progress_report(), I'm not sure we can do this replace for this code.

snprintf(total_size_str, sizeof(total_size_str), INT64_FORMAT,
total_size / (1024 * 1024));
snprintf(current_size_str, sizeof(current_size_str), INT64_FORMAT,
current_size / (1024 * 1024));

fprintf(stderr, _("%*s/%s MB (%d%%) computed"),
(int) strlen(current_size_str), current_size_str, total_size_str,
percent);

I think you replied to the wrong thread...

I'm sorry! There is a problem with my email client and I didn't notice the
subject of the reply email.

Again, sorry for the noise!

--
Regrads,
Japin Li.
ChengDu WenWu Information Technology Co.,Ltd.

#34Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Laurenz Albe (#31)
Re: [PATCH] Add reloption for views to enable RLS

On Mon, 21 Mar 2022 at 09:47, Laurenz Albe <laurenz.albe@cybertec.at> wrote:

Thanks for your diligent work on this, and the patch looks good to me.

Thanks for looking again. Pushed.

Regards,
Dean