expressions, as they are assumed to be trustworthy.
</para>
+ <para>
+ For INSERT and UPDATE queries, WITH CHECK expressions are enforced after
+ BEFORE triggers are fired, and before any data modifications are made.
+ Thus a BEFORE ROW trigger may modify the data to be inserted, affecting
+ the result of the security policy check. WITH CHECK expressions are
+ enforced before any other constraints.
+ </para>
+
<para>
Policy names are per-table, therefore one policy name can be used for many
different tables and have a definition for each table which is appropriate to
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
+ * of the specified kind.
+ *
+ * Note that this needs to be called multiple times to ensure that all kinds of
+ * WITH CHECK OPTIONs are handled (both those from views which have the WITH
+ * CHECK OPTION set and from row level security policies). See ExecInsert()
+ * and ExecUpdate().
*/
void
-ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
+ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
Relation rel = resultRelInfo->ri_RelationDesc;
WithCheckOption *wco = (WithCheckOption *) lfirst(l1);
ExprState *wcoExpr = (ExprState *) lfirst(l2);
+ /*
+ * Skip any WCOs which are not the kind we are looking for at this
+ * time.
+ */
+ if (wco->kind != kind)
+ 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;
- modifiedCols = GetModifiedColumns(resultRelInfo, estate);
- val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
- slot,
- tupdesc,
- modifiedCols,
- 64);
-
- ereport(ERROR,
- (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
- errmsg("new row violates WITH CHECK OPTION for \"%s\"",
- wco->viewname),
- val_desc ? errdetail("Failing row contains %s.", val_desc) :
- 0));
+ switch (wco->kind)
+ {
+ /*
+ * For WITH CHECK OPTIONs coming from views, we might be able to
+ * provide the details on the row, depending on the permissions
+ * on the relation (that is, if the user could view it directly
+ * anyway). For RLS violations, we don't include the data since
+ * we don't know if the user should be able to view the tuple as
+ * as that depends on the USING policy.
+ */
+ case WCO_VIEW_CHECK:
+ modifiedCols = GetModifiedColumns(resultRelInfo, estate);
+ val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+ slot,
+ tupdesc,
+ modifiedCols,
+ 64);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
+ errmsg("new row violates WITH CHECK OPTION for \"%s\"",
+ wco->relname),
+ val_desc ? errdetail("Failing row contains %s.",
+ val_desc) : 0));
+ break;
+ case WCO_RLS_INSERT_CHECK:
+ case WCO_RLS_UPDATE_CHECK:
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("new row violates row level security policy for \"%s\"",
+ wco->relname)));
+ break;
+ default:
+ elog(ERROR, "unrecognized WCO kind: %u", wco->kind);
+ break;
+ }
}
}
}
*/
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
+ /*
+ * Check any RLS INSERT WITH CHECK policies
+ *
+ * ExecWithCheckOptions() will skip any WCOs which are not of
+ * the kind we are looking for at this point.
+ */
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_INSERT_CHECK,
+ resultRelInfo, slot, estate);
+
/*
* Check the constraints of the tuple
*/
list_free(recheckIndexes);
- /* Check any WITH CHECK OPTION constraints */
+ /*
+ * Check any WITH CHECK OPTION constraints from parent views. We
+ * are required to do this after testing all constraints and
+ * uniqueness violations per the SQL spec, so we do it after actually
+ * inserting the record into the heap and all indexes.
+ *
+ * ExecWithCheckOptions will elog(ERROR) if a violation is found, so
+ * the tuple will never be seen, if it violates the the WITH CHECK
+ * OPTION.
+ *
+ * ExecWithCheckOptions() will skip any WCOs which are not of
+ * the kind we are looking for at this point.
+ */
if (resultRelInfo->ri_WithCheckOptions != NIL)
- ExecWithCheckOptions(resultRelInfo, slot, estate);
+ ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
- * Check the constraints of the tuple
+ * Check any RLS UPDATE WITH CHECK policies
*
* If we generate a new candidate tuple after EvalPlanQual testing, we
- * must loop back here and recheck constraints. (We don't need to
- * redo triggers, however. If there are any BEFORE triggers then
- * trigger.c will have done heap_lock_tuple to lock the correct tuple,
- * so there's no need to do them again.)
+ * must loop back here and recheck any RLS policies and constraints.
+ * (We don't need to redo triggers, however. If there are any BEFORE
+ * triggers then trigger.c will have done heap_lock_tuple to lock the
+ * correct tuple, so there's no need to do them again.)
+ *
+ * ExecWithCheckOptions() will skip any WCOs which are not of
+ * the kind we are looking for at this point.
*/
lreplace:;
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
+ * Check the constraints of the tuple
+ */
if (resultRelationDesc->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
list_free(recheckIndexes);
- /* Check any WITH CHECK OPTION constraints */
+ /*
+ * Check any WITH CHECK OPTION constraints from parent views. We
+ * are required to do this after testing all constraints and
+ * uniqueness violations per the SQL spec, so we do it after actually
+ * updating the record in the heap and all indexes.
+ *
+ * ExecWithCheckOptions() will skip any WCOs which are not of
+ * the kind we are looking for at this point.
+ */
if (resultRelInfo->ri_WithCheckOptions != NIL)
- ExecWithCheckOptions(resultRelInfo, slot, estate);
+ ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
{
WithCheckOption *newnode = makeNode(WithCheckOption);
- COPY_STRING_FIELD(viewname);
+ COPY_SCALAR_FIELD(kind);
+ COPY_STRING_FIELD(relname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
- COMPARE_STRING_FIELD(viewname);
+ COMPARE_SCALAR_FIELD(kind);
+ COMPARE_STRING_FIELD(relname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
- WRITE_STRING_FIELD(viewname);
+ WRITE_ENUM_FIELD(kind, WCOKind);
+ WRITE_STRING_FIELD(relname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
{
READ_LOCALS(WithCheckOption);
- READ_STRING_FIELD(viewname);
+ READ_ENUM_FIELD(kind, WCOKind);
+ READ_STRING_FIELD(relname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
- wco->viewname = pstrdup(RelationGetRelationName(view));
+ wco->kind = WCO_VIEW_CHECK;
+ wco->relname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
- wco->viewname = pstrdup(RelationGetRelationName(rel));
+ wco->kind = root->commandType == CMD_INSERT ? WCO_RLS_INSERT_CHECK :
+ WCO_RLS_UPDATE_CHECK;
+ wco->relname = pstrdup(RelationGetRelationName(rel));
wco->qual = (Node *) hook_with_check_expr_restrictive;
wco->cascaded = false;
*withCheckOptions = lappend(*withCheckOptions, wco);
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
- wco->viewname = pstrdup(RelationGetRelationName(rel));
+ wco->kind = root->commandType == CMD_INSERT ? WCO_RLS_INSERT_CHECK :
+ WCO_RLS_UPDATE_CHECK;
+ wco->relname = pstrdup(RelationGetRelationName(rel));
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
*withCheckOptions = lappend(*withCheckOptions, wco);
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
- wco->viewname = pstrdup(RelationGetRelationName(rel));
+ wco->kind = root->commandType == CMD_INSERT ? WCO_RLS_INSERT_CHECK :
+ WCO_RLS_UPDATE_CHECK;
+ wco->relname = pstrdup(RelationGetRelationName(rel));
wco->qual = (Node *) hook_with_check_expr_permissive;
wco->cascaded = false;
*withCheckOptions = lappend(*withCheckOptions, wco);
List *combined_quals = NIL;
Expr *combined_qual_eval;
- combined_quals = lcons(copyObject(rowsec_with_check_expr), combined_quals);
- combined_quals = lcons(copyObject(hook_with_check_expr_permissive), combined_quals);
+ combined_quals = lcons(copyObject(rowsec_with_check_expr),
+ combined_quals);
+
+ combined_quals = lcons(copyObject(hook_with_check_expr_permissive),
+ combined_quals);
combined_qual_eval = makeBoolExpr(OR_EXPR, combined_quals, -1);
wco = (WithCheckOption *) makeNode(WithCheckOption);
- wco->viewname = pstrdup(RelationGetRelationName(rel));
+ wco->kind = root->commandType == CMD_INSERT ? WCO_RLS_INSERT_CHECK :
+ WCO_RLS_UPDATE_CHECK;
+ wco->relname = pstrdup(RelationGetRelationName(rel));
wco->qual = (Node *) combined_qual_eval;
wco->cascaded = false;
*withCheckOptions = lappend(*withCheckOptions, wco);
Expr *combined_qual_eval;
combined_quals = lcons(copyObject(rowsec_expr), combined_quals);
- combined_quals = lcons(copyObject(hook_expr_permissive), combined_quals);
+ combined_quals = lcons(copyObject(hook_expr_permissive),
+ combined_quals);
combined_qual_eval = makeBoolExpr(OR_EXPR, combined_quals, -1);
extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
-extern void ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
+extern void ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
* TrigInstrument optional runtime measurements for triggers
* FdwRoutine FDW callback functions, if foreign table
* FdwState available to save private state of FDW
- * WithCheckOptions list of WithCheckOption's for views
+ * WithCheckOptions list of WithCheckOption's to be checked
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
- * when inserting/updating an auto-updatable view.
+ * when inserting/updating an auto-updatable view, or RLS WITH CHECK
+ * policies to be applied when inserting/updating a relation with RLS.
*/
+typedef enum WCOKind
+{
+ WCO_VIEW_CHECK, /* WCO on an auto-updatable view */
+ WCO_RLS_INSERT_CHECK, /* RLS INSERT WITH CHECK policy */
+ WCO_RLS_UPDATE_CHECK /* RLS UPDATE WITH CHECK policy */
+} WCOKind;
+
typedef struct WithCheckOption
{
NodeTag type;
- char *viewname; /* name of view that specified the WCO */
+ WCOKind kind; /* kind of WCO */
+ char *relname; /* name of relation that specified the WCO */
Node *qual; /* constraint qual to check */
- bool cascaded; /* true = WITH CASCADED CHECK OPTION */
+ bool cascaded; /* true for a cascaded WCO on a view */
} WithCheckOption;
/*
INSERT INTO rls_test_permissive VALUES ('r1','s1',10);
-- failure
INSERT INTO rls_test_permissive VALUES ('r4','s4',10);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_permissive"
+ERROR: new row violates row level security policy for "rls_test_permissive"
SET ROLE s1;
-- With only the hook's policies, restrictive
-- hook's policy is current_user = supervisor
INSERT INTO rls_test_restrictive VALUES ('r1','s1',10);
-- failure
INSERT INTO rls_test_restrictive VALUES ('r4','s4',10);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive"
+ERROR: new row violates row level security policy for "rls_test_restrictive"
SET ROLE s1;
-- With only the hook's policies, both
-- permissive hook's policy is current_user = username
-- failure
INSERT INTO rls_test_both VALUES ('r1','s1',10);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_both"
+ERROR: new row violates row level security policy for "rls_test_both"
-- failure
INSERT INTO rls_test_both VALUES ('r4','s1',10);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_both"
+ERROR: new row violates row level security policy for "rls_test_both"
-- failure
INSERT INTO rls_test_both VALUES ('r4','s4',10);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_both"
+ERROR: new row violates row level security policy for "rls_test_both"
RESET ROLE;
-- Create "internal" policies, to check that the policies from
-- the hooks are combined correctly.
INSERT INTO rls_test_permissive VALUES ('r3','s3',10);
-- failure
INSERT INTO rls_test_permissive VALUES ('r4','s4',7);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_permissive"
+ERROR: new row violates row level security policy for "rls_test_permissive"
SET ROLE s1;
-- With both internal and hook policies, restrictive
EXPLAIN (costs off) SELECT * FROM rls_test_restrictive;
INSERT INTO rls_test_restrictive VALUES ('r1','s1',8);
-- failure
INSERT INTO rls_test_restrictive VALUES ('r3','s3',10);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive"
+ERROR: new row violates row level security policy for "rls_test_restrictive"
-- failure
INSERT INTO rls_test_restrictive VALUES ('r1','s1',7);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive"
+ERROR: new row violates row level security policy for "rls_test_restrictive"
-- failure
INSERT INTO rls_test_restrictive VALUES ('r4','s4',7);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_restrictive"
+ERROR: new row violates row level security policy for "rls_test_restrictive"
-- With both internal and hook policies, both permissive
-- and restrictive hook policies
EXPLAIN (costs off) SELECT * FROM rls_test_both;
INSERT INTO rls_test_both VALUES ('r1','s1',8);
-- failure
INSERT INTO rls_test_both VALUES ('r3','s3',10);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_both"
+ERROR: new row violates row level security policy for "rls_test_both"
-- failure
INSERT INTO rls_test_both VALUES ('r1','s1',7);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_both"
+ERROR: new row violates row level security policy for "rls_test_both"
-- failure
INSERT INTO rls_test_both VALUES ('r4','s4',7);
-ERROR: new row violates WITH CHECK OPTION for "rls_test_both"
+ERROR: new row violates row level security policy for "rls_test_both"
RESET ROLE;
DROP TABLE rls_test_restrictive;
DROP TABLE rls_test_permissive;
-----+-----+--------+---------+--------
(0 rows)
+-- RLS policies are checked before constraints
+INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+ERROR: new row violates row level security policy for "document"
+UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+ERROR: new row violates row level security policy for "document"
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
(5 rows)
INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO
-ERROR: new row violates WITH CHECK OPTION for "b1"
+ERROR: new row violates row level security policy for "b1"
INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check
-ERROR: new row violates WITH CHECK OPTION for "b1"
+ERROR: new row violates row level security policy for "b1"
INSERT INTO bv1 VALUES (12, 'xxx'); -- ok
EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
QUERY PLAN
(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 row level security policy 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 row level security policy for "t1"
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
----+---------
INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user1', 'my third manga'); -- Must fail with unique violation, revealing presence of did we can't see
SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+-- RLS policies are checked before constraints
+INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;