On 10 January 2015 at 15:12, Stephen Frost <[email protected]> wrote:
> * Dean Rasheed ([email protected]) wrote:
>> Currently we're applying RLS CHECKs after the INSERT or UPDATE, like
>> WITH CHECK OPTIONs on views. The SQL spec says that WITH CHECK OPTIONs
>> on views have to be applied after the INSERT/UPDATE on the base
>> relation, but we're free to do something different for RLS CHECKs if
>> that makes more sense. If we want RLS to be more like column-level
>> privilege checking, then it does make sense to do the checks sooner,
>> so perhaps we should be checking the RLS policies before the
>> INSERT/UPDATE, like CHECK constraints.
>
> Were you thinking about working up a patch for such a change? If not,
> I'll see about finding time to do it, unless someone else wants to
> volunteer. :)
>
Attached is a patch to make RLS checks run before attempting to
insert/update any data rather than afterwards.
In the end I decided not to create a new structure for RLS checks
because most of the code that handles them treats them the same as
WCOs. Instead, I just added a new 'kind' enum field to the existing
structure and renamed/reworded things a bit.
The patch also changes the error message for a RLS check violation, to
make the cause of the error clearer. One thing I'm not sure about is
what sqlstate code to use for this error, but I don't think that using
WITH_CHECK_OPTION_VIOLATION is appropriate, because that seems to be
specifically intended for views.
Regards,
Dean
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
new file mode 100644
index 5b70cc9..6762b63
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
*************** ExecConstraints(ResultRelInfo *resultRel
*** 1638,1646 ****
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
*/
void
! ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
ExprContext *econtext;
--- 1638,1647 ----
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
+ * of the specified kind.
*/
void
! ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
ExprContext *econtext;
*************** ExecWithCheckOptions(ResultRelInfo *resu
*** 1663,1668 ****
--- 1664,1672 ----
WithCheckOption *wco = (WithCheckOption *) lfirst(l1);
ExprState *wcoExpr = (ExprState *) lfirst(l2);
+ 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
*************** ExecWithCheckOptions(ResultRelInfo *resu
*** 1673,1686 ****
* case (the opposite of what we do above for CHECK constraints).
*/
if (!ExecQual((List *) wcoExpr, econtext, false))
! ereport(ERROR,
! (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
! errmsg("new row violates WITH CHECK OPTION for \"%s\"",
! wco->viewname),
! errdetail("Failing row contains %s.",
! ExecBuildSlotValueDescription(slot,
! RelationGetDescr(resultRelInfo->ri_RelationDesc),
! 64))));
}
}
--- 1677,1711 ----
* case (the opposite of what we do above for CHECK constraints).
*/
if (!ExecQual((List *) wcoExpr, econtext, false))
! {
! switch (wco->kind)
! {
! case WCO_VIEW_CHECK:
! ereport(ERROR,
! (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
! errmsg("new row violates WITH CHECK OPTION for \"%s\"",
! wco->relname),
! errdetail("Failing row contains %s.",
! ExecBuildSlotValueDescription(slot,
! RelationGetDescr(resultRelInfo->ri_RelationDesc),
! 64))));
! 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),
! errdetail("Failing row contains %s.",
! ExecBuildSlotValueDescription(slot,
! RelationGetDescr(resultRelInfo->ri_RelationDesc),
! 64))));
! break;
! default:
! elog(ERROR, "unrecognized WCO kind: %u", wco->kind);
! break;
! }
! }
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index f96fb24..be879a4
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
*************** ExecInsert(TupleTableSlot *slot,
*** 253,258 ****
--- 253,265 ----
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
+ * Check any RLS INSERT WITH CHECK policies
+ */
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_INSERT_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
* Check the constraints of the tuple
*/
if (resultRelationDesc->rd_att->constr)
*************** ExecInsert(TupleTableSlot *slot,
*** 287,295 ****
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 294,302 ----
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
*************** ExecUpdate(ItemPointer tupleid,
*** 653,667 ****
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
! * Check the constraints of the tuple
*
* 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.)
*/
lreplace:;
if (resultRelationDesc->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
--- 660,681 ----
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
! * Check any RLS UPDATE WITH CHECK policies
*
* If we generate a new candidate tuple after EvalPlanQual testing, we
! * 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.)
*/
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);
*************** lreplace:;
*** 780,788 ****
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 794,802 ----
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index f1a24f5..777df58
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyWithCheckOption(const WithCheckOpti
*** 2055,2061 ****
{
WithCheckOption *newnode = makeNode(WithCheckOption);
! COPY_STRING_FIELD(viewname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
--- 2055,2062 ----
{
WithCheckOption *newnode = makeNode(WithCheckOption);
! COPY_SCALAR_FIELD(kind);
! COPY_STRING_FIELD(relname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 6e8b308..0b88c84
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblFunction(const RangeTblFun
*** 2368,2374 ****
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
! COMPARE_STRING_FIELD(viewname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
--- 2368,2375 ----
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
! COMPARE_SCALAR_FIELD(kind);
! COMPARE_STRING_FIELD(relname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index dd1278b..f514388
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outWithCheckOption(StringInfo str, cons
*** 2319,2325 ****
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
! WRITE_STRING_FIELD(viewname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
--- 2319,2326 ----
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
! WRITE_ENUM_FIELD(kind, WCOKind);
! WRITE_STRING_FIELD(relname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index ae24d05..86d1d1f
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readWithCheckOption(void)
*** 266,272 ****
{
READ_LOCALS(WithCheckOption);
! READ_STRING_FIELD(viewname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
--- 266,273 ----
{
READ_LOCALS(WithCheckOption);
! READ_ENUM_FIELD(kind, WCOKind);
! READ_STRING_FIELD(relname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index b8e6e7a..2ce7c23
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** rewriteTargetView(Query *parsetree, Rela
*** 2910,2916 ****
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
! wco->viewname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
--- 2910,2917 ----
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
! wco->kind = WCO_VIEW_CHECK;
! wco->relname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 35790a9..e55f8fe
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
*************** prepend_row_security_policies(Query* roo
*** 226,232 ****
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
--- 226,234 ----
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->kind = root->commandType == CMD_INSERT ?
! WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
! wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
*************** prepend_row_security_policies(Query* roo
*** 240,246 ****
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
--- 242,250 ----
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->kind = root->commandType == CMD_INSERT ?
! WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
! wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
new file mode 100644
index 40fde83..cb4f158
*** a/src/include/executor/executor.h
--- b/src/include/executor/executor.h
*************** extern ResultRelInfo *ExecGetTriggerResu
*** 194,200 ****
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);
extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
--- 194,200 ----
extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
! 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);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 41288ed..791343c
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
*************** typedef struct JunkFilter
*** 303,309 ****
* 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
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
--- 303,309 ----
* 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 to be checked
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index b1dfa85..3fadb7d
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblFunction
*** 854,867 ****
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
! * when inserting/updating an auto-updatable view.
*/
typedef struct WithCheckOption
{
NodeTag type;
! char *viewname; /* name of view that specified the WCO */
Node *qual; /* constraint qual to check */
! bool cascaded; /* true = WITH CASCADED CHECK OPTION */
} WithCheckOption;
/*
--- 854,876 ----
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
! * 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;
! WCOKind kind; /* kind of WCO */
! char *relname; /* name of relation that specified the WCO */
Node *qual; /* constraint qual to check */
! bool cascaded; /* true for a cascaded WCO on a view */
} WithCheckOption;
/*
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 430da55..e7777e6
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** SELECT * FROM document WHERE did = 8; --
*** 301,306 ****
--- 301,313 ----
-----+-----+--------+---------+--------
(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"
+ DETAIL: Failing row contains (8, 44, 1, rls_regress_user2, my third manga).
+ 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"
+ DETAIL: Failing row contains (8, 44, 2, rls_regress_user2, my second manga).
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
*************** EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT
*** 1682,1688 ****
(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"
DETAIL: Failing row contains (1, cfcd208495d565ef66e7dff9f98764da).
WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
a | b
--- 1689,1695 ----
(6 rows)
WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates row level security policy for "t1"
DETAIL: Failing row contains (1, cfcd208495d565ef66e7dff9f98764da).
WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
a | b
*************** WITH cte1 AS (UPDATE t1 SET a = a RETURN
*** 1701,1707 ****
(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"
DETAIL: Failing row contains (21, Fail).
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
--- 1708,1714 ----
(11 rows)
WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates row level security policy for "t1"
DETAIL: Failing row contains (21, Fail).
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ee28a2c..9652dda
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** SET SESSION AUTHORIZATION rls_regress_us
*** 146,151 ****
--- 146,155 ----
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;
--
Sent via pgsql-hackers mailing list ([email protected])
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers