From: Peter Geoghegan Date: Wed, 7 Jan 2015 00:32:21 +0000 (-0800) Subject: RLS support for ON CONFLICT UPDATE X-Git-Url: http://git.postgresql.org/gitweb/static/gitweb.js?a=commitdiff_plain;h=7ac7752fd8146cbd46460e00f62853ae2bdeab0c;p=users%2Fandresfreund%2Fpostgres.git RLS support for ON CONFLICT UPDATE 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. --- diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml index 868a6c1cd3..b1896d0fd8 100644 --- a/doc/src/sgml/ref/create_policy.sgml +++ b/doc/src/sgml/ref/create_policy.sgml @@ -67,14 +67,18 @@ CREATE POLICY name ON - 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 ON CONFLICT UPDATE and + INSERT policies are not combined in this way, but + rather enforced as noted at each stage of 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). @@ -255,6 +259,12 @@ CREATE POLICY name ON + + Note that INSERT with ON CONFLICT + UPDATE requires that any INSERT + policy WITH CHECK expression passes for any rows appended to + the relation by the INSERT path only. + @@ -262,23 +272,45 @@ CREATE POLICY name ON UPDATE - Using UPDATE for a policy means that it will apply - to UPDATE commands. As UPDATE - involves pulling an existing record and then making changes to some - portion (but possibly not all) of the record, the - UPDATE policy accepts both a USING expression and - a WITH CHECK expression. The USING expression will be used to - determine which records the UPDATE command will - see to operate against, while the WITH CHECK - expression defines what rows are allowed to be added back into the - relation (similar to the INSERT policy). - Any rows whose resulting values do not pass the - WITH CHECK expression will cause an ERROR and the - entire command will be aborted. Note that if only a - USING clause is specified then that clause will be - used for both USING and + Using UPDATE for a policy means that it + will apply to UPDATE commands (or + auxiliary ON CONFLICT UPDATE clauses of + INSERT commands). As + UPDATE involves pulling an existing record + and then making changes to some portion (but possibly not + all) of the record, the UPDATE policy + accepts both a USING expression and a + WITH CHECK expression. The + USING expression will be used to determine + which records the UPDATE command will see + to operate against, while the WITH CHECK + expression defines what rows are allowed to be added back + into the relation (similar to the INSERT + policy). Any rows whose resulting values do not pass the + WITH CHECK expression will cause an ERROR + and the entire command will be aborted. Note that if only a + USING clause is specified then that clause + will be used for both USING and WITH CHECK cases. + + Note, however, that INSERT with + ON CONFLICT UPDATE requires that an + UPDATE policy USING + expression always be encforced as a WITH + CHECK expression. This UPDATE + policy must always pass when the UPDATE + path is taken. Any existing row that necessitates that the + UPDATE path be taken must pass the (UPDATE + or ALL) USING qualifications (combined + using OR), which are always enforced as + WTIH CHECK options in this context (the + UPDATE path will never be + silently avoided; an error will be thrown instead). Finally, + the final row appended to the relation must pass any + WITH CHECK options that a conventional + UPDATE is required to pass. + diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index d1faf4df5a..bdf78ec0a3 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -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)); diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 08872e60fd..726892587f 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -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, diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 4b8014bba9..9f889e24e9 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -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; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 785a0b83b4..4afd5001cb 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -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; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 54775d230f..3cc26dc3d2 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -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); } diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 0ceef8c712..a9f27b5b8f 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -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(); diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 32c096a49d..afc13d74fe 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -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, diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c index 7669130e2b..1457e9ef8a 100644 --- a/src/backend/rewrite/rowsecurity.c +++ b/src/backend/rewrite/rowsecurity.c @@ -56,11 +56,14 @@ #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; } diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 9229aa3b5d..e3afea1a77 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -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, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 616efff7dc..32d361f41b 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -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; diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 44e8dab44c..20f91efe6c 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -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 ----+--------- diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 7f1831890a..fa4eb2263f 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -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 diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql index ed7adbf8d1..dcbcc22bca 100644 --- a/src/test/regress/sql/rowsecurity.sql +++ b/src/test/regress/sql/rowsecurity.sql @@ -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 --