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, walt...@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
From 7d8f8e1cb27a3d22d049a5d820ddd66ec77a3297 Mon Sep 17 00:00:00 2001
From: Christoph Heiss <christoph.he...@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.he...@cybertec.at>
Co-Author: Laurenz Albe <laurenz.a...@cybertec.at>
Discussion: https://postgr.es/m/b66dd6d6-ad3e-c6f2-8b90-47be773da240%40cybertec.at
Signed-off-by: Christoph Heiss <christoph.he...@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

Reply via email to