</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>
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>
<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>
*/
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);
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
{
char *val_desc;
Bitmapset *modifiedCols;
+ char *command = NULL;
modifiedCols = GetUpdatedColumns(resultRelInfo, estate);
modifiedCols = bms_union(modifiedCols, GetInsertedColumns(resultRelInfo, estate));
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));
/* 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)
/* 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)
*/
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,
COPY_STRING_FIELD(viewname);
COPY_NODE_FIELD(qual);
+ COPY_SCALAR_FIELD(commandType);
+ COPY_SCALAR_FIELD(secBarrier);
COPY_SCALAR_FIELD(cascaded);
return newnode;
{
COMPARE_STRING_FIELD(viewname);
COMPARE_NODE_FIELD(qual);
+ COMPARE_SCALAR_FIELD(commandType);
+ COMPARE_SCALAR_FIELD(secBarrier);
COMPARE_SCALAR_FIELD(cascaded);
return true;
WRITE_STRING_FIELD(viewname);
WRITE_NODE_FIELD(qual);
+ WRITE_ENUM_FIELD(commandType, CmdType);
+ WRITE_BOOL_FIELD(secBarrier);
WRITE_BOOL_FIELD(cascaded);
}
READ_STRING_FIELD(viewname);
READ_NODE_FIELD(qual);
+ READ_ENUM_FIELD(commandType, CmdType);
+ READ_BOOL_FIELD(secBarrier);
READ_BOOL_FIELD(cascaded);
READ_DONE();
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);
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,
#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);
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;
/* 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.
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.
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);
}
/*
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);
}
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);
}
*
*/
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;
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))
* 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
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;
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
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;
}
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,
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;
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
--
(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
----+----------------------------------
(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
----+---------
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
(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
(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;
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
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
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;
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;
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
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
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
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
--