RLS support for ON CONFLICT UPDATE
authorPeter Geoghegan <[email protected]>
Wed, 7 Jan 2015 00:32:21 +0000 (16:32 -0800)
committerPeter Geoghegan <[email protected]>
Mon, 20 Apr 2015 04:28:03 +0000 (21:28 -0700)
This commit establishes that only insert policies are enforced on
post-insert tuples (as before), only update USING/security barrier quals
are enforced (as WCOs, with errors thrown) on existing TARGET.* tuples
(before ON CONFLICT UPDATE), and post-update tuple only has WCO
enforcement (without USING/security barrier qual enforcement).

In general, the implementation treats RLS with INSERT ... ON CONFLICT
UPDATE as close to regular INSERT/UPDATE behavior as possible.  The
consistent enforcement of USING/security barrier qualifications as WCOs
is the only real deviation.

This commit also adds additional diagnostics, so that user-visible error
messages give some idea about which policy/policy command type is the
cause of a violation error, and if the WCO enforced originated as a
USING security barrier qual.

14 files changed:
doc/src/sgml/ref/create_policy.sgml
src/backend/executor/execMain.c
src/backend/executor/nodeModifyTable.c
src/backend/nodes/copyfuncs.c
src/backend/nodes/equalfuncs.c
src/backend/nodes/outfuncs.c
src/backend/nodes/readfuncs.c
src/backend/rewrite/rewriteHandler.c
src/backend/rewrite/rowsecurity.c
src/include/executor/executor.h
src/include/nodes/parsenodes.h
src/test/regress/expected/rowsecurity.out
src/test/regress/expected/updatable_views.out
src/test/regress/sql/rowsecurity.sql

index 868a6c1cd34daeddaa14fe7ded8128a6ba07a547..b1896d0fd8cf81610350fc5d451968bb81331657 100644 (file)
@@ -67,14 +67,18 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
   </para>
 
   <para>
-   Policies can be applied for specific commands or for specific roles.  The
-   default for newly created policies is that they apply for all commands and
-   roles, unless otherwise specified.  If multiple policies apply to a given
-   query, they will be combined using OR.  Further, for commands which can have
-   both USING and WITH CHECK policies (ALL and UPDATE), if no WITH CHECK policy
-   is defined then the USING policy will be used for both what rows are visible
-   (normal USING case) and which rows will be allowed to be added (WITH CHECK
-   case).
+   Policies can be applied for specific commands or for specific
+   roles.  The default for newly created policies is that they apply
+   for all commands and roles, unless otherwise specified.  If
+   multiple policies apply to a given query, they will be combined
+   using OR (although <literal>ON CONFLICT UPDATE</> and
+   <literal>INSERT</> policies are not combined in this way, but
+   rather enforced as noted at each stage of <literal>ON CONFLICT</>
+   execution).  Further, for commands which can have both USING and
+   WITH CHECK policies (ALL and UPDATE), if no WITH CHECK policy is
+   defined then the USING policy will be used for both what rows are
+   visible (normal USING case) and which rows will be allowed to be
+   added (WITH CHECK case).
   </para>
 
   <para>
@@ -255,6 +259,12 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
          as it only ever applies in cases where records are being added to the
          relation.
        </para>
+       <para>
+         Note that <literal>INSERT</literal> with <literal>ON CONFLICT
+         UPDATE</literal> requires that any <literal>INSERT</literal>
+         policy WITH CHECK expression passes for any rows appended to
+         the relation by the INSERT path only.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -262,23 +272,45 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
       <term><literal>UPDATE</></term>
       <listitem>
        <para>
-         Using <literal>UPDATE</literal> for a policy means that it will apply
-         to <literal>UPDATE</literal> commands.  As <literal>UPDATE</literal>
-         involves pulling an existing record and then making changes to some
-         portion (but possibly not all) of the record, the
-         <literal>UPDATE</literal> policy accepts both a USING expression and
-         a WITH CHECK expression.  The USING expression will be used to
-         determine which records the <literal>UPDATE</literal> command will
-         see to operate against, while the <literal>WITH CHECK</literal>
-         expression defines what rows are allowed to be added back into the
-         relation (similar to the <literal>INSERT</literal> policy).
-         Any rows whose resulting values do not pass the
-         <literal>WITH CHECK</literal> expression will cause an ERROR and the
-         entire command will be aborted.  Note that if only a
-         <literal>USING</literal> clause is specified then that clause will be
-         used for both <literal>USING</literal> and
+         Using <literal>UPDATE</literal> for a policy means that it
+         will apply to <literal>UPDATE</literal> commands (or
+         auxiliary <literal>ON CONFLICT UPDATE</literal> clauses of
+         <literal>INSERT</literal> commands).  As
+         <literal>UPDATE</literal> involves pulling an existing record
+         and then making changes to some portion (but possibly not
+         all) of the record, the <literal>UPDATE</literal> policy
+         accepts both a <literal>USING</literal> expression and a
+         <literal>WITH CHECK</literal> expression.  The
+         <literal>USING</literal> expression will be used to determine
+         which records the <literal>UPDATE</literal> command will see
+         to operate against, while the <literal>WITH CHECK</literal>
+         expression defines what rows are allowed to be added back
+         into the relation (similar to the <literal>INSERT</literal>
+         policy).  Any rows whose resulting values do not pass the
+         <literal>WITH CHECK</literal> expression will cause an ERROR
+         and the entire command will be aborted.  Note that if only a
+         <literal>USING</literal> clause is specified then that clause
+         will be used for both <literal>USING</literal> and
          <literal>WITH CHECK</literal> cases.
        </para>
+       <para>
+         Note, however, that <literal>INSERT</literal> with
+         <literal>ON CONFLICT UPDATE</literal> requires that an
+         <literal>UPDATE</literal> policy <literal>USING</literal>
+         expression always be encforced as a <literal>WITH
+         CHECK</literal> expression.  This <literal>UPDATE</literal>
+         policy must always pass when the <literal>UPDATE</literal>
+         path is taken.  Any existing row that necessitates that the
+         <literal>UPDATE</literal> path be taken must pass the (UPDATE
+         or ALL) <literal>USING</literal> qualifications (combined
+         using <literal>OR</literal>), which are always enforced as
+         WTIH CHECK options in this context (the
+         <literal>UPDATE</literal> path will <emphasis>never</> be
+         silently avoided; an error will be thrown instead).  Finally,
+         the final row appended to the relation must pass any
+         <literal>WITH CHECK</literal> options that a conventional
+         <literal>UPDATE</literal> is required to pass.
+       </para>
       </listitem>
      </varlistentry>
 
index d1faf4df5a997b795b5c6f9180783b8e660cee0d..bdf78ec0a305cb96207e279eb210b01a6d9f6bfd 100644 (file)
@@ -1703,7 +1703,9 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
  */
 void
 ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
-                    TupleTableSlot *slot, EState *estate)
+                    TupleTableSlot *slot, bool onlyInsert,
+                    bool securityBarrierUpdateQuals, bool WCOUpdate,
+                    EState *estate)
 {
    Relation    rel = resultRelInfo->ri_RelationDesc;
    TupleDesc   tupdesc = RelationGetDescr(rel);
@@ -1727,6 +1729,25 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
        WithCheckOption *wco = (WithCheckOption *) lfirst(l1);
        ExprState  *wcoExpr = (ExprState *) lfirst(l2);
 
+       /*
+        * INSERT ... ON CONFLICT UPDATE callers may require that not all WITH
+        * CHECK OPTIONs (which may have originated a USING security barrier
+        * quals) associated with resultRelInfo are enforced at all stages of
+        * query processing
+        */
+       if (wco->commandType != CMD_SELECT)
+       {
+           if (wco->commandType != CMD_INSERT && onlyInsert)
+               continue;
+           if (wco->commandType != CMD_UPDATE &&
+               (securityBarrierUpdateQuals || WCOUpdate))
+               continue;
+           if (securityBarrierUpdateQuals && !wco->secBarrier)
+               continue;
+           if (WCOUpdate && wco->secBarrier)
+               continue;
+       }
+
        /*
         * WITH CHECK OPTION checks are intended to ensure that the new tuple
         * is visible (in the case of a view) or that it passes the
@@ -1740,6 +1761,7 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
        {
            char       *val_desc;
            Bitmapset  *modifiedCols;
+           char       *command = NULL;
 
            modifiedCols = GetUpdatedColumns(resultRelInfo, estate);
            modifiedCols = bms_union(modifiedCols, GetInsertedColumns(resultRelInfo, estate));
@@ -1749,9 +1771,20 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
                                                     modifiedCols,
                                                     64);
 
+           if (wco->commandType == CMD_INSERT)
+               command = "INSERT-applicable ";
+           else if (wco->commandType == CMD_UPDATE)
+               command = "UPDATE-applicable ";
+           else if (wco->commandType == CMD_DELETE)
+               command = "DELETE-applicable ";
+           else if (wco->commandType == CMD_SELECT)
+               command = "SELECT-applicable ";
+
            ereport(ERROR,
                    (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
-                errmsg("new row violates WITH CHECK OPTION for \"%s\"",
+                errmsg("new row violates %sWITH CHECK OPTION %sfor \"%s\"",
+                       command ? command : "",
+                       wco->secBarrier ? "(originally security barrier) ":"",
                        wco->viewname),
                    val_desc ? errdetail("Failing row contains %s.", val_desc) :
                               0));
index 08872e60fdf8e4eefb57e619392f8f45d6e08cf8..726892587fdb8ff02596b75be169259a8e31a63c 100644 (file)
@@ -452,7 +452,8 @@ vlock:
 
    /* Check any WITH CHECK OPTION constraints */
    if (resultRelInfo->ri_WithCheckOptions != NIL)
-       ExecWithCheckOptions(resultRelInfo, slot, estate);
+       ExecWithCheckOptions(resultRelInfo, slot, spec == SPEC_INSERT, false,
+                            false, estate);
 
    /* Process RETURNING if present */
    if (resultRelInfo->ri_projectReturning)
@@ -946,7 +947,7 @@ lreplace:;
 
    /* Check any WITH CHECK OPTION constraints */
    if (resultRelInfo->ri_WithCheckOptions != NIL)
-       ExecWithCheckOptions(resultRelInfo, slot, estate);
+       ExecWithCheckOptions(resultRelInfo, slot, false, false, true, estate);
 
    /* Process RETURNING if present */
    if (resultRelInfo->ri_projectReturning)
@@ -1143,6 +1144,58 @@ ExecLockUpdateTuple(ResultRelInfo *resultRelInfo,
             */
            slot = EvalPlanQualNext(&onConflict->mt_epqstate);
 
+           /*
+            * For RLS with ON CONFLICT UPDATE, security quals are always
+            * treated as WITH CHECK options, even when there were separate
+            * security quals and explicit WITH CHECK options (ordinarily,
+            * security quals are only treated as WITH CHECK options when there
+            * are no explicit WITH CHECK options).  Also, CHECK OPTIONs
+            * (originating either explicitly, or implicitly as security quals)
+            * are checked (as CHECK OPTIONs) at three different points for
+            * three distinct but related tuples/slots in the context of ON
+            * CONFLICT UPDATE exact policies (or "parts" of policies -- USING
+            * security barrier quals, or WCOs) enforced vary.  There are three
+            * relevant ExecWithCheckOptions() calls:
+            *
+            * * After successful insertion, within ExecInsert(), against the
+            * inserted tuple.  This only includes INSERT-applicable policies.
+            *
+            * * Here, after row locking but before calling ExecUpdate(), on
+            * the existing tuple in the target relation (which we cannot leak
+            * details of).  This is conceptually like a security barrier qual
+            * for the purposes of the auxiliary update, although unlike
+            * regular updates that require security barrier quals we prefer to
+            * raise an error (by treating the security barrier quals as CHECK
+            * OPTIONS) rather than silently not affect rows, because the
+            * intent to update seems clear and unambiguous for ON CONFLICT
+            * UPDATE.  This includes only UPDATE-applicable WCOs that
+            * originated as USING security barrier quals (not WCOs themselves,
+            * which may be tracked separately, despite relating to the same
+            * original policy).
+            *
+            * * On the final tuple created by the update within ExecUpdate()
+            * (if any).  This is not subject to INSERT policy enforcement.  It
+            * includes only WCOs that actually originated as WCOs (not
+            * security barrier USING quals enforced as WCOs, which are only
+            * used with the pre-update tuple).
+            */
+           if (resultRelInfo->ri_WithCheckOptions != NIL)
+           {
+               TupleTableSlot *opts;
+
+               /* Construct temp slot for locked tuple from target */
+               opts = MakeSingleTupleTableSlot(slot->tts_tupleDescriptor);
+               ExecStoreTuple(copyTuple, opts, InvalidBuffer, false);
+
+               /*
+                * Check existing/TARGET.* tuple against UPDATE-applicable
+                * USING security barrier quals (if any), enforced here as WITH
+                * CHECK OPTIONs.
+                */
+               ExecWithCheckOptions(resultRelInfo, opts, false, true, false,
+                                    estate);
+           }
+
            if (!TupIsNull(slot))
                *returning = ExecUpdate(&tuple.t_data->t_tidstate.t_ctid, NULL,
                                        slot, planSlot,
index 4b8014bba9e6f26e1b8935bb2569381100b4f054..9f889e24e90e5789cbbc9c8850af6d2e362ddb2c 100644 (file)
@@ -2099,6 +2099,8 @@ _copyWithCheckOption(const WithCheckOption *from)
 
    COPY_STRING_FIELD(viewname);
    COPY_NODE_FIELD(qual);
+   COPY_SCALAR_FIELD(commandType);
+   COPY_SCALAR_FIELD(secBarrier);
    COPY_SCALAR_FIELD(cascaded);
 
    return newnode;
index 785a0b83b47bb2acd42bfb78d4427af9ce946dd4..4afd5001cba1c88ce78b404365ff5e840fcea768 100644 (file)
@@ -2390,6 +2390,8 @@ _equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
 {
    COMPARE_STRING_FIELD(viewname);
    COMPARE_NODE_FIELD(qual);
+   COMPARE_SCALAR_FIELD(commandType);
+   COMPARE_SCALAR_FIELD(secBarrier);
    COMPARE_SCALAR_FIELD(cascaded);
 
    return true;
index 54775d230fd93f9729d6146260da26c831cbd26d..3cc26dc3d2d4d1fab51781eb49541a3b5bf61f15 100644 (file)
@@ -2360,6 +2360,8 @@ _outWithCheckOption(StringInfo str, const WithCheckOption *node)
 
    WRITE_STRING_FIELD(viewname);
    WRITE_NODE_FIELD(qual);
+   WRITE_ENUM_FIELD(commandType, CmdType);
+   WRITE_BOOL_FIELD(secBarrier);
    WRITE_BOOL_FIELD(cascaded);
 }
 
index 0ceef8c71299cf7cab310417280f8034f7010f08..a9f27b5b8fa655273074fb71200513fceaf4197f 100644 (file)
@@ -272,6 +272,8 @@ _readWithCheckOption(void)
 
    READ_STRING_FIELD(viewname);
    READ_NODE_FIELD(qual);
+   READ_ENUM_FIELD(commandType, CmdType);
+   READ_BOOL_FIELD(secBarrier);
    READ_BOOL_FIELD(cascaded);
 
    READ_DONE();
index 32c096a49d2e4a6750d3ec0c86ac3d0a52aab8cd..afc13d74fec4d57292a87c54e5e87239d85831ca 100644 (file)
@@ -1767,6 +1767,8 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown)
                List               *quals = NIL;
 
                wco = (WithCheckOption *) makeNode(WithCheckOption);
+               wco->commandType = parsetree->commandType;
+               wco->secBarrier = true;
                quals = lcons(wco->qual, quals);
 
                activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
@@ -2934,6 +2936,8 @@ rewriteTargetView(Query *parsetree, Relation view)
            wco = makeNode(WithCheckOption);
            wco->viewname = pstrdup(RelationGetRelationName(view));
            wco->qual = NULL;
+           wco->commandType = viewquery->commandType;
+           wco->secBarrier = false;
            wco->cascaded = cascaded;
 
            parsetree->withCheckOptions = lcons(wco,
index 7669130e2b67592a5c270432c919f349ed1172ef..1457e9ef8a5ba2407c96d2c958ba7069012e77e5 100644 (file)
 #include "utils/syscache.h"
 #include "tcop/utility.h"
 
-static List *pull_row_security_policies(CmdType cmd, Relation relation,
-                                       Oid user_id);
+static List *pull_row_security_policies(CmdType cmd, bool onConflict,
+                                       Relation relation, Oid user_id);
 static void process_policies(List *policies, int rt_index,
+                            bool onConflict,
                             Expr **final_qual,
                             Expr **final_with_check_qual,
+                            Expr **on_conflict_using_check_eval,
+                            Expr **on_conflict_with_check_eval,
                             bool *hassublinks);
 static bool check_role_for_policy(ArrayType *policy_roles, Oid user_id);
 
@@ -89,6 +92,14 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
    Expr               *hook_expr = NULL;
    Expr               *hook_with_check_expr = NULL;
 
+   /*
+    * ON CONFLICT UPDATE enforces both UPDATE-applicable USING security
+    * barrier quals and UPDATE-applicable WCOs as WCOs (although at different
+    * points, for different tuples)
+    */
+   Expr               *on_conflict_using_expr = NULL;
+   Expr               *on_conflict_with_check_expr = NULL;
+
    List               *rowsec_policies;
    List               *hook_policies = NIL;
 
@@ -149,8 +160,9 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
    /* Grab the built-in policies which should be applied to this relation. */
    rel = heap_open(rte->relid, NoLock);
 
-   rowsec_policies = pull_row_security_policies(root->commandType, rel,
-                                                user_id);
+   rowsec_policies = pull_row_security_policies(root->commandType,
+                                                root->specClause == SPEC_INSERT,
+                                                rel, user_id);
 
    /*
     * Check if this is only the default-deny policy.
@@ -167,8 +179,10 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
        defaultDeny = true;
 
    /* Now that we have our policies, build the expressions from them. */
-   process_policies(rowsec_policies, rt_index, &rowsec_expr,
-                    &rowsec_with_check_expr, &hassublinks);
+   process_policies(rowsec_policies, rt_index,
+                    root->specClause == SPEC_INSERT, &rowsec_expr,
+                    &rowsec_with_check_expr, &on_conflict_using_expr,
+                    &on_conflict_with_check_expr, &hassublinks);
 
    /*
     * Also, allow extensions to add their own policies.
@@ -197,8 +211,10 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
        hook_policies = (*row_security_policy_hook)(root->commandType, rel);
 
        /* Build the expression from any policies returned. */
-       process_policies(hook_policies, rt_index, &hook_expr,
-                        &hook_with_check_expr, &hassublinks);
+       process_policies(hook_policies, rt_index,
+                        root->specClause == SPEC_INSERT, &hook_expr,
+                        &hook_with_check_expr, &on_conflict_using_expr,
+                        &on_conflict_with_check_expr, &hassublinks);
    }
 
    /*
@@ -229,6 +245,8 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
            wco = (WithCheckOption *) makeNode(WithCheckOption);
            wco->viewname = RelationGetRelationName(rel);
            wco->qual = (Node *) rowsec_with_check_expr;
+           wco->commandType = root->commandType;
+           wco->secBarrier = false;
            wco->cascaded = false;
            root->withCheckOptions = lcons(wco, root->withCheckOptions);
        }
@@ -243,6 +261,38 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
            wco = (WithCheckOption *) makeNode(WithCheckOption);
            wco->viewname = RelationGetRelationName(rel);
            wco->qual = (Node *) hook_with_check_expr;
+           wco->commandType = root->commandType;
+           wco->secBarrier = false;
+           wco->cascaded = false;
+           root->withCheckOptions = lcons(wco, root->withCheckOptions);
+       }
+
+       if (on_conflict_using_expr)
+       {
+           WithCheckOption    *wco;
+
+           wco = (WithCheckOption *) makeNode(WithCheckOption);
+           wco->viewname = RelationGetRelationName(rel);
+           wco->qual = (Node *) on_conflict_using_expr;
+           wco->commandType = CMD_UPDATE;
+           wco->secBarrier = true;
+           wco->cascaded = false;
+           root->withCheckOptions = lcons(wco, root->withCheckOptions);
+       }
+
+       /*
+        * Also add the expression, if any, returned from the extension that
+        * applies to auxiliary UPDATE within ON CONFLICT UPDATE.
+        */
+       if (on_conflict_with_check_expr)
+       {
+           WithCheckOption    *wco;
+
+           wco = (WithCheckOption *) makeNode(WithCheckOption);
+           wco->viewname = RelationGetRelationName(rel);
+           wco->qual = (Node *) on_conflict_with_check_expr;
+           wco->commandType = CMD_UPDATE;
+           wco->secBarrier = false;
            wco->cascaded = false;
            root->withCheckOptions = lcons(wco, root->withCheckOptions);
        }
@@ -288,7 +338,8 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
  *
  */
 static List *
-pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id)
+pull_row_security_policies(CmdType cmd, bool onConflict, Relation relation,
+                          Oid user_id)
 {
    List               *policies = NIL;
    ListCell           *item;
@@ -322,7 +373,9 @@ pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id)
                if (policy->polcmd == ACL_INSERT_CHR
                    && check_role_for_policy(policy->roles, user_id))
                    policies = lcons(policy, policies);
-               break;
+               if (!onConflict)
+                   break;
+               /* FALL THRU */
            case CMD_UPDATE:
                if (policy->polcmd == ACL_UPDATE_CHR
                    && check_role_for_policy(policy->roles, user_id))
@@ -380,15 +433,25 @@ pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id)
  * rewrite them as necessary, and produce an Expr for the normal security
  * quals and an Expr for the with check quals.
  *
- * qual_eval, with_check_eval, and hassublinks are output variables
+ * onConflict callers have us break out auxiliary UPDATE-applicable WCOs, that
+ * originate as either USING security barrier quals, or actual WCOs (this
+ * distinction is preserved later, for diagnostic purposes).  This allows
+ * enforcement at different points of ON CONFLICT UPDATE, for distinct tuples.
+ *
+ * qual_eval, with_check_eval, on_conflict_using_check_eval,
+ * on_conflict_with_check_eval, and hassublinks are output variables
  */
 static void
-process_policies(List *policies, int rt_index, Expr **qual_eval,
-                Expr **with_check_eval, bool *hassublinks)
+process_policies(List *policies, int rt_index, bool onConflict,
+                Expr **qual_eval, Expr **with_check_eval,
+                Expr **on_conflict_using_check_eval,
+                Expr **on_conflict_with_check_eval, bool *hassublinks)
 {
    ListCell           *item;
    List               *quals = NIL;
    List               *with_check_quals = NIL;
+   List               *conflict_quals = NIL;
+   List               *conflict_with_check_quals = NIL;
 
    /*
     * Extract the USING and WITH CHECK quals from each of the policies
@@ -399,11 +462,33 @@ process_policies(List *policies, int rt_index, Expr **qual_eval,
        RowSecurityPolicy *policy = (RowSecurityPolicy *) lfirst(item);
 
        if (policy->qual != NULL)
-           quals = lcons(copyObject(policy->qual), quals);
+       {
+           if (!onConflict || policy->polcmd != ACL_UPDATE_CHR)
+               quals = lcons(copyObject(policy->qual), quals);
+           else
+               conflict_quals = lcons(copyObject(policy->qual), conflict_quals);
+
+           /* Don't neglect to also prepend ALL quals to auxiliary UPDATE */
+           if (onConflict && policy->polcmd == '*')
+               conflict_quals = lcons(copyObject(policy->qual), conflict_quals);
+       }
 
        if (policy->with_check_qual != NULL)
-           with_check_quals = lcons(copyObject(policy->with_check_qual),
-                                    with_check_quals);
+       {
+           if (!onConflict || policy->polcmd != ACL_UPDATE_CHR)
+               with_check_quals = lcons(copyObject(policy->with_check_qual),
+                                        with_check_quals);
+           else
+               conflict_with_check_quals =
+                   lcons(copyObject(policy->with_check_qual),
+                         conflict_with_check_quals);
+
+           /* Don't neglect to also prepend ALL quals to auxiliary UPDATE */
+           if (onConflict && policy->polcmd == '*')
+               conflict_with_check_quals =
+                   lcons(copyObject(policy->with_check_qual),
+                         conflict_with_check_quals);
+       }
 
        if (policy->hassublinks)
            *hassublinks = true;
@@ -424,6 +509,15 @@ process_policies(List *policies, int rt_index, Expr **qual_eval,
    if (with_check_quals == NIL)
        with_check_quals = copyObject(quals);
 
+   /*
+    * Do the same for ON CONFLICT UPDATE tracked quals.  These are not
+    * reported as originating as security barrier quals -- that only occurs
+    * with the special enforcement of security barrier quals as WCOs that ON
+    * CONFLICT UPDATE performs in respect of an existing/target tuple.
+    */
+   if (onConflict && conflict_with_check_quals == NIL)
+       conflict_with_check_quals = copyObject(conflict_quals);
+
    /*
     * Row security quals always have the target table as varno 1, as no
     * joins are permitted in row security expressions. We must walk the
@@ -453,6 +547,32 @@ process_policies(List *policies, int rt_index, Expr **qual_eval,
    else
        *with_check_eval = (Expr*) linitial(with_check_quals);
 
+   /*
+    * Ditto for auxiliary UPDATE USING security barrier quals.
+    */
+   if (conflict_quals != NIL)
+   {
+       if (list_length(conflict_quals) > 1)
+           *on_conflict_using_check_eval =
+               makeBoolExpr(OR_EXPR, conflict_quals, -1);
+       else
+           *on_conflict_using_check_eval = (Expr*) linitial(conflict_quals);
+   }
+
+   /*
+    * Ditto for auxiliary UPDATE WCOs.  Note that the same policy could appear
+    * in both lists (although often not the same actual quals).
+    */
+   if (conflict_with_check_quals != NIL)
+   {
+       if (list_length(conflict_with_check_quals) > 1)
+           *on_conflict_with_check_eval =
+               makeBoolExpr(OR_EXPR, conflict_with_check_quals, -1);
+       else
+           *on_conflict_with_check_eval =
+               (Expr*) linitial(conflict_with_check_quals);
+   }
+
    return;
 }
 
index 9229aa3b5dc99ce3044a9154265ba58c28e52a65..e3afea1a77b7eb77a91eb23b8e01acf5af61db66 100644 (file)
@@ -194,7 +194,9 @@ extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
 extern void ExecConstraints(ResultRelInfo *resultRelInfo,
                TupleTableSlot *slot, EState *estate);
 extern void ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
-                    TupleTableSlot *slot, EState *estate);
+                    TupleTableSlot *slot, bool onlyInsert,
+                    bool securityBarrierUpdateQuals, bool WCOUpdate,
+                    EState *estate);
 extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
 extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
 extern TupleTableSlot *EvalPlanQual(EState *estate, EPQState *epqstate,
index 616efff7dc9ea225cb7bc562a22ba8b86fd2e7cd..32d361f41b92a7a595f12aa55e7f1958bd6a0b58 100644 (file)
@@ -886,6 +886,8 @@ typedef struct WithCheckOption
    NodeTag     type;
    char       *viewname;       /* name of view that specified the WCO */
    Node       *qual;           /* constraint qual to check */
+   CmdType     commandType;    /* select|insert|update|delete originated? */
+   bool        secBarrier;     /* originated as security barrier qual? */
    bool        cascaded;       /* true = WITH CASCADED CHECK OPTION */
 } WithCheckOption;
 
index 44e8dab44c27f9ff130be8d5f7d89898b79012b4..20f91efe6c74375e2c17eb4f428006e70a94d6e3 100644 (file)
@@ -1192,6 +1192,135 @@ NOTICE:  f_leak => yyyyyy
  302 | 2 | yyyyyy | (2,yyyyyy)
 (3 rows)
 
+--
+-- INSERT ... ON CONFLICT UPDATE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+DROP POLICY p1 ON document;
+CREATE POLICY p1 ON document FOR SELECT USING (true);
+CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user);
+CREATE POLICY p3 ON document FOR UPDATE
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+  WITH CHECK (dauthor = current_user);
+SET SESSION AUTHORIZATION rls_regress_user1;
+-- Exists...
+SELECT * FROM document WHERE did = 2;
+ did | cid | dlevel |      dauthor      |     dtitle      
+-----+-----+--------+-------------------+-----------------
+   2 |  11 |      2 | rls_regress_user1 | my second novel
+(1 row)
+
+-- ...so violates actual WITH CHECK OPTION within UPDATE (not INSERT, since
+-- alternative UPDATE path happens to be taken).  This is a WCO violation, so
+-- violating (would-be appended) tuple may be reported as a detail:
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, dauthor = EXCLUDED.dauthor;
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION for "document"
+-- Essentially the same, but since INSERT path is now taken, this should be
+-- reported as violating INSERT policy:
+INSERT INTO document VALUES (101, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, dauthor = EXCLUDED.dauthor;
+ERROR:  new row violates INSERT-applicable WITH CHECK OPTION for "document"
+-- Violates USING qual for UPDATE policy p3 (ON CONFLICT enforces these
+-- somewhat like WCOs).
+--
+-- UPDATE path is taken, but UPDATE fails purely because *existing* row to be
+-- updated is not a "novel"/cid 11 (row is not leaked, even though we have
+-- SELECT privileges sufficient to see the row in this instance):
+INSERT INTO document VALUES (33, 22, 1, 'rls_regress_user1', 'okay science fiction'); -- preparation for next statement
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'Some novel, replaces sci-fi') -- takes UPDATE path
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle;
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION (originally security barrier) for "document"
+-- Fine (we UPDATE, since INSERT WCOs and UPDATE security barrier quals + WCOs
+-- not violated):
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+ did | cid | dlevel |      dauthor      |     dtitle     
+-----+-----+--------+-------------------+----------------
+   2 |  11 |      2 | rls_regress_user1 | my first novel
+(1 row)
+
+-- Fine (we INSERT, so "cid = 33" ("technology") isn't evaluated):
+INSERT INTO document VALUES (78, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'some technology novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33 RETURNING *;
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+  78 |  11 |      1 | rls_regress_user1 | some technology novel
+(1 row)
+
+-- Works (same query, but we UPDATE, so "cid = 33", ("technology") is evaluated
+-- at end of UPDATE):
+INSERT INTO document VALUES (78, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'some technology novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33 RETURNING *;
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+  78 |  33 |      1 | rls_regress_user1 | some technology novel
+(1 row)
+
+-- Don't fail because INSERT doesn't satisfy WITH CHECK option that originated
+-- as a barrier/USING() qual from the UPDATE.  Note that the UPDATE path
+-- *isn't* taken, and so UPDATE-related policy does not apply:
+INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+ did | cid | dlevel |      dauthor      |              dtitle              
+-----+-----+--------+-------------------+----------------------------------
+  79 |  33 |      1 | rls_regress_user1 | technology book, can only insert
+(1 row)
+
+-- But this time, the same statement fails, because the UPDATE path is taken,
+-- and updating the row just inserted falls afoul of security barrier qual
+-- (enforced as WCO) -- what we might have updated target tuple to is
+-- irrelevant, in fact.
+INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION (originally security barrier) for "document"
+-- Test default USING qual enforced as WCO
+SET SESSION AUTHORIZATION rls_regress_user0;
+DROP POLICY p1 ON document; -- irrelevant now
+DROP POLICY p3 ON document;
+CREATE POLICY p3_with_default ON document FOR UPDATE
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'));
+SET SESSION AUTHORIZATION rls_regress_user1;
+-- Just because WCO-style enforcement of USING quals occurs with
+-- existing/target tuple does not mean that the implementation can be allowed
+-- to fail to also enforce this qual against the final tuple appended to
+-- relation (since in the absence of an explicit WCO, this is also interpreted
+-- as an UPDATE/ALL WCO in general).
+--
+-- UPDATE path is taken here (fails due to existing tuple):
+INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION (originally security barrier) for "document"
+-- UPDATE path is taken here.  Existing tuple passes, since it's cid
+-- corresponds to "novel", but default USING qual is enforced against
+-- post-UPDATE tuple too (as always when updating with a policy that lacks an
+-- explicit WCO), and so this fails:
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET cid = EXCLUDED.cid, dtitle = EXCLUDED.dtitle RETURNING *;
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION for "document"
+SET SESSION AUTHORIZATION rls_regress_user0;
+DROP POLICY p3_with_default ON document;
+--
+-- Test ALL policies with ON CONFLICT UPDATE (much the same as existing UPDATE
+-- tests)
+--
+CREATE POLICY p3_with_all ON document FOR ALL
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+  WITH CHECK (dauthor = current_user);
+SET SESSION AUTHORIZATION rls_regress_user1;
+-- Fails, since ALL WCO is enforced in insert path:
+INSERT INTO document VALUES (80, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33;
+ERROR:  new row violates INSERT-applicable WITH CHECK OPTION for "document"
+-- Fails, since ALL policy USING qual is enforced (existing, target tuple is in
+-- violation, since it has the "manga" cid):
+INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle;
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION (originally security barrier) for "document"
+-- Fails, since ALL WCO are enforced:
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dauthor = 'rls_regress_user2';
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION for "document"
 --
 -- ROLE/GROUP
 --
@@ -1695,7 +1824,7 @@ EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FRO
 (6 rows)
 
 WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail
-ERROR:  new row violates WITH CHECK OPTION for "t1"
+ERROR:  new row violates UPDATE-applicable WITH CHECK OPTION for "t1"
 WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
  a  |                b                 
 ----+----------------------------------
@@ -1713,7 +1842,7 @@ WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
 (11 rows)
 
 WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail
-ERROR:  new row violates WITH CHECK OPTION for "t1"
+ERROR:  new row violates INSERT-applicable WITH CHECK OPTION for "t1"
 WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
  a  |    b    
 ----+---------
index 7f1831890aa4fc32e90b1436cda63f030aaa4d09..fa4eb2263f48ccdaf04decbee34071db37f35a7b 100644 (file)
@@ -1400,18 +1400,18 @@ SELECT * FROM information_schema.views WHERE table_name = 'rw_view1';
 
 INSERT INTO rw_view1 VALUES(3,4); -- ok
 INSERT INTO rw_view1 VALUES(4,3); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (4, 3).
 INSERT INTO rw_view1 VALUES(5,null); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (5, null).
 UPDATE rw_view1 SET b = 5 WHERE a = 3; -- ok
 UPDATE rw_view1 SET b = -5 WHERE a = 3; -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (3, -5).
 INSERT INTO rw_view1(a) VALUES (9); -- ok
 INSERT INTO rw_view1(a) VALUES (10); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (10, 10).
 SELECT * FROM base_tbl;
  a | b  
@@ -1450,11 +1450,11 @@ SELECT * FROM information_schema.views WHERE table_name = 'rw_view2';
 (1 row)
 
 INSERT INTO rw_view2 VALUES (-5); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (-5).
 INSERT INTO rw_view2 VALUES (5); -- ok
 INSERT INTO rw_view2 VALUES (15); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view2"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view2"
 DETAIL:  Failing row contains (15).
 SELECT * FROM base_tbl;
  a 
@@ -1463,10 +1463,10 @@ SELECT * FROM base_tbl;
 (1 row)
 
 UPDATE rw_view2 SET a = a - 10; -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (-5).
 UPDATE rw_view2 SET a = a + 10; -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view2"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view2"
 DETAIL:  Failing row contains (15).
 CREATE OR REPLACE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a < 10
   WITH LOCAL CHECK OPTION;
@@ -1491,7 +1491,7 @@ SELECT * FROM information_schema.views WHERE table_name = 'rw_view2';
 
 INSERT INTO rw_view2 VALUES (-10); -- ok, but not in view
 INSERT INTO rw_view2 VALUES (20); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view2"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view2"
 DETAIL:  Failing row contains (20).
 SELECT * FROM base_tbl;
   a  
@@ -1505,10 +1505,10 @@ ERROR:  invalid value for "check_option" option
 DETAIL:  Valid values are "local" and "cascaded".
 ALTER VIEW rw_view1 SET (check_option=local);
 INSERT INTO rw_view2 VALUES (-20); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (-20).
 INSERT INTO rw_view2 VALUES (30); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view2"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view2"
 DETAIL:  Failing row contains (30).
 ALTER VIEW rw_view2 RESET (check_option);
 \d+ rw_view2
@@ -1564,7 +1564,7 @@ INSERT INTO rw_view1 VALUES (1); -- ok
 INSERT INTO rw_view2 VALUES (-2); -- ok, but not in view
 INSERT INTO rw_view2 VALUES (2); -- ok
 INSERT INTO rw_view3 VALUES (-3); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view2"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view2"
 DETAIL:  Failing row contains (-3).
 INSERT INTO rw_view3 VALUES (3); -- ok
 DROP TABLE base_tbl CASCADE;
@@ -1578,16 +1578,16 @@ CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a = ANY (b)
   WITH CHECK OPTION;
 INSERT INTO rw_view1 VALUES (1, ARRAY[1,2,3]); -- ok
 INSERT INTO rw_view1 VALUES (10, ARRAY[4,5]); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (10, {4,5}).
 UPDATE rw_view1 SET b[2] = -b[2] WHERE a = 1; -- ok
 UPDATE rw_view1 SET b[1] = -b[1] WHERE a = 1; -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (1, {-1,-2,3}).
 PREPARE ins(int, int[]) AS INSERT INTO rw_view1 VALUES($1, $2);
 EXECUTE ins(2, ARRAY[1,2,3]); -- ok
 EXECUTE ins(10, ARRAY[4,5]); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (10, {4,5}).
 DEALLOCATE PREPARE ins;
 DROP TABLE base_tbl CASCADE;
@@ -1602,11 +1602,11 @@ CREATE VIEW rw_view1 AS
   WITH CHECK OPTION;
 INSERT INTO rw_view1 VALUES (5); -- ok
 INSERT INTO rw_view1 VALUES (15); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (15).
 UPDATE rw_view1 SET a = a + 5; -- ok
 UPDATE rw_view1 SET a = a + 5; -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (15).
 EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (5);
                           QUERY PLAN                           
@@ -1654,10 +1654,10 @@ CREATE TRIGGER base_tbl_trig BEFORE INSERT OR UPDATE ON base_tbl
 CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a < b WITH CHECK OPTION;
 INSERT INTO rw_view1 VALUES (5,0); -- ok
 INSERT INTO rw_view1 VALUES (15, 20); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (15, 10).
 UPDATE rw_view1 SET a = 20, b = 30; -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view1"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view1"
 DETAIL:  Failing row contains (20, 10).
 DROP TABLE base_tbl CASCADE;
 NOTICE:  drop cascades to view rw_view1
@@ -1688,12 +1688,12 @@ CREATE TRIGGER rw_view1_trig
 CREATE VIEW rw_view2 AS
   SELECT * FROM rw_view1 WHERE a > 0 WITH LOCAL CHECK OPTION;
 INSERT INTO rw_view2 VALUES (-5); -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view2"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view2"
 DETAIL:  Failing row contains (-5).
 INSERT INTO rw_view2 VALUES (5); -- ok
 INSERT INTO rw_view2 VALUES (50); -- ok, but not in view
 UPDATE rw_view2 SET a = a - 10; -- should fail
-ERROR:  new row violates WITH CHECK OPTION for "rw_view2"
+ERROR:  new row violates SELECT-applicable WITH CHECK OPTION for "rw_view2"
 DETAIL:  Failing row contains (-5).
 SELECT * FROM base_tbl;
  a  | b  
index ed7adbf8d1325e35236058c373d1bdeb61ad55bd..dcbcc22bca259f5c251597eace405af24c78b074 100644 (file)
@@ -435,6 +435,116 @@ EXPLAIN (COSTS OFF) DELETE FROM t1 WHERE f_leak(b);
 DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
 DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
 
+--
+-- INSERT ... ON CONFLICT UPDATE and Row-level security
+--
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+DROP POLICY p1 ON document;
+
+CREATE POLICY p1 ON document FOR SELECT USING (true);
+CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user);
+CREATE POLICY p3 ON document FOR UPDATE
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+  WITH CHECK (dauthor = current_user);
+
+SET SESSION AUTHORIZATION rls_regress_user1;
+
+-- Exists...
+SELECT * FROM document WHERE did = 2;
+
+-- ...so violates actual WITH CHECK OPTION within UPDATE (not INSERT, since
+-- alternative UPDATE path happens to be taken).  This is a WCO violation, so
+-- violating (would-be appended) tuple may be reported as a detail:
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, dauthor = EXCLUDED.dauthor;
+-- Essentially the same, but since INSERT path is now taken, this should be
+-- reported as violating INSERT policy:
+INSERT INTO document VALUES (101, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, dauthor = EXCLUDED.dauthor;
+
+-- Violates USING qual for UPDATE policy p3 (ON CONFLICT enforces these
+-- somewhat like WCOs).
+--
+-- UPDATE path is taken, but UPDATE fails purely because *existing* row to be
+-- updated is not a "novel"/cid 11 (row is not leaked, even though we have
+-- SELECT privileges sufficient to see the row in this instance):
+INSERT INTO document VALUES (33, 22, 1, 'rls_regress_user1', 'okay science fiction'); -- preparation for next statement
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'Some novel, replaces sci-fi') -- takes UPDATE path
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle;
+-- Fine (we UPDATE, since INSERT WCOs and UPDATE security barrier quals + WCOs
+-- not violated):
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+-- Fine (we INSERT, so "cid = 33" ("technology") isn't evaluated):
+INSERT INTO document VALUES (78, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'some technology novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33 RETURNING *;
+-- Works (same query, but we UPDATE, so "cid = 33", ("technology") is evaluated
+-- at end of UPDATE):
+INSERT INTO document VALUES (78, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'some technology novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33 RETURNING *;
+-- Don't fail because INSERT doesn't satisfy WITH CHECK option that originated
+-- as a barrier/USING() qual from the UPDATE.  Note that the UPDATE path
+-- *isn't* taken, and so UPDATE-related policy does not apply:
+INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+-- But this time, the same statement fails, because the UPDATE path is taken,
+-- and updating the row just inserted falls afoul of security barrier qual
+-- (enforced as WCO) -- what we might have updated target tuple to is
+-- irrelevant, in fact.
+INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+
+-- Test default USING qual enforced as WCO
+SET SESSION AUTHORIZATION rls_regress_user0;
+DROP POLICY p1 ON document; -- irrelevant now
+DROP POLICY p3 ON document;
+
+CREATE POLICY p3_with_default ON document FOR UPDATE
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'));
+
+SET SESSION AUTHORIZATION rls_regress_user1;
+-- Just because WCO-style enforcement of USING quals occurs with
+-- existing/target tuple does not mean that the implementation can be allowed
+-- to fail to also enforce this qual against the final tuple appended to
+-- relation (since in the absence of an explicit WCO, this is also interpreted
+-- as an UPDATE/ALL WCO in general).
+--
+-- UPDATE path is taken here (fails due to existing tuple):
+INSERT INTO document VALUES (79, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'technology book, can only insert')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle RETURNING *;
+
+-- UPDATE path is taken here.  Existing tuple passes, since it's cid
+-- corresponds to "novel", but default USING qual is enforced against
+-- post-UPDATE tuple too (as always when updating with a policy that lacks an
+-- explicit WCO), and so this fails:
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'technology'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET cid = EXCLUDED.cid, dtitle = EXCLUDED.dtitle RETURNING *;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+DROP POLICY p3_with_default ON document;
+
+--
+-- Test ALL policies with ON CONFLICT UPDATE (much the same as existing UPDATE
+-- tests)
+--
+CREATE POLICY p3_with_all ON document FOR ALL
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+  WITH CHECK (dauthor = current_user);
+
+SET SESSION AUTHORIZATION rls_regress_user1;
+
+-- Fails, since ALL WCO is enforced in insert path:
+INSERT INTO document VALUES (80, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user2', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle, cid = 33;
+-- Fails, since ALL policy USING qual is enforced (existing, target tuple is in
+-- violation, since it has the "manga" cid):
+INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dtitle = EXCLUDED.dtitle;
+-- Fails, since ALL WCO are enforced:
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'rls_regress_user1', 'my first novel')
+    ON CONFLICT (did) UPDATE SET dauthor = 'rls_regress_user2';
+
 --
 -- ROLE/GROUP
 --